ADR-0022 follow-up r4: column-type completion
Round-4 user finding: typing `(de` at a column-type slot
showed the parser's "unknown type 'de'" error and Tab did
nothing — completion was blind to the type vocabulary
entirely.
Root cause: type names are NOT in the Keyword enum (ADR-0020
§2 — they remain identifiers, validated by Type::from_str),
so the keyword-iter path in candidates_at_cursor missed
them. The schema-identifier path also missed them (they're
not in the schema cache).
Fix: when the parser's expected-set contains the `"type"`
label (from `ident_inner().labelled("type")` inside
`type_keyword`), produce candidates from `Type::all()`
filtered by the partial prefix. Centralised as
`TYPE_SLOT_LABEL` constant so the parser and the completion
engine agree on the magic string.
Candidates appear in `Type::all()` declaration order
(text/int/real/decimal/bool/date/datetime/blob/serial/
shortid) — matching ADR-0005's pedagogical grouping. Coloured
as Keyword (purple) since type names are closed-set
grammar, not user content.
Verified end-to-end:
- `(de` → ["decimal"] (single match → Tab inserts with space)
- `(da` → ["date", "datetime"] (multi → cycles)
- `(sh` → ["shortid"]
- `(` → all 10 types in declaration order
- `(var` → [] (no Tab candidates; parser custom error fires on submit)
Tests: 760 passing, 0 failing, 1 ignored (755 baseline +5
new type-slot cases). Clippy clean.
This commit is contained in:
+92
-2
@@ -16,9 +16,16 @@
|
||||
|
||||
use crate::dsl::ident_slot::IdentSlot;
|
||||
use crate::dsl::keyword::Keyword;
|
||||
use crate::dsl::types::Type;
|
||||
use crate::dsl::usage;
|
||||
use crate::dsl::{ParseError, parse_command};
|
||||
|
||||
/// Label emitted by `type_keyword` (in `dsl::parser`) when it
|
||||
/// expects a column-type token. Matches the `.labelled("type")`
|
||||
/// applied on the inner `select_ref!`. Centralised here so the
|
||||
/// completion engine and the parser agree on the magic string.
|
||||
const TYPE_SLOT_LABEL: &str = "type";
|
||||
|
||||
/// Per-project schema lookup cache (ADR-0022 §9).
|
||||
///
|
||||
/// Held by `App::schema_cache` and consulted by the completion
|
||||
@@ -139,6 +146,24 @@ pub fn candidates_at_cursor(
|
||||
let mut seen_kw = std::collections::HashSet::new();
|
||||
keywords.retain(|k| seen_kw.insert(k.clone()));
|
||||
|
||||
// Source 1.5: type-name candidates when the parser expects
|
||||
// a column-type slot. Type names live outside the Keyword
|
||||
// enum (ADR-0020 §2 — type names stay as identifiers,
|
||||
// validated by Type::from_str), so they need their own
|
||||
// completion path. Preserve `Type::all()` declaration
|
||||
// order — that's text/int/real/decimal/bool/date/datetime/
|
||||
// blob/serial/shortid, the order a learner reads them in
|
||||
// ADR-0005.
|
||||
let type_names: Vec<String> = if expected.iter().any(|s| s == TYPE_SLOT_LABEL) {
|
||||
Type::all()
|
||||
.iter()
|
||||
.map(|t| t.keyword().to_string())
|
||||
.filter(|s| matches_prefix(s))
|
||||
.collect()
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
|
||||
// Source 2: schema identifiers — accumulated across every
|
||||
// matching known-set slot. `NewName` slots return `&[]`.
|
||||
let mut identifiers: Vec<String> = expected
|
||||
@@ -157,12 +182,18 @@ pub fn candidates_at_cursor(
|
||||
identifiers.retain(|name| !keywords.contains(name));
|
||||
|
||||
// Keywords first (grammar parts read before content),
|
||||
// identifiers after. Within each group, alphabetical.
|
||||
let mut candidates: Vec<Candidate> = Vec::with_capacity(keywords.len() + identifiers.len());
|
||||
// then type names (closed-set grammar — coloured as
|
||||
// keywords), then schema identifiers.
|
||||
let mut candidates: Vec<Candidate> =
|
||||
Vec::with_capacity(keywords.len() + type_names.len() + identifiers.len());
|
||||
candidates.extend(keywords.into_iter().map(|text| Candidate {
|
||||
text,
|
||||
kind: CandidateKind::Keyword,
|
||||
}));
|
||||
candidates.extend(type_names.into_iter().map(|text| Candidate {
|
||||
text,
|
||||
kind: CandidateKind::Keyword,
|
||||
}));
|
||||
candidates.extend(identifiers.into_iter().map(|text| Candidate {
|
||||
text,
|
||||
kind: CandidateKind::Identifier,
|
||||
@@ -529,6 +560,65 @@ mod tests {
|
||||
assert_eq!(comp.partial_prefix, "");
|
||||
}
|
||||
|
||||
// ---- type-name completion (round-3 follow-up #2) ----
|
||||
|
||||
#[test]
|
||||
fn type_slot_offers_full_type_vocabulary_when_partial_empty() {
|
||||
// After `add column to T: Name (` the parser expects
|
||||
// a column type. With no partial typed, all ten types
|
||||
// from `Type::all()` are offered in declaration order.
|
||||
let cs = cands("add column to T: Name (", 23);
|
||||
assert_eq!(
|
||||
cs,
|
||||
vec![
|
||||
"text".to_string(),
|
||||
"int".to_string(),
|
||||
"real".to_string(),
|
||||
"decimal".to_string(),
|
||||
"bool".to_string(),
|
||||
"date".to_string(),
|
||||
"datetime".to_string(),
|
||||
"blob".to_string(),
|
||||
"serial".to_string(),
|
||||
"shortid".to_string(),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn type_slot_narrows_to_prefix_matches() {
|
||||
// `de` matches only `decimal` (despite the surface
|
||||
// resemblance to `date`/`datetime`, those start with
|
||||
// `da`). The user-reported case from real testing
|
||||
// round 4.
|
||||
let cs = cands("add column to T: Name (de", 25);
|
||||
assert_eq!(cs, vec!["decimal".to_string()]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn type_slot_narrows_to_da_for_date_family() {
|
||||
// `da` correctly returns date and datetime — in
|
||||
// Type::all() declaration order (date before datetime,
|
||||
// matching ADR-0005's grouping).
|
||||
let cs = cands("add column to T: Name (da", 25);
|
||||
assert_eq!(cs, vec!["date".to_string(), "datetime".to_string()]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn type_slot_single_match_for_unique_prefix() {
|
||||
// `sh` uniquely identifies `shortid`.
|
||||
let cs = cands("add column to T: Name (sh", 25);
|
||||
assert_eq!(cs, vec!["shortid".to_string()]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn type_slot_no_match_for_invalid_prefix() {
|
||||
// `var` matches nothing — Tab is a no-op; the parser's
|
||||
// unknown-type custom error still fires on submit.
|
||||
let cs = cands("add column to T: Name (var", 26);
|
||||
assert!(cs.is_empty(), "got {cs:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn keywords_come_before_identifiers_in_grammar_order() {
|
||||
// "add column " has both keyword candidates and
|
||||
|
||||
Reference in New Issue
Block a user