feat: ADR-0035 4i(e) — colour DSL vs SQL completions when mixed

Building on the 4i(d) merge: tag each completion Candidate with a
ModeClass (Both/Advanced/Simple) and, in the hint UI, colour the
continuations by mode ONLY when a candidate list actually mixes modes
(a shared entry word offering both SQL and DSL forms) — Advanced →
theme.mode_advanced, Simple → theme.mode_simple, Both → the token-kind
colour. A single-mode list (the common case, e.g. deep inside a SQL
statement) keeps the token-kind colours, so the tint appears only where
it distinguishes DSL from SQL. With (d)'s Both → Advanced → Simple
block-ordering, each colour reads as one contiguous block.

Candidate gains a `mode` field (typing_surface snapshots regenerated —
uniformly `mode: Both`, no semantic change). Tests: render_candidate_line
mixed-mode colours + the single-mode-keeps-kind-colour rule. Full suite
1913 passing / 0 failing / 1 ignored; clippy clean.
This commit is contained in:
claude@clouddev1
2026-05-26 12:11:12 +00:00
parent 1afcf4ed29
commit f85261032d
132 changed files with 699 additions and 30 deletions
+64 -1
View File
@@ -915,13 +915,35 @@ fn render_candidate_line(
}
let separator_style = Style::default().fg(theme.muted);
let marker_style = Style::default().fg(theme.fg);
// (ADR-0035 §4i e) When a shared entry word merged simple + advanced
// continuations, the list mixes mode-classes — colour SQL-only
// (`Advanced`) and DSL-only (`Simple`) continuations with the mode
// palette so a learner sees which is which; `Both` (and every
// single-mode list) keeps the token-kind colour, so the tint appears
// only where it is informative.
let mixed = {
let mut seen = std::collections::HashSet::new();
for c in items {
seen.insert(c.mode.block_order());
}
seen.len() > 1
};
let style_for = |i: usize| {
let base_fg = match items[i].kind {
let kind_fg = match items[i].kind {
crate::completion::CandidateKind::Keyword => theme.tok_keyword,
crate::completion::CandidateKind::Identifier => theme.tok_identifier,
crate::completion::CandidateKind::Flag => theme.tok_flag,
crate::completion::CandidateKind::Punct => theme.tok_punct,
};
let base_fg = if mixed {
match items[i].mode {
crate::completion::ModeClass::Both => kind_fg,
crate::completion::ModeClass::Advanced => theme.mode_advanced,
crate::completion::ModeClass::Simple => theme.mode_simple,
}
} else {
kind_fg
};
let mut s = Style::default().fg(base_fg);
if Some(i) == selected {
s = s.add_modifier(Modifier::BOLD);
@@ -1072,6 +1094,47 @@ mod tests {
assert_eq!(rendered.spans[2].style.fg, Some(theme.fg));
}
#[test]
fn candidate_line_colours_mixed_mode_continuations() {
// ADR-0035 §4i (e): when a shared entry word's completions mix
// mode-classes, Advanced (SQL-only) → mode_advanced, Simple
// (DSL-only) → mode_simple, Both → token-kind colour. Spans
// alternate candidate / separator, so candidate `i` is span `2*i`.
use crate::completion::{Candidate, CandidateKind, ModeClass};
let theme = Theme::dark();
let items = vec![
Candidate { text: "table".into(), kind: CandidateKind::Keyword, mode: ModeClass::Both },
Candidate { text: "index".into(), kind: CandidateKind::Keyword, mode: ModeClass::Advanced },
Candidate { text: "relationship".into(), kind: CandidateKind::Keyword, mode: ModeClass::Simple },
];
let line = render_candidate_line(&items, None, 100, &theme);
assert_eq!(line.spans[0].content.as_ref(), "table");
assert_eq!(line.spans[0].style.fg, Some(theme.tok_keyword), "Both keeps the kind colour");
assert_eq!(line.spans[2].content.as_ref(), "index");
assert_eq!(line.spans[2].style.fg, Some(theme.mode_advanced), "Advanced → advanced colour");
assert_eq!(line.spans[4].content.as_ref(), "relationship");
assert_eq!(line.spans[4].style.fg, Some(theme.mode_simple), "Simple → simple colour");
}
#[test]
fn candidate_line_single_mode_keeps_kind_colour() {
// The mode tint applies ONLY when the list mixes classes. An
// all-one-mode list (the common case, e.g. deep inside a SQL
// statement) keeps the token-kind colours — no redundant tint.
use crate::completion::{Candidate, CandidateKind, ModeClass};
let theme = Theme::dark();
let items = vec![
Candidate { text: "values".into(), kind: CandidateKind::Keyword, mode: ModeClass::Advanced },
Candidate { text: "select".into(), kind: CandidateKind::Keyword, mode: ModeClass::Advanced },
];
let line = render_candidate_line(&items, None, 100, &theme);
assert_eq!(
line.spans[0].style.fg,
Some(theme.tok_keyword),
"an all-Advanced list is not tinted (would be redundant noise)"
);
}
#[test]
fn a_line_without_styled_runs_keeps_whole_line_kind_styling() {
let theme = Theme::dark();