Skip to content

v0.4.0 — saying no

Latest

Choose a tag to compare

@teocns teocns released this 04 Aug 08:03
2e2ef13

There was no discoverable way to reject a suggestion. <C-]> was bound and worked, but it lived in one un-highlighted README table row and never appeared beside a suggestion — so the reasonable conclusion was that dismissal did not exist.

<Esc> now dismisses, and the suggestion says so:

⟪neocursor · <Tab> accept · <Esc> dismiss⟫

No <Esc> mapping was installed

This is the point. <Esc> already dismissed — leaving insert mode clears the suggestion — it just filed nothing, so the identical edit came straight back when you re-entered insert. What changed is that it now counts. The key was always working; nothing about your <Esc> is intercepted.

That matters, because a conditional <Esc> would have cost real things. In VS Code, Escape is a free key. In neovim it is the mode transition:

if <Esc> were mapped
macros non-deterministic — whether a ghost is showing depends on a debounced network reply
im-select, snippet plugins clobbered; their insert-mode <Esc> never runs
fallthrough no clean form — feedkeys "n" skips their mapping, "m" re-enters ours

Advertising behaviour that already exists costs none of that.

<C-]> stays, as the dismiss-without-leaving-insert variant for when you want to keep typing. It is the key copilot.vim, copilot.lua and avante.nvim all use.

Dismissal is tiered

The first dismiss clears the edit and keeps the jump target. Dismiss again with nothing showing and the target goes too. That is Cursor's split, not an invention: their Escape handler files the suggestion as rejected and then calls maybeShowHintLineWidget immediately after, deliberately keeping the prediction alive.

It takes the hint

Rejecting the same edit twice already suppressed it. New in this release is the gate that catches the model being wrong in a new way every time — Cursor's hasRejectedTooManySuggestions. After 20 dismissals with nothing accepted in between, neocursor stops volunteering on the passive triggers:

trigger after the budget
entering insert silent
moving to another line silent
typing still asks

Accepting anything refills it, as does switching buffers. :NeocursorLog shows the tally live and flips to MUTED at the threshold. The 20 is not invented — it now arrives with the rest of CppConfig, and the live backend returns exactly that.

On Cursor under a vim layer

Worth knowing if you also use Cursor: its handleKeyDownForCppKeys is a raw document keydown listener rather than a registered keybinding, and the suggestion branch never calls preventDefault — only the auto-import branch does. So with vscode-neovim, Escape dismisses the suggestion and propagates, leaving insert mode. Both happen. Cursor's tiering collapses to one tier for vim users by accident, which is precisely the behaviour this release makes deliberate here.

One deliberate divergence

Cursor increments its counter on every clearSuggestions(). This counts dismissals instead. Their suggestion survives typing via isOnShortestEditPath; ours only does for inline ghosts — a diff is dropped and refetched per keystroke, so counting clears would mute you after ~20 characters rather than ~20 ignored suggestions. Same intent, adjusted for where the two renderers differ; documented at the call site.

On not muting jumps you never refused

The first draft of this change filed a prediction rejection on every InsertLeave. Two ordinary trips through normal mode and the jump target went dead for the 30s pred_rejects TTL. flow_spec round 4 caught it, and a baseline comparison confirmed the regression was mine rather than flake — which is the whole reason that round asserts the prediction survives a chain. Fixed by splitting reject_suggestion() from reject_prediction().

Full CI matrix green on ubuntu, windows and macos.

Full changelog: v0.3.0...v0.4.0