Skip to content

Commit 2690e1b

Browse files
authored
Merge pull request #5621 from shannmu/dynamic_valuehint
Support dynamic value of argument completion
2 parents fc6aaca + 7fd7b3e commit 2690e1b

File tree

3 files changed

+137
-30
lines changed

3 files changed

+137
-30
lines changed

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: 104 additions & 29 deletions
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

@@ -198,7 +200,7 @@ fn complete_arg(
198200
comp.get_content().to_string_lossy()
199201
))
200202
.help(comp.get_help().cloned())
201-
.visible(comp.is_visible())
203+
.hide(comp.is_hide_set())
202204
}),
203205
);
204206
}
@@ -241,7 +243,6 @@ fn complete_arg(
241243
comp.get_content().to_string_lossy()
242244
))
243245
.help(comp.get_help().cloned())
244-
.visible(true)
245246
}),
246247
);
247248
} else if let Some(short) = arg.to_short() {
@@ -271,7 +272,7 @@ fn complete_arg(
271272
comp.get_content().to_string_lossy()
272273
))
273274
.help(comp.get_help().cloned())
274-
.visible(comp.is_visible())
275+
.hide(comp.is_hide_set())
275276
}),
276277
);
277278
} else {
@@ -283,7 +284,7 @@ fn complete_arg(
283284
comp.get_content().to_string_lossy()
284285
))
285286
.help(comp.get_help().cloned())
286-
.visible(comp.is_visible())
287+
.hide(comp.is_hide_set())
287288
},
288289
));
289290
}
@@ -324,8 +325,8 @@ fn complete_arg(
324325
}
325326
}
326327
}
327-
if completions.iter().any(|a| a.is_visible()) {
328-
completions.retain(|a| a.is_visible());
328+
if completions.iter().any(|a| !a.is_hide_set()) {
329+
completions.retain(|a| !a.is_hide_set());
329330
}
330331

331332
Ok(completions)
@@ -346,10 +347,16 @@ fn complete_arg_value(
346347
name.starts_with(value).then(|| {
347348
CompletionCandidate::new(OsString::from(name))
348349
.help(p.get_help().cloned())
349-
.visible(!p.is_hide_set())
350+
.hide(p.is_hide_set())
350351
})
351352
}));
352353
}
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));
353360
} else {
354361
let value_os = match value {
355362
Ok(value) => OsStr::new(value),
@@ -386,6 +393,7 @@ fn complete_arg_value(
386393
values.extend(complete_path(value_os, current_dir, |_| true));
387394
}
388395
}
396+
389397
values.sort();
390398
}
391399

@@ -428,27 +436,36 @@ fn complete_path(
428436
let path = entry.path();
429437
let mut suggestion = pathdiff::diff_paths(&path, current_dir).unwrap_or(path);
430438
suggestion.push(""); // Ensure trailing `/`
431-
completions.push(
432-
CompletionCandidate::new(suggestion.as_os_str().to_owned())
433-
.help(None)
434-
.visible(true),
435-
);
439+
completions
440+
.push(CompletionCandidate::new(suggestion.as_os_str().to_owned()).help(None));
436441
} else {
437442
let path = entry.path();
438443
if is_wanted(&path) {
439444
let suggestion = pathdiff::diff_paths(&path, current_dir).unwrap_or(path);
440-
completions.push(
441-
CompletionCandidate::new(suggestion.as_os_str().to_owned())
442-
.help(None)
443-
.visible(true),
444-
);
445+
completions
446+
.push(CompletionCandidate::new(suggestion.as_os_str().to_owned()).help(None));
445447
}
446448
}
447449
}
448450

449451
completions
450452
}
451453

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+
452469
fn complete_subcommand(value: &str, cmd: &clap::Command) -> Vec<CompletionCandidate> {
453470
debug!(
454471
"complete_subcommand: cmd={:?}, value={:?}",
@@ -476,7 +493,7 @@ fn longs_and_visible_aliases(p: &clap::Command) -> Vec<CompletionCandidate> {
476493
longs.into_iter().map(|s| {
477494
CompletionCandidate::new(format!("--{}", s))
478495
.help(a.get_help().cloned())
479-
.visible(!a.is_hide_set())
496+
.hide(a.is_hide_set())
480497
})
481498
})
482499
})
@@ -494,7 +511,7 @@ fn hidden_longs_aliases(p: &clap::Command) -> Vec<CompletionCandidate> {
494511
longs.into_iter().map(|s| {
495512
CompletionCandidate::new(format!("--{}", s))
496513
.help(a.get_help().cloned())
497-
.visible(false)
514+
.hide(true)
498515
})
499516
})
500517
})
@@ -513,7 +530,7 @@ fn shorts_and_visible_aliases(p: &clap::Command) -> Vec<CompletionCandidate> {
513530
shorts.into_iter().map(|s| {
514531
CompletionCandidate::new(s.to_string())
515532
.help(a.get_help().cloned())
516-
.visible(!a.is_hide_set())
533+
.hide(a.is_hide_set())
517534
})
518535
})
519536
})
@@ -546,12 +563,12 @@ fn subcommands(p: &clap::Command) -> Vec<CompletionCandidate> {
546563
.map(|s| {
547564
CompletionCandidate::new(s.to_string())
548565
.help(sc.get_about().cloned())
549-
.visible(!sc.is_hide_set())
566+
.hide(sc.is_hide_set())
550567
})
551568
.chain(sc.get_aliases().map(|s| {
552569
CompletionCandidate::new(s.to_string())
553570
.help(sc.get_about().cloned())
554-
.visible(false)
571+
.hide(true)
555572
}))
556573
})
557574
.collect()
@@ -667,8 +684,8 @@ pub struct CompletionCandidate {
667684
/// Help message with a completion candidate
668685
help: Option<StyledStr>,
669686

670-
/// Whether the completion candidate is visible
671-
visible: bool,
687+
/// Whether the completion candidate is hidden
688+
hidden: bool,
672689
}
673690

674691
impl CompletionCandidate {
@@ -688,8 +705,8 @@ impl CompletionCandidate {
688705
}
689706

690707
/// Set the visibility of the completion candidate
691-
pub fn visible(mut self, visible: bool) -> Self {
692-
self.visible = visible;
708+
pub fn hide(mut self, hidden: bool) -> Self {
709+
self.hidden = hidden;
693710
self
694711
}
695712

@@ -704,7 +721,65 @@ impl CompletionCandidate {
704721
}
705722

706723
/// Get the visibility of the completion candidate
707-
pub fn is_visible(&self) -> bool {
708-
self.visible
724+
pub fn is_hide_set(&self) -> bool {
725+
self.hidden
709726
}
710727
}
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: 32 additions & 0 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 {
@@ -590,6 +591,37 @@ val3
590591
);
591592
}
592593

594+
#[test]
595+
fn suggest_custom_arg_value() {
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+
);
614+
615+
assert_data_eq!(
616+
complete!(cmd, "--custom [TAB]"),
617+
snapbox::str![
618+
"custom1
619+
custom2
620+
custom3"
621+
],
622+
);
623+
}
624+
593625
#[test]
594626
fn suggest_multi_positional() {
595627
let mut cmd = Command::new("dynamic")

0 commit comments

Comments
 (0)