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 7a9015399a8..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 _; @@ -198,7 +200,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 +243,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 +272,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 +284,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 +325,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,10 +347,16 @@ 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()) }) })); } + } 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), @@ -386,6 +393,7 @@ fn complete_arg_value( values.extend(complete_path(value_os, current_dir, |_| true)); } } + values.sort(); } @@ -428,20 +436,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)); } } } @@ -449,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={:?}", @@ -476,7 +493,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 +511,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 +530,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 +563,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 +684,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 +705,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 +721,65 @@ 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 } } + +/// 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 0c0fe22b720..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 { @@ -590,6 +591,37 @@ val3 ); } +#[test] +fn suggest_custom_arg_value() { + #[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![ + "custom1 +custom2 +custom3" + ], + ); +} + #[test] fn suggest_multi_positional() { let mut cmd = Command::new("dynamic")