Skip to content

Commit 82a360a

Browse files
committed
feat(complete): Add ArgValueCompleter
1 parent 47aedc6 commit 82a360a

File tree

7 files changed

+134
-2
lines changed

7 files changed

+134
-2
lines changed

clap_complete/src/command/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ pub use shells::*;
5050
/// - [`ValueHint`][crate::ValueHint]
5151
/// - [`ValueEnum`][clap::ValueEnum]
5252
/// - [`ArgValueCandidates`][crate::ArgValueCandidates]
53+
/// - [`ArgValueCompleter`][crate::ArgValueCompleter]
5354
///
5455
/// **Warning:** `stdout` should not be written to before [`CompleteCommand::complete`] has had a
5556
/// chance to run.
@@ -122,6 +123,7 @@ impl CompleteCommand {
122123
/// - [`ValueHint`][crate::ValueHint]
123124
/// - [`ValueEnum`][clap::ValueEnum]
124125
/// - [`ArgValueCandidates`][crate::ArgValueCandidates]
126+
/// - [`ArgValueCompleter`][crate::ArgValueCompleter]
125127
///
126128
/// **Warning:** `stdout` should not be written to before [`CompleteArgs::complete`] has had a
127129
/// chance to run.

clap_complete/src/engine/complete.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ use clap_lex::OsStrExt as _;
55

66
use super::custom::complete_path;
77
use super::ArgValueCandidates;
8+
use super::ArgValueCompleter;
89
use super::CompletionCandidate;
910

1011
/// Complete the given command, shell-agnostic
@@ -271,7 +272,9 @@ fn complete_arg_value(
271272
Err(value_os) => value_os,
272273
};
273274

274-
if let Some(completer) = arg.get::<ArgValueCandidates>() {
275+
if let Some(completer) = arg.get::<ArgValueCompleter>() {
276+
values.extend(completer.complete(value_os));
277+
} else if let Some(completer) = arg.get::<ArgValueCandidates>() {
275278
values.extend(complete_custom_arg_value(value_os, completer));
276279
} else if let Some(possible_values) = possible_values(arg) {
277280
if let Ok(value) = value {

clap_complete/src/engine/custom.rs

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,85 @@ where
7171
}
7272
}
7373

74+
/// Extend [`Arg`][clap::Arg] with a completer
75+
///
76+
/// # Example
77+
///
78+
/// ```rust
79+
/// use clap::Parser;
80+
/// use clap_complete::engine::{ArgValueCompleter, CompletionCandidate};
81+
///
82+
/// fn custom_completer(current: &std::ffi::OsStr) -> Vec<CompletionCandidate> {
83+
/// let mut completions = vec![];
84+
/// let Some(current) = current.to_str() else {
85+
/// return completions;
86+
/// };
87+
///
88+
/// if "foo".starts_with(current) {
89+
/// completions.push(CompletionCandidate::new("foo"));
90+
/// }
91+
/// if "bar".starts_with(current) {
92+
/// completions.push(CompletionCandidate::new("bar"));
93+
/// }
94+
/// if "baz".starts_with(current) {
95+
/// completions.push(CompletionCandidate::new("baz"));
96+
/// }
97+
/// completions
98+
/// }
99+
///
100+
/// #[derive(Debug, Parser)]
101+
/// struct Cli {
102+
/// #[arg(long, add = ArgValueCompleter::new(custom_completer))]
103+
/// custom: Option<String>,
104+
/// }
105+
/// ```
106+
#[derive(Clone)]
107+
pub struct ArgValueCompleter(Arc<dyn ValueCompleter>);
108+
109+
impl ArgValueCompleter {
110+
/// Create a new `ArgValueCompleter` with a custom completer
111+
pub fn new<C>(completer: C) -> Self
112+
where
113+
C: ValueCompleter + 'static,
114+
{
115+
Self(Arc::new(completer))
116+
}
117+
118+
/// Candidates that match `current`
119+
///
120+
/// See [`CompletionCandidate`] for more information.
121+
pub fn complete(&self, current: &OsStr) -> Vec<CompletionCandidate> {
122+
self.0.complete(current)
123+
}
124+
}
125+
126+
impl std::fmt::Debug for ArgValueCompleter {
127+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
128+
f.write_str(type_name::<Self>())
129+
}
130+
}
131+
132+
impl ArgExt for ArgValueCompleter {}
133+
134+
/// User-provided completion candidates for an [`Arg`][clap::Arg], see [`ArgValueCompleter`]
135+
///
136+
/// This is useful when predefined value hints are not enough.
137+
pub trait ValueCompleter: Send + Sync {
138+
/// All potential candidates for an argument.
139+
///
140+
/// See [`CompletionCandidate`] for more information.
141+
fn complete(&self, current: &OsStr) -> Vec<CompletionCandidate>;
142+
}
143+
144+
impl<F> ValueCompleter for F
145+
where
146+
F: Fn(&OsStr) -> Vec<CompletionCandidate> + Send + Sync,
147+
{
148+
fn complete(&self, current: &OsStr) -> Vec<CompletionCandidate> {
149+
self(current)
150+
}
151+
}
152+
74153
pub(crate) fn complete_path(
75154
value_os: &OsStr,
76155
current_dir: Option<&std::path::Path>,

clap_complete/src/engine/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,6 @@ mod custom;
99
pub use candidate::CompletionCandidate;
1010
pub use complete::complete;
1111
pub use custom::ArgValueCandidates;
12+
pub use custom::ArgValueCompleter;
1213
pub use custom::ValueCandidates;
14+
pub use custom::ValueCompleter;

clap_complete/src/env/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
//! - [`ValueHint`][crate::ValueHint]
2121
//! - [`ValueEnum`][clap::ValueEnum]
2222
//! - [`ArgValueCandidates`][crate::ArgValueCandidates]
23+
//! - [`ArgValueCompleter`][crate::ArgValueCompleter]
2324
//!
2425
//! To source your completions:
2526
//!

clap_complete/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,8 @@ pub use command::CompleteCommand;
8181
#[doc(inline)]
8282
#[cfg(feature = "unstable-dynamic")]
8383
pub use engine::ArgValueCandidates;
84+
#[cfg(feature = "unstable-dynamic")]
85+
pub use engine::ArgValueCompleter;
8486
#[doc(inline)]
8587
#[cfg(feature = "unstable-dynamic")]
8688
pub use engine::CompletionCandidate;

clap_complete/tests/testsuite/engine.rs

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

66
use clap::{builder::PossibleValue, Command};
7-
use clap_complete::engine::{ArgValueCandidates, CompletionCandidate};
7+
use clap_complete::engine::{ArgValueCandidates, ArgValueCompleter, CompletionCandidate};
88
use snapbox::assert_data_eq;
99

1010
macro_rules! complete {
@@ -609,6 +609,49 @@ baz
609609
);
610610
}
611611

612+
#[test]
613+
fn suggest_custom_arg_completer() {
614+
fn custom_completer(current: &std::ffi::OsStr) -> Vec<CompletionCandidate> {
615+
let mut completions = vec![];
616+
let Some(current) = current.to_str() else {
617+
return completions;
618+
};
619+
620+
if "foo".starts_with(current) {
621+
completions.push(CompletionCandidate::new("foo"));
622+
}
623+
if "bar".starts_with(current) {
624+
completions.push(CompletionCandidate::new("bar"));
625+
}
626+
if "baz".starts_with(current) {
627+
completions.push(CompletionCandidate::new("baz"));
628+
}
629+
completions
630+
}
631+
632+
let mut cmd = Command::new("dynamic").arg(
633+
clap::Arg::new("custom")
634+
.long("custom")
635+
.add(ArgValueCompleter::new(custom_completer)),
636+
);
637+
638+
assert_data_eq!(
639+
complete!(cmd, "--custom [TAB]"),
640+
snapbox::str![[r#"
641+
foo
642+
bar
643+
baz
644+
"#]]
645+
);
646+
assert_data_eq!(
647+
complete!(cmd, "--custom b[TAB]"),
648+
snapbox::str![[r#"
649+
bar
650+
baz
651+
"#]]
652+
);
653+
}
654+
612655
#[test]
613656
fn suggest_multi_positional() {
614657
let mut cmd = Command::new("dynamic")

0 commit comments

Comments
 (0)