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:
+96
-3
@@ -242,6 +242,11 @@ const fn catalog_key_for_value_type(ty: crate::dsl::types::Type) -> &'static str
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CompletionProbe {
|
||||
pub expected: Vec<outcome::Expectation>,
|
||||
/// Mode-class for each entry in `expected` (same length / order).
|
||||
/// All `Both` except where a shared entry word's candidate nodes were
|
||||
/// merged (ADR-0035 §4i d/e), so the completion engine can block-order
|
||||
/// (and the UI colour) mixed simple/advanced continuations.
|
||||
pub expected_modes: Vec<crate::completion::ModeClass>,
|
||||
/// Columns of `current_table` resolved at the cursor (set
|
||||
/// by an `Ident { source: Tables, writes_table: true }`
|
||||
/// earlier in the walk). `None` when the walker is
|
||||
@@ -310,8 +315,10 @@ pub fn completion_probe_in_mode(
|
||||
};
|
||||
|
||||
if source.trim().is_empty() {
|
||||
let entries = mode_filtered_entries();
|
||||
return CompletionProbe {
|
||||
expected: mode_filtered_entries(),
|
||||
expected_modes: vec![crate::completion::ModeClass::Both; entries.len()],
|
||||
expected: entries,
|
||||
current_table_columns: None,
|
||||
pending_hint_mode: None,
|
||||
from_scope: Vec::new(),
|
||||
@@ -322,15 +329,17 @@ pub fn completion_probe_in_mode(
|
||||
ctx.mode = mode;
|
||||
let (result, _cmd) = walk(source, outcome::WalkBound::EndOfInput, &mut ctx);
|
||||
let Some(result) = result else {
|
||||
let entries = mode_filtered_entries();
|
||||
return CompletionProbe {
|
||||
expected: mode_filtered_entries(),
|
||||
expected_modes: vec![crate::completion::ModeClass::Both; entries.len()],
|
||||
expected: entries,
|
||||
current_table_columns: None,
|
||||
pending_hint_mode: None,
|
||||
from_scope: Vec::new(),
|
||||
cte_bindings: Vec::new(),
|
||||
};
|
||||
};
|
||||
let expected = match result.outcome {
|
||||
let mut expected = match result.outcome {
|
||||
outcome::WalkOutcome::Match { .. } => result.tail_expected,
|
||||
// A trailing-junk Mismatch (the shape matched, then the
|
||||
// user kept typing) still carries the outer shape's
|
||||
@@ -377,8 +386,92 @@ pub fn completion_probe_in_mode(
|
||||
}
|
||||
(top_from, ctes)
|
||||
};
|
||||
// ADR-0035 §4i (d/e): shared-entry-word completion merge. In advanced
|
||||
// mode an entry word like `create` / `drop` has several candidate
|
||||
// nodes (SQL forms + the DSL fallback), but the walk above committed
|
||||
// to ONE, so only that node's continuations are in `expected`. At the
|
||||
// entry-word boundary (nothing typed after the entry word yet — the
|
||||
// divergence point), walk every candidate, keep the viable
|
||||
// (Incomplete) ones, and union their next-keyword continuations,
|
||||
// tagging each by the producing category — so completion offers all
|
||||
// valid continuations and can colour/order them by mode. Deeper
|
||||
// positions keep the committed walk's `expected` untouched.
|
||||
let mut expected_modes = vec![crate::completion::ModeClass::Both; expected.len()];
|
||||
if mode == crate::mode::Mode::Advanced {
|
||||
let s = skip_whitespace(source, 0);
|
||||
if let Some((kw_start, kw_end)) = consume_ident(source, s)
|
||||
&& skip_whitespace(source, kw_end) >= source.len()
|
||||
{
|
||||
let entry = &source[kw_start..kw_end];
|
||||
let candidates = grammar::commands_for_entry_word(entry);
|
||||
if candidates.len() > 1 {
|
||||
use crate::dsl::grammar::CommandCategory;
|
||||
// (continuation word, produced-by-simple, produced-by-advanced)
|
||||
let mut tally: Vec<(&'static str, bool, bool)> = Vec::new();
|
||||
for (_, node, category) in candidates {
|
||||
let mut sctx = context::WalkContext::with_schema(schema);
|
||||
sctx.mode = mode;
|
||||
let (res, _) =
|
||||
walk_one_command(source, source, kw_start, kw_end, 0, node, &mut sctx);
|
||||
let outcome::WalkOutcome::Incomplete { expected: cont, .. } = res.outcome
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let advanced = category == CommandCategory::Advanced;
|
||||
for e in &cont {
|
||||
if let outcome::Expectation::Word(w) | outcome::Expectation::Literal(w) = e {
|
||||
match tally.iter_mut().find(|(kw, _, _)| kw == w) {
|
||||
Some(rec) => {
|
||||
if advanced {
|
||||
rec.2 = true;
|
||||
} else {
|
||||
rec.1 = true;
|
||||
}
|
||||
}
|
||||
None => tally.push((w, !advanced, advanced)),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if !tally.is_empty() {
|
||||
// Augment `expected` with merged continuations the
|
||||
// committed node lacked.
|
||||
for &(w, _, _) in &tally {
|
||||
let present = expected.iter().any(|e| {
|
||||
matches!(e,
|
||||
outcome::Expectation::Word(x) | outcome::Expectation::Literal(x)
|
||||
if *x == w)
|
||||
});
|
||||
if !present {
|
||||
expected.push(outcome::Expectation::Word(w));
|
||||
}
|
||||
}
|
||||
// Classify every expectation by the merged tally.
|
||||
expected_modes = expected
|
||||
.iter()
|
||||
.map(|e| match e {
|
||||
outcome::Expectation::Word(w) | outcome::Expectation::Literal(w) => {
|
||||
tally.iter().find(|(kw, _, _)| kw == w).map_or(
|
||||
crate::completion::ModeClass::Both,
|
||||
|&(_, simple, adv)| match (simple, adv) {
|
||||
(true, true) | (false, false) => {
|
||||
crate::completion::ModeClass::Both
|
||||
}
|
||||
(false, true) => crate::completion::ModeClass::Advanced,
|
||||
(true, false) => crate::completion::ModeClass::Simple,
|
||||
},
|
||||
)
|
||||
}
|
||||
_ => crate::completion::ModeClass::Both,
|
||||
})
|
||||
.collect();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
CompletionProbe {
|
||||
expected,
|
||||
expected_modes,
|
||||
current_table_columns: ctx.current_table_columns,
|
||||
pending_hint_mode: ctx.pending_hint_mode,
|
||||
from_scope,
|
||||
|
||||
Reference in New Issue
Block a user