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:
@@ -18,7 +18,7 @@ use crate::dsl::command::{
|
||||
};
|
||||
use crate::dsl::value::Value;
|
||||
use crate::dsl::grammar::{
|
||||
CommandNode, HintMode, IdentSource, Node, ValidationError, Word,
|
||||
CommandNode, HighlightClass, HintMode, IdentSource, Node, ValidationError, Word,
|
||||
shared::{REFERENTIAL_CLAUSES, TYPE_SLOT, TYPE_VALIDATOR},
|
||||
};
|
||||
|
||||
@@ -1079,7 +1079,7 @@ const COL_SPEC_NODES: &[Node] = &[
|
||||
source: IdentSource::Types,
|
||||
role: "col_type",
|
||||
validator: Some(TYPE_VALIDATOR),
|
||||
highlight_override: None,
|
||||
highlight_override: Some(HighlightClass::Type),
|
||||
writes_table: false,
|
||||
writes_column: false,
|
||||
writes_user_listed_column: false,
|
||||
|
||||
@@ -47,6 +47,12 @@ use crate::dsl::walker::outcome::MatchedPath;
|
||||
pub enum HighlightClass {
|
||||
Keyword,
|
||||
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,
|
||||
String,
|
||||
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.
|
||||
pub fn matches(&self, candidate: &str) -> bool {
|
||||
if candidate.eq_ignore_ascii_case(self.primary) {
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
|
||||
use crate::completion::TableColumn;
|
||||
use crate::dsl::grammar::{
|
||||
HintMode, IdentSource, IdentValidator, Node, NumberValidator,
|
||||
ValidationError, Word,
|
||||
HighlightClass, HintMode, IdentSource, IdentValidator, Node,
|
||||
NumberValidator, ValidationError, Word,
|
||||
};
|
||||
use crate::dsl::types::Type;
|
||||
use crate::dsl::walker::context::WalkContext;
|
||||
@@ -50,7 +50,7 @@ pub const TYPE_SLOT: Node = Node::Ident {
|
||||
source: IdentSource::Types,
|
||||
role: "type",
|
||||
validator: Some(TYPE_VALIDATOR),
|
||||
highlight_override: None,
|
||||
highlight_override: Some(HighlightClass::Type),
|
||||
writes_table: false,
|
||||
writes_column: false,
|
||||
writes_user_listed_column: false,
|
||||
|
||||
@@ -24,7 +24,9 @@
|
||||
//! `sql_insert::SQL_INSERT_SHAPE`, which starts at `INTO`).
|
||||
|
||||
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;
|
||||
|
||||
static COMMA: Node = Node::Punct(',');
|
||||
@@ -60,7 +62,7 @@ const SQL_TYPE_NAME: Node = Node::Ident {
|
||||
source: IdentSource::Types,
|
||||
role: "col_type",
|
||||
validator: Some(validate_sql_type_name),
|
||||
highlight_override: None,
|
||||
highlight_override: Some(HighlightClass::Type),
|
||||
writes_table: false,
|
||||
writes_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
|
||||
// `Type::Real`.
|
||||
static DOUBLE_PRECISION_NODES: &[Node] = &[
|
||||
Node::Word(Word::keyword("double")),
|
||||
Node::Word(Word::keyword("precision")),
|
||||
Node::Word(Word::type_keyword("double")),
|
||||
Node::Word(Word::type_keyword("precision")),
|
||||
];
|
||||
static TYPE_WITH_LENGTH_NODES: &[Node] = &[SQL_TYPE_NAME, LENGTH_OPT];
|
||||
static SQL_TYPE_CHOICES: &[Node] = &[
|
||||
|
||||
@@ -180,7 +180,7 @@ fn walk_node_inner(
|
||||
source: src,
|
||||
role,
|
||||
validator,
|
||||
highlight_override: _,
|
||||
highlight_override,
|
||||
writes_table,
|
||||
writes_column,
|
||||
writes_user_listed_column,
|
||||
@@ -193,6 +193,7 @@ fn walk_node_inner(
|
||||
*src,
|
||||
role,
|
||||
*validator,
|
||||
*highlight_override,
|
||||
*writes_table,
|
||||
*writes_column,
|
||||
*writes_user_listed_column,
|
||||
@@ -336,7 +337,10 @@ fn walk_word(
|
||||
per_byte.push(ByteClass {
|
||||
start,
|
||||
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() }
|
||||
} else {
|
||||
@@ -397,6 +401,7 @@ fn walk_ident(
|
||||
src: crate::dsl::grammar::IdentSource,
|
||||
role: &'static str,
|
||||
validator: Option<crate::dsl::grammar::IdentValidator>,
|
||||
highlight_override: Option<crate::dsl::grammar::HighlightClass>,
|
||||
writes_table: bool,
|
||||
writes_column: bool,
|
||||
writes_user_listed_column: bool,
|
||||
@@ -554,7 +559,10 @@ fn walk_ident(
|
||||
per_byte.push(ByteClass {
|
||||
start,
|
||||
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() }
|
||||
}
|
||||
|
||||
@@ -297,6 +297,76 @@ mod tests {
|
||||
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]
|
||||
fn full_command_walks_with_each_class() {
|
||||
// `update T set Name='hi' --all-rows` — walker covers it
|
||||
|
||||
Reference in New Issue
Block a user