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(&self, path: &str) -> Box<dyn IncrementalHighlighter> {
547 self.incremental_highlighter_for(self.resolve(path))
548 }
549}
550
551pub struct PlainHighlighter;
553
554impl Highlighter for PlainHighlighter {
555 fn highlight(&self, text: &str) -> Vec<HighlightSpan> {
556 if text.is_empty() {
557 return Vec::new();
558 }
559 vec![HighlightSpan {
560 span: ByteSpan::new(0, u32::try_from(text.len()).unwrap_or(u32::MAX)),
561 class: HlClass::Plain,
562 }]
563 }
564}
565
566#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
571pub struct Rgb {
572 pub r: u8,
573 pub g: u8,
574 pub b: u8,
575}
576
577impl Rgb {
578 #[must_use]
579 pub const fn new(r: u8, g: u8, b: u8) -> Self {
580 Self { r, g, b }
581 }
582}
583
584pub trait Theme: Send + Sync {
587 fn color(&self, class: HlClass) -> Rgb;
588}
589
590pub struct NordTheme;
592
593impl Theme for NordTheme {
594 fn color(&self, class: HlClass) -> Rgb {
595 match class {
600 HlClass::Comment { .. } => Rgb::new(0x61, 0x6E, 0x88),
601 HlClass::Keyword => Rgb::new(0x81, 0xA1, 0xC1),
602 HlClass::KeywordArg | HlClass::Attribute => Rgb::new(0xB4, 0x8E, 0xAD),
603 HlClass::Type | HlClass::Namespace => Rgb::new(0x8F, 0xBC, 0xBB),
604 HlClass::Function => Rgb::new(0x88, 0xC0, 0xD0),
605 HlClass::Str => Rgb::new(0xA3, 0xBE, 0x8C),
606 HlClass::Escape | HlClass::Special => Rgb::new(0xEB, 0xCB, 0x8B),
607 HlClass::Numeric { .. } => Rgb::new(0xB4, 0x8E, 0xAD),
608 HlClass::Boolean | HlClass::Constant => Rgb::new(0xD0, 0x87, 0x70),
609 HlClass::Operator => Rgb::new(0x81, 0xA1, 0xC1),
610 HlClass::Punctuation => Rgb::new(0xEC, 0xEF, 0xF4),
611 HlClass::Hyperlink => Rgb::new(0x5E, 0x81, 0xAC),
612 HlClass::Error | HlClass::Removed => Rgb::new(0xBF, 0x61, 0x6A),
613 HlClass::Warning => Rgb::new(0xEB, 0xCB, 0x8B),
614 HlClass::Info => Rgb::new(0x81, 0xA1, 0xC1),
615 HlClass::Hint => Rgb::new(0x5E, 0x81, 0xAC),
616 HlClass::Added => Rgb::new(0xA3, 0xBE, 0x8C),
617 HlClass::Variable | HlClass::Whitespace | HlClass::Unchanged | HlClass::Plain => {
618 Rgb::new(0xD8, 0xDE, 0xE9)
619 }
620 }
621 }
622}
623
624pub mod langs {
632 use super::{HlClass, Language, LanguageLexer, LanguagePlugin, LineDriven, Selector, SpanSink};
633
634 pub struct LangTable {
636 pub keywords: &'static [&'static str],
637 pub line_comments: &'static [&'static str],
638 pub block_comment: Option<(&'static str, &'static str)>,
639 pub string_delims: &'static [char],
640 pub colon_keywords: bool,
642 }
643
644 #[derive(Clone, Copy, PartialEq, Eq, Default)]
646 pub enum LineMode {
647 #[default]
648 Normal,
649 InBlockComment,
650 InString(char),
652 }
653
654 pub struct TableLexer {
656 pub table: &'static LangTable,
657 }
658
659 #[inline]
660 fn is_ident_start(c: char) -> bool {
661 c == '_' || c.is_alphabetic()
662 }
663 #[inline]
664 fn is_ident_continue(c: char) -> bool {
665 c == '_' || c.is_alphanumeric()
666 }
667
668 impl LanguageLexer for TableLexer {
669 type LineState = LineMode;
670
671 #[allow(clippy::too_many_lines)]
672 fn lex_line(
673 &self,
674 line: &str,
675 line_start: u32,
676 entry: LineMode,
677 sink: &mut SpanSink,
678 ) -> LineMode {
679 let t = self.table;
680 let n = line.len();
681 let base = line_start;
682 let push = |sink: &mut SpanSink, s: usize, e: usize, class: HlClass| {
683 sink.push(base + s as u32, base + e as u32, class);
684 };
685 let mut i = 0usize;
686 let mut mode = entry;
687
688 match mode {
690 LineMode::InBlockComment => {
691 if let Some((_, close)) = t.block_comment {
692 if let Some(rel) = line.find(close) {
693 let e = rel + close.len();
694 push(sink, 0, e, HlClass::Comment { multiline: true });
695 i = e;
696 mode = LineMode::Normal;
697 } else {
698 push(sink, 0, n, HlClass::Comment { multiline: true });
699 return LineMode::InBlockComment;
700 }
701 } else {
702 mode = LineMode::Normal;
703 }
704 }
705 LineMode::InString(delim) => {
706 let e = scan_string_body(line, 0, delim);
707 match e {
708 Some(end) => {
709 push(sink, 0, end, HlClass::Str);
710 i = end;
711 mode = LineMode::Normal;
712 }
713 None => {
714 push(sink, 0, n, HlClass::Str);
715 return LineMode::InString(delim);
716 }
717 }
718 }
719 LineMode::Normal => {}
720 }
721
722 let _ = mode;
723 'scan: while i < n {
724 let c = line[i..].chars().next().unwrap();
725 let cl = c.len_utf8();
726
727 if c.is_whitespace() {
729 let s = i;
730 while i < n {
731 let d = line[i..].chars().next().unwrap();
732 if !d.is_whitespace() {
733 break;
734 }
735 i += d.len_utf8();
736 }
737 push(sink, s, i, HlClass::Whitespace);
738 continue 'scan;
739 }
740
741 for lc in t.line_comments {
743 if line[i..].starts_with(lc) {
744 push(sink, i, n, HlClass::Comment { multiline: false });
745 i = n;
746 continue 'scan;
747 }
748 }
749
750 if let Some((open, close)) = t.block_comment {
752 if line[i..].starts_with(open) {
753 if let Some(rel) = line[i + open.len()..].find(close) {
754 let e = i + open.len() + rel + close.len();
755 push(sink, i, e, HlClass::Comment { multiline: true });
756 i = e;
757 continue 'scan;
758 }
759 push(sink, i, n, HlClass::Comment { multiline: true });
760 return LineMode::InBlockComment;
761 }
762 }
763
764 if t.string_delims.contains(&c) {
766 match scan_string_body(line, i + cl, c) {
767 Some(end) => {
768 push(sink, i, end, HlClass::Str);
769 i = end;
770 continue 'scan;
771 }
772 None => {
773 push(sink, i, n, HlClass::Str);
774 return LineMode::InString(c);
775 }
776 }
777 }
778
779 if c.is_ascii_digit() {
781 let s = i;
782 let mut is_float = false;
783 i += cl;
784 while i < n {
785 let d = line[i..].chars().next().unwrap();
786 if d.is_ascii_alphanumeric() || d == '_' {
787 i += d.len_utf8();
788 } else if d == '.' {
789 is_float = true;
790 i += 1;
791 } else {
792 break;
793 }
794 }
795 push(sink, s, i, HlClass::Numeric { float: is_float });
796 continue 'scan;
797 }
798
799 if t.colon_keywords && c == ':' && i + 1 < n {
801 let next = line[i + 1..].chars().next().unwrap();
802 if is_ident_start(next) {
803 let s = i;
804 i += 1;
805 while i < n {
806 let d = line[i..].chars().next().unwrap();
807 if !is_ident_continue(d) {
808 break;
809 }
810 i += d.len_utf8();
811 }
812 push(sink, s, i, HlClass::KeywordArg);
813 continue 'scan;
814 }
815 }
816
817 if is_ident_start(c) {
819 let s = i;
820 i += cl;
821 while i < n {
822 let d = line[i..].chars().next().unwrap();
823 if !is_ident_continue(d) {
824 break;
825 }
826 i += d.len_utf8();
827 }
828 let word = &line[s..i];
829 let class = if t.keywords.contains(&word) {
830 HlClass::Keyword
831 } else if matches!(
832 word,
833 "true" | "false" | "True" | "False" | "None" | "nil" | "null"
834 ) {
835 HlClass::Boolean
836 } else if word.chars().next().is_some_and(char::is_uppercase) {
837 HlClass::Type
838 } else {
839 HlClass::Variable
840 };
841 push(sink, s, i, class);
842 continue 'scan;
843 }
844
845 let class = if "+-*/%=<>!&|^~".contains(c) {
847 HlClass::Operator
848 } else {
849 HlClass::Punctuation
850 };
851 push(sink, i, i + cl, class);
852 i += cl;
853 }
854
855 LineMode::Normal
856 }
857 }
858
859 fn scan_string_body(line: &str, from: usize, delim: char) -> Option<usize> {
863 let n = line.len();
864 let mut i = from;
865 while i < n {
866 let c = line[i..].chars().next().unwrap();
867 let cl = c.len_utf8();
868 if c == '\\' && i + cl < n {
869 let e = line[i + cl..].chars().next().unwrap();
870 i += cl + e.len_utf8();
871 continue;
872 }
873 i += cl;
874 if c == delim {
875 return Some(i);
876 }
877 }
878 None
879 }
880
881 pub struct TablePlugin {
883 pub language: Language,
884 pub selectors: &'static [Selector],
885 pub table: &'static LangTable,
886 }
887
888 impl LanguagePlugin for TablePlugin {
889 fn language(&self) -> Language {
890 self.language
891 }
892 fn selectors(&self) -> &'static [Selector] {
893 self.selectors
894 }
895 fn make_highlighter(&self) -> Box<dyn super::Highlighter> {
896 Box::new(LineDriven::new(TableLexer { table: self.table }))
897 }
898 fn make_incremental(&self) -> Box<dyn super::IncrementalHighlighter> {
899 Box::new(super::LineCache::new(TableLexer { table: self.table }))
902 }
903 }
904
905 static RUST_KW: &[&str] = &[
908 "as", "async", "await", "break", "const", "continue", "crate", "dyn", "else", "enum",
909 "extern", "fn", "for", "if", "impl", "in", "let", "loop", "match", "mod", "move", "mut",
910 "pub", "ref", "return", "self", "Self", "static", "struct", "super", "trait", "type",
911 "unsafe", "use", "where", "while",
912 ];
913 static RUST_TABLE: LangTable = LangTable {
914 keywords: RUST_KW,
915 line_comments: &["//"],
916 block_comment: Some(("/*", "*/")),
917 string_delims: &['"'],
918 colon_keywords: false,
919 };
920 static RUST_SEL: &[Selector] = &[Selector::Extension("rs")];
921
922 static PY_KW: &[&str] = &[
923 "and", "as", "assert", "async", "await", "break", "class", "continue", "def", "del",
924 "elif", "else", "except", "finally", "for", "from", "global", "if", "import", "in", "is",
925 "lambda", "nonlocal", "not", "or", "pass", "raise", "return", "try", "while", "with",
926 "yield",
927 ];
928 static PY_TABLE: LangTable = LangTable {
929 keywords: PY_KW,
930 line_comments: &["#"],
931 block_comment: None,
932 string_delims: &['"', '\''],
933 colon_keywords: false,
934 };
935 static PY_SEL: &[Selector] = &[Selector::Extension("py")];
936
937 static LISP_KW: &[&str] = &[
947 "def",
948 "defn",
949 "defmacro",
950 "defcaixa",
951 "deflexer",
952 "define",
953 "defun",
954 "defmodule",
955 "defsuite",
956 "deftest",
957 "let",
958 "let*",
959 "letrec",
960 "lambda",
961 "fn",
962 "if",
963 "cond",
964 "case",
965 "when",
966 "unless",
967 "do",
968 "begin",
969 "quote",
970 "quasiquote",
971 "unquote",
972 "set!",
973 "and",
974 "or",
975 "not",
976 "import",
977 "importar",
978 ];
979 static LISP_TABLE: LangTable = LangTable {
980 keywords: LISP_KW,
981 line_comments: &[";"],
982 block_comment: Some(("#|", "|#")),
983 string_delims: &['"'],
984 colon_keywords: true,
985 };
986 static LISP_SEL: &[Selector] = &[
987 Selector::Extension("lisp"),
988 Selector::Extension("tlisp"),
993 Selector::Extension("lsp"),
994 Selector::Extension("el"),
995 Selector::Extension("scm"),
996 ];
997
998 static JSON_TABLE: LangTable = LangTable {
999 keywords: &["true", "false", "null"],
1000 line_comments: &[],
1001 block_comment: None,
1002 string_delims: &['"'],
1003 colon_keywords: false,
1004 };
1005 static JSON_SEL: &[Selector] = &[Selector::Extension("json")];
1006
1007 static TOML_TABLE: LangTable = LangTable {
1008 keywords: &["true", "false"],
1009 line_comments: &["#"],
1010 block_comment: None,
1011 string_delims: &['"', '\''],
1012 colon_keywords: false,
1013 };
1014 static TOML_SEL: &[Selector] = &[
1015 Selector::Extension("toml"),
1016 Selector::Filename("Cargo.lock"),
1017 ];
1018
1019 static MD_TABLE: LangTable = LangTable {
1020 keywords: &[],
1021 line_comments: &[],
1022 block_comment: None,
1023 string_delims: &['`'],
1024 colon_keywords: false,
1025 };
1026 static MD_SEL: &[Selector] = &[Selector::Extension("md"), Selector::Extension("markdown")];
1027
1028 #[must_use]
1030 pub fn builtins() -> Vec<Box<dyn LanguagePlugin>> {
1031 vec![
1032 Box::new(TablePlugin {
1033 language: Language("rust"),
1034 selectors: RUST_SEL,
1035 table: &RUST_TABLE,
1036 }),
1037 Box::new(TablePlugin {
1038 language: Language("python"),
1039 selectors: PY_SEL,
1040 table: &PY_TABLE,
1041 }),
1042 Box::new(TablePlugin {
1043 language: Language("lisp"),
1044 selectors: LISP_SEL,
1045 table: &LISP_TABLE,
1046 }),
1047 Box::new(TablePlugin {
1048 language: Language("json"),
1049 selectors: JSON_SEL,
1050 table: &JSON_TABLE,
1051 }),
1052 Box::new(TablePlugin {
1053 language: Language("toml"),
1054 selectors: TOML_SEL,
1055 table: &TOML_TABLE,
1056 }),
1057 Box::new(TablePlugin {
1058 language: Language("markdown"),
1059 selectors: MD_SEL,
1060 table: &MD_TABLE,
1061 }),
1062 ]
1063 }
1064}
1065
1066#[cfg(test)]
1069mod tests {
1070 use super::*;
1071
1072 fn covers(text: &str, spans: &[HighlightSpan]) {
1073 let mut cursor = 0u32;
1075 for s in spans {
1076 assert_eq!(s.span.start, cursor, "gap/overlap at {cursor}");
1077 assert!(s.span.end > s.span.start);
1078 cursor = s.span.end;
1079 }
1080 assert_eq!(cursor as usize, text.len(), "partition does not cover text");
1081 }
1082
1083 #[test]
1084 fn partition_is_coverage_complete() {
1085 let eco = Ecosystem::with_builtins();
1086 for (path, src) in [
1087 ("a.rs", "fn main() {\n let x = 42; // hi\n}\n"),
1088 ("b.py", "def f(x):\n return \"s\" # c\n"),
1089 ("c.lisp", "(defcaixa :name \"x\" 42) ; c\n"),
1090 ("c.tlisp", "(define f (lambda (x) \"s\")) ; c\n"),
1091 ("d.txt", "no language here\n"),
1092 ] {
1093 let h = eco.highlighter_for_path(path);
1094 let spans = h.highlight(src);
1095 covers(src, &spans);
1096 }
1097 }
1098
1099 #[test]
1100 fn resolves_by_extension_not_always_rust() {
1101 let eco = Ecosystem::with_builtins();
1102 assert_eq!(eco.resolve("src/main.rs"), Language("rust"));
1103 assert_eq!(eco.resolve("app.py"), Language("python"));
1104 assert_eq!(eco.resolve("x.lisp"), Language("lisp"));
1105 assert_eq!(eco.resolve("x.tlisp"), Language("lisp"));
1108 assert_eq!(eco.resolve("tools/check.tlisp"), Language("lisp"));
1109 assert_eq!(eco.resolve("Cargo.lock"), Language("toml"));
1110 assert_eq!(eco.resolve("notes.txt"), PLAIN_TEXT);
1112 assert_ne!(eco.resolve("app.py"), Language("rust"));
1113 }
1114
1115 #[test]
1116 fn rust_keyword_is_classified() {
1117 let eco = Ecosystem::with_builtins();
1118 let spans = eco.highlighter_for_path("a.rs").highlight("fn x");
1119 assert_eq!(spans[0].class, HlClass::Keyword); }
1121
1122 #[test]
1123 fn multiline_string_and_block_comment_thread_state() {
1124 let eco = Ecosystem::with_builtins();
1125 let spans = eco.highlighter_for_path("a.rs").highlight("/* a\nb */ x\n");
1126 covers("/* a\nb */ x\n", &spans);
1127 assert!(matches!(
1128 spans[0].class,
1129 HlClass::Comment { multiline: true }
1130 ));
1131 }
1132
1133 #[test]
1134 fn plain_text_is_one_plain_span() {
1135 let h = PlainHighlighter;
1136 let spans = h.highlight("hello");
1137 assert_eq!(spans.len(), 1);
1138 assert_eq!(spans[0].class, HlClass::Plain);
1139 }
1140
1141 fn lcg(state: &mut u64) -> u64 {
1146 *state = state
1147 .wrapping_mul(6_364_136_223_846_793_005)
1148 .wrapping_add(1_442_695_040_888_963_407);
1149 *state >> 33
1150 }
1151
1152 #[test]
1156 fn incremental_is_byte_identical_to_one_shot() {
1157 let eco = Ecosystem::with_builtins();
1158 let one_shot = eco.highlighter_for_path("f.rs");
1159 let mut cache = eco.incremental_highlighter_for_path("f.rs");
1160
1161 let alphabet: Vec<char> = "fn xy=42;{}\n/*/ \"ab\"//c".chars().collect();
1162 let mut text = String::from("fn main() {\n let x = 1;\n}\n");
1163 let mut seed = 0x1234_5678_9abc_def0u64;
1164
1165 for _ in 0..400 {
1166 let len = text.chars().count();
1168 let at = if len == 0 {
1169 0
1170 } else {
1171 (lcg(&mut seed) as usize) % (len + 1)
1172 };
1173 let byte_at = text.char_indices().nth(at).map_or(text.len(), |(b, _)| b);
1174 if len > 4 && lcg(&mut seed) % 2 == 0 {
1175 if let Some((b, c)) = text[byte_at..].char_indices().next() {
1177 let start = byte_at + b;
1178 text.replace_range(start..start + c.len_utf8(), "");
1179 }
1180 } else {
1181 let c = alphabet[(lcg(&mut seed) as usize) % alphabet.len()];
1182 text.insert(byte_at, c);
1183 }
1184
1185 let inc = cache.highlight(&text);
1186 let full = one_shot.highlight(&text);
1187 assert_eq!(inc, full, "incremental != one-shot for {text:?}");
1188 covers(&text, &inc);
1189 }
1190 }
1191
1192 #[test]
1194 fn idle_rehighlight_relexes_zero_lines() {
1195 let eco = Ecosystem::with_builtins();
1196 let mut cache = eco.incremental_highlighter_for_path("f.rs");
1197 let text = "fn a() {}\nfn b() {}\nfn c() {}\n";
1198 let _ = cache.highlight(text);
1199 let _ = cache.highlight(text); assert_eq!(
1201 cache.last_relexed(),
1202 0,
1203 "idle re-render must re-lex nothing"
1204 );
1205 }
1206
1207 #[test]
1211 fn single_line_edit_relexes_locally() {
1212 let eco = Ecosystem::with_builtins();
1213 let mut cache = eco.incremental_highlighter_for_path("f.rs");
1214 let mut text = String::new();
1215 for i in 0..60 {
1216 text.push_str(&format!("let v{i} = {i};\n"));
1217 }
1218 let _ = cache.highlight(&text); let edited = text.replacen("let v30 = 30;", "let v30 = 999;", 1);
1221 let _ = cache.highlight(&edited);
1222 assert_eq!(
1223 cache.last_relexed(),
1224 1,
1225 "a local edit must re-lex exactly its own line (state re-converges immediately)"
1226 );
1227 }
1228
1229 #[test]
1232 fn cross_line_state_change_propagates_then_converges() {
1233 let eco = Ecosystem::with_builtins();
1234 let mut cache = eco.incremental_highlighter_for_path("f.rs");
1235 let text = "let a = 1;\nlet b = 2;\nlet c = 3;\nlet d = 4;\n";
1236 let _ = cache.highlight(text);
1237 let edited = "let a = 1; /*\nstill comment\n*/ let c = 3;\nlet d = 4;\n";
1239 let inc = cache.highlight(edited);
1240 assert_eq!(inc, eco.highlighter_for_path("f.rs").highlight(edited));
1241 assert!(
1243 cache.last_relexed() <= 3,
1244 "re-lex must stop once the block comment closes and state reconverges"
1245 );
1246 }
1247}