app: mode-threaded completion, overlay, and validity indicator
The dispatch-layer mode gate (previous commit) made the submit behaviour correct — `select` runs in advanced mode and shows the SQL hint in simple mode. This commit extends that gating to the ambient assistance layer so simple-mode users do not see SQL leak through Tab completion, the live error overlay, or the `[ERR]`/`[WRN]` validity indicator either. `_in_mode` walker variants -------------------------- - `completion_probe_in_mode`, `expected_at_input_in_mode`, `input_verdict_in_mode`. Each sets `ctx.mode` before walking. The empty-input / unknown-entry fallback in `completion_probe` and `expected_at_input` filters the `REGISTRY` listing by `is_advanced_only` so Tab does not offer `select` in simple mode. Old signatures keep delegating to `Mode::Advanced` (back-compat for tests + other callers). `_in_mode` completion variants ------------------------------ - `candidates_at_cursor_in_mode`, `candidates_at_cursor_with_in_mode`. Internally they route the `parse_command` completeness probe through `parse_command_in_mode(input, mode)`, the `completion_probe` call through `completion_probe_in_mode`, and the `expected_at` fallback through `expected_at_input_in_mode`. Old signatures default to `Mode::Advanced`. `EffectiveMode::as_mode` ------------------------ - Collapses the persistent / one-shot distinction the UI cares about into the plain `Mode` the walker reads from `WalkContext::mode`. App-level call sites that thread mode into the walker chain use this. App / input-render wiring ------------------------- - `App::input_validity_verdict` runs only when effective mode is plain `Simple` (per ADR-0027), so it hardcodes `Mode::Simple` into the new `input_verdict_in_mode` call rather than threading. - `App::start_or_complete_at` / `_last` (the Tab handlers) pass `self.effective_mode().as_mode()` into `candidates_at_cursor_in_mode`, so a `:` one-shot or persistent advanced gives full SQL completion, persistent simple does not offer SQL. - `input_render::render_input_runs` and `ambient_hint` are invoked from `ui.rs` only when effective mode is plain `Simple` (advanced rendering uses `plain_input_spans` and skips ambient hinting per ADR-0022 §12). Their internal `classify_input_with_schema` / `candidates_at_cursor` / `parse_command` calls now go through the mode-aware variants with `Mode::Simple` hardcoded — a SQL form in simple mode surfaces as a definite-error overlay and the hint panel does not offer it. After this commit a simple-mode user typing `select` or `sel<Tab>` sees nothing SQL-shaped: no live highlight, no Tab completion candidate, the `[ERR]` indicator lit, and the on- submit hint that names the recovery paths. An advanced-mode user or a `:` one-shot sees the full SQL surface.
This commit is contained in:
+72
-18
@@ -250,26 +250,46 @@ pub fn completion_probe(
|
||||
source: &str,
|
||||
schema: &crate::completion::SchemaCache,
|
||||
) -> CompletionProbe {
|
||||
use crate::dsl::grammar::REGISTRY;
|
||||
completion_probe_in_mode(source, schema, crate::mode::Mode::Advanced)
|
||||
}
|
||||
|
||||
/// Mode-aware [`completion_probe`] (ADR-0030 §2).
|
||||
///
|
||||
/// In `Mode::Simple` the empty-input / fall-through fallback
|
||||
/// omits advanced-only entry words so Tab does not offer SQL
|
||||
/// commands in simple mode, and the walker — running with
|
||||
/// `ctx.mode = mode` — gates SQL-only forms inline.
|
||||
pub fn completion_probe_in_mode(
|
||||
source: &str,
|
||||
schema: &crate::completion::SchemaCache,
|
||||
mode: crate::mode::Mode,
|
||||
) -> CompletionProbe {
|
||||
use crate::dsl::grammar::{REGISTRY, is_advanced_only};
|
||||
|
||||
let mode_filtered_entries = || -> Vec<outcome::Expectation> {
|
||||
REGISTRY
|
||||
.iter()
|
||||
.filter(|c| {
|
||||
mode == crate::mode::Mode::Advanced
|
||||
|| !is_advanced_only(c.entry.primary)
|
||||
})
|
||||
.map(|c| outcome::Expectation::Word(c.entry.primary))
|
||||
.collect()
|
||||
};
|
||||
|
||||
if source.trim().is_empty() {
|
||||
return CompletionProbe {
|
||||
expected: REGISTRY
|
||||
.iter()
|
||||
.map(|c| outcome::Expectation::Word(c.entry.primary))
|
||||
.collect(),
|
||||
expected: mode_filtered_entries(),
|
||||
current_table_columns: None,
|
||||
pending_hint_mode: None,
|
||||
};
|
||||
}
|
||||
let mut ctx = context::WalkContext::with_schema(schema);
|
||||
ctx.mode = mode;
|
||||
let (result, _cmd) = walk(source, outcome::WalkBound::EndOfInput, &mut ctx);
|
||||
let Some(result) = result else {
|
||||
return CompletionProbe {
|
||||
expected: REGISTRY
|
||||
.iter()
|
||||
.map(|c| outcome::Expectation::Word(c.entry.primary))
|
||||
.collect(),
|
||||
expected: mode_filtered_entries(),
|
||||
current_table_columns: None,
|
||||
pending_hint_mode: None,
|
||||
};
|
||||
@@ -320,6 +340,22 @@ pub fn completion_probe(
|
||||
pub fn input_verdict(
|
||||
source: &str,
|
||||
schema: Option<&crate::completion::SchemaCache>,
|
||||
) -> Option<outcome::Severity> {
|
||||
input_verdict_in_mode(source, schema, crate::mode::Mode::Advanced)
|
||||
}
|
||||
|
||||
/// Mode-aware [`input_verdict`] (ADR-0030 §2).
|
||||
///
|
||||
/// The `[ERR]` / `[WRN]` indicator reads this through
|
||||
/// `App::input_verdict_for_indicator` passing the line's
|
||||
/// effective mode, so a simple-mode `select` lights up ERROR
|
||||
/// (the SQL-hint validation failure) and an advanced-mode
|
||||
/// `select` does not.
|
||||
#[must_use]
|
||||
pub fn input_verdict_in_mode(
|
||||
source: &str,
|
||||
schema: Option<&crate::completion::SchemaCache>,
|
||||
mode: crate::mode::Mode,
|
||||
) -> Option<outcome::Severity> {
|
||||
use outcome::Severity;
|
||||
if source.trim().is_empty() {
|
||||
@@ -329,6 +365,7 @@ pub fn input_verdict(
|
||||
context::WalkContext::new,
|
||||
context::WalkContext::with_schema,
|
||||
);
|
||||
ctx.mode = mode;
|
||||
let (result, _cmd) = walk(source, outcome::WalkBound::EndOfInput, &mut ctx);
|
||||
let Some(result) = result else {
|
||||
// The first token is not a registered command word —
|
||||
@@ -685,24 +722,41 @@ fn pair_type_mismatch(
|
||||
/// produces.
|
||||
#[must_use]
|
||||
pub fn expected_at_input(source: &str) -> Vec<outcome::Expectation> {
|
||||
use crate::dsl::grammar::REGISTRY;
|
||||
expected_at_input_in_mode(source, crate::mode::Mode::Advanced)
|
||||
}
|
||||
|
||||
/// Mode-aware [`expected_at_input`] (ADR-0030 §2). Filters the
|
||||
/// empty / unknown-entry fallback by mode so simple mode does
|
||||
/// not surface advanced-only entry words.
|
||||
#[must_use]
|
||||
pub fn expected_at_input_in_mode(
|
||||
source: &str,
|
||||
mode: crate::mode::Mode,
|
||||
) -> Vec<outcome::Expectation> {
|
||||
use crate::dsl::grammar::{REGISTRY, is_advanced_only};
|
||||
|
||||
let mode_filtered = || -> Vec<outcome::Expectation> {
|
||||
REGISTRY
|
||||
.iter()
|
||||
.filter(|c| {
|
||||
mode == crate::mode::Mode::Advanced
|
||||
|| !is_advanced_only(c.entry.primary)
|
||||
})
|
||||
.map(|c| outcome::Expectation::Word(c.entry.primary))
|
||||
.collect()
|
||||
};
|
||||
|
||||
if source.trim().is_empty() {
|
||||
return REGISTRY
|
||||
.iter()
|
||||
.map(|c| outcome::Expectation::Word(c.entry.primary))
|
||||
.collect();
|
||||
return mode_filtered();
|
||||
}
|
||||
let mut ctx = context::WalkContext::new();
|
||||
ctx.mode = mode;
|
||||
let (result, _cmd) = walk(source, outcome::WalkBound::EndOfInput, &mut ctx);
|
||||
let Some(result) = result else {
|
||||
// Walker didn't engage (unknown entry word): the
|
||||
// completion engine should still surface the available
|
||||
// entry words so the user can recover.
|
||||
return REGISTRY
|
||||
.iter()
|
||||
.map(|c| outcome::Expectation::Word(c.entry.primary))
|
||||
.collect();
|
||||
return mode_filtered();
|
||||
};
|
||||
match result.outcome {
|
||||
// On Match, surface the outer-shape's skipped-Optional
|
||||
|
||||
Reference in New Issue
Block a user