Skip to content

Commit

Permalink
Merge pull request #5621 from shannmu/dynamic_valuehint
Browse files Browse the repository at this point in the history
Support dynamic value of argument completion
  • Loading branch information
epage authored Aug 8, 2024
2 parents fc6aaca + 7fd7b3e commit 2690e1b
Show file tree
Hide file tree
Showing 3 changed files with 137 additions and 30 deletions.
2 changes: 1 addition & 1 deletion clap_complete/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
133 changes: 104 additions & 29 deletions clap_complete/src/dynamic/completer.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
use core::num;
use std::any::type_name;
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 _;

Expand Down Expand Up @@ -198,7 +200,7 @@ fn complete_arg(
comp.get_content().to_string_lossy()
))
.help(comp.get_help().cloned())
.visible(comp.is_visible())
.hide(comp.is_hide_set())
}),
);
}
Expand Down Expand Up @@ -241,7 +243,6 @@ fn complete_arg(
comp.get_content().to_string_lossy()
))
.help(comp.get_help().cloned())
.visible(true)
}),
);
} else if let Some(short) = arg.to_short() {
Expand Down Expand Up @@ -271,7 +272,7 @@ fn complete_arg(
comp.get_content().to_string_lossy()
))
.help(comp.get_help().cloned())
.visible(comp.is_visible())
.hide(comp.is_hide_set())
}),
);
} else {
Expand All @@ -283,7 +284,7 @@ fn complete_arg(
comp.get_content().to_string_lossy()
))
.help(comp.get_help().cloned())
.visible(comp.is_visible())
.hide(comp.is_hide_set())
},
));
}
Expand Down Expand Up @@ -324,8 +325,8 @@ fn complete_arg(
}
}
}
if completions.iter().any(|a| a.is_visible()) {
completions.retain(|a| a.is_visible());
if completions.iter().any(|a| !a.is_hide_set()) {
completions.retain(|a| !a.is_hide_set());
}

Ok(completions)
Expand All @@ -346,10 +347,16 @@ fn complete_arg_value(
name.starts_with(value).then(|| {
CompletionCandidate::new(OsString::from(name))
.help(p.get_help().cloned())
.visible(!p.is_hide_set())
.hide(p.is_hide_set())
})
}));
}
} else if let Some(completer) = arg.get::<ArgValueCompleter>() {
let value_os = match value {
Ok(value) => OsStr::new(value),
Err(value_os) => value_os,
};
values.extend(complete_custom_arg_value(value_os, completer));
} else {
let value_os = match value {
Ok(value) => OsStr::new(value),
Expand Down Expand Up @@ -386,6 +393,7 @@ fn complete_arg_value(
values.extend(complete_path(value_os, current_dir, |_| true));
}
}

values.sort();
}

Expand Down Expand Up @@ -428,27 +436,36 @@ fn complete_path(
let path = entry.path();
let mut suggestion = pathdiff::diff_paths(&path, current_dir).unwrap_or(path);
suggestion.push(""); // Ensure trailing `/`
completions.push(
CompletionCandidate::new(suggestion.as_os_str().to_owned())
.help(None)
.visible(true),
);
completions
.push(CompletionCandidate::new(suggestion.as_os_str().to_owned()).help(None));
} else {
let path = entry.path();
if is_wanted(&path) {
let suggestion = pathdiff::diff_paths(&path, current_dir).unwrap_or(path);
completions.push(
CompletionCandidate::new(suggestion.as_os_str().to_owned())
.help(None)
.visible(true),
);
completions
.push(CompletionCandidate::new(suggestion.as_os_str().to_owned()).help(None));
}
}
}

completions
}

fn complete_custom_arg_value(
value: &OsStr,
completer: &ArgValueCompleter,
) -> Vec<CompletionCandidate> {
debug!("complete_custom_arg_value: completer={completer:?}, value={value:?}");

let mut values = Vec::new();
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<CompletionCandidate> {
debug!(
"complete_subcommand: cmd={:?}, value={:?}",
Expand Down Expand Up @@ -476,7 +493,7 @@ fn longs_and_visible_aliases(p: &clap::Command) -> Vec<CompletionCandidate> {
longs.into_iter().map(|s| {
CompletionCandidate::new(format!("--{}", s))
.help(a.get_help().cloned())
.visible(!a.is_hide_set())
.hide(a.is_hide_set())
})
})
})
Expand All @@ -494,7 +511,7 @@ fn hidden_longs_aliases(p: &clap::Command) -> Vec<CompletionCandidate> {
longs.into_iter().map(|s| {
CompletionCandidate::new(format!("--{}", s))
.help(a.get_help().cloned())
.visible(false)
.hide(true)
})
})
})
Expand All @@ -513,7 +530,7 @@ fn shorts_and_visible_aliases(p: &clap::Command) -> Vec<CompletionCandidate> {
shorts.into_iter().map(|s| {
CompletionCandidate::new(s.to_string())
.help(a.get_help().cloned())
.visible(!a.is_hide_set())
.hide(a.is_hide_set())
})
})
})
Expand Down Expand Up @@ -546,12 +563,12 @@ fn subcommands(p: &clap::Command) -> Vec<CompletionCandidate> {
.map(|s| {
CompletionCandidate::new(s.to_string())
.help(sc.get_about().cloned())
.visible(!sc.is_hide_set())
.hide(sc.is_hide_set())
})
.chain(sc.get_aliases().map(|s| {
CompletionCandidate::new(s.to_string())
.help(sc.get_about().cloned())
.visible(false)
.hide(true)
}))
})
.collect()
Expand Down Expand Up @@ -667,8 +684,8 @@ pub struct CompletionCandidate {
/// Help message with a completion candidate
help: Option<StyledStr>,

/// Whether the completion candidate is visible
visible: bool,
/// Whether the completion candidate is hidden
hidden: bool,
}

impl CompletionCandidate {
Expand All @@ -688,8 +705,8 @@ impl CompletionCandidate {
}

/// Set the visibility of the completion candidate
pub fn visible(mut self, visible: bool) -> Self {
self.visible = visible;
pub fn hide(mut self, hidden: bool) -> Self {
self.hidden = hidden;
self
}

Expand All @@ -704,7 +721,65 @@ impl CompletionCandidate {
}

/// Get the visibility of the completion candidate
pub fn is_visible(&self) -> bool {
self.visible
pub fn is_hide_set(&self) -> bool {
self.hidden
}
}

/// User-provided completion candidates for an argument.
///
/// This is useful when predefined value hints are not enough.
pub trait CustomCompleter: Send + Sync {
/// All potential candidates for an argument.
///
/// See [`CompletionCandidate`] for more information.
fn completions(&self) -> Vec<CompletionCandidate>;
}

impl<F> CustomCompleter for F
where
F: Fn() -> Vec<CompletionCandidate> + Send + Sync,
{
fn completions(&self) -> Vec<CompletionCandidate> {
self()
}
}

/// A wrapper for custom completer
///
/// # Example
///
/// ```rust
/// use clap::Parser;
/// use clap_complete::dynamic::{ArgValueCompleter, CompletionCandidate};
///
/// #[derive(Debug, Parser)]
/// struct Cli {
/// #[arg(long, add = ArgValueCompleter::new(|| { vec![
/// CompletionCandidate::new("foo"),
/// CompletionCandidate::new("bar"),
/// CompletionCandidate::new("baz")] }))]
/// custom: Option<String>,
/// }
///
/// ```
#[derive(Clone)]
pub struct ArgValueCompleter(Arc<dyn CustomCompleter>);

impl ArgValueCompleter {
/// Create a new `ArgValueCompleter` with a custom completer
pub fn new<C: CustomCompleter>(completer: C) -> Self
where
C: 'static + CustomCompleter,
{
Self(Arc::new(completer))
}
}

impl std::fmt::Debug for ArgValueCompleter {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(type_name::<Self>())
}
}

impl ArgExt for ArgValueCompleter {}
32 changes: 32 additions & 0 deletions clap_complete/tests/testsuite/dynamic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -590,6 +591,37 @@ val3
);
}

#[test]
fn suggest_custom_arg_value() {
#[derive(Debug)]
struct MyCustomCompleter {}

impl CustomCompleter for MyCustomCompleter {
fn completions(&self) -> Vec<CompletionCandidate> {
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>(ArgValueCompleter::new(MyCustomCompleter {})),
);

assert_data_eq!(
complete!(cmd, "--custom [TAB]"),
snapbox::str![
"custom1
custom2
custom3"
],
);
}

#[test]
fn suggest_multi_positional() {
let mut cmd = Command::new("dynamic")
Expand Down

0 comments on commit 2690e1b

Please sign in to comment.