fix: advanced CREATE TABLE completion cluster

Three completion / hint bugs in the same advanced-mode grammar
+ walker path:

1. `create table T ` offered only `with` (the DSL fallback) — the
   `(` continuation for the SQL column-def list (ADR-0035 §4) was
   missing because the shared-entry-word completion merge in
   `completion_probe_in_mode` only fired at the entry-word boundary.
   Broadened to fire at any cursor depth and to handle
   `Expectation::Punct` continuations alongside `Word`/`Literal`. A
   shared-entry-word candidate whose grammar has already diverged
   (e.g. SQL `CREATE INDEX` past `create table …`) returns
   Mismatch and is naturally skipped — the viability check stays the
   gate, not the cursor depth.

2. `create table T (` showed only the table-level constraint
   keywords (`primary`, `unique`, `check`, `constraint`, `foreign`)
   in the ambient hint, leaving the column-name role invisible
   because COLUMN_DEF starts with an `Ident::NewName` slot that
   produces no concrete candidate. Added a new `HintMode::IntroProse(
   &'static str)` variant that surfaces catalog prose at slot entry
   without suppressing Tab completion (unlike `ProseOnly`) and
   without requiring `typing_name_at_cursor` to fire (unlike
   `ForceProse`). Wrapped ELEMENT in `Node::Hinted { mode: IntroProse(
   "hint.create_table_element"), … }`, with prose "Type a column
   name, or a table-level constraint: `primary`, `unique`, `check`,
   `constraint`, `foreign`". Tab still cycles every keyword.

3. The SQL_TYPE position leaked the bare keyword `double` (the
   first token of the dedicated `double precision` Choice branch
   per ADR-0035 §6.3) alongside the playground's regular type list.
   Added `("double", "double precision")` to `COMPOSITE_CANDIDATES`
   and extended the keyword filter to drop composite openers so the
   composite phrase replaces the bare opener instead of appearing
   alongside it. Tab now offers `double precision` as a single
   coherent candidate; the partial-typing prose at the same slot is
   subsumed by item 2's IntroProse (the user reads "Type a column
   name…" while mid-typing, then advances to the clean type list).

Tests added (4): pinning each behavioural promise above plus the
no-leakage assertion at the partial-typing prose position. Full
suite 2035 passed / 0 failed / 0 unexpected skips. Clippy clean.

The new `HintMode::IntroProse` variant is an additive extension to
the ADR-0024 HintMode-per-node model; no behaviour change to
existing modes. An ADR-0024 amendment recording it can follow later
if desired — flagged but not written.
This commit is contained in:
claude@clouddev1
2026-05-28 18:56:13 +00:00
parent c12ed1da9a
commit 6f87ad1842
7 changed files with 259 additions and 35 deletions
+22 -5
View File
@@ -25,11 +25,22 @@ use crate::mode::Mode;
/// one token but which the user types as a single fluent piece.
/// Pairs of (walker-expected-literal, full-composite-text).
///
/// When the walker reports `Expectation::Literal(opener)` at the
/// cursor, the engine surfaces the full composite text as a Tab
/// candidate. Today the only entry is `1:n` (the opener for
/// `add 1:n relationship`) — adding more is a one-line edit.
const COMPOSITE_CANDIDATES: &[(&str, &str)] = &[("1", "1:n")];
/// When the walker reports `Expectation::Literal(opener)` or
/// `Expectation::Word(opener)` at the cursor, the engine surfaces the
/// full composite text as a Tab candidate. Used for multi-token
/// fragments the user thinks of as a single phrase:
///
/// - `1:n` — the opener for `add 1:n relationship`.
/// - `double precision` — the lone two-word SQL type alias
/// (ADR-0035 §6.3; the grammar has a dedicated branch so the per-word
/// `Ident` validator never has to make sense of `double` alone).
/// Surfacing it as a composite stops bare `double` from appearing in
/// the type candidate list alongside `int`/`text`/etc. (issue #5).
/// Source 1's keyword filter drops openers that appear here so the
/// composite replaces the bare opener rather than appearing
/// alongside it.
const COMPOSITE_CANDIDATES: &[(&str, &str)] =
&[("1", "1:n"), ("double", "double precision")];
/// Per-project schema lookup cache (ADR-0022 §9, ADR-0024 §Phase D).
///
@@ -525,6 +536,11 @@ pub fn candidates_at_cursor_with_in_mode(
// Declaration order is preserved (matches the canonical
// command shape, e.g. `to` before `table` for
// `add column [to] [table] …`).
//
// Composite openers (e.g. `double` for `double precision`) are
// filtered out here so Source 1.6 (the composite pipeline) can
// surface the full multi-word candidate without the bare opener
// also appearing alongside (issue #5).
let mut keywords: Vec<String> = expected
.iter()
.filter_map(|e| match e {
@@ -532,6 +548,7 @@ pub fn candidates_at_cursor_with_in_mode(
_ => None,
})
.filter(|w| !w.is_empty() && w.chars().all(|c| c.is_ascii_alphabetic()))
.filter(|w| !COMPOSITE_CANDIDATES.iter().any(|(opener, _)| opener == w))
.map(str::to_string)
.filter(|name| matches_prefix(name))
.collect();