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:
claude@clouddev1
2026-05-19 21:48:21 +00:00
parent 6369066fe4
commit 83e0ddc2ff
4 changed files with 157 additions and 35 deletions
+39 -10
View File
@@ -18,6 +18,8 @@ use crate::dsl::grammar::IdentSource;
use crate::dsl::types::Type;
use crate::dsl::walker::outcome::Expectation;
use crate::dsl::{ParseError, parse_command};
use crate::dsl::parser::parse_command_in_mode;
use crate::mode::Mode;
/// Composite literal candidates whose lexed shape is more than
/// one token but which the user types as a single fluent piece.
@@ -97,8 +99,8 @@ impl SchemaCache {
/// expressed as structured `Expectation`s direct from the
/// walker (ADR-0024 §architecture, Phase F walker-driven
/// completion). Replaces the `ParseError`-string round-trip.
fn expected_at(leading: &str) -> Vec<Expectation> {
crate::dsl::walker::expected_at_input(leading)
fn expected_at(leading: &str, mode: Mode) -> Vec<Expectation> {
crate::dsl::walker::expected_at_input_in_mode(leading, mode)
}
/// A single Tab-insertable item with its source (so the
@@ -184,7 +186,20 @@ pub fn candidates_at_cursor(
cursor: usize,
cache: &SchemaCache,
) -> Option<Completion> {
candidates_at_cursor_with(input, cursor, cache, identity_ranker)
candidates_at_cursor_in_mode(input, cursor, cache, Mode::Advanced)
}
/// Mode-aware [`candidates_at_cursor`] (ADR-0030 §2). Tab in
/// simple mode no longer offers advanced-mode-only commands;
/// the walker's mode gate flows through the probe inside.
#[must_use]
pub fn candidates_at_cursor_in_mode(
input: &str,
cursor: usize,
cache: &SchemaCache,
mode: Mode,
) -> Option<Completion> {
candidates_at_cursor_with_in_mode(input, cursor, cache, identity_ranker, mode)
}
/// Variant of [`candidates_at_cursor`] that applies a custom
@@ -197,6 +212,18 @@ pub fn candidates_at_cursor_with(
cursor: usize,
cache: &SchemaCache,
ranker: Ranker,
) -> Option<Completion> {
candidates_at_cursor_with_in_mode(input, cursor, cache, ranker, Mode::Advanced)
}
/// Mode-aware [`candidates_at_cursor_with`].
#[must_use]
pub fn candidates_at_cursor_with_in_mode(
input: &str,
cursor: usize,
cache: &SchemaCache,
ranker: Ranker,
mode: Mode,
) -> Option<Completion> {
let cursor = cursor.min(input.len());
@@ -225,20 +252,22 @@ pub fn candidates_at_cursor_with(
// `pk` at the end of `create table T with pk`. The
// optional-suffix case (`save ` → `as`) is preserved
// because there `partial_prefix` is empty.
let input_parses_complete = parse_command(input).is_ok();
let input_parses_complete = parse_command_in_mode(input, mode).is_ok();
// Schema-aware probe: one walk yields both the expected set
// and the table-context snapshot (ADR-0024 §Phase D
// §column-narrowing). The engine reads
// `current_table_columns` to narrow column candidates to the
// active table rather than the flat `cache.columns` (which
// unions every table's columns).
let probe = crate::dsl::walker::completion_probe(leading, cache);
// unions every table's columns). ADR-0030 §2: the probe runs
// with the active mode so simple-mode users don't see SQL
// commands offered.
let probe = crate::dsl::walker::completion_probe_in_mode(leading, cache, mode);
let current_table_columns: Option<&[TableColumn]> =
probe.current_table_columns.as_deref();
let expected = if probe.expected.is_empty() {
expected_at(leading)
expected_at(leading, mode)
} else {
probe.expected.clone()
};
@@ -548,7 +577,7 @@ pub fn value_literal_hint_at_cursor(input: &str, cursor: usize) -> Option<String
return None;
}
let leading = &input[..start];
let expected = expected_at(leading);
let expected = expected_at(leading, Mode::Advanced);
if !is_value_literal_signature(&expected) {
return None;
}
@@ -608,7 +637,7 @@ pub fn typing_name_at_cursor(input: &str, cursor: usize) -> Option<TypingName> {
}
}
let leading = &input[..start];
let expected = expected_at(leading);
let expected = expected_at(leading, Mode::Advanced);
let is_new_name_slot = expected.iter().any(|e| {
matches!(
e,
@@ -697,7 +726,7 @@ pub fn invalid_ident_at_cursor(
return None;
}
let leading = &input[..start];
let expected = expected_at(leading);
let expected = expected_at(leading, Mode::Advanced);
if expected.is_empty() {
return None;
}