Skip to content

feat(go): answer what could go where the cursor is - #984

Merged
jdx merged 5 commits into
go/typedfrom
go/complete
Aug 17, 2026
Merged

feat(go): answer what could go where the cursor is#984
jdx merged 5 commits into
go/typedfrom
go/complete

Conversation

@jdx

@jdx jdx commented Aug 17, 2026

Copy link
Copy Markdown
Owner

argv.Walk reads the words before the cursor and reports what it is standing in; argv.Candidates says what could go there — subcommands and their aliases, the flags in scope, and the values a choices list allows.

Stacked on #978.

Asking the parser rather than re-deriving it

That is the whole design. A completion advertising a flag the parser would refuse is worse than no completion, so the scope a candidate comes from is the scope a token would be resolved in:

  • a global is offered inside a subcommand
  • a subcommand redeclaring an inherited name offers its own
  • a hidden flag binds without being advertised
  • past a --, nothing is offered — there is no flag of this CLI to type there

Errors are not failures here

A line being completed is unfinished by definition, so a parse error means "the grammar runs out here" — which is the position being asked about. missing_flag_value says the cursor is standing in a flag's value, and that flag's choices take the position entirely. help says the cursor is naming a command to read about, where nothing else belongs.

Not included

Turning candidates into the text each shell expects, and running the complete scripts a spec can declare. The first is per-shell formatting, the second runs subprocesses, and neither belongs in a package whose claim is that a parse does not allocate. Both are noted in the README.

One note from writing the tests

The first fixture numbered its keys 10–14 in an eight-entry table. Both cold tables are indexed by key, so every lookup missed and the assertions about hiding and choices passed for the wrong reason — they only started failing once the keys were dense. The invariant is documented on Metadata; worth knowing that breaking it fails quietly rather than loudly.

🤖 Generated with Claude Code


Note

Medium Risk
New completion surface area mirrors subtle parser/help scope rules; behavior is heavily tested but mistakes would mislead users without breaking parses.

Overview
Adds parser-driven tab completion in argv: Walk parses words before the cursor into a Position (command chain, whether flags/subcommands still apply, awaiting flag value, variadic collection, pending arg, help topic mode), and Candidates returns subcommands/aliases, in-scope flag spellings, and choices values filtered by a partial prefix.

Completion reuses the same scope rules as help via flagsInScope (per-spelling shadowing of inherited globals, negation vs long-form precedence) and refactors everyFormInScope / negationSurvives in scope.go so help and completions stay aligned. Parser.SubcommandsPossible exposes when descent into subcommands has stopped.

README documents the completion API and notes that per-shell formatting and complete scripts are still out of scope.

Reviewed by Cursor Bugbot for commit 84b303e. Bugbot is set up for automated code reviews on this repo. Configure here.

@coderabbitai

coderabbitai Bot commented Aug 17, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Central YAML (base), Organization UI (inherited)

Review profile: CHILL

Plan: Pro Plus

Run ID: 80f46d25-b431-4065-9087-d1c92377fb51

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@greptile-apps

greptile-apps Bot commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

The PR adds Go APIs that derive completion positions and candidates from parser state, with focused fixes ensuring completion visibility and flag ownership match parser behavior.

  • Adds Walk and Candidates for commands, aliases, scoped flags, and declared choices.
  • Shares negation and flag-scope rules between help rendering and completion.
  • Covers hidden commands, required separators, positional state, variadic values, and shadowed flag spellings with tests.
  • Documents the completion API and its deliberately excluded shell-formatting responsibilities.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains.

Important Files Changed

Filename Overview
go/argv/complete.go Adds parser-driven completion state and candidate generation; the previously reported visibility, separator, shadowing, and negation defects are addressed.
go/argv/complete_test.go Adds focused regression coverage for completion scope, hidden entries, required separators, variadic collection, and negation collisions.
go/argv/parser.go Exposes whether parser state still permits subcommand descent for completion parity.
go/argv/scope.go Extracts shared form and negation-survival logic without changing the established help-rendering contract.
go/README.md Documents the new parser-driven completion APIs and clarifies that shell-specific output remains out of scope.

Reviews (8): Last reviewed commit: "fix(go): a variadic still collecting is ..." | Re-trigger Greptile

Comment thread go/argv/complete.go
Comment thread go/argv/complete.go Outdated
Comment thread go/argv/complete.go Outdated
Comment thread go/argv/complete.go
Comment thread go/argv/complete.go
Comment thread go/argv/complete.go Outdated
Comment thread go/argv/complete.go
@github-actions

github-actions Bot commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

Instruction counts

Nothing was compared, and so nothing was gated. No series appears on both sides: either the base has no measurements recorded, or the two were measured on different runner classes, which are deliberately not comparable — counts shift between machine types by more than a real regression does.

New, nothing to compare against: markdown on bamboo-v2-ubuntu24.04-x64-30vcpu-24gb-rust1.97.1, startup on bamboo-v2-ubuntu24.04-x64-30vcpu-24gb-rust1.97.1

Only instruction counts gate. Wall clock is shown for context — on identical hardware it moves 4-20% run to run.

Measured by tak — instruction-counted CLI benchmarks, stored in this repository's git notes.

Shadow comparison

Parsing mise use -g node@20 against a shadow of mise's committed spec.
Reported, not gated: the shadow grows as the derive learns to express more, so
what to watch is the ratio rather than either column.

framework instructions, cold parse vs usage
usage 4161
argh 6292 1.5x
clap 5895248 1416x
bpaf 21917778 5267x
                                              min       p01       p10    median
usage-rs: argv -> struct                      210       214       219       227  ns
argh: argv -> struct                          273       278       283       295  ns
clap: build tree + parse -> struct         482935    483649    487867    501766  ns
bpaf: build parser + parse -> struct      1563765   1563765   1569977   1594023  ns

usage: argv -> struct                             206 ns      0.21 µs
clap: build tree + parse -> struct             505175 ns    505.17 µs
clap: parse -> struct, tree reused              24485 ns     24.48 µs
clap: build tree only                          319377 ns    319.38 µs

84b303e0bbd4 vs c5d9ad605c95 · measured on the runner, not pushed to the history.

Comment thread go/argv/complete.go Outdated
Comment thread go/argv/complete.go Outdated
Comment thread go/argv/complete.go Outdated
Comment thread go/argv/complete.go Outdated

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit cf338af. Configure here.

Comment thread go/argv/complete.go
Comment thread go/argv/complete.go Outdated
jdx and others added 5 commits August 17, 2026 20:29
`Walk` reads the words before the cursor and reports what it is standing in;
`Candidates` says what could go there — subcommands and their aliases, the flags
in scope, and the values a `choices` list allows.

Both ask the parser rather than re-deriving its rules, and that is the whole
design. A completion advertising a flag the parser would refuse is worse than no
completion, so the scope a candidate comes from is the scope a token would be
resolved in: a global is offered inside a subcommand, a subcommand redeclaring an
inherited name offers its own, a hidden flag binds without being advertised, and
past a `--` nothing is offered because there is no flag of this CLI to type
there.

Errors are not failures here. A line being completed is unfinished by definition,
so a parse error means "the grammar runs out here" — which is the position being
asked about. `missing_flag_value` says the cursor is standing in a flag's value,
and the choices for that flag take the position entirely. `help` says the cursor
is naming a command to read about, where nothing else belongs.

Not included, and listed in the README: turning candidates into the text each
shell expects, and running the `complete` scripts a spec can declare. The first
is per-shell formatting and the second runs subprocesses; neither belongs in a
package whose claim is that a parse does not allocate.

One note from writing the tests. The first fixture numbered its keys 10 to 14 in
an eight-entry table, so every cold-table lookup missed and the assertions about
hiding and choices passed for the wrong reason — the tests only started failing
once the keys were dense. The invariant is documented on `Metadata` and it is
worth knowing that breaking it fails quietly rather than loudly.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Four findings on the completion candidates, all of them the same failure in
different places: advertising something the parser refuses, which is precisely
what this design was meant to rule out.

A hidden command was offered. Flags were filtered by `hide` and commands were
not, so `hide` kept a command off its parent's help page while tab-completion
listed it — including after `help`, where it would have been most discoverable.

A help topic offered no aliases. `findNamed` resolves a topic by name or alias
exactly as it resolves a command to run, so hiding them there made accepted
spellings undiscoverable in the one place someone is looking for a name.

An argument that reads only after a `--` had its choices offered before one. Those
words come back as `arg_requires_double_dash` — a completion producing a command
line the grammar refuses, which is the failure mode the whole approach exists to
avoid. It waits for the separator now.

And an inherited global was dropped whole when a nearer flag took one of its
spellings. A flag answers to several, and a subcommand reclaiming `--jobs` leaves
an inherited `-j` and `--workers` binding; withdrawing all three hid two the
parser still accepts. Masking is per spelling now, which is the same rule the
help pages follow and for the same reason. A spelling is claimed whether or not
anything is left of the flag, so something farther away cannot pick it up either.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…elling

`Candidates` appended a flag's negation whatever else was in scope, and
`flagsInScope` dropped a flag whose primary forms were all taken. Both are the
same mistake from opposite sides: a negation was not treated as a spelling.

An ancestor's literal `--no-color` beats a nearer flag's negation, because the
parser asks for every long across the whole scope before it asks for any
negation — so the word was offered twice, described two different ways, one of
them wrong. And a nearer command reclaiming `--color` left the inherited flag
with nothing but its negation, which still binds and was never offered.

The pages already had this rule. It now lives in one place and both halves call
it, because a spelling offered by one and not the other is the two of them
disagreeing about what the parser does.

The tests ask the parser which flag binds the word before asserting who offers
it: the claim here is that what is offered is what would be accepted, and only
the parser can answer the second half.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… spelling once

Two more places where the completion advertised something the parser would not
take.

Descent stops once a positional of the command has taken a word — `word()` asks
`!argFilled && !flagsStopped` before it looks for a subcommand — so after that a
name matching a subcommand is a value or a failure. `Position` carried the flag
half of that rule and nothing for the command half, so the names were still
offered. The parser now says so directly, and the test asks it whether the word
still binds before asserting that it is not offered.

And a flag may spell its negation the same as its own long form:
`flag "--no-color" negate="--no-color"`. That parses, and it was offered twice.
usage-lib prints such a flag as `--no-color / --no-color`, so the page saying it
twice is the reference's own behaviour and stays; a completion is a list of
things to type, where the same thing twice is a repeated row.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…hat is owed

Two positions were being treated as one. A flag that has not been given its value
refuses a flag-like token, so nothing but its values belongs there. A variadic
that already has one *stops* collecting when it meets a flag, and that flag
binds — so `--tools a ⌶` was offering the tool names and hiding every flag that
works there.

Offered now: the variadic's own values, and the flags. Not a subcommand name or
the positional behind it, because a plain word would be collected rather than
bound.

And the exemption that lets a flag spelled `--x` still offer a negation spelled
`--x` was reaching past a nearer flag that had claimed the word. The parser binds
`--x` to the child; the inherited global was offering it a second time.

Both tests ask the parser what binds before asserting who offers it, and both
fail on the code above them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@jdx
jdx merged commit 3cffaf9 into main Aug 17, 2026
11 checks passed
@jdx
jdx deleted the go/complete branch August 17, 2026 22:42
jdx added a commit that referenced this pull request Aug 17, 2026
`RenderAnswer` turns candidates into the protocol a shell's completion
machinery expects. Stacked on #984.

| shell | shape |
| --- | --- |
| bash | `value` |
| fish, nu, PowerShell | `value\tdescription` |
| zsh | `value\tdescription\tinsert` |

zsh takes a third field because what it *displays* and what it *types*
are not always the same string. Checked against what `usage
complete-word --shell zsh` already emits:

```
use^IInstalls a tool and adds the version to mise.toml.^Iuse$
```

## Three rules that look like details and are not

**Descriptions are all-or-nothing per answer.** A column that appears on
some rows and not others reads as missing data rather than as an absent
description.

**A description is collapsed onto one line, not truncated at the first
break**, so a two-line description still says both halves. The protocols
are line-based and a break would look like another candidate. This is
why the one-lining *moved here* from `describe` in #984, where it was
throwing the second half away — `Candidates` now carries the whole text
and the renderer decides how to fit it.

**"Paths belong here too" is a whole line**, not a flag on the protocol,
because every one of the five shells can already split output into lines
and look at the last one. `\x01` opens it because no candidate can
contain a control character.

## Naming

`RenderAnswer` rather than `Render`, which already belongs to failures.
Two things in one package turning a value into text for a terminal is
reason enough to say which.

## Still to do

Running the `complete` scripts a spec can declare, which means
subprocesses — a decision for the layer above this one.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> Additive completion-output formatting with broad unit tests; no
changes to parsing, binding, or security-sensitive paths.
> 
> **Overview**
> Adds **`argv.RenderAnswer`** so completion **`Answer`** values
serialize to the line protocol each shell expects: bash values only;
fish, nu, and PowerShell use `value` + tab + description; zsh adds a
third insert field with **`zshQuote`** for safe typing.
> 
> Rendering enforces **all-or-nothing description columns** (only among
candidates that actually emit), **collapses multi-line help with
`oneLine`** instead of truncating, **drops candidates whose values
contain control chars/tabs/newlines** rather than repairing them, and
appends **`\x01files` / `\x01dirs` marker lines** when paths should
complete too.
> 
> **`describe` in `complete.go`** now attaches the full short help
(including newlines); one-lining moved here from candidate building.
README documents **`RenderAnswer`**; tests cover per-shell shapes and
edge cases.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
f37999a. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
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.

1 participant