diff --git a/clap_complete/src/engine/complete.rs b/clap_complete/src/engine/complete.rs index c5eff4f90cb..28067b96cd9 100644 --- a/clap_complete/src/engine/complete.rs +++ b/clap_complete/src/engine/complete.rs @@ -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)); @@ -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() @@ -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. @@ -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}="))), ); @@ -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 { "" }; @@ -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 { 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)); @@ -341,7 +357,7 @@ fn complete_arg_value( }; if let Some(completer) = arg.get::() { - values.extend(completer.complete(value_os)); + values.extend(completer.complete_at(arg_index, value_os)); } else if let Some(completer) = arg.get::() { values.extend(complete_custom_arg_value(value_os, completer)); } else if let Some(possible_values) = possible_values(arg) { diff --git a/clap_complete/src/engine/custom.rs b/clap_complete/src/engine/custom.rs index 52f8117b92c..adea9cd1e8d 100644 --- a/clap_complete/src/engine/custom.rs +++ b/clap_complete/src/engine/custom.rs @@ -58,6 +58,22 @@ impl ArgValueCompleter { pub fn complete(&self, current: &OsStr) -> Vec { 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 `). + /// + /// `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 { + self.0.complete_at(arg_index, current) + } } impl std::fmt::Debug for ArgValueCompleter { @@ -76,6 +92,23 @@ pub trait ValueCompleter: Send + Sync { /// /// See [`CompletionCandidate`] for more information. fn complete(&self, current: &OsStr) -> Vec; + + /// 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 `). + /// + /// `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 { + let _ = arg_index; + self.complete(current) + } } impl ValueCompleter for F diff --git a/clap_complete/tests/testsuite/engine.rs b/clap_complete/tests/testsuite/engine.rs index 5e04dcc7004..c611373b829 100644 --- a/clap_complete/tests/testsuite/engine.rs +++ b/clap_complete/tests/testsuite/engine.rs @@ -916,6 +916,75 @@ baz ); } +#[test] +fn suggest_custom_arg_completer_at_index() { + struct UpstreamCompleter; + + impl clap_complete::engine::ValueCompleter for UpstreamCompleter { + fn complete(&self, _current: &std::ffi::OsStr) -> Vec { + // 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 { + 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")