1#![forbid(unsafe_code)]
29#![allow(
36 clippy::many_single_char_names,
37 clippy::cast_possible_truncation,
38 clippy::doc_markdown,
39 clippy::single_match_else,
40 clippy::collapsible_if,
41 clippy::match_same_arms
42)]
43
44#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
50pub struct ByteSpan {
51 pub start: u32,
52 pub end: u32,
53}
54
55impl ByteSpan {
56 #[must_use]
62 pub fn new(start: u32, end: u32) -> Self {
63 assert!(start <= end, "ByteSpan::new: start {start} > end {end}");
64 Self { start, end }
65 }
66
67 #[must_use]
68 pub fn len(&self) -> u32 {
69 self.end - self.start
70 }
71
72 #[must_use]
73 pub fn is_empty(&self) -> bool {
74 self.start == self.end
75 }
76
77 #[must_use]
79 pub fn range(&self) -> std::ops::Range<usize> {
80 self.start as usize..self.end as usize
81 }
82}
83
84#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
90pub enum HlClass {
91 Comment {
92 multiline: bool,
93 },
94 Keyword,
95 KeywordArg,
97 Type,
98 Function,
99 Namespace,
100 Variable,
101 Constant,
102 Str,
103 Escape,
104 Numeric {
105 float: bool,
106 },
107 Boolean,
108 Punctuation,
109 Operator,
110 Attribute,
111 Special,
112 Hyperlink,
113 Whitespace,
114 Error,
115 Warning,
117 Info,
118 Hint,
119 Added,
121 Removed,
122 Unchanged,
124 Plain,
126}
127
128#[derive(Clone, Copy, PartialEq, Eq, Debug)]
130pub struct HighlightSpan {
131 pub span: ByteSpan,
132 pub class: HlClass,
133}
134
135pub struct SpanSink {
145 cursor: u32,
146 line_end: u32,
147 out: Vec<HighlightSpan>,
148}
149
150impl SpanSink {
151 #[must_use]
156 pub fn new(line_start: u32, line_len: u32) -> Self {
157 Self {
158 cursor: line_start,
159 line_end: line_start + line_len,
160 out: Vec::new(),
161 }
162 }
163
164 #[must_use]
167 pub fn for_document(len: u32) -> Self {
168 Self::new(0, len)
169 }
170
171 pub fn push(&mut self, start: u32, end: u32, class: HlClass) {
175 let start = start.max(self.cursor);
176 let end = end.min(self.line_end);
177 if end <= start {
178 return;
179 }
180 if start > self.cursor {
181 self.out.push(HighlightSpan {
182 span: ByteSpan::new(self.cursor, start),
183 class: HlClass::Plain,
184 });
185 }
186 self.out.push(HighlightSpan {
187 span: ByteSpan::new(start, end),
188 class,
189 });
190 self.cursor = end;
191 }
192
193 #[must_use]
196 pub fn finish(mut self) -> Vec<HighlightSpan> {
197 if self.cursor < self.line_end {
198 self.out.push(HighlightSpan {
199 span: ByteSpan::new(self.cursor, self.line_end),
200 class: HlClass::Plain,
201 });
202 }
203 self.out
204 }
205}
206
207pub trait LanguageLexer: Send + Sync {
215 type LineState: Copy + Eq + Default + Send + Sync;
216
217 fn lex_line(
218 &self,
219 line: &str,
220 line_start: u32,
221 entry: Self::LineState,
222 sink: &mut SpanSink,
223 ) -> Self::LineState;
224}
225
226pub trait Highlighter: Send + Sync {
230 fn highlight(&self, text: &str) -> Vec<HighlightSpan>;
231}
232
233pub struct LineDriven<L: LanguageLexer> {
236 pub lexer: L,
237}
238
239impl<L: LanguageLexer> LineDriven<L> {
240 #[must_use]
241 pub fn new(lexer: L) -> Self {
242 Self { lexer }
243 }
244}
245
246impl<L: LanguageLexer> Highlighter for LineDriven<L> {
247 fn highlight(&self, text: &str) -> Vec<HighlightSpan> {
248 let mut out = Vec::new();
249 let mut state = L::LineState::default();
250 let mut offset: u32 = 0;
251 for line in text.split_inclusive('\n') {
254 let line_len = u32::try_from(line.len()).unwrap_or(u32::MAX);
255 let mut sink = SpanSink::new(offset, line_len);
256 state = self.lexer.lex_line(line, offset, state, &mut sink);
257 out.extend(sink.finish());
258 offset = offset.saturating_add(line_len);
259 }
260 out
261 }
262}
263
264pub trait IncrementalHighlighter: Send + Sync {
274 fn highlight(&mut self, text: &str) -> Vec<HighlightSpan>;
277
278 fn last_relexed(&self) -> usize;
282}
283
284struct CachedLine<S> {
289 entry: S,
290 exit: S,
291 text: Box<str>,
292 rel_spans: Vec<HighlightSpan>,
293}
294
295pub struct LineCache<L: LanguageLexer> {
306 lexer: L,
307 lines: Vec<CachedLine<L::LineState>>,
308 last_relexed: usize,
309}
310
311impl<L: LanguageLexer> LineCache<L> {
312 #[must_use]
313 pub fn new(lexer: L) -> Self {
314 Self {
315 lexer,
316 lines: Vec::new(),
317 last_relexed: 0,
318 }
319 }
320
321 fn lex_relative(&self, line: &str, entry: L::LineState) -> (L::LineState, Vec<HighlightSpan>) {
323 let line_len = u32::try_from(line.len()).unwrap_or(u32::MAX);
324 let mut sink = SpanSink::new(0, line_len);
325 let exit = self.lexer.lex_line(line, 0, entry, &mut sink);
326 (exit, sink.finish())
327 }
328}
329
330impl<L: LanguageLexer> IncrementalHighlighter for LineCache<L> {
331 fn highlight(&mut self, text: &str) -> Vec<HighlightSpan> {
332 let mut next: Vec<CachedLine<L::LineState>> = Vec::new();
333 let mut out: Vec<HighlightSpan> = Vec::new();
334 let mut entry = L::LineState::default();
335 let mut offset: u32 = 0;
336 let mut relexed = 0usize;
337
338 for (i, line) in text.split_inclusive('\n').enumerate() {
339 let line_len = u32::try_from(line.len()).unwrap_or(u32::MAX);
340 let (exit, rel_spans) = match self.lines.get(i) {
345 Some(c) if c.entry == entry && &*c.text == line => (c.exit, c.rel_spans.clone()),
346 _ => {
347 relexed += 1;
348 self.lex_relative(line, entry)
349 }
350 };
351
352 for s in &rel_spans {
354 out.push(HighlightSpan {
355 span: ByteSpan::new(offset + s.span.start, offset + s.span.end),
356 class: s.class,
357 });
358 }
359 next.push(CachedLine {
360 entry,
361 exit,
362 text: line.into(),
363 rel_spans,
364 });
365 entry = exit;
366 offset = offset.saturating_add(line_len);
367 }
368
369 self.lines = next;
370 self.last_relexed = relexed;
371 out
372 }
373
374 fn last_relexed(&self) -> usize {
375 self.last_relexed
376 }
377}
378
379pub struct WholeReHighlighter {
384 inner: Box<dyn Highlighter>,
385 last_relexed: usize,
386}
387
388impl WholeReHighlighter {
389 #[must_use]
390 pub fn new(inner: Box<dyn Highlighter>) -> Self {
391 Self {
392 inner,
393 last_relexed: 0,
394 }
395 }
396}
397
398impl IncrementalHighlighter for WholeReHighlighter {
399 fn highlight(&mut self, text: &str) -> Vec<HighlightSpan> {
400 self.last_relexed = text.split_inclusive('\n').count();
401 self.inner.highlight(text)
402 }
403 fn last_relexed(&self) -> usize {
404 self.last_relexed
405 }
406}
407
408#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
413pub struct Language(pub &'static str);
414
415pub const PLAIN_TEXT: Language = Language("plaintext");
418
419#[derive(Clone, Copy, PartialEq, Eq, Debug)]
421pub enum Selector {
422 Extension(&'static str),
424 Filename(&'static str),
426}
427
428pub trait LanguagePlugin: Send + Sync {
430 fn language(&self) -> Language;
431 fn selectors(&self) -> &'static [Selector];
432 fn make_highlighter(&self) -> Box<dyn Highlighter>;
433
434 fn make_incremental(&self) -> Box<dyn IncrementalHighlighter> {
440 Box::new(WholeReHighlighter::new(self.make_highlighter()))
441 }
442}
443
444pub struct Ecosystem {
447 plugins: Vec<Box<dyn LanguagePlugin>>,
448}
449
450impl Default for Ecosystem {
451 fn default() -> Self {
452 Self::with_builtins()
453 }
454}
455
456impl Ecosystem {
457 #[must_use]
459 pub fn new() -> Self {
460 Self {
461 plugins: Vec::new(),
462 }
463 }
464
465 #[must_use]
467 pub fn with_builtins() -> Self {
468 let mut eco = Self::new();
469 for p in langs::builtins() {
470 eco.plugins.push(p);
471 }
472 eco
473 }
474
475 pub fn register(&mut self, plugin: Box<dyn LanguagePlugin>) {
476 self.plugins.push(plugin);
477 }
478
479 #[must_use]
481 pub fn languages(&self) -> Vec<Language> {
482 self.plugins.iter().map(|p| p.language()).collect()
483 }
484
485 #[must_use]
488 pub fn resolve(&self, path: &str) -> Language {
489 let name = path.rsplit(['/', '\\']).next().unwrap_or(path);
490 for p in &self.plugins {
491 for sel in p.selectors() {
492 if let Selector::Filename(f) = sel {
493 if name.eq_ignore_ascii_case(f) {
494 return p.language();
495 }
496 }
497 }
498 }
499 if let Some(ext) = name.rsplit_once('.').map(|(_, e)| e) {
500 for p in &self.plugins {
501 for sel in p.selectors() {
502 if let Selector::Extension(e) = sel {
503 if ext.eq_ignore_ascii_case(e) {
504 return p.language();
505 }
506 }
507 }
508 }
509 }
510 PLAIN_TEXT
511 }
512
513 #[must_use]
516 pub fn highlighter_for(&self, lang: Language) -> Box<dyn Highlighter> {
517 for p in &self.plugins {
518 if p.language() == lang {
519 return p.make_highlighter();
520 }
521 }
522 Box::new(PlainHighlighter)
523 }
524
525 #[must_use]
527 pub fn highlighter_for_path(&self, path: &str) -> Box<dyn Highlighter> {
528 self.highlighter_for(self.resolve(path))
529 }
530
531 #[must_use]
535 pub fn incremental_highlighter_for(&self, lang: Language) -> Box<dyn IncrementalHighlighter> {
536 for p in &self.plugins {
537 if p.language() == lang {
538 return p.make_incremental();
539 }
540 }
541 Box::new(WholeReHighlighter::new(Box::new(PlainHighlighter)))
542 }
543
544 #[must_use]
546 pub fn incremental_highlighter_for_path(
547 &self,
548 path: &str,
549 ) -> Box<dyn IncrementalHighlighter> {
550 self.incremental_highlighter_for(self.resolve(path))
551 }
552}
553
554pub struct PlainHighlighter;
556
557impl Highlighter for PlainHighlighter {
558 fn highlight(&self, text: &str) -> Vec<HighlightSpan> {
559 if text.is_empty() {
560 return Vec::new();
561 }
562 vec![HighlightSpan {
563 span: ByteSpan::new(0, u32::try_from(text.len()).unwrap_or(u32::MAX)),
564 class: HlClass::Plain,
565 }]
566 }
567}
568
569#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
574pub struct Rgb {
575 pub r: u8,
576 pub g: u8,
577 pub b: u8,
578}
579
580impl Rgb {
581 #[must_use]
582 pub const fn new(r: u8, g: u8, b: u8) -> Self {
583 Self { r, g, b }
584 }
585}
586
587pub trait Theme: Send + Sync {
590 fn color(&self, class: HlClass) -> Rgb;
591}
592
593pub struct NordTheme;
595
596impl Theme for NordTheme {
597 fn color(&self, class: HlClass) -> Rgb {
598 match class {
603 HlClass::Comment { .. } => Rgb::new(0x61, 0x6E, 0x88),
604 HlClass::Keyword => Rgb::new(0x81, 0xA1, 0xC1),
605 HlClass::KeywordArg | HlClass::Attribute => Rgb::new(0xB4, 0x8E, 0xAD),
606 HlClass::Type | HlClass::Namespace => Rgb::new(0x8F, 0xBC, 0xBB),
607 HlClass::Function => Rgb::new(0x88, 0xC0, 0xD0),
608 HlClass::Str => Rgb::new(0xA3, 0xBE, 0x8C),
609 HlClass::Escape | HlClass::Special => Rgb::new(0xEB, 0xCB, 0x8B),
610 HlClass::Numeric { .. } => Rgb::new(0xB4, 0x8E, 0xAD),
611 HlClass::Boolean | HlClass::Constant => Rgb::new(0xD0, 0x87, 0x70),
612 HlClass::Operator => Rgb::new(0x81, 0xA1, 0xC1),
613 HlClass::Punctuation => Rgb::new(0xEC, 0xEF, 0xF4),
614 HlClass::Hyperlink => Rgb::new(0x5E, 0x81, 0xAC),
615 HlClass::Error | HlClass::Removed => Rgb::new(0xBF, 0x61, 0x6A),
616 HlClass::Warning => Rgb::new(0xEB, 0xCB, 0x8B),
617 HlClass::Info => Rgb::new(0x81, 0xA1, 0xC1),
618 HlClass::Hint => Rgb::new(0x5E, 0x81, 0xAC),
619 HlClass::Added => Rgb::new(0xA3, 0xBE, 0x8C),
620 HlClass::Variable | HlClass::Whitespace | HlClass::Unchanged | HlClass::Plain => {
621 Rgb::new(0xD8, 0xDE, 0xE9)
622 }
623 }
624 }
625}
626
627pub mod langs {
635 use super::{HlClass, Language, LanguageLexer, LanguagePlugin, LineDriven, Selector, SpanSink};
636
637 pub struct LangTable {
639 pub keywords: &'static [&'static str],
640 pub line_comments: &'static [&'static str],
641 pub block_comment: Option<(&'static str, &'static str)>,
642 pub string_delims: &'static [char],
643 pub colon_keywords: bool,
645 }
646
647 #[derive(Clone, Copy, PartialEq, Eq, Default)]
649 pub enum LineMode {
650 #[default]
651 Normal,
652 InBlockComment,
653 InString(char),
655 }
656
657 pub struct TableLexer {
659 pub table: &'static LangTable,
660 }
661
662 #[inline]
663 fn is_ident_start(c: char) -> bool {
664 c == '_' || c.is_alphabetic()
665 }
666 #[inline]
667 fn is_ident_continue(c: char) -> bool {
668 c == '_' || c.is_alphanumeric()
669 }
670
671 impl LanguageLexer for TableLexer {
672 type LineState = LineMode;
673
674 #[allow(clippy::too_many_lines)]
675 fn lex_line(
676 &self,
677 line: &str,
678 line_start: u32,
679 entry: LineMode,
680 sink: &mut SpanSink,
681 ) -> LineMode {
682 let t = self.table;
683 let n = line.len();
684 let base = line_start;
685 let push = |sink: &mut SpanSink, s: usize, e: usize, class: HlClass| {
686 sink.push(base + s as u32, base + e as u32, class);
687 };
688 let mut i = 0usize;
689 let mut mode = entry;
690
691 match mode {
693 LineMode::InBlockComment => {
694 if let Some((_, close)) = t.block_comment {
695 if let Some(rel) = line.find(close) {
696 let e = rel + close.len();
697 push(sink, 0, e, HlClass::Comment { multiline: true });
698 i = e;
699 mode = LineMode::Normal;
700 } else {
701 push(sink, 0, n, HlClass::Comment { multiline: true });
702 return LineMode::InBlockComment;
703 }
704 } else {
705 mode = LineMode::Normal;
706 }
707 }
708 LineMode::InString(delim) => {
709 let e = scan_string_body(line, 0, delim);
710 match e {
711 Some(end) => {
712 push(sink, 0, end, HlClass::Str);
713 i = end;
714 mode = LineMode::Normal;
715 }
716 None => {
717 push(sink, 0, n, HlClass::Str);
718 return LineMode::InString(delim);
719 }
720 }
721 }
722 LineMode::Normal => {}
723 }
724
725 let _ = mode;
726 'scan: while i < n {
727 let c = line[i..].chars().next().unwrap();
728 let cl = c.len_utf8();
729
730 if c.is_whitespace() {
732 let s = i;
733 while i < n {
734 let d = line[i..].chars().next().unwrap();
735 if !d.is_whitespace() {
736 break;
737 }
738 i += d.len_utf8();
739 }
740 push(sink, s, i, HlClass::Whitespace);
741 continue 'scan;
742 }
743
744 for lc in t.line_comments {
746 if line[i..].starts_with(lc) {
747 push(sink, i, n, HlClass::Comment { multiline: false });
748 i = n;
749 continue 'scan;
750 }
751 }
752
753 if let Some((open, close)) = t.block_comment {
755 if line[i..].starts_with(open) {
756 if let Some(rel) = line[i + open.len()..].find(close) {
757 let e = i + open.len() + rel + close.len();
758 push(sink, i, e, HlClass::Comment { multiline: true });
759 i = e;
760 continue 'scan;
761 }
762 push(sink, i, n, HlClass::Comment { multiline: true });
763 return LineMode::InBlockComment;
764 }
765 }
766
767 if t.string_delims.contains(&c) {
769 match scan_string_body(line, i + cl, c) {
770 Some(end) => {
771 push(sink, i, end, HlClass::Str);
772 i = end;
773 continue 'scan;
774 }
775 None => {
776 push(sink, i, n, HlClass::Str);
777 return LineMode::InString(c);
778 }
779 }
780 }
781
782 if c.is_ascii_digit() {
784 let s = i;
785 let mut is_float = false;
786 i += cl;
787 while i < n {
788 let d = line[i..].chars().next().unwrap();
789 if d.is_ascii_alphanumeric() || d == '_' {
790 i += d.len_utf8();
791 } else if d == '.' {
792 is_float = true;
793 i += 1;
794 } else {
795 break;
796 }
797 }
798 push(sink, s, i, HlClass::Numeric { float: is_float });
799 continue 'scan;
800 }
801
802 if t.colon_keywords && c == ':' && i + 1 < n {
804 let next = line[i + 1..].chars().next().unwrap();
805 if is_ident_start(next) {
806 let s = i;
807 i += 1;
808 while i < n {
809 let d = line[i..].chars().next().unwrap();
810 if !is_ident_continue(d) {
811 break;
812 }
813 i += d.len_utf8();
814 }
815 push(sink, s, i, HlClass::KeywordArg);
816 continue 'scan;
817 }
818 }
819
820 if is_ident_start(c) {
822 let s = i;
823 i += cl;
824 while i < n {
825 let d = line[i..].chars().next().unwrap();
826 if !is_ident_continue(d) {
827 break;
828 }
829 i += d.len_utf8();
830 }
831 let word = &line[s..i];
832 let class = if t.keywords.contains(&word) {
833 HlClass::Keyword
834 } else if matches!(
835 word,
836 "true" | "false" | "True" | "False" | "None" | "nil" | "null"
837 ) {
838 HlClass::Boolean
839 } else if word.chars().next().is_some_and(char::is_uppercase) {
840 HlClass::Type
841 } else {
842 HlClass::Variable
843 };
844 push(sink, s, i, class);
845 continue 'scan;
846 }
847
848 let class = if "+-*/%=<>!&|^~".contains(c) {
850 HlClass::Operator
851 } else {
852 HlClass::Punctuation
853 };
854 push(sink, i, i + cl, class);
855 i += cl;
856 }
857
858 LineMode::Normal
859 }
860 }
861
862 fn scan_string_body(line: &str, from: usize, delim: char) -> Option<usize> {
866 let n = line.len();
867 let mut i = from;
868 while i < n {
869 let c = line[i..].chars().next().unwrap();
870 let cl = c.len_utf8();
871 if c == '\\' && i + cl < n {
872 let e = line[i + cl..].chars().next().unwrap();
873 i += cl + e.len_utf8();
874 continue;
875 }
876 i += cl;
877 if c == delim {
878 return Some(i);
879 }
880 }
881 None
882 }
883
884 pub struct TablePlugin {
886 pub language: Language,
887 pub selectors: &'static [Selector],
888 pub table: &'static LangTable,
889 }
890
891 impl LanguagePlugin for TablePlugin {
892 fn language(&self) -> Language {
893 self.language
894 }
895 fn selectors(&self) -> &'static [Selector] {
896 self.selectors
897 }
898 fn make_highlighter(&self) -> Box<dyn super::Highlighter> {
899 Box::new(LineDriven::new(TableLexer { table: self.table }))
900 }
901 fn make_incremental(&self) -> Box<dyn super::IncrementalHighlighter> {
902 Box::new(super::LineCache::new(TableLexer { table: self.table }))
905 }
906 }
907
908 static RUST_KW: &[&str] = &[
911 "as", "async", "await", "break", "const", "continue", "crate", "dyn", "else", "enum",
912 "extern", "fn", "for", "if", "impl", "in", "let", "loop", "match", "mod", "move", "mut",
913 "pub", "ref", "return", "self", "Self", "static", "struct", "super", "trait", "type",
914 "unsafe", "use", "where", "while",
915 ];
916 static RUST_TABLE: LangTable = LangTable {
917 keywords: RUST_KW,
918 line_comments: &["//"],
919 block_comment: Some(("/*", "*/")),
920 string_delims: &['"'],
921 colon_keywords: false,
922 };
923 static RUST_SEL: &[Selector] = &[Selector::Extension("rs")];
924
925 static PY_KW: &[&str] = &[
926 "and", "as", "assert", "async", "await", "break", "class", "continue", "def", "del",
927 "elif", "else", "except", "finally", "for", "from", "global", "if", "import", "in", "is",
928 "lambda", "nonlocal", "not", "or", "pass", "raise", "return", "try", "while", "with",
929 "yield",
930 ];
931 static PY_TABLE: LangTable = LangTable {
932 keywords: PY_KW,
933 line_comments: &["#"],
934 block_comment: None,
935 string_delims: &['"', '\''],
936 colon_keywords: false,
937 };
938 static PY_SEL: &[Selector] = &[Selector::Extension("py")];
939
940 static LISP_KW: &[&str] = &[
941 "def", "defn", "defmacro", "defcaixa", "deflexer", "let", "lambda", "fn", "if", "cond",
942 "when", "unless", "do", "quote",
943 ];
944 static LISP_TABLE: LangTable = LangTable {
945 keywords: LISP_KW,
946 line_comments: &[";"],
947 block_comment: Some(("#|", "|#")),
948 string_delims: &['"'],
949 colon_keywords: true,
950 };
951 static LISP_SEL: &[Selector] = &[
952 Selector::Extension("lisp"),
953 Selector::Extension("lsp"),
954 Selector::Extension("el"),
955 Selector::Extension("scm"),
956 ];
957
958 static JSON_TABLE: LangTable = LangTable {
959 keywords: &["true", "false", "null"],
960 line_comments: &[],
961 block_comment: None,
962 string_delims: &['"'],
963 colon_keywords: false,
964 };
965 static JSON_SEL: &[Selector] = &[Selector::Extension("json")];
966
967 static TOML_TABLE: LangTable = LangTable {
968 keywords: &["true", "false"],
969 line_comments: &["#"],
970 block_comment: None,
971 string_delims: &['"', '\''],
972 colon_keywords: false,
973 };
974 static TOML_SEL: &[Selector] = &[
975 Selector::Extension("toml"),
976 Selector::Filename("Cargo.lock"),
977 ];
978
979 static MD_TABLE: LangTable = LangTable {
980 keywords: &[],
981 line_comments: &[],
982 block_comment: None,
983 string_delims: &['`'],
984 colon_keywords: false,
985 };
986 static MD_SEL: &[Selector] = &[Selector::Extension("md"), Selector::Extension("markdown")];
987
988 #[must_use]
990 pub fn builtins() -> Vec<Box<dyn LanguagePlugin>> {
991 vec![
992 Box::new(TablePlugin {
993 language: Language("rust"),
994 selectors: RUST_SEL,
995 table: &RUST_TABLE,
996 }),
997 Box::new(TablePlugin {
998 language: Language("python"),
999 selectors: PY_SEL,
1000 table: &PY_TABLE,
1001 }),
1002 Box::new(TablePlugin {
1003 language: Language("lisp"),
1004 selectors: LISP_SEL,
1005 table: &LISP_TABLE,
1006 }),
1007 Box::new(TablePlugin {
1008 language: Language("json"),
1009 selectors: JSON_SEL,
1010 table: &JSON_TABLE,
1011 }),
1012 Box::new(TablePlugin {
1013 language: Language("toml"),
1014 selectors: TOML_SEL,
1015 table: &TOML_TABLE,
1016 }),
1017 Box::new(TablePlugin {
1018 language: Language("markdown"),
1019 selectors: MD_SEL,
1020 table: &MD_TABLE,
1021 }),
1022 ]
1023 }
1024}
1025
1026#[cfg(test)]
1029mod tests {
1030 use super::*;
1031
1032 fn covers(text: &str, spans: &[HighlightSpan]) {
1033 let mut cursor = 0u32;
1035 for s in spans {
1036 assert_eq!(s.span.start, cursor, "gap/overlap at {cursor}");
1037 assert!(s.span.end > s.span.start);
1038 cursor = s.span.end;
1039 }
1040 assert_eq!(cursor as usize, text.len(), "partition does not cover text");
1041 }
1042
1043 #[test]
1044 fn partition_is_coverage_complete() {
1045 let eco = Ecosystem::with_builtins();
1046 for (path, src) in [
1047 ("a.rs", "fn main() {\n let x = 42; // hi\n}\n"),
1048 ("b.py", "def f(x):\n return \"s\" # c\n"),
1049 ("c.lisp", "(defcaixa :name \"x\" 42) ; c\n"),
1050 ("d.txt", "no language here\n"),
1051 ] {
1052 let h = eco.highlighter_for_path(path);
1053 let spans = h.highlight(src);
1054 covers(src, &spans);
1055 }
1056 }
1057
1058 #[test]
1059 fn resolves_by_extension_not_always_rust() {
1060 let eco = Ecosystem::with_builtins();
1061 assert_eq!(eco.resolve("src/main.rs"), Language("rust"));
1062 assert_eq!(eco.resolve("app.py"), Language("python"));
1063 assert_eq!(eco.resolve("x.lisp"), Language("lisp"));
1064 assert_eq!(eco.resolve("Cargo.lock"), Language("toml"));
1065 assert_eq!(eco.resolve("notes.txt"), PLAIN_TEXT);
1067 assert_ne!(eco.resolve("app.py"), Language("rust"));
1068 }
1069
1070 #[test]
1071 fn rust_keyword_is_classified() {
1072 let eco = Ecosystem::with_builtins();
1073 let spans = eco.highlighter_for_path("a.rs").highlight("fn x");
1074 assert_eq!(spans[0].class, HlClass::Keyword); }
1076
1077 #[test]
1078 fn multiline_string_and_block_comment_thread_state() {
1079 let eco = Ecosystem::with_builtins();
1080 let spans = eco.highlighter_for_path("a.rs").highlight("/* a\nb */ x\n");
1081 covers("/* a\nb */ x\n", &spans);
1082 assert!(matches!(
1083 spans[0].class,
1084 HlClass::Comment { multiline: true }
1085 ));
1086 }
1087
1088 #[test]
1089 fn plain_text_is_one_plain_span() {
1090 let h = PlainHighlighter;
1091 let spans = h.highlight("hello");
1092 assert_eq!(spans.len(), 1);
1093 assert_eq!(spans[0].class, HlClass::Plain);
1094 }
1095
1096 fn lcg(state: &mut u64) -> u64 {
1101 *state = state
1102 .wrapping_mul(6_364_136_223_846_793_005)
1103 .wrapping_add(1_442_695_040_888_963_407);
1104 *state >> 33
1105 }
1106
1107 #[test]
1111 fn incremental_is_byte_identical_to_one_shot() {
1112 let eco = Ecosystem::with_builtins();
1113 let one_shot = eco.highlighter_for_path("f.rs");
1114 let mut cache = eco.incremental_highlighter_for_path("f.rs");
1115
1116 let alphabet: Vec<char> = "fn xy=42;{}\n/*/ \"ab\"//c".chars().collect();
1117 let mut text = String::from("fn main() {\n let x = 1;\n}\n");
1118 let mut seed = 0x1234_5678_9abc_def0u64;
1119
1120 for _ in 0..400 {
1121 let len = text.chars().count();
1123 let at = if len == 0 {
1124 0
1125 } else {
1126 (lcg(&mut seed) as usize) % (len + 1)
1127 };
1128 let byte_at = text.char_indices().nth(at).map_or(text.len(), |(b, _)| b);
1129 if len > 4 && lcg(&mut seed) % 2 == 0 {
1130 if let Some((b, c)) = text[byte_at..].char_indices().next() {
1132 let start = byte_at + b;
1133 text.replace_range(start..start + c.len_utf8(), "");
1134 }
1135 } else {
1136 let c = alphabet[(lcg(&mut seed) as usize) % alphabet.len()];
1137 text.insert(byte_at, c);
1138 }
1139
1140 let inc = cache.highlight(&text);
1141 let full = one_shot.highlight(&text);
1142 assert_eq!(inc, full, "incremental != one-shot for {text:?}");
1143 covers(&text, &inc);
1144 }
1145 }
1146
1147 #[test]
1149 fn idle_rehighlight_relexes_zero_lines() {
1150 let eco = Ecosystem::with_builtins();
1151 let mut cache = eco.incremental_highlighter_for_path("f.rs");
1152 let text = "fn a() {}\nfn b() {}\nfn c() {}\n";
1153 let _ = cache.highlight(text);
1154 let _ = cache.highlight(text); assert_eq!(cache.last_relexed(), 0, "idle re-render must re-lex nothing");
1156 }
1157
1158 #[test]
1162 fn single_line_edit_relexes_locally() {
1163 let eco = Ecosystem::with_builtins();
1164 let mut cache = eco.incremental_highlighter_for_path("f.rs");
1165 let mut text = String::new();
1166 for i in 0..60 {
1167 text.push_str(&format!("let v{i} = {i};\n"));
1168 }
1169 let _ = cache.highlight(&text); let edited = text.replacen("let v30 = 30;", "let v30 = 999;", 1);
1172 let _ = cache.highlight(&edited);
1173 assert_eq!(
1174 cache.last_relexed(),
1175 1,
1176 "a local edit must re-lex exactly its own line (state re-converges immediately)"
1177 );
1178 }
1179
1180 #[test]
1183 fn cross_line_state_change_propagates_then_converges() {
1184 let eco = Ecosystem::with_builtins();
1185 let mut cache = eco.incremental_highlighter_for_path("f.rs");
1186 let text = "let a = 1;\nlet b = 2;\nlet c = 3;\nlet d = 4;\n";
1187 let _ = cache.highlight(text);
1188 let edited = "let a = 1; /*\nstill comment\n*/ let c = 3;\nlet d = 4;\n";
1190 let inc = cache.highlight(edited);
1191 assert_eq!(inc, eco.highlighter_for_path("f.rs").highlight(edited));
1192 assert!(
1194 cache.last_relexed() <= 3,
1195 "re-lex must stop once the block comment closes and state reconverges"
1196 );
1197 }
1198}