Skip to content

Commit

Permalink
feat(complete): Add ArgValueCompleter
Browse files Browse the repository at this point in the history
  • Loading branch information
epage committed Aug 21, 2024
1 parent 47aedc6 commit 82a360a
Show file tree
Hide file tree
Showing 7 changed files with 134 additions and 2 deletions.
2 changes: 2 additions & 0 deletions clap_complete/src/command/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ pub use shells::*;
/// - [`ValueHint`][crate::ValueHint]
/// - [`ValueEnum`][clap::ValueEnum]
/// - [`ArgValueCandidates`][crate::ArgValueCandidates]
/// - [`ArgValueCompleter`][crate::ArgValueCompleter]
///
/// **Warning:** `stdout` should not be written to before [`CompleteCommand::complete`] has had a
/// chance to run.
Expand Down Expand Up @@ -122,6 +123,7 @@ impl CompleteCommand {
/// - [`ValueHint`][crate::ValueHint]
/// - [`ValueEnum`][clap::ValueEnum]
/// - [`ArgValueCandidates`][crate::ArgValueCandidates]
/// - [`ArgValueCompleter`][crate::ArgValueCompleter]
///
/// **Warning:** `stdout` should not be written to before [`CompleteArgs::complete`] has had a
/// chance to run.
Expand Down
5 changes: 4 additions & 1 deletion clap_complete/src/engine/complete.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use clap_lex::OsStrExt as _;

use super::custom::complete_path;
use super::ArgValueCandidates;
use super::ArgValueCompleter;
use super::CompletionCandidate;

/// Complete the given command, shell-agnostic
Expand Down Expand Up @@ -271,7 +272,9 @@ fn complete_arg_value(
Err(value_os) => value_os,
};

if let Some(completer) = arg.get::<ArgValueCandidates>() {
if let Some(completer) = arg.get::<ArgValueCompleter>() {
values.extend(completer.complete(value_os));
} else if let Some(completer) = arg.get::<ArgValueCandidates>() {
values.extend(complete_custom_arg_value(value_os, completer));
} else if let Some(possible_values) = possible_values(arg) {
if let Ok(value) = value {
Expand Down
79 changes: 79 additions & 0 deletions clap_complete/src/engine/custom.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,85 @@ where
}
}

/// Extend [`Arg`][clap::Arg] with a completer
///
/// # Example
///
/// ```rust
/// use clap::Parser;
/// use clap_complete::engine::{ArgValueCompleter, CompletionCandidate};
///
/// fn custom_completer(current: &std::ffi::OsStr) -> Vec<CompletionCandidate> {
/// let mut completions = vec![];
/// let Some(current) = current.to_str() else {
/// return completions;
/// };
///
/// if "foo".starts_with(current) {
/// completions.push(CompletionCandidate::new("foo"));
/// }
/// if "bar".starts_with(current) {
/// completions.push(CompletionCandidate::new("bar"));
/// }
/// if "baz".starts_with(current) {
/// completions.push(CompletionCandidate::new("baz"));
/// }
/// completions
/// }
///
/// #[derive(Debug, Parser)]
/// struct Cli {
/// #[arg(long, add = ArgValueCompleter::new(custom_completer))]
/// custom: Option<String>,
/// }
/// ```
#[derive(Clone)]
pub struct ArgValueCompleter(Arc<dyn ValueCompleter>);

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

/// Candidates that match `current`
///
/// See [`CompletionCandidate`] for more information.
pub fn complete(&self, current: &OsStr) -> Vec<CompletionCandidate> {
self.0.complete(current)
}
}

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 {}

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

impl<F> ValueCompleter for F
where
F: Fn(&OsStr) -> Vec<CompletionCandidate> + Send + Sync,
{
fn complete(&self, current: &OsStr) -> Vec<CompletionCandidate> {
self(current)
}
}

pub(crate) fn complete_path(
value_os: &OsStr,
current_dir: Option<&std::path::Path>,
Expand Down
2 changes: 2 additions & 0 deletions clap_complete/src/engine/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,6 @@ mod custom;
pub use candidate::CompletionCandidate;
pub use complete::complete;
pub use custom::ArgValueCandidates;
pub use custom::ArgValueCompleter;
pub use custom::ValueCandidates;
pub use custom::ValueCompleter;
1 change: 1 addition & 0 deletions clap_complete/src/env/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
//! - [`ValueHint`][crate::ValueHint]
//! - [`ValueEnum`][clap::ValueEnum]
//! - [`ArgValueCandidates`][crate::ArgValueCandidates]
//! - [`ArgValueCompleter`][crate::ArgValueCompleter]
//!
//! To source your completions:
//!
Expand Down
2 changes: 2 additions & 0 deletions clap_complete/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,8 @@ pub use command::CompleteCommand;
#[doc(inline)]
#[cfg(feature = "unstable-dynamic")]
pub use engine::ArgValueCandidates;
#[cfg(feature = "unstable-dynamic")]
pub use engine::ArgValueCompleter;
#[doc(inline)]
#[cfg(feature = "unstable-dynamic")]
pub use engine::CompletionCandidate;
Expand Down
45 changes: 44 additions & 1 deletion clap_complete/tests/testsuite/engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use std::fs;
use std::path::Path;

use clap::{builder::PossibleValue, Command};
use clap_complete::engine::{ArgValueCandidates, CompletionCandidate};
use clap_complete::engine::{ArgValueCandidates, ArgValueCompleter, CompletionCandidate};
use snapbox::assert_data_eq;

macro_rules! complete {
Expand Down Expand Up @@ -609,6 +609,49 @@ baz
);
}

#[test]
fn suggest_custom_arg_completer() {
fn custom_completer(current: &std::ffi::OsStr) -> Vec<CompletionCandidate> {
let mut completions = vec![];
let Some(current) = current.to_str() else {
return completions;
};

if "foo".starts_with(current) {
completions.push(CompletionCandidate::new("foo"));
}
if "bar".starts_with(current) {
completions.push(CompletionCandidate::new("bar"));
}
if "baz".starts_with(current) {
completions.push(CompletionCandidate::new("baz"));
}
completions
}

let mut cmd = Command::new("dynamic").arg(
clap::Arg::new("custom")
.long("custom")
.add(ArgValueCompleter::new(custom_completer)),
);

assert_data_eq!(
complete!(cmd, "--custom [TAB]"),
snapbox::str![[r#"
foo
bar
baz
"#]]
);
assert_data_eq!(
complete!(cmd, "--custom b[TAB]"),
snapbox::str![[r#"
bar
baz
"#]]
);
}

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

0 comments on commit 82a360a

Please sign in to comment.