walker: F5 — drop preceding-clause keywords from committed-child Incomplete sets

walk_seq's Incomplete arm unconditionally merged the accumulated
skipped-Optional expectations (pending_skipped) into the child's
expected set. When a child committed terminals before going
Incomplete (e.g. `order by` consumed, now awaiting a sort item),
this leaked ~13 clause keywords from clauses positioned *before*
the committed child — WHERE/GROUP BY/HAVING, the FROM's JOIN
options, set-ops — into the ORDER BY completion list, shoving the
actual columns off-screen.

Merge pending_skipped only when the Incomplete-producing child
consumed nothing (path length unchanged): the cursor still sits at
the optional boundary, so those optionals are genuine alternatives.
A committed child means the cursor is past them.

Tests: walker expected-set guard (+ over-correction guard) and a
full-stack completion-layer regression test.
This commit is contained in:
claude@clouddev1
2026-05-21 20:52:20 +00:00
parent 5b88ccd2c3
commit 43c49f4d1b
3 changed files with 111 additions and 3 deletions
+18 -3
View File
@@ -850,6 +850,7 @@ fn walk_seq(
// engine see optional connectives that haven't been typed.
let mut pending_skipped: Vec<Expectation> = Vec::new();
for child in children {
let path_before = path.items.len();
match walk_node(source, cur, child, ctx, path, per_byte) {
NodeWalkResult::Matched { end, skipped } => {
if end == cur {
@@ -900,9 +901,23 @@ fn walk_seq(
position,
mut expected,
} => {
for e in std::mem::take(&mut pending_skipped) {
if !expected.contains(&e) {
expected.push(e);
// Only merge the skipped-Optional expectations when
// the Incomplete-producing child consumed nothing
// (path didn't grow): the cursor still sits at the
// optional boundary, so those optionals are genuine
// alternatives. If the child committed terminals
// (e.g. `order by` consumed, now awaiting a sort
// item) the cursor has moved *past* the skipped
// optionals — clauses positioned before this child
// are no longer valid continuations, so dropping
// `pending_skipped` keeps them out of the expected
// set (handoff 30 §3.3, F5).
let child_consumed = path.items.len() > path_before;
if !child_consumed {
for e in std::mem::take(&mut pending_skipped) {
if !expected.contains(&e) {
expected.push(e);
}
}
}
return NodeWalkResult::Incomplete { position, expected };