From 67e31af334346e35a540c49ba2fec46bda5ae880 Mon Sep 17 00:00:00 2001 From: shannmu Date: Fri, 5 Jul 2024 17:32:50 +0800 Subject: [PATCH 1/3] refactor(clap_complete): Add `CompletionCandidate` to replace `(OsString, Option)` --- clap_complete/src/dynamic/completer.rs | 109 ++++++++++++++++----- clap_complete/src/dynamic/shells/bash.rs | 4 +- clap_complete/src/dynamic/shells/elvish.rs | 4 +- clap_complete/src/dynamic/shells/fish.rs | 6 +- clap_complete/src/dynamic/shells/zsh.rs | 4 +- clap_complete/tests/testsuite/dynamic.rs | 6 +- 6 files changed, 94 insertions(+), 39 deletions(-) diff --git a/clap_complete/src/dynamic/completer.rs b/clap_complete/src/dynamic/completer.rs index 4c6d9c92fe9..f2911ce153a 100644 --- a/clap_complete/src/dynamic/completer.rs +++ b/clap_complete/src/dynamic/completer.rs @@ -32,7 +32,7 @@ pub fn complete( args: Vec, arg_index: usize, current_dir: Option<&std::path::Path>, -) -> Result)>, std::io::Error> { +) -> Result, std::io::Error> { cmd.build(); let raw_args = clap_lex::RawArgs::new(args); @@ -91,7 +91,7 @@ fn complete_arg( current_dir: Option<&std::path::Path>, pos_index: usize, is_escaped: bool, -) -> Result)>, std::io::Error> { +) -> Result, std::io::Error> { debug!( "complete_arg: arg={:?}, cmd={:?}, current_dir={:?}, pos_index={}, is_escaped={}", arg, @@ -100,7 +100,7 @@ fn complete_arg( pos_index, is_escaped ); - let mut completions = Vec::new(); + let mut completions = Vec::::new(); if !is_escaped { if let Some((flag, value)) = arg.to_long() { @@ -112,23 +112,25 @@ fn complete_arg( .into_iter() .map(|(os, help)| { // HACK: Need better `OsStr` manipulation - (format!("--{}={}", flag, os.to_string_lossy()).into(), help) + CompletionCandidate::new(format!( + "--{}={}", + flag, + os.to_string_lossy() + )) + .help(help) }), ); } } else { - completions.extend(longs_and_visible_aliases(cmd).into_iter().filter_map( - |(f, help)| f.starts_with(flag).then(|| (format!("--{f}").into(), help)), - )); + completions.extend(longs_and_visible_aliases(cmd).into_iter().filter(|comp| { + comp.get_content() + .starts_with(format!("--{}", flag).as_str()) + })); } } } else if arg.is_escape() || arg.is_stdio() || arg.is_empty() { // HACK: Assuming knowledge of is_escape / is_stdio - completions.extend( - longs_and_visible_aliases(cmd) - .into_iter() - .map(|(f, help)| (format!("--{f}").into(), help)), - ); + completions.extend(longs_and_visible_aliases(cmd)); } if arg.is_empty() || arg.is_stdio() || arg.is_short() { @@ -142,7 +144,14 @@ fn complete_arg( shorts_and_visible_aliases(cmd) .into_iter() // HACK: Need better `OsStr` manipulation - .map(|(f, help)| (format!("{}{}", dash_or_arg, f).into(), help)), + .map(|comp| { + CompletionCandidate::new(format!( + "{}{}", + dash_or_arg, + comp.get_content().to_string_lossy() + )) + .help(comp.get_help().cloned()) + }), ); } } @@ -151,7 +160,11 @@ fn complete_arg( .get_positionals() .find(|p| p.get_index() == Some(pos_index)) { - completions.extend(complete_arg_value(arg.to_value(), positional, current_dir)); + completions.extend( + complete_arg_value(arg.to_value(), positional, current_dir) + .into_iter() + .map(|(os, help)| CompletionCandidate::new(os).help(help)), + ); } if let Ok(value) = arg.to_value() { @@ -174,7 +187,7 @@ fn complete_arg_value( values.extend(possible_values.into_iter().filter_map(|p| { let name = p.get_name(); name.starts_with(value) - .then(|| (name.into(), p.get_help().cloned())) + .then(|| (OsString::from(name), p.get_help().cloned())) })); } } else { @@ -268,7 +281,7 @@ fn complete_path( completions } -fn complete_subcommand(value: &str, cmd: &clap::Command) -> Vec<(OsString, Option)> { +fn complete_subcommand(value: &str, cmd: &clap::Command) -> Vec { debug!( "complete_subcommand: cmd={:?}, value={:?}", cmd.get_name(), @@ -277,8 +290,7 @@ fn complete_subcommand(value: &str, cmd: &clap::Command) -> Vec<(OsString, Optio let mut scs = subcommands(cmd) .into_iter() - .filter(|x| x.0.starts_with(value)) - .map(|x| (OsString::from(&x.0), x.1)) + .filter(|x| x.content.starts_with(value)) .collect::>(); scs.sort(); scs.dedup(); @@ -287,15 +299,15 @@ fn complete_subcommand(value: &str, cmd: &clap::Command) -> Vec<(OsString, Optio /// Gets all the long options, their visible aliases and flags of a [`clap::Command`]. /// Includes `help` and `version` depending on the [`clap::Command`] settings. -fn longs_and_visible_aliases(p: &clap::Command) -> Vec<(String, Option)> { +fn longs_and_visible_aliases(p: &clap::Command) -> Vec { debug!("longs: name={}", p.get_name()); p.get_arguments() .filter_map(|a| { a.get_long_and_visible_aliases().map(|longs| { - longs - .into_iter() - .map(|s| (s.to_string(), a.get_help().cloned())) + longs.into_iter().map(|s| { + CompletionCandidate::new(format!("--{}", s)).help(a.get_help().cloned()) + }) }) }) .flatten() @@ -304,13 +316,16 @@ fn longs_and_visible_aliases(p: &clap::Command) -> Vec<(String, Option Vec<(char, Option)> { +fn shorts_and_visible_aliases(p: &clap::Command) -> Vec { debug!("shorts: name={}", p.get_name()); p.get_arguments() .filter_map(|a| { - a.get_short_and_visible_aliases() - .map(|shorts| shorts.into_iter().map(|s| (s, a.get_help().cloned()))) + a.get_short_and_visible_aliases().map(|shorts| { + shorts + .into_iter() + .map(|s| CompletionCandidate::new(s.to_string()).help(a.get_help().cloned())) + }) }) .flatten() .collect() @@ -331,14 +346,54 @@ fn possible_values(a: &clap::Arg) -> Option> { /// /// Subcommand `rustup toolchain install` would be converted to /// `("install", "rustup toolchain install")`. -fn subcommands(p: &clap::Command) -> Vec<(String, Option)> { +fn subcommands(p: &clap::Command) -> Vec { debug!("subcommands: name={}", p.get_name()); debug!("subcommands: Has subcommands...{:?}", p.has_subcommands()); p.get_subcommands() .flat_map(|sc| { sc.get_name_and_visible_aliases() .into_iter() - .map(|s| (s.to_string(), sc.get_about().cloned())) + .map(|s| CompletionCandidate::new(s.to_string()).help(sc.get_about().cloned())) }) .collect() } + +/// A completion candidate defination +/// +/// This makes it easier to add more fields to completion candidate, +/// rather than using `(OsString, Option)` or `(String, Option)` to represent a completion candidate +#[derive(Default, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub struct CompletionCandidate { + /// Main completion candidate content + content: OsString, + + /// Help message with a completion candidate + help: Option, +} + +impl CompletionCandidate { + /// Create a new completion candidate + pub fn new(content: impl Into) -> Self { + let content = content.into(); + Self { + content, + ..Default::default() + } + } + + /// Set the help message of the completion candidate + pub fn help(mut self, help: Option) -> Self { + self.help = help; + self + } + + /// Get the content of the completion candidate + pub fn get_content(&self) -> &OsStr { + &self.content + } + + /// Get the help message of the completion candidate + pub fn get_help(&self) -> Option<&StyledStr> { + self.help.as_ref() + } +} diff --git a/clap_complete/src/dynamic/shells/bash.rs b/clap_complete/src/dynamic/shells/bash.rs index 1308e67a35d..d01a932b080 100644 --- a/clap_complete/src/dynamic/shells/bash.rs +++ b/clap_complete/src/dynamic/shells/bash.rs @@ -73,11 +73,11 @@ complete -o nospace -o bashdefault -F _clap_complete_NAME BIN let ifs: Option = std::env::var("IFS").ok().and_then(|i| i.parse().ok()); let completions = crate::dynamic::complete(cmd, args, index, current_dir)?; - for (i, (completion, _)) in completions.iter().enumerate() { + for (i, candidate) in completions.iter().enumerate() { if i != 0 { write!(buf, "{}", ifs.as_deref().unwrap_or("\n"))?; } - write!(buf, "{}", completion.to_string_lossy())?; + write!(buf, "{}", candidate.get_content().to_string_lossy())?; } Ok(()) } diff --git a/clap_complete/src/dynamic/shells/elvish.rs b/clap_complete/src/dynamic/shells/elvish.rs index 57351b171ee..323226c672d 100644 --- a/clap_complete/src/dynamic/shells/elvish.rs +++ b/clap_complete/src/dynamic/shells/elvish.rs @@ -47,11 +47,11 @@ set edit:completion:arg-completer[BIN] = { |@words| let ifs: Option = std::env::var("_CLAP_IFS").ok().and_then(|i| i.parse().ok()); let completions = crate::dynamic::complete(cmd, args, index, current_dir)?; - for (i, (completion, _)) in completions.iter().enumerate() { + for (i, candidate) in completions.iter().enumerate() { if i != 0 { write!(buf, "{}", ifs.as_deref().unwrap_or("\n"))?; } - write!(buf, "{}", completion.to_string_lossy())?; + write!(buf, "{}", candidate.get_content().to_string_lossy())?; } Ok(()) } diff --git a/clap_complete/src/dynamic/shells/fish.rs b/clap_complete/src/dynamic/shells/fish.rs index cfb95195f2c..269539e0e03 100644 --- a/clap_complete/src/dynamic/shells/fish.rs +++ b/clap_complete/src/dynamic/shells/fish.rs @@ -30,9 +30,9 @@ impl crate::dynamic::Completer for Fish { let index = args.len() - 1; let completions = crate::dynamic::complete(cmd, args, index, current_dir)?; - for (completion, help) in completions { - write!(buf, "{}", completion.to_string_lossy())?; - if let Some(help) = help { + for candidate in completions { + write!(buf, "{}", candidate.get_content().to_string_lossy())?; + if let Some(help) = candidate.get_help() { write!( buf, "\t{}", diff --git a/clap_complete/src/dynamic/shells/zsh.rs b/clap_complete/src/dynamic/shells/zsh.rs index d6e699f7539..4326cd16dc7 100644 --- a/clap_complete/src/dynamic/shells/zsh.rs +++ b/clap_complete/src/dynamic/shells/zsh.rs @@ -54,11 +54,11 @@ compdef _clap_dynamic_completer BIN"# } let completions = crate::dynamic::complete(cmd, args, index, current_dir)?; - for (i, (completion, _)) in completions.iter().enumerate() { + for (i, candidate) in completions.iter().enumerate() { if i != 0 { write!(buf, "{}", ifs.as_deref().unwrap_or("\n"))?; } - write!(buf, "{}", completion.to_string_lossy())?; + write!(buf, "{}", candidate.get_content().to_string_lossy())?; } Ok(()) } diff --git a/clap_complete/tests/testsuite/dynamic.rs b/clap_complete/tests/testsuite/dynamic.rs index fa4827822ec..07987b6316a 100644 --- a/clap_complete/tests/testsuite/dynamic.rs +++ b/clap_complete/tests/testsuite/dynamic.rs @@ -171,9 +171,9 @@ fn complete(cmd: &mut Command, args: impl AsRef, current_dir: Option<&Path> clap_complete::dynamic::complete(cmd, args, arg_index, current_dir) .unwrap() .into_iter() - .map(|(compl, help)| { - let compl = compl.to_str().unwrap(); - if let Some(help) = help { + .map(|candidate| { + let compl = candidate.get_content().to_str().unwrap(); + if let Some(help) = candidate.get_help() { format!("{compl}\t{help}") } else { compl.to_owned() From 903b73bd1cd08e1b80cf647490587bf35f1ea601 Mon Sep 17 00:00:00 2001 From: shannmu Date: Wed, 10 Jul 2024 22:43:58 +0800 Subject: [PATCH 2/3] test(clap_complete): Add test cases for hiding long flags and their long aliases --- clap_complete/tests/testsuite/dynamic.rs | 99 +++++++++++++++++++++--- 1 file changed, 89 insertions(+), 10 deletions(-) diff --git a/clap_complete/tests/testsuite/dynamic.rs b/clap_complete/tests/testsuite/dynamic.rs index 07987b6316a..e202ef8be1a 100644 --- a/clap_complete/tests/testsuite/dynamic.rs +++ b/clap_complete/tests/testsuite/dynamic.rs @@ -23,11 +23,38 @@ fn suggest_subcommand_subset() { .subcommand(Command::new("hello-moon")) .subcommand(Command::new("goodbye-world")); - assert_data_eq!(complete!(cmd, "he"), snapbox::str![[r#" + assert_data_eq!( + complete!(cmd, "he"), + snapbox::str![[r#" hello-moon hello-world help Print this message or the help of the given subcommand(s) -"#]],); +"#]], + ); +} + +#[test] +fn suggest_hidden_long_flags() { + let mut cmd = Command::new("exhaustive") + .arg(clap::Arg::new("hello-world-visible").long("hello-world-visible")) + .arg( + clap::Arg::new("hello-world-hidden") + .long("hello-world-hidden") + .hide(true), + ); + + assert_data_eq!( + complete!(cmd, "--hello-world"), + snapbox::str![ + "--hello-world-visible +--hello-world-hidden" + ] + ); + + assert_data_eq!( + complete!(cmd, "--hello-world-h"), + snapbox::str!["--hello-world-hidden"] + ) } #[test] @@ -60,6 +87,46 @@ hello-world-foo" ); } +#[test] +fn suggest_hidden_long_flag_aliases() { + let mut cmd = Command::new("exhaustive") + .arg( + clap::Arg::new("test_visible") + .long("test_visible") + .visible_alias("test_visible-alias_visible") + .alias("test_visible-alias_hidden"), + ) + .arg( + clap::Arg::new("test_hidden") + .long("test_hidden") + .visible_alias("test_hidden-alias_visible") + .alias("test_hidden-alias_hidden") + .hide(true), + ); + + assert_data_eq!( + complete!(cmd, "--test"), + snapbox::str![ + "--test_visible +--test_visible-alias_visible +--test_hidden +--test_hidden-alias_visible" + ] + ); + + assert_data_eq!( + complete!(cmd, "--test_h"), + snapbox::str![ + "--test_hidden +--test_hidden-alias_visible" + ] + ); + + assert_data_eq!(complete!(cmd, "--test_visible-alias_h"), snapbox::str![""]); + + assert_data_eq!(complete!(cmd, "--test_hidden-alias_h"), snapbox::str![""]); +} + #[test] fn suggest_long_flag_subset() { let mut cmd = Command::new("exhaustive") @@ -79,11 +146,14 @@ fn suggest_long_flag_subset() { .action(clap::ArgAction::Count), ); - assert_data_eq!(complete!(cmd, "--he"), snapbox::str![[r#" + assert_data_eq!( + complete!(cmd, "--he"), + snapbox::str![[r#" --hello-world --hello-moon --help Print help -"#]],); +"#]], + ); } #[test] @@ -95,10 +165,13 @@ fn suggest_possible_value_subset() { "goodbye-world".into(), ])); - assert_data_eq!(complete!(cmd, "hello"), snapbox::str![[r#" + assert_data_eq!( + complete!(cmd, "hello"), + snapbox::str![[r#" hello-world Say hello to the world hello-moon -"#]],); +"#]], + ); } #[test] @@ -120,12 +193,15 @@ fn suggest_additional_short_flags() { .action(clap::ArgAction::Count), ); - assert_data_eq!(complete!(cmd, "-a"), snapbox::str![[r#" + assert_data_eq!( + complete!(cmd, "-a"), + snapbox::str![[r#" -aa -ab -ac -ah Print help -"#]],); +"#]], + ); } #[test] @@ -138,13 +214,16 @@ fn suggest_subcommand_positional() { ]), )); - assert_data_eq!(complete!(cmd, "hello-world [TAB]"), snapbox::str![[r#" + assert_data_eq!( + complete!(cmd, "hello-world [TAB]"), + snapbox::str![[r#" --help Print help (see more with '--help') -h Print help (see more with '--help') hello-world Say hello to the world hello-moon goodbye-world -"#]],); +"#]], + ); } fn complete(cmd: &mut Command, args: impl AsRef, current_dir: Option<&Path>) -> String { From d1e0f6073c18e4d0fa5790ec21b39281b98d4a60 Mon Sep 17 00:00:00 2001 From: shannmu Date: Wed, 10 Jul 2024 22:46:17 +0800 Subject: [PATCH 3/3] feat(clap_complete): Support hiding long flags and their long aliases --- clap_builder/src/builder/arg.rs | 15 +++++ clap_complete/src/dynamic/completer.rs | 71 ++++++++++++++++++++---- clap_complete/tests/testsuite/dynamic.rs | 22 ++++---- 3 files changed, 88 insertions(+), 20 deletions(-) diff --git a/clap_builder/src/builder/arg.rs b/clap_builder/src/builder/arg.rs index d0929e515d3..a40d0cca419 100644 --- a/clap_builder/src/builder/arg.rs +++ b/clap_builder/src/builder/arg.rs @@ -3952,6 +3952,21 @@ impl Arg { Some(longs) } + /// Get hidden aliases for this argument, if any + #[inline] + pub fn get_aliases(&self) -> Option> { + if self.aliases.is_empty() { + None + } else { + Some( + self.aliases + .iter() + .filter_map(|(s, v)| if !*v { Some(s.as_str()) } else { None }) + .collect(), + ) + } + } + /// Get the names of possible values for this argument. Only useful for user /// facing applications, such as building help messages or man files pub fn get_possible_values(&self) -> Vec { diff --git a/clap_complete/src/dynamic/completer.rs b/clap_complete/src/dynamic/completer.rs index f2911ce153a..640e2d57a81 100644 --- a/clap_complete/src/dynamic/completer.rs +++ b/clap_complete/src/dynamic/completer.rs @@ -118,6 +118,7 @@ fn complete_arg( os.to_string_lossy() )) .help(help) + .visible(true) }), ); } @@ -126,11 +127,18 @@ fn complete_arg( comp.get_content() .starts_with(format!("--{}", flag).as_str()) })); + + completions.extend(hidden_longs_aliases(cmd).into_iter().filter(|comp| { + comp.get_content() + .starts_with(format!("--{}", flag).as_str()) + })) } } } else if arg.is_escape() || arg.is_stdio() || arg.is_empty() { // HACK: Assuming knowledge of is_escape / is_stdio completions.extend(longs_and_visible_aliases(cmd)); + + completions.extend(hidden_longs_aliases(cmd)); } if arg.is_empty() || arg.is_stdio() || arg.is_short() { @@ -151,6 +159,7 @@ fn complete_arg( comp.get_content().to_string_lossy() )) .help(comp.get_help().cloned()) + .visible(true) }), ); } @@ -163,7 +172,7 @@ fn complete_arg( completions.extend( complete_arg_value(arg.to_value(), positional, current_dir) .into_iter() - .map(|(os, help)| CompletionCandidate::new(os).help(help)), + .map(|(os, help)| CompletionCandidate::new(os).help(help).visible(true)), ); } @@ -171,6 +180,10 @@ fn complete_arg( completions.extend(complete_subcommand(value, cmd)); } + if completions.iter().any(|a| a.is_visible()) { + completions.retain(|a| a.is_visible()) + } + Ok(completions) } @@ -297,7 +310,7 @@ fn complete_subcommand(value: &str, cmd: &clap::Command) -> Vec Vec { debug!("longs: name={}", p.get_name()); @@ -306,7 +319,27 @@ fn longs_and_visible_aliases(p: &clap::Command) -> Vec { .filter_map(|a| { a.get_long_and_visible_aliases().map(|longs| { longs.into_iter().map(|s| { - CompletionCandidate::new(format!("--{}", s)).help(a.get_help().cloned()) + CompletionCandidate::new(format!("--{}", s.to_string())) + .help(a.get_help().cloned()) + .visible(!a.is_hide_set()) + }) + }) + }) + .flatten() + .collect() +} + +/// Gets all the long hidden aliases and flags of a [`clap::Command`]. +fn hidden_longs_aliases(p: &clap::Command) -> Vec { + debug!("longs: name={}", p.get_name()); + + p.get_arguments() + .filter_map(|a| { + a.get_aliases().map(|longs| { + longs.into_iter().map(|s| { + CompletionCandidate::new(format!("--{}", s.to_string())) + .help(a.get_help().cloned()) + .visible(false) }) }) }) @@ -322,9 +355,11 @@ fn shorts_and_visible_aliases(p: &clap::Command) -> Vec { p.get_arguments() .filter_map(|a| { a.get_short_and_visible_aliases().map(|shorts| { - shorts - .into_iter() - .map(|s| CompletionCandidate::new(s.to_string()).help(a.get_help().cloned())) + shorts.into_iter().map(|s| { + CompletionCandidate::new(s.to_string()) + .help(a.get_help().cloned()) + .visible(!a.is_hide_set()) + }) }) }) .flatten() @@ -351,14 +386,16 @@ fn subcommands(p: &clap::Command) -> Vec { debug!("subcommands: Has subcommands...{:?}", p.has_subcommands()); p.get_subcommands() .flat_map(|sc| { - sc.get_name_and_visible_aliases() - .into_iter() - .map(|s| CompletionCandidate::new(s.to_string()).help(sc.get_about().cloned())) + sc.get_name_and_visible_aliases().into_iter().map(|s| { + CompletionCandidate::new(s.to_string()) + .help(sc.get_about().cloned()) + .visible(true) + }) }) .collect() } -/// A completion candidate defination +/// A completion candidate definition /// /// This makes it easier to add more fields to completion candidate, /// rather than using `(OsString, Option)` or `(String, Option)` to represent a completion candidate @@ -369,6 +406,9 @@ pub struct CompletionCandidate { /// Help message with a completion candidate help: Option, + + /// Whether the completion candidate is visible + visible: bool, } impl CompletionCandidate { @@ -387,6 +427,12 @@ impl CompletionCandidate { self } + /// Set the visibility of the completion candidate + pub fn visible(mut self, visible: bool) -> Self { + self.visible = visible; + self + } + /// Get the content of the completion candidate pub fn get_content(&self) -> &OsStr { &self.content @@ -396,4 +442,9 @@ impl CompletionCandidate { pub fn get_help(&self) -> Option<&StyledStr> { self.help.as_ref() } + + /// Get the visibility of the completion candidate + pub fn is_visible(&self) -> bool { + self.visible + } } diff --git a/clap_complete/tests/testsuite/dynamic.rs b/clap_complete/tests/testsuite/dynamic.rs index e202ef8be1a..df8bac01d56 100644 --- a/clap_complete/tests/testsuite/dynamic.rs +++ b/clap_complete/tests/testsuite/dynamic.rs @@ -45,10 +45,7 @@ fn suggest_hidden_long_flags() { assert_data_eq!( complete!(cmd, "--hello-world"), - snapbox::str![ - "--hello-world-visible ---hello-world-hidden" - ] + snapbox::str!["--hello-world-visible"] ); assert_data_eq!( @@ -108,9 +105,7 @@ fn suggest_hidden_long_flag_aliases() { complete!(cmd, "--test"), snapbox::str![ "--test_visible ---test_visible-alias_visible ---test_hidden ---test_hidden-alias_visible" +--test_visible-alias_visible" ] ); @@ -118,13 +113,20 @@ fn suggest_hidden_long_flag_aliases() { complete!(cmd, "--test_h"), snapbox::str![ "--test_hidden ---test_hidden-alias_visible" +--test_hidden-alias_visible +--test_hidden-alias_hidden" ] ); - assert_data_eq!(complete!(cmd, "--test_visible-alias_h"), snapbox::str![""]); + assert_data_eq!( + complete!(cmd, "--test_visible-alias_h"), + snapbox::str!["--test_visible-alias_hidden"] + ); - assert_data_eq!(complete!(cmd, "--test_hidden-alias_h"), snapbox::str![""]); + assert_data_eq!( + complete!(cmd, "--test_hidden-alias_h"), + snapbox::str!["--test_hidden-alias_hidden"] + ); } #[test]