Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 23 additions & 7 deletions clap_complete/src/engine/complete.rs
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,12 @@ fn complete_arg(
.get_positionals()
.find(|p| p.get_index() == Some(pos_index))
{
completions.extend(complete_arg_value(arg.to_value(), positional, current_dir));
completions.extend(complete_arg_value(
arg.to_value(),
positional,
current_dir,
0,
));
}
if !is_escaped {
completions.extend(complete_option(arg, cmd, current_dir));
Expand All @@ -171,7 +176,12 @@ fn complete_arg(
.get_positionals()
.find(|p| p.get_index() == Some(pos_index))
{
completions.extend(complete_arg_value(arg.to_value(), positional, current_dir));
completions.extend(complete_arg_value(
arg.to_value(),
positional,
current_dir,
num_arg.saturating_sub(1),
));
if !is_escaped
&& positional
.get_num_args()
Expand All @@ -182,7 +192,12 @@ fn complete_arg(
}
}
ParseState::Opt((opt, count)) => {
completions.extend(complete_arg_value(arg.to_value(), opt, current_dir));
completions.extend(complete_arg_value(
arg.to_value(),
opt,
current_dir,
count.saturating_sub(1),
));
let min = opt.get_num_args().map(|r| r.min_values()).unwrap_or(0);
if count > min {
// Also complete this raw_arg as a positional argument, flags, options and subcommand.
Expand Down Expand Up @@ -270,7 +285,7 @@ fn complete_option(
if let Some(value) = value {
if let Some(arg) = cmd.get_arguments().find(|a| a.get_long() == Some(flag)) {
completions.extend(
complete_arg_value(value.to_str().ok_or(value), arg, current_dir)
complete_arg_value(value.to_str().ok_or(value), arg, current_dir, 0)
.into_iter()
.map(|comp| comp.add_prefix(format!("--{flag}="))),
);
Expand Down Expand Up @@ -305,7 +320,7 @@ fn complete_option(

let value = short.next_value_os().unwrap_or(OsStr::new(""));
completions.extend(
complete_arg_value(value.to_str().ok_or(value), opt, current_dir)
complete_arg_value(value.to_str().ok_or(value), opt, current_dir, 0)
.into_iter()
.map(|comp| {
let sep = if has_equal { "=" } else { "" };
Expand All @@ -328,9 +343,10 @@ fn complete_arg_value(
value: Result<&str, &OsStr>,
arg: &clap::Arg,
current_dir: Option<&std::path::Path>,
arg_index: usize,
) -> Vec<CompletionCandidate> {
let mut values = Vec::new();
debug!("complete_arg_value: arg={arg:?}, value={value:?}");
debug!("complete_arg_value: arg={arg:?}, value={value:?}, arg_index={arg_index:?}");

let (prefix, value) =
rsplit_delimiter(value, arg.get_value_delimiter()).unwrap_or((None, value));
Expand All @@ -341,7 +357,7 @@ fn complete_arg_value(
};

if let Some(completer) = arg.get::<ArgValueCompleter>() {
values.extend(completer.complete(value_os));
values.extend(completer.complete_at(arg_index, 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) {
Expand Down
33 changes: 33 additions & 0 deletions clap_complete/src/engine/custom.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,22 @@ impl ArgValueCompleter {
pub fn complete(&self, current: &OsStr) -> Vec<CompletionCandidate> {
self.0.complete(current)
}

/// Candidates that match `current` at `arg_index` within the current occurrence.
///
/// `arg_index` is the position of the value being completed within an
/// argument's [`num_args`][clap::Arg::num_args] range, starting at `0`.
/// This lets a completer return different candidates for each value of a
/// multi-value argument (for example, a remote name first and a branch
/// name second for `--set-upstream <REMOTE> <BRANCH>`).
///
/// `arg_index` is unaffected by [`value_delimiter`][clap::Arg::value_delimiter]:
/// it counts shell arguments, not delimiter-separated values.
///
/// See [`CompletionCandidate`] for more information.
pub fn complete_at(&self, arg_index: usize, current: &OsStr) -> Vec<CompletionCandidate> {
self.0.complete_at(arg_index, current)
}
}

impl std::fmt::Debug for ArgValueCompleter {
Expand All @@ -76,6 +92,23 @@ pub trait ValueCompleter: Send + Sync {
///
/// See [`CompletionCandidate`] for more information.
fn complete(&self, current: &OsStr) -> Vec<CompletionCandidate>;

/// All potential candidates for the value at `arg_index` within the current occurrence.
///
/// `arg_index` is the position of the value being completed within an
/// argument's [`num_args`][clap::Arg::num_args] range, starting at `0`.
/// This lets a completer return different candidates for each value of a
/// multi-value argument (for example, a remote name first and a branch
/// name second for `--set-upstream <REMOTE> <BRANCH>`).
///
/// `arg_index` is unaffected by [`value_delimiter`][clap::Arg::value_delimiter]:
/// it counts shell arguments, not delimiter-separated values.
///
/// See [`CompletionCandidate`] for more information.
fn complete_at(&self, arg_index: usize, current: &OsStr) -> Vec<CompletionCandidate> {
let _ = arg_index;
self.complete(current)
}
}

impl<F> ValueCompleter for F
Expand Down
69 changes: 69 additions & 0 deletions clap_complete/tests/testsuite/engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -916,6 +916,75 @@ baz
);
}

#[test]
fn suggest_custom_arg_completer_at_index() {
Comment thread
epage marked this conversation as resolved.
struct UpstreamCompleter;

impl clap_complete::engine::ValueCompleter for UpstreamCompleter {
fn complete(&self, _current: &std::ffi::OsStr) -> Vec<CompletionCandidate> {
// Falls back when callers use the index-unaware path.
vec![CompletionCandidate::new("unindexed")]
}

fn complete_at(
&self,
arg_index: usize,
current: &std::ffi::OsStr,
) -> Vec<CompletionCandidate> {
let prefix = current.to_str().unwrap_or("");
match arg_index {
0 => ["origin", "upstream"]
.into_iter()
.filter(|name| name.starts_with(prefix))
.map(CompletionCandidate::new)
.collect(),
1 => ["main", "master", "dev"]
.into_iter()
.filter(|name| name.starts_with(prefix))
.map(CompletionCandidate::new)
.collect(),
_ => Vec::new(),
}
}
}

let mut cmd = Command::new("dynamic").arg(
clap::Arg::new("set-upstream")
.long("set-upstream")
.short('u')
.num_args(2)
.value_names(["REMOTE", "BRANCH"])
.add(ArgValueCompleter::new(UpstreamCompleter)),
);

assert_data_eq!(
complete!(cmd, "--set-upstream [TAB]"),
snapbox::str![[r#"
origin
upstream
"#]]
);
assert_data_eq!(
complete!(cmd, "--set-upstream o[TAB]"),
snapbox::str!["origin"]
);
assert_data_eq!(
complete!(cmd, "--set-upstream origin [TAB]"),
snapbox::str![[r#"
main
master
dev
"#]]
);
assert_data_eq!(
complete!(cmd, "--set-upstream origin m[TAB]"),
snapbox::str![[r#"
main
master
"#]]
);
}

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