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

feat(clap_complete): add native completion support for zsh #5545

Merged
merged 1 commit into from
Jul 17, 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
2 changes: 2 additions & 0 deletions clap_complete/src/dynamic/shells/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 _;
Expand Down
6 changes: 5 additions & 1 deletion clap_complete/src/dynamic/shells/shell.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ pub enum Shell {
Bash,
/// Friendly Interactive `SHell` (fish)
Fish,
/// Z shell (zsh)
Zsh,
}

impl Display for Shell {
Expand Down Expand Up @@ -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<PossibleValue> {
Some(match self {
Shell::Bash => PossibleValue::new("bash"),
Shell::Fish => PossibleValue::new("fish"),
Shell::Zsh => PossibleValue::new("zsh"),
})
}
}
Expand All @@ -55,6 +58,7 @@ impl Shell {
match self {
Self::Bash => &super::Bash,
Self::Fish => &super::Fish,
Self::Zsh => &super::Zsh,
}
}
}
Expand Down
65 changes: 65 additions & 0 deletions clap_complete/src/dynamic/shells/zsh.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/// Completion support for zsh
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
pub struct Zsh;
epage marked this conversation as resolved.
Show resolved Hide resolved

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<std::ffi::OsString>,
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<String> = 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(())
}
}
Original file line number Diff line number Diff line change
@@ -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='%% '
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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\')'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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'\'')]' \
Expand Down
39 changes: 39 additions & 0 deletions clap_complete/tests/testsuite/zsh.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::<completest_pty::ZshRuntimeBuilder>("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::<completest_pty::ZshRuntimeBuilder>("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);
}
Loading