mirror of
https://github.com/clap-rs/clap
synced 2025-03-04 23:37:32 +00:00
Merge pull request #5691 from epage/custom
feat(complete): Provide ArgValueCompleter
This commit is contained in:
commit
3266c36abf
10 changed files with 432 additions and 151 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -482,7 +482,6 @@ dependencies = [
|
|||
"completest",
|
||||
"completest-pty",
|
||||
"is_executable",
|
||||
"pathdiff",
|
||||
"shlex",
|
||||
"snapbox",
|
||||
"trycmd",
|
||||
|
|
|
@ -37,7 +37,6 @@ bench = false
|
|||
clap = { path = "../", version = "4.5.15", default-features = false, features = ["std"] }
|
||||
clap_lex = { path = "../clap_lex", version = "0.7.0", optional = true }
|
||||
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 }
|
||||
|
||||
|
@ -57,8 +56,8 @@ required-features = ["unstable-dynamic", "unstable-command"]
|
|||
[features]
|
||||
default = []
|
||||
unstable-doc = ["unstable-dynamic", "unstable-command"] # for docs.rs
|
||||
unstable-dynamic = ["dep:clap_lex", "dep:shlex", "dep:is_executable", "dep:pathdiff", "clap/unstable-ext"]
|
||||
unstable-command = ["unstable-dynamic", "dep:unicode-xid", "clap/derive", "dep:is_executable", "dep:pathdiff", "clap/unstable-ext"]
|
||||
unstable-dynamic = ["dep:clap_lex", "dep:shlex", "dep:is_executable", "clap/unstable-ext"]
|
||||
unstable-command = ["unstable-dynamic", "dep:unicode-xid", "clap/derive", "dep:is_executable", "clap/unstable-ext"]
|
||||
debug = ["clap/debug"]
|
||||
|
||||
[lints]
|
||||
|
|
|
@ -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.
|
||||
|
@ -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.
|
||||
|
|
|
@ -65,3 +65,9 @@ impl CompletionCandidate {
|
|||
self.hidden
|
||||
}
|
||||
}
|
||||
|
||||
impl<S: Into<OsString>> From<S> for CompletionCandidate {
|
||||
fn from(s: S) -> Self {
|
||||
CompletionCandidate::new(s.into())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,7 +3,9 @@ use std::ffi::OsString;
|
|||
|
||||
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
|
||||
|
@ -270,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 {
|
||||
|
@ -289,17 +293,17 @@ fn complete_arg_value(
|
|||
// Should not complete
|
||||
}
|
||||
clap::ValueHint::Unknown | clap::ValueHint::AnyPath => {
|
||||
values.extend(complete_path(value_os, current_dir, |_| true));
|
||||
values.extend(complete_path(value_os, current_dir, &|_| true));
|
||||
}
|
||||
clap::ValueHint::FilePath => {
|
||||
values.extend(complete_path(value_os, current_dir, |p| p.is_file()));
|
||||
values.extend(complete_path(value_os, current_dir, &|p| p.is_file()));
|
||||
}
|
||||
clap::ValueHint::DirPath => {
|
||||
values.extend(complete_path(value_os, current_dir, |p| p.is_dir()));
|
||||
values.extend(complete_path(value_os, current_dir, &|p| p.is_dir()));
|
||||
}
|
||||
clap::ValueHint::ExecutablePath => {
|
||||
use is_executable::IsExecutable;
|
||||
values.extend(complete_path(value_os, current_dir, |p| p.is_executable()));
|
||||
values.extend(complete_path(value_os, current_dir, &|p| p.is_executable()));
|
||||
}
|
||||
clap::ValueHint::CommandName
|
||||
| clap::ValueHint::CommandString
|
||||
|
@ -312,7 +316,7 @@ fn complete_arg_value(
|
|||
}
|
||||
_ => {
|
||||
// Safe-ish fallback
|
||||
values.extend(complete_path(value_os, current_dir, |_| true));
|
||||
values.extend(complete_path(value_os, current_dir, &|_| true));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -341,69 +345,14 @@ fn rsplit_delimiter<'s, 'o>(
|
|||
Some((Some(prefix), Ok(value)))
|
||||
}
|
||||
|
||||
fn complete_path(
|
||||
value_os: &OsStr,
|
||||
current_dir: Option<&std::path::Path>,
|
||||
is_wanted: impl Fn(&std::path::Path) -> bool,
|
||||
) -> Vec<CompletionCandidate> {
|
||||
let mut completions = Vec::new();
|
||||
|
||||
let current_dir = match current_dir {
|
||||
Some(current_dir) => current_dir,
|
||||
None => {
|
||||
// Can't complete without a `current_dir`
|
||||
return Vec::new();
|
||||
}
|
||||
};
|
||||
let (existing, prefix) = value_os
|
||||
.split_once("\\")
|
||||
.unwrap_or((OsStr::new(""), value_os));
|
||||
let root = current_dir.join(existing);
|
||||
debug!("complete_path: root={root:?}, prefix={prefix:?}");
|
||||
let prefix = prefix.to_string_lossy();
|
||||
|
||||
for entry in std::fs::read_dir(&root)
|
||||
.ok()
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.filter_map(Result::ok)
|
||||
{
|
||||
let raw_file_name = entry.file_name();
|
||||
if !raw_file_name.starts_with(&prefix) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if entry.metadata().map(|m| m.is_dir()).unwrap_or(false) {
|
||||
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));
|
||||
} 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
completions
|
||||
}
|
||||
|
||||
fn complete_custom_arg_value(
|
||||
value: &OsStr,
|
||||
completer: &ArgValueCandidates,
|
||||
) -> Vec<CompletionCandidate> {
|
||||
debug!("complete_custom_arg_value: completer={completer:?}, value={value:?}");
|
||||
|
||||
let mut values = Vec::new();
|
||||
let custom_arg_values = completer.candidates();
|
||||
values.extend(custom_arg_values);
|
||||
|
||||
let mut values = completer.candidates();
|
||||
values.retain(|comp| comp.get_content().starts_with(&value.to_string_lossy()));
|
||||
|
||||
values
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
use std::any::type_name;
|
||||
use std::ffi::OsStr;
|
||||
use std::sync::Arc;
|
||||
|
||||
use clap::builder::ArgExt;
|
||||
use clap_lex::OsStrExt as _;
|
||||
|
||||
use super::CompletionCandidate;
|
||||
|
||||
|
@ -68,3 +70,248 @@ where
|
|||
self()
|
||||
}
|
||||
}
|
||||
|
||||
/// 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)
|
||||
}
|
||||
}
|
||||
|
||||
/// Complete a value as a [`std::path::Path`]
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// use clap::Parser;
|
||||
/// use clap_complete::engine::{ArgValueCompleter, PathCompleter};
|
||||
///
|
||||
/// #[derive(Debug, Parser)]
|
||||
/// struct Cli {
|
||||
/// #[arg(long, add = ArgValueCompleter::new(PathCompleter::file()))]
|
||||
/// custom: Option<String>,
|
||||
/// }
|
||||
/// ```
|
||||
pub struct PathCompleter {
|
||||
current_dir: Option<std::path::PathBuf>,
|
||||
filter: Option<Box<dyn Fn(&std::path::Path) -> bool + Send + Sync>>,
|
||||
stdio: bool,
|
||||
}
|
||||
|
||||
impl PathCompleter {
|
||||
/// Any path is allowed
|
||||
pub fn any() -> Self {
|
||||
Self {
|
||||
filter: None,
|
||||
current_dir: None,
|
||||
stdio: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Complete only files
|
||||
pub fn file() -> Self {
|
||||
Self::any().filter(|p| p.is_file())
|
||||
}
|
||||
|
||||
/// Complete only directories
|
||||
pub fn dir() -> Self {
|
||||
Self::any().filter(|p| p.is_dir())
|
||||
}
|
||||
|
||||
/// Include stdio (`-`)
|
||||
pub fn stdio(mut self) -> Self {
|
||||
self.stdio = true;
|
||||
self
|
||||
}
|
||||
|
||||
/// Select which paths should be completed
|
||||
pub fn filter(
|
||||
mut self,
|
||||
filter: impl Fn(&std::path::Path) -> bool + Send + Sync + 'static,
|
||||
) -> Self {
|
||||
self.filter = Some(Box::new(filter));
|
||||
self
|
||||
}
|
||||
|
||||
/// Override [`std::env::current_dir`]
|
||||
pub fn current_dir(mut self, path: impl Into<std::path::PathBuf>) -> Self {
|
||||
self.current_dir = Some(path.into());
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for PathCompleter {
|
||||
fn default() -> Self {
|
||||
Self::any()
|
||||
}
|
||||
}
|
||||
|
||||
impl ValueCompleter for PathCompleter {
|
||||
fn complete(&self, current: &OsStr) -> Vec<CompletionCandidate> {
|
||||
let filter = self.filter.as_deref().unwrap_or(&|_| true);
|
||||
let mut current_dir_actual = None;
|
||||
let current_dir = self.current_dir.as_deref().or_else(|| {
|
||||
current_dir_actual = std::env::current_dir().ok();
|
||||
current_dir_actual.as_deref()
|
||||
});
|
||||
let mut candidates = complete_path(current, current_dir, filter);
|
||||
if self.stdio && current.is_empty() {
|
||||
candidates.push(CompletionCandidate::new("-").help(Some("stdio".into())));
|
||||
}
|
||||
candidates
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn complete_path(
|
||||
value_os: &OsStr,
|
||||
current_dir: Option<&std::path::Path>,
|
||||
is_wanted: &dyn Fn(&std::path::Path) -> bool,
|
||||
) -> Vec<CompletionCandidate> {
|
||||
let mut completions = Vec::new();
|
||||
let mut potential = Vec::new();
|
||||
|
||||
let value_path = std::path::Path::new(value_os);
|
||||
let (prefix, current) = split_file_name(value_path);
|
||||
let current = current.to_string_lossy();
|
||||
let search_root = if prefix.is_absolute() {
|
||||
prefix.to_owned()
|
||||
} else {
|
||||
let current_dir = match current_dir {
|
||||
Some(current_dir) => current_dir,
|
||||
None => {
|
||||
// Can't complete without a `current_dir`
|
||||
return completions;
|
||||
}
|
||||
};
|
||||
current_dir.join(prefix)
|
||||
};
|
||||
debug!("complete_path: search_root={search_root:?}, prefix={prefix:?}");
|
||||
|
||||
for entry in std::fs::read_dir(&search_root)
|
||||
.ok()
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.filter_map(Result::ok)
|
||||
{
|
||||
let raw_file_name = entry.file_name();
|
||||
if !raw_file_name.starts_with(¤t) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if entry.metadata().map(|m| m.is_dir()).unwrap_or(false) {
|
||||
let mut suggestion = prefix.join(raw_file_name);
|
||||
suggestion.push(""); // Ensure trailing `/`
|
||||
let candidate = CompletionCandidate::new(suggestion.as_os_str().to_owned());
|
||||
|
||||
if is_wanted(&entry.path()) {
|
||||
completions.push(candidate);
|
||||
} else {
|
||||
potential.push(candidate);
|
||||
}
|
||||
} else {
|
||||
if is_wanted(&entry.path()) {
|
||||
let suggestion = prefix.join(raw_file_name);
|
||||
let candidate = CompletionCandidate::new(suggestion.as_os_str().to_owned());
|
||||
completions.push(candidate);
|
||||
}
|
||||
}
|
||||
}
|
||||
completions.sort();
|
||||
potential.sort();
|
||||
completions.extend(potential);
|
||||
|
||||
completions
|
||||
}
|
||||
|
||||
fn split_file_name(path: &std::path::Path) -> (&std::path::Path, &OsStr) {
|
||||
// Workaround that `Path::new("name/").file_name()` reports `"name"`
|
||||
if path_has_name(path) {
|
||||
(
|
||||
path.parent().unwrap_or_else(|| std::path::Path::new("")),
|
||||
path.file_name().expect("not called with `..`"),
|
||||
)
|
||||
} else {
|
||||
(path, Default::default())
|
||||
}
|
||||
}
|
||||
|
||||
fn path_has_name(path: &std::path::Path) -> bool {
|
||||
let path_bytes = path.as_os_str().as_encoded_bytes();
|
||||
let Some(trailing) = path_bytes.last() else {
|
||||
return false;
|
||||
};
|
||||
let trailing = *trailing as char;
|
||||
!std::path::is_separator(trailing) && path.file_name().is_some()
|
||||
}
|
||||
|
|
|
@ -9,4 +9,7 @@ mod custom;
|
|||
pub use candidate::CompletionCandidate;
|
||||
pub use complete::complete;
|
||||
pub use custom::ArgValueCandidates;
|
||||
pub use custom::ArgValueCompleter;
|
||||
pub use custom::PathCompleter;
|
||||
pub use custom::ValueCandidates;
|
||||
pub use custom::ValueCompleter;
|
||||
|
|
1
clap_complete/src/env/mod.rs
vendored
1
clap_complete/src/env/mod.rs
vendored
|
@ -20,6 +20,7 @@
|
|||
//! - [`ValueHint`][crate::ValueHint]
|
||||
//! - [`ValueEnum`][clap::ValueEnum]
|
||||
//! - [`ArgValueCandidates`][crate::ArgValueCandidates]
|
||||
//! - [`ArgValueCompleter`][crate::ArgValueCompleter]
|
||||
//!
|
||||
//! To source your completions:
|
||||
//!
|
||||
|
|
|
@ -81,10 +81,14 @@ 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;
|
||||
#[cfg(feature = "unstable-dynamic")]
|
||||
pub use engine::PathCompleter;
|
||||
#[cfg(feature = "unstable-dynamic")]
|
||||
pub use env::CompleteEnv;
|
||||
|
||||
/// Deprecated, see [`aot`]
|
||||
|
|
|
@ -4,7 +4,9 @@ use std::fs;
|
|||
use std::path::Path;
|
||||
|
||||
use clap::{builder::PossibleValue, Command};
|
||||
use clap_complete::engine::{ArgValueCandidates, CompletionCandidate, ValueCandidates};
|
||||
use clap_complete::engine::{
|
||||
ArgValueCandidates, ArgValueCompleter, CompletionCandidate, PathCompleter,
|
||||
};
|
||||
use snapbox::assert_data_eq;
|
||||
|
||||
macro_rules! complete {
|
||||
|
@ -293,18 +295,18 @@ goodbye-world
|
|||
#[test]
|
||||
fn suggest_argument_value() {
|
||||
let mut cmd = Command::new("dynamic")
|
||||
.arg(
|
||||
clap::Arg::new("input")
|
||||
.long("input")
|
||||
.short('i')
|
||||
.value_hint(clap::ValueHint::FilePath),
|
||||
)
|
||||
.arg(
|
||||
clap::Arg::new("format")
|
||||
.long("format")
|
||||
.short('F')
|
||||
.value_parser(["json", "yaml", "toml"]),
|
||||
)
|
||||
.arg(
|
||||
clap::Arg::new("stream")
|
||||
.long("stream")
|
||||
.short('S')
|
||||
.value_parser(["stdout", "stderr"]),
|
||||
)
|
||||
.arg(
|
||||
clap::Arg::new("count")
|
||||
.long("count")
|
||||
|
@ -314,44 +316,6 @@ fn suggest_argument_value() {
|
|||
.arg(clap::Arg::new("positional").value_parser(["pos_a", "pos_b", "pos_c"]))
|
||||
.args_conflicts_with_subcommands(true);
|
||||
|
||||
let testdir = snapbox::dir::DirRoot::mutable_temp().unwrap();
|
||||
let testdir_path = testdir.path().unwrap();
|
||||
|
||||
fs::write(testdir_path.join("a_file"), "").unwrap();
|
||||
fs::write(testdir_path.join("b_file"), "").unwrap();
|
||||
fs::create_dir_all(testdir_path.join("c_dir")).unwrap();
|
||||
fs::create_dir_all(testdir_path.join("d_dir")).unwrap();
|
||||
|
||||
assert_data_eq!(
|
||||
complete!(cmd, "--input [TAB]", current_dir = Some(testdir_path)),
|
||||
snapbox::str![[r#"
|
||||
a_file
|
||||
b_file
|
||||
c_dir/
|
||||
d_dir/
|
||||
"#]],
|
||||
);
|
||||
|
||||
assert_data_eq!(
|
||||
complete!(cmd, "-i [TAB]", current_dir = Some(testdir_path)),
|
||||
snapbox::str![[r#"
|
||||
a_file
|
||||
b_file
|
||||
c_dir/
|
||||
d_dir/
|
||||
"#]],
|
||||
);
|
||||
|
||||
assert_data_eq!(
|
||||
complete!(cmd, "--input a[TAB]", current_dir = Some(testdir_path)),
|
||||
snapbox::str!["a_file"],
|
||||
);
|
||||
|
||||
assert_data_eq!(
|
||||
complete!(cmd, "-i b[TAB]", current_dir = Some(testdir_path)),
|
||||
snapbox::str!["b_file"],
|
||||
);
|
||||
|
||||
assert_data_eq!(
|
||||
complete!(cmd, "--format [TAB]"),
|
||||
snapbox::str![[r#"
|
||||
|
@ -388,14 +352,14 @@ toml
|
|||
);
|
||||
|
||||
assert_data_eq!(
|
||||
complete!(cmd, "--input a_file [TAB]"),
|
||||
complete!(cmd, "--format toml [TAB]"),
|
||||
snapbox::str![[r#"
|
||||
--input
|
||||
--format
|
||||
--stream
|
||||
--count
|
||||
--help Print help
|
||||
-i
|
||||
-F
|
||||
-S
|
||||
-c
|
||||
-h Print help
|
||||
pos_a
|
||||
|
@ -405,39 +369,26 @@ pos_c
|
|||
);
|
||||
|
||||
assert_data_eq!(
|
||||
complete!(cmd, "-ci[TAB]", current_dir = Some(testdir_path)),
|
||||
complete!(cmd, "-cS[TAB]"),
|
||||
snapbox::str![[r#"
|
||||
-cia_file
|
||||
-cib_file
|
||||
-cic_dir/
|
||||
-cid_dir/
|
||||
-cSstdout
|
||||
-cSstderr
|
||||
"#]]
|
||||
);
|
||||
|
||||
assert_data_eq!(
|
||||
complete!(cmd, "-ci=[TAB]", current_dir = Some(testdir_path)),
|
||||
complete!(cmd, "-cS=[TAB]"),
|
||||
snapbox::str![[r#"
|
||||
-ci=a_file
|
||||
-ci=b_file
|
||||
-ci=c_dir/
|
||||
-ci=d_dir/
|
||||
-cS=stdout
|
||||
-cS=stderr
|
||||
"#]]
|
||||
);
|
||||
|
||||
assert_data_eq!(
|
||||
complete!(cmd, "-ci=a[TAB]", current_dir = Some(testdir_path)),
|
||||
snapbox::str!["-ci=a_file"]
|
||||
);
|
||||
assert_data_eq!(complete!(cmd, "-cS=stdo[TAB]"), snapbox::str!["-cS=stdout"]);
|
||||
|
||||
assert_data_eq!(
|
||||
complete!(cmd, "-ciF[TAB]", current_dir = Some(testdir_path)),
|
||||
snapbox::str![]
|
||||
);
|
||||
assert_data_eq!(complete!(cmd, "-cSF[TAB]"), snapbox::str![]);
|
||||
|
||||
assert_data_eq!(
|
||||
complete!(cmd, "-ciF=[TAB]", current_dir = Some(testdir_path)),
|
||||
snapbox::str![]
|
||||
);
|
||||
assert_data_eq!(complete!(cmd, "-cSF=[TAB]"), snapbox::str![]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -592,34 +543,154 @@ val3
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn suggest_custom_arg_value() {
|
||||
#[derive(Debug)]
|
||||
struct MyCustomCompleter {}
|
||||
fn suggest_value_hint_file_path() {
|
||||
let mut cmd = Command::new("dynamic")
|
||||
.arg(
|
||||
clap::Arg::new("input")
|
||||
.long("input")
|
||||
.short('i')
|
||||
.value_hint(clap::ValueHint::FilePath),
|
||||
)
|
||||
.args_conflicts_with_subcommands(true);
|
||||
|
||||
impl ValueCandidates for MyCustomCompleter {
|
||||
fn candidates(&self) -> Vec<CompletionCandidate> {
|
||||
vec![
|
||||
CompletionCandidate::new("custom1"),
|
||||
CompletionCandidate::new("custom2"),
|
||||
CompletionCandidate::new("custom3"),
|
||||
]
|
||||
}
|
||||
let testdir = snapbox::dir::DirRoot::mutable_temp().unwrap();
|
||||
let testdir_path = testdir.path().unwrap();
|
||||
|
||||
fs::write(testdir_path.join("a_file"), "").unwrap();
|
||||
fs::write(testdir_path.join("b_file"), "").unwrap();
|
||||
fs::create_dir_all(testdir_path.join("c_dir")).unwrap();
|
||||
fs::create_dir_all(testdir_path.join("d_dir")).unwrap();
|
||||
|
||||
assert_data_eq!(
|
||||
complete!(cmd, "--input [TAB]", current_dir = Some(testdir_path)),
|
||||
snapbox::str![[r#"
|
||||
a_file
|
||||
b_file
|
||||
c_dir/
|
||||
d_dir/
|
||||
"#]],
|
||||
);
|
||||
|
||||
assert_data_eq!(
|
||||
complete!(cmd, "--input a[TAB]", current_dir = Some(testdir_path)),
|
||||
snapbox::str!["a_file"],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn suggest_value_path_file() {
|
||||
let testdir = snapbox::dir::DirRoot::mutable_temp().unwrap();
|
||||
let testdir_path = testdir.path().unwrap();
|
||||
fs::write(testdir_path.join("a_file"), "").unwrap();
|
||||
fs::write(testdir_path.join("b_file"), "").unwrap();
|
||||
fs::create_dir_all(testdir_path.join("c_dir")).unwrap();
|
||||
fs::create_dir_all(testdir_path.join("d_dir")).unwrap();
|
||||
|
||||
let mut cmd = Command::new("dynamic")
|
||||
.arg(
|
||||
clap::Arg::new("input")
|
||||
.long("input")
|
||||
.short('i')
|
||||
.add(ArgValueCompleter::new(
|
||||
PathCompleter::file()
|
||||
.stdio()
|
||||
.current_dir(testdir_path.to_owned()),
|
||||
)),
|
||||
)
|
||||
.args_conflicts_with_subcommands(true);
|
||||
|
||||
assert_data_eq!(
|
||||
complete!(cmd, "--input [TAB]", current_dir = Some(testdir_path)),
|
||||
snapbox::str![[r#"
|
||||
a_file
|
||||
b_file
|
||||
c_dir/
|
||||
d_dir/
|
||||
- stdio
|
||||
"#]],
|
||||
);
|
||||
|
||||
assert_data_eq!(
|
||||
complete!(cmd, "--input a[TAB]", current_dir = Some(testdir_path)),
|
||||
snapbox::str!["a_file"],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn suggest_custom_arg_value() {
|
||||
fn custom_completer() -> Vec<CompletionCandidate> {
|
||||
vec![
|
||||
CompletionCandidate::new("foo"),
|
||||
CompletionCandidate::new("bar"),
|
||||
CompletionCandidate::new("baz"),
|
||||
]
|
||||
}
|
||||
|
||||
let mut cmd = Command::new("dynamic").arg(
|
||||
clap::Arg::new("custom")
|
||||
.long("custom")
|
||||
.add::<ArgValueCandidates>(ArgValueCandidates::new(MyCustomCompleter {})),
|
||||
.add(ArgValueCandidates::new(custom_completer)),
|
||||
);
|
||||
|
||||
assert_data_eq!(
|
||||
complete!(cmd, "--custom [TAB]"),
|
||||
snapbox::str![[r#"
|
||||
custom1
|
||||
custom2
|
||||
custom3
|
||||
foo
|
||||
bar
|
||||
baz
|
||||
"#]],
|
||||
);
|
||||
|
||||
assert_data_eq!(
|
||||
complete!(cmd, "--custom b[TAB]"),
|
||||
snapbox::str![[r#"
|
||||
bar
|
||||
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]
|
||||
|
|
Loading…
Add table
Reference in a new issue