Skip to content

Commit 7fd7b3e

Browse files
committed
feat(clap_complete): Support to complete custom value of argument
1 parent bbb2e6f commit 7fd7b3e

File tree

4 files changed

+112
-5
lines changed

4 files changed

+112
-5
lines changed

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@ string = ["clap_builder/string"] # Allow runtime generated strings
168168

169169
# In-work features
170170
unstable-v5 = ["clap_builder/unstable-v5", "clap_derive?/unstable-v5", "deprecated"]
171-
unstable-ext = []
171+
unstable-ext = ["clap_builder/unstable-ext"]
172172
unstable-styles = ["clap_builder/unstable-styles"] # deprecated
173173

174174
[lib]

clap_complete/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ required-features = ["unstable-dynamic"]
5757
[features]
5858
default = []
5959
unstable-doc = ["unstable-dynamic"] # for docs.rs
60-
unstable-dynamic = ["dep:clap_lex", "dep:shlex", "dep:unicode-xid", "clap/derive", "dep:is_executable", "dep:pathdiff"]
60+
unstable-dynamic = ["dep:clap_lex", "dep:shlex", "dep:unicode-xid", "clap/derive", "dep:is_executable", "dep:pathdiff", "clap/unstable-ext"]
6161
debug = ["clap/debug"]
6262

6363
[lints]

clap_complete/src/dynamic/completer.rs

Lines changed: 83 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1-
use core::num;
1+
use std::any::type_name;
22
use std::ffi::OsStr;
33
use std::ffi::OsString;
4+
use std::sync::Arc;
45

6+
use clap::builder::ArgExt;
57
use clap::builder::StyledStr;
68
use clap_lex::OsStrExt as _;
79

@@ -349,6 +351,12 @@ fn complete_arg_value(
349351
})
350352
}));
351353
}
354+
} else if let Some(completer) = arg.get::<ArgValueCompleter>() {
355+
let value_os = match value {
356+
Ok(value) => OsStr::new(value),
357+
Err(value_os) => value_os,
358+
};
359+
values.extend(complete_custom_arg_value(value_os, completer));
352360
} else {
353361
let value_os = match value {
354362
Ok(value) => OsStr::new(value),
@@ -385,6 +393,7 @@ fn complete_arg_value(
385393
values.extend(complete_path(value_os, current_dir, |_| true));
386394
}
387395
}
396+
388397
values.sort();
389398
}
390399

@@ -442,6 +451,21 @@ fn complete_path(
442451
completions
443452
}
444453

454+
fn complete_custom_arg_value(
455+
value: &OsStr,
456+
completer: &ArgValueCompleter,
457+
) -> Vec<CompletionCandidate> {
458+
debug!("complete_custom_arg_value: completer={completer:?}, value={value:?}");
459+
460+
let mut values = Vec::new();
461+
let custom_arg_values = completer.0.completions();
462+
values.extend(custom_arg_values);
463+
464+
values.retain(|comp| comp.get_content().starts_with(&value.to_string_lossy()));
465+
466+
values
467+
}
468+
445469
fn complete_subcommand(value: &str, cmd: &clap::Command) -> Vec<CompletionCandidate> {
446470
debug!(
447471
"complete_subcommand: cmd={:?}, value={:?}",
@@ -701,3 +725,61 @@ impl CompletionCandidate {
701725
self.hidden
702726
}
703727
}
728+
729+
/// User-provided completion candidates for an argument.
730+
///
731+
/// This is useful when predefined value hints are not enough.
732+
pub trait CustomCompleter: Send + Sync {
733+
/// All potential candidates for an argument.
734+
///
735+
/// See [`CompletionCandidate`] for more information.
736+
fn completions(&self) -> Vec<CompletionCandidate>;
737+
}
738+
739+
impl<F> CustomCompleter for F
740+
where
741+
F: Fn() -> Vec<CompletionCandidate> + Send + Sync,
742+
{
743+
fn completions(&self) -> Vec<CompletionCandidate> {
744+
self()
745+
}
746+
}
747+
748+
/// A wrapper for custom completer
749+
///
750+
/// # Example
751+
///
752+
/// ```rust
753+
/// use clap::Parser;
754+
/// use clap_complete::dynamic::{ArgValueCompleter, CompletionCandidate};
755+
///
756+
/// #[derive(Debug, Parser)]
757+
/// struct Cli {
758+
/// #[arg(long, add = ArgValueCompleter::new(|| { vec![
759+
/// CompletionCandidate::new("foo"),
760+
/// CompletionCandidate::new("bar"),
761+
/// CompletionCandidate::new("baz")] }))]
762+
/// custom: Option<String>,
763+
/// }
764+
///
765+
/// ```
766+
#[derive(Clone)]
767+
pub struct ArgValueCompleter(Arc<dyn CustomCompleter>);
768+
769+
impl ArgValueCompleter {
770+
/// Create a new `ArgValueCompleter` with a custom completer
771+
pub fn new<C: CustomCompleter>(completer: C) -> Self
772+
where
773+
C: 'static + CustomCompleter,
774+
{
775+
Self(Arc::new(completer))
776+
}
777+
}
778+
779+
impl std::fmt::Debug for ArgValueCompleter {
780+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
781+
f.write_str(type_name::<Self>())
782+
}
783+
}
784+
785+
impl ArgExt for ArgValueCompleter {}

clap_complete/tests/testsuite/dynamic.rs

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ use std::fs;
44
use std::path::Path;
55

66
use clap::{builder::PossibleValue, Command};
7+
use clap_complete::dynamic::{ArgValueCompleter, CompletionCandidate, CustomCompleter};
78
use snapbox::assert_data_eq;
89

910
macro_rules! complete {
@@ -592,9 +593,33 @@ val3
592593

593594
#[test]
594595
fn suggest_custom_arg_value() {
595-
let mut cmd = Command::new("dynamic").arg(clap::Arg::new("custom").long("custom"));
596+
#[derive(Debug)]
597+
struct MyCustomCompleter {}
598+
599+
impl CustomCompleter for MyCustomCompleter {
600+
fn completions(&self) -> Vec<CompletionCandidate> {
601+
vec![
602+
CompletionCandidate::new("custom1"),
603+
CompletionCandidate::new("custom2"),
604+
CompletionCandidate::new("custom3"),
605+
]
606+
}
607+
}
608+
609+
let mut cmd = Command::new("dynamic").arg(
610+
clap::Arg::new("custom")
611+
.long("custom")
612+
.add::<ArgValueCompleter>(ArgValueCompleter::new(MyCustomCompleter {})),
613+
);
596614

597-
assert_data_eq!(complete!(cmd, "--custom [TAB]"), snapbox::str![""],);
615+
assert_data_eq!(
616+
complete!(cmd, "--custom [TAB]"),
617+
snapbox::str![
618+
"custom1
619+
custom2
620+
custom3"
621+
],
622+
);
598623
}
599624

600625
#[test]

0 commit comments

Comments
 (0)