ADR-0022 stage 8 follow-up: fixes from real-app testing

Three fixes from the user's testing run, plus an
investigation note on a fourth.

#4 Sticky hint during cycling. The previous code recomputed
candidates_at_cursor at the post-Tab cursor position, which
made the panel whiplash through "what comes next at the new
cursor" between cycles. ambient_hint now short-circuits to
the memo's stored candidate list while the memo is alive —
so Tab Tab Tab keeps showing the same list with the
selection moving, then snaps to the post-Tab ambient state
once any non-Tab key clears the memo.

#2 Candidate ordering and kind-coloured rendering. New
`Candidate { text, kind: Keyword|Identifier }` carries the
classification through completion, last-completion memo,
and ambient-hint payload. candidates_at_cursor now sorts
keywords first (alphabetical), identifiers second
(alphabetical), and the hint-panel renderer colours keywords
in `tok_keyword` and identifiers in `tok_identifier`.
Keyword-vs-identifier name collisions resolve in favour of
the keyword (rare; the user can still address their table
via different syntax).

#3 tok_identifier no longer matches theme.fg. Identifiers
in the input pane now render in a distinct cool grey-blue
(dark) / dark steel-blue (light), so they stand out from
prose-like default text without competing with keyword
purple. Same colour drives the identifier candidates in
the hint panel for visual consistency input ↔ hint.

Limitation worth knowing: "keywords first, alphabetical"
is not the same as grammatical order. For "add column "
the hint shows `table to` not `to table` — chumsky's
expected-set doesn't preserve combinator-source order, and
encoding it in the registry adds maintenance overhead the
fix doesn't cleanly justify. Marked for future revisit if
it bites.

#1 (Tab does nothing on "add column ") — not reproduced
through App::update. The internal logic works correctly:
"add column " + Tab inserts "Customers ", second Tab
cycles to "Orders ", third to "Thing ". The most likely
explanation is a stale binary or a terminal-level event
intercept (tmux focus, kitty-keyboard protocol differences,
etc.) — needs user verification with a fresh build.

Tests: 747 passing, 0 failing, 1 ignored (744 baseline →
+3: 2 new completion-ordering cases including the
keyword-wins-on-name-collision edge, plus 1 hint-mid-cycle
sticky test). Clippy clean.
This commit is contained in:
claude@clouddev1
2026-05-11 22:12:16 +00:00
parent 8214e4136a
commit bd1cce672d
5 changed files with 237 additions and 82 deletions
+2 -2
View File
@@ -705,7 +705,7 @@ impl App {
&self.schema_cache,
)?;
let idx = start_idx % comp.candidates.len();
let inserted = format!("{} ", comp.candidates[idx]);
let inserted = format!("{} ", comp.candidates[idx].text);
let original_text =
self.input[comp.replaced_range.0..comp.replaced_range.1].to_string();
self.input
@@ -746,7 +746,7 @@ impl App {
memo: crate::completion::LastCompletion,
idx: usize,
) -> crate::completion::LastCompletion {
let new_inserted = format!("{} ", memo.candidates[idx]);
let new_inserted = format!("{} ", memo.candidates[idx].text);
let (start, end) = memo.inserted_range;
self.input.replace_range(start..end, &new_inserted);
let new_end = start + new_inserted.len();