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:
+72
-22
@@ -133,13 +133,15 @@ pub enum AmbientHint {
|
||||
/// identifier or punctuation slot), and definite-error
|
||||
/// states with optional usage template.
|
||||
Prose(String),
|
||||
/// Multi-candidate (or single-candidate keyword)
|
||||
/// completion at the cursor. Stage 8b renders these as
|
||||
/// styled spans with the selected item highlighted (if
|
||||
/// any) and `<` / `>` scroll markers when items overflow
|
||||
/// the panel width.
|
||||
/// Multi-candidate (or single-candidate) completion at
|
||||
/// the cursor. Each item carries its kind so the
|
||||
/// renderer can colour keywords differently from
|
||||
/// schema-identifiers (post-stage-8 user feedback).
|
||||
/// The selected item — if any — gets bold + brighter
|
||||
/// colour; `<` / `>` markers appear at the edges when
|
||||
/// items overflow the panel width.
|
||||
Candidates {
|
||||
items: Vec<String>,
|
||||
items: Vec<crate::completion::Candidate>,
|
||||
/// Index into `items` of the currently-inserted Tab
|
||||
/// candidate (per the live `LastCompletion` memo), or
|
||||
/// `None` if the user hasn't pressed Tab yet.
|
||||
@@ -162,16 +164,28 @@ pub fn ambient_hint(
|
||||
if input.trim().is_empty() {
|
||||
return None;
|
||||
}
|
||||
// First check for candidates at the cursor (keywords +
|
||||
// schema identifiers). When any exist the user can Tab to
|
||||
// insert one, and the panel surfaces them directly — this
|
||||
// wins over the prose IncompleteAtEof framing because the
|
||||
// candidate list is more actionable.
|
||||
// Mid-cycle through Tab candidates: the memo carries the
|
||||
// candidate list captured when Tab was first pressed, plus
|
||||
// the current selection_idx. While the memo is alive the
|
||||
// hint shows that exact list — recomputing at the
|
||||
// post-insert cursor would whiplash the panel through "what
|
||||
// comes next at the new cursor" between cycles. Closes
|
||||
// the user-reported #4 in stage-8 testing.
|
||||
if let Some(m) = memo {
|
||||
return Some(AmbientHint::Candidates {
|
||||
items: m.candidates.clone(),
|
||||
selected: Some(m.selection_idx),
|
||||
});
|
||||
}
|
||||
// No memo: fall back to candidates_at_cursor. When any
|
||||
// exist the user can Tab to insert one, and the panel
|
||||
// surfaces them directly — this wins over the prose
|
||||
// IncompleteAtEof framing because the candidate list is
|
||||
// more actionable.
|
||||
if let Some(comp) = crate::completion::candidates_at_cursor(input, cursor, cache) {
|
||||
let selected = memo.map(|m| m.selection_idx);
|
||||
return Some(AmbientHint::Candidates {
|
||||
items: comp.candidates,
|
||||
selected,
|
||||
selected: None,
|
||||
});
|
||||
}
|
||||
// Invalid identifier: cursor sits in a known-set slot but
|
||||
@@ -471,7 +485,9 @@ mod tests {
|
||||
|
||||
fn cands_hint(input: &str, cursor: usize) -> Option<Vec<String>> {
|
||||
match ambient_hint(input, cursor, None, &empty_cache()) {
|
||||
Some(AmbientHint::Candidates { items, .. }) => Some(items),
|
||||
Some(AmbientHint::Candidates { items, .. }) => {
|
||||
Some(items.into_iter().map(|c| c.text).collect())
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
@@ -562,27 +578,61 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn ambient_hint_with_memo_carries_selected_index() {
|
||||
use crate::completion::LastCompletion;
|
||||
// Simulate the post-Tab state at "show " — but with
|
||||
// the original word still pending (cursor placed
|
||||
// after `show ` to expose the multi-candidate slot).
|
||||
// The memo's selection_idx is what the renderer uses
|
||||
// to highlight one of the items.
|
||||
use crate::completion::{Candidate, CandidateKind, LastCompletion};
|
||||
let memo = LastCompletion {
|
||||
inserted_range: (5, 5),
|
||||
original_text: String::new(),
|
||||
candidates: vec!["data".to_string(), "table".to_string()],
|
||||
candidates: vec![
|
||||
Candidate { text: "data".to_string(), kind: CandidateKind::Keyword },
|
||||
Candidate { text: "table".to_string(), kind: CandidateKind::Keyword },
|
||||
],
|
||||
selection_idx: 1,
|
||||
};
|
||||
match ambient_hint("show ", 5, Some(&memo), &empty_cache()) {
|
||||
Some(AmbientHint::Candidates { items, selected }) => {
|
||||
assert_eq!(items, vec!["data".to_string(), "table".to_string()]);
|
||||
assert_eq!(items.len(), 2);
|
||||
assert_eq!(items[0].text, "data");
|
||||
assert_eq!(items[1].text, "table");
|
||||
assert_eq!(selected, Some(1));
|
||||
}
|
||||
other => panic!("expected Candidates, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ambient_hint_during_cycling_shows_memo_list_not_recomputed() {
|
||||
// Stage-8 user-reported #4: cycling through candidates
|
||||
// moves the cursor; the panel should NOT shift to "what
|
||||
// comes next at the new cursor position" — it should
|
||||
// keep showing the memo's candidate list with the
|
||||
// updated selection. Without the memo short-circuit,
|
||||
// ambient_hint would recompute candidates_at_cursor
|
||||
// post-Tab and produce a different list.
|
||||
use crate::completion::{Candidate, CandidateKind, LastCompletion};
|
||||
let memo = LastCompletion {
|
||||
inserted_range: (5, 11),
|
||||
original_text: String::new(),
|
||||
// Include candidates whose order would NOT match
|
||||
// what candidates_at_cursor("show table ", 11) would
|
||||
// produce — proves the memo's list is being used,
|
||||
// not a recomputed one.
|
||||
candidates: vec![
|
||||
Candidate { text: "data".to_string(), kind: CandidateKind::Keyword },
|
||||
Candidate { text: "table".to_string(), kind: CandidateKind::Keyword },
|
||||
],
|
||||
selection_idx: 1,
|
||||
};
|
||||
match ambient_hint("show table ", 11, Some(&memo), &empty_cache()) {
|
||||
Some(AmbientHint::Candidates { items, selected }) => {
|
||||
assert_eq!(items.len(), 2);
|
||||
assert_eq!(items[0].text, "data");
|
||||
assert_eq!(items[1].text, "table");
|
||||
assert_eq!(selected, Some(1));
|
||||
}
|
||||
other => panic!("expected Candidates from memo, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
// ---- classify_input + error overlay (stage 4) ----
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user