From 7881839c7f9c1a2b3ebab6766820a3bb6c61a9d1 Mon Sep 17 00:00:00 2001 From: Jai A P Date: Tue, 28 Jul 2026 17:01:11 +0530 Subject: [PATCH 1/2] fix: complete repeated variadic args --- cli/src/cli/complete_word.rs | 19 ++++++++++++++++++- cli/tests/complete_word.rs | 21 +++++++++++++++++++++ examples/variadic-completion.usage.kdl | 9 +++++++++ 3 files changed, 48 insertions(+), 1 deletion(-) create mode 100644 examples/variadic-completion.usage.kdl diff --git a/cli/src/cli/complete_word.rs b/cli/src/cli/complete_word.rs index 32718443..319161b9 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,22 @@ 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..c3c6769d 100644 --- a/cli/tests/complete_word.rs +++ b/cli/tests/complete_word.rs @@ -15,6 +15,27 @@ 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'" From 3cefa0c1a5f842b31873ec0ed71bdbdfc604399f Mon Sep 17 00:00:00 2001 From: Jai A P Date: Tue, 28 Jul 2026 17:28:21 +0530 Subject: [PATCH 2/2] chore: run render --- cli/src/cli/complete_word.rs | 7 ++++--- cli/tests/complete_word.rs | 20 ++++++++++++-------- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/cli/src/cli/complete_word.rs b/cli/src/cli/complete_word.rs index 319161b9..f1c1cbf9 100644 --- a/cli/src/cli/complete_word.rs +++ b/cli/src/cli/complete_word.rs @@ -377,9 +377,10 @@ 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) - }); + 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 => { diff --git a/cli/tests/complete_word.rs b/cli/tests/complete_word.rs index c3c6769d..172cdb70 100644 --- a/cli/tests/complete_word.rs +++ b/cli/tests/complete_word.rs @@ -17,18 +17,22 @@ fn complete_word_completer() { #[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"); + 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", ""]).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", ""],