walker+completion: surface list trailing-optionals + identifiers-first ordering (ADR-0022 Amendment 2)
walk_repeated discarded the last matched item's trailing-optional expectations at a clean item boundary, so a comma-separated list offered no continuation after a complete item: `order by Name ` gave no asc/desc, `select Name ` no `as`, `create table … Code(text) ` no not/unique/default/check. Capture the last item's skipped set and surface it when the list ends at an item boundary (the separator `,` itself is deliberately not surfaced). That fix made expression-position candidate lists long, which exposed a visibility problem: the hint panel's candidate line is single-row and window-scrolls on overflow, centring on item 0 when nothing is selected — so with keywords-first, schema identifiers scrolled off behind the `>` marker. Reverse the ordering: schema identifiers (table/column/relationship names) now sort before keywords, since a name the user would have to look up is the highest-value completion and must stay visible (keywords are learned over time; the tok_identifier/tok_keyword colour split marks the boundary). This reverses the handoff-14 keywords-first call, now recorded in ADR-0022 Amendment 2. Tests: walker expected-set + completion-layer regressions for the trailing-optionals and the ordering; candidate_ordering.rs header invariant inverted; ~20 typing-surface snapshots re-baselined; a two-line hint box recorded as a deferred follow-up.
This commit is contained in:
@@ -698,6 +698,20 @@ fn walk_repeated(
|
||||
let mut cur = position;
|
||||
let mut count = 0_usize;
|
||||
let mut last_expected: Option<Vec<Expectation>> = None;
|
||||
// Trailing-optional expectations carried by the most recently
|
||||
// matched item — e.g. `asc`/`desc` after an ORDER BY sort
|
||||
// item, or a projection's `as` alias. Surfaced when the list
|
||||
// ends cleanly at an item boundary so completion still offers
|
||||
// the optional suffix the user could type next (handoff 31 —
|
||||
// the `desc` follow-up to F5). The separator itself is
|
||||
// deliberately NOT surfaced.
|
||||
let mut last_item_skipped: Vec<Expectation> = Vec::new();
|
||||
// Set when the loop stops because the separator did not match
|
||||
// at an item boundary (a clean end of list), as opposed to an
|
||||
// inner mismatch past an already-consumed separator. Only at a
|
||||
// clean boundary are the last item's trailing optionals valid
|
||||
// continuations at the cursor.
|
||||
let mut ended_at_item_boundary = false;
|
||||
loop {
|
||||
let saved_path_len = path.items.len();
|
||||
let saved_byte_len = per_byte.len();
|
||||
@@ -720,6 +734,7 @@ fn walk_repeated(
|
||||
NodeWalkResult::NoMatch { .. } => {
|
||||
path.items.truncate(sep_saved_path);
|
||||
per_byte.truncate(sep_saved_byte);
|
||||
ended_at_item_boundary = true;
|
||||
break;
|
||||
}
|
||||
other => return other,
|
||||
@@ -728,9 +743,10 @@ fn walk_repeated(
|
||||
walk_node(source, cur, inner, ctx, path, per_byte)
|
||||
};
|
||||
match result {
|
||||
NodeWalkResult::Matched { end, .. } => {
|
||||
NodeWalkResult::Matched { end, skipped } => {
|
||||
cur = end;
|
||||
count += 1;
|
||||
last_item_skipped = skipped;
|
||||
}
|
||||
NodeWalkResult::NoMatch { expected, position: inner_pos } => {
|
||||
// Mid-typing-the-next-item recovery: if the
|
||||
@@ -771,13 +787,18 @@ fn walk_repeated(
|
||||
expected: last_expected.unwrap_or_default(),
|
||||
};
|
||||
}
|
||||
// The "could continue with another inner" expectations
|
||||
// become this Repeated's `skipped` set so the caller's
|
||||
// expected-set surfaces them at completion time.
|
||||
NodeWalkResult::Matched {
|
||||
end: cur,
|
||||
skipped: last_expected.unwrap_or_default(),
|
||||
}
|
||||
// The "could continue" expectations become this Repeated's
|
||||
// `skipped` set so the caller's expected-set surfaces them at
|
||||
// completion time. When the list ended cleanly at an item
|
||||
// boundary, that is the last item's trailing optionals (e.g.
|
||||
// `asc`/`desc`); otherwise it is whatever the final inner
|
||||
// attempt expected.
|
||||
let skipped = if ended_at_item_boundary {
|
||||
last_item_skipped
|
||||
} else {
|
||||
last_expected.unwrap_or_default()
|
||||
};
|
||||
NodeWalkResult::Matched { end: cur, skipped }
|
||||
}
|
||||
|
||||
fn walk_bare_path(
|
||||
|
||||
@@ -4844,6 +4844,26 @@ mod order_by_expected_set_tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn order_by_after_sort_item_offers_direction() {
|
||||
// After a complete sort item (`order by Name`) the
|
||||
// sort-direction keywords are valid continuations.
|
||||
// walk_repeated used to discard the item's trailing
|
||||
// optionals, so completion offered neither.
|
||||
let words = expected_words("select Name from T order by Name ");
|
||||
assert!(words.contains(&"asc"), "expected `asc`; got {words:?}");
|
||||
assert!(words.contains(&"desc"), "expected `desc`; got {words:?}");
|
||||
// The separator is deliberately not surfaced (user choice).
|
||||
let full = expected_at_input_in_mode(
|
||||
"select Name from T order by Name ",
|
||||
Mode::Advanced,
|
||||
);
|
||||
assert!(
|
||||
!full.iter().any(|e| matches!(e, Expectation::Punct(','))),
|
||||
"`,` separator should not be surfaced; got {full:?}",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn order_by_still_offers_a_sort_item() {
|
||||
// Guard against over-correction: the legitimate sort-item
|
||||
|
||||
Reference in New Issue
Block a user