diff --git a/tests/typing_surface/candidate_ordering.rs b/tests/typing_surface/candidate_ordering.rs new file mode 100644 index 0000000..468ee44 --- /dev/null +++ b/tests/typing_surface/candidate_ordering.rs @@ -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] ` 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); +} diff --git a/tests/typing_surface/mod.rs b/tests/typing_surface/mod.rs index 751906b..931396c 100644 --- a/tests/typing_surface/mod.rs +++ b/tests/typing_surface/mod.rs @@ -34,6 +34,7 @@ pub mod drop_relationship; pub mod add_relationship; pub mod rename_change_column; pub mod app_commands; +pub mod candidate_ordering; // ========================================================= // Canonical schema shapes (handoff §1 — CANONICAL_SCHEMA_SHAPES) diff --git a/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__candidate_ordering__add_column_after_to_lists_table_before_identifiers@add_column_after_to.snap b/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__candidate_ordering__add_column_after_to_lists_table_before_identifiers@add_column_after_to.snap new file mode 100644 index 0000000..cc84197 --- /dev/null +++ b/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__candidate_ordering__add_column_after_to_lists_table_before_identifiers@add_column_after_to.snap @@ -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)", + ), +} diff --git a/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__candidate_ordering__add_column_keyword_precedes_table_identifiers@add_column_keyword_then_idents.snap b/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__candidate_ordering__add_column_keyword_precedes_table_identifiers@add_column_keyword_then_idents.snap new file mode 100644 index 0000000..1be0c7a --- /dev/null +++ b/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__candidate_ordering__add_column_keyword_precedes_table_identifiers@add_column_keyword_then_idents.snap @@ -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)", + ), +} diff --git a/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__candidate_ordering__add_column_lists_to_before_table@add_column_connectives.snap b/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__candidate_ordering__add_column_lists_to_before_table@add_column_connectives.snap new file mode 100644 index 0000000..1be0c7a --- /dev/null +++ b/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__candidate_ordering__add_column_lists_to_before_table@add_column_connectives.snap @@ -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)", + ), +} diff --git a/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__candidate_ordering__change_column_lists_in_before_table@change_column_connectives.snap b/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__candidate_ordering__change_column_lists_in_before_table@change_column_connectives.snap new file mode 100644 index 0000000..58d4f6c --- /dev/null +++ b/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__candidate_ordering__change_column_lists_in_before_table@change_column_connectives.snap @@ -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)", + ), +} diff --git a/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__candidate_ordering__drop_column_keyword_precedes_table_identifiers@drop_column_keyword_then_idents.snap b/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__candidate_ordering__drop_column_keyword_precedes_table_identifiers@drop_column_keyword_then_idents.snap new file mode 100644 index 0000000..f2fdeca --- /dev/null +++ b/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__candidate_ordering__drop_column_keyword_precedes_table_identifiers@drop_column_keyword_then_idents.snap @@ -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)", + ), +} diff --git a/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__candidate_ordering__drop_column_lists_from_before_table@drop_column_connectives.snap b/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__candidate_ordering__drop_column_lists_from_before_table@drop_column_connectives.snap new file mode 100644 index 0000000..f2fdeca --- /dev/null +++ b/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__candidate_ordering__drop_column_lists_from_before_table@drop_column_connectives.snap @@ -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)", + ), +} diff --git a/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__candidate_ordering__insert_into_table_keyword_precedes_nothing_when_only_idents@insert_into_idents_only.snap b/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__candidate_ordering__insert_into_table_keyword_precedes_nothing_when_only_idents@insert_into_idents_only.snap new file mode 100644 index 0000000..3980b0c --- /dev/null +++ b/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__candidate_ordering__insert_into_table_keyword_precedes_nothing_when_only_idents@insert_into_idents_only.snap @@ -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)", + ), +} diff --git a/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__candidate_ordering__rename_column_lists_in_before_table@rename_column_connectives.snap b/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__candidate_ordering__rename_column_lists_in_before_table@rename_column_connectives.snap new file mode 100644 index 0000000..5889200 --- /dev/null +++ b/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__candidate_ordering__rename_column_lists_in_before_table@rename_column_connectives.snap @@ -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)", + ), +}