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:
@@ -693,6 +693,17 @@ fn ambient_hint_core_in_mode(
|
||||
return Some(AmbientHint::Prose(text));
|
||||
}
|
||||
}
|
||||
Some(crate::dsl::grammar::HintMode::IntroProse(key)) => {
|
||||
// Slot entry: surface the catalog prose so an
|
||||
// invisible-by-default ident slot (the column-name
|
||||
// `NewName` at the CREATE TABLE element position,
|
||||
// issue #4) reads as the dominant first move with
|
||||
// the keyword alternatives folded into the prose.
|
||||
// Tab candidates remain available via the parallel
|
||||
// completion surface; the user still cycles the
|
||||
// keyword set.
|
||||
return Some(AmbientHint::Prose(crate::friendly::translate(key, &[])));
|
||||
}
|
||||
Some(crate::dsl::grammar::HintMode::SuppressProse | crate::dsl::grammar::HintMode::Default)
|
||||
| None => {}
|
||||
}
|
||||
@@ -1111,6 +1122,143 @@ mod tests {
|
||||
assert!(ambient_hint(" ", 3, None, &empty_cache()).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn advanced_create_table_element_position_introduces_column_name() {
|
||||
// Issue #4: at `create table T (`, the user is at the
|
||||
// ELEMENT slot of the column-def list. The current candidate
|
||||
// list shows only table-level constraint keywords (`primary`,
|
||||
// `unique`, `check`, `constraint`, `foreign`); a new column
|
||||
// is the dominant first move and is currently invisible
|
||||
// because the COLUMN_DEF branch starts with an `Ident::NewName`
|
||||
// slot which produces no concrete candidate.
|
||||
//
|
||||
// The fix wraps the ELEMENT choice in a `Hinted::IntroProse`
|
||||
// that surfaces a prose hint mentioning the column name first,
|
||||
// with the constraint keywords as the alternative. Tab
|
||||
// candidates remain available.
|
||||
let cache = crate::completion::SchemaCache::default();
|
||||
let input = "create table Orders (";
|
||||
match ambient_hint_in_mode(input, input.len(), None, &cache, Mode::Advanced) {
|
||||
Some(AmbientHint::Prose(p)) => {
|
||||
assert!(
|
||||
p.to_lowercase().contains("column name"),
|
||||
"prose must mention `column name`; got: {p:?}",
|
||||
);
|
||||
// Constraint alternatives should still be mentioned.
|
||||
assert!(
|
||||
p.contains("primary") && p.contains("unique"),
|
||||
"prose should mention constraint alternatives; got: {p:?}",
|
||||
);
|
||||
}
|
||||
other => panic!("expected Prose hint at ELEMENT slot; got: {other:?}"),
|
||||
}
|
||||
// Tab candidates should remain available (the keywords still cycle).
|
||||
let comp = crate::completion::candidates_at_cursor_in_mode(
|
||||
input,
|
||||
input.len(),
|
||||
&cache,
|
||||
Mode::Advanced,
|
||||
)
|
||||
.expect("completion must remain available");
|
||||
let texts: Vec<&str> = comp.candidates.iter().map(|c| c.text.as_str()).collect();
|
||||
for kw in &["primary", "unique"] {
|
||||
assert!(
|
||||
texts.contains(kw),
|
||||
"Tab candidate `{kw}` must remain; got {texts:?}",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn advanced_partial_typing_does_not_leak_bare_double_in_prose() {
|
||||
// Issue #5 (prose half): at `create table Orders (count` (no
|
||||
// trailing space), the user is mid-typing what's
|
||||
// grammatically a column name (`count` could be the start of
|
||||
// `counterparty`). The bare `Word("double")` from the
|
||||
// DOUBLE_PRECISION_NODES branch must not appear in the
|
||||
// ambient hint at this position — the new IntroProse hint
|
||||
// from issue #4 already covers this position by introducing
|
||||
// the element slot ("Type a column name, or a table-level
|
||||
// constraint: …"), and the user discovers the type list
|
||||
// (with `double precision` as a single composite, not bare
|
||||
// `double`) when they advance to the SQL_TYPE slot.
|
||||
let cache = crate::completion::SchemaCache::default();
|
||||
let input = "create table Orders (count";
|
||||
match ambient_hint_in_mode(input, input.len(), None, &cache, Mode::Advanced) {
|
||||
Some(AmbientHint::Prose(p)) => {
|
||||
assert!(
|
||||
!p.contains("`double`"),
|
||||
"bare `double` must not appear in the prose; got: {p:?}",
|
||||
);
|
||||
}
|
||||
other => panic!("expected Prose hint at partial column name; got: {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn advanced_type_position_offers_double_precision_not_bare_double() {
|
||||
// Issue #5: at the SQL_TYPE position (`create table Orders
|
||||
// (count `), the candidate list previously surfaced `double`
|
||||
// as a peer of the playground's regular types — the user
|
||||
// sees a leading "double" alongside int/text/etc. and has
|
||||
// to know it's the start of the two-word `double precision`
|
||||
// alias. The fix surfaces `double precision` as a single
|
||||
// composite candidate and suppresses the bare `double`.
|
||||
let cache = crate::completion::SchemaCache::default();
|
||||
let input = "create table Orders (count ";
|
||||
let comp = crate::completion::candidates_at_cursor_in_mode(
|
||||
input,
|
||||
input.len(),
|
||||
&cache,
|
||||
Mode::Advanced,
|
||||
)
|
||||
.expect("completion expected at the SQL_TYPE position");
|
||||
let texts: Vec<&str> = comp.candidates.iter().map(|c| c.text.as_str()).collect();
|
||||
assert!(
|
||||
!texts.contains(&"double"),
|
||||
"bare `double` must NOT appear as a type candidate; got {texts:?}",
|
||||
);
|
||||
assert!(
|
||||
texts.contains(&"double precision"),
|
||||
"`double precision` should appear as a composite type candidate; got {texts:?}",
|
||||
);
|
||||
// The regular type vocabulary still appears.
|
||||
for t in &["int", "text", "real", "serial"] {
|
||||
assert!(
|
||||
texts.iter().any(|x| x == t),
|
||||
"regular type `{t}` must remain a candidate; got {texts:?}",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn advanced_create_table_offers_open_paren_after_name() {
|
||||
// Issue #3: typing `create table Orders ` in advanced mode
|
||||
// should offer both `with` (DSL form, ADR-0009) and `(` (SQL
|
||||
// form, ADR-0035 §4) as the next-step continuation. Today
|
||||
// only `with` surfaces — the shared-entry-word completion
|
||||
// merge only fires at the entry-word boundary, so deeper
|
||||
// positions show only the committed node's continuations.
|
||||
let cache = crate::completion::SchemaCache::default();
|
||||
let input = "create table Orders ";
|
||||
let comp = crate::completion::candidates_at_cursor_in_mode(
|
||||
input,
|
||||
input.len(),
|
||||
&cache,
|
||||
Mode::Advanced,
|
||||
)
|
||||
.expect("completion expected for advanced create-table after name");
|
||||
let texts: Vec<&str> = comp.candidates.iter().map(|c| c.text.as_str()).collect();
|
||||
assert!(
|
||||
texts.contains(&"("),
|
||||
"advanced mode must offer `(` for the SQL column-def list; got {texts:?}",
|
||||
);
|
||||
assert!(
|
||||
texts.contains(&"with"),
|
||||
"advanced mode must keep `with` for the DSL form; got {texts:?}",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn advanced_mode_ambient_offers_sql_from_slot_candidate() {
|
||||
// ADR-0022 Amendment 1: advanced-mode ambient assistance
|
||||
|
||||
Reference in New Issue
Block a user