Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for completing --flag value<TAB>, -fbar<TAB>, -f=bar<TAB> and -f bar<TAB> #5533

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
238 changes: 203 additions & 35 deletions clap_complete/src/dynamic/completer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,31 @@
) -> Result<(), std::io::Error>;
}

/// Complete the given command
/// NOTE: borrow the idea from Parser.rs::ParseState to record the state during completion.s
#[derive(Debug, PartialEq, Eq, Clone)]
enum CompletionState {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's call this ParseState to be more specific (and match clap's parser)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: we generally put implementation details for logic after the logic (so move this after complete)

/// Completion value done, there is no state to record.
ValueDone,

/// Completing a optional flag
Opt(clap::Arg),

/// Completing a positional argument
Pos(usize),

/// Error during completing parse
Unknown,
}

// TODO: Index the positional argument and support more subcommnad completion. Consider things as follows:

Check warning on line 45 in clap_complete/src/dynamic/completer.rs

View workflow job for this annotation

GitHub Actions / Spell Check with Typos

"subcommnad" should be "subcommand".
// 1. `allow_missing_positional`
// 2. long_flag, short_flag and alias for subcommand
// 3. `allow_external_subcommands`
// 4. `args_conflicts_with_subcommands`
// 5. `subcommand_precedence_over_arg`
// 6. `multicall`

/// Complete the given command
pub fn complete(
cmd: &mut clap::Command,
args: Vec<OsString>,
Expand All @@ -45,37 +69,115 @@
// As we loop, `cursor` will always be pointing to the next item
raw_args.next_os(&mut target_cursor);

// TODO: Multicall support
// TODO: Multicall support => We should do something in glue script code.
if !cmd.is_no_binary_name_set() {
raw_args.next_os(&mut cursor);
}

let mut current_cmd = &*cmd;
let mut pos_index = 1;
let mut is_escaped = false;
let mut state = CompletionState::Unknown;
while let Some(arg) = raw_args.next(&mut cursor) {
if cursor == target_cursor {
return complete_arg(&arg, current_cmd, current_dir, pos_index, is_escaped);
match state {
CompletionState::ValueDone | CompletionState::Unknown => {
return complete_arg(&arg, current_cmd, current_dir, pos_index, is_escaped);
}
CompletionState::Opt(opt) => {
return Ok(complete_arg_value(arg.to_value(), &opt, current_dir));
}
CompletionState::Pos(pos) => {
if let Some(positional) =
cmd.get_positionals().find(|p| p.get_index() == Some(pos))
{
return Ok(complete_arg_value(arg.to_value(), positional, current_dir));
}
}
}
}

debug!("complete::next: Begin parsing '{:?}'", arg.to_value_os(),);

if let Ok(value) = arg.to_value() {
if let Some(next_cmd) = current_cmd.find_subcommand(value) {
current_cmd = next_cmd;
state = CompletionState::ValueDone;
pos_index = 1;
continue;
}
}

if is_escaped {
pos_index += 1;
} else if arg.is_escape() {
is_escaped = true;
} else if let Some(_long) = arg.to_long() {
} else if let Some(_short) = arg.to_short() {
state = CompletionState::Pos(pos_index);
} else {
pos_index += 1;
if arg.is_long() {
if let Some((flag, value)) = arg.to_long() {
if let Ok(flag) = flag {
state = if let None = value {
let opt = current_cmd
.get_arguments()
.find(|a| a.get_long() == Some(flag));
if let Some(opt) = opt {
// HACK: Assuming knowledge of `--flag=value` will be split into `--flag` `=` `value` in bash.
// And it will not be split in other shells.
// It will limit the completion for the situation that `=` is a value of an optional flag in other shells.
if let Some(equal) = raw_args.peek(&cursor) {
if equal.is_equal() {
raw_args.next(&mut cursor);
}
}
CompletionState::Opt(opt.clone())
} else {
CompletionState::Unknown
}
} else {
CompletionState::ValueDone
}
}
}
} else if arg.is_escape() {
is_escaped = true;
state = CompletionState::Pos(pos_index);
} else if arg.is_negative_number() {
state = CompletionState::ValueDone;
} else if arg.is_short() {
if let Some(short) = arg.to_short() {
let mut short = short.clone();
// HACK: During completion parsing, we assume that ShortFlags are in the format of `-fbar` or `-f=bar`.
let opt = short.next_flag();
state = if let Some(opt) = opt {
if let Ok(opt) = opt {
let opt = current_cmd
.get_arguments()
.find(|a| a.get_short() == Some(opt));
if let Some(opt) = opt {
// HACK: Assuming knowledge of `-f=value` will be split into `-f` `=` `value` in bash.
// And it will not be split in other shells.
// It will limit the completion for the situation that `=` is a value of an optional flag in other shells.
if let Some(equal) = raw_args.peek(&cursor) {
if equal.is_equal() {
raw_args.next(&mut cursor);
}
}
CompletionState::Opt(opt.clone())
} else {
CompletionState::Unknown
}
} else {
CompletionState::Unknown
}
} else {
CompletionState::Unknown
}
}
} else if arg.is_stdio() {
} else if arg.is_empty() {
} else {
pos_index += 1;
state = CompletionState::Pos(pos_index);
}
}
}

Expand All @@ -101,48 +203,112 @@
is_escaped
);
let mut completions = Vec::new();

if !is_escaped {
if let Some((flag, value)) = arg.to_long() {
if let Ok(flag) = flag {
if let Some(value) = value {
if arg.is_long() {
if let Some((flag, value)) = arg.to_long() {
if let Ok(flag) = flag {
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)
.into_iter()
.map(|(os, help)| {
// HACK: Need better `OsStr` manipulation
(format!("--{}={}", flag, os.to_string_lossy()).into(), help)
}),
);
if let Some(value) = value {
completions.extend(
complete_arg_value(value.to_str().ok_or(value), arg, current_dir)
.into_iter()
.map(|(os, help)| {
(
format!("--{}={}", flag, os.to_string_lossy()).into(),
help,
)
}),
);
}
} else {
if let Some(_) = value {
} else {
completions.extend(
longs_and_visible_aliases(cmd).into_iter().filter_map(
|(f, help)| {
f.starts_with(flag).then(|| (format!("--{f}").into(), help))
},
),
);
}
}
} else {
completions.extend(longs_and_visible_aliases(cmd).into_iter().filter_map(
|(f, help)| f.starts_with(flag).then(|| (format!("--{f}").into(), help)),
));
}
}
} else if arg.is_escape() || arg.is_stdio() || arg.is_empty() {
// HACK: Assuming knowledge of is_escape / is_stdio
} else if arg.is_escape() {
// HACK: Assuming knowledge of is_escape
completions.extend(
longs_and_visible_aliases(cmd)
.into_iter()
.map(|(f, help)| (format!("--{f}").into(), help)),
);
}
} else if arg.is_negative_number() {
} else if arg.is_short() {
// HACK: Assuming knowledge of -f<TAB>` and `-f=<TAB>` to complete the value of `-f`, and `-f<TAB>` to complete another short flag of cmd.
if let Some(short) = arg.to_short() {
let mut short = short.clone();
let opt = short.next_flag();
if let Some(opt) = opt {
if let Ok(opt) = opt {
if let Some(arg) = cmd.get_arguments().find(|a| a.get_short() == Some(opt))
{
let mut equal_char = "";
if let Some(equal) = short.peek_next_flag() {
if let Ok(equal) = equal {
if equal == '=' {
equal_char = "=";
short.next_flag();
}
}
}
if let Some(value) = short.next_value_os() {
completions.extend(
complete_arg_value(
value.to_str().ok_or(value),
arg,
current_dir,
)
.into_iter()
.map(|(f, help)| {
(format!("-{}{}{}", opt, equal_char, f.to_string_lossy()).into(), help)
}),
)
}
}
}
}

if arg.is_empty() || arg.is_stdio() || arg.is_short() {
let dash_or_arg = if arg.is_empty() {
"-".into()
} else {
arg.to_value_os().to_string_lossy()
};
completions.extend(
shorts_and_visible_aliases(cmd)
.into_iter()
.map(|(f, help)| (format!("{}{}", arg.to_value_os().to_string_lossy(), f).into(), help)),
);

}
} else if arg.is_stdio() {
// HACK: Assuming knowledge of is_stdio
completions.extend(
longs_and_visible_aliases(cmd)
.into_iter()
.map(|(f, help)| (format!("--{f}").into(), help)),
);
// HACK: Assuming knowledge of is_stdio / is_escape
completions.extend(
shorts_and_visible_aliases(cmd)
.into_iter()
.map(|(f, help)| (format!("-{}", f).into(), help)),
);
} else if arg.is_empty() {
// Complete all the long and short flag of current command.
completions.extend(
longs_and_visible_aliases(cmd)
.into_iter()
.map(|(f, help)| (format!("--{f}").into(), help)),
);

completions.extend(
shorts_and_visible_aliases(cmd)
.into_iter()
// HACK: Need better `OsStr` manipulation
.map(|(f, help)| (format!("{}{}", dash_or_arg, f).into(), help)),
.map(|(f, help)| (format!("-{}", f).into(), help)),
);
}
}
Expand Down Expand Up @@ -268,6 +434,7 @@
completions
}

// TODO: support more subcommands alias completion.
fn complete_subcommand(value: &str, cmd: &clap::Command) -> Vec<(OsString, Option<StyledStr>)> {
debug!(
"complete_subcommand: cmd={:?}, value={:?}",
Expand Down Expand Up @@ -327,6 +494,7 @@
}
}

// TODO: support more subcommands alias completion.
/// Gets subcommands of [`clap::Command`] in the form of `("name", "bin_name")`.
///
/// Subcommand `rustup toolchain install` would be converted to
Expand Down
12 changes: 12 additions & 0 deletions clap_lex/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,11 @@ impl<'s> ParsedArg<'s> {
self.inner == "--"
}

/// Does the argument look like an equal sign (`=`)
pub fn is_equal(&self) -> bool {
self.inner == "="
}

/// Does the argument look like a negative number?
///
/// This won't parse the number in full but attempts to see if this looks
Expand Down Expand Up @@ -421,6 +426,13 @@ impl<'s> ShortFlags<'s> {
self.invalid_suffix.is_none() && is_number(self.utf8_prefix.as_str())
}

/// Peek the next short flag
///
/// On error, returns a None
pub fn peek_next_flag(&self) -> Option<Result<char, &'s OsStr>> {
self.utf8_prefix.clone().next().map(|(_, c)| Ok(c))
}

/// Advance the iterator, returning the next short flag on success
///
/// On error, returns the invalid-UTF8 value
Expand Down
Loading