ADR-0022 stage 2/8: input panel — token-class highlighting
New `input_render` module with `render_input_runs(input, cursor_byte, theme) -> Vec<StyledRun>`. Lexes the input, assigns each token its `theme.token_color`, preserves whitespace gaps as `theme.fg` runs, and injects the cursor by splitting the run that contains it into before/under/after sub-spans (under marked Modifier::REVERSED). End-of-input cursor is an empty-range sentinel rendered as an inverted space. ui::render_input_panel switches over EffectiveMode: simple mode goes through render_input_runs + a small runs_to_spans helper that borrows from the input string; advanced modes (persistent + one-shot `:`) keep the previous plain before/under/after rendering since the DSL lexer doesn't speak SQL (ADR-0022 §12). Multi-byte UTF-8 in string literals is handled by walking to the next char boundary when splitting the cursor run, mirroring the previous renderer. Tests: 682 passing, 0 failing, 1 ignored (672 baseline → +10: 9 input_render unit tests covering each token class, cursor placements, multi-byte, full-command shape; +1 new "all token classes" UI snapshot). Clippy clean. Caveat (noted inline in the new snapshot test): the TestBackend/render_to_string path records text symbols only, not ratatui style. The new snapshot is therefore a text-layout regression net; the unit tests in input_render::tests are the authoritative regression net for colour mappings. Stage 3 wires the same colouring into simple-mode echo lines in the output panel.
This commit is contained in:
@@ -580,30 +580,67 @@ fn render_input_panel(app: &App, theme: &Theme, frame: &mut Frame<'_>, area: Rec
|
||||
|
||||
// Cursor block: render the character at the cursor position
|
||||
// inverted so the cursor is visible without enabling a real
|
||||
// terminal cursor. When the cursor is at end-of-input we
|
||||
// append an inverted space.
|
||||
// terminal cursor.
|
||||
//
|
||||
// Simple-mode input gets per-token colouring (ADR-0022 §3)
|
||||
// via input_render::render_input_runs. Advanced-mode input
|
||||
// — DSL lexer doesn't speak SQL — renders plain (§12), with
|
||||
// the same before/under/after cursor shape we always had.
|
||||
let cursor = app.input_cursor.min(app.input.len());
|
||||
let before = &app.input[..cursor];
|
||||
let (under, after) = if cursor < app.input.len() {
|
||||
// Find the end of the character under the cursor.
|
||||
let spans = match effective {
|
||||
EffectiveMode::Simple => {
|
||||
let runs = crate::input_render::render_input_runs(&app.input, cursor, theme);
|
||||
runs_to_spans(&app.input, &runs)
|
||||
}
|
||||
EffectiveMode::AdvancedPersistent | EffectiveMode::AdvancedOneShot => {
|
||||
plain_input_spans(&app.input, cursor, theme)
|
||||
}
|
||||
};
|
||||
let paragraph = Paragraph::new(Line::from(spans)).block(block);
|
||||
frame.render_widget(paragraph, area);
|
||||
}
|
||||
|
||||
/// Convert `StyledRun`s into ratatui `Span`s borrowed from
|
||||
/// `input`. The end-of-input cursor sentinel (empty range) is
|
||||
/// rendered as an inverted space.
|
||||
fn runs_to_spans<'a>(
|
||||
input: &'a str,
|
||||
runs: &[crate::input_render::StyledRun],
|
||||
) -> Vec<Span<'a>> {
|
||||
runs.iter()
|
||||
.map(|r| {
|
||||
if r.byte_range.0 == r.byte_range.1 {
|
||||
Span::styled(" ", r.style)
|
||||
} else {
|
||||
Span::styled(&input[r.byte_range.0..r.byte_range.1], r.style)
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Plain (no token highlighting) input rendering for advanced
|
||||
/// mode. Same before/under/after cursor shape as the
|
||||
/// pre-ADR-0022 input panel; here as a deliberate fallback.
|
||||
fn plain_input_spans<'a>(input: &'a str, cursor: usize, theme: &Theme) -> Vec<Span<'a>> {
|
||||
let cursor = cursor.min(input.len());
|
||||
let before = &input[..cursor];
|
||||
let (under, after) = if cursor < input.len() {
|
||||
let mut end = cursor + 1;
|
||||
while end < app.input.len() && !app.input.is_char_boundary(end) {
|
||||
while end < input.len() && !input.is_char_boundary(end) {
|
||||
end += 1;
|
||||
}
|
||||
(&app.input[cursor..end], &app.input[end..])
|
||||
(&input[cursor..end], &input[end..])
|
||||
} else {
|
||||
(" ", "")
|
||||
};
|
||||
let spans = vec![
|
||||
vec![
|
||||
Span::styled(before, Style::default().fg(theme.fg)),
|
||||
Span::styled(
|
||||
under,
|
||||
Style::default().fg(theme.fg).add_modifier(Modifier::REVERSED),
|
||||
),
|
||||
Span::styled(after, Style::default().fg(theme.fg)),
|
||||
];
|
||||
let paragraph = Paragraph::new(Line::from(spans)).block(block);
|
||||
frame.render_widget(paragraph, area);
|
||||
]
|
||||
}
|
||||
|
||||
fn render_hint_panel(app: &App, theme: &Theme, frame: &mut Frame<'_>, area: Rect) {
|
||||
@@ -731,6 +768,26 @@ mod tests {
|
||||
insta::assert_snapshot!("default_advanced_dark", snapshot);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn highlighted_input_all_token_classes_snapshot() {
|
||||
// ADR-0022 stage 2: representative input that exercises
|
||||
// every token class — keyword (insert / into / values
|
||||
// / null), identifier (T), number (1), string ('hi'),
|
||||
// punct (parens, comma), flag (--all-rows), lex error
|
||||
// ($ at the end). The snapshot captures the rendered
|
||||
// text symbols only — `render_to_string` does not record
|
||||
// ratatui style — so this test is a regression net for
|
||||
// text layout, not for colour mappings. Colour mappings
|
||||
// are unit-tested in `input_render::tests`.
|
||||
let mut app = App::new();
|
||||
app.input
|
||||
.push_str("insert into T values (1, 'hi', null) --all-rows $");
|
||||
app.input_cursor = app.input.len();
|
||||
let theme = Theme::dark();
|
||||
let snapshot = render_to_string(&mut app, &theme, 80, 24);
|
||||
insta::assert_snapshot!("highlighted_input_all_token_classes_dark", snapshot);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn one_shot_advanced_prompt_snapshot() {
|
||||
// Typing `:sel` in simple mode should flip the input panel
|
||||
|
||||
Reference in New Issue
Block a user