Skip to content

Commit ca2265c

Browse files
authored
Merge pull request #5601 from shannmu/multi-values
Support multiple values in native completions
2 parents 16fba4b + f0bd475 commit ca2265c

File tree

2 files changed

+334
-15
lines changed

2 files changed

+334
-15
lines changed

clap_complete/src/dynamic/completer.rs

+88-15
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use core::num;
12
use std::ffi::OsStr;
23
use std::ffi::OsString;
34

@@ -71,8 +72,7 @@ pub fn complete(
7172
}
7273

7374
if is_escaped {
74-
pos_index += 1;
75-
state = ParseState::Pos(pos_index);
75+
(state, pos_index) = parse_positional(current_cmd, pos_index, is_escaped, state);
7676
} else if arg.is_escape() {
7777
is_escaped = true;
7878
state = ParseState::ValueDone;
@@ -92,7 +92,7 @@ pub fn complete(
9292
if value.is_some() {
9393
ParseState::ValueDone
9494
} else {
95-
ParseState::Opt(opt.unwrap())
95+
ParseState::Opt((opt.unwrap(), 1))
9696
}
9797
}
9898
Some(clap::ArgAction::SetTrue) | Some(clap::ArgAction::SetFalse) => {
@@ -115,7 +115,7 @@ pub fn complete(
115115
Some(opt) => {
116116
state = match short.next_value_os() {
117117
Some(_) => ParseState::ValueDone,
118-
None => ParseState::Opt(opt),
118+
None => ParseState::Opt((opt, 1)),
119119
};
120120
}
121121
None => {
@@ -124,13 +124,24 @@ pub fn complete(
124124
}
125125
} else {
126126
match state {
127-
ParseState::ValueDone | ParseState::Pos(_) => {
128-
pos_index += 1;
129-
state = ParseState::ValueDone;
130-
}
131-
ParseState::Opt(_) => {
132-
state = ParseState::ValueDone;
127+
ParseState::ValueDone | ParseState::Pos(..) => {
128+
(state, pos_index) =
129+
parse_positional(current_cmd, pos_index, is_escaped, state);
133130
}
131+
132+
ParseState::Opt((ref opt, count)) => match opt.get_num_args() {
133+
Some(range) => {
134+
let max = range.max_values();
135+
if count < max {
136+
state = ParseState::Opt((opt.clone(), count + 1));
137+
} else {
138+
state = ParseState::ValueDone;
139+
}
140+
}
141+
None => {
142+
state = ParseState::ValueDone;
143+
}
144+
},
134145
}
135146
}
136147
}
@@ -146,11 +157,11 @@ enum ParseState<'a> {
146157
/// Parsing a value done, there is no state to record.
147158
ValueDone,
148159

149-
/// Parsing a positional argument after `--`
150-
Pos(usize),
160+
/// Parsing a positional argument after `--`. Pos(pos_index, takes_num_args)
161+
Pos((usize, usize)),
151162

152163
/// Parsing a optional flag argument
153-
Opt(&'a clap::Arg),
164+
Opt((&'a clap::Arg, usize)),
154165
}
155166

156167
fn complete_arg(
@@ -290,16 +301,27 @@ fn complete_arg(
290301
completions.extend(complete_subcommand(value, cmd));
291302
}
292303
}
293-
ParseState::Pos(_) => {
304+
ParseState::Pos(..) => {
294305
if let Some(positional) = cmd
295306
.get_positionals()
296307
.find(|p| p.get_index() == Some(pos_index))
297308
{
298309
completions.extend(complete_arg_value(arg.to_value(), positional, current_dir));
299310
}
300311
}
301-
ParseState::Opt(opt) => {
312+
ParseState::Opt((opt, count)) => {
302313
completions.extend(complete_arg_value(arg.to_value(), opt, current_dir));
314+
let min = opt.get_num_args().map(|r| r.min_values()).unwrap_or(0);
315+
if count > min {
316+
// Also complete this raw_arg as a positional argument, flags, options and subcommand.
317+
completions.extend(complete_arg(
318+
arg,
319+
cmd,
320+
current_dir,
321+
pos_index,
322+
ParseState::ValueDone,
323+
)?);
324+
}
303325
}
304326
}
305327
if completions.iter().any(|a| a.is_visible()) {
@@ -582,6 +604,57 @@ fn parse_shortflags<'c, 's>(
582604
(leading_flags, takes_value_opt, short)
583605
}
584606

607+
/// Parse the positional arguments. Return the new state and the new positional index.
608+
fn parse_positional<'a>(
609+
cmd: &clap::Command,
610+
pos_index: usize,
611+
is_escaped: bool,
612+
state: ParseState<'a>,
613+
) -> (ParseState<'a>, usize) {
614+
let pos_arg = cmd
615+
.get_positionals()
616+
.find(|p| p.get_index() == Some(pos_index));
617+
let num_args = pos_arg
618+
.and_then(|a| a.get_num_args().and_then(|r| Some(r.max_values())))
619+
.unwrap_or(1);
620+
621+
let update_state_with_new_positional = |pos_index| -> (ParseState<'a>, usize) {
622+
if num_args > 1 {
623+
(ParseState::Pos((pos_index, 1)), pos_index)
624+
} else {
625+
if is_escaped {
626+
(ParseState::Pos((pos_index, 1)), pos_index + 1)
627+
} else {
628+
(ParseState::ValueDone, pos_index + 1)
629+
}
630+
}
631+
};
632+
match state {
633+
ParseState::ValueDone => {
634+
update_state_with_new_positional(pos_index)
635+
},
636+
ParseState::Pos((prev_pos_index, num_arg)) => {
637+
if prev_pos_index == pos_index {
638+
if num_arg + 1 < num_args {
639+
(ParseState::Pos((pos_index, num_arg + 1)), pos_index)
640+
} else {
641+
if is_escaped {
642+
(ParseState::Pos((pos_index, 1)), pos_index + 1)
643+
} else {
644+
(ParseState::ValueDone, pos_index + 1)
645+
}
646+
}
647+
} else {
648+
update_state_with_new_positional(pos_index)
649+
}
650+
}
651+
ParseState::Opt(..) => unreachable!(
652+
"This branch won't be hit,
653+
because ParseState::Opt should not be seen as a positional argument and passed to this function."
654+
),
655+
}
656+
}
657+
585658
/// A completion candidate definition
586659
///
587660
/// This makes it easier to add more fields to completion candidate,

0 commit comments

Comments
 (0)