From 999071c46dca0367d93f66ecd97b2e3507963284 Mon Sep 17 00:00:00 2001 From: shanmu Date: Thu, 8 Aug 2024 00:57:11 +0800 Subject: [PATCH 1/3] fix: Change `visible` to `hidden` --- clap_complete/src/dynamic/completer.rs | 49 +++++++++++--------------- 1 file changed, 21 insertions(+), 28 deletions(-) diff --git a/clap_complete/src/dynamic/completer.rs b/clap_complete/src/dynamic/completer.rs index 7a9015399a8..44837d758df 100644 --- a/clap_complete/src/dynamic/completer.rs +++ b/clap_complete/src/dynamic/completer.rs @@ -198,7 +198,7 @@ fn complete_arg( comp.get_content().to_string_lossy() )) .help(comp.get_help().cloned()) - .visible(comp.is_visible()) + .hide(comp.is_hide_set()) }), ); } @@ -241,7 +241,6 @@ fn complete_arg( comp.get_content().to_string_lossy() )) .help(comp.get_help().cloned()) - .visible(true) }), ); } else if let Some(short) = arg.to_short() { @@ -271,7 +270,7 @@ fn complete_arg( comp.get_content().to_string_lossy() )) .help(comp.get_help().cloned()) - .visible(comp.is_visible()) + .hide(comp.is_hide_set()) }), ); } else { @@ -283,7 +282,7 @@ fn complete_arg( comp.get_content().to_string_lossy() )) .help(comp.get_help().cloned()) - .visible(comp.is_visible()) + .hide(comp.is_hide_set()) }, )); } @@ -324,8 +323,8 @@ fn complete_arg( } } } - if completions.iter().any(|a| a.is_visible()) { - completions.retain(|a| a.is_visible()); + if completions.iter().any(|a| !a.is_hide_set()) { + completions.retain(|a| !a.is_hide_set()); } Ok(completions) @@ -346,7 +345,7 @@ fn complete_arg_value( name.starts_with(value).then(|| { CompletionCandidate::new(OsString::from(name)) .help(p.get_help().cloned()) - .visible(!p.is_hide_set()) + .hide(p.is_hide_set()) }) })); } @@ -428,20 +427,14 @@ fn complete_path( let path = entry.path(); let mut suggestion = pathdiff::diff_paths(&path, current_dir).unwrap_or(path); suggestion.push(""); // Ensure trailing `/` - completions.push( - CompletionCandidate::new(suggestion.as_os_str().to_owned()) - .help(None) - .visible(true), - ); + completions + .push(CompletionCandidate::new(suggestion.as_os_str().to_owned()).help(None)); } else { let path = entry.path(); if is_wanted(&path) { let suggestion = pathdiff::diff_paths(&path, current_dir).unwrap_or(path); - completions.push( - CompletionCandidate::new(suggestion.as_os_str().to_owned()) - .help(None) - .visible(true), - ); + completions + .push(CompletionCandidate::new(suggestion.as_os_str().to_owned()).help(None)); } } } @@ -476,7 +469,7 @@ fn longs_and_visible_aliases(p: &clap::Command) -> Vec { longs.into_iter().map(|s| { CompletionCandidate::new(format!("--{}", s)) .help(a.get_help().cloned()) - .visible(!a.is_hide_set()) + .hide(a.is_hide_set()) }) }) }) @@ -494,7 +487,7 @@ fn hidden_longs_aliases(p: &clap::Command) -> Vec { longs.into_iter().map(|s| { CompletionCandidate::new(format!("--{}", s)) .help(a.get_help().cloned()) - .visible(false) + .hide(true) }) }) }) @@ -513,7 +506,7 @@ fn shorts_and_visible_aliases(p: &clap::Command) -> Vec { shorts.into_iter().map(|s| { CompletionCandidate::new(s.to_string()) .help(a.get_help().cloned()) - .visible(!a.is_hide_set()) + .hide(a.is_hide_set()) }) }) }) @@ -546,12 +539,12 @@ fn subcommands(p: &clap::Command) -> Vec { .map(|s| { CompletionCandidate::new(s.to_string()) .help(sc.get_about().cloned()) - .visible(!sc.is_hide_set()) + .hide(sc.is_hide_set()) }) .chain(sc.get_aliases().map(|s| { CompletionCandidate::new(s.to_string()) .help(sc.get_about().cloned()) - .visible(false) + .hide(true) })) }) .collect() @@ -667,8 +660,8 @@ pub struct CompletionCandidate { /// Help message with a completion candidate help: Option, - /// Whether the completion candidate is visible - visible: bool, + /// Whether the completion candidate is hidden + hidden: bool, } impl CompletionCandidate { @@ -688,8 +681,8 @@ impl CompletionCandidate { } /// Set the visibility of the completion candidate - pub fn visible(mut self, visible: bool) -> Self { - self.visible = visible; + pub fn hide(mut self, hidden: bool) -> Self { + self.hidden = hidden; self } @@ -704,7 +697,7 @@ impl CompletionCandidate { } /// Get the visibility of the completion candidate - pub fn is_visible(&self) -> bool { - self.visible + pub fn is_hide_set(&self) -> bool { + self.hidden } } From bbb2e6fdde1c724e39c2f2616332310252c12ab8 Mon Sep 17 00:00:00 2001 From: shanmu Date: Fri, 2 Aug 2024 23:16:03 +0800 Subject: [PATCH 2/3] test: Add test case for completing custom value of argument --- clap_complete/tests/testsuite/dynamic.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/clap_complete/tests/testsuite/dynamic.rs b/clap_complete/tests/testsuite/dynamic.rs index 0c0fe22b720..b909159134b 100644 --- a/clap_complete/tests/testsuite/dynamic.rs +++ b/clap_complete/tests/testsuite/dynamic.rs @@ -590,6 +590,13 @@ val3 ); } +#[test] +fn suggest_custom_arg_value() { + let mut cmd = Command::new("dynamic").arg(clap::Arg::new("custom").long("custom")); + + assert_data_eq!(complete!(cmd, "--custom [TAB]"), snapbox::str![""],); +} + #[test] fn suggest_multi_positional() { let mut cmd = Command::new("dynamic") From 7fd7b3e40bd835070253432accf4076bb020beda Mon Sep 17 00:00:00 2001 From: shanmu Date: Fri, 2 Aug 2024 23:18:47 +0800 Subject: [PATCH 3/3] feat(clap_complete): Support to complete custom value of argument --- Cargo.toml | 2 +- clap_complete/Cargo.toml | 2 +- clap_complete/src/dynamic/completer.rs | 84 +++++++++++++++++++++++- clap_complete/tests/testsuite/dynamic.rs | 29 +++++++- 4 files changed, 112 insertions(+), 5 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index f4f3f97f134..d8706089b66 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -168,7 +168,7 @@ string = ["clap_builder/string"] # Allow runtime generated strings # In-work features unstable-v5 = ["clap_builder/unstable-v5", "clap_derive?/unstable-v5", "deprecated"] -unstable-ext = [] +unstable-ext = ["clap_builder/unstable-ext"] unstable-styles = ["clap_builder/unstable-styles"] # deprecated [lib] diff --git a/clap_complete/Cargo.toml b/clap_complete/Cargo.toml index 495ba99ab98..c24e0749462 100644 --- a/clap_complete/Cargo.toml +++ b/clap_complete/Cargo.toml @@ -57,7 +57,7 @@ required-features = ["unstable-dynamic"] [features] default = [] unstable-doc = ["unstable-dynamic"] # for docs.rs -unstable-dynamic = ["dep:clap_lex", "dep:shlex", "dep:unicode-xid", "clap/derive", "dep:is_executable", "dep:pathdiff"] +unstable-dynamic = ["dep:clap_lex", "dep:shlex", "dep:unicode-xid", "clap/derive", "dep:is_executable", "dep:pathdiff", "clap/unstable-ext"] debug = ["clap/debug"] [lints] diff --git a/clap_complete/src/dynamic/completer.rs b/clap_complete/src/dynamic/completer.rs index 44837d758df..9d3e62f43c6 100644 --- a/clap_complete/src/dynamic/completer.rs +++ b/clap_complete/src/dynamic/completer.rs @@ -1,7 +1,9 @@ -use core::num; +use std::any::type_name; use std::ffi::OsStr; use std::ffi::OsString; +use std::sync::Arc; +use clap::builder::ArgExt; use clap::builder::StyledStr; use clap_lex::OsStrExt as _; @@ -349,6 +351,12 @@ fn complete_arg_value( }) })); } + } else if let Some(completer) = arg.get::() { + let value_os = match value { + Ok(value) => OsStr::new(value), + Err(value_os) => value_os, + }; + values.extend(complete_custom_arg_value(value_os, completer)); } else { let value_os = match value { Ok(value) => OsStr::new(value), @@ -385,6 +393,7 @@ fn complete_arg_value( values.extend(complete_path(value_os, current_dir, |_| true)); } } + values.sort(); } @@ -442,6 +451,21 @@ fn complete_path( completions } +fn complete_custom_arg_value( + value: &OsStr, + completer: &ArgValueCompleter, +) -> Vec { + debug!("complete_custom_arg_value: completer={completer:?}, value={value:?}"); + + let mut values = Vec::new(); + let custom_arg_values = completer.0.completions(); + values.extend(custom_arg_values); + + values.retain(|comp| comp.get_content().starts_with(&value.to_string_lossy())); + + values +} + fn complete_subcommand(value: &str, cmd: &clap::Command) -> Vec { debug!( "complete_subcommand: cmd={:?}, value={:?}", @@ -701,3 +725,61 @@ impl CompletionCandidate { self.hidden } } + +/// User-provided completion candidates for an argument. +/// +/// This is useful when predefined value hints are not enough. +pub trait CustomCompleter: Send + Sync { + /// All potential candidates for an argument. + /// + /// See [`CompletionCandidate`] for more information. + fn completions(&self) -> Vec; +} + +impl CustomCompleter for F +where + F: Fn() -> Vec + Send + Sync, +{ + fn completions(&self) -> Vec { + self() + } +} + +/// A wrapper for custom completer +/// +/// # Example +/// +/// ```rust +/// use clap::Parser; +/// use clap_complete::dynamic::{ArgValueCompleter, CompletionCandidate}; +/// +/// #[derive(Debug, Parser)] +/// struct Cli { +/// #[arg(long, add = ArgValueCompleter::new(|| { vec![ +/// CompletionCandidate::new("foo"), +/// CompletionCandidate::new("bar"), +/// CompletionCandidate::new("baz")] }))] +/// custom: Option, +/// } +/// +/// ``` +#[derive(Clone)] +pub struct ArgValueCompleter(Arc); + +impl ArgValueCompleter { + /// Create a new `ArgValueCompleter` with a custom completer + pub fn new(completer: C) -> Self + where + C: 'static + CustomCompleter, + { + Self(Arc::new(completer)) + } +} + +impl std::fmt::Debug for ArgValueCompleter { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(type_name::()) + } +} + +impl ArgExt for ArgValueCompleter {} diff --git a/clap_complete/tests/testsuite/dynamic.rs b/clap_complete/tests/testsuite/dynamic.rs index b909159134b..31214fa1339 100644 --- a/clap_complete/tests/testsuite/dynamic.rs +++ b/clap_complete/tests/testsuite/dynamic.rs @@ -4,6 +4,7 @@ use std::fs; use std::path::Path; use clap::{builder::PossibleValue, Command}; +use clap_complete::dynamic::{ArgValueCompleter, CompletionCandidate, CustomCompleter}; use snapbox::assert_data_eq; macro_rules! complete { @@ -592,9 +593,33 @@ val3 #[test] fn suggest_custom_arg_value() { - let mut cmd = Command::new("dynamic").arg(clap::Arg::new("custom").long("custom")); + #[derive(Debug)] + struct MyCustomCompleter {} + + impl CustomCompleter for MyCustomCompleter { + fn completions(&self) -> Vec { + vec![ + CompletionCandidate::new("custom1"), + CompletionCandidate::new("custom2"), + CompletionCandidate::new("custom3"), + ] + } + } + + let mut cmd = Command::new("dynamic").arg( + clap::Arg::new("custom") + .long("custom") + .add::(ArgValueCompleter::new(MyCustomCompleter {})), + ); - assert_data_eq!(complete!(cmd, "--custom [TAB]"), snapbox::str![""],); + assert_data_eq!( + complete!(cmd, "--custom [TAB]"), + snapbox::str![ + "custom1 +custom2 +custom3" + ], + ); } #[test]