diff --git a/Cargo.lock b/Cargo.lock index a04c3626a488..383d0538da85 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -468,6 +468,7 @@ version = "4.5.12" dependencies = [ "automod", "clap 4.5.13", + "clap_builder 4.5.13", "clap_lex 0.7.2", "completest", "completest-pty", diff --git a/clap_complete/Cargo.toml b/clap_complete/Cargo.toml index 495ba99ab988..0788ce145f7e 100644 --- a/clap_complete/Cargo.toml +++ b/clap_complete/Cargo.toml @@ -40,6 +40,7 @@ is_executable = { version = "1.0.1", optional = true } pathdiff = { version = "0.2.1", optional = true } shlex = { version = "1.1.0", optional = true } unicode-xid = { version = "0.2.2", optional = true } +clap_builder = { path = "../clap_builder", version = "4.5.13", default-features = false, features = ["unstable-ext"]} [dev-dependencies] snapbox = { version = "0.6.0", features = ["diff", "dir", "examples"] } @@ -49,6 +50,7 @@ completest = "0.4.1" completest-pty = "0.5.2" clap = { path = "../", version = "4.5.10", default-features = false, features = ["std", "derive", "help"] } automod = "1.0.14" +clap_builder = { path = "../clap_builder", version = "4.5.13", default-features = false, features = ["unstable-ext"]} [[example]] name = "dynamic" @@ -57,7 +59,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_builder/unstable-ext"] debug = ["clap/debug"] [lints] diff --git a/clap_complete/src/dynamic/completer.rs b/clap_complete/src/dynamic/completer.rs index 7a9015399a84..426f958ac13c 100644 --- a/clap_complete/src/dynamic/completer.rs +++ b/clap_complete/src/dynamic/completer.rs @@ -1,7 +1,9 @@ -use core::num; use std::ffi::OsStr; use std::ffi::OsString; +use std::ops::Deref; +use std::sync::Arc; +use clap::builder::ArgExt; use clap::builder::StyledStr; use clap_lex::OsStrExt as _; @@ -386,9 +388,16 @@ fn complete_arg_value( values.extend(complete_path(value_os, current_dir, |_| true)); } } + values.sort(); } + let value_os = match value { + Ok(value) => OsStr::new(value), + Err(value_os) => value_os, + }; + values.extend(complete_custom_arg_value(value_os, arg)); + values } @@ -449,6 +458,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.custom_hint(); + 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={:?}", @@ -708,3 +731,75 @@ impl CompletionCandidate { self.visible } } + +/// This trait is used to provide users a way to add custom value hint to the argument. +/// This is useful when predefined value hints are not enough. +pub trait CustomCompleter: core::fmt::Debug + Send + Sync { + /// This method should return a list of custom value completions. + /// If there is no completion, it should return `vec![]`. + /// + /// See [`CompletionCandidate`] for more information. + fn custom_hint(&self) -> Vec; +} + +/// A wrapper for custom completer +/// +/// # Example +/// +/// ```rust +/// use clap_complete::dynamic::{ArgValueCompleter, CustomCompleter}; +/// use clap_complete::dynamic::CompletionCandidate; +/// use std::ffi::OsString; +/// +/// #[derive(Debug)] +/// struct MyCustomCompleter { +/// file: std::path::PathBuf, +/// } +/// +/// impl CustomCompleter for MyCustomCompleter { +/// fn custom_hint(&self) -> Vec { +/// let content = std::fs::read_to_string(&self.file); +/// match content { +/// Ok(content) => { +/// content.lines().map(|os| CompletionCandidate::new(os).visible(true)).collect() +/// } +/// Err(_) => vec![], +/// } +/// } +/// } +/// +/// fn main() { +/// let completer = ArgValueCompleter::new(MyCustomCompleter{ +/// file: std::path::PathBuf::from("/path/to/file"), +/// }); +/// +/// // TODO: Need to implement this command by using derive API. +/// // This is just a placeholder to show how to add custom completer. +/// let mut arg = clap::Arg::new("custom").long("custom"); +/// arg.add(completer); +/// let mut cmd = clap::Command::new("dynamic").arg(arg); +/// } +/// +/// ``` +#[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 Deref for ArgValueCompleter { + type Target = dyn CustomCompleter; + + fn deref(&self) -> &Self::Target { + &*self.0 + } +} + +impl ArgExt for ArgValueCompleter {} diff --git a/clap_complete/tests/testsuite/dynamic.rs b/clap_complete/tests/testsuite/dynamic.rs index b909159134b4..4754659fb03e 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,32 @@ 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 custom_hint(&self) -> Vec { + vec![ + CompletionCandidate::new("custom1").visible(true), + CompletionCandidate::new("custom2").visible(true), + CompletionCandidate::new("custom3").visible(true), + ] + } + } + + let mut arg = clap::Arg::new("custom").long("custom"); + arg.add::(ArgValueCompleter::new(MyCustomCompleter {})); + + let mut cmd = Command::new("dynamic").arg(arg); - assert_data_eq!(complete!(cmd, "--custom [TAB]"), snapbox::str![""],); + assert_data_eq!( + complete!(cmd, "--custom [TAB]"), + snapbox::str![ + "custom1 +custom2 +custom3" + ], + ); } #[test]