feat: ADR-0035 4i(d) — merge shared-entry-word completions
In advanced mode an entry word like `create`/`drop` has several candidate nodes (the SQL forms + the DSL fallback), but the walker commits to one, so completion offered only that node's continuations — `drop ` showed just `table`, and `drop rel` dead-ended at an empty list even though the DSL drops parse via fallback. At the entry-word boundary (advanced mode), walk every candidate, keep the viable (Incomplete) ones, and union their next-keyword continuations: `drop ` → table·index·column·relationship·constraint; `drop rel` → relationship; `create ` → table·unique·index. Deeper positions keep the committed walk untouched (no change to insert/update/delete/select). Each continuation is classified by producing category (Both/Advanced/ Simple) and block-ordered Both → Advanced → Simple, so they read as contiguous groups (the foundation for the 4i(e) colour, landing next). CompletionProbe carries a parallel expected_modes; the parse path is unchanged (the merge is completion-only). Tests: completion merge + partial + block-order cases; the two tests that encoded the old single-node behaviour updated. Full suite 1911 passing / 0 failing / 1 ignored; clippy clean.
This commit is contained in:
+108
-15
@@ -167,6 +167,35 @@ pub const fn identity_ranker(candidates: Vec<Candidate>) -> Vec<Candidate> {
|
||||
candidates
|
||||
}
|
||||
|
||||
/// Which input mode(s) a shared-entry-word continuation belongs to.
|
||||
///
|
||||
/// (ADR-0035 §4i d/e.) Computed only where a shared entry word's
|
||||
/// candidate nodes are merged; everywhere else a continuation is `Both`
|
||||
/// (neutral). Drives the contiguous colour-block ordering (and, in the
|
||||
/// UI, the colour) when a candidate list mixes modes.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ModeClass {
|
||||
/// Valid in both simple (DSL) and advanced (SQL) mode — neutral.
|
||||
Both,
|
||||
/// Advanced/SQL-only continuation.
|
||||
Advanced,
|
||||
/// Simple/DSL-only continuation.
|
||||
Simple,
|
||||
}
|
||||
|
||||
impl ModeClass {
|
||||
/// Block-ordering key: `Both` (0) → `Advanced` (1) → `Simple` (2), so
|
||||
/// each colour reads as one contiguous block (user-confirmed order).
|
||||
#[must_use]
|
||||
pub const fn block_order(self) -> u8 {
|
||||
match self {
|
||||
Self::Both => 0,
|
||||
Self::Advanced => 1,
|
||||
Self::Simple => 2,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum CandidateKind {
|
||||
/// One of the parser's expected keywords.
|
||||
@@ -504,6 +533,35 @@ pub fn candidates_at_cursor_with_in_mode(
|
||||
let mut seen_kw = std::collections::HashSet::new();
|
||||
keywords.retain(|k| seen_kw.insert(k.clone()));
|
||||
|
||||
// (ADR-0035 §4i e) Block-order the keyword continuations by
|
||||
// mode-class when a shared entry word merged simple + advanced forms,
|
||||
// so the colours read as contiguous blocks Both → Advanced → Simple
|
||||
// (user-confirmed order). A single-mode list (all `Both`) is left in
|
||||
// its declaration order. `sort_by_key` is stable, preserving the
|
||||
// intra-block order.
|
||||
let kw_mode = |kw: &str| -> ModeClass {
|
||||
probe
|
||||
.expected
|
||||
.iter()
|
||||
.zip(&probe.expected_modes)
|
||||
.find_map(|(e, m)| match e {
|
||||
Expectation::Word(w) | Expectation::Literal(w) if w.eq_ignore_ascii_case(kw) => {
|
||||
Some(*m)
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
.unwrap_or(ModeClass::Both)
|
||||
};
|
||||
let mixed = keywords
|
||||
.iter()
|
||||
.map(|k| kw_mode(k.as_str()).block_order())
|
||||
.collect::<std::collections::HashSet<u8>>()
|
||||
.len()
|
||||
> 1;
|
||||
if mixed {
|
||||
keywords.sort_by_key(|k| kw_mode(k.as_str()).block_order());
|
||||
}
|
||||
|
||||
// Source 1.5: type-name candidates when the walker expects
|
||||
// a column-type slot. Type names are a closed set sourced
|
||||
// from `Type::all()` (ADR-0005 declaration order:
|
||||
@@ -1177,9 +1235,41 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn at_token_boundary_offers_next_expected_keyword() {
|
||||
// After `create ` the parser expects `table`.
|
||||
// After `create ` advanced mode offers `table` (valid in both
|
||||
// modes) plus the SQL-only `unique` (`create unique index`) and
|
||||
// `index` — the shared-entry-word merge (ADR-0035 §4i d).
|
||||
// `table` (Both) blocks before the Advanced-only `unique`/`index`.
|
||||
let cs = cands("create ", 7);
|
||||
assert_eq!(cs, vec!["table".to_string()]);
|
||||
assert_eq!(
|
||||
cs,
|
||||
vec!["table".to_string(), "unique".to_string(), "index".to_string()]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shared_entry_word_drop_merges_all_continuations() {
|
||||
// ADR-0035 §4i (d): in advanced mode `drop` is a shared entry
|
||||
// word (SQL `drop table`/`drop index` + the DSL drops). The
|
||||
// completion must offer EVERY valid continuation, not just the
|
||||
// first-decided node's. Block order: Both (table, index) then
|
||||
// Simple-only (column, relationship, constraint).
|
||||
let cs = cands("drop ", 5);
|
||||
for kw in ["table", "index", "column", "relationship", "constraint"] {
|
||||
assert!(cs.contains(&kw.to_string()), "`drop ` should offer `{kw}`; got {cs:?}");
|
||||
}
|
||||
// Both-mode continuations block before the simple-only ones.
|
||||
let pos = |k: &str| cs.iter().position(|c| c == k).unwrap();
|
||||
assert!(pos("table") < pos("column"), "Both block precedes Simple block: {cs:?}");
|
||||
assert!(pos("index") < pos("relationship"), "Both block precedes Simple block: {cs:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shared_entry_word_drop_partial_keeps_matching_continuation() {
|
||||
// A partial second keyword (`drop rel`) used to dead-end at an
|
||||
// empty list (only the SQL node walked); the merge keeps the
|
||||
// DSL `relationship` continuation.
|
||||
let cs = cands("drop rel", 8);
|
||||
assert_eq!(cs, vec!["relationship".to_string()]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1489,20 +1579,23 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn drop_in_advanced_mode_surfaces_the_sql_drop_table_completion() {
|
||||
fn drop_in_advanced_mode_surfaces_all_merged_continuations() {
|
||||
// ADR-0035 §4c: `drop` gained an advanced SQL node
|
||||
// (`DROP TABLE [IF EXISTS]`). As with the `create`/`insert`/
|
||||
// `update`/`delete` shared entry words (ADR-0033 Amendment 3),
|
||||
// advanced mode surfaces the SQL grammar's completion — here
|
||||
// just `table` — rather than the DSL subcommands. The DSL drops
|
||||
// (`drop column` etc.) still parse via fallback; only the
|
||||
// completion hint differs (and a partial DSL keyword like
|
||||
// `drop rel` returns an empty list — a mid-word dead end).
|
||||
// ADR-0035 §13 4i (d)/(e) tracks merging the candidate sets for
|
||||
// shared entry words, and the user's request to visually
|
||||
// distinguish simple- vs advanced-mode completions in the hint
|
||||
// UI (likely by colour); this expectation grows when 4i lands.
|
||||
assert_eq!(cands("drop ", 5), vec!["table".to_string()]);
|
||||
// (`DROP TABLE [IF EXISTS]`). With the 4i (d) shared-entry-word
|
||||
// merge, advanced-mode `drop ` now offers EVERY valid
|
||||
// continuation across the SQL and DSL nodes, block-ordered Both →
|
||||
// Advanced → Simple: `table`/`index` (valid in both modes) before
|
||||
// the DSL-only `column`/`relationship`/`constraint`.
|
||||
assert_eq!(
|
||||
cands("drop ", 5),
|
||||
vec![
|
||||
"table".to_string(),
|
||||
"index".to_string(),
|
||||
"column".to_string(),
|
||||
"relationship".to_string(),
|
||||
"constraint".to_string(),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user