Skip to content
Open
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
5 changes: 5 additions & 0 deletions clap_complete/src/env/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,11 @@
//! echo "source <(COMPLETE=zsh your_program)" >> ~/.zshrc
//! ```
//!
//! To show completions grouped by category (options, commands, etc.), also add:
//! ```zsh
//! echo "zstyle ':completion:*:descriptions' format '%d'" >> ~/.zshrc
//! ```
//!
//! To disable completions, you can set `COMPLETE=` or `COMPLETE=0`

mod shells;
Expand Down
64 changes: 54 additions & 10 deletions clap_complete/src/env/shells.rs
Comment thread
epage marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -431,25 +431,61 @@ function _clap_dynamic_completer_NAME() {
)}")

if [[ -n $completions ]]; then
local -a dirs=()
local -a other=()
local -A tag_map tag_map_dirs
local completion
local tag
local value
local to_add

for completion in $completions; do
local value="${completion%%:*}"
IFS=: read -r tag value <<< "$completion"

if [[ "$value" == */ ]]; then
local dir_no_slash="${value%/}"
if [[ "$completion" == *:* ]]; then
local desc="${completion#*:}"
dirs+=("$dir_no_slash:$desc")

if [[ "$value" == *:* ]]; then
local desc="${value#*:}"
to_add="$dir_no_slash:$desc"
else
dirs+=("$dir_no_slash")
to_add="$dir_no_slash"
fi

tag_map_dirs[$tag]+=${tag_map_dirs[$tag]:+$'\n'}"$to_add"
else
other+=("$completion")
tag_map[$tag]+=${tag_map[$tag]:+$'\n'}"$value"
fi
Comment on lines 443 to +456
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

done
[[ -n $dirs ]] && _describe -V 'values' dirs -S '/' -r '/'
[[ -n $other ]] && _describe -V 'values' other

local -A tags
for k in ${(k)tag_map} ${(k)tag_map_dirs}; do
tags[$k]=1
done

local extglob=${options[extendedglob]}
setopt extendedglob
for tag in ${(Ok)tags}; do
# _describe -t tag cannot begin with a `-`
local desc_tag=${tag##-##}
local values=("${(@f)tag_map[$tag]}") # split on newline

if (( $#values )); then
if [[ -n "$desc_tag" ]]; then
_describe -V -t "$desc_tag" "$desc_tag" values
else
_describe -V 'options' values
fi
fi

values=("${(@f)tag_map_dirs[$tag]}")
if (( $#values )); then
if [[ -n "$desc_tag" ]]; then
_describe -V -t "${desc_tag}-dirs" "$desc_tag" values -S '/' -r '/'
else
_describe -V 'options' values -S '/' -r '/'
fi
fi
done
[[ $extglob == on ]] || unsetopt extendedglob
fi
}

Expand Down Expand Up @@ -486,6 +522,14 @@ compdef _clap_dynamic_completer_NAME BIN"#
if i != 0 {
write!(buf, "{}", ifs.as_deref().unwrap_or("\n"))?;
}

let tag = candidate
.get_tag()
.map(|t| t.to_string())
.unwrap_or_default();

write!(buf, "{}:", Self::escape_value(&tag))?;

write!(
buf,
"{}",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,25 +11,61 @@ function _clap_dynamic_completer_exhaustive() {
)}")

if [[ -n $completions ]]; then
local -a dirs=()
local -a other=()
local -A tag_map tag_map_dirs
local completion
local tag
local value
local to_add

for completion in $completions; do
local value="${completion%%:*}"
IFS=: read -r tag value <<< "$completion"

if [[ "$value" == */ ]]; then
local dir_no_slash="${value%/}"
if [[ "$completion" == *:* ]]; then
local desc="${completion#*:}"
dirs+=("$dir_no_slash:$desc")

if [[ "$value" == *:* ]]; then
local desc="${value#*:}"
to_add="$dir_no_slash:$desc"
else
dirs+=("$dir_no_slash")
to_add="$dir_no_slash"
fi

tag_map_dirs[$tag]+=${tag_map_dirs[$tag]:+$'\n'}"$to_add"
else
other+=("$completion")
tag_map[$tag]+=${tag_map[$tag]:+$'\n'}"$value"
fi
done

local -A tags
for k in ${(k)tag_map} ${(k)tag_map_dirs}; do
tags[$k]=1
done

local extglob=${options[extendedglob]}
setopt extendedglob
for tag in ${(Ok)tags}; do
# _describe -t tag cannot begin with a `-`
local desc_tag=${tag##-##}
local values=("${(@f)tag_map[$tag]}") # split on newline

if (( $#values )); then
if [[ -n "$desc_tag" ]]; then
_describe -V -t "$desc_tag" "$desc_tag" values
else
_describe -V 'options' values
fi
fi

values=("${(@f)tag_map_dirs[$tag]}")
if (( $#values )); then
if [[ -n "$desc_tag" ]]; then
_describe -V -t "${desc_tag}-dirs" "$desc_tag" values -S '/' -r '/'
else
_describe -V 'options' values -S '/' -r '/'
fi
fi
done
[[ -n $dirs ]] && _describe -V 'values' dirs -S '/' -r '/'
[[ -n $other ]] && _describe -V 'values' other
[[ $extglob == on ]] || unsetopt extendedglob
fi
}

Expand Down
89 changes: 65 additions & 24 deletions clap_complete/tests/testsuite/zsh.rs
Original file line number Diff line number Diff line change
Expand Up @@ -251,11 +251,11 @@ fn complete_dynamic_env_toplevel() {
let input = "exhaustive \t\t";
let expected = snapbox::str![[r#"
% exhaustive
help -- Print this message or the help of the given subcommand(s)
--generate -- generate
--help -- Print help
empty action value last hint
global quote pacman alias --empty-choice
help -- Print this message or the help of the given subcommand(s)
--empty-choice empty action value last hint
global quote pacman alias
"#]];
let actual = runtime.complete(input, &term).unwrap();
assert_data_eq!(actual, expected);
Expand All @@ -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

--help -- Print help (see more with '--help')
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
--single-quotes -- Can be 'always', 'auto', or 'never'
--double-quotes -- Can be "always", "auto", or "never"
--backticks -- For more information see `echo test`
--backslash -- Avoid '/n'
--brackets -- List packages [filter]
--expansions -- Execute the shell command with $SHELL
--help -- Print help (see more with '--help')
cmd-single-quotes -- Can be 'always', 'auto', or 'never'
cmd-double-quotes -- Can be "always", "auto", or "never"
cmd-backticks -- For more information see `echo test`
cmd-backslash -- Avoid '/n'
cmd-brackets -- List packages [filter]
cmd-expansions -- Execute the shell command with $SHELL
escape-help -- /tab "'
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);
Expand Down Expand Up @@ -392,16 +398,22 @@ fn complete_dynamic_empty_space() {
let input = "exhaustive quote -\x1b[D\x1b[D\t\t";
let expected = snapbox::str![[r#"
% exhaustive quote -
--help -- Print help (see more with '--help')
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
--single-quotes -- Can be 'always', 'auto', or 'never'
--double-quotes -- Can be "always", "auto", or "never"
--backticks -- For more information see `echo test`
--backslash -- Avoid '/n'
--brackets -- List packages [filter]
--expansions -- Execute the shell command with $SHELL
--help -- Print help (see more with '--help')
cmd-single-quotes -- Can be 'always', 'auto', or 'never'
cmd-double-quotes -- Can be "always", "auto", or "never"
cmd-backticks -- For more information see `echo test`
cmd-backslash -- Avoid '/n'
cmd-brackets -- List packages [filter]
cmd-expansions -- Execute the shell command with $SHELL
escape-help -- /tab "'
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);
Expand Down Expand Up @@ -430,8 +442,37 @@ fn complete_dynamic_dir_no_trailing_space() {
let input = "exhaustive hint --file tests/\t\t";
let expected = snapbox::str![[r#"
% exhaustive hint --file tests/
tests/snapshots tests/testsuite tests/examples.rs
tests/examples.rs tests/snapshots tests/testsuite
"#]];
let actual = runtime.complete(input, &term).unwrap();
assert_data_eq!(actual, expected);
}

#[test]
#[cfg(all(unix, feature = "unstable-dynamic"))]
#[cfg(feature = "unstable-shell-tests")]
fn complete_dynamic_tagged_options() {
if !common::has_command(CMD) {
return;
}

let term = completest::Term::new();
let mut runtime = common::load_runtime::<RuntimeBuilder>("dynamic-env", "exhaustive");

let input = [
"zstyle ':completion:*:descriptions' format '%d'",
"exhaustive -\t\t",
]
.join("\n");

let expected = snapbox::str![[r#"
% zstyle ':completion:*:descriptions' format '%d'
% exhaustive -
Options
--generate -- generate
-h -- Print help
--empty-choice
"#]];
let actual = runtime.complete(&input, &term).unwrap();
assert_data_eq!(actual, expected);
}
Loading