diff --git a/Cargo.toml b/Cargo.toml index f4f3f97f1345..d8706089b66b 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 495ba99ab988..c24e07494624 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 44837d758df7..6b1ae4aae24a 100644 --- a/clap_complete/src/dynamic/completer.rs +++ b/clap_complete/src/dynamic/completer.rs @@ -1,7 +1,8 @@ -use core::num; 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 _; @@ -385,6 +386,10 @@ fn complete_arg_value( values.extend(complete_path(value_os, current_dir, |_| true)); } } + + // Add custom completion at the same level as the ValueHint. + values.extend(complete_custom_arg_value(value_os, arg)); + values.sort(); } @@ -442,6 +447,20 @@ fn complete_path( completions } +fn complete_custom_arg_value(value: &OsStr, arg: &clap::Arg) -> Vec { + let mut values = Vec::new(); + debug!("complete_custom_arg_value: arg={arg:?}, value={value:?}"); + + if let Some(completer) = arg.get::() { + 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 +720,96 @@ impl CompletionCandidate { self.hidden } } + +/// User-provided completion candidates for an argument. +/// +/// This is useful when predefined value hints are not enough. +pub trait CustomCompleter: core::fmt::Debug + Send + Sync { + /// All potential candidates for an argument. + /// + /// See [`CompletionCandidate`] for more information. + fn completions(&self) -> Vec; +} + +/// A closure-based custom completer +/// +/// The closure should return a list of completion candidates. +pub struct ClosureCompleter +where + F: Fn() -> Vec + Send + Sync, +{ + closure: F, + debug_info: String, +} + +impl ClosureCompleter +where + F: Fn() -> Vec + Send + Sync, +{ + /// Create a new `ClosureCompleter` with a closure and debug info. + pub fn new(closure: F, debug_info: impl Into) -> Self { + Self { + closure, + debug_info: debug_info.into(), + } + } +} + +impl CustomCompleter for ClosureCompleter +where + F: Fn() -> Vec + Send + Sync, +{ + fn completions(&self) -> Vec { + (self.closure)() + } +} + +impl std::fmt::Debug for ClosureCompleter +where + F: Fn() -> Vec + Send + Sync, +{ + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("ClosureCompleter") + .field("debug_info", &self.debug_info) + .finish() + } +} + +/// A wrapper for custom completer +/// +/// # Example +/// +/// ```rust +/// use clap::Parser; +/// use clap_complete::dynamic::CompletionCandidate; +/// use clap_complete::dynamic::{ArgValueCompleter, ClosureCompleter}; +/// +/// #[derive(Debug, Parser)] +/// struct Cli { +/// #[arg(long, add = ArgValueCompleter::new(ClosureCompleter::new(|| { vec![ +/// CompletionCandidate::new("foo"), +/// CompletionCandidate::new("bar"), +/// CompletionCandidate::new("baz")] }, "debug info")))] +/// custom: Option, +/// } +/// +/// fn main() { +/// let _cli = Cli::parse(); +/// +/// // normal logic continues... +/// } +/// ``` +#[derive(Debug, 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 ArgExt for ArgValueCompleter {} diff --git a/clap_complete/tests/testsuite/dynamic.rs b/clap_complete/tests/testsuite/dynamic.rs index b909159134b4..31214fa13398 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]