diff --git a/clap_complete/src/command/mod.rs b/clap_complete/src/command/mod.rs index b7fbbf5b0fe..ed3ebeabd58 100644 --- a/clap_complete/src/command/mod.rs +++ b/clap_complete/src/command/mod.rs @@ -50,6 +50,7 @@ pub use shells::*; /// - [`ValueHint`][crate::ValueHint] /// - [`ValueEnum`][clap::ValueEnum] /// - [`ArgValueCandidates`][crate::ArgValueCandidates] +/// - [`ArgValueCompleter`][crate::ArgValueCompleter] /// /// **Warning:** `stdout` should not be written to before [`CompleteCommand::complete`] has had a /// chance to run. @@ -122,6 +123,7 @@ impl CompleteCommand { /// - [`ValueHint`][crate::ValueHint] /// - [`ValueEnum`][clap::ValueEnum] /// - [`ArgValueCandidates`][crate::ArgValueCandidates] +/// - [`ArgValueCompleter`][crate::ArgValueCompleter] /// /// **Warning:** `stdout` should not be written to before [`CompleteArgs::complete`] has had a /// chance to run. diff --git a/clap_complete/src/engine/complete.rs b/clap_complete/src/engine/complete.rs index b2cdfa30e3c..34110062bee 100644 --- a/clap_complete/src/engine/complete.rs +++ b/clap_complete/src/engine/complete.rs @@ -5,6 +5,7 @@ use clap_lex::OsStrExt as _; use super::custom::complete_path; use super::ArgValueCandidates; +use super::ArgValueCompleter; use super::CompletionCandidate; /// Complete the given command, shell-agnostic @@ -271,7 +272,9 @@ fn complete_arg_value( Err(value_os) => value_os, }; - if let Some(completer) = arg.get::() { + if let Some(completer) = arg.get::() { + values.extend(completer.complete(value_os)); + } else if let Some(completer) = arg.get::() { values.extend(complete_custom_arg_value(value_os, completer)); } else if let Some(possible_values) = possible_values(arg) { if let Ok(value) = value { diff --git a/clap_complete/src/engine/custom.rs b/clap_complete/src/engine/custom.rs index 240972cfb54..6879de3f3b1 100644 --- a/clap_complete/src/engine/custom.rs +++ b/clap_complete/src/engine/custom.rs @@ -71,6 +71,85 @@ where } } +/// Extend [`Arg`][clap::Arg] with a completer +/// +/// # Example +/// +/// ```rust +/// use clap::Parser; +/// use clap_complete::engine::{ArgValueCompleter, CompletionCandidate}; +/// +/// fn custom_completer(current: &std::ffi::OsStr) -> Vec { +/// let mut completions = vec![]; +/// let Some(current) = current.to_str() else { +/// return completions; +/// }; +/// +/// if "foo".starts_with(current) { +/// completions.push(CompletionCandidate::new("foo")); +/// } +/// if "bar".starts_with(current) { +/// completions.push(CompletionCandidate::new("bar")); +/// } +/// if "baz".starts_with(current) { +/// completions.push(CompletionCandidate::new("baz")); +/// } +/// completions +/// } +/// +/// #[derive(Debug, Parser)] +/// struct Cli { +/// #[arg(long, add = ArgValueCompleter::new(custom_completer))] +/// 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: ValueCompleter + 'static, + { + Self(Arc::new(completer)) + } + + /// Candidates that match `current` + /// + /// See [`CompletionCandidate`] for more information. + pub fn complete(&self, current: &OsStr) -> Vec { + self.0.complete(current) + } +} + +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 {} + +/// User-provided completion candidates for an [`Arg`][clap::Arg], see [`ArgValueCompleter`] +/// +/// This is useful when predefined value hints are not enough. +pub trait ValueCompleter: Send + Sync { + /// All potential candidates for an argument. + /// + /// See [`CompletionCandidate`] for more information. + fn complete(&self, current: &OsStr) -> Vec; +} + +impl ValueCompleter for F +where + F: Fn(&OsStr) -> Vec + Send + Sync, +{ + fn complete(&self, current: &OsStr) -> Vec { + self(current) + } +} + pub(crate) fn complete_path( value_os: &OsStr, current_dir: Option<&std::path::Path>, diff --git a/clap_complete/src/engine/mod.rs b/clap_complete/src/engine/mod.rs index 937577ebf3e..2e7e25d22c6 100644 --- a/clap_complete/src/engine/mod.rs +++ b/clap_complete/src/engine/mod.rs @@ -9,4 +9,6 @@ mod custom; pub use candidate::CompletionCandidate; pub use complete::complete; pub use custom::ArgValueCandidates; +pub use custom::ArgValueCompleter; pub use custom::ValueCandidates; +pub use custom::ValueCompleter; diff --git a/clap_complete/src/env/mod.rs b/clap_complete/src/env/mod.rs index 746549ab2f1..69e0b1d7233 100644 --- a/clap_complete/src/env/mod.rs +++ b/clap_complete/src/env/mod.rs @@ -20,6 +20,7 @@ //! - [`ValueHint`][crate::ValueHint] //! - [`ValueEnum`][clap::ValueEnum] //! - [`ArgValueCandidates`][crate::ArgValueCandidates] +//! - [`ArgValueCompleter`][crate::ArgValueCompleter] //! //! To source your completions: //! diff --git a/clap_complete/src/lib.rs b/clap_complete/src/lib.rs index 5e20c8a3483..f09b8fd1098 100644 --- a/clap_complete/src/lib.rs +++ b/clap_complete/src/lib.rs @@ -81,6 +81,8 @@ pub use command::CompleteCommand; #[doc(inline)] #[cfg(feature = "unstable-dynamic")] pub use engine::ArgValueCandidates; +#[cfg(feature = "unstable-dynamic")] +pub use engine::ArgValueCompleter; #[doc(inline)] #[cfg(feature = "unstable-dynamic")] pub use engine::CompletionCandidate; diff --git a/clap_complete/tests/testsuite/engine.rs b/clap_complete/tests/testsuite/engine.rs index ed70bd0931c..996e394d7ac 100644 --- a/clap_complete/tests/testsuite/engine.rs +++ b/clap_complete/tests/testsuite/engine.rs @@ -4,7 +4,7 @@ use std::fs; use std::path::Path; use clap::{builder::PossibleValue, Command}; -use clap_complete::engine::{ArgValueCandidates, CompletionCandidate}; +use clap_complete::engine::{ArgValueCandidates, ArgValueCompleter, CompletionCandidate}; use snapbox::assert_data_eq; macro_rules! complete { @@ -609,6 +609,49 @@ baz ); } +#[test] +fn suggest_custom_arg_completer() { + fn custom_completer(current: &std::ffi::OsStr) -> Vec { + let mut completions = vec![]; + let Some(current) = current.to_str() else { + return completions; + }; + + if "foo".starts_with(current) { + completions.push(CompletionCandidate::new("foo")); + } + if "bar".starts_with(current) { + completions.push(CompletionCandidate::new("bar")); + } + if "baz".starts_with(current) { + completions.push(CompletionCandidate::new("baz")); + } + completions + } + + let mut cmd = Command::new("dynamic").arg( + clap::Arg::new("custom") + .long("custom") + .add(ArgValueCompleter::new(custom_completer)), + ); + + assert_data_eq!( + complete!(cmd, "--custom [TAB]"), + snapbox::str![[r#" +foo +bar +baz +"#]] + ); + assert_data_eq!( + complete!(cmd, "--custom b[TAB]"), + snapbox::str![[r#" +bar +baz +"#]] + ); +} + #[test] fn suggest_multi_positional() { let mut cmd = Command::new("dynamic")