diff --git a/cli/src/cli/complete_word.rs b/cli/src/cli/complete_word.rs index 32718443..f1c1cbf9 100644 --- a/cli/src/cli/complete_word.rs +++ b/cli/src/cli/complete_word.rs @@ -12,6 +12,7 @@ use std::sync::LazyLock; use xx::process::check_status; use xx::{regex, XXError, XXResult}; +use usage::parse::{ParseOutput, ParseValue}; use usage::{Spec, SpecArg, SpecCommand, SpecComplete, SpecFlag}; use crate::cli::generate; @@ -143,7 +144,7 @@ impl CompleteWord { self.complete_arg(&ctx, spec, &parsed.cmd, arg, &ctoken)? } else { let mut choices = vec![]; - if let Some(arg) = parsed.cmd.args.get(parsed.args.len()) { + if let Some(arg) = next_arg_for_completion(&parsed) { has_explicit_choices = arg.choices.is_some(); choices.extend(self.complete_arg(&ctx, spec, &parsed.cmd, arg, &ctoken)?); } @@ -374,6 +375,23 @@ impl CompleteWord { } } +fn next_arg_for_completion(parsed: &ParseOutput) -> Option<&SpecArg> { + parsed.cmd.args.iter().find(|arg| { + let parsed_value = parsed + .args + .iter() + .find_map(|(parsed_arg, value)| (parsed_arg.name == arg.name).then_some(value)); + + match parsed_value { + Some(ParseValue::MultiString(values)) if arg.var => { + values.len() < arg.var_max.unwrap_or(usize::MAX) + } + Some(_) => false, + None => true, + } + }) +} + /// Wrap a completion value in single quotes if any character would otherwise /// be interpreted by the shell. The result is meant to be inserted by /// `compadd -Q` verbatim, so the user sees consistent single-quote quoting diff --git a/cli/tests/complete_word.rs b/cli/tests/complete_word.rs index 43c2225a..172cdb70 100644 --- a/cli/tests/complete_word.rs +++ b/cli/tests/complete_word.rs @@ -15,6 +15,31 @@ fn complete_word_completer() { .stdout("plugin-1\tdesc\nplugin-2\tdesc\nplugin-3\tdesc\n"); } +#[test] +fn complete_word_variadic_arg_reuses_completer() { + assert_cmd("variadic-completion.usage.kdl", &["--", "variadic", ""]).stdout("foo\nbar\n"); + assert_cmd( + "variadic-completion.usage.kdl", + &["--", "variadic", "foo", ""], + ) + .stdout("foo\nbar\n"); +} + +#[test] +fn complete_word_variadic_arg_respects_var_max() { + assert_cmd("variadic-completion.usage.kdl", &["--", "bounded", ""]).stdout("foo\nbar\n"); + assert_cmd( + "variadic-completion.usage.kdl", + &["--", "bounded", "foo", ""], + ) + .stdout("foo\nbar\n"); + assert_cmd( + "variadic-completion.usage.kdl", + &["--", "bounded", "foo", "bar", ""], + ) + .stdout(contains("Cargo.toml")); +} + #[test] fn complete_word_subcommands() { assert_cmd("basic.usage.kdl", &["plugins", "install"]).stdout(contains("install")); diff --git a/examples/variadic-completion.usage.kdl b/examples/variadic-completion.usage.kdl new file mode 100644 index 00000000..d2927ac0 --- /dev/null +++ b/examples/variadic-completion.usage.kdl @@ -0,0 +1,9 @@ +cmd variadic { + arg "[ITEM]…" var=#true required=#false +} + +cmd bounded { + arg "[ITEM]…" var=#true var_max=2 required=#false +} + +complete "item" run="printf 'foo\nbar\n'"