From 5d8c84b83e9e5850d1e34b4bac516c1627c55691 Mon Sep 17 00:00:00 2001 From: shannmu Date: Wed, 24 Jul 2024 19:45:46 +0800 Subject: [PATCH 1/4] test(clap_complete): Add test case for multi-values comlpetion after flags --- clap_complete/tests/testsuite/dynamic.rs | 151 +++++++++++++++++++++++ 1 file changed, 151 insertions(+) diff --git a/clap_complete/tests/testsuite/dynamic.rs b/clap_complete/tests/testsuite/dynamic.rs index 0264aea38ab..4b5da1b32c6 100644 --- a/clap_complete/tests/testsuite/dynamic.rs +++ b/clap_complete/tests/testsuite/dynamic.rs @@ -439,6 +439,157 @@ pos_c" ); } +#[test] +fn suggest_argument_multi_values() { + let mut cmd = Command::new("dynamic") + .arg( + clap::Arg::new("certain-num") + .long("certain-num") + .short('Y') + .value_parser(["val1", "val2", "val3"]) + .num_args(3), + ) + .arg( + clap::Arg::new("uncertain-num") + .long("uncertain-num") + .short('N') + .value_parser(["val1", "val2", "val3"]) + .num_args(1..=3), + ); + + assert_data_eq!( + complete!(cmd, "--certain-num [TAB]"), + snapbox::str![ + "val1 +val2 +val3" + ] + ); + + assert_data_eq!( + complete!(cmd, "--certain-num val1 [TAB]"), + snapbox::str![ + "--certain-num +--uncertain-num +--help\tPrint help +-Y +-N +-h\tPrint help" + ] + ); + + assert_data_eq!( + complete!(cmd, "--certain-num val1 val2 val3 [TAB]"), + snapbox::str![ + "--certain-num +--uncertain-num +--help\tPrint help +-Y +-N +-h\tPrint help" + ] + ); + + assert_data_eq!( + complete!(cmd, "--uncertain-num [TAB]"), + snapbox::str![ + "val1 +val2 +val3" + ] + ); + + assert_data_eq!( + complete!(cmd, "--uncertain-num val1 [TAB]"), + snapbox::str![ + "--certain-num +--uncertain-num +--help\tPrint help +-Y +-N +-h\tPrint help" + ] + ); + + assert_data_eq!( + complete!(cmd, "--uncertain-num val1 val2 val3 [TAB]"), + snapbox::str![ + "--certain-num +--uncertain-num +--help\tPrint help +-Y +-N +-h\tPrint help" + ] + ); + + assert_data_eq!( + complete!(cmd, "-Y [TAB]"), + snapbox::str![ + "val1 +val2 +val3" + ] + ); + + assert_data_eq!( + complete!(cmd, "-Y val1 [TAB]"), + snapbox::str![ + "--certain-num +--uncertain-num +--help\tPrint help +-Y +-N +-h\tPrint help" + ] + ); + + assert_data_eq!( + complete!(cmd, "-Y val1 val2 val3 [TAB]"), + snapbox::str![ + "--certain-num +--uncertain-num +--help\tPrint help +-Y +-N +-h\tPrint help" + ] + ); + + assert_data_eq!( + complete!(cmd, "-N [TAB]"), + snapbox::str![ + "val1 +val2 +val3" + ] + ); + + assert_data_eq!( + complete!(cmd, "-N val1 [TAB]"), + snapbox::str![ + "--certain-num +--uncertain-num +--help\tPrint help +-Y +-N +-h\tPrint help" + ] + ); + + assert_data_eq!( + complete!(cmd, "-N val1 val2 val3 [TAB]"), + snapbox::str![ + "--certain-num +--uncertain-num +--help\tPrint help +-Y +-N +-h\tPrint help" + ] + ); +} + fn complete(cmd: &mut Command, args: impl AsRef, current_dir: Option<&Path>) -> String { let input = args.as_ref(); let mut args = vec![std::ffi::OsString::from(cmd.get_name())]; From 75a45e5aa0e910bdabcdc1624ac30cc0cf2bca84 Mon Sep 17 00:00:00 2001 From: shannmu Date: Thu, 25 Jul 2024 00:21:06 +0800 Subject: [PATCH 2/4] feat(clap_complete): Support multiple values after flags in native completions --- clap_complete/src/dynamic/completer.rs | 35 +++++++++++++++++++----- clap_complete/tests/testsuite/dynamic.rs | 28 +++++++++---------- 2 files changed, 42 insertions(+), 21 deletions(-) diff --git a/clap_complete/src/dynamic/completer.rs b/clap_complete/src/dynamic/completer.rs index 617d73d269f..1c711e810fd 100644 --- a/clap_complete/src/dynamic/completer.rs +++ b/clap_complete/src/dynamic/completer.rs @@ -92,7 +92,7 @@ pub fn complete( if value.is_some() { ParseState::ValueDone } else { - ParseState::Opt(opt.unwrap()) + ParseState::Opt((opt.unwrap(), 1)) } } Some(clap::ArgAction::SetTrue) | Some(clap::ArgAction::SetFalse) => { @@ -115,7 +115,7 @@ pub fn complete( Some(opt) => { state = match short.next_value_os() { Some(_) => ParseState::ValueDone, - None => ParseState::Opt(opt), + None => ParseState::Opt((opt, 1)), }; } None => { @@ -128,9 +128,19 @@ pub fn complete( pos_index += 1; state = ParseState::ValueDone; } - ParseState::Opt(_) => { - state = ParseState::ValueDone; - } + ParseState::Opt((ref opt, count)) => match opt.get_num_args() { + Some(range) => { + let max = range.max_values(); + if count < max { + state = ParseState::Opt((opt.clone(), count + 1)); + } else { + state = ParseState::ValueDone; + } + } + None => { + state = ParseState::ValueDone; + } + }, } } } @@ -150,7 +160,7 @@ enum ParseState<'a> { Pos(usize), /// Parsing a optional flag argument - Opt(&'a clap::Arg), + Opt((&'a clap::Arg, usize)), } fn complete_arg( @@ -298,8 +308,19 @@ fn complete_arg( completions.extend(complete_arg_value(arg.to_value(), positional, current_dir)); } } - ParseState::Opt(opt) => { + ParseState::Opt((opt, count)) => { completions.extend(complete_arg_value(arg.to_value(), opt, current_dir)); + 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. + completions.extend(complete_arg( + arg, + cmd, + current_dir, + pos_index, + ParseState::ValueDone, + )?); + } } } if completions.iter().any(|a| a.is_visible()) { diff --git a/clap_complete/tests/testsuite/dynamic.rs b/clap_complete/tests/testsuite/dynamic.rs index 4b5da1b32c6..235ed46e758 100644 --- a/clap_complete/tests/testsuite/dynamic.rs +++ b/clap_complete/tests/testsuite/dynamic.rs @@ -469,12 +469,9 @@ val3" assert_data_eq!( complete!(cmd, "--certain-num val1 [TAB]"), snapbox::str![ - "--certain-num ---uncertain-num ---help\tPrint help --Y --N --h\tPrint help" + "val1 +val2 +val3" ] ); @@ -502,7 +499,10 @@ val3" assert_data_eq!( complete!(cmd, "--uncertain-num val1 [TAB]"), snapbox::str![ - "--certain-num + "val1 +val2 +val3 +--certain-num --uncertain-num --help\tPrint help -Y @@ -535,12 +535,9 @@ val3" assert_data_eq!( complete!(cmd, "-Y val1 [TAB]"), snapbox::str![ - "--certain-num ---uncertain-num ---help\tPrint help --Y --N --h\tPrint help" + "val1 +val2 +val3" ] ); @@ -568,7 +565,10 @@ val3" assert_data_eq!( complete!(cmd, "-N val1 [TAB]"), snapbox::str![ - "--certain-num + "val1 +val2 +val3 +--certain-num --uncertain-num --help\tPrint help -Y From 3f2466b2f6fadddd73054736cf88003926f80964 Mon Sep 17 00:00:00 2001 From: shanmu Date: Thu, 25 Jul 2024 02:23:12 +0800 Subject: [PATCH 3/4] test(clap_complete): Add test case for multi-values of positional argument with `num_args(N)` --- clap_complete/tests/testsuite/dynamic.rs | 90 ++++++++++++++++++++++++ 1 file changed, 90 insertions(+) diff --git a/clap_complete/tests/testsuite/dynamic.rs b/clap_complete/tests/testsuite/dynamic.rs index 235ed46e758..a843b78b320 100644 --- a/clap_complete/tests/testsuite/dynamic.rs +++ b/clap_complete/tests/testsuite/dynamic.rs @@ -590,6 +590,96 @@ val3 ); } +#[test] +fn suggest_multi_positional() { + let mut cmd = Command::new("dynamic") + .arg( + clap::Arg::new("positional") + .value_parser(["pos_1, pos_2, pos_3"]) + .index(1), + ) + .arg( + clap::Arg::new("positional-2") + .value_parser(["pos_a", "pos_b", "pos_c"]) + .index(2) + .num_args(3), + ) + .arg( + clap::Arg::new("--format") + .long("format") + .short('F') + .value_parser(["json", "yaml", "toml"]), + ); + + assert_data_eq!( + complete!(cmd, "pos_1 pos_a [TAB]"), + snapbox::str![ + "--format +--help\tPrint help +-F +-h\tPrint help" + ] + ); + + assert_data_eq!( + complete!(cmd, "pos_1 pos_a pos_b [TAB]"), + snapbox::str![ + "--format +--help\tPrint help +-F +-h\tPrint help" + ] + ); + + assert_data_eq!( + complete!(cmd, "--format json pos_1 [TAB]"), + snapbox::str![ + "--format +--help\tPrint help +-F +-h\tPrint help +pos_a +pos_b +pos_c" + ] + ); + + assert_data_eq!( + complete!(cmd, "--format json pos_1 pos_a [TAB]"), + snapbox::str![ + "--format +--help\tPrint help +-F +-h\tPrint help" + ] + ); + + assert_data_eq!( + complete!(cmd, "--format json pos_1 pos_a pos_b pos_c [TAB]"), + snapbox::str![ + "--format +--help\tPrint help +-F +-h\tPrint help" + ] + ); + + assert_data_eq!( + complete!(cmd, "--format json -- pos_1 pos_a [TAB]"), + snapbox::str![""] + ); + + assert_data_eq!( + complete!(cmd, "--format json -- pos_1 pos_a pos_b [TAB]"), + snapbox::str![""] + ); + + assert_data_eq!( + complete!(cmd, "--format json -- pos_1 pos_a pos_b pos_c [TAB]"), + snapbox::str![""] + ); +} + fn complete(cmd: &mut Command, args: impl AsRef, current_dir: Option<&Path>) -> String { let input = args.as_ref(); let mut args = vec![std::ffi::OsString::from(cmd.get_name())]; From f0bd47506f0bca749c41c3af6f3ca63589992607 Mon Sep 17 00:00:00 2001 From: shannmu Date: Fri, 26 Jul 2024 20:08:16 +0800 Subject: [PATCH 4/4] feat(clap_complete): Support multi-values of positional argument with `num_arg(N)` --- clap_complete/src/dynamic/completer.rs | 68 +++++++++++++++++++++--- clap_complete/tests/testsuite/dynamic.rs | 33 +++++++----- 2 files changed, 79 insertions(+), 22 deletions(-) diff --git a/clap_complete/src/dynamic/completer.rs b/clap_complete/src/dynamic/completer.rs index 1c711e810fd..7a9015399a8 100644 --- a/clap_complete/src/dynamic/completer.rs +++ b/clap_complete/src/dynamic/completer.rs @@ -1,3 +1,4 @@ +use core::num; use std::ffi::OsStr; use std::ffi::OsString; @@ -71,8 +72,7 @@ pub fn complete( } if is_escaped { - pos_index += 1; - state = ParseState::Pos(pos_index); + (state, pos_index) = parse_positional(current_cmd, pos_index, is_escaped, state); } else if arg.is_escape() { is_escaped = true; state = ParseState::ValueDone; @@ -124,10 +124,11 @@ pub fn complete( } } else { match state { - ParseState::ValueDone | ParseState::Pos(_) => { - pos_index += 1; - state = ParseState::ValueDone; + ParseState::ValueDone | ParseState::Pos(..) => { + (state, pos_index) = + parse_positional(current_cmd, pos_index, is_escaped, state); } + ParseState::Opt((ref opt, count)) => match opt.get_num_args() { Some(range) => { let max = range.max_values(); @@ -156,8 +157,8 @@ enum ParseState<'a> { /// Parsing a value done, there is no state to record. ValueDone, - /// Parsing a positional argument after `--` - Pos(usize), + /// Parsing a positional argument after `--`. Pos(pos_index, takes_num_args) + Pos((usize, usize)), /// Parsing a optional flag argument Opt((&'a clap::Arg, usize)), @@ -300,7 +301,7 @@ fn complete_arg( completions.extend(complete_subcommand(value, cmd)); } } - ParseState::Pos(_) => { + ParseState::Pos(..) => { if let Some(positional) = cmd .get_positionals() .find(|p| p.get_index() == Some(pos_index)) @@ -603,6 +604,57 @@ fn parse_shortflags<'c, 's>( (leading_flags, takes_value_opt, short) } +/// Parse the positional arguments. Return the new state and the new positional index. +fn parse_positional<'a>( + cmd: &clap::Command, + pos_index: usize, + is_escaped: bool, + state: ParseState<'a>, +) -> (ParseState<'a>, usize) { + let pos_arg = cmd + .get_positionals() + .find(|p| p.get_index() == Some(pos_index)); + let num_args = pos_arg + .and_then(|a| a.get_num_args().and_then(|r| Some(r.max_values()))) + .unwrap_or(1); + + let update_state_with_new_positional = |pos_index| -> (ParseState<'a>, usize) { + if num_args > 1 { + (ParseState::Pos((pos_index, 1)), pos_index) + } else { + if is_escaped { + (ParseState::Pos((pos_index, 1)), pos_index + 1) + } else { + (ParseState::ValueDone, pos_index + 1) + } + } + }; + match state { + ParseState::ValueDone => { + update_state_with_new_positional(pos_index) + }, + ParseState::Pos((prev_pos_index, num_arg)) => { + if prev_pos_index == pos_index { + if num_arg + 1 < num_args { + (ParseState::Pos((pos_index, num_arg + 1)), pos_index) + } else { + if is_escaped { + (ParseState::Pos((pos_index, 1)), pos_index + 1) + } else { + (ParseState::ValueDone, pos_index + 1) + } + } + } else { + update_state_with_new_positional(pos_index) + } + } + ParseState::Opt(..) => unreachable!( + "This branch won't be hit, + because ParseState::Opt should not be seen as a positional argument and passed to this function." + ), + } +} + /// A completion candidate definition /// /// This makes it easier to add more fields to completion candidate, diff --git a/clap_complete/tests/testsuite/dynamic.rs b/clap_complete/tests/testsuite/dynamic.rs index a843b78b320..0c0fe22b720 100644 --- a/clap_complete/tests/testsuite/dynamic.rs +++ b/clap_complete/tests/testsuite/dynamic.rs @@ -614,20 +614,18 @@ fn suggest_multi_positional() { assert_data_eq!( complete!(cmd, "pos_1 pos_a [TAB]"), snapbox::str![ - "--format ---help\tPrint help --F --h\tPrint help" + "pos_a +pos_b +pos_c" ] ); assert_data_eq!( complete!(cmd, "pos_1 pos_a pos_b [TAB]"), snapbox::str![ - "--format ---help\tPrint help --F --h\tPrint help" + "pos_a +pos_b +pos_c" ] ); @@ -647,10 +645,9 @@ pos_c" assert_data_eq!( complete!(cmd, "--format json pos_1 pos_a [TAB]"), snapbox::str![ - "--format ---help\tPrint help --F --h\tPrint help" + "pos_a +pos_b +pos_c" ] ); @@ -666,12 +663,20 @@ pos_c" assert_data_eq!( complete!(cmd, "--format json -- pos_1 pos_a [TAB]"), - snapbox::str![""] + snapbox::str![ + "pos_a +pos_b +pos_c" + ] ); assert_data_eq!( complete!(cmd, "--format json -- pos_1 pos_a pos_b [TAB]"), - snapbox::str![""] + snapbox::str![ + "pos_a +pos_b +pos_c" + ] ); assert_data_eq!(