From cef9393c5d4a29fe366e31c601e37f7e1b7efaa5 Mon Sep 17 00:00:00 2001 From: shannmu Date: Tue, 25 Jun 2024 13:31:57 +0800 Subject: [PATCH] feat(clap_complete): Add zsh support for native completion --- clap_complete/src/dynamic/shells/mod.rs | 2 + clap_complete/src/dynamic/shells/shell.rs | 6 +- clap_complete/src/dynamic/shells/zsh.rs | 65 +++++++++++++++++++ .../home/dynamic/exhaustive/zsh/.zshenv | 5 ++ .../dynamic/exhaustive/zsh/zsh/_exhaustive | 13 ++++ .../home/static/exhaustive/bash/.bashrc | 2 +- .../fish/fish/completions/exhaustive.fish | 2 +- .../static/exhaustive/zsh/zsh/_exhaustive | 2 +- clap_complete/tests/testsuite/zsh.rs | 39 +++++++++++ 9 files changed, 132 insertions(+), 4 deletions(-) create mode 100644 clap_complete/src/dynamic/shells/zsh.rs create mode 100644 clap_complete/tests/snapshots/home/dynamic/exhaustive/zsh/.zshenv create mode 100644 clap_complete/tests/snapshots/home/dynamic/exhaustive/zsh/zsh/_exhaustive diff --git a/clap_complete/src/dynamic/shells/mod.rs b/clap_complete/src/dynamic/shells/mod.rs index 6cfed97e..8caff617 100644 --- a/clap_complete/src/dynamic/shells/mod.rs +++ b/clap_complete/src/dynamic/shells/mod.rs @@ -3,10 +3,12 @@ mod bash; mod fish; mod shell; +mod zsh; pub use bash::*; pub use fish::*; pub use shell::*; +pub use zsh::*; use std::ffi::OsString; use std::io::Write as _; diff --git a/clap_complete/src/dynamic/shells/shell.rs b/clap_complete/src/dynamic/shells/shell.rs index 51386514..c7f0a970 100644 --- a/clap_complete/src/dynamic/shells/shell.rs +++ b/clap_complete/src/dynamic/shells/shell.rs @@ -12,6 +12,8 @@ pub enum Shell { Bash, /// Friendly Interactive `SHell` (fish) Fish, + /// Z shell (zsh) + Zsh, } impl Display for Shell { @@ -39,13 +41,14 @@ impl FromStr for Shell { // Hand-rolled so it can work even when `derive` feature is disabled impl ValueEnum for Shell { fn value_variants<'a>() -> &'a [Self] { - &[Shell::Bash, Shell::Fish] + &[Shell::Bash, Shell::Fish, Shell::Zsh] } fn to_possible_value(&self) -> Option { Some(match self { Shell::Bash => PossibleValue::new("bash"), Shell::Fish => PossibleValue::new("fish"), + Shell::Zsh => PossibleValue::new("zsh"), }) } } @@ -55,6 +58,7 @@ impl Shell { match self { Self::Bash => &super::Bash, Self::Fish => &super::Fish, + Self::Zsh => &super::Zsh, } } } diff --git a/clap_complete/src/dynamic/shells/zsh.rs b/clap_complete/src/dynamic/shells/zsh.rs new file mode 100644 index 00000000..d6e699f7 --- /dev/null +++ b/clap_complete/src/dynamic/shells/zsh.rs @@ -0,0 +1,65 @@ +/// Completion support for zsh +#[derive(Copy, Clone, PartialEq, Eq, Debug)] +pub struct Zsh; + +impl crate::dynamic::Completer for Zsh { + fn file_name(&self, name: &str) -> String { + format!("{name}.zsh") + } + fn write_registration( + &self, + _name: &str, + bin: &str, + completer: &str, + buf: &mut dyn std::io::Write, + ) -> Result<(), std::io::Error> { + let bin = shlex::quote(bin); + let completer = shlex::quote(completer); + let script = r#"#compdef BIN +function _clap_dynamic_completer() { + export _CLAP_COMPLETE_INDEX=$(expr $CURRENT - 1) + export _CLAP_IFS=$'\n' + + local completions=("${(@f)$(COMPLETER complete --shell zsh -- ${words} 2>/dev/null)}") + + if [[ -n $completions ]]; then + compadd -a completions + fi +} + +compdef _clap_dynamic_completer BIN"# + .replace("COMPLETER", &completer) + .replace("BIN", &bin); + + writeln!(buf, "{script}")?; + Ok(()) + } + fn write_complete( + &self, + cmd: &mut clap::Command, + args: Vec, + current_dir: Option<&std::path::Path>, + buf: &mut dyn std::io::Write, + ) -> Result<(), std::io::Error> { + let index: usize = std::env::var("_CLAP_COMPLETE_INDEX") + .ok() + .and_then(|i| i.parse().ok()) + .unwrap_or_default(); + let ifs: Option = std::env::var("_CLAP_IFS").ok().and_then(|i| i.parse().ok()); + + // If the current word is empty, add an empty string to the args + let mut args = args.clone(); + if args.len() == index { + args.push("".into()); + } + let completions = crate::dynamic::complete(cmd, args, index, current_dir)?; + + for (i, (completion, _)) in completions.iter().enumerate() { + if i != 0 { + write!(buf, "{}", ifs.as_deref().unwrap_or("\n"))?; + } + write!(buf, "{}", completion.to_string_lossy())?; + } + Ok(()) + } +} diff --git a/clap_complete/tests/snapshots/home/dynamic/exhaustive/zsh/.zshenv b/clap_complete/tests/snapshots/home/dynamic/exhaustive/zsh/.zshenv new file mode 100644 index 00000000..6d309f24 --- /dev/null +++ b/clap_complete/tests/snapshots/home/dynamic/exhaustive/zsh/.zshenv @@ -0,0 +1,5 @@ +fpath=($fpath $ZDOTDIR/zsh) +autoload -U +X compinit && compinit -u # bypass compaudit security checking +precmd_functions="" # avoid the prompt being overwritten +PS1='%% ' +PROMPT='%% ' diff --git a/clap_complete/tests/snapshots/home/dynamic/exhaustive/zsh/zsh/_exhaustive b/clap_complete/tests/snapshots/home/dynamic/exhaustive/zsh/zsh/_exhaustive new file mode 100644 index 00000000..97d2d1ab --- /dev/null +++ b/clap_complete/tests/snapshots/home/dynamic/exhaustive/zsh/zsh/_exhaustive @@ -0,0 +1,13 @@ +#compdef exhaustive +function _clap_dynamic_completer() { + export _CLAP_COMPLETE_INDEX=$(expr $CURRENT - 1) + export _CLAP_IFS=$'\n' + + local completions=("${(@f)$(exhaustive complete --shell zsh -- ${words} 2>/dev/null)}") + + if [[ -n $completions ]]; then + compadd -a completions + fi +} + +compdef _clap_dynamic_completer exhaustive diff --git a/clap_complete/tests/snapshots/home/static/exhaustive/bash/.bashrc b/clap_complete/tests/snapshots/home/static/exhaustive/bash/.bashrc index bfbaec6f..52999c45 100644 --- a/clap_complete/tests/snapshots/home/static/exhaustive/bash/.bashrc +++ b/clap_complete/tests/snapshots/home/static/exhaustive/bash/.bashrc @@ -245,7 +245,7 @@ _exhaustive() { fi case "${prev}" in --shell) - COMPREPLY=($(compgen -W "bash fish" -- "${cur}")) + COMPREPLY=($(compgen -W "bash fish zsh" -- "${cur}")) return 0 ;; --register) diff --git a/clap_complete/tests/snapshots/home/static/exhaustive/fish/fish/completions/exhaustive.fish b/clap_complete/tests/snapshots/home/static/exhaustive/fish/fish/completions/exhaustive.fish index a5bb92fb..dd52a170 100644 --- a/clap_complete/tests/snapshots/home/static/exhaustive/fish/fish/completions/exhaustive.fish +++ b/clap_complete/tests/snapshots/home/static/exhaustive/fish/fish/completions/exhaustive.fish @@ -136,7 +136,7 @@ complete -c exhaustive -n "__fish_exhaustive_using_subcommand hint" -l email -r complete -c exhaustive -n "__fish_exhaustive_using_subcommand hint" -l global -d 'everywhere' complete -c exhaustive -n "__fish_exhaustive_using_subcommand hint" -s h -l help -d 'Print help' complete -c exhaustive -n "__fish_exhaustive_using_subcommand hint" -s V -l version -d 'Print version' -complete -c exhaustive -n "__fish_exhaustive_using_subcommand complete" -l shell -d 'Specify shell to complete for' -r -f -a "{bash\t'',fish\t''}" +complete -c exhaustive -n "__fish_exhaustive_using_subcommand complete" -l shell -d 'Specify shell to complete for' -r -f -a "{bash\t'',fish\t'',zsh\t''}" complete -c exhaustive -n "__fish_exhaustive_using_subcommand complete" -l register -d 'Path to write completion-registration to' -r -F complete -c exhaustive -n "__fish_exhaustive_using_subcommand complete" -l global -d 'everywhere' complete -c exhaustive -n "__fish_exhaustive_using_subcommand complete" -s h -l help -d 'Print help (see more with \'--help\')' diff --git a/clap_complete/tests/snapshots/home/static/exhaustive/zsh/zsh/_exhaustive b/clap_complete/tests/snapshots/home/static/exhaustive/zsh/zsh/_exhaustive index a49a847f..d6329ade 100644 --- a/clap_complete/tests/snapshots/home/static/exhaustive/zsh/zsh/_exhaustive +++ b/clap_complete/tests/snapshots/home/static/exhaustive/zsh/zsh/_exhaustive @@ -325,7 +325,7 @@ _arguments "${_arguments_options[@]}" : \ ;; (complete) _arguments "${_arguments_options[@]}" : \ -'--shell=[Specify shell to complete for]:SHELL:(bash fish)' \ +'--shell=[Specify shell to complete for]:SHELL:(bash fish zsh)' \ '--register=[Path to write completion-registration to]:REGISTER:_files' \ '--global[everywhere]' \ '-h[Print help (see more with '\''--help'\'')]' \ diff --git a/clap_complete/tests/testsuite/zsh.rs b/clap_complete/tests/testsuite/zsh.rs index 8acb42a6..e483e7fd 100644 --- a/clap_complete/tests/testsuite/zsh.rs +++ b/clap_complete/tests/testsuite/zsh.rs @@ -162,3 +162,42 @@ pacman action alias value quote hint last -- let actual = runtime.complete(input, &term).unwrap(); assert_data_eq!(actual, expected); } + + +#[cfg(all(unix, feature = "unstable-dynamic"))] +#[test] +fn register_dynamic() { + common::register_example::("dynamic", "exhaustive"); +} + +#[test] +#[cfg(all(unix, feature = "unstable-dynamic"))] +fn complete_dynamic() { + if !common::has_command("zsh") { + return; + } + + let term = completest::Term::new(); + let mut runtime = + common::load_runtime::("dynamic", "exhaustive"); + + let input = "exhaustive \t\t"; + let expected = snapbox::str![ + r#"% exhaustive +--generate --help -V action complete hint pacman value +--global --version -h alias help last quote "# + ]; + let actual = runtime.complete(input, &term).unwrap(); + assert_data_eq!(actual, expected); + + let input = "exhaustive quote \t\t"; + let expected = snapbox::str![ + r#"% exhaustive quote +--backslash --double-quotes --single-quotes cmd-backslash cmd-expansions +--backticks --expansions --version cmd-backticks cmd-single-quotes +--brackets --global -V cmd-brackets escape-help +--choice --help -h cmd-double-quotes help "# + ]; + let actual = runtime.complete(input, &term).unwrap(); + assert_data_eq!(actual, expected); +} \ No newline at end of file