1use std::collections::HashMap;
48use std::ops::Range;
49use std::path::Path;
50use std::sync::{Arc, OnceLock};
51
52use rich::Text;
53use rich::color::Color;
54use rich::protocol::{
55 CodeHighlighter, HighlightError, HighlightSpan, HighlightedCode, HighlightedLine,
56};
57use rich::style::Style;
58use rich_plugin_api::{Plugin, PluginError, PluginMetadata, PluginRegistrar};
59
60pub const LANGUAGES: &[&str] = &[
62 "sql",
63 "postgresql",
64 "postgres",
65 "psql",
66 "plpgsql",
67 "mysql",
68 "mariadb",
69 "sqlite",
70 "sqlite3",
71 "bigquery",
72 "snowflake",
73 "duckdb",
74 "tsql",
75 "plsql",
76];
77
78pub const THEMES: &[&str] = &["ansi_dark", "ansi_light", "monokai"];
80
81pub const DEFAULT_THEME: &str = "monokai";
83
84#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
86pub enum TokenKind {
87 Whitespace,
89 Keyword,
91 Type,
93 Constant,
95 Function,
97 Name,
99 QuotedName,
101 String,
103 Number,
105 Comment,
107 Parameter,
109 Operator,
111 Punctuation,
113 Template,
115 Other,
117}
118
119#[derive(Clone, Debug, PartialEq, Eq)]
121pub struct Token {
122 pub kind: TokenKind,
123 pub range: Range<usize>,
124}
125
126const KEYWORDS: &[&str] = &[
127 "abort",
128 "absolute",
129 "action",
130 "add",
131 "after",
132 "all",
133 "alter",
134 "always",
135 "analyse",
136 "analyze",
137 "and",
138 "any",
139 "array",
140 "as",
141 "asc",
142 "asof",
143 "attach",
144 "authorization",
145 "autoincrement",
146 "auto_increment",
147 "before",
148 "begin",
149 "between",
150 "both",
151 "by",
152 "cascade",
153 "case",
154 "cast",
155 "chain",
156 "check",
157 "cluster",
158 "collate",
159 "column",
160 "columns",
161 "comment",
162 "commit",
163 "concurrently",
164 "conflict",
165 "connect",
166 "constraint",
167 "copy",
168 "create",
169 "cross",
170 "cube",
171 "current",
172 "current_date",
173 "current_time",
174 "current_timestamp",
175 "current_user",
176 "cursor",
177 "database",
178 "deallocate",
179 "declare",
180 "default",
181 "deferrable",
182 "deferred",
183 "delete",
184 "desc",
185 "describe",
186 "detach",
187 "distinct",
188 "do",
189 "drop",
190 "each",
191 "else",
192 "elseif",
193 "elsif",
194 "end",
195 "engine",
196 "escape",
197 "except",
198 "exclude",
199 "exclusive",
200 "execute",
201 "exists",
202 "explain",
203 "extension",
204 "fetch",
205 "filter",
206 "first",
207 "following",
208 "for",
209 "foreign",
210 "format",
211 "from",
212 "full",
213 "function",
214 "glob",
215 "grant",
216 "group",
217 "grouping",
218 "having",
219 "if",
220 "ignore",
221 "ilike",
222 "immediate",
223 "in",
224 "include",
225 "index",
226 "inherits",
227 "initially",
228 "inner",
229 "insert",
230 "instead",
231 "intersect",
232 "interval",
233 "into",
234 "is",
235 "isnull",
236 "join",
237 "key",
238 "language",
239 "last",
240 "lateral",
241 "leading",
242 "left",
243 "like",
244 "limit",
245 "load",
246 "local",
247 "lock",
248 "loop",
249 "match",
250 "matched",
251 "materialized",
252 "merge",
253 "minus",
254 "natural",
255 "next",
256 "no",
257 "not",
258 "nothing",
259 "notnull",
260 "nulls",
261 "of",
262 "offset",
263 "on",
264 "only",
265 "option",
266 "options",
267 "or",
268 "order",
269 "ordinality",
270 "outer",
271 "over",
272 "overwrite",
273 "owner",
274 "partition",
275 "pivot",
276 "pragma",
277 "preceding",
278 "primary",
279 "procedure",
280 "qualify",
281 "range",
282 "recursive",
283 "references",
284 "regexp",
285 "reindex",
286 "release",
287 "rename",
288 "repeat",
289 "replace",
290 "restrict",
291 "return",
292 "returning",
293 "returns",
294 "revoke",
295 "right",
296 "rlike",
297 "rollback",
298 "rollup",
299 "row",
300 "rows",
301 "sample",
302 "savepoint",
303 "schema",
304 "select",
305 "semi",
306 "sequence",
307 "set",
308 "sets",
309 "show",
310 "similar",
311 "some",
312 "stage",
313 "start",
314 "stream",
315 "struct",
316 "summarize",
317 "table",
318 "tablesample",
319 "temp",
320 "temporary",
321 "then",
322 "ties",
323 "to",
324 "top",
325 "trailing",
326 "transaction",
327 "trigger",
328 "truncate",
329 "unbounded",
330 "union",
331 "unique",
332 "unnest",
333 "unpivot",
334 "until",
335 "update",
336 "upsert",
337 "use",
338 "user",
339 "using",
340 "vacuum",
341 "values",
342 "view",
343 "virtual",
344 "volatile",
345 "warehouse",
346 "when",
347 "where",
348 "while",
349 "window",
350 "with",
351 "within",
352 "without",
353 "work",
354];
355
356const TYPES: &[&str] = &[
357 "bigint",
358 "bigserial",
359 "binary",
360 "bit",
361 "blob",
362 "bool",
363 "boolean",
364 "bpchar",
365 "bytea",
366 "bytes",
367 "char",
368 "character",
369 "cidr",
370 "clob",
371 "date",
372 "datetime",
373 "datetime2",
374 "dec",
375 "decimal",
376 "double",
377 "enum",
378 "float",
379 "float4",
380 "float8",
381 "float64",
382 "geography",
383 "geometry",
384 "hugeint",
385 "inet",
386 "int",
387 "int2",
388 "int4",
389 "int8",
390 "int64",
391 "integer",
392 "json",
393 "jsonb",
394 "list",
395 "long",
396 "map",
397 "mediumint",
398 "money",
399 "nchar",
400 "number",
401 "numeric",
402 "nvarchar",
403 "object",
404 "precision",
405 "real",
406 "serial",
407 "smallint",
408 "smallserial",
409 "string",
410 "text",
411 "time",
412 "timestamp",
413 "timestamp_ltz",
414 "timestamp_ntz",
415 "timestamp_tz",
416 "timestamptz",
417 "timetz",
418 "tinyint",
419 "ubigint",
420 "uinteger",
421 "usmallint",
422 "utinyint",
423 "uuid",
424 "varbinary",
425 "varchar",
426 "variant",
427 "xml",
428];
429
430const CONSTANTS: &[&str] = &["false", "null", "true", "unknown"];
431
432fn word_kinds() -> &'static HashMap<&'static str, TokenKind> {
434 static KINDS: OnceLock<HashMap<&'static str, TokenKind>> = OnceLock::new();
435 KINDS.get_or_init(|| {
436 let mut kinds = HashMap::new();
437 for (list, kind) in [
438 (KEYWORDS, TokenKind::Keyword),
439 (TYPES, TokenKind::Type),
440 (CONSTANTS, TokenKind::Constant),
441 ] {
442 for word in list {
443 kinds.insert(*word, kind);
444 }
445 }
446 kinds
447 })
448}
449
450fn word_kind(word: &str) -> Option<TokenKind> {
451 if word.len() > 24 {
452 return None;
453 }
454 word_kinds()
455 .get(word.to_ascii_lowercase().as_str())
456 .copied()
457}
458
459fn is_ident_start(ch: char) -> bool {
460 ch.is_alphabetic() || ch == '_'
461}
462
463fn is_ident_char(ch: char) -> bool {
464 ch.is_alphanumeric() || ch == '_' || ch == '$'
465}
466
467const OPERATOR_CHARS: &str = "+-*/%=<>!~^&|#";
468
469pub fn tokenize(code: &str) -> Vec<Token> {
492 Lexer {
493 code,
494 at: 0,
495 tokens: Vec::new(),
496 }
497 .run()
498}
499
500struct Lexer<'a> {
501 code: &'a str,
502 at: usize,
503 tokens: Vec<Token>,
504}
505
506impl Lexer<'_> {
507 fn rest(&self) -> &str {
508 &self.code[self.at..]
509 }
510
511 fn peek(&self) -> Option<char> {
512 self.rest().chars().next()
513 }
514
515 fn peek_at(&self, n: usize) -> Option<char> {
516 self.rest().chars().nth(n)
517 }
518
519 fn push(&mut self, kind: TokenKind, end: usize) {
520 let end = end.clamp(self.at, self.code.len());
521 if end > self.at {
522 self.tokens.push(Token {
523 kind,
524 range: self.at..end,
525 });
526 }
527 self.at = end;
528 }
529
530 fn until(&self, from: usize, close: &str) -> usize {
532 self.code
533 .get(from..)
534 .and_then(|rest| rest.find(close))
535 .map_or(self.code.len(), |found| from + found + close.len())
536 }
537
538 fn previous(&self) -> Option<&Token> {
540 self.tokens
541 .iter()
542 .rev()
543 .find(|t| !matches!(t.kind, TokenKind::Whitespace | TokenKind::Comment))
544 }
545
546 fn run(mut self) -> Vec<Token> {
547 while let Some(ch) = self.peek() {
548 let start = self.at;
549 match ch {
550 c if c.is_whitespace() => {
551 let len = self
552 .rest()
553 .find(|c: char| !c.is_whitespace())
554 .unwrap_or(self.rest().len());
555 self.push(TokenKind::Whitespace, start + len);
556 }
557 '-' if self.peek_at(1) == Some('-') => {
558 let end = self
559 .rest()
560 .find('\n')
561 .map_or(self.code.len(), |n| start + n);
562 self.push(TokenKind::Comment, end);
563 }
564 '/' if self.peek_at(1) == Some('*') => {
565 let end = self.block_comment();
566 self.push(TokenKind::Comment, end);
567 }
568 '{' if matches!(self.peek_at(1), Some('{' | '%' | '#')) => {
569 let close = match self.peek_at(1) {
570 Some('{') => "}}",
571 Some('%') => "%}",
572 _ => "#}",
573 };
574 let end = self.until(start + 2, close);
575 self.push(TokenKind::Template, end);
576 }
577 '\'' => {
578 let end = self.quoted_string(start, '\'', false);
579 self.push(TokenKind::String, end);
580 }
581 '"' => {
582 let body = start + 3;
583 if self.rest().starts_with("\"\"\"")
584 && !self.code[body..].starts_with('"')
585 && self.code[body..].contains("\"\"\"")
586 {
587 let end = self.until(body, "\"\"\"");
588 self.push(TokenKind::String, end);
589 } else {
590 let end = self.doubled(start, '"');
591 self.push(TokenKind::QuotedName, end);
592 }
593 }
594 '`' => {
595 let end = self.doubled(start, '`');
596 self.push(TokenKind::QuotedName, end);
597 }
598 '[' if self.bracket_is_name() => {
599 let end = self.doubled(start, ']');
600 self.push(TokenKind::QuotedName, end);
601 }
602 '$' => self.dollar(start),
603 '0'..='9' => {
604 let end = self.number(start);
605 self.push(TokenKind::Number, end);
606 }
607 '.' if self.peek_at(1).is_some_and(|c| c.is_ascii_digit())
608 && !self.previous().is_some_and(|t| {
609 matches!(t.kind, TokenKind::Name | TokenKind::QuotedName)
610 && t.range.end == start
611 }) =>
612 {
613 let end = self.number(start);
614 self.push(TokenKind::Number, end);
615 }
616 ':' if self.peek_at(1) == Some(':') => self.push(TokenKind::Operator, start + 2),
617 ':' if self.peek_at(1) == Some('=') => self.push(TokenKind::Operator, start + 2),
618 ':' if self.peek_at(1).is_some_and(is_ident_start) => {
619 let end = self.word_end(start + 1);
620 self.push(TokenKind::Parameter, end);
621 }
622 '@' if self
623 .peek_at(1)
624 .is_some_and(|c| is_ident_start(c) || c == '@') =>
625 {
626 let skip = if self.peek_at(1) == Some('@') { 2 } else { 1 };
627 let end = self.word_end(start + skip);
628 self.push(TokenKind::Parameter, end);
629 }
630 '?' => self.push(TokenKind::Parameter, start + 1),
631 c if is_ident_start(c) => self.word(start),
632 c if OPERATOR_CHARS.contains(c) || c == '@' => {
633 let end = self.operator(start);
634 self.push(TokenKind::Operator, end);
635 }
636 '(' | ')' | ',' | ';' | '.' | '[' | ']' | '{' | '}' | ':' => {
637 self.push(TokenKind::Punctuation, start + 1)
638 }
639 c => self.push(TokenKind::Other, start + c.len_utf8()),
640 }
641 }
642 self.tokens
643 }
644
645 fn block_comment(&self) -> usize {
647 let bytes = self.code.as_bytes();
648 let mut depth = 0usize;
649 let mut i = self.at;
650 while i + 1 < bytes.len() {
651 match (bytes[i], bytes[i + 1]) {
652 (b'/', b'*') => {
653 depth += 1;
654 i += 2;
655 }
656 (b'*', b'/') => {
657 depth -= 1;
658 i += 2;
659 if depth == 0 {
660 return i;
661 }
662 }
663 _ => i += 1,
664 }
665 }
666 self.code.len()
667 }
668
669 fn quoted_string(&self, start: usize, quote: char, backslash: bool) -> usize {
672 let open = self.code[start..].find(quote).map_or(start, |n| start + n);
673 let triple: String = std::iter::repeat_n(quote, 3).collect();
676 let body = open + 3;
677 if self.code[open..].starts_with(&triple)
678 && !self.code[body..].starts_with(quote)
679 && self.code[body..].contains(triple.as_str())
680 {
681 return self.until(body, &triple);
682 }
683 let mut chars = self.code[open + 1..].char_indices().peekable();
684 while let Some((i, ch)) = chars.next() {
685 if backslash && ch == '\\' {
686 chars.next();
687 } else if ch == quote {
688 if chars.peek().is_some_and(|&(_, next)| next == quote) {
689 chars.next();
690 } else {
691 return open + 1 + i + ch.len_utf8();
692 }
693 }
694 }
695 self.code.len()
696 }
697
698 fn doubled(&self, start: usize, close: char) -> usize {
700 let mut chars = self.code[start + 1..].char_indices().peekable();
701 while let Some((i, ch)) = chars.next() {
702 if ch == close {
703 if chars.peek().is_some_and(|&(_, next)| next == close) {
704 chars.next();
705 } else {
706 return start + 1 + i + ch.len_utf8();
707 }
708 }
709 }
710 self.code.len()
711 }
712
713 fn bracket_is_name(&self) -> bool {
716 let Some(previous) = self.previous() else {
717 return true;
718 };
719 let text = &self.code[previous.range.clone()];
720 let attached = previous.range.end == self.at;
721 match previous.kind {
722 TokenKind::Name | TokenKind::QuotedName | TokenKind::Function => !attached,
723 TokenKind::Punctuation => !matches!(text, ")" | "]"),
724 TokenKind::Keyword => !text.eq_ignore_ascii_case("array"),
725 TokenKind::Number | TokenKind::String | TokenKind::Parameter => false,
726 _ => true,
727 }
728 }
729
730 fn dollar(&mut self, start: usize) {
732 let after = &self.code[start + 1..];
733 if after.starts_with(|c: char| c.is_ascii_digit()) {
734 let len = after
735 .find(|c: char| !c.is_ascii_digit())
736 .unwrap_or(after.len());
737 self.push(TokenKind::Parameter, start + 1 + len);
738 return;
739 }
740 let tag_len = after
741 .find(|c: char| !(c.is_alphanumeric() || c == '_'))
742 .unwrap_or(after.len());
743 if after[tag_len..].starts_with('$') {
744 let delimiter = &self.code[start..start + tag_len + 2];
745 let end = self.until(start + delimiter.len(), delimiter);
746 self.push(TokenKind::String, end);
747 } else if tag_len > 0 {
748 self.push(TokenKind::Parameter, start + 1 + tag_len);
749 } else {
750 self.push(TokenKind::Operator, start + 1);
751 }
752 }
753
754 fn number(&self, start: usize) -> usize {
755 let bytes = self.code.as_bytes();
756 let mut i = start;
757 if bytes[i] == b'0' && matches!(bytes.get(i + 1), Some(b'x' | b'X')) {
758 i += 2;
759 while i < bytes.len() && (bytes[i].is_ascii_hexdigit() || bytes[i] == b'_') {
760 i += 1;
761 }
762 return i;
763 }
764 while i < bytes.len() && (bytes[i].is_ascii_digit() || bytes[i] == b'_') {
765 i += 1;
766 }
767 if i < bytes.len() && bytes[i] == b'.' && bytes.get(i + 1) != Some(&b'.') {
768 i += 1;
769 while i < bytes.len() && (bytes[i].is_ascii_digit() || bytes[i] == b'_') {
770 i += 1;
771 }
772 }
773 if i < bytes.len() && matches!(bytes[i], b'e' | b'E') {
774 let mut j = i + 1;
775 if j < bytes.len() && matches!(bytes[j], b'+' | b'-') {
776 j += 1;
777 }
778 if j < bytes.len() && bytes[j].is_ascii_digit() {
779 i = j;
780 while i < bytes.len() && bytes[i].is_ascii_digit() {
781 i += 1;
782 }
783 }
784 }
785 i
786 }
787
788 fn word_end(&self, from: usize) -> usize {
789 self.code[from..]
790 .find(|c: char| !is_ident_char(c))
791 .map_or(self.code.len(), |n| from + n)
792 }
793
794 fn word(&mut self, start: usize) {
795 let end = self.word_end(start);
796 let word = &self.code[start..end];
797 if self.code[end..].starts_with(['\'', '"']) && word.len() <= 2 {
800 let lower = word.to_ascii_lowercase();
801 let quote = self.code[end..].chars().next().unwrap_or('\'');
802 let string_prefix = matches!(lower.as_str(), "e" | "n" | "b" | "x" | "r" | "rb" | "br");
803 if string_prefix && (quote == '\'' || matches!(lower.as_str(), "r" | "b" | "rb" | "br"))
804 {
805 let backslash = lower == "e" || (lower.contains('b') && !lower.contains('r'));
806 let close = self.quoted_string(start, quote, backslash);
807 self.push(TokenKind::String, close);
808 return;
809 }
810 }
811 let after_dot = self.previous().is_some_and(|t| {
812 t.kind == TokenKind::Punctuation
813 && &self.code[t.range.clone()] == "."
814 && t.range.end == start
815 });
816 let call = self.code[end..]
817 .trim_start_matches([' ', '\t'])
818 .starts_with('(');
819 let kind = if after_dot {
820 if call {
821 TokenKind::Function
822 } else {
823 TokenKind::Name
824 }
825 } else if let Some(kind) = word_kind(word) {
826 kind
827 } else if call {
828 TokenKind::Function
829 } else {
830 TokenKind::Name
831 };
832 self.push(kind, end);
833 }
834
835 fn operator(&self, start: usize) -> usize {
837 let mut end = start;
838 for (i, ch) in self.code[start..].char_indices() {
839 let here = &self.code[start + i..];
840 if !(OPERATOR_CHARS.contains(ch) || (ch == '@' && i == 0))
841 || (i > 0 && (here.starts_with("--") || here.starts_with("/*")))
842 {
843 break;
844 }
845 end = start + i + ch.len_utf8();
846 }
847 end
848 }
849}
850
851pub fn token_style(kind: TokenKind, theme: &str) -> Option<Style> {
857 use TokenKind::*;
858 let markup = match theme {
859 "ansi_dark" | "ansi_light" => {
860 let dark = theme == "ansi_dark";
861 match (kind, dark) {
862 (Keyword | Constant, true) | (Number, true) => "bright_blue",
863 (Keyword | Constant, false) | (Number, false) => "blue",
864 (Type, true) | (Template, true) => "bright_cyan",
865 (Type, false) | (Template, false) => "cyan",
866 (Function, true) => "bright_green",
867 (Function, false) => "green",
868 (Parameter, true) => "bright_red",
869 (Parameter, false) => "red",
870 (String, _) => "yellow",
871 (Comment, _) => "dim",
872 (Other, _) => "red underline",
873 _ => return None,
874 }
875 }
876 _ => match kind {
877 Keyword | Type | Constant => "#66d9ef",
878 Function => "#a6e22e",
879 String => "#e6db74",
880 Number => "#ae81ff",
881 Comment => "#959077",
882 Operator => "#ff4689",
883 Parameter => "#fd971f",
884 Template => "italic #fd971f",
885 Other => "#ed007e on #1e0010",
886 _ => return None,
887 },
888 };
889 Style::parse(markup).ok()
890}
891
892fn theme_base(theme: &str) -> (Style, Option<Color>) {
894 match theme {
895 "ansi_dark" | "ansi_light" => (Style::new(), None),
896 _ => {
897 let background = Color::parse("#272822").ok();
898 let mut base = Style::parse("#f8f8f2").unwrap_or_default();
899 if let Some(background) = &background {
900 base = base.with_bgcolor(background.clone());
901 }
902 (base, background)
903 }
904 }
905}
906
907pub fn highlight(code: &str, theme: &str) -> Text {
916 let theme = known_theme(theme);
917 let (base, _) = theme_base(theme);
918 let mut text = Text::new(code);
919 if !base.is_null() {
920 text.set_base_style(base.clone());
921 }
922 for token in tokenize(code) {
923 if let Some(style) = token_style(token.kind, theme) {
924 text.stylize(base.combine(&style), token.range.start, token.range.end);
925 }
926 }
927 text
928}
929
930fn known_theme(theme: &str) -> &str {
931 if THEMES.contains(&theme) {
932 theme
933 } else {
934 DEFAULT_THEME
935 }
936}
937
938pub fn is_sql(language: &str) -> bool {
940 LANGUAGES
941 .iter()
942 .any(|name| name.eq_ignore_ascii_case(language))
943}
944
945#[derive(Clone, Default)]
952pub struct SqlHighlighter {
953 fallback: Option<Arc<dyn CodeHighlighter>>,
954}
955
956impl std::fmt::Debug for SqlHighlighter {
957 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
958 f.debug_struct("SqlHighlighter")
959 .field("fallback", &self.fallback.is_some())
960 .finish()
961 }
962}
963
964impl SqlHighlighter {
965 pub fn new() -> Self {
967 Self::default()
968 }
969
970 pub fn with_fallback(fallback: Arc<dyn CodeHighlighter>) -> Self {
982 SqlHighlighter {
983 fallback: Some(fallback),
984 }
985 }
986
987 pub fn shared() -> Arc<dyn CodeHighlighter> {
989 static SHARED: OnceLock<Arc<SqlHighlighter>> = OnceLock::new();
990 SHARED
991 .get_or_init(|| Arc::new(SqlHighlighter::new()))
992 .clone()
993 }
994}
995
996impl CodeHighlighter for SqlHighlighter {
997 fn highlight(
998 &self,
999 code: &str,
1000 language: Option<&str>,
1001 theme: &str,
1002 ) -> Result<HighlightedCode, HighlightError> {
1003 if !language.is_some_and(is_sql) {
1004 return match &self.fallback {
1005 Some(fallback) => fallback.highlight(code, language, theme),
1006 None => {
1007 let (default_style, background) = theme_base(known_theme(theme));
1008 Ok(HighlightedCode {
1009 lines: vec![HighlightedLine::default(); code.split('\n').count()],
1010 background,
1011 default_style,
1012 })
1013 }
1014 };
1015 }
1016 let theme = known_theme(theme);
1017 let (default_style, background) = theme_base(theme);
1018 let mut lines = Lines::new(code);
1019 for token in tokenize(code) {
1020 if let Some(style) = token_style(token.kind, theme) {
1021 lines.push(token.range, &default_style.combine(&style));
1022 }
1023 }
1024 Ok(HighlightedCode {
1025 lines: lines.lines,
1026 background,
1027 default_style,
1028 })
1029 }
1030
1031 fn default_theme(&self) -> &str {
1032 DEFAULT_THEME
1033 }
1034
1035 fn themes(&self) -> Vec<String> {
1036 let mut names: Vec<String> = THEMES.iter().map(|name| name.to_string()).collect();
1037 if let Some(fallback) = &self.fallback {
1038 names.extend(fallback.themes());
1039 names.sort();
1040 names.dedup();
1041 }
1042 names
1043 }
1044
1045 fn languages(&self) -> Vec<String> {
1046 let mut names: Vec<String> = LANGUAGES.iter().map(|name| name.to_string()).collect();
1047 if let Some(fallback) = &self.fallback {
1048 names.extend(fallback.languages());
1049 names.sort();
1050 names.dedup();
1051 }
1052 names
1053 }
1054
1055 fn language_for_path(&self, path: &Path) -> Option<String> {
1056 let sql = path
1057 .extension()
1058 .and_then(|ext| ext.to_str())
1059 .is_some_and(|ext| ext.eq_ignore_ascii_case("sql"));
1060 if sql {
1061 return Some("sql".to_string());
1062 }
1063 self.fallback.as_ref()?.language_for_path(path)
1064 }
1065
1066 fn token_style(&self, theme: &str, token: &str) -> Option<Style> {
1067 if !THEMES.contains(&theme)
1068 && let Some(fallback) = &self.fallback
1069 {
1070 return fallback.token_style(theme, token);
1071 }
1072 let theme = known_theme(theme);
1073 match token {
1074 "Comment" => token_style(TokenKind::Comment, theme),
1075 "Text" => Some(theme_base(theme).0).filter(|style| !style.is_null()),
1076 _ => None,
1077 }
1078 }
1079}
1080
1081struct Lines {
1083 starts: Vec<usize>,
1085 len: usize,
1086 lines: Vec<HighlightedLine>,
1087}
1088
1089impl Lines {
1090 fn new(code: &str) -> Self {
1091 let mut starts = vec![0];
1092 starts.extend(code.match_indices('\n').map(|(at, _)| at + 1));
1093 let lines = vec![HighlightedLine::default(); starts.len()];
1094 Lines {
1095 starts,
1096 len: code.len(),
1097 lines,
1098 }
1099 }
1100
1101 fn line_of(&self, at: usize) -> usize {
1102 self.starts.partition_point(|&start| start <= at) - 1
1103 }
1104
1105 fn push(&mut self, range: Range<usize>, style: &Style) {
1107 if range.is_empty() || range.end > self.len {
1108 return;
1109 }
1110 for line in self.line_of(range.start)..=self.line_of(range.end - 1) {
1111 let begin = self.starts[line];
1112 let end = self.starts.get(line + 1).map_or(self.len, |next| next - 1);
1113 let (from, to) = (range.start.max(begin), range.end.min(end));
1114 if from < to {
1115 self.lines[line].spans.push(HighlightSpan {
1116 range: from - begin..to - begin,
1117 style: style.clone(),
1118 });
1119 }
1120 if end < self.len && range.start <= end && end < range.end {
1121 self.lines[line].newline_style = Some(style.clone());
1122 }
1123 }
1124 }
1125}
1126
1127#[derive(Clone, Copy, Debug, Default)]
1129pub struct SqlPlugin;
1130
1131impl Plugin for SqlPlugin {
1132 fn metadata(&self) -> PluginMetadata {
1133 PluginMetadata::new("sql", "SQL highlighter", env!("CARGO_PKG_VERSION"))
1134 .description("a pure-Rust SQL lexer with Jinja blocks, for SQL code")
1135 }
1136
1137 fn register(&self, registrar: &mut dyn PluginRegistrar) -> Result<(), PluginError> {
1138 registrar.code_highlighter("sql", SqlHighlighter::shared());
1139 Ok(())
1140 }
1141}
1142
1143#[cfg(test)]
1144mod tests;