sql_dialect_fmt_highlight/
lib.rs1use sql_dialect_fmt_lexer::{tokenize, LexError};
8use sql_dialect_fmt_syntax::{keyword_kind, SyntaxKind};
9
10pub mod semantic;
11pub use semantic::{
12 delta_encode, detect_injections, line_tokens, resolve_tokens, semantic_token, semantic_tokens,
13 semantic_tokens_lsp, InjectedLanguage, Injection, LineToken, ResolvedToken,
14 SemanticTokenModifiers, SemanticTokenType, SemanticTokens,
15};
16
17#[derive(Clone, Debug, PartialEq, Eq)]
20#[non_exhaustive]
21pub struct Highlighted<'a> {
22 pub tokens: Vec<HighlightToken<'a>>,
24 pub errors: Vec<LexError>,
26}
27
28#[derive(Clone, Debug, PartialEq, Eq)]
31#[non_exhaustive]
32pub struct HighlightToken<'a> {
33 pub kind: HighlightKind,
35 pub text: &'a str,
37 pub range: std::ops::Range<usize>,
39}
40
41#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
42pub enum HighlightKind {
43 Whitespace,
44 Comment,
45 Keyword,
46 Type,
47 Identifier,
48 QuotedIdentifier,
49 String,
50 DollarString,
51 Number,
52 Variable,
53 Operator,
54 Punctuation,
55 Error,
56}
57
58impl HighlightKind {
59 pub const fn scope(self) -> &'static str {
60 match self {
61 HighlightKind::Whitespace => "text.whitespace",
62 HighlightKind::Comment => "comment.line-or-block",
63 HighlightKind::Keyword => "keyword.sql",
64 HighlightKind::Type => "support.type.sql",
65 HighlightKind::Identifier => "identifier.sql",
66 HighlightKind::QuotedIdentifier => "identifier.quoted.sql",
67 HighlightKind::String => "string.quoted.sql",
68 HighlightKind::DollarString => "string.dollar-quoted.sql",
69 HighlightKind::Number => "constant.numeric.sql",
70 HighlightKind::Variable => "variable.parameter.sql",
71 HighlightKind::Operator => "operator.sql",
72 HighlightKind::Punctuation => "punctuation.sql",
73 HighlightKind::Error => "invalid.illegal.sql",
74 }
75 }
76}
77
78pub fn highlight(input: &str) -> Highlighted<'_> {
79 let lexed = tokenize(input);
80 let tokens = lexed
81 .tokens
82 .into_iter()
83 .scan(0usize, |offset, token| {
84 let start = *offset;
85 *offset += token.text.len();
86 Some(HighlightToken {
87 kind: classify(token.kind, token.text),
88 text: token.text,
89 range: start..*offset,
90 })
91 })
92 .collect();
93
94 Highlighted {
95 tokens,
96 errors: lexed.errors,
97 }
98}
99
100pub fn classify(kind: SyntaxKind, text: &str) -> HighlightKind {
101 if kind.is_trivia() {
102 return match kind {
103 SyntaxKind::COMMENT | SyntaxKind::BLOCK_COMMENT => HighlightKind::Comment,
104 _ => HighlightKind::Whitespace,
105 };
106 }
107
108 if kind.is_keyword()
109 || kind == SyntaxKind::CONTEXTUAL_KEYWORD
110 || (kind == SyntaxKind::IDENT && keyword_kind(text).is_some())
111 {
112 return HighlightKind::Keyword;
113 }
114 if kind == SyntaxKind::IDENT && is_builtin_type(text) {
115 return HighlightKind::Type;
116 }
117
118 match kind {
119 SyntaxKind::IDENT => HighlightKind::Identifier,
120 SyntaxKind::QUOTED_IDENT => HighlightKind::QuotedIdentifier,
121 SyntaxKind::STRING => HighlightKind::String,
122 SyntaxKind::DOLLAR_STRING => HighlightKind::DollarString,
123 SyntaxKind::INT_NUMBER | SyntaxKind::FLOAT_NUMBER => HighlightKind::Number,
124 SyntaxKind::VARIABLE | SyntaxKind::DOLLAR | SyntaxKind::QUESTION => HighlightKind::Variable,
125 SyntaxKind::ERROR | SyntaxKind::BANG => HighlightKind::Error,
126 kind if is_operator(kind) => HighlightKind::Operator,
127 _ => HighlightKind::Punctuation,
128 }
129}
130
131fn is_builtin_type(text: &str) -> bool {
132 const TYPES: &[&str] = &[
133 "ARRAY",
134 "BIGINT",
135 "BINARY",
136 "BOOLEAN",
137 "CHAR",
138 "DATE",
139 "DATETIME",
140 "DEC",
141 "DECIMAL",
142 "DOUBLE",
143 "FLOAT",
144 "GEOGRAPHY",
145 "GEOMETRY",
146 "INT",
147 "INTEGER",
148 "MAP",
149 "NUMBER",
150 "NUMERIC",
151 "OBJECT",
152 "REAL",
153 "STRING",
154 "TEXT",
155 "TIME",
156 "TIMESTAMP",
157 "TIMESTAMP_LTZ",
158 "TIMESTAMP_NTZ",
159 "TIMESTAMP_TZ",
160 "VARIANT",
161 "VARCHAR",
162 "VECTOR",
163 ];
164 TYPES
165 .iter()
166 .any(|candidate| candidate.eq_ignore_ascii_case(text))
167}
168
169fn is_operator(kind: SyntaxKind) -> bool {
170 matches!(
171 kind,
172 SyntaxKind::COLON
173 | SyntaxKind::COLON2
174 | SyntaxKind::ASSIGN
175 | SyntaxKind::EQ
176 | SyntaxKind::NEQ
177 | SyntaxKind::LT
178 | SyntaxKind::LTE
179 | SyntaxKind::GT
180 | SyntaxKind::GTE
181 | SyntaxKind::PLUS
182 | SyntaxKind::MINUS
183 | SyntaxKind::STAR
184 | SyntaxKind::SLASH
185 | SyntaxKind::PERCENT
186 | SyntaxKind::CONCAT
187 | SyntaxKind::PIPE
188 | SyntaxKind::PIPE_GT
189 | SyntaxKind::FLOW_PIPE
190 | SyntaxKind::ARROW
191 | SyntaxKind::FAT_ARROW
192 | SyntaxKind::AMP
193 | SyntaxKind::CARET
194 | SyntaxKind::TILDE
195 | SyntaxKind::AT
196 )
197}
198
199#[cfg(test)]
200mod tests {
201 use super::*;
202
203 #[test]
209 fn classifies_core_snowflake_tokens() {
210 let h = highlight("SELECT $1::NUMBER ->> SELECT \"name\" FROM $1 -- ok\n");
211 assert!(h.errors.is_empty());
212
213 let interesting: Vec<_> = h
214 .tokens
215 .iter()
216 .filter(|token| !matches!(token.kind, HighlightKind::Whitespace))
217 .map(|token| (token.text, token.kind))
218 .collect();
219
220 assert_eq!(
221 interesting,
222 vec![
223 ("SELECT", HighlightKind::Keyword),
224 ("$1", HighlightKind::Variable),
225 ("::", HighlightKind::Operator),
226 ("NUMBER", HighlightKind::Type),
227 ("->>", HighlightKind::Operator),
228 ("SELECT", HighlightKind::Keyword),
229 ("\"name\"", HighlightKind::QuotedIdentifier),
230 ("FROM", HighlightKind::Keyword),
231 ("$1", HighlightKind::Variable),
232 ("-- ok", HighlightKind::Comment),
233 ]
234 );
235 }
236
237 #[test]
238 fn ranges_cover_input_losslessly() {
239 let sql = "SELECT payload:items[0]::VARIANT FROM raw";
240 let highlighted = highlight(sql);
241 assert!(highlighted.errors.is_empty());
242 let joined: String = highlighted.tokens.iter().map(|token| token.text).collect();
243 assert_eq!(joined, sql);
244 for token in &highlighted.tokens {
245 assert_eq!(&sql[token.range.clone()], token.text);
246 }
247 }
248}