feat: give column data types a dedicated syntax-highlight colour
Both Node::Ident and Word carried a highlight_override field, and both were dead — the walker driver discarded the Ident's and walk_word hardcoded Keyword. So column types (int, serial, …) rendered identically to table/column names. Wire both overrides through, and add a dedicated HighlightClass::Type with its own theme colour (tok_type), distinct from keyword-purple and identifier-teal. The three type Ident slots opt in, so canonical types and the advanced-mode single-word SQL aliases (float, varchar, …) render as types; the two-word `double precision` alias opts in via a new Word::type_keyword constructor. ADR-0022 Amendment 4.
This commit is contained in:
@@ -587,6 +587,61 @@ guard above, and three re-baselined `typing_surface` snapshots
|
|||||||
(`form_a_in_progress_one_value`, `form_b_too_few_values`,
|
(`form_a_in_progress_one_value`, `form_b_too_few_values`,
|
||||||
`form_c_wrong_count`).
|
`form_c_wrong_count`).
|
||||||
|
|
||||||
|
## Amendment 4 — Column types get a dedicated highlight class (2026-05-29)
|
||||||
|
|
||||||
|
§3 introduced **seven** token-class colour fields and the matching
|
||||||
|
`HighlightClass` enum. Column data-type keywords (`int`, `serial`,
|
||||||
|
`text`, …) had no class of their own: the walker driver hardcoded
|
||||||
|
`HighlightClass::Identifier` for every matched `Node::Ident`, so a
|
||||||
|
type rendered identical to a table/column name (clause keywords were
|
||||||
|
already distinct, in `tok_keyword`). A user-reported bug (issue #8)
|
||||||
|
noted that in `create table Orders (count int, id serial PRIMARY KEY)`
|
||||||
|
the identifiers and the type keywords were indistinguishably teal.
|
||||||
|
|
||||||
|
Both terminal kinds already carried a `highlight_override:
|
||||||
|
Option<HighlightClass>` field — `Node::Ident` and the `Word` struct
|
||||||
|
alike — but **both were dead**: the driver destructured the Ident's to
|
||||||
|
`_`, and `walk_word` hardcoded `Keyword`, neither ever consulting the
|
||||||
|
field. This amendment wires both through.
|
||||||
|
|
||||||
|
**Change:**
|
||||||
|
|
||||||
|
1. **New class.** `HighlightClass::Type` (the eighth variant) and a
|
||||||
|
matching eighth `Theme` field `tok_type`, populated in both
|
||||||
|
`dark()` and `light()` with a tone deliberately distinct from both
|
||||||
|
`tok_keyword` and `tok_identifier` (a pink / deep-magenta in the
|
||||||
|
red-purple range — the only free slot in the existing palette).
|
||||||
|
`highlight_class_color` maps the new variant.
|
||||||
|
2. **Overrides are now live.** `walk_ident` emits
|
||||||
|
`override.unwrap_or(Identifier)` and `walk_word` emits
|
||||||
|
`override.unwrap_or(Keyword)` for the matched byte range. Every
|
||||||
|
slot that leaves the field `None` is unchanged.
|
||||||
|
3. **Type slots opt in.** The three `IdentSource::Types` slots —
|
||||||
|
`shared::TYPE_SLOT` (`add column` / `change column`), the inline
|
||||||
|
create-table column-type Ident (`ddl.rs`), and `sql_create_table::
|
||||||
|
SQL_TYPE_NAME` (advanced) — set `highlight_override: Some(Type)`.
|
||||||
|
In advanced mode, every single-word SQL type-name *alias*
|
||||||
|
(`float`, `varchar`, `integer`, … — ADR-0035 §3) flows through
|
||||||
|
`SQL_TYPE_NAME` and so is type-coloured for free.
|
||||||
|
4. **`double precision` too.** The lone two-word alias (ADR-0035 §3)
|
||||||
|
is matched as keyword tokens, not an `IdentSource::Types` Ident, so
|
||||||
|
it cannot ride rule 3. Its `double` / `precision` grammar nodes use
|
||||||
|
the new `Word::type_keyword` constructor (`highlight_override:
|
||||||
|
Some(Type)`) so the spelling renders type-coloured like its
|
||||||
|
single-word synonyms `float` / `real`.
|
||||||
|
|
||||||
|
**Pedagogy:** a dedicated colour (over the lighter option of reusing
|
||||||
|
`tok_keyword`) lets a learner tell *"this is a type"* from a clause
|
||||||
|
keyword and from a name they invented — three distinct roles, three
|
||||||
|
distinct colours.
|
||||||
|
|
||||||
|
**Coverage:** `dsl_type_keyword_classified_as_type`,
|
||||||
|
`advanced_type_keywords_classified_as_type` (the ticket's exact
|
||||||
|
example), `advanced_double_precision_classified_as_type`,
|
||||||
|
`type_colour_is_distinct_from_keyword_and_identifier`, and the
|
||||||
|
extended theme mapping/contrast tests. Text snapshots are colour-blind
|
||||||
|
(`render_to_string` strips style), so none churned.
|
||||||
|
|
||||||
## Out of scope
|
## Out of scope
|
||||||
|
|
||||||
Deliberately deferred to keep this ADR shippable as a single
|
Deliberately deferred to keep this ADR shippable as a single
|
||||||
|
|||||||
+1
-1
@@ -27,7 +27,7 @@ This directory contains the project's ADRs, recorded per
|
|||||||
- [ADR-0019 — Friendly error layer (H1) and i18n message catalog](0019-friendly-error-layer-and-i18n.md)
|
- [ADR-0019 — Friendly error layer (H1) and i18n message catalog](0019-friendly-error-layer-and-i18n.md)
|
||||||
- [ADR-0020 — Tokenization layer for the DSL parser](0020-tokenization-layer-for-the-dsl-parser.md)
|
- [ADR-0020 — Tokenization layer for the DSL parser](0020-tokenization-layer-for-the-dsl-parser.md)
|
||||||
- [ADR-0021 — Parser-as-source-of-truth for H1a (per-command usage in parse errors)](0021-parser-as-source-of-truth-for-h1a.md)
|
- [ADR-0021 — Parser-as-source-of-truth for H1a (per-command usage in parse errors)](0021-parser-as-source-of-truth-for-h1a.md)
|
||||||
- [ADR-0022 — Ambient typing assistance: colour, hint panel, completion (I3 + I4)](0022-ambient-typing-assistance.md) — **Amendment 1 supersedes §12's simple-mode-only carve-out**: the unified mode-aware walker (ADR-0030/0031/0032) now speaks SQL, so advanced-mode ambient assistance is re-enabled. `ambient_hint_in_mode` + `hint_resolution_at_input_in_mode` + `expected_for_hint_snapshot` thread `Mode`; `render_hint_panel` calls ambient for all modes (no more advanced-mode `None`); the one-shot `:` sigil is stripped before the ambient walk. Fixes a live bug where advanced-mode SQL hinting/completion-preview were dead despite Phase 2 marking them green (validated at the engine layer, not the UI). Simple-mode gating, highlighting, and the §13 performance posture are unchanged; covered by an app-level render test plus ambient-layer regression locks; **Amendment 2 reverses the handoff-14 keywords-first candidate ordering** — schema identifiers (table/column/relationship names) now sort *before* keywords so a name the user would have to look up stays visible in the single-row, window-scrolled candidate line (keywords are learned over time; the `tok_identifier`/`tok_keyword` colour split marks the boundary); shipped with a `walk_repeated` fix that surfaces a list item's trailing optionals at a clean boundary (`order by Name ` → `asc`/`desc`, `select Name ` → `as`, `create table … Code(text) ` → `not`/`unique`/`default`/`check`; the `,` separator deliberately not surfaced); records a deferred two-line hint box for growing lists; **Amendment 3 makes the ambient-hint fallback rung schema-aware** — Amendment 1's bottom-rung `parse_command_in_mode` was schemaless while every earlier rung was not, so between-values insert hints pointed at `)` (type-blind close) instead of `,` and wrong-arity closed tuples read "submit with Enter" for an input the schema-aware parse rejects (issue #2); now uses `parse_command_with_schema_in_mode`, no extra walk, with the friendly arity diagnostic still winning at its higher rung
|
- [ADR-0022 — Ambient typing assistance: colour, hint panel, completion (I3 + I4)](0022-ambient-typing-assistance.md) — **Amendment 1 supersedes §12's simple-mode-only carve-out**: the unified mode-aware walker (ADR-0030/0031/0032) now speaks SQL, so advanced-mode ambient assistance is re-enabled. `ambient_hint_in_mode` + `hint_resolution_at_input_in_mode` + `expected_for_hint_snapshot` thread `Mode`; `render_hint_panel` calls ambient for all modes (no more advanced-mode `None`); the one-shot `:` sigil is stripped before the ambient walk. Fixes a live bug where advanced-mode SQL hinting/completion-preview were dead despite Phase 2 marking them green (validated at the engine layer, not the UI). Simple-mode gating, highlighting, and the §13 performance posture are unchanged; covered by an app-level render test plus ambient-layer regression locks; **Amendment 2 reverses the handoff-14 keywords-first candidate ordering** — schema identifiers (table/column/relationship names) now sort *before* keywords so a name the user would have to look up stays visible in the single-row, window-scrolled candidate line (keywords are learned over time; the `tok_identifier`/`tok_keyword` colour split marks the boundary); shipped with a `walk_repeated` fix that surfaces a list item's trailing optionals at a clean boundary (`order by Name ` → `asc`/`desc`, `select Name ` → `as`, `create table … Code(text) ` → `not`/`unique`/`default`/`check`; the `,` separator deliberately not surfaced); records a deferred two-line hint box for growing lists; **Amendment 3 makes the ambient-hint fallback rung schema-aware** — Amendment 1's bottom-rung `parse_command_in_mode` was schemaless while every earlier rung was not, so between-values insert hints pointed at `)` (type-blind close) instead of `,` and wrong-arity closed tuples read "submit with Enter" for an input the schema-aware parse rejects (issue #2); now uses `parse_command_with_schema_in_mode`, no extra walk, with the friendly arity diagnostic still winning at its higher rung; **Amendment 4 gives column types a dedicated highlight class** — both `Node::Ident.highlight_override` *and* the `Word.highlight_override` field were dead (driver destructured the former to `_`, `walk_word` hardcoded `Keyword`); now both wired through, with a new `HighlightClass::Type` + eighth `Theme` field `tok_type` (a pink/deep-magenta distinct from both keyword purple and identifier teal) so types no longer render identically to identifiers (issue #8); the three `IdentSource::Types` slots opt in via `Some(Type)` (advanced-mode single-word SQL aliases — `float`, `varchar`, … per ADR-0035 §3 — ride along for free), and the two-word `double precision` alias opts in via the new `Word::type_keyword` constructor so it matches its synonyms
|
||||||
- [ADR-0023 — Unified declarative grammar tree](0023-unified-grammar-tree.md) — direction (superseded for execution detail by ADR-0024)
|
- [ADR-0023 — Unified declarative grammar tree](0023-unified-grammar-tree.md) — direction (superseded for execution detail by ADR-0024)
|
||||||
- [ADR-0024 — Unified grammar tree: execution plan](0024-unified-grammar-tree-execution-plan.md) — **Accepted**, the executable spec — implemented (Phases A–F; Phase F shipped "minimal", `parser.rs` retained as the router — see the ADR's Phase F implementation note)
|
- [ADR-0024 — Unified grammar tree: execution plan](0024-unified-grammar-tree-execution-plan.md) — **Accepted**, the executable spec — implemented (Phases A–F; Phase F shipped "minimal", `parser.rs` retained as the router — see the ADR's Phase F implementation note)
|
||||||
- [ADR-0025 — Indexes](0025-indexes.md) — **Accepted** (**Amendment 1, 2026-05-25**: UNIQUE indexes admitted on the **advanced-mode** surface via `CREATE UNIQUE INDEX` — ADR-0035 §4d; the `IndexSchema.unique` flag round-trips through `project.yaml` with no new metadata table since the engine reports uniqueness natively; simple-mode `add unique index` stays deferred), `add index` / `drop index`, persistence, rebuild-table preservation, and items-list display (`C3` index portion + `S2`)
|
- [ADR-0025 — Indexes](0025-indexes.md) — **Accepted** (**Amendment 1, 2026-05-25**: UNIQUE indexes admitted on the **advanced-mode** surface via `CREATE UNIQUE INDEX` — ADR-0035 §4d; the `IndexSchema.unique` flag round-trips through `project.yaml` with no new metadata table since the engine reports uniqueness natively; simple-mode `add unique index` stays deferred), `add index` / `drop index`, persistence, rebuild-table preservation, and items-list display (`C3` index portion + `S2`)
|
||||||
|
|||||||
@@ -106,6 +106,10 @@ handoff-14 cleanup; 449 after B2/C2.)
|
|||||||
- [ ] **I3** Tab completion for app commands, DSL keywords, table
|
- [ ] **I3** Tab completion for app commands, DSL keywords, table
|
||||||
names, column names, and SQL keywords.
|
names, column names, and SQL keywords.
|
||||||
- [ ] **I4** Syntax highlighting for both the DSL and SQL.
|
- [ ] **I4** Syntax highlighting for both the DSL and SQL.
|
||||||
|
*(Refinement 2026-05-29, issue #8: column data types now carry a
|
||||||
|
dedicated `HighlightClass::Type` / `tok_type` colour, distinct from
|
||||||
|
identifiers and clause keywords — ADR-0022 Amendment 4. The broad
|
||||||
|
highlighting goal stays open.)*
|
||||||
- [ ] **I5** In-flight query/command cancellation (Ctrl-C in the
|
- [ ] **I5** In-flight query/command cancellation (Ctrl-C in the
|
||||||
output area or input field).
|
output area or input field).
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ use crate::dsl::command::{
|
|||||||
};
|
};
|
||||||
use crate::dsl::value::Value;
|
use crate::dsl::value::Value;
|
||||||
use crate::dsl::grammar::{
|
use crate::dsl::grammar::{
|
||||||
CommandNode, HintMode, IdentSource, Node, ValidationError, Word,
|
CommandNode, HighlightClass, HintMode, IdentSource, Node, ValidationError, Word,
|
||||||
shared::{REFERENTIAL_CLAUSES, TYPE_SLOT, TYPE_VALIDATOR},
|
shared::{REFERENTIAL_CLAUSES, TYPE_SLOT, TYPE_VALIDATOR},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1079,7 +1079,7 @@ const COL_SPEC_NODES: &[Node] = &[
|
|||||||
source: IdentSource::Types,
|
source: IdentSource::Types,
|
||||||
role: "col_type",
|
role: "col_type",
|
||||||
validator: Some(TYPE_VALIDATOR),
|
validator: Some(TYPE_VALIDATOR),
|
||||||
highlight_override: None,
|
highlight_override: Some(HighlightClass::Type),
|
||||||
writes_table: false,
|
writes_table: false,
|
||||||
writes_column: false,
|
writes_column: false,
|
||||||
writes_user_listed_column: false,
|
writes_user_listed_column: false,
|
||||||
|
|||||||
@@ -47,6 +47,12 @@ use crate::dsl::walker::outcome::MatchedPath;
|
|||||||
pub enum HighlightClass {
|
pub enum HighlightClass {
|
||||||
Keyword,
|
Keyword,
|
||||||
Identifier,
|
Identifier,
|
||||||
|
/// Column data-type keyword (`int`, `serial`, `text`, …).
|
||||||
|
/// Distinct from `Keyword` and `Identifier` so learners can
|
||||||
|
/// tell "this is a type" from a clause keyword or a name they
|
||||||
|
/// invented (ADR-0022 Amendment 4). Assigned via a type slot's
|
||||||
|
/// `highlight_override`, not by byte shape.
|
||||||
|
Type,
|
||||||
Number,
|
Number,
|
||||||
String,
|
String,
|
||||||
Punct,
|
Punct,
|
||||||
@@ -191,6 +197,21 @@ impl Word {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A keyword that highlights as a column **type** rather than a
|
||||||
|
/// clause keyword (ADR-0022 Amendment 4). The one user today is
|
||||||
|
/// the two-word `double precision` SQL alias (ADR-0035 §3): it
|
||||||
|
/// is matched as keyword tokens, not an `IdentSource::Types`
|
||||||
|
/// `Ident`, so without this it would render keyword-coloured
|
||||||
|
/// while its single-word synonyms (`float`, `real`) render as
|
||||||
|
/// types.
|
||||||
|
pub const fn type_keyword(primary: &'static str) -> Self {
|
||||||
|
Self {
|
||||||
|
primary,
|
||||||
|
aliases: &[],
|
||||||
|
highlight_override: Some(HighlightClass::Type),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Case-insensitive match against the primary or any alias.
|
/// Case-insensitive match against the primary or any alias.
|
||||||
pub fn matches(&self, candidate: &str) -> bool {
|
pub fn matches(&self, candidate: &str) -> bool {
|
||||||
if candidate.eq_ignore_ascii_case(self.primary) {
|
if candidate.eq_ignore_ascii_case(self.primary) {
|
||||||
|
|||||||
@@ -7,8 +7,8 @@
|
|||||||
|
|
||||||
use crate::completion::TableColumn;
|
use crate::completion::TableColumn;
|
||||||
use crate::dsl::grammar::{
|
use crate::dsl::grammar::{
|
||||||
HintMode, IdentSource, IdentValidator, Node, NumberValidator,
|
HighlightClass, HintMode, IdentSource, IdentValidator, Node,
|
||||||
ValidationError, Word,
|
NumberValidator, ValidationError, Word,
|
||||||
};
|
};
|
||||||
use crate::dsl::types::Type;
|
use crate::dsl::types::Type;
|
||||||
use crate::dsl::walker::context::WalkContext;
|
use crate::dsl::walker::context::WalkContext;
|
||||||
@@ -50,7 +50,7 @@ pub const TYPE_SLOT: Node = Node::Ident {
|
|||||||
source: IdentSource::Types,
|
source: IdentSource::Types,
|
||||||
role: "type",
|
role: "type",
|
||||||
validator: Some(TYPE_VALIDATOR),
|
validator: Some(TYPE_VALIDATOR),
|
||||||
highlight_override: None,
|
highlight_override: Some(HighlightClass::Type),
|
||||||
writes_table: false,
|
writes_table: false,
|
||||||
writes_column: false,
|
writes_column: false,
|
||||||
writes_user_listed_column: false,
|
writes_user_listed_column: false,
|
||||||
|
|||||||
@@ -24,7 +24,9 @@
|
|||||||
//! `sql_insert::SQL_INSERT_SHAPE`, which starts at `INTO`).
|
//! `sql_insert::SQL_INSERT_SHAPE`, which starts at `INTO`).
|
||||||
|
|
||||||
use crate::dsl::grammar::sql_select::reject_internal_table;
|
use crate::dsl::grammar::sql_select::reject_internal_table;
|
||||||
use crate::dsl::grammar::{IdentSource, Node, ValidationError, Word, shared, sql_expr};
|
use crate::dsl::grammar::{
|
||||||
|
HighlightClass, IdentSource, Node, ValidationError, Word, shared, sql_expr,
|
||||||
|
};
|
||||||
use crate::dsl::types::Type;
|
use crate::dsl::types::Type;
|
||||||
|
|
||||||
static COMMA: Node = Node::Punct(',');
|
static COMMA: Node = Node::Punct(',');
|
||||||
@@ -60,7 +62,7 @@ const SQL_TYPE_NAME: Node = Node::Ident {
|
|||||||
source: IdentSource::Types,
|
source: IdentSource::Types,
|
||||||
role: "col_type",
|
role: "col_type",
|
||||||
validator: Some(validate_sql_type_name),
|
validator: Some(validate_sql_type_name),
|
||||||
highlight_override: None,
|
highlight_override: Some(HighlightClass::Type),
|
||||||
writes_table: false,
|
writes_table: false,
|
||||||
writes_column: false,
|
writes_column: false,
|
||||||
writes_user_listed_column: false,
|
writes_user_listed_column: false,
|
||||||
@@ -86,8 +88,8 @@ const LENGTH_OPT: Node = Node::Optional(&Node::Seq(LENGTH_NODES));
|
|||||||
// on its own (ADR-0035 §6.3). The builder maps the pair to
|
// on its own (ADR-0035 §6.3). The builder maps the pair to
|
||||||
// `Type::Real`.
|
// `Type::Real`.
|
||||||
static DOUBLE_PRECISION_NODES: &[Node] = &[
|
static DOUBLE_PRECISION_NODES: &[Node] = &[
|
||||||
Node::Word(Word::keyword("double")),
|
Node::Word(Word::type_keyword("double")),
|
||||||
Node::Word(Word::keyword("precision")),
|
Node::Word(Word::type_keyword("precision")),
|
||||||
];
|
];
|
||||||
static TYPE_WITH_LENGTH_NODES: &[Node] = &[SQL_TYPE_NAME, LENGTH_OPT];
|
static TYPE_WITH_LENGTH_NODES: &[Node] = &[SQL_TYPE_NAME, LENGTH_OPT];
|
||||||
static SQL_TYPE_CHOICES: &[Node] = &[
|
static SQL_TYPE_CHOICES: &[Node] = &[
|
||||||
|
|||||||
@@ -180,7 +180,7 @@ fn walk_node_inner(
|
|||||||
source: src,
|
source: src,
|
||||||
role,
|
role,
|
||||||
validator,
|
validator,
|
||||||
highlight_override: _,
|
highlight_override,
|
||||||
writes_table,
|
writes_table,
|
||||||
writes_column,
|
writes_column,
|
||||||
writes_user_listed_column,
|
writes_user_listed_column,
|
||||||
@@ -193,6 +193,7 @@ fn walk_node_inner(
|
|||||||
*src,
|
*src,
|
||||||
role,
|
role,
|
||||||
*validator,
|
*validator,
|
||||||
|
*highlight_override,
|
||||||
*writes_table,
|
*writes_table,
|
||||||
*writes_column,
|
*writes_column,
|
||||||
*writes_user_listed_column,
|
*writes_user_listed_column,
|
||||||
@@ -336,7 +337,10 @@ fn walk_word(
|
|||||||
per_byte.push(ByteClass {
|
per_byte.push(ByteClass {
|
||||||
start,
|
start,
|
||||||
end,
|
end,
|
||||||
class: HighlightClass::Keyword,
|
// A keyword may opt into a non-default colour via
|
||||||
|
// `Word::type_keyword` (e.g. `double precision`, ADR-0022
|
||||||
|
// Amendment 4). Plain keywords leave it `None`.
|
||||||
|
class: word.highlight_override.unwrap_or(HighlightClass::Keyword),
|
||||||
});
|
});
|
||||||
NodeWalkResult::Matched { end, skipped: Vec::new() }
|
NodeWalkResult::Matched { end, skipped: Vec::new() }
|
||||||
} else {
|
} else {
|
||||||
@@ -397,6 +401,7 @@ fn walk_ident(
|
|||||||
src: crate::dsl::grammar::IdentSource,
|
src: crate::dsl::grammar::IdentSource,
|
||||||
role: &'static str,
|
role: &'static str,
|
||||||
validator: Option<crate::dsl::grammar::IdentValidator>,
|
validator: Option<crate::dsl::grammar::IdentValidator>,
|
||||||
|
highlight_override: Option<crate::dsl::grammar::HighlightClass>,
|
||||||
writes_table: bool,
|
writes_table: bool,
|
||||||
writes_column: bool,
|
writes_column: bool,
|
||||||
writes_user_listed_column: bool,
|
writes_user_listed_column: bool,
|
||||||
@@ -554,7 +559,10 @@ fn walk_ident(
|
|||||||
per_byte.push(ByteClass {
|
per_byte.push(ByteClass {
|
||||||
start,
|
start,
|
||||||
end,
|
end,
|
||||||
class: HighlightClass::Identifier,
|
// A type slot (and any future slot that wants a non-default
|
||||||
|
// colour) overrides the otherwise-uniform Identifier class
|
||||||
|
// (issue #8 / ADR-0022 Amendment 4).
|
||||||
|
class: highlight_override.unwrap_or(HighlightClass::Identifier),
|
||||||
});
|
});
|
||||||
NodeWalkResult::Matched { end, skipped: Vec::new() }
|
NodeWalkResult::Matched { end, skipped: Vec::new() }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -297,6 +297,76 @@ mod tests {
|
|||||||
assert_eq!(runs[1].2, HighlightClass::Keyword);
|
assert_eq!(runs[1].2, HighlightClass::Keyword);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn dsl_type_keyword_classified_as_type() {
|
||||||
|
// Issue #8 / ADR-0022 Amendment 4: the column type in a DSL
|
||||||
|
// `create table` (`serial`) is a Type, distinct from the
|
||||||
|
// identifiers `T` / `id` which stay Identifier.
|
||||||
|
let runs = run("create table T with pk id(serial)");
|
||||||
|
// `serial` occupies bytes 26..32 in the input.
|
||||||
|
let serial = runs
|
||||||
|
.iter()
|
||||||
|
.find(|(s, e, _)| &"create table T with pk id(serial)"[*s..*e] == "serial")
|
||||||
|
.expect("serial run present");
|
||||||
|
assert_eq!(serial.2, HighlightClass::Type);
|
||||||
|
// The invented identifiers are NOT typed.
|
||||||
|
for name in ["T", "id"] {
|
||||||
|
let r = runs
|
||||||
|
.iter()
|
||||||
|
.find(|(s, e, _)| &"create table T with pk id(serial)"[*s..*e] == name)
|
||||||
|
.unwrap_or_else(|| panic!("{name} run present"));
|
||||||
|
assert_eq!(r.2, HighlightClass::Identifier, "{name} stays Identifier");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn advanced_type_keywords_classified_as_type() {
|
||||||
|
// Issue #8: the exact example from the ticket. In advanced
|
||||||
|
// mode the SQL type keywords `int` / `serial` are Type;
|
||||||
|
// `Orders` / `count` / `id` stay Identifier.
|
||||||
|
let input = "create table Orders (count int, id serial)";
|
||||||
|
let runs: Vec<(usize, usize, HighlightClass)> =
|
||||||
|
highlight_runs_in_mode(input, crate::mode::Mode::Advanced)
|
||||||
|
.into_iter()
|
||||||
|
.map(|c| (c.start, c.end, c.class))
|
||||||
|
.collect();
|
||||||
|
let class_of = |needle: &str| {
|
||||||
|
runs.iter()
|
||||||
|
.find(|(s, e, _)| &input[*s..*e] == needle)
|
||||||
|
.unwrap_or_else(|| panic!("{needle} run present"))
|
||||||
|
.2
|
||||||
|
};
|
||||||
|
assert_eq!(class_of("int"), HighlightClass::Type);
|
||||||
|
assert_eq!(class_of("serial"), HighlightClass::Type);
|
||||||
|
assert_eq!(class_of("Orders"), HighlightClass::Identifier);
|
||||||
|
assert_eq!(class_of("count"), HighlightClass::Identifier);
|
||||||
|
assert_eq!(class_of("id"), HighlightClass::Identifier);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn advanced_double_precision_classified_as_type() {
|
||||||
|
// Issue #8 follow-up: the two-word `double precision` SQL
|
||||||
|
// alias (ADR-0035 §3) is matched as keyword tokens, but
|
||||||
|
// `Word::type_keyword` colours it as a type so it matches
|
||||||
|
// its single-word synonyms `float` / `real`. Both words
|
||||||
|
// carry the Type class.
|
||||||
|
let input = "create table T (x double precision)";
|
||||||
|
let runs: Vec<(usize, usize, HighlightClass)> =
|
||||||
|
highlight_runs_in_mode(input, crate::mode::Mode::Advanced)
|
||||||
|
.into_iter()
|
||||||
|
.map(|c| (c.start, c.end, c.class))
|
||||||
|
.collect();
|
||||||
|
let class_of = |needle: &str| {
|
||||||
|
runs.iter()
|
||||||
|
.find(|(s, e, _)| &input[*s..*e] == needle)
|
||||||
|
.unwrap_or_else(|| panic!("{needle} run present"))
|
||||||
|
.2
|
||||||
|
};
|
||||||
|
assert_eq!(class_of("double"), HighlightClass::Type);
|
||||||
|
assert_eq!(class_of("precision"), HighlightClass::Type);
|
||||||
|
assert_eq!(class_of("x"), HighlightClass::Identifier);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn full_command_walks_with_each_class() {
|
fn full_command_walks_with_each_class() {
|
||||||
// `update T set Name='hi' --all-rows` — walker covers it
|
// `update T set Name='hi' --all-rows` — walker covers it
|
||||||
|
|||||||
@@ -51,6 +51,11 @@ pub struct Theme {
|
|||||||
// ---- Per-token-class colours (ADR-0022 §3) -------------------
|
// ---- Per-token-class colours (ADR-0022 §3) -------------------
|
||||||
pub tok_keyword: Color,
|
pub tok_keyword: Color,
|
||||||
pub tok_identifier: Color,
|
pub tok_identifier: Color,
|
||||||
|
/// Column data-type keyword colour (ADR-0022 Amendment 4) —
|
||||||
|
/// a dedicated tone distinct from both `tok_keyword` and
|
||||||
|
/// `tok_identifier` so a learner can tell a type from a
|
||||||
|
/// clause keyword or an invented name at a glance.
|
||||||
|
pub tok_type: Color,
|
||||||
pub tok_number: Color,
|
pub tok_number: Color,
|
||||||
pub tok_string: Color,
|
pub tok_string: Color,
|
||||||
pub tok_punct: Color,
|
pub tok_punct: Color,
|
||||||
@@ -84,6 +89,7 @@ impl Theme {
|
|||||||
// distinct from the mode-banner blue.
|
// distinct from the mode-banner blue.
|
||||||
tok_keyword: Color::Rgb(0xC7, 0x92, 0xEA), // muted purple
|
tok_keyword: Color::Rgb(0xC7, 0x92, 0xEA), // muted purple
|
||||||
tok_identifier: Color::Rgb(0x56, 0xB6, 0xC2), // cyan-teal — identifiers are the user's content, deserve a vivid distinct colour
|
tok_identifier: Color::Rgb(0x56, 0xB6, 0xC2), // cyan-teal — identifiers are the user's content, deserve a vivid distinct colour
|
||||||
|
tok_type: Color::Rgb(0xF0, 0x8F, 0xC0), // pink — types sit in the red-purple range, clearly apart from the lavender keyword and teal identifier
|
||||||
tok_number: Color::Rgb(0xF7, 0x8C, 0x6C), // warm orange
|
tok_number: Color::Rgb(0xF7, 0x8C, 0x6C), // warm orange
|
||||||
tok_string: Color::Rgb(0xC3, 0xE8, 0x8D), // soft green
|
tok_string: Color::Rgb(0xC3, 0xE8, 0x8D), // soft green
|
||||||
tok_punct: Color::Rgb(0x8B, 0x90, 0x9A), // == muted
|
tok_punct: Color::Rgb(0x8B, 0x90, 0x9A), // == muted
|
||||||
@@ -113,6 +119,7 @@ impl Theme {
|
|||||||
// literals + flags; cool accent for keyword.
|
// literals + flags; cool accent for keyword.
|
||||||
tok_keyword: Color::Rgb(0x6F, 0x42, 0xC1), // royal purple
|
tok_keyword: Color::Rgb(0x6F, 0x42, 0xC1), // royal purple
|
||||||
tok_identifier: Color::Rgb(0x0F, 0x6B, 0x76), // deep teal — same role as dark variant: identifiers stand out
|
tok_identifier: Color::Rgb(0x0F, 0x6B, 0x76), // deep teal — same role as dark variant: identifiers stand out
|
||||||
|
tok_type: Color::Rgb(0xA8, 0x2D, 0x73), // deep magenta — red-purple, distinct from royal-purple keyword + teal identifier
|
||||||
tok_number: Color::Rgb(0xBC, 0x4F, 0x1F), // burnt orange
|
tok_number: Color::Rgb(0xBC, 0x4F, 0x1F), // burnt orange
|
||||||
tok_string: Color::Rgb(0x22, 0x86, 0x3A), // forest green
|
tok_string: Color::Rgb(0x22, 0x86, 0x3A), // forest green
|
||||||
tok_punct: Color::Rgb(0x60, 0x66, 0x73), // == muted
|
tok_punct: Color::Rgb(0x60, 0x66, 0x73), // == muted
|
||||||
@@ -132,6 +139,7 @@ impl Theme {
|
|||||||
match class {
|
match class {
|
||||||
HighlightClass::Keyword => self.tok_keyword,
|
HighlightClass::Keyword => self.tok_keyword,
|
||||||
HighlightClass::Identifier => self.tok_identifier,
|
HighlightClass::Identifier => self.tok_identifier,
|
||||||
|
HighlightClass::Type => self.tok_type,
|
||||||
HighlightClass::Number => self.tok_number,
|
HighlightClass::Number => self.tok_number,
|
||||||
HighlightClass::String => self.tok_string,
|
HighlightClass::String => self.tok_string,
|
||||||
HighlightClass::Punct => self.tok_punct,
|
HighlightClass::Punct => self.tok_punct,
|
||||||
@@ -156,6 +164,7 @@ mod tests {
|
|||||||
let t = Theme::dark();
|
let t = Theme::dark();
|
||||||
for (name, c) in [
|
for (name, c) in [
|
||||||
("tok_keyword", t.tok_keyword),
|
("tok_keyword", t.tok_keyword),
|
||||||
|
("tok_type", t.tok_type),
|
||||||
("tok_number", t.tok_number),
|
("tok_number", t.tok_number),
|
||||||
("tok_string", t.tok_string),
|
("tok_string", t.tok_string),
|
||||||
("tok_flag", t.tok_flag),
|
("tok_flag", t.tok_flag),
|
||||||
@@ -174,6 +183,7 @@ mod tests {
|
|||||||
let t = Theme::light();
|
let t = Theme::light();
|
||||||
for (name, c) in [
|
for (name, c) in [
|
||||||
("tok_keyword", t.tok_keyword),
|
("tok_keyword", t.tok_keyword),
|
||||||
|
("tok_type", t.tok_type),
|
||||||
("tok_number", t.tok_number),
|
("tok_number", t.tok_number),
|
||||||
("tok_string", t.tok_string),
|
("tok_string", t.tok_string),
|
||||||
("tok_flag", t.tok_flag),
|
("tok_flag", t.tok_flag),
|
||||||
@@ -192,10 +202,22 @@ mod tests {
|
|||||||
let t = Theme::dark();
|
let t = Theme::dark();
|
||||||
assert_eq!(t.highlight_class_color(HighlightClass::Keyword), t.tok_keyword);
|
assert_eq!(t.highlight_class_color(HighlightClass::Keyword), t.tok_keyword);
|
||||||
assert_eq!(t.highlight_class_color(HighlightClass::Identifier), t.tok_identifier);
|
assert_eq!(t.highlight_class_color(HighlightClass::Identifier), t.tok_identifier);
|
||||||
|
assert_eq!(t.highlight_class_color(HighlightClass::Type), t.tok_type);
|
||||||
assert_eq!(t.highlight_class_color(HighlightClass::Number), t.tok_number);
|
assert_eq!(t.highlight_class_color(HighlightClass::Number), t.tok_number);
|
||||||
assert_eq!(t.highlight_class_color(HighlightClass::String), t.tok_string);
|
assert_eq!(t.highlight_class_color(HighlightClass::String), t.tok_string);
|
||||||
assert_eq!(t.highlight_class_color(HighlightClass::Punct), t.tok_punct);
|
assert_eq!(t.highlight_class_color(HighlightClass::Punct), t.tok_punct);
|
||||||
assert_eq!(t.highlight_class_color(HighlightClass::Flag), t.tok_flag);
|
assert_eq!(t.highlight_class_color(HighlightClass::Flag), t.tok_flag);
|
||||||
assert_eq!(t.highlight_class_color(HighlightClass::Error), t.tok_error);
|
assert_eq!(t.highlight_class_color(HighlightClass::Error), t.tok_error);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn type_colour_is_distinct_from_keyword_and_identifier() {
|
||||||
|
// ADR-0022 Amendment 4 / issue #8: the whole point of a
|
||||||
|
// dedicated type class is that types do NOT share a colour
|
||||||
|
// with clause keywords or invented identifiers.
|
||||||
|
for t in [Theme::dark(), Theme::light()] {
|
||||||
|
assert_ne!(t.tok_type, t.tok_keyword);
|
||||||
|
assert_ne!(t.tok_type, t.tok_identifier);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user