Skip to content

feat(complete): Add per-candidate trailing space control#6266

Draft
AndreasBackx wants to merge 2 commits intoclap-rs:masterfrom
AndreasBackx:feat/completion-space-control
Draft

feat(complete): Add per-candidate trailing space control#6266
AndreasBackx wants to merge 2 commits intoclap-rs:masterfrom
AndreasBackx:feat/completion-space-control

Conversation

@AndreasBackx
Copy link
Copy Markdown
Contributor

This PR was generated with the assistance of an AI agent and reviewed by me.

Summary

Adds a nospace field to CompletionCandidate so the engine can tell shells "don't add a trailing space after this completion." Each shell adapter translates this into its native mechanism, replacing ad-hoc heuristics.

Fixes #5587

Changes

  • clap_complete/src/engine/candidate.rs: Add nospace: bool field with builder .nospace(bool) and getter .is_nospace_set().
  • clap_complete/src/engine/custom.rs: Set nospace(true) on directory candidates in complete_path().
  • clap_complete/src/engine/complete.rs: Set nospace(true) on --flag=value completions, -F=value completions, delimiter-prefixed completions, and require_equals options with = suffix.
  • clap_complete/src/env/shells.rs:
    • Bash: Output _CLAP_COMPLETE_NOSPACE sentinel; registration script detects it and calls compopt -o nospace. Replaces the [=/:]$ regex heuristic.
    • Zsh: Nospace candidates prefixed with \x1f; registration script splits into nospace/other groups using _describe 'values' nospace -S ''. Replaces directory-only split.
    • Elvish: Nospace candidates prefixed with \x1f; registration script wraps them in edit:complex-candidate $value &code-suffix=''.
    • Fish/PowerShell: No changes needed.
  • clap_complete/tests/testsuite/engine.rs: 5 new tests for nospace on directories, equals values, short equals, require_equals, and delimiters.
  • Snapshot files: Updated registration snapshots for Bash, Zsh, and Elvish.

Test Plan

  • cargo test -p clap_complete --features unstable-dynamic — 112 tests pass
  • cargo clippy -p clap_complete --features unstable-dynamic — no warnings
  • cargo fmt -p clap_complete --check — clean

…ompletions

Add a `nospace: bool` field to `CompletionCandidate` that tells shell
adapters to suppress the trailing space after a completion. This is set
on:

- Directory path completions (ending in `/`)
- `--flag=value` inline completions (long and short)
- Delimiter-prefixed completions (e.g., `comma,space`)
- `require_equals` options (completed with trailing `=`)

The field defaults to `false` and is exposed via:
- Builder: `pub fn nospace(mut self, nospace: bool) -> Self`
- Getter: `pub fn is_nospace_set(&self) -> bool`

This is the first part of clap-rs#5587; per-shell handling follows.
Each shell adapter now translates the `nospace` field into its native
mechanism:

- **Bash**: Completions output a `_CLAP_COMPLETE_NOSPACE` sentinel as
  the last entry when any candidate has nospace set. The registration
  script detects this sentinel, strips it, and calls
  `compopt -o nospace`. This replaces the previous `[=/:]$` regex
  heuristic.

- **Zsh**: Nospace candidates are prefixed with `\x1f` (unit separator)
  in the wire output. The registration script splits candidates into
  `nospace` and `other` groups, using `_describe 'values' nospace -S ''`
  for the former. This replaces the previous directory-only split.

- **Elvish**: Nospace candidates are prefixed with `\x1f`. The
  registration script pipes output through `from-lines` and wraps
  nospace values in `edit:complex-candidate $value &code-suffix=''`.

- **Fish**: No changes needed — Fish naturally suppresses trailing
  space for `/`-terminated completions and handles `=`-joined tokens
  through its tokenization.

- **PowerShell**: No changes — `CompletionResult` does not support
  per-candidate nospace control.

Closes clap-rs#5587
@AndreasBackx AndreasBackx marked this pull request as draft February 13, 2026 03:02
Comment on lines +1482 to +1483
/// Like `complete` but includes `[nospace]` marker for candidates with nospace set
fn complete_with_nospace(
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 generally ask for tests to be added in a prior commit, with them passing in that commit

Comment on lines +66 to +68
/// This is useful for completions like directory paths (ending in `/`),
/// `--flag=` values, and delimiter-separated values where a trailing space
/// would be incorrect.
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 useful when there are more completion candidates if this completion is selected, like ...

complete_arg_value(value.to_str().ok_or(value), arg, current_dir)
.into_iter()
.map(|comp| comp.add_prefix(format!("--{flag}="))),
.map(|comp| comp.add_prefix(format!("--{flag}=")).nospace(true)),
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 is this unconditionally adding nospace?

Comment on lines +311 to +316
let comp = comp.add_prefix(format!("-{leading_flags}{sep}"));
if has_equal {
comp.nospace(true)
} else {
comp
}
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?

values = values
.into_iter()
.map(|comp| comp.add_prefix(prefix))
.map(|comp| comp.add_prefix(prefix).nospace(true))
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?

Comment on lines +487 to +491
if a.is_require_equals_set() {
populate_arg_candidate(CompletionCandidate::new(format!("--{s}=")), a)
.nospace(true)
} else {
populate_arg_candidate(CompletionCandidate::new(format!("--{s}")), a)
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.

Doesn't this conflict with your requires_equals PR?

I also think I prefer the approach taken in that PR.

let candidate = CompletionCandidate::new(suggestion.as_os_str().to_owned())
.hide(is_hidden(&raw_file_name));
.hide(is_hidden(&raw_file_name))
.nospace(true);
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.

Should we limit this to !is_wanted? I'm guessing not because something may look like a valid candidate but the user may instead want a directory inside of the current one.

Granted, besides sort order, we don't have a good way of communicating which directories match is_wanted and which don't. Maybe the way to deal with that is to provide a way to add a description for wanted paths.

Comment on lines +96 to +107
let has_nospace = completions.iter().any(|c| c.is_nospace_set());

for (i, candidate) in completions.iter().enumerate() {
if i != 0 {
write!(buf, "{}", ifs.as_deref().unwrap_or("\n"))?;
}
write!(buf, "{}", candidate.get_value().to_string_lossy())?;
}
if has_nospace && !completions.is_empty() {
write!(buf, "{}", ifs.as_deref().unwrap_or("\n"))?;
write!(buf, "_CLAP_COMPLETE_NOSPACE")?;
}
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 makes it so if any candidate doesn't have a space, none of them do.

Instead, if nospace is supported, could we always append a space and have bash always add nospace?

Comment on lines +398 to +409
local -a nospace=()
local -a other=()
local completion
for completion in $completions; do
local value="${completion%%:*}"
if [[ "$value" == */ ]]; then
local dir_no_slash="${value%/}"
if [[ "$completion" == *:* ]]; then
local desc="${completion#*:}"
dirs+=("$dir_no_slash:$desc")
else
dirs+=("$dir_no_slash")
fi
if [[ "$completion" == $'\x1f'* ]]; then
completion="${completion:1}"
nospace+=("$completion")
else
other+=("$completion")
fi
done
[[ -n $dirs ]] && _describe 'values' dirs -S '/' -r '/'
[[ -n $nospace ]] && _describe 'values' nospace -S ''
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.

Again, this is doing it all or nothing

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.

Intentionally control when a space is appended for native completions

2 participants