1use std::ops::Range;
21
22use gpui::Hsla;
23
24use crate::theme::{ColorName, Theme};
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
28pub enum TokenKind {
29 Keyword,
30 Ident,
31 Number,
32 StringLit,
33 Comment,
34 Punct,
35 Type,
36 Function,
37}
38
39impl TokenKind {
40 pub const ALL: [TokenKind; 8] = [
43 TokenKind::Keyword,
44 TokenKind::Ident,
45 TokenKind::Number,
46 TokenKind::StringLit,
47 TokenKind::Comment,
48 TokenKind::Punct,
49 TokenKind::Type,
50 TokenKind::Function,
51 ];
52
53 pub fn index(self) -> usize {
55 self as usize
56 }
57}
58
59#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
62pub struct LineState {
63 block_depth: u32,
64}
65
66pub trait Highlighter {
69 fn line(&self, text: &str, state: &mut LineState) -> Vec<(Range<usize>, TokenKind)>;
73}
74
75#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
77pub enum Language {
78 #[default]
80 None,
81 Rust,
82 Sql,
83 Json,
84}
85
86impl Highlighter for Language {
87 fn line(&self, text: &str, state: &mut LineState) -> Vec<(Range<usize>, TokenKind)> {
88 match self {
89 Language::None => Vec::new(),
90 Language::Rust => tokenize(&RUST, text, state),
91 Language::Sql => tokenize(&SQL, text, state),
92 Language::Json => tokenize(&JSON, text, state),
93 }
94 }
95}
96
97pub fn token_color(kind: TokenKind, t: &Theme) -> Hsla {
100 let shade = if t.scheme.is_dark() { 4 } else { 7 };
101 match kind {
102 TokenKind::Keyword => t.color(ColorName::Violet, shade).hsla(),
103 TokenKind::StringLit => t.color(ColorName::Green, shade).hsla(),
104 TokenKind::Number => t.color(ColorName::Orange, shade).hsla(),
105 TokenKind::Type => t.color(ColorName::Teal, shade).hsla(),
106 TokenKind::Function => t.color(ColorName::Blue, shade).hsla(),
107 TokenKind::Comment => t.dimmed().hsla(),
108 TokenKind::Ident | TokenKind::Punct => t.text().hsla(),
109 }
110}
111
112#[derive(Clone, Copy)]
116enum Escape {
117 Backslash,
119 Doubled,
121}
122
123struct Syntax {
125 line_comment: Option<&'static str>,
126 block_comment: Option<(&'static str, &'static str)>,
127 nested_blocks: bool,
129 strings: &'static [(char, Escape)],
130 keywords: &'static [&'static str],
131 types: &'static [&'static str],
132 case_insensitive: bool,
134 uppercase_types: bool,
136}
137
138const RUST: Syntax = Syntax {
139 line_comment: Some("//"),
140 block_comment: Some(("/*", "*/")),
141 nested_blocks: true,
142 strings: &[('"', Escape::Backslash)],
143 keywords: &[
144 "as", "async", "await", "break", "const", "continue", "crate", "dyn", "else", "enum",
145 "extern", "false", "fn", "for", "if", "impl", "in", "let", "loop", "match", "mod", "move",
146 "mut", "pub", "ref", "return", "self", "Self", "static", "struct", "super", "trait",
147 "true", "type", "union", "unsafe", "use", "where", "while",
148 ],
149 types: &[],
150 case_insensitive: false,
151 uppercase_types: true,
152};
153
154const SQL: Syntax = Syntax {
155 line_comment: Some("--"),
156 block_comment: Some(("/*", "*/")),
157 nested_blocks: false,
158 strings: &[('\'', Escape::Doubled)],
159 keywords: &[
160 "add",
161 "all",
162 "alter",
163 "and",
164 "as",
165 "asc",
166 "begin",
167 "between",
168 "by",
169 "case",
170 "cast",
171 "check",
172 "column",
173 "commit",
174 "constraint",
175 "create",
176 "cross",
177 "default",
178 "delete",
179 "desc",
180 "distinct",
181 "drop",
182 "else",
183 "end",
184 "exists",
185 "false",
186 "foreign",
187 "from",
188 "full",
189 "group",
190 "having",
191 "if",
192 "in",
193 "index",
194 "inner",
195 "insert",
196 "into",
197 "is",
198 "join",
199 "key",
200 "left",
201 "like",
202 "limit",
203 "not",
204 "null",
205 "offset",
206 "on",
207 "or",
208 "order",
209 "outer",
210 "primary",
211 "references",
212 "replace",
213 "returning",
214 "right",
215 "rollback",
216 "select",
217 "set",
218 "table",
219 "then",
220 "transaction",
221 "true",
222 "union",
223 "unique",
224 "update",
225 "values",
226 "view",
227 "when",
228 "where",
229 "with",
230 ],
231 types: &[
232 "bigint",
233 "blob",
234 "bool",
235 "boolean",
236 "bytea",
237 "char",
238 "date",
239 "decimal",
240 "double",
241 "float",
242 "int",
243 "integer",
244 "interval",
245 "json",
246 "jsonb",
247 "numeric",
248 "real",
249 "serial",
250 "smallint",
251 "text",
252 "time",
253 "timestamp",
254 "timestamptz",
255 "uuid",
256 "varchar",
257 ],
258 case_insensitive: true,
259 uppercase_types: false,
260};
261
262const JSON: Syntax = Syntax {
263 line_comment: None,
264 block_comment: None,
265 nested_blocks: false,
266 strings: &[('"', Escape::Backslash)],
267 keywords: &["false", "null", "true"],
268 types: &[],
269 case_insensitive: false,
270 uppercase_types: false,
271};
272
273fn tokenize(syntax: &Syntax, text: &str, state: &mut LineState) -> Vec<(Range<usize>, TokenKind)> {
276 let chars: Vec<(usize, char)> = text.char_indices().collect();
277 let n = chars.len();
278 let byte_at = |i: usize| chars.get(i).map(|&(b, _)| b).unwrap_or(text.len());
279 let mut out: Vec<(Range<usize>, TokenKind)> = Vec::new();
280 let mut i = 0;
281
282 if state.block_depth > 0 {
284 match syntax.block_comment {
285 Some((open, close)) => {
286 let (end, depth) = scan_block(
287 &chars,
288 0,
289 open,
290 close,
291 syntax.nested_blocks,
292 state.block_depth,
293 );
294 state.block_depth = depth;
295 if byte_at(end) > 0 {
296 out.push((0..byte_at(end), TokenKind::Comment));
297 }
298 i = end;
299 }
300 None => state.block_depth = 0,
302 }
303 }
304
305 while i < n {
306 let (b, c) = chars[i];
307 if c.is_whitespace() {
308 i += 1;
309 continue;
310 }
311 if let Some(lc) = syntax.line_comment {
312 if starts_with_at(&chars, i, lc) {
313 out.push((b..text.len(), TokenKind::Comment));
314 break;
315 }
316 }
317 if let Some((open, close)) = syntax.block_comment {
318 if starts_with_at(&chars, i, open) {
319 let after_open = i + open.chars().count();
320 let (end, depth) =
321 scan_block(&chars, after_open, open, close, syntax.nested_blocks, 1);
322 state.block_depth = depth;
323 out.push((b..byte_at(end), TokenKind::Comment));
324 i = end;
325 continue;
326 }
327 }
328 if let Some(&(_, esc)) = syntax.strings.iter().find(|&&(q, _)| q == c) {
329 let end = scan_string(&chars, i + 1, c, esc);
330 out.push((b..byte_at(end), TokenKind::StringLit));
331 i = end;
332 continue;
333 }
334 if c.is_ascii_digit() {
335 let end = scan_number(&chars, i);
336 out.push((b..byte_at(end), TokenKind::Number));
337 i = end;
338 continue;
339 }
340 if c.is_alphabetic() || c == '_' {
341 let end = scan_ident(&chars, i);
342 let word = &text[b..byte_at(end)];
343 out.push((b..byte_at(end), classify_word(syntax, word, &chars, end)));
344 i = end;
345 continue;
346 }
347 out.push((b..byte_at(i + 1), TokenKind::Punct));
348 i += 1;
349 }
350
351 coalesce(out)
352}
353
354fn starts_with_at(chars: &[(usize, char)], i: usize, pat: &str) -> bool {
356 let mut j = i;
357 for p in pat.chars() {
358 match chars.get(j) {
359 Some(&(_, c)) if c == p => j += 1,
360 _ => return false,
361 }
362 }
363 true
364}
365
366fn scan_block(
370 chars: &[(usize, char)],
371 mut i: usize,
372 open: &str,
373 close: &str,
374 nested: bool,
375 mut depth: u32,
376) -> (usize, u32) {
377 let n = chars.len();
378 while i < n {
379 if nested && starts_with_at(chars, i, open) {
380 depth += 1;
381 i += open.chars().count();
382 } else if starts_with_at(chars, i, close) {
383 depth -= 1;
384 i += close.chars().count();
385 if depth == 0 {
386 return (i, 0);
387 }
388 } else {
389 i += 1;
390 }
391 }
392 (n, depth)
393}
394
395fn scan_string(chars: &[(usize, char)], mut i: usize, quote: char, esc: Escape) -> usize {
399 let n = chars.len();
400 while i < n {
401 let c = chars[i].1;
402 match esc {
403 Escape::Backslash if c == '\\' => {
404 i += 2;
405 continue;
406 }
407 Escape::Doubled if c == quote => {
408 if i + 1 < n && chars[i + 1].1 == quote {
409 i += 2;
410 continue;
411 }
412 return i + 1;
413 }
414 _ if c == quote => return i + 1,
415 _ => i += 1,
416 }
417 }
418 n
419}
420
421fn scan_number(chars: &[(usize, char)], mut i: usize) -> usize {
424 let n = chars.len();
425 if chars[i].1 == '0' && i + 1 < n && matches!(chars[i + 1].1, 'x' | 'X' | 'b' | 'B' | 'o' | 'O')
426 {
427 i += 2;
428 while i < n && (chars[i].1.is_ascii_alphanumeric() || chars[i].1 == '_') {
429 i += 1;
430 }
431 return i;
432 }
433 while i < n && (chars[i].1.is_ascii_digit() || chars[i].1 == '_') {
434 i += 1;
435 }
436 if i + 1 < n && chars[i].1 == '.' && chars[i + 1].1.is_ascii_digit() {
437 i += 1;
438 while i < n && (chars[i].1.is_ascii_digit() || chars[i].1 == '_') {
439 i += 1;
440 }
441 }
442 if i < n && matches!(chars[i].1, 'e' | 'E') {
443 let mut j = i + 1;
444 if j < n && matches!(chars[j].1, '+' | '-') {
445 j += 1;
446 }
447 if j < n && chars[j].1.is_ascii_digit() {
448 i = j;
449 while i < n && chars[i].1.is_ascii_digit() {
450 i += 1;
451 }
452 }
453 }
454 while i < n && (chars[i].1.is_ascii_alphanumeric() || chars[i].1 == '_') {
455 i += 1;
456 }
457 i
458}
459
460fn scan_ident(chars: &[(usize, char)], mut i: usize) -> usize {
462 let n = chars.len();
463 while i < n && (chars[i].1.is_alphanumeric() || chars[i].1 == '_') {
464 i += 1;
465 }
466 i
467}
468
469fn classify_word(syntax: &Syntax, word: &str, chars: &[(usize, char)], end: usize) -> TokenKind {
472 let in_set = |set: &[&str]| {
473 if syntax.case_insensitive {
474 set.iter().any(|k| k.eq_ignore_ascii_case(word))
475 } else {
476 set.contains(&word)
477 }
478 };
479 if in_set(syntax.keywords) {
480 return TokenKind::Keyword;
481 }
482 if in_set(syntax.types) {
483 return TokenKind::Type;
484 }
485 if syntax.uppercase_types && word.chars().next().is_some_and(char::is_uppercase) {
486 return TokenKind::Type;
487 }
488 match chars.get(end).map(|&(_, c)| c) {
490 Some('(') => TokenKind::Function,
491 Some('!') if matches!(chars.get(end + 1), Some(&(_, '('))) => TokenKind::Function,
492 _ => TokenKind::Ident,
493 }
494}
495
496fn coalesce(tokens: Vec<(Range<usize>, TokenKind)>) -> Vec<(Range<usize>, TokenKind)> {
499 let mut out: Vec<(Range<usize>, TokenKind)> = Vec::new();
500 for (range, kind) in tokens {
501 if let Some((last, last_kind)) = out.last_mut() {
502 if *last_kind == kind && last.end == range.start {
503 last.end = range.end;
504 continue;
505 }
506 }
507 out.push((range, kind));
508 }
509 out
510}
511
512#[cfg(test)]
513mod tests {
514 use super::*;
515
516 fn kinds(lang: Language, line: &str) -> Vec<(String, TokenKind)> {
517 let mut state = LineState::default();
518 lang.line(line, &mut state)
519 .into_iter()
520 .map(|(r, k)| (line[r].to_string(), k))
521 .collect()
522 }
523
524 fn kind_of(lang: Language, line: &str, word: &str) -> TokenKind {
525 kinds(lang, line)
526 .into_iter()
527 .find(|(w, _)| w == word)
528 .map(|(_, k)| k)
529 .unwrap_or_else(|| panic!("token {word:?} not found in {line:?}"))
530 }
531
532 #[test]
533 fn none_language_emits_nothing() {
534 assert!(kinds(Language::None, "let x = 1;").is_empty());
535 }
536
537 #[test]
538 fn ranges_are_ascending_and_in_bounds() {
539 let line = "let s = \"héllo\"; // café";
540 let mut state = LineState::default();
541 let tokens = Language::Rust.line(line, &mut state);
542 let mut at = 0;
543 for (range, _) in &tokens {
544 assert!(range.start >= at, "overlapping range");
545 assert!(range.end <= line.len());
546 assert!(line.is_char_boundary(range.start));
547 assert!(line.is_char_boundary(range.end));
548 at = range.end;
549 }
550 }
551
552 #[test]
553 fn rust_basics() {
554 assert_eq!(
555 kind_of(Language::Rust, "let x = 1;", "let"),
556 TokenKind::Keyword
557 );
558 assert_eq!(kind_of(Language::Rust, "let x = 1;", "x"), TokenKind::Ident);
559 assert_eq!(
560 kind_of(Language::Rust, "let x = 10.5e3;", "10.5e3"),
561 TokenKind::Number
562 );
563 assert_eq!(
564 kind_of(Language::Rust, "let n = 0xff_u8;", "0xff_u8"),
565 TokenKind::Number
566 );
567 assert_eq!(
568 kind_of(
569 Language::Rust,
570 r#"let s = "hi \" there";"#,
571 r#""hi \" there""#
572 ),
573 TokenKind::StringLit
574 );
575 assert_eq!(
576 kind_of(Language::Rust, "let v: Vec<u8>;", "Vec"),
577 TokenKind::Type
578 );
579 assert_eq!(
580 kind_of(Language::Rust, "foo(1)", "foo"),
581 TokenKind::Function
582 );
583 assert_eq!(
584 kind_of(Language::Rust, "println!(\"x\")", "println"),
585 TokenKind::Function
586 );
587 assert_eq!(
588 kind_of(Language::Rust, "a + b // sum", "// sum"),
589 TokenKind::Comment
590 );
591 }
592
593 #[test]
594 fn rust_block_comment_carries_state() {
595 let mut state = LineState::default();
596 let t1 = Language::Rust.line("start /* open", &mut state);
597 assert_eq!(
598 t1.last()
599 .map(|(r, k)| ("start /* open"[r.clone()].to_string(), *k)),
600 Some(("/* open".to_string(), TokenKind::Comment))
601 );
602 let t2 = Language::Rust.line("all comment", &mut state);
603 assert_eq!(t2, vec![(0..11, TokenKind::Comment)]);
604 let t3 = Language::Rust.line("done */ let x", &mut state);
605 assert_eq!(t3[0], (0..7, TokenKind::Comment));
606 assert_eq!("done */ let x"[t3[1].clone().0].to_string(), "let");
607 assert_eq!(state, LineState::default());
608 }
609
610 #[test]
611 fn rust_block_comments_nest() {
612 let mut state = LineState::default();
613 Language::Rust.line("/* a /* b */ still", &mut state);
614 assert_ne!(state, LineState::default());
615 let t = Language::Rust.line("c */ code", &mut state);
616 assert_eq!(t[0], (0..4, TokenKind::Comment));
617 assert_eq!(state, LineState::default());
618 }
619
620 #[test]
621 fn sql_keywords_are_case_insensitive() {
622 assert_eq!(
623 kind_of(Language::Sql, "SELECT * FROM users;", "SELECT"),
624 TokenKind::Keyword
625 );
626 assert_eq!(
627 kind_of(Language::Sql, "select * from users;", "select"),
628 TokenKind::Keyword
629 );
630 assert_eq!(
631 kind_of(Language::Sql, "id INT PRIMARY KEY", "INT"),
632 TokenKind::Type
633 );
634 assert_eq!(
635 kind_of(Language::Sql, "count(*)", "count"),
636 TokenKind::Function
637 );
638 }
639
640 #[test]
641 fn sql_comments_and_strings() {
642 assert_eq!(
643 kind_of(Language::Sql, "x -- note", "-- note"),
644 TokenKind::Comment
645 );
646 assert_eq!(
647 kind_of(Language::Sql, "name = 'it''s'", "'it''s'"),
648 TokenKind::StringLit
649 );
650 let mut state = LineState::default();
651 Language::Sql.line("/* multi", &mut state);
652 let t = Language::Sql.line("line */ SELECT", &mut state);
653 assert_eq!(t[0], (0..7, TokenKind::Comment));
654 assert_eq!(state, LineState::default());
655 }
656
657 #[test]
658 fn sql_blocks_do_not_nest() {
659 let mut state = LineState::default();
660 Language::Sql.line("/* a /* b */ tail", &mut state);
661 assert_eq!(state, LineState::default());
663 }
664
665 #[test]
666 fn json_tokens() {
667 let line = r#"{"key": [1.5, true, null, "value"]}"#;
668 assert_eq!(
669 kind_of(Language::Json, line, r#""key""#),
670 TokenKind::StringLit
671 );
672 assert_eq!(kind_of(Language::Json, line, "1.5"), TokenKind::Number);
673 assert_eq!(kind_of(Language::Json, line, "true"), TokenKind::Keyword);
674 assert_eq!(kind_of(Language::Json, line, "null"), TokenKind::Keyword);
675 }
676
677 #[test]
678 fn multibyte_idents_and_strings() {
679 let line = "let café = \"日本語\"; // été";
680 assert_eq!(kind_of(Language::Rust, line, "café"), TokenKind::Ident);
681 assert_eq!(
682 kind_of(Language::Rust, line, "\"日本語\""),
683 TokenKind::StringLit
684 );
685 assert_eq!(kind_of(Language::Rust, line, "// été"), TokenKind::Comment);
686 }
687
688 #[test]
689 fn unterminated_string_stops_at_line_end() {
690 let line = "let s = \"open";
691 assert_eq!(
692 kind_of(Language::Rust, line, "\"open"),
693 TokenKind::StringLit
694 );
695 let mut state = LineState::default();
697 Language::Rust.line(line, &mut state);
698 assert_eq!(state, LineState::default());
699 }
700
701 #[test]
702 fn punctuation_coalesces() {
703 let tokens = kinds(Language::Rust, "a->b");
704 assert_eq!(
705 tokens,
706 vec![
707 ("a".to_string(), TokenKind::Ident),
708 ("->".to_string(), TokenKind::Punct),
709 ("b".to_string(), TokenKind::Ident),
710 ]
711 );
712 }
713
714 #[test]
715 fn empty_line_inside_block_comment() {
716 let mut state = LineState::default();
717 Language::Rust.line("/* open", &mut state);
718 let t = Language::Rust.line("", &mut state);
719 assert!(t.is_empty());
720 assert_ne!(state, LineState::default());
721 }
722}