Matrix: pin natural candidate ordering

8 tests covering completion-candidate order: connective keywords in
reading order (`to`/`from`/`in` before `table`), and command-part
keywords before schema identifiers. The ordering already held via
declaration-order preservation + keywords-first sectioning in
candidates_at_cursor; nothing pinned it until now, so a future
grammar or sort change could silently break the hint panel's
left-to-right reading.
This commit is contained in:
claude@clouddev1
2026-05-15 21:29:54 +00:00
parent 50b78253d8
commit bcc5ad2f20
10 changed files with 610 additions and 0 deletions
+129
View File
@@ -0,0 +1,129 @@
//! Matrix coverage for completion-candidate *ordering*.
//!
//! The order candidates appear in is load-bearing for the hint
//! panel: it reads left-to-right, so the sequence must match
//! how the command is spoken. Two invariants the user called
//! out (handoff-14 ranker discussion):
//!
//! 1. Connective keywords appear in grammar-declaration / reading
//! order — `to` before `table` so `add column to table T`
//! reads correctly, never the jarring `table` / `to`.
//! 2. Command-part keywords appear before schema identifiers
//! (table / column names) — grammar parts are read before
//! the content that fills them.
//!
//! These hold today via declaration-order preservation +
//! keywords-first sectioning in `candidates_at_cursor`. Nothing
//! pinned them until now, so a future grammar/sort change could
//! silently break the reading order.
use crate::typing_surface::*;
/// Index of `needle` in the ordered completion candidate list,
/// or panic with the full list for diagnosis.
fn pos(a: &Assessment, needle: &str) -> usize {
let cands = completion_candidate_texts(a);
cands
.iter()
.position(|c| c == needle)
.unwrap_or_else(|| panic!("{needle:?} not in candidates: {cands:?}"))
}
/// Assert `first` appears before `second` in the candidate list.
fn assert_before(a: &Assessment, first: &str, second: &str) {
let pf = pos(a, first);
let ps = pos(a, second);
assert!(
pf < ps,
"expected {first:?} (idx {pf}) before {second:?} (idx {ps}): {:?}",
completion_candidate_texts(a),
);
}
// =========================================================
// Connective ordering: the `[connective] [table] <T>` shape
// shared by the DDL commands.
// =========================================================
#[test]
fn add_column_lists_to_before_table() {
let schema = schema_multi_table();
let a = assess_at_end("add column ", &schema);
assert_before(&a, "to", "table");
crate::snap!("add_column_connectives", a);
}
#[test]
fn drop_column_lists_from_before_table() {
let schema = schema_multi_table();
let a = assess_at_end("drop column ", &schema);
assert_before(&a, "from", "table");
crate::snap!("drop_column_connectives", a);
}
#[test]
fn rename_column_lists_in_before_table() {
let schema = schema_multi_table();
let a = assess_at_end("rename column ", &schema);
assert_before(&a, "in", "table");
crate::snap!("rename_column_connectives", a);
}
#[test]
fn change_column_lists_in_before_table() {
let schema = schema_multi_table();
let a = assess_at_end("change column ", &schema);
assert_before(&a, "in", "table");
crate::snap!("change_column_connectives", a);
}
// =========================================================
// Keywords-before-identifiers: at a position where both a
// connective keyword and schema table names are valid, the
// keyword comes first.
// =========================================================
#[test]
fn add_column_keyword_precedes_table_identifiers() {
let schema = schema_multi_table();
let a = assess_at_end("add column ", &schema);
// `to` / `table` are command parts; Customers / Orders are
// schema identifiers — every keyword precedes every ident.
assert_before(&a, "table", "Customers");
assert_before(&a, "to", "Customers");
assert_before(&a, "to", "Orders");
crate::snap!("add_column_keyword_then_idents", a);
}
#[test]
fn drop_column_keyword_precedes_table_identifiers() {
let schema = schema_multi_table();
let a = assess_at_end("drop column ", &schema);
assert_before(&a, "table", "Customers");
assert_before(&a, "from", "Orders");
crate::snap!("drop_column_keyword_then_idents", a);
}
#[test]
fn insert_into_table_keyword_precedes_nothing_when_only_idents() {
// Sanity: at `insert into ` only table identifiers are
// valid — no keyword to precede them, but the ordering must
// still be deterministic (alphabetical among idents).
let schema = schema_multi_table();
let a = assess_at_end("insert into ", &schema);
assert_before(&a, "Customers", "Orders");
crate::snap!("insert_into_idents_only", a);
}
// =========================================================
// After consuming the first connective, the second still
// surfaces ahead of identifiers.
// =========================================================
#[test]
fn add_column_after_to_lists_table_before_identifiers() {
let schema = schema_multi_table();
let a = assess_at_end("add column to ", &schema);
assert_before(&a, "table", "Customers");
crate::snap!("add_column_after_to", a);
}
+1
View File
@@ -34,6 +34,7 @@ pub mod drop_relationship;
pub mod add_relationship; pub mod add_relationship;
pub mod rename_change_column; pub mod rename_change_column;
pub mod app_commands; pub mod app_commands;
pub mod candidate_ordering;
// ========================================================= // =========================================================
// Canonical schema shapes (handoff §1 — CANONICAL_SCHEMA_SHAPES) // Canonical schema shapes (handoff §1 — CANONICAL_SCHEMA_SHAPES)
@@ -0,0 +1,55 @@
---
source: tests/typing_surface/candidate_ordering.rs
description: "input=\"add column to \" cursor=14"
expression: "& a"
---
Assessment {
input: "add column to ",
cursor: 14,
state: IncompleteAtEof,
hint: Some(
Candidates {
items: [
Candidate {
text: "table",
kind: Keyword,
},
Candidate {
text: "Customers",
kind: Identifier,
},
Candidate {
text: "Orders",
kind: Identifier,
},
],
selected: None,
},
),
completion: Some(
Completion {
replaced_range: (
14,
14,
),
partial_prefix: "",
candidates: [
Candidate {
text: "table",
kind: Keyword,
},
Candidate {
text: "Customers",
kind: Identifier,
},
Candidate {
text: "Orders",
kind: Identifier,
},
],
},
),
parse_result: Err(
"Invalid(at_eof)",
),
}
@@ -0,0 +1,63 @@
---
source: tests/typing_surface/candidate_ordering.rs
description: "input=\"add column \" cursor=11"
expression: "& a"
---
Assessment {
input: "add column ",
cursor: 11,
state: IncompleteAtEof,
hint: Some(
Candidates {
items: [
Candidate {
text: "to",
kind: Keyword,
},
Candidate {
text: "table",
kind: Keyword,
},
Candidate {
text: "Customers",
kind: Identifier,
},
Candidate {
text: "Orders",
kind: Identifier,
},
],
selected: None,
},
),
completion: Some(
Completion {
replaced_range: (
11,
11,
),
partial_prefix: "",
candidates: [
Candidate {
text: "to",
kind: Keyword,
},
Candidate {
text: "table",
kind: Keyword,
},
Candidate {
text: "Customers",
kind: Identifier,
},
Candidate {
text: "Orders",
kind: Identifier,
},
],
},
),
parse_result: Err(
"Invalid(at_eof)",
),
}
@@ -0,0 +1,63 @@
---
source: tests/typing_surface/candidate_ordering.rs
description: "input=\"add column \" cursor=11"
expression: "& a"
---
Assessment {
input: "add column ",
cursor: 11,
state: IncompleteAtEof,
hint: Some(
Candidates {
items: [
Candidate {
text: "to",
kind: Keyword,
},
Candidate {
text: "table",
kind: Keyword,
},
Candidate {
text: "Customers",
kind: Identifier,
},
Candidate {
text: "Orders",
kind: Identifier,
},
],
selected: None,
},
),
completion: Some(
Completion {
replaced_range: (
11,
11,
),
partial_prefix: "",
candidates: [
Candidate {
text: "to",
kind: Keyword,
},
Candidate {
text: "table",
kind: Keyword,
},
Candidate {
text: "Customers",
kind: Identifier,
},
Candidate {
text: "Orders",
kind: Identifier,
},
],
},
),
parse_result: Err(
"Invalid(at_eof)",
),
}
@@ -0,0 +1,63 @@
---
source: tests/typing_surface/candidate_ordering.rs
description: "input=\"change column \" cursor=14"
expression: "& a"
---
Assessment {
input: "change column ",
cursor: 14,
state: IncompleteAtEof,
hint: Some(
Candidates {
items: [
Candidate {
text: "in",
kind: Keyword,
},
Candidate {
text: "table",
kind: Keyword,
},
Candidate {
text: "Customers",
kind: Identifier,
},
Candidate {
text: "Orders",
kind: Identifier,
},
],
selected: None,
},
),
completion: Some(
Completion {
replaced_range: (
14,
14,
),
partial_prefix: "",
candidates: [
Candidate {
text: "in",
kind: Keyword,
},
Candidate {
text: "table",
kind: Keyword,
},
Candidate {
text: "Customers",
kind: Identifier,
},
Candidate {
text: "Orders",
kind: Identifier,
},
],
},
),
parse_result: Err(
"Invalid(at_eof)",
),
}
@@ -0,0 +1,63 @@
---
source: tests/typing_surface/candidate_ordering.rs
description: "input=\"drop column \" cursor=12"
expression: "& a"
---
Assessment {
input: "drop column ",
cursor: 12,
state: IncompleteAtEof,
hint: Some(
Candidates {
items: [
Candidate {
text: "from",
kind: Keyword,
},
Candidate {
text: "table",
kind: Keyword,
},
Candidate {
text: "Customers",
kind: Identifier,
},
Candidate {
text: "Orders",
kind: Identifier,
},
],
selected: None,
},
),
completion: Some(
Completion {
replaced_range: (
12,
12,
),
partial_prefix: "",
candidates: [
Candidate {
text: "from",
kind: Keyword,
},
Candidate {
text: "table",
kind: Keyword,
},
Candidate {
text: "Customers",
kind: Identifier,
},
Candidate {
text: "Orders",
kind: Identifier,
},
],
},
),
parse_result: Err(
"Invalid(at_eof)",
),
}
@@ -0,0 +1,63 @@
---
source: tests/typing_surface/candidate_ordering.rs
description: "input=\"drop column \" cursor=12"
expression: "& a"
---
Assessment {
input: "drop column ",
cursor: 12,
state: IncompleteAtEof,
hint: Some(
Candidates {
items: [
Candidate {
text: "from",
kind: Keyword,
},
Candidate {
text: "table",
kind: Keyword,
},
Candidate {
text: "Customers",
kind: Identifier,
},
Candidate {
text: "Orders",
kind: Identifier,
},
],
selected: None,
},
),
completion: Some(
Completion {
replaced_range: (
12,
12,
),
partial_prefix: "",
candidates: [
Candidate {
text: "from",
kind: Keyword,
},
Candidate {
text: "table",
kind: Keyword,
},
Candidate {
text: "Customers",
kind: Identifier,
},
Candidate {
text: "Orders",
kind: Identifier,
},
],
},
),
parse_result: Err(
"Invalid(at_eof)",
),
}
@@ -0,0 +1,47 @@
---
source: tests/typing_surface/candidate_ordering.rs
description: "input=\"insert into \" cursor=12"
expression: "& a"
---
Assessment {
input: "insert into ",
cursor: 12,
state: IncompleteAtEof,
hint: Some(
Candidates {
items: [
Candidate {
text: "Customers",
kind: Identifier,
},
Candidate {
text: "Orders",
kind: Identifier,
},
],
selected: None,
},
),
completion: Some(
Completion {
replaced_range: (
12,
12,
),
partial_prefix: "",
candidates: [
Candidate {
text: "Customers",
kind: Identifier,
},
Candidate {
text: "Orders",
kind: Identifier,
},
],
},
),
parse_result: Err(
"Invalid(at_eof)",
),
}
@@ -0,0 +1,63 @@
---
source: tests/typing_surface/candidate_ordering.rs
description: "input=\"rename column \" cursor=14"
expression: "& a"
---
Assessment {
input: "rename column ",
cursor: 14,
state: IncompleteAtEof,
hint: Some(
Candidates {
items: [
Candidate {
text: "in",
kind: Keyword,
},
Candidate {
text: "table",
kind: Keyword,
},
Candidate {
text: "Customers",
kind: Identifier,
},
Candidate {
text: "Orders",
kind: Identifier,
},
],
selected: None,
},
),
completion: Some(
Completion {
replaced_range: (
14,
14,
),
partial_prefix: "",
candidates: [
Candidate {
text: "in",
kind: Keyword,
},
Candidate {
text: "table",
kind: Keyword,
},
Candidate {
text: "Customers",
kind: Identifier,
},
Candidate {
text: "Orders",
kind: Identifier,
},
],
},
),
parse_result: Err(
"Invalid(at_eof)",
),
}