Skip to content

feat(complete): group options by tag (in zsh)#6334

Open
bobrippling wants to merge 2 commits into
clap-rs:masterfrom
bobrippling:feat/zsh-completion-grouping
Open

feat(complete): group options by tag (in zsh)#6334
bobrippling wants to merge 2 commits into
clap-rs:masterfrom
bobrippling:feat/zsh-completion-grouping

Conversation

@bobrippling
Copy link
Copy Markdown

@bobrippling bobrippling commented Apr 3, 2026

This allows zsh to group completions by whether they're global, and then by their tag. For example:

$ dynamic -<tab>
completing "global" options
-v  -- --verbose
completing "Options" options
-F  -- --format
-h  -- Print help
-i  -- --input

Closes #6320

@bobrippling bobrippling force-pushed the feat/zsh-completion-grouping branch 11 times, most recently from e5a4b4d to 629cac1 Compare April 11, 2026 16:26
@bobrippling bobrippling marked this pull request as ready for review April 11, 2026 16:37
@bobrippling bobrippling changed the title feat(complete): group options by tag (in zsh) feat(complete): group options by global state and tag (in zsh) Apr 11, 2026
@bobrippling bobrippling force-pushed the feat/zsh-completion-grouping branch 4 times, most recently from 65f0b45 to b161c88 Compare April 11, 2026 19:35
Comment thread clap_complete/src/engine/candidate.rs Outdated
@bobrippling bobrippling force-pushed the feat/zsh-completion-grouping branch from b161c88 to 0609ae5 Compare April 13, 2026 17:11
@epage epage changed the title feat(complete): group options by global state and tag (in zsh) feat(complete): group options by tag (in zsh) Apr 13, 2026
Comment thread clap_complete/tests/testsuite/zsh.rs Outdated
Comment on lines 388 to +402
if [[ "$value" == */ ]]; then
local dir_no_slash="${value%/}"
if [[ "$completion" == *:* ]]; then
local desc="${completion#*:}"
if [[ "$value" == *:* ]]; then
local desc="${value#*:}"
dirs+=("$dir_no_slash:$desc")
else
dirs+=("$dir_no_slash")
fi
else
other+=("$completion")
if (( ${+tag_map["$tag"]} )); then # key exists?
tag_map["$tag"]+=$'\n'"$value"
else
tag_map["$tag"]="$value"
fi
fi
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this mean we aren't getting the / handling if a tag is present?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The opposite in fact - if a / is there, we'll handle it (since we test that first) and only perform the option grouping for remaining options

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Items with / should still have grouping though

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you, I see what it's for now - fix in place

Comment thread clap_complete/src/env/shells.rs Outdated
}
write!(
buf,
"{}:",
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is : is in a tag?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suppose we have two options:

  • Pick a different separator - but what if that separator is in the tag?
  • Avoid : in tags, either by filtering them out at this point, or making it part of the tag requirement

What do you think?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can pick an unlikely separator, like _SEP_

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thinking about it, I think we can avoid this entirely - I'll use the escape_value() function which looks to be for precisely this.

Comment thread clap_complete/src/env/shells.rs
@bobrippling bobrippling force-pushed the feat/zsh-completion-grouping branch 3 times, most recently from eeee9ab to db855d8 Compare May 1, 2026 17:15
@bobrippling bobrippling force-pushed the feat/zsh-completion-grouping branch from db855d8 to 9e85b97 Compare May 1, 2026 17:17
Comment thread clap_complete/tests/testsuite/zsh.rs Outdated
% zstyle ':completion:*' group-name ''
% zstyle ':completion:*:descriptions' format '%d'
% exhaustive -
"Options" options
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is redundant. In --help, we just list the header. We should likely do the same here as well.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it was getting my goat a bit too. Fixed, just the header is listed now - the expected output is:

% zstyle ':completion:*:descriptions' format '%d'
% exhaustive -<Tab>
Options                      <-------------------------- header
--generate      -- generate
-h              -- Print help
--empty-choice

Comment thread clap_complete/tests/testsuite/zsh.rs Outdated
Comment on lines +451 to +452
"zstyle ':completion:*' group-name ''",
"zstyle ':completion:*:descriptions' format '%d'",
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are these needed?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The format style is needed to have zsh show group descriptions - for example:

% ls -<TAB>
# ^ no option completions


% autoload -Uz compinit
% compinit
% ls -<TAB>
-@      -- display extended attribute keys and sizes in long listing                                                                                                                          
-1      -- single column output                                                                                                                                                               
-a      -- list entries starting with .                                                                                                                                                       
-A      -- list all except . and ..                                                                                                                                                           
...
# ^ options are shown

% zstyle ':completion:*:descriptions' format '%d'
% ls -<TAB>
option      # <----
-@      -- display extended attribute keys and sizes in long listing                          
-1      -- single column output                                                               
-a      -- list entries starting with .                                                       
-A      -- list all except . and ..                                                           
...
# ^ note that the "description" header is now shown, in this case a plain "option"

(group-name isn't needed so I've dropped it)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a user setting needed to opt-in? Should we document that in the setup instructions?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, should we have the tests opt-in by default?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a user setting needed to opt-in? Should we document that in the setup instructions?

It is - added to the docs, let me know what you think:

//! To show completions grouped by category (options, commands, etc.), also add:
//! ```zsh
//! echo "zstyle ':completion:*:descriptions' format '%d'" >> ~/.zshrc
//! ```

Also, should we have the tests opt-in by default?

I think that would be useful to spot regressions. Would you like me to do that in this PR or a separate one?

(If it helps, I have it ready to go - 1 file changed, 129 insertions(+), 37 deletions(-))

@bobrippling bobrippling force-pushed the feat/zsh-completion-grouping branch 2 times, most recently from 5fea91e to a9f4522 Compare May 7, 2026 16:45
Comment thread clap_complete/src/env/shells.rs Outdated
write!(buf, "{}", ifs.as_deref().unwrap_or("\n"))?;
}

let tag = format!("{}", candidate.get_tag().unwrap_or(&StyledStr::from("")));
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A temporary hack

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is this hack addressing and what should it be replaced by?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, disregard that - it's necessary in order for us to escape the string. We need a &str (rather than a &StyledStr) in order to call escape_value(), which costs us an allocation here.

I've simplified this now, too

@bobrippling bobrippling force-pushed the feat/zsh-completion-grouping branch 2 times, most recently from 97bba1a to de9a2f5 Compare May 7, 2026 17:13
@bobrippling bobrippling marked this pull request as draft May 7, 2026 18:00
if comp.get_tag().is_some() {
comp
} else {
comp.tag(Some(arg.to_string().into()))
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why was this changed?

The intention here is that a positional's value (and maybe an option's value) can have many possible values and it would help the user to name the context we are completing within. Only flags should we default to completing under a header.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reverted (this was a foray into a slightly different approach)

Comment thread clap_complete/src/env/shells.rs Outdated
write!(buf, "{}", ifs.as_deref().unwrap_or("\n"))?;
}

let tag = format!("{}", candidate.get_tag().unwrap_or(&StyledStr::from("")));
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is this hack addressing and what should it be replaced by?

@bobrippling bobrippling force-pushed the feat/zsh-completion-grouping branch 3 times, most recently from a189e12 to 0f3a37b Compare May 19, 2026 20:54
@bobrippling bobrippling force-pushed the feat/zsh-completion-grouping branch 2 times, most recently from d8db0b5 to c1b8c79 Compare May 19, 2026 21:19
@bobrippling bobrippling force-pushed the feat/zsh-completion-grouping branch from c1b8c79 to 5b6a1ca Compare May 19, 2026 21:28
@bobrippling bobrippling marked this pull request as ready for review May 19, 2026 21:38
@@ -275,16 +275,22 @@ fn complete_dynamic_env_quoted_help() {
let input = "exhaustive quote \t\t";
let expected = snapbox::str![[r#"
% exhaustive quote
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this test, the --backslash and cmd-backslash have the same description but (I expect) different tags, so I think that's why they're now split

Perhaps we ensure their tags are the same to continue to group them? (if I'm right about the tagging - out of time but I can check later)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes - it's the tags. Options get tagged as "Options" by default:

.tag(Some(
arg.get_help_heading()
.unwrap_or("Options")
.to_owned()
.into(),
))

and commands similarly:

.tag(Some(
cmd.get_subcommand_help_heading()
.unwrap_or("Commands")
.to_owned()
.into(),
))


To keep the ordering in the tests the same, we could tag commands as options (for the test only):

                     .help("enum"),
             ]),
             clap::Command::new("quote")
+                .subcommand_help_heading("Options") // group subcommands with options
                 .args([
                     clap::Arg::new("single-quotes")
                         .long("single-quotes")

(in clap_complete/examples/exhaustive.rs)

or we could remove the default tag entirely, which is a larger change which would affect not just the tests

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Zsh completion tagging / grouping

3 participants