Repository: clap-rs/clap
Pull request #5544 (2024-06-21 11:42:54)

feat(clap_complete): add native completion support for elvish
Related issue #3919

#### Work in this PR
- Add elvish glue script code to support that we could call external native completion.
- Add the same level of tests as fish

epage: > related issue: https://github.com/clap-rs/clap/issues/3919

If this isn't closing out the issue, what is left?
shannmu: >If this isn't closing out the issue, what is left?

I don't understand what you mean. Adding the related issue is for easier tracking :)
epage: > I don't understand what you mean. Adding the related issue is for easier tracking :)

The language used doesn't make this sound like the issue is fully resolved by this PR, so I was asking what is left.  In fact, there are standardized terms for fully resolving an issue as Github has support for [keywords](https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/using-keywords-in-issues-and-pull-requests) to link PRs to issues (not just backlinks) and even auto-close issues. 
epage: FYI this now has merge conflicts
epage: New set of conflicts to resolve

Patch:
diff --git a/clap_complete/src/dynamic/shells/elvish.rs b/clap_complete/src/dynamic/shells/elvish.rs
new file mode 100644
index 00000000..57351b17
--- /dev/null
+++ b/clap_complete/src/dynamic/shells/elvish.rs
@@ -0,0 +1,58 @@
+/// Completion support for Bash
+#[derive(Copy, Clone, PartialEq, Eq, Debug)]
+pub struct Elvish;
+
+impl crate::dynamic::Completer for Elvish {
+    fn file_name(&self, name: &str) -> String {
+        format!("{name}.elv")
+    }
+    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#"
+set edit:completion:arg-completer[BIN] = { |@words|
+    set E:_CLAP_IFS = "\n"
+
+    var index = (count $words)
+    set index = (- $index 1)
+    set E:_CLAP_COMPLETE_INDEX = (to-string $index)
+
+    put (COMPLETER complete --shell elvish -- $@words) | to-lines
+}
+"#
+        .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());
+        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/src/dynamic/shells/mod.rs b/clap_complete/src/dynamic/shells/mod.rs
index 8caff617..b7fed5da 100644
--- a/clap_complete/src/dynamic/shells/mod.rs
+++ b/clap_complete/src/dynamic/shells/mod.rs
@@ -1,11 +1,13 @@
 //! Shell support
 
 mod bash;
+mod elvish;
 mod fish;
 mod shell;
 mod zsh;
 
 pub use bash::*;
+pub use elvish::*;
 pub use fish::*;
 pub use shell::*;
 pub use zsh::*;
diff --git a/clap_complete/src/dynamic/shells/shell.rs b/clap_complete/src/dynamic/shells/shell.rs
index c7f0a970..dfc091c3 100644
--- a/clap_complete/src/dynamic/shells/shell.rs
+++ b/clap_complete/src/dynamic/shells/shell.rs
@@ -14,6 +14,8 @@ pub enum Shell {
     Fish,
     /// Z shell (zsh)
     Zsh,
+    /// Elf `SHell` (elvish)
+    Elvish,
 }
 
 impl Display for Shell {
@@ -41,7 +43,7 @@ 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::Zsh]
+        &[Shell::Bash, Shell::Fish, Shell::Zsh, Shell::Elvish]
     }
 
     fn to_possible_value(&self) -> Option<PossibleValue> {
@@ -49,6 +51,7 @@ impl ValueEnum for Shell {
             Shell::Bash => PossibleValue::new("bash"),
             Shell::Fish => PossibleValue::new("fish"),
             Shell::Zsh => PossibleValue::new("zsh"),
+            Shell::Elvish => PossibleValue::new("elvish"),
         })
     }
 }
@@ -59,6 +62,7 @@ impl Shell {
             Self::Bash => &super::Bash,
             Self::Fish => &super::Fish,
             Self::Zsh => &super::Zsh,
+            Self::Elvish => &super::Elvish,
         }
     }
 }


Test patch:
diff --git a/clap_complete/tests/snapshots/home/dynamic/exhaustive/elvish/elvish/rc.elv b/clap_complete/tests/snapshots/home/dynamic/exhaustive/elvish/elvish/rc.elv
new file mode 100644
index 00000000..2e54dd4f
--- /dev/null
+++ b/clap_complete/tests/snapshots/home/dynamic/exhaustive/elvish/elvish/rc.elv
@@ -0,0 +1,14 @@
+set edit:rprompt = (constantly "")
+set edit:prompt = (constantly "% ")
+
+set edit:completion:arg-completer[exhaustive] = { |@words|
+    set E:_CLAP_IFS = "\n"
+
+    var index = (count $words)
+    set index = (- $index 1)
+    set E:_CLAP_COMPLETE_INDEX = (to-string $index)
+
+    put (exhaustive complete --shell elvish -- $@words) | to-lines
+}
+
+
diff --git a/clap_complete/tests/snapshots/home/static/exhaustive/bash/.bashrc b/clap_complete/tests/snapshots/home/static/exhaustive/bash/.bashrc
index 52999c45..c552413c 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 zsh" -- "${cur}"))
+                    COMPREPLY=($(compgen -W "bash fish zsh elvish" -- "${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 dd52a170..d371096e 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'',zsh\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'',elvish\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 d6329ade..67fe3c40 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 zsh)' \
+'--shell=[Specify shell to complete for]:SHELL:(bash fish zsh elvish)' \
 '--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/elvish.rs b/clap_complete/tests/testsuite/elvish.rs
index 17cf1cf6..2aa95f7e 100644
--- a/clap_complete/tests/testsuite/elvish.rs
+++ b/clap_complete/tests/testsuite/elvish.rs
@@ -174,3 +174,43 @@ value          value
     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::ElvishRuntimeBuilder>("dynamic", "exhaustive");
+}
+
+#[test]
+#[cfg(all(unix, feature = "unstable-dynamic"))]
+fn complete_dynamic() {
+    if !common::has_command("elvish") {
+        return;
+    }
+
+    let term = completest::Term::new();
+    let mut runtime =
+        common::load_runtime::<completest_pty::ElvishRuntimeBuilder>("dynamic", "exhaustive");
+
+    let input = "exhaustive \t";
+    let expected = snapbox::str![
+        r#"% exhaustive --generate
+ COMPLETING argument  
+--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";
+    let expected = snapbox::str![
+        r#"% exhaustive quote --backslash
+ COMPLETING argument  
+--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