Repository: clap-rs/clap
Pull request #5775 (2024-10-13 19:19:42)

feat(complete): Add descriptions to dynamic Zsh completions
When using `unstable-dynamic` completions in Zsh, suggested values are not described. It's a step backwards compared to the default behavior and it makes things more difficult for users.

I had a quick look and it seems relatively easy to fix. But I'm not familiar with either clap or Zsh autocompletion, so I may miss something here.

vivienm: Also, I had to add this to my zshrc to prevent zsh from changing the order of suggestions:

```zsh
zstyle ':completion:*:*:my-program:*:*' sort false
```

Not necessarily a problem, but it could be worth documenting, or maybe write it in the completion script?
epage: > Not necessarily a problem, but it could be worth documenting, or maybe write it in the completion script?

We generally want things sorted according to our order.  We tweaked bash to ensure this.  If you know how to do insert this in our script to be respected, please help us out and post a PR for this!

Patch:
diff --git a/clap_complete/src/env/shells.rs b/clap_complete/src/env/shells.rs
index 72966475..5600927d 100644
--- a/clap_complete/src/env/shells.rs
+++ b/clap_complete/src/env/shells.rs
@@ -361,7 +361,7 @@ function _clap_dynamic_completer_NAME() {
     )}")
 
     if [[ -n $completions ]]; then
-        compadd -a completions
+        _describe 'values' completions
     fi
 }
 
@@ -398,8 +398,31 @@ compdef _clap_dynamic_completer_NAME BIN"#
             if i != 0 {
                 write!(buf, "{}", ifs.as_deref().unwrap_or("\n"))?;
             }
-            write!(buf, "{}", candidate.get_value().to_string_lossy())?;
+            write!(
+                buf,
+                "{}",
+                Self::escape_value(&candidate.get_value().to_string_lossy())
+            )?;
+            if let Some(help) = candidate.get_help() {
+                write!(
+                    buf,
+                    ":{}",
+                    Self::escape_help(help.to_string().lines().next().unwrap_or_default())
+                )?;
+            }
         }
         Ok(())
     }
 }
+
+impl Zsh {
+    /// Escape value string
+    fn escape_value(string: &str) -> String {
+        string.replace('\\', "\\\\").replace(':', "\\:")
+    }
+
+    /// Escape help string
+    fn escape_help(string: &str) -> String {
+        string.replace('\\', "\\\\")
+    }
+}


Test patch:
diff --git a/clap_complete/tests/snapshots/home/dynamic-env/exhaustive/zsh/zsh/_exhaustive b/clap_complete/tests/snapshots/home/dynamic-env/exhaustive/zsh/zsh/_exhaustive
index e59950fc..34af9fbe 100644
--- a/clap_complete/tests/snapshots/home/dynamic-env/exhaustive/zsh/zsh/_exhaustive
+++ b/clap_complete/tests/snapshots/home/dynamic-env/exhaustive/zsh/zsh/_exhaustive
@@ -11,7 +11,7 @@ function _clap_dynamic_completer_exhaustive() {
     )}")
 
     if [[ -n $completions ]]; then
-        compadd -a completions
+        _describe 'values' completions
     fi
 }
 
diff --git a/clap_complete/tests/testsuite/zsh.rs b/clap_complete/tests/testsuite/zsh.rs
index 575d4fbb..757c33f1 100644
--- a/clap_complete/tests/testsuite/zsh.rs
+++ b/clap_complete/tests/testsuite/zsh.rs
@@ -192,8 +192,12 @@ fn complete_dynamic_env_toplevel() {
     let input = "exhaustive \t\t";
     let expected = snapbox::str![[r#"
 % exhaustive
---generate  --help      action      help        last        quote       
---global    --version   alias       hint        pacman      value       
+--generate  -- generate
+--global    -- everywhere
+--help      -- Print help
+--version   -- Print version
+help        -- Print this message or the help of the given subcommand(s)
+action  alias   hint    last    pacman  quote   value
 "#]];
     let actual = runtime.complete(input, &term).unwrap();
     assert_data_eq!(actual, expected);
@@ -213,9 +217,18 @@ fn complete_dynamic_env_quoted_help() {
     let input = "exhaustive quote \t\t";
     let expected = snapbox::str![[r#"
 % exhaustive quote
---backslash        --choice           --global           --version          cmd-brackets       cmd-single-quotes
---backticks        --double-quotes    --help             cmd-backslash      cmd-double-quotes  escape-help
---brackets         --expansions       --single-quotes    cmd-backticks      cmd-expansions     help
+--global                            -- everywhere                                                                     
+--help                              -- Print help (see more with '--help')                                            
+--version                           -- Print version                                                                  
+cmd-backslash      --backslash      -- Avoid '/n'                                                                     
+cmd-backticks      --backticks      -- For more information see `echo test`                                           
+cmd-brackets       --brackets       -- List packages [filter]                                                         
+cmd-double-quotes  --double-quotes  -- Can be "always", "auto", or "never"                                            
+cmd-expansions     --expansions     -- Execute the shell command with $SHELL                                          
+cmd-single-quotes  --single-quotes  -- Can be 'always', 'auto', or 'never'                                            
+escape-help                         -- /tab/t"'                                                                       
+help                                -- Print this message or the help of the given subcommand(s)                      
+--choice
 "#]];
     let actual = runtime.complete(input, &term).unwrap();
     assert_data_eq!(actual, expected);
@@ -260,7 +273,10 @@ fn complete_dynamic_env_quoted_value() {
     let input = "exhaustive quote --choice \t\t";
     let expected = snapbox::str![[r#"
 % exhaustive quote --choice
-another/ shell  bash            fish            zsh
+another shell  -- something with a space
+bash           -- bash (shell)
+fish           -- fish shell
+zsh            -- zsh shell
 "#]];
     let actual = runtime.complete(input, &term).unwrap();
     assert_data_eq!(actual, expected);