Skip to content

Commit

Permalink
Add support for fish descriptions
Browse files Browse the repository at this point in the history
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:

```
bar<TAB>The bar command
baz<TAB>The baz command
qux<TAB>The 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.
  • Loading branch information
pfgray committed Sep 25, 2024
1 parent a4e52b3 commit 5708346
Show file tree
Hide file tree
Showing 5 changed files with 43 additions and 28 deletions.
10 changes: 6 additions & 4 deletions docker/fish/foo.tabry
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 1 addition & 1 deletion shell/tabry_fish.fish
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
13 changes: 8 additions & 5 deletions src/app/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand All @@ -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() {
Expand All @@ -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::<usize>().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))?;
Expand All @@ -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(())
}

Expand Down
35 changes: 21 additions & 14 deletions src/engine/options_finder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>,
}

pub struct OptionsResults {
prefix: String,
pub options: HashSet<String>,
pub options: HashSet<OptionResult>,
pub special_options: HashSet<String>,
}

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) });
}
}

Expand All @@ -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<OptionsResults, TabryConfError> {
Expand Down Expand Up @@ -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 });
}
}

Expand All @@ -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> {
Expand All @@ -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(());
}

Expand All @@ -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);
}
}
}
Expand All @@ -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())
}
Expand All @@ -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);
}
}
}
Expand Down Expand Up @@ -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()
}

Expand Down Expand Up @@ -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();

Expand Down
11 changes: 7 additions & 4 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,8 @@ enum Subcommands {
import_path: Option<String>,
},

/// 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)]
Expand All @@ -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,
},
}

Expand All @@ -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(())
}

0 comments on commit 5708346

Please sign in to comment.