Skip to content

Commit

Permalink
feat(clap_complete): Support to complete custom value of argument
Browse files Browse the repository at this point in the history
  • Loading branch information
shannmu committed Aug 7, 2024
1 parent c5588ab commit 0ff617a
Show file tree
Hide file tree
Showing 4 changed files with 126 additions and 4 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion clap_complete/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"] }
Expand All @@ -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"
Expand All @@ -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]
Expand Down
97 changes: 96 additions & 1 deletion 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::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 _;

Expand Down Expand Up @@ -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
}

Expand Down Expand Up @@ -449,6 +458,20 @@ fn complete_path(
completions
}

fn complete_custom_arg_value(value: &OsStr, arg: &clap::Arg) -> Vec<CompletionCandidate> {
let mut values = Vec::new();
debug!("complete_custom_arg_value: arg={arg:?}, value={value:?}");

if let Some(completer) = arg.get::<ArgValueCompleter>() {
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<CompletionCandidate> {
debug!(
"complete_subcommand: cmd={:?}, value={:?}",
Expand Down Expand Up @@ -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<CompletionCandidate>;
}

/// 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<CompletionCandidate> {
/// 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<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 Deref for ArgValueCompleter {
type Target = dyn CustomCompleter;

fn deref(&self) -> &Self::Target {
&*self.0
}
}

impl ArgExt for ArgValueCompleter {}
28 changes: 26 additions & 2 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 @@ -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<CompletionCandidate> {
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>(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]
Expand Down

0 comments on commit 0ff617a

Please sign in to comment.