Skip to content

Commit

Permalink
feat(clap_complete): Add zsh support for native completion
Browse files Browse the repository at this point in the history
  • Loading branch information
shannmu committed Jun 25, 2024
1 parent 70e8417 commit ec7a147
Show file tree
Hide file tree
Showing 9 changed files with 132 additions and 4 deletions.
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;

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 exhaustive
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
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 @@ -110,7 +110,7 @@ complete -c exhaustive -n "__fish_seen_subcommand_from hint" -l email -r -f
complete -c exhaustive -n "__fish_seen_subcommand_from hint" -l global -d 'everywhere'
complete -c exhaustive -n "__fish_seen_subcommand_from hint" -s h -l help -d 'Print help'
complete -c exhaustive -n "__fish_seen_subcommand_from hint" -s V -l version -d 'Print version'
complete -c exhaustive -n "__fish_seen_subcommand_from complete" -l shell -d 'Specify shell to complete for' -r -f -a "{bash '',fish ''}"
complete -c exhaustive -n "__fish_seen_subcommand_from complete" -l shell -d 'Specify shell to complete for' -r -f -a "{bash '',fish '',zsh ''}"
complete -c exhaustive -n "__fish_seen_subcommand_from complete" -l register -d 'Path to write completion-registration to' -r -F
complete -c exhaustive -n "__fish_seen_subcommand_from complete" -l global -d 'everywhere'
complete -c exhaustive -n "__fish_seen_subcommand_from 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);
}

0 comments on commit ec7a147

Please sign in to comment.