2g: advanced-mode highlight + engine.* wiring + matrix tests
Cross-cut verification matrix for ADR-0032 Phase 2 is now fully populated with concrete test references — every row green. Filling the matrix surfaced three real gaps that this commit closes. 1. Advanced-mode syntax highlighting (ADR-0030 §8 matrix row). The `ui.rs` Advanced branch routed through `plain_input_spans`, bypassing the highlight walker entirely. In production SQL keywords past the entry word rendered as plain identifiers. Fix: mode-aware variants of `highlight_runs`, `render_input_runs`, `lex_to_runs`, and `input_diagnostics`; the Advanced render path now uses the highlighted form with `Mode::Advanced`. `plain_input_spans` removed (unused). 2. Engine.* key wiring (ADR-0032 §11.4 / §13 matrix rows + handoff §3.3 follow-up). The four Phase-2 engine.* catalog entries were authored in 2d but never reached: `translate_generic` discarded the engine message and returned a vague catalog entry. Fix: pattern-match the engine message text for the four Phase-2 categories (aggregate misuse, group-by required, compound arity mismatch fallback, scalar-subquery cardinality) inside `translate_generic`, routing each to its engine-neutral catalog entry. 3. Matrix-coverage tests. Thirteen new tests covering the rows that had no explicit coverage: - 3 SQL keyword/operator/CASE highlight tests - 4 engine.* engine-message tests - 3 sql_expr column-completion tests (WHERE, HAVING) - 3 predicate-warning slot tests (CASE, ORDER BY, projection) - 1 all-10-playground-types recovery test (tests/sql_select.rs) Plan document (docs/plans/20260520-adr-0032-phase-2.md) updated: every (TBD) row in the cross-cut matrix replaced with a concrete test file::function reference and a green status marker. Test totals: 1428 → 1441 passing (+13 new). Clippy clean.
This commit is contained in:
@@ -31,13 +31,28 @@ use crate::dsl::walker::outcome::{ByteClass, WalkBound};
|
||||
|
||||
/// Produce the per-byte highlight classes for `source`.
|
||||
///
|
||||
/// On a successful walk this is exactly the walker's recorded
|
||||
/// classes. On partial / unmatched input the byte-shape scanner
|
||||
/// fills the gap so the renderer keeps colouring through trailing
|
||||
/// tokens and unknown-command inputs.
|
||||
/// Defaults to `Mode::Simple`. Callers in advanced-mode UIs
|
||||
/// should use [`highlight_runs_in_mode`] so SQL keywords get
|
||||
/// matched and highlighted past the entry word (the simple-mode
|
||||
/// gate at the dispatcher truncates the walker on advanced-only
|
||||
/// commands, ADR-0030 §2).
|
||||
#[must_use]
|
||||
pub fn highlight_runs(source: &str) -> Vec<ByteClass> {
|
||||
highlight_runs_in_mode(source, crate::mode::Mode::Simple)
|
||||
}
|
||||
|
||||
/// Mode-aware [`highlight_runs`] (ADR-0032 §10.6 follow-up).
|
||||
///
|
||||
/// In `Mode::Advanced` the walker matches every Phase-2 SQL
|
||||
/// token, producing the keyword classes the renderer needs to
|
||||
/// colour `select` / `from` / `where` / `union` / `case` / etc.
|
||||
#[must_use]
|
||||
pub fn highlight_runs_in_mode(
|
||||
source: &str,
|
||||
mode: crate::mode::Mode,
|
||||
) -> Vec<ByteClass> {
|
||||
let mut ctx = WalkContext::new();
|
||||
ctx.mode = mode;
|
||||
let (result, _cmd) = super::walk(source, WalkBound::EndOfInput, &mut ctx);
|
||||
let mut classes: Vec<ByteClass> = result
|
||||
.map(|r| r.per_byte_class)
|
||||
@@ -316,4 +331,70 @@ mod tests {
|
||||
assert_eq!(runs[0].2, HighlightClass::String);
|
||||
assert_eq!(runs[0].1, "'café'".len());
|
||||
}
|
||||
|
||||
// ---- ADR-0030 §8 / ADR-0032 — SQL keyword highlighting ----
|
||||
|
||||
fn run_advanced(input: &str) -> Vec<(usize, usize, HighlightClass)> {
|
||||
highlight_runs_in_mode(input, crate::mode::Mode::Advanced)
|
||||
.into_iter()
|
||||
.map(|c| (c.start, c.end, c.class))
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sql_select_keywords_classified() {
|
||||
// ADR-0030 §8 — `select` / `from` get keyword class in
|
||||
// Advanced mode (Simple mode gates SELECT out at the
|
||||
// dispatcher, so only the entry word would highlight).
|
||||
let runs = run_advanced("select * from t");
|
||||
assert!(
|
||||
runs.iter().any(|(s, e, c)| {
|
||||
*c == HighlightClass::Keyword && (*s, *e) == (0, 6)
|
||||
}),
|
||||
"expected `select` keyword span 0..6; got {runs:?}",
|
||||
);
|
||||
assert!(
|
||||
runs.iter().any(|(s, e, c)| {
|
||||
*c == HighlightClass::Keyword && (*s, *e) == (9, 13)
|
||||
}),
|
||||
"expected `from` keyword span 9..13; got {runs:?}",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sql_expression_operators_classified_as_keywords() {
|
||||
// ADR-0031 §5: LIKE / BETWEEN / IN / IS / AND / OR / NOT
|
||||
// are part of the predicate ladder. Walker matches them
|
||||
// as Word nodes; highlight class = Keyword.
|
||||
let input = "select * from t where a like 'x' and b between 1 and 5";
|
||||
let runs = run_advanced(input);
|
||||
let keywords: Vec<&str> = runs
|
||||
.iter()
|
||||
.filter(|(_, _, c)| *c == HighlightClass::Keyword)
|
||||
.map(|(s, e, _)| &input[*s..*e])
|
||||
.collect();
|
||||
assert!(keywords.contains(&"like"), "no `like`; got {keywords:?}");
|
||||
assert!(keywords.contains(&"and"), "no `and`; got {keywords:?}");
|
||||
assert!(
|
||||
keywords.contains(&"between"),
|
||||
"no `between`; got {keywords:?}",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sql_case_expression_keywords_classified() {
|
||||
let input = "select case when a = 1 then 'one' else 'other' end from t";
|
||||
let runs = run_advanced(input);
|
||||
let keywords: Vec<&str> = runs
|
||||
.iter()
|
||||
.filter(|(_, _, c)| *c == HighlightClass::Keyword)
|
||||
.map(|(s, e, _)| &input[*s..*e])
|
||||
.collect();
|
||||
for kw in ["case", "when", "then", "else", "end"] {
|
||||
assert!(
|
||||
keywords.contains(&kw),
|
||||
"missing `{kw}` keyword; got {keywords:?}",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user