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:
+101
-25
@@ -158,8 +158,8 @@ pub struct Completion {
|
||||
/// Partial prefix the user has typed at the cursor. Empty
|
||||
/// when the cursor is at a token boundary.
|
||||
pub partial_prefix: String,
|
||||
/// Fitting candidates, ordered keywords-first then
|
||||
/// identifiers, alphabetised within each group, deduplicated.
|
||||
/// Fitting candidates, ordered schema-identifiers-first then
|
||||
/// keywords, alphabetised within each group, deduplicated.
|
||||
pub candidates: Vec<Candidate>,
|
||||
}
|
||||
|
||||
@@ -585,19 +585,26 @@ pub fn candidates_at_cursor_with_in_mode(
|
||||
// ambiguity in the live render.
|
||||
identifiers.retain(|name| !keywords.contains(name));
|
||||
|
||||
// Keywords first (grammar parts read before content),
|
||||
// then type names (closed-set grammar — coloured as
|
||||
// keywords), then composite literals (`1:n`, …), then
|
||||
// branching punct (`(` opening a sub-shape), then flags
|
||||
// (own colour), then schema identifiers.
|
||||
// Schema identifiers first: a column / table name the user
|
||||
// would otherwise have to look up is the highest-value
|
||||
// completion (valuable to experts, not just learners, who
|
||||
// come to know the keywords over time). Keywords and the
|
||||
// other closed-set grammar parts follow: keywords, then type
|
||||
// names (closed-set grammar — coloured as keywords), then
|
||||
// composite literals (`1:n`, …), then branching punct (`(`
|
||||
// opening a sub-shape), then flags (own colour).
|
||||
let mut candidates: Vec<Candidate> = Vec::with_capacity(
|
||||
keywords.len()
|
||||
identifiers.len()
|
||||
+ keywords.len()
|
||||
+ type_names.len()
|
||||
+ composites.len()
|
||||
+ punct_candidates.len()
|
||||
+ flags.len()
|
||||
+ identifiers.len(),
|
||||
+ flags.len(),
|
||||
);
|
||||
candidates.extend(identifiers.into_iter().map(|text| Candidate {
|
||||
text,
|
||||
kind: CandidateKind::Identifier,
|
||||
}));
|
||||
candidates.extend(keywords.into_iter().map(|text| Candidate {
|
||||
text,
|
||||
kind: CandidateKind::Keyword,
|
||||
@@ -618,10 +625,6 @@ pub fn candidates_at_cursor_with_in_mode(
|
||||
text,
|
||||
kind: CandidateKind::Flag,
|
||||
}));
|
||||
candidates.extend(identifiers.into_iter().map(|text| Candidate {
|
||||
text,
|
||||
kind: CandidateKind::Identifier,
|
||||
}));
|
||||
|
||||
if candidates.is_empty() {
|
||||
return None;
|
||||
@@ -1506,6 +1509,78 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn order_by_after_sort_item_offers_direction() {
|
||||
use crate::dsl::types::Type;
|
||||
// walk_repeated trailing-optional fix: after a complete
|
||||
// sort item the direction keywords surface as
|
||||
// continuations (previously discarded at the Repeated
|
||||
// boundary, so completion offered neither).
|
||||
let cache = schema_with_table(
|
||||
"Things",
|
||||
&[("Name", Type::Text), ("Qty", Type::Int)],
|
||||
);
|
||||
let input = "select Name from Things order by Name ";
|
||||
let cs = cands_with(input, input.len(), &cache);
|
||||
assert!(cs.contains(&"asc".to_string()), "got {cs:?}");
|
||||
assert!(cs.contains(&"desc".to_string()), "got {cs:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn projection_after_item_offers_alias_keyword() {
|
||||
use crate::dsl::types::Type;
|
||||
// walk_repeated trailing-optional fix: after a complete
|
||||
// projection item the `as` alias keyword surfaces.
|
||||
let cache = schema_with_table(
|
||||
"Things",
|
||||
&[("Name", Type::Text), ("Qty", Type::Int)],
|
||||
);
|
||||
let input = "select Name ";
|
||||
let cs = cands_with(input, input.len(), &cache);
|
||||
assert!(cs.contains(&"as".to_string()), "got {cs:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_table_after_column_spec_offers_constraints() {
|
||||
// walk_repeated trailing-optional fix: after a complete
|
||||
// column spec the optional column constraints surface as
|
||||
// continuations (was a bare "Submit with Enter" prose).
|
||||
let input = "create table Customers with pk Code(text) ";
|
||||
let cs = cands_with(input, input.len(), &SchemaCache::default());
|
||||
for kw in ["not", "unique", "default", "check"] {
|
||||
assert!(
|
||||
cs.contains(&kw.to_string()),
|
||||
"expected column-constraint `{kw}`; got {cs:?}",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn identifiers_precede_keywords_at_expression_position() {
|
||||
use crate::dsl::types::Type;
|
||||
// ADR-0022 Amendment 2: at an expression position offering
|
||||
// both column names and keywords, every column precedes
|
||||
// every keyword so the names stay visible by default.
|
||||
let cache = schema_with_table(
|
||||
"Things",
|
||||
&[("Name", Type::Text), ("Qty", Type::Int)],
|
||||
);
|
||||
let input = "select * from Things where ";
|
||||
let cs = cands_with(input, input.len(), &cache);
|
||||
let pos = |needle: &str| {
|
||||
cs.iter().position(|c| c == needle).unwrap_or_else(|| {
|
||||
panic!("{needle:?} not in candidates: {cs:?}")
|
||||
})
|
||||
};
|
||||
// Both columns come before any expression-start keyword.
|
||||
let last_ident = pos("Name").max(pos("Qty"));
|
||||
let first_kw = pos("not").min(pos("exists"));
|
||||
assert!(
|
||||
last_ident < first_kw,
|
||||
"identifiers must precede keywords; got {cs:?}",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn update_where_offers_only_current_table_columns() {
|
||||
use crate::dsl::types::Type;
|
||||
@@ -1681,15 +1756,16 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn keywords_come_before_identifiers_in_grammar_order() {
|
||||
// "add column " has both keyword candidates and
|
||||
// schema-identifier candidates. Per the user's stage-8
|
||||
// feedback round 2: keywords first in *grammar order*
|
||||
// (so `to` before `table` because the canonical shape
|
||||
// is `add column [to] [table] <Table>:…`), identifiers
|
||||
// after, alphabetised. The grammar order falls out of
|
||||
// chumsky's source-order expected-set traversal — we
|
||||
// preserve that order through `describe_expected`.
|
||||
fn identifiers_come_before_keywords_in_grammar_order() {
|
||||
// "add column " has both schema-identifier candidates and
|
||||
// keyword candidates. Per ADR-0022 Amendment 2: schema
|
||||
// identifiers first (alphabetised) so the names the user
|
||||
// would have to look up stay visible, then keywords in
|
||||
// *grammar order* (`to` before `table` because the
|
||||
// canonical shape is `add column [to] [table] <Table>:…`).
|
||||
// The grammar order falls out of the walker's source-order
|
||||
// expected-set traversal — we preserve that order through
|
||||
// `describe_expected`.
|
||||
let cache = SchemaCache {
|
||||
tables: vec!["Customers".to_string(), "Orders".to_string()],
|
||||
..SchemaCache::default()
|
||||
@@ -1698,10 +1774,10 @@ mod tests {
|
||||
assert_eq!(
|
||||
kinds,
|
||||
vec![
|
||||
("to".to_string(), CandidateKind::Keyword),
|
||||
("table".to_string(), CandidateKind::Keyword),
|
||||
("Customers".to_string(), CandidateKind::Identifier),
|
||||
("Orders".to_string(), CandidateKind::Identifier),
|
||||
("to".to_string(), CandidateKind::Keyword),
|
||||
("table".to_string(), CandidateKind::Keyword),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user