Skip to content

Commit

Permalink
feat: complete descriptions
Browse files Browse the repository at this point in the history
Fixes #62
  • Loading branch information
jdx committed May 26, 2024
1 parent a4fa07f commit a8afca7
Show file tree
Hide file tree
Showing 4 changed files with 161 additions and 132 deletions.
277 changes: 153 additions & 124 deletions cli/src/cli/complete_word.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,9 @@ use crate::cli::generate;
#[derive(Debug, Args)]
#[clap(visible_alias = "cw")]
pub struct CompleteWord {
// #[clap(value_parser = ["bash", "fish", "zsh"])]
// shell: String,
#[clap(long, value_parser = ["bash", "fish", "zsh"])]
shell: Option<String>,

/// user's input from the command line
words: Vec<String>,

Expand All @@ -40,14 +41,21 @@ impl CompleteWord {
pub fn run(&self) -> miette::Result<()> {
let spec = generate::file_or_spec(&self.file, &self.spec)?;
let choices = self.complete_word(&spec)?;
for c in &choices {
println!("{}", c);
let shell = self.shell.as_deref().unwrap_or_default();
let any_descriptions = choices.iter().any(|(_, d)| !d.is_empty());
for (c, description) in choices {
match (any_descriptions, shell) {
(true, "bash") => println!("{c}"),
(true, "fish") => println!("{c}\t{description}"),
(true, "zsh") => println!("{c}\\:'{description}'"),
_ => println!("{c}"),
}
}

Ok(())
}

fn complete_word(&self, spec: &Spec) -> miette::Result<Vec<String>> {
fn complete_word(&self, spec: &Spec) -> miette::Result<Vec<(String, String)>> {
let cword = self.cword.unwrap_or(self.words.len().max(1) - 1);
let ctoken = self.words.get(cword).cloned().unwrap_or_default();
let words: Vec<_> = self.words.iter().take(cword).cloned().collect();
Expand All @@ -65,148 +73,169 @@ impl CompleteWord {
let parsed = usage::cli::parse(spec, &words)?;
debug!("parsed cmd: {}", parsed.cmd.full_cmd.join(" "));
let choices = if !parsed.cmd.subcommands.is_empty() {
complete_subcommands(parsed.cmd, &ctoken)
self.complete_subcommands(parsed.cmd, &ctoken)
} else if ctoken == "-" {
let shorts = complete_short_flag_names(&parsed.available_flags, "");
let longs = complete_long_flag_names(&parsed.available_flags, "");
let shorts = self.complete_short_flag_names(&parsed.available_flags, "");
let longs = self.complete_long_flag_names(&parsed.available_flags, "");
shorts.into_iter().chain(longs).collect()
} else if ctoken.starts_with("--") {
complete_long_flag_names(&parsed.available_flags, &ctoken)
self.complete_long_flag_names(&parsed.available_flags, &ctoken)
} else if ctoken.starts_with('-') {
complete_short_flag_names(&parsed.available_flags, &ctoken)
self.complete_short_flag_names(&parsed.available_flags, &ctoken)
} else if let Some(flag) = parsed.flag_awaiting_value {
complete_arg(&ctx, spec, flag.arg.as_ref().unwrap(), &ctoken)?
self.complete_arg(&ctx, spec, flag.arg.as_ref().unwrap(), &ctoken)?
} else if let Some(arg) = parsed.cmd.args.get(parsed.args.len()) {
complete_arg(&ctx, spec, arg, &ctoken)?
self.complete_arg(&ctx, spec, arg, &ctoken)?
} else {
vec![]
};
Ok(choices)
}
}

fn complete_subcommands(cmd: &SpecCommand, ctoken: &str) -> Vec<String> {
trace!("complete_subcommands: {ctoken}");
let mut choices = vec![];
for subcommand in cmd.subcommands.values() {
if subcommand.hide {
continue;
}
choices.push(subcommand.name.clone());
for alias in &subcommand.aliases {
choices.push(alias.clone());
fn complete_subcommands(&self, cmd: &SpecCommand, ctoken: &str) -> Vec<(String, String)> {
trace!("complete_subcommands: {ctoken}");
let mut choices = vec![];
for subcommand in cmd.subcommands.values() {
if subcommand.hide {
continue;
}
choices.push((
subcommand.name.clone(),
subcommand.help.clone().unwrap_or_default(),
));
for alias in &subcommand.aliases {
choices.push((alias.clone(), subcommand.help.clone().unwrap_or_default()));
}
}
choices
.into_iter()
.filter(|(c, _)| c.starts_with(ctoken))
.sorted()
.collect()
}
choices
.into_iter()
.filter(|c| c.starts_with(ctoken))
.sorted()
.collect()
}

fn complete_long_flag_names(flags: &BTreeMap<String, SpecFlag>, ctoken: &str) -> Vec<String> {
debug!("complete_long_flag_names: {ctoken}");
trace!("flags: {}", flags.keys().join(", "));
let ctoken = ctoken.strip_prefix("--").unwrap_or(ctoken);
flags
.values()
.filter(|f| !f.hide)
.flat_map(|f| &f.long)
.unique()
.filter(|c| c.starts_with(ctoken))
.map(|c| format!("--{c}"))
.sorted()
.collect()
}

fn complete_short_flag_names(flags: &BTreeMap<String, SpecFlag>, ctoken: &str) -> Vec<String> {
debug!("complete_short_flag_names: {ctoken}");
let cur = ctoken.chars().nth(1);
flags
.values()
.filter(|f| !f.hide)
.flat_map(|f| &f.short)
.unique()
.filter(|c| cur.is_none() || cur == Some(**c))
.map(|c| format!("-{c}"))
.sorted()
.collect()
}

fn complete_builtin(type_: &str, ctoken: &str) -> Vec<String> {
match (type_, env::current_dir()) {
("path", Ok(cwd)) => complete_path(&cwd, ctoken, |_| true),
("dir", Ok(cwd)) => complete_path(&cwd, ctoken, |p| p.is_dir()),
("file", Ok(cwd)) => complete_path(&cwd, ctoken, |p| p.is_file()),
_ => vec![],
fn complete_long_flag_names(
&self,
flags: &BTreeMap<String, SpecFlag>,
ctoken: &str,
) -> Vec<(String, String)> {
debug!("complete_long_flag_names: {ctoken}");
trace!("flags: {}", flags.keys().join(", "));
let ctoken = ctoken.strip_prefix("--").unwrap_or(ctoken);
flags
.values()
.filter(|f| !f.hide)
.flat_map(|f| &f.long)
.unique()
.filter(|c| c.starts_with(ctoken))
// TODO: get flag description
.map(|c| (format!("--{c}"), String::new()))
.sorted()
.collect()
}
}

fn complete_arg(
ctx: &tera::Context,
spec: &Spec,
arg: &SpecArg,
ctoken: &str,
) -> miette::Result<Vec<String>> {
static EMPTY_COMPL: Lazy<Complete> = Lazy::new(Complete::default);

trace!("complete_arg: {arg} {ctoken}");
let name = arg.name.to_lowercase();
let complete = spec.complete.get(&name).unwrap_or(&EMPTY_COMPL);
let type_ = complete.type_.as_ref().unwrap_or(&name);

let builtin = complete_builtin(type_, ctoken);
if !builtin.is_empty() {
return Ok(builtin);
fn complete_short_flag_names(
&self,
flags: &BTreeMap<String, SpecFlag>,
ctoken: &str,
) -> Vec<(String, String)> {
debug!("complete_short_flag_names: {ctoken}");
let cur = ctoken.chars().nth(1);
flags
.values()
.filter(|f| !f.hide)
.flat_map(|f| &f.short)
.unique()
.filter(|c| cur.is_none() || cur == Some(**c))
// TODO: get flag description
.map(|c| (format!("-{c}"), String::new()))
.sorted()
.collect()
}

if let Some(run) = &complete.run {
let run = tera::Tera::one_off(run, ctx, false).into_diagnostic()?;
trace!("run: {run}");
let stdout = sh(&run)?;
// trace!("stdout: {stdout}");
return Ok(stdout
.lines()
.filter(|l| l.starts_with(ctoken))
.map(|l| l.to_string())
.collect());
fn complete_builtin(&self, type_: &str, ctoken: &str) -> Vec<(String, String)> {
let names = match (type_, env::current_dir()) {
("path", Ok(cwd)) => self.complete_path(&cwd, ctoken, |_| true),
("dir", Ok(cwd)) => self.complete_path(&cwd, ctoken, |p| p.is_dir()),
("file", Ok(cwd)) => self.complete_path(&cwd, ctoken, |p| p.is_file()),
_ => vec![],
};
names.into_iter().map(|n| (n, String::new())).collect()
}

Ok(vec![])
}
fn complete_arg(
&self,
ctx: &tera::Context,
spec: &Spec,
arg: &SpecArg,
ctoken: &str,
) -> miette::Result<Vec<(String, String)>> {
static EMPTY_COMPL: Lazy<Complete> = Lazy::new(Complete::default);

trace!("complete_arg: {arg} {ctoken}");
let name = arg.name.to_lowercase();
let complete = spec.complete.get(&name).unwrap_or(&EMPTY_COMPL);
let type_ = complete.type_.as_ref().unwrap_or(&name);

let builtin = self.complete_builtin(type_, ctoken);
if !builtin.is_empty() {
return Ok(builtin);
}

if let Some(run) = &complete.run {
let run = tera::Tera::one_off(run, ctx, false).into_diagnostic()?;
trace!("run: {run}");
let stdout = sh(&run)?;
// trace!("stdout: {stdout}");
return Ok(stdout
.lines()
.filter(|l| l.starts_with(ctoken))
// TODO: allow a description somehow
.map(|l| (l.to_string(), String::new()))
.collect());
}

fn complete_path(base: &Path, ctoken: &str, filter: impl Fn(&Path) -> bool) -> Vec<String> {
trace!("complete_path: {ctoken}");
let path = PathBuf::from(ctoken);
let mut dir = path.parent().unwrap_or(&path).to_path_buf();
if dir.is_relative() {
dir = base.join(dir);
Ok(vec![])
}

fn complete_path(
&self,
base: &Path,
ctoken: &str,
filter: impl Fn(&Path) -> bool,
) -> Vec<String> {
trace!("complete_path: {ctoken}");
let path = PathBuf::from(ctoken);
let mut dir = path.parent().unwrap_or(&path).to_path_buf();
if dir.is_relative() {
dir = base.join(dir);
}
let mut prefix = path
.file_name()
.unwrap_or_default()
.to_string_lossy()
.to_string();
if path.is_dir() && ctoken.ends_with('/') {
dir = path.to_path_buf();
prefix = "".to_string();
};
std::fs::read_dir(dir)
.ok()
.into_iter()
.flatten()
.filter_map(Result::ok)
.filter(|de| de.file_name().to_string_lossy().starts_with(&prefix))
.map(|de| de.path())
.filter(|p| filter(p))
.map(|p| {
p.strip_prefix(base)
.unwrap_or(&p)
.to_string_lossy()
.to_string()
})
.sorted()
.collect()
}
let mut prefix = path
.file_name()
.unwrap_or_default()
.to_string_lossy()
.to_string();
if path.is_dir() && ctoken.ends_with('/') {
dir = path.to_path_buf();
prefix = "".to_string();
};
std::fs::read_dir(dir)
.ok()
.into_iter()
.flatten()
.filter_map(Result::ok)
.filter(|de| de.file_name().to_string_lossy().starts_with(&prefix))
.map(|de| de.path())
.filter(|p| filter(p))
.map(|p| {
p.strip_prefix(base)
.unwrap_or(&p)
.to_string_lossy()
.to_string()
})
.sorted()
.collect()
}

fn sh(script: &str) -> XXResult<String> {
Expand Down
8 changes: 4 additions & 4 deletions lib/src/complete/bash.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ _{bin}() {{
if [[ -z ${{_USAGE_SPEC_{bin_up}:-}} ]]; then
_USAGE_SPEC_{bin_up}="$({usage_cmd})"
fi
COMPREPLY=( $(usage complete-word -s "${{_USAGE_SPEC_{bin_up}}}" --cword="$COMP_CWORD" -- "${{COMP_WORDS[@]}}" ) )
COMPREPLY=( $(usage complete-word --shell bash -s "${{_USAGE_SPEC_{bin_up}}}" --cword="$COMP_CWORD" -- "${{COMP_WORDS[@]}}" ) )
if [[ $? -ne 0 ]]; then
unset COMPREPLY
fi
Expand Down Expand Up @@ -50,8 +50,8 @@ mod tests {
if [[ -z ${_USAGE_SPEC_MYCLI:-} ]]; then
_USAGE_SPEC_MYCLI="$(mycli complete --usage)"
fi
COMPREPLY=( $(usage complete-word -s "${_USAGE_SPEC_MYCLI}" --cword="$COMP_CWORD" -- "${COMP_WORDS[@]}" ) )

COMPREPLY=( $(usage complete-word --shell bash -s "${_USAGE_SPEC_MYCLI}" --cword="$COMP_CWORD" -- "${COMP_WORDS[@]}" ) )
if [[ $? -ne 0 ]]; then
unset COMPREPLY
fi
Expand Down
4 changes: 2 additions & 2 deletions lib/src/complete/fish.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ if ! command -v usage &> /dev/null
end
set _usage_spec_{bin} ({usage_cmd} | string collect)
complete -xc {bin} -a '(usage complete-word -s "$_usage_spec_{bin}" -- (commandline -cop) (commandline -t))'
complete -xc {bin} -a '(usage complete-word --shell fish -s "$_usage_spec_{bin}" -- (commandline -cop) (commandline -t))'
"#
)
}
Expand All @@ -37,7 +37,7 @@ mod tests {
end
set _usage_spec_mycli (mycli complete --usage | string collect)
complete -xc mycli -a '(usage complete-word -s "$_usage_spec_mycli" -- (commandline -cop) (commandline -t))'
complete -xc mycli -a '(usage complete-word --shell fish -s "$_usage_spec_mycli" -- (commandline -cop) (commandline -t))'
"###);
}
}
4 changes: 2 additions & 2 deletions lib/src/complete/zsh.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ _{bin}() {{
_store_cache _usage_{bin}_spec spec
fi
_arguments '*: :($(usage complete-word -s "$spec" -- "${{words[@]}}" ))'
_arguments "*: :(($(usage complete-word --shell zsh -s "$spec" -- "${{words[@]}}" )))"
return 0
}}
Expand Down Expand Up @@ -107,7 +107,7 @@ mod tests {
_store_cache _usage_mycli_spec spec
fi
_arguments '*: :($(usage complete-word -s "$spec" -- "${words[@]}" ))'
_arguments "*: :(($(usage complete-word --shell zsh -s "$spec" -- "${words[@]}" )))"
return 0
}
Expand Down

0 comments on commit a8afca7

Please sign in to comment.