feat(completion): unify shell completions behind task __complete - #2897
Open
vmaerten wants to merge 45 commits into
Open
feat(completion): unify shell completions behind task __complete#2897vmaerten wants to merge 45 commits into
task __complete#2897vmaerten wants to merge 45 commits into
Conversation
vmaerten
force-pushed
the
feat/completion-engine
branch
3 times, most recently
from
June 29, 2026 15:42
b13cc89 to
f9f2ecb
Compare
Member
Author
|
@arturict, please don't comment on draft PRs. Also, your comments appear to be AI-generated. We're still open to meaningful, human-written comments. Finally, please avoid giving instructions to maintainers as if you were one of them. |
|
Understood, and sorry about that. I should not have commented on a draft or phrased the comment as an instruction. I'll step back. |
vmaerten
force-pushed
the
feat/completion-engine
branch
from
July 19, 2026 15:47
1521c9e to
32cfaec
Compare
vmaerten
marked this pull request as ready for review
July 19, 2026 18:20
vmaerten
force-pushed
the
feat/completion-engine
branch
from
August 3, 2026 20:39
0b986fe to
3a37bf5
Compare
vmaerten
force-pushed
the
feat/completion-engine
branch
3 times, most recently
from
August 13, 2026 13:55
b7f3248 to
9e8b673
Compare
Engine: - Emit DirectiveKeepOrder for task variables so the `requires` declaration order is preserved instead of being sorted by the shell. - Return full `--flag=value` candidates for the inline `--output=` form so all shells match against the whole current token. - Add `--no-aliases` / `--no-descriptions` completion flags (via complete.Options) parsed from the __complete invocation; the zsh wrapper maps its show-aliases and verbose zstyles onto them. - Skip Taskfile setup when completing flags (NeedsTaskfile) and load the task list lazily; drop unused parameters; document exported identifiers. Shell wrappers: - zsh: reindent to tabs (.editorconfig), bridge zstyles to engine flags. - fish: reindent to 2 spaces, handle every file-completion directive in the wrapper (--no-files disables the native fallback), drop the duplicated yml/yaml extension list now that it lives only in the engine. - bash: prefix-filter by hand to preserve values containing spaces, exclude `=` from word breaks so `--output=` reaches the engine as one token. - powershell: filter candidates by the current word, fall back to file completion for DirectiveDefault. NoSpace is not representable in fish/PowerShell completion APIs; documented in the wrappers.
Add completion/tests/ harnesses that exercise the engine protocol and every shell wrapper without a TTY: the engine via `task __complete`, bash/zsh by stubbing the completion-system helpers, fish via `complete -C`, and PowerShell via the completion API. A `test:completion` task and a matching CI job (with a strict mode that fails when an expected shell is missing) run them all. Writing the suite surfaced and fixed real wrapper bugs: - fish: `math` has no bitwise `&`, so every directive check errored; test bits with integer division + modulo instead. Also filter FilterFileExt results ourselves, as __fish_complete_suffix only prioritizes the extension. - powershell: `Get-ChildItem -Include` is ignored without -Recurse, so file extension filtering returned nothing; filter with Where-Object instead. - bash: guard empty-array expansion so completion with zero candidates does not trip `set -u` on bash 3.2. Also name the completion directive bits (NO_SPACE, FILTER_FILE_EXT, …) in every wrapper instead of using raw numbers.
Follow cobra's testing philosophy: the completion protocol (candidates + directive) is business logic and belongs in Go, while the shell wrappers only need to be checked for how they interpret each directive. - Add completion/protocol_test.go: a table-driven black-box test that runs the real `task __complete` binary and asserts the offered values and the emitted directive. Unlike the in-process engine tests, it exercises the actual entrypoint dispatch, runComplete wiring and — crucially — the real flag set, so it catches drift between the completion enum/directive maps and the flag definitions. Runs on every OS, including Windows where the shell smokes don't. - Delete completion/tests/engine.sh (the protocol is now covered in Go) and drop it from run.sh. - Reduce the bash/zsh/fish/powershell smokes to directive routing only (files vs dirs vs no-files vs no-space), removing the task/alias/var assertions that the Go tests already own.
Move the thin __complete wrappers under completion/next/ and restore the stable, hand-written scripts at their canonical paths so the default output of `task --completion <shell>` (and the goreleaser/brew packaged scripts) is unchanged. Repoint the cross-shell test harness at completion/next/. This is the groundwork for offering the new engine as opt-in before it becomes the default.
Embed the completion/next/ wrappers and expose them through a new `task --new-completion <shell>` flag, alongside the unchanged `task --completion <shell>`. This lets users try the unified __complete engine (same suggestions across bash/zsh/fish/powershell: task names, aliases, flags, flag values and requires vars) while it is opt-in; it will become the default of --completion in a future release.
Add a subsection to the installation guide explaining how to try the new completion engine by swapping --completion for --new-completion, and noting it will become the default in a future release.
PowerShell replaces the whole token being completed with the CompletionResult text, so returning `$_.Name` (the basename) for file/dir suggestions dropped any directory the user had already typed (e.g. `task --dir sub/<TAB>` turned `sub/` into `foo`). Prepend the typed path prefix to each candidate and make the default file fallback honor the current word instead of always listing the working directory. Covered by a nested-path smoke assertion.
…st word The task-var detection evaluated taskNames(e) — a full walk of every task and alias — unconditionally, even though detectTaskName bails out when there is no prior word. Guarding on len(args) > 1 avoids that walk (and a second one via GetTaskList) for the common `task <TAB>` case, which matters on large monorepo Taskfiles where completion latency is user-visible.
The --new-completion flag takes a shell name (bash/zsh/fish/powershell) just like --completion, but it was missing from flagEnums, so `task --new-completion <TAB>` offered nothing. Add the matching entry.
sanitize built a new strings.Replacer on every call (twice per suggestion). Hoist it to a package var.
Inline path flags (`--dir=foo`, `--taskfile=foo`, `--cacert=foo`) were broken: the shells ran file/dir completion against the whole `--flag=value` token, so nothing matched. Each wrapper now strips the `--flag=` prefix before completing and re-applies it to the results (zsh via `compset -P '*='`, the others by hand). Also fixes two smaller wrapper issues in the process: PowerShell prefix matching is now case-insensitive, and the fish wrapper erases inherited completion rules before registering (fish accumulates them, unlike bash/zsh/PowerShell). Adds inline smoke assertions for bash, fish and PowerShell.
The requires field added to the --list --json output is unused by the new completion engine (which reads requires via FastCompiledTask), untested, and did not handle enum.ref. Revert it to keep the JSON API change out of this branch.
…text) Use slices.Contains for the after-dash scan (modernize), and exec.CommandContext in the protocol test (noctx), with a targeted gosec exception for launching the test-built binary with test-controlled args.
…nt label The Options doc claimed the zero value shows everything, but it shows neither aliases nor descriptions (DefaultOptions enables both). Also remove the redundant `# FilterDirs` label in the PowerShell wrapper.
Nushell exposes a single global external completer rather than a per-command
registration, so the wrapper chains to whatever completer is already installed
and delegates command lines that do not start with Task.
Two directives are free here and one is unavailable: Nushell never appends a
space after an external completion (NoSpace) and never re-sorts the results
(KeepOrder), while `{completions, options}` records are rejected for an external
completer, so prefix filtering is done in the wrapper as PowerShell does.
`flagEnums["completion"]` gains `nu` too, which assumes the standalone Nushell
script lands on main first.
Naming the fields after the --no-aliases / --no-descriptions flags removes DefaultOptions and the trap of a zero value that means the opposite of the default.
The four call sites also used TrimSuffix, which drops a single colon where --list uses TrimRight.
GetTaskList compiles every task (vars, labels, prompts) on every keystroke, while the suggestions read Task, Aliases and Desc. Walking the Taskfile directly and falling back to a full compilation only for templated descriptions cuts most of the per-TAB work, and a single broken task no longer empties the list.
The engine had its own copy, which only accepted a []any and so completed nothing for a ref resolving to the []string or []int that resolvedAsAnySlice already handles for the runtime.
NeedsTaskfile repeated three of Complete's branch conditions, so a new context had to be added to both or the Taskfile would silently stop loading.
A DirectiveDefault entry is what an absent key already yields, so the three had to be kept in sync with the flag definitions for no effect.
The five shells were spelled out in four places: two identical switches and the two completion flag enums. A shell can no longer be advertised by the completion of --completion while the lookup rejects it.
strings.Cut for the inline --flag=value split, slices.Contains instead of a set built to answer a few lookups, slices.SortFunc so the stdlib sort no longer shadows internal/sort, slicesext.Convert for the candidate mapping.
One write syscall per suggestion, for a list emitted in full on every keystroke.
…ecutor The dir was set twice, once in the constructor and once over it.
SetOutput(io.Discard) already silences the only path that reaches usage.
…the wrappers The fish NoSpace and KeepOrder variables were never tested, the bash cword guard is covered by the empty-args fallback right below, and the PowerShell element-count and line-count checks cannot be false.
The Get-ChildItem to CompletionResult conversion was written three times, twice verbatim.
Four copies of the same command -v / run / skip block.
CommandElements were forwarded via ToString(), i.e. their source text, so `task --dir "/a b" <TAB>` reached the engine with the quotes, found no Taskfile and fell back to file completion. A string element now yields its Value, the current word is unquoted before use, and a candidate holding a space is quoted back so insertion does not split it into several arguments.
…aded Completion read only --dir, --taskfile and --global, dropping --sort, --insecure, --trusted-hosts, the certificates, the timeout and the remote cache options: a Taskfile that loads fine on a real run could complete nothing. The flag package now parses the words being completed leniently into pflag.CommandLine, and the executor is built from flags.WithFlags(), so a new flag needs no change here.
A remote Taskfile whose cache was missing or expired made TAB download it, which could freeze the shell for up to --timeout (10s by default), prompt for trust and write the cache. Completion now runs offline: the cache is served when present, and a missing one fails instantly. The empty stdin keeps a prompt from ever reading the terminal.
`task -t - <TAB>` reached NewRootNode with a "-" entrypoint, whose StdinNode reads os.Stdin to completion: the shell stayed frozen until the completion was killed. The test drives it with an unwritten pipe.
The enum ref resolution was exported from the root package only so the completion engine could reach it, which committed a library API to an internal wiring need. It now lives beside the matrix ref helper it shares, and asAnySlice joins slicesext. The moved test also covers a ref resolving to a []string, the case the engine's former copy rejected.
CompletionShells was exported from the root package for the engine alone. The list is private to the engine again, and a test asserts every shell it offers is one the root package can serve — the drift the shared symbol was meant to prevent.
`-V` was appended to the options forwarded to compadd, where it takes the next argument as a group name: it swallowed the `-d` that _describe appends, and zsh offered _describe's own _tmpd and _tmpm variables as candidates. Completing a task with `requires: vars` — the only context emitting KeepOrder — was therefore unusable. The test covered that context already, but its _describe stub ignored where an option landed. It now mirrors the real signature and asserts each option in its own zone.
A wildcard task name is a pattern, not a runnable name: inserting `release:*` on the command line only gets the shell to escape the glob, and running it leaves `.MATCH` empty. Suggest the literal prefix up to the first `*` instead, with NoSpace so the cursor stays against it. The directive is only raised when such a candidate is in the response, so Taskfiles without wildcard tasks keep the trailing space.
…emote flags The remote Taskfile flags left the experiment in #2906, but the legacy Bash, Fish, Zsh and PowerShell wrappers this branch restores still gated them behind `__task_is_experiment_enabled REMOTE_TASKFILES`.
… official Remote includes are resolved for everyone since #2906, so a keystroke on a Taskfile including an uncached remote one is now a real freeze: without the offline override it waits the full --timeout and hits the network. The experiment-gated flag test moved to --force-all, the only flag still behind an experiment.
vmaerten
force-pushed
the
feat/completion-engine
branch
from
August 13, 2026 14:05
9e8b673 to
d0a53f4
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds a unified shell completion engine: a single
task __completecommand, backed by a shared Go package (internal/complete), that drives Bash, Fish, Zsh and PowerShell. The shell scripts become thin wrappers that forward the current words totask __completeand render the suggestions, so every shell gets the same, richer behaviour and there is no more per-shell drift.The wire protocol mirrors cobra v2 (
value\tdescriptionlines followed by a:<directive>bitfield) behavior.Why
It's painful to maintain 4 (or more) different completion scripts, and it's also difficult to test them. With this approach, all new flags will be included in every shell by default.
Opt-in rollout
Rather than switching every user at once (millions of users, 4 shells, multiple OSes), the new engine ships opt-in:
task --completion <shell>is unchanged and keeps returning the current, stable scripts (still what goreleaser/Homebrew package).task --new-completion <shell>returns the new__complete-powered wrappers.completion/next/; the stable ones stay at their canonicalcompletion/<shell>/paths.--new-completionwill become the default of--completionin a future release, at which point thenext/wrappers move to the canonical paths and the flag is removed.What the engine completes
--xxx) and their shorthands--taskfile, directories for--dir, enums for--output/--sort/--completion/--new-completionenumvalues (task build VAR=)--taskfile foo) and inline (--taskfile=foo) flag forms, including nested paths (--dir=sub/…)The engine skips loading the Taskfile entirely when only flags/flag-values are being completed (
NeedsTaskfile), and remote-Taskfile completion never blocks: the approval prompt is a no-op without a TTY, so completion degrades to flag-only suggestions until the remote is approved by runningtasknormally.Testing
Following the cobra approach, the protocol is tested in Go and the generated scripts are only smoke-tested for how they route each directive:
internal/complete/complete_test.go: white-box tests of the engine.completion/protocol_test.go: black-box test of the real binary's__completeoutput (candidates + directive).completion/tests/: thin per-shell smokes (bash/zsh/fish/powershell) that assert directive routing (no-files, dir-only, extension filter, inline--flag=, …), runnable viatask test:completion.completionCI job runs the smokes on Ubuntu and macOS (with fish and PowerShell installed, strict mode).