fix(input): thread the : one-shot escape into live SQL feedback

The `:` one-shot escape (ADR-0003) is stripped at submission, but the
*live* feedback kept the leading `:` in the buffer it handed the
walker — so Tab completion, the validity verdict, and the highlight
overlays all bailed at the `:` and treated the SQL as an unknown
command. Effect: in `:`-mode, Tab completed nothing and a valid query
could flash an error, while the identical line in full `mode advanced`
worked. (The ambient hint already stripped it, which is why the hint
showed the right column name while Tab did nothing.)

Add `App::feedback_view()` — the `:`-stripped SQL view, the cursor
mapped into it, and the stripped byte offset — and route all four live
paths through it:

- completion (Tab): complete against the view, then shift the returned
  `replaced_range` back by the offset so the edit lands in the buffer;
- validity verdict: verdict the SQL, not the sigil;
- highlight/overlays: new `render_input_runs_feedback` highlights and
  diagnoses the view (shifted by the offset) while the `:` renders as
  plain text;
- ambient hint: consolidated onto `feedback_view`, replacing the
  duplicate local `strip_one_shot_prefix`.
This commit is contained in:
claude@clouddev1
2026-06-12 12:43:00 +00:00
parent 4cacb8261c
commit f7155ceafc
3 changed files with 254 additions and 53 deletions
+19 -27
View File
@@ -1438,12 +1438,19 @@ fn render_input_one_row(
let offset = input_scroll_offset(line_cols, cursor_col, tw, app.input_scroll_offset);
app.input_scroll_offset = offset;
let runs = crate::input_render::render_input_runs_in_mode(
// Strip the `:` one-shot prefix for the SQL highlighting/overlays
// (ADR-0003); the `:` itself renders as plain text. Identity for
// non-one-shot input.
let (fb_view, fb_cursor, fb_off) = app.feedback_view();
let runs = crate::input_render::render_input_runs_feedback(
&app.input,
cursor,
theme,
&app.schema_cache,
mode_for_render,
fb_view,
fb_cursor,
fb_off,
);
let spans = runs_to_spans(&app.input, &runs);
@@ -1507,12 +1514,19 @@ fn render_input_two_rows(
let offset = input_scroll_offset(line_cols, cursor_col, capacity, app.input_scroll_offset);
app.input_scroll_offset = offset;
let runs = crate::input_render::render_input_runs_in_mode(
// Strip the `:` one-shot prefix for the SQL highlighting/overlays
// (ADR-0003); the `:` itself renders as plain text. Identity for
// non-one-shot input.
let (fb_view, fb_cursor, fb_off) = app.feedback_view();
let runs = crate::input_render::render_input_runs_feedback(
&app.input,
cursor,
theme,
&app.schema_cache,
mode_for_render,
fb_view,
fb_cursor,
fb_off,
);
let cells = expand_runs_to_cells(&app.input, &runs);
let len = cells.len();
@@ -1621,23 +1635,6 @@ fn runs_to_spans<'a>(
.collect()
}
/// Strip a leading one-shot `:` sigil (and the whitespace after
/// it) from `input`, returning the advanced command slice and the
/// cursor remapped into it. Mirrors `App::submit`'s `:` handling
/// so the hint panel hints at the command, not the sigil
/// (ADR-0022 Amendment 1). Used only when the effective mode is
/// `AdvancedOneShot`, where `input` is guaranteed to start (after
/// any leading whitespace) with `:`.
fn strip_one_shot_prefix(input: &str, cursor: usize) -> (&str, usize) {
let lead_ws = input.len() - input.trim_start().len();
let after_colon = lead_ws + 1; // skip the `:`
let ws_after = input[after_colon..].len() - input[after_colon..].trim_start().len();
let prefix_len = (after_colon + ws_after).min(input.len());
let effective = &input[prefix_len..];
let effective_cursor = cursor.saturating_sub(prefix_len).min(effective.len());
(effective, effective_cursor)
}
/// Resolve the Hint panel body into its rendered lines, pre-wrapped
/// to the panel's inner width and clamped to `max_rows` with an
/// ellipsis backstop (issue #12). `max_rows` is the geometry-fixed row
@@ -1679,14 +1676,9 @@ fn resolve_hint_lines(
// In one-shot advanced mode (`:` prefix in simple mode) the
// raw input carries the `:` sigil, which is not part of the
// grammar. Strip it for the ambient computation so the hint
// reflects the advanced command — mirroring `App::submit`.
let (hint_input, hint_cursor) = match app.effective_mode() {
EffectiveMode::AdvancedOneShot => {
strip_one_shot_prefix(&app.input, app.input_cursor)
}
_ => (app.input.as_str(), app.input_cursor),
};
// grammar. The shared feedback view strips it so the hint reflects
// the advanced command — mirroring `App::submit` (ADR-0003).
let (hint_input, hint_cursor, _off) = app.feedback_view();
let ambient = crate::input_render::ambient_hint_in_mode(
hint_input,
hint_cursor,