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

fix: allow calling usage g completion -f #143

Merged
merged 1 commit into from
Oct 31, 2024
Merged
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
34 changes: 18 additions & 16 deletions cli/src/cli/generate/completion.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
use clap::Args;
use std::path::PathBuf;
use usage::Spec;

#[derive(Args)]
#[clap(visible_alias = "c", aliases = ["complete", "completions"])]
Expand All @@ -14,31 +16,31 @@ pub struct Completion {
/// Defaults to "$bin --usage"
#[clap(long)]
usage_cmd: Option<String>,
// #[clap(short, long)]
// file: Option<PathBuf>,
//
#[clap(short, long)]
file: Option<PathBuf>,
// #[clap(short, long, required_unless_present = "file", overrides_with = "file")]
// spec: Option<String>,
}

impl Completion {
pub fn run(&self) -> miette::Result<()> {
// let spec = if let Some(file) = &self.file {
// let (spec, _) = Spec::parse_file(file)?;
// spec
// } else {
// Spec::parse_spec(self.spec.as_ref().unwrap())?
// };
// TODO: refactor this
let (spec, _) = match &self.file {
Some(file) => Spec::parse_file(file)?,
None => (Spec::default(), "".to_string()),
};
let spec = match self.file.is_some() {
true => Some(&spec),
false => None,
};

let bin = &self.bin;
let usage_cmd = self
.usage_cmd
.clone()
.unwrap_or_else(|| format!("{bin} --usage"));
let usage_cmd = self.usage_cmd.as_deref();

let script = match self.shell.as_str() {
"bash" => usage::complete::bash::complete_bash(bin, &usage_cmd),
"fish" => usage::complete::fish::complete_fish(bin, &usage_cmd),
"zsh" => usage::complete::zsh::complete_zsh(bin, &usage_cmd),
"bash" => usage::complete::bash::complete_bash(bin, usage_cmd, spec),
"fish" => usage::complete::fish::complete_fish(bin, usage_cmd, spec),
"zsh" => usage::complete::zsh::complete_zsh(bin, usage_cmd, spec),
_ => unreachable!(),
};
println!("{}", script.trim());
Expand Down
93 changes: 82 additions & 11 deletions lib/src/complete/bash.rs
Original file line number Diff line number Diff line change
@@ -1,24 +1,39 @@
use crate::Spec;
use heck::ToShoutySnakeCase;

pub fn complete_bash(bin: &str, usage_cmd: &str) -> String {
// let usage = env::USAGE_BIN.display();
pub fn complete_bash(bin: &str, usage_cmd: Option<&str>, spec: Option<&Spec>) -> String {
let bin_up = bin.to_shouty_snake_case();
// let bin = &spec.bin;
// let raw = shell_escape::unix::escape(spec.to_string().into());
format!(
r#"
_{bin}() {{
let mut out = vec![format!(
r#"_{bin}() {{
if ! command -v usage &> /dev/null; then
echo >&2
echo "Error: usage CLI not found. This is required for completions to work in {bin}." >&2
echo "See https://usage.jdx.dev for more information." >&2
return 1
fi
fi"#
)];

if let Some(usage_cmd) = &usage_cmd {
out.push(format!(
r#"
if [[ -z ${{_USAGE_SPEC_{bin_up}:-}} ]]; then
_USAGE_SPEC_{bin_up}="$({usage_cmd})"
fi
fi"#
));
}

if let Some(spec) = &spec {
out.push(format!(
r#"
read -r -d '' _USAGE_SPEC_{bin_up} <<'__USAGE_EOF__'
{spec}
__USAGE_EOF__"#,
spec = spec.to_string().trim()
));
}

out.push(format!(
r#"
COMPREPLY=( $(usage complete-word --shell bash -s "${{_USAGE_SPEC_{bin_up}}}" --cword="$COMP_CWORD" -- "${{COMP_WORDS[@]}}" ) )
if [[ $? -ne 0 ]]; then
unset COMPREPLY
Expand All @@ -29,17 +44,20 @@ _{bin}() {{
shopt -u hostcomplete && complete -o nospace -o bashdefault -o nosort -F _{bin} {bin}
# vim: noet ci pi sts=0 sw=4 ts=4 ft=sh
"#
)
));

out.join("\n")
}

#[cfg(test)]
mod tests {
use super::*;
use crate::test::SPEC_KITCHEN_SINK;
use insta::assert_snapshot;

#[test]
fn test_complete_bash() {
assert_snapshot!(complete_bash("mycli", "mycli complete --usage").trim(), @r###"
assert_snapshot!(complete_bash("mycli", Some("mycli complete --usage"), None).trim(), @r###"
_mycli() {
if ! command -v usage &> /dev/null; then
echo >&2
Expand All @@ -62,5 +80,58 @@ mod tests {
shopt -u hostcomplete && complete -o nospace -o bashdefault -o nosort -F _mycli mycli
# vim: noet ci pi sts=0 sw=4 ts=4 ft=sh
"###);

assert_snapshot!(complete_bash("mycli", None, Some(&SPEC_KITCHEN_SINK)).trim(), @r##"
_mycli() {
if ! command -v usage &> /dev/null; then
echo >&2
echo "Error: usage CLI not found. This is required for completions to work in mycli." >&2
echo "See https://usage.jdx.dev for more information." >&2
return 1
fi

read -r -d '' _USAGE_SPEC_MYCLI <<'__USAGE_EOF__'
name "mycli"
bin "mycli"
source_code_link_template "https://github.com/jdx/mise/blob/main/src/cli/{{path}}.rs"
flag "--flag1" help="flag1 description"
flag "--flag2" help="flag2 description" {
long_help "flag2 long description"
}
flag "--flag3" help="flag3 description" negate="--no-flag3"
flag "--shell" {
arg "<shell>" {
choices "bash" "zsh" "fish"
}
}
arg "<arg1>" help="arg1 description"
arg "<arg2>" help="arg2 description" default="default value" {
choices "choice1" "choice2" "choice3"
}
arg "<arg3>" help="arg3 description" help_long="arg3 long description"
arg "<argrest>..." var=true
cmd "plugin" {
cmd "install" {
flag "-g --global"
flag "-d --dir" {
arg "<dir>"
}
flag "-f --force" negate="--no-force"
arg "<plugin>"
arg "<version>"
}
}
__USAGE_EOF__

COMPREPLY=( $(usage complete-word --shell bash -s "${_USAGE_SPEC_MYCLI}" --cword="$COMP_CWORD" -- "${COMP_WORDS[@]}" ) )
if [[ $? -ne 0 ]]; then
unset COMPREPLY
fi
return 0
}

shopt -u hostcomplete && complete -o nospace -o bashdefault -o nosort -F _mycli mycli
# vim: noet ci pi sts=0 sw=4 ts=4 ft=sh
"##);
}
}
82 changes: 71 additions & 11 deletions lib/src/complete/fish.rs
Original file line number Diff line number Diff line change
@@ -1,34 +1,51 @@
pub fn complete_fish(bin: &str, usage_cmd: &str) -> String {
// let usage = env::USAGE_BIN.display();
// let bin = &spec.bin;
// let raw = spec.to_string().replace('\'', r"\'").to_string();
format!(
use crate::Spec;

pub fn complete_fish(bin: &str, usage_cmd: Option<&str>, spec: Option<&Spec>) -> String {
let mut out = vec![format!(
r#"
# if "usage" is not installed show an error
if ! command -v usage &> /dev/null
echo >&2
echo "Error: usage CLI not found. This is required for completions to work in {bin}." >&2
echo "See https://usage.jdx.dev for more information." >&2
return 1
end
end"#
)];

if let Some(usage_cmd) = &usage_cmd {
out.push(format!(
r#"
set _usage_spec_{bin} ({usage_cmd} | string collect)"#
));
}

if let Some(spec) = &spec {
let spec_escaped = spec.to_string().replace("'", r"\'");
out.push(format!(
r#"
set -x _usage_spec_{bin} '{spec_escaped}'"#
));
}

out.push(format!(
r#"complete -xc {bin} -a '(usage complete-word --shell fish -s "$_usage_spec_{bin}" -- (commandline -cop) (commandline -t))'"#
));

set _usage_spec_{bin} ({usage_cmd} | string collect)
complete -xc {bin} -a '(usage complete-word --shell fish -s "$_usage_spec_{bin}" -- (commandline -cop) (commandline -t))'
"#
)
out.join("\n")
}

#[cfg(test)]
mod tests {
use super::*;
use crate::test::SPEC_KITCHEN_SINK;
use insta::assert_snapshot;

#[test]
fn test_complete_fish() {
// let spec = r#"
// "#;
// let spec = Spec::parse(&Default::default(), spec).unwrap();
assert_snapshot!(complete_fish("mycli", "mycli complete --usage").trim(), @r###"
assert_snapshot!(complete_fish("mycli", Some("mycli complete --usage"), None).trim(), @r###"
# if "usage" is not installed show an error
if ! command -v usage &> /dev/null
echo >&2
Expand All @@ -40,5 +57,48 @@ mod tests {
set _usage_spec_mycli (mycli complete --usage | string collect)
complete -xc mycli -a '(usage complete-word --shell fish -s "$_usage_spec_mycli" -- (commandline -cop) (commandline -t))'
"###);

assert_snapshot!(complete_fish("mycli", None, Some(&SPEC_KITCHEN_SINK)).trim(), @r##"
# if "usage" is not installed show an error
if ! command -v usage &> /dev/null
echo >&2
echo "Error: usage CLI not found. This is required for completions to work in mycli." >&2
echo "See https://usage.jdx.dev for more information." >&2
return 1
end

set -x _usage_spec_mycli 'name "mycli"
bin "mycli"
source_code_link_template "https://github.com/jdx/mise/blob/main/src/cli/{{path}}.rs"
flag "--flag1" help="flag1 description"
flag "--flag2" help="flag2 description" {
long_help "flag2 long description"
}
flag "--flag3" help="flag3 description" negate="--no-flag3"
flag "--shell" {
arg "<shell>" {
choices "bash" "zsh" "fish"
}
}
arg "<arg1>" help="arg1 description"
arg "<arg2>" help="arg2 description" default="default value" {
choices "choice1" "choice2" "choice3"
}
arg "<arg3>" help="arg3 description" help_long="arg3 long description"
arg "<argrest>..." var=true
cmd "plugin" {
cmd "install" {
flag "-g --global"
flag "-d --dir" {
arg "<dir>"
}
flag "-f --force" negate="--no-force"
arg "<plugin>"
arg "<version>"
}
}
'
complete -xc mycli -a '(usage complete-word --shell fish -s "$_usage_spec_mycli" -- (commandline -cop) (commandline -t))'
"##);
}
}
Loading