ADR-0022 stage 8 follow-up r2: completion UX fixes from real testing

Two concrete behaviour changes from the user's second testing
round:

1. **Single vs multi commit paths.** Previously every Tab,
   even single-candidate, created a memo so Esc/Backspace could
   undo. The downside: with one candidate, repeated Tab "cycled"
   through the same item invisibly — looked stuck. Now:
   - Single candidate → insert with trailing space, no memo.
     The user can keep typing or hit Tab again to fresh-complete
     at the new cursor. (Trade-off: Esc/Backspace no longer
     whole-span undo for unique completions; the user accepted
     this for the chained-Tab fluency.)
   - Multi candidate → insert WITHOUT trailing space, create
     memo for cycling. The natural commit gesture is space —
     pressing it clears the memo and inserts the space normally,
     producing "<chosen> " ready for the next position.
   The "stuck on unique" symptom goes away, and the missing
   trailing space on multi-Tab signals "you're picking; press
   space when you're done" without needing modal affordances.

2. **Keyword candidates in grammar order.** Dropped the
   alphabetical sort in `describe_expected` in favour of
   chumsky's native source-order traversal of `or_not`/`choice`
   chains — empirically this matches the canonical command
   shape. Result: `add column ` now offers `to` before
   `table` (as `add column [to] [table] <Table>:…` reads),
   not `table` before `to` which previously suggested the
   nonsensical `add column table to ...`. Identifiers still
   alphabetised within their group; entry-keyword fallback
   for the no-prefix case stays alphabetical (no source order
   when 10 separate command branches).

Tests: 750 passing, 0 failing, 1 ignored (747 baseline →
+3 net: replaced single-candidate Esc/Backspace tests with
new multi-candidate variants; added the unique-Tab-chains-
naturally case that drove the round-2 fix; kept the
keywords-in-grammar-order test updated to assert
`to`/`table`/identifiers ordering).
This commit is contained in:
claude@clouddev1
2026-05-11 22:27:54 +00:00
parent bd1cce672d
commit f94a999e66
3 changed files with 189 additions and 109 deletions
+16 -12
View File
@@ -126,15 +126,18 @@ pub fn candidates_at_cursor(
let matches_prefix = |s: &str| s.to_lowercase().starts_with(&lowered_prefix);
// Source 1: keyword candidates from the parser's
// expected-set.
// expected-set. Preserve `expected`'s order — it reflects
// chumsky's source-order traversal of `or_not` / `choice`
// chains, which matches the canonical command shape (e.g.
// `to` before `table` for `add column [to] [table] …`).
let mut keywords: Vec<String> = expected
.iter()
.filter_map(|item| strip_backticks(item))
.filter_map(|name| Keyword::from_word(name).map(|_| name.to_string()))
.filter(|name| matches_prefix(name))
.collect();
keywords.sort();
keywords.dedup();
let mut seen_kw = std::collections::HashSet::new();
keywords.retain(|k| seen_kw.insert(k.clone()));
// Source 2: schema identifiers — accumulated across every
// matching known-set slot. `NewName` slots return `&[]`.
@@ -444,24 +447,25 @@ mod tests {
}
#[test]
fn keywords_come_before_identifiers_in_candidate_order() {
// "add column " has both keyword candidates (`to`,
// `table`) and schema-identifier candidates. Per the
// user feedback after stage 8: keywords first
// (alphabetical within), identifiers second
// (alphabetical within).
fn keywords_come_before_identifiers_in_grammar_order() {
// "add column " has both keyword candidates and
// schema-identifier candidates. Per the user's stage-8
// feedback round 2: keywords first in *grammar order*
// (so `to` before `table` because the canonical shape
// is `add column [to] [table] <Table>:…`), identifiers
// after, alphabetised. The grammar order falls out of
// chumsky's source-order expected-set traversal — we
// preserve that order through `describe_expected`.
let cache = SchemaCache {
tables: vec!["Customers".to_string(), "Orders".to_string()],
..SchemaCache::default()
};
let kinds = cand_kinds_with("add column ", 11, &cache);
// Expect: ["table", "to"] keywords, then ["Customers",
// "Orders"] identifiers.
assert_eq!(
kinds,
vec![
("table".to_string(), CandidateKind::Keyword),
("to".to_string(), CandidateKind::Keyword),
("table".to_string(), CandidateKind::Keyword),
("Customers".to_string(), CandidateKind::Identifier),
("Orders".to_string(), CandidateKind::Identifier),
],