ADR-0022 stage 8 follow-up: fixes from real-app testing
Three fixes from the user's testing run, plus an investigation note on a fourth. #4 Sticky hint during cycling. The previous code recomputed candidates_at_cursor at the post-Tab cursor position, which made the panel whiplash through "what comes next at the new cursor" between cycles. ambient_hint now short-circuits to the memo's stored candidate list while the memo is alive — so Tab Tab Tab keeps showing the same list with the selection moving, then snaps to the post-Tab ambient state once any non-Tab key clears the memo. #2 Candidate ordering and kind-coloured rendering. New `Candidate { text, kind: Keyword|Identifier }` carries the classification through completion, last-completion memo, and ambient-hint payload. candidates_at_cursor now sorts keywords first (alphabetical), identifiers second (alphabetical), and the hint-panel renderer colours keywords in `tok_keyword` and identifiers in `tok_identifier`. Keyword-vs-identifier name collisions resolve in favour of the keyword (rare; the user can still address their table via different syntax). #3 tok_identifier no longer matches theme.fg. Identifiers in the input pane now render in a distinct cool grey-blue (dark) / dark steel-blue (light), so they stand out from prose-like default text without competing with keyword purple. Same colour drives the identifier candidates in the hint panel for visual consistency input ↔ hint. Limitation worth knowing: "keywords first, alphabetical" is not the same as grammatical order. For "add column " the hint shows `table to` not `to table` — chumsky's expected-set doesn't preserve combinator-source order, and encoding it in the registry adds maintenance overhead the fix doesn't cleanly justify. Marked for future revisit if it bites. #1 (Tab does nothing on "add column ") — not reproduced through App::update. The internal logic works correctly: "add column " + Tab inserts "Customers ", second Tab cycles to "Orders ", third to "Thing ". The most likely explanation is a stale binary or a terminal-level event intercept (tmux focus, kitty-keyboard protocol differences, etc.) — needs user verification with a fresh build. Tests: 747 passing, 0 failing, 1 ignored (744 baseline → +3: 2 new completion-ordering cases including the keyword-wins-on-name-collision edge, plus 1 hint-mid-cycle sticky test). Clippy clean.
This commit is contained in:
@@ -730,16 +730,20 @@ fn render_hint_panel(app: &App, theme: &Theme, frame: &mut Frame<'_>, area: Rect
|
||||
}
|
||||
|
||||
/// Render the candidate-list line for the hint panel
|
||||
/// (ADR-0022 §7 + the user's #2). Items are space-separated;
|
||||
/// the selected item (if any) gets a brighter colour + bold;
|
||||
/// when the items overflow `width`, a window centred on the
|
||||
/// selected (or starting at item 0 with no selection) shows
|
||||
/// scroll markers `<` / `>` at the edges.
|
||||
/// (ADR-0022 §7 + the user's #2). Items are space-separated.
|
||||
/// Each candidate is colour-coded by kind — keywords in
|
||||
/// `tok_keyword`, identifiers in `tok_identifier` — so the
|
||||
/// user can tell command grammar apart from schema names at
|
||||
/// a glance (post-stage-8 user feedback). The selected item
|
||||
/// (if any) gets bolded; when the items overflow `width`,
|
||||
/// scroll markers `<` / `>` appear at the edges with the
|
||||
/// window centred on the selection (or item 0 with no
|
||||
/// selection).
|
||||
///
|
||||
/// Returns `Line<'static>` (each item cloned into its span)
|
||||
/// so the caller doesn't have to manage the items' lifetime.
|
||||
fn render_candidate_line(
|
||||
items: &[String],
|
||||
items: &[crate::completion::Candidate],
|
||||
selected: Option<usize>,
|
||||
width: usize,
|
||||
theme: &Theme,
|
||||
@@ -747,29 +751,32 @@ fn render_candidate_line(
|
||||
if items.is_empty() {
|
||||
return Line::default();
|
||||
}
|
||||
let item_style = Style::default().fg(theme.muted);
|
||||
let selected_style = Style::default()
|
||||
.fg(theme.fg)
|
||||
.add_modifier(Modifier::BOLD);
|
||||
let separator_style = Style::default().fg(theme.muted);
|
||||
let marker_style = Style::default().fg(theme.fg);
|
||||
let style_for = |i: usize| {
|
||||
let base_fg = match items[i].kind {
|
||||
crate::completion::CandidateKind::Keyword => theme.tok_keyword,
|
||||
crate::completion::CandidateKind::Identifier => theme.tok_identifier,
|
||||
};
|
||||
let mut s = Style::default().fg(base_fg);
|
||||
if Some(i) == selected {
|
||||
s = s.add_modifier(Modifier::BOLD);
|
||||
}
|
||||
s
|
||||
};
|
||||
|
||||
let total_width: usize = items
|
||||
.iter()
|
||||
.map(|s| s.len() + 1)
|
||||
.map(|c| c.text.len() + 1)
|
||||
.sum::<usize>()
|
||||
.saturating_sub(1);
|
||||
if total_width <= width {
|
||||
let mut spans: Vec<Span<'static>> = Vec::with_capacity(items.len() * 2);
|
||||
for (i, item) in items.iter().enumerate() {
|
||||
if i > 0 {
|
||||
spans.push(Span::styled(" ".to_string(), item_style));
|
||||
spans.push(Span::styled(" ".to_string(), separator_style));
|
||||
}
|
||||
let style = if Some(i) == selected {
|
||||
selected_style
|
||||
} else {
|
||||
item_style
|
||||
};
|
||||
spans.push(Span::styled(item.clone(), style));
|
||||
spans.push(Span::styled(item.text.clone(), style_for(i)));
|
||||
}
|
||||
return Line::from(spans);
|
||||
}
|
||||
@@ -780,12 +787,11 @@ fn render_candidate_line(
|
||||
let center = selected.unwrap_or(0);
|
||||
let mut left = center;
|
||||
let mut right = center;
|
||||
let mut used = items[center].len();
|
||||
let mut used = items[center].text.len();
|
||||
let avail = width.saturating_sub(4);
|
||||
while (left > 0 || right + 1 < items.len()) && used < avail {
|
||||
// Expand right first, then left.
|
||||
if right + 1 < items.len() {
|
||||
let cost = items[right + 1].len() + 1;
|
||||
let cost = items[right + 1].text.len() + 1;
|
||||
if used + cost <= avail {
|
||||
right += 1;
|
||||
used += cost;
|
||||
@@ -793,7 +799,7 @@ fn render_candidate_line(
|
||||
}
|
||||
}
|
||||
if left > 0 {
|
||||
let cost = items[left - 1].len() + 1;
|
||||
let cost = items[left - 1].text.len() + 1;
|
||||
if used + cost <= avail {
|
||||
left -= 1;
|
||||
used += cost;
|
||||
@@ -811,14 +817,9 @@ fn render_candidate_line(
|
||||
}
|
||||
for (offset, item) in items[left..=right].iter().enumerate() {
|
||||
if offset > 0 {
|
||||
spans.push(Span::styled(" ".to_string(), item_style));
|
||||
spans.push(Span::styled(" ".to_string(), separator_style));
|
||||
}
|
||||
let style = if Some(left + offset) == selected {
|
||||
selected_style
|
||||
} else {
|
||||
item_style
|
||||
};
|
||||
spans.push(Span::styled(item.clone(), style));
|
||||
spans.push(Span::styled(item.text.clone(), style_for(left + offset)));
|
||||
}
|
||||
if need_right_marker {
|
||||
spans.push(Span::styled(" >".to_string(), marker_style));
|
||||
|
||||
Reference in New Issue
Block a user