From 5708346fb86a619231c3f23ccea8e0f8f0b8497f Mon Sep 17 00:00:00 2001 From: Paul Gray Date: Tue, 24 Sep 2024 15:18:12 -0400 Subject: [PATCH] Add support for fish descriptions Fish supports "descriptions" for completions, which are displayed to the right of the value offered for completion, to provide additional context for the user. To display a description for a completion, the description is printed after a tab character in the completion line. so, instead of just printing: ``` bar baz qux ``` for completions, we can print: ``` barThe bar command bazThe baz command quxThe qux command ``` This commit adds support for fish descriptions to tabry. Instead of OptionsResults referencing a collection of `String`s, it now holds a collection of `OptionResult`s, which hold the value of the completion and an optional description. Tabry will print a description if it is present, and if the `TABRY_PRINT_DESCRIPTIONS` environment variable is set. --- docker/fish/foo.tabry | 10 ++++++---- shell/tabry_fish.fish | 2 +- src/app/mod.rs | 13 ++++++++----- src/engine/options_finder.rs | 35 +++++++++++++++++++++-------------- src/main.rs | 11 +++++++---- 5 files changed, 43 insertions(+), 28 deletions(-) diff --git a/docker/fish/foo.tabry b/docker/fish/foo.tabry index 3f31416..48f6c82 100644 --- a/docker/fish/foo.tabry +++ b/docker/fish/foo.tabry @@ -1,21 +1,23 @@ cmd foo -sub bar { +sub bar "The bar command" { arg file { opts const (car motorcycle) opts file } + flag dry-run,d "Don't act, only show what would be done" + flag something-else,s "This is another flag" } -sub baz { - arg directory { +sub baz "The baz command" { + arg directory "a directory, yo" { opts const (car motorcycle) opts dir opts file } } -sub qux { +sub qux "The qux command" { arg directory { opts const (car motorcycle) opts dir diff --git a/shell/tabry_fish.fish b/shell/tabry_fish.fish index 052e0da..d77d32c 100644 --- a/shell/tabry_fish.fish +++ b/shell/tabry_fish.fish @@ -98,7 +98,7 @@ function __tabry_offer_completions set cursor_position (commandline -C) set cmd (commandline) - set -l result ($_tabry_executable complete "$cmd" "$cursor_position") + set -l result ($_tabry_executable complete --include-descriptions "$cmd" "$cursor_position") # get the last item diff --git a/src/app/mod.rs b/src/app/mod.rs index d76c8be..458bbed 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -13,7 +13,7 @@ use crate::{ lang, }; -fn print_options(config_filename: &str, tokens: &[String], last_token: &str) -> Result<()> { +fn print_options(config_filename: &str, tokens: &[String], last_token: &str, include_descriptions: bool) -> Result<()> { let config = config::TabryConf::from_file(config_filename).with_context(|| "invalid config file")?; let result = @@ -23,11 +23,14 @@ fn print_options(config_filename: &str, tokens: &[String], last_token: &str) -> println!("{}", serde_json::to_string_pretty(&result.state)?); } - let options_finder = options_finder::OptionsFinder::new(result); + let options_finder = options_finder::OptionsFinder::new(result, include_descriptions); let opts = options_finder.options(last_token)?; for opt in &opts.options { - println!("{}", opt); + match opt.desc.as_ref() { + Some(desc) => println!("{} {}", opt.value, desc), + None => println!("{}", opt.value), + } } if !opts.special_options.is_empty() { @@ -44,7 +47,7 @@ fn print_options(config_filename: &str, tokens: &[String], last_token: &str) -> } // This runs using the filename plus 2nd arg as compline (shellsplits ARGV[2]) -pub fn run_as_compline(compline: &str, comppoint: &str) -> Result<()> { +pub fn run_as_compline(compline: &str, comppoint: &str, include_descriptions: bool) -> Result<()> { let comppoint = comppoint.parse::().wrap_err_with(|| eyre!("Invalid compoint: {}", comppoint))?; let tokenized_result = shell_tokenizer::split_with_comppoint(compline, comppoint).wrap_err_with(|| eyre!("Failed to split compline {} on comppoint {}", compline, comppoint))?; @@ -55,7 +58,7 @@ pub fn run_as_compline(compline: &str, comppoint: &str) -> Result<()> { let config_file = config_finder::find_tabry_config(&tokenized_result.command_basename)?; let compiled_config_file = cached_jsons::resolve_and_compile_cache_file(&config_file)?; - print_options(&compiled_config_file, &args[..], &last_arg)?; + print_options(&compiled_config_file, &args[..], &last_arg, include_descriptions)?; Ok(()) } diff --git a/src/engine/options_finder.rs b/src/engine/options_finder.rs index 9839cd8..a77ab5d 100644 --- a/src/engine/options_finder.rs +++ b/src/engine/options_finder.rs @@ -8,19 +8,26 @@ use serde_json::json; pub struct OptionsFinder { result: TabryResult, + include_descriptions: bool, +} + +#[derive(PartialEq, Eq, Hash)] +pub struct OptionResult { + pub value: String, + pub desc: Option, } pub struct OptionsResults { prefix: String, - pub options: HashSet, + pub options: HashSet, pub special_options: HashSet, } impl OptionsResults { - fn insert(&mut self, value: &str) { + fn insert(&mut self, value: &str, desc: Option<&str>) { if value.starts_with(&self.prefix) { // TODO get_or_insert_owned() in nightly would be ideal - self.options.insert(value.to_owned()); + self.options.insert(OptionResult { value: value.to_owned(), desc: desc.map(str::to_owned) }); } } @@ -30,8 +37,8 @@ impl OptionsResults { } impl OptionsFinder { - pub fn new(result: TabryResult) -> Self { - Self { result } + pub fn new(result: TabryResult, include_descriptions: bool) -> Self { + Self { result, include_descriptions } } pub fn options(&self, token: &str) -> Result { @@ -68,7 +75,7 @@ impl OptionsFinder { let concrete_subs = self.result.config.flatten_subs(opaque_subs).unwrap(); for s in concrete_subs { // TODO: error here if no name -- only allowable for top level - res.insert(s.name.as_ref().unwrap()); + res.insert(s.name.as_ref().unwrap(), if self.include_descriptions { s.description.as_deref() } else { None }); } } @@ -77,13 +84,13 @@ impl OptionsFinder { || self.result.state.flag_args.contains_key(&flag.name) } - fn add_option_for_flag(res: &mut OptionsResults, flag: &TabryConcreteFlag) { + fn add_option_for_flag(res: &mut OptionsResults, flag: &TabryConcreteFlag, include_descriptions: bool) { let flag_str = if flag.name.len() == 1 { format!("-{}", flag.name) } else { format!("--{}", flag.name) }; - res.insert(&flag_str); + res.insert(&flag_str, if include_descriptions { flag.description.as_deref() } else { None }); } fn add_options_subcommand_flags(&self, res: &mut OptionsResults) -> Result<(), TabryConfError> { @@ -97,7 +104,7 @@ impl OptionsFinder { .expand_flags(&self.result.current_sub().flags); let first_reqd_flag = current_sub_flags.find(|f| f.required && !self.flag_is_used(f)); if let Some(first_reqd_flag) = first_reqd_flag { - Self::add_option_for_flag(res, first_reqd_flag); + Self::add_option_for_flag(res, first_reqd_flag, self.include_descriptions); return Ok(()); } @@ -109,7 +116,7 @@ impl OptionsFinder { for sub in self.result.sub_stack.iter() { for flag in self.result.config.expand_flags(&sub.flags) { if !self.flag_is_used(flag) { - Self::add_option_for_flag(res, flag); + Self::add_option_for_flag(res, flag, self.include_descriptions); } } } @@ -126,7 +133,7 @@ impl OptionsFinder { match &opt { TabryOpt::File => res.insert_special("file"), TabryOpt::Dir => res.insert_special("dir"), - TabryOpt::Const { value } => res.insert(value), + TabryOpt::Const { value } => res.insert(value, None), TabryOpt::Delegate { value } => { res.insert_special(format!("delegate {}", value).as_str()) } @@ -151,7 +158,7 @@ impl OptionsFinder { let output_str = std::str::from_utf8(&output_bytes.stdout[..]).unwrap(); for line in output_str.split('\n') { if !line.is_empty() { - res.insert(line); + res.insert(line, None); } } } @@ -213,7 +220,7 @@ mod tests { fn options_with_machine_state(machine_state: MachineState, token: &str) -> OptionsResults { let tabry_conf: TabryConf = load_fixture_file("vehicles.json"); let tabry_result = TabryResult::new(tabry_conf, machine_state); - let options_finder = OptionsFinder::new(tabry_result); + let options_finder = OptionsFinder::new(tabry_result, false); options_finder.options(token).unwrap() } @@ -241,7 +248,7 @@ mod tests { }; let options_results = options_with_machine_state(machine_state, token); let actual_strs : HashSet<&str> = - options_results.options.iter().map(|s| s.as_str()).collect(); + options_results.options.iter().map(|s| s.value.as_str()).collect(); let actual_specials_strs : HashSet<&str> = options_results.special_options.iter().map(|s| s.as_str()).collect(); diff --git a/src/main.rs b/src/main.rs index 86b4244..88a4094 100644 --- a/src/main.rs +++ b/src/main.rs @@ -40,8 +40,8 @@ enum Subcommands { import_path: Option, }, - /// Output completion script for bash - /// Usage in ~/.bash_profile: `tabry fish | source` or + /// Output completion script for fish + /// Usage in ~/.config/fish/config.fish: `tabry fish | source` or /// `tabry fish | source; tabry_completion_init mycmd` Fish { #[arg(long)] @@ -67,6 +67,10 @@ enum Subcommands { compline: String, /// TODO desc comppoint: String, + + /// Include descriptions in completions (for fish shell only) + #[clap(long, short, action)] + include_descriptions: bool, }, } @@ -77,13 +81,12 @@ fn main() -> Result<()> { use tabry::app::*; let cli = Cli::parse(); match cli.command { - Complete { compline, comppoint } => run_as_compline(&compline, &comppoint)?, + Complete { compline, comppoint, include_descriptions } => run_as_compline(&compline, &comppoint, include_descriptions)?, Compile => compile()?, Commands => commands(), Bash { import_path, no_auto } => bash(import_path.as_deref(), no_auto), Zsh { import_path, no_auto } => zsh(import_path.as_deref(), no_auto), Fish { import_path, no_auto } => fish(import_path.as_deref(), no_auto), } - Ok(()) }