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] = &[
950 "def", "defn", "defmacro", "defcaixa", "deflexer", "define", "defun", "defmodule",
951 "defsuite", "deftest", "let", "let*", "letrec", "lambda", "fn", "if", "cond", "case",
952 "when", "unless", "do", "begin", "quote", "quasiquote", "unquote", "set!", "and", "or",
953 "not", "import", "importar",
954 ];
955 static LISP_TABLE: LangTable = LangTable {
956 keywords: LISP_KW,
957 line_comments: &[";"],
958 block_comment: Some(("#|", "|#")),
959 string_delims: &['"'],
960 colon_keywords: true,
961 };
962 static LISP_SEL: &[Selector] = &[
963 Selector::Extension("lisp"),
964 Selector::Extension("tlisp"),
969 Selector::Extension("lsp"),
970 Selector::Extension("el"),
971 Selector::Extension("scm"),
972 ];
973
974 static JSON_TABLE: LangTable = LangTable {
975 keywords: &["true", "false", "null"],
976 line_comments: &[],
977 block_comment: None,
978 string_delims: &['"'],
979 colon_keywords: false,
980 };
981 static JSON_SEL: &[Selector] = &[Selector::Extension("json")];
982
983 static TOML_TABLE: LangTable = LangTable {
984 keywords: &["true", "false"],
985 line_comments: &["#"],
986 block_comment: None,
987 string_delims: &['"', '\''],
988 colon_keywords: false,
989 };
990 static TOML_SEL: &[Selector] = &[
991 Selector::Extension("toml"),
992 Selector::Filename("Cargo.lock"),
993 ];
994
995 static MD_TABLE: LangTable = LangTable {
996 keywords: &[],
997 line_comments: &[],
998 block_comment: None,
999 string_delims: &['`'],
1000 colon_keywords: false,
1001 };
1002 static MD_SEL: &[Selector] = &[Selector::Extension("md"), Selector::Extension("markdown")];
1003
1004 #[must_use]
1006 pub fn builtins() -> Vec<Box<dyn LanguagePlugin>> {
1007 vec![
1008 Box::new(TablePlugin {
1009 language: Language("rust"),
1010 selectors: RUST_SEL,
1011 table: &RUST_TABLE,
1012 }),
1013 Box::new(TablePlugin {
1014 language: Language("python"),
1015 selectors: PY_SEL,
1016 table: &PY_TABLE,
1017 }),
1018 Box::new(TablePlugin {
1019 language: Language("lisp"),
1020 selectors: LISP_SEL,
1021 table: &LISP_TABLE,
1022 }),
1023 Box::new(TablePlugin {
1024 language: Language("json"),
1025 selectors: JSON_SEL,
1026 table: &JSON_TABLE,
1027 }),
1028 Box::new(TablePlugin {
1029 language: Language("toml"),
1030 selectors: TOML_SEL,
1031 table: &TOML_TABLE,
1032 }),
1033 Box::new(TablePlugin {
1034 language: Language("markdown"),
1035 selectors: MD_SEL,
1036 table: &MD_TABLE,
1037 }),
1038 ]
1039 }
1040}
1041
1042#[cfg(test)]
1045mod tests {
1046 use super::*;
1047
1048 fn covers(text: &str, spans: &[HighlightSpan]) {
1049 let mut cursor = 0u32;
1051 for s in spans {
1052 assert_eq!(s.span.start, cursor, "gap/overlap at {cursor}");
1053 assert!(s.span.end > s.span.start);
1054 cursor = s.span.end;
1055 }
1056 assert_eq!(cursor as usize, text.len(), "partition does not cover text");
1057 }
1058
1059 #[test]
1060 fn partition_is_coverage_complete() {
1061 let eco = Ecosystem::with_builtins();
1062 for (path, src) in [
1063 ("a.rs", "fn main() {\n let x = 42; // hi\n}\n"),
1064 ("b.py", "def f(x):\n return \"s\" # c\n"),
1065 ("c.lisp", "(defcaixa :name \"x\" 42) ; c\n"),
1066 ("c.tlisp", "(define f (lambda (x) \"s\")) ; c\n"),
1067 ("d.txt", "no language here\n"),
1068 ] {
1069 let h = eco.highlighter_for_path(path);
1070 let spans = h.highlight(src);
1071 covers(src, &spans);
1072 }
1073 }
1074
1075 #[test]
1076 fn resolves_by_extension_not_always_rust() {
1077 let eco = Ecosystem::with_builtins();
1078 assert_eq!(eco.resolve("src/main.rs"), Language("rust"));
1079 assert_eq!(eco.resolve("app.py"), Language("python"));
1080 assert_eq!(eco.resolve("x.lisp"), Language("lisp"));
1081 assert_eq!(eco.resolve("x.tlisp"), Language("lisp"));
1084 assert_eq!(eco.resolve("tools/check.tlisp"), Language("lisp"));
1085 assert_eq!(eco.resolve("Cargo.lock"), Language("toml"));
1086 assert_eq!(eco.resolve("notes.txt"), PLAIN_TEXT);
1088 assert_ne!(eco.resolve("app.py"), Language("rust"));
1089 }
1090
1091 #[test]
1092 fn rust_keyword_is_classified() {
1093 let eco = Ecosystem::with_builtins();
1094 let spans = eco.highlighter_for_path("a.rs").highlight("fn x");
1095 assert_eq!(spans[0].class, HlClass::Keyword); }
1097
1098 #[test]
1099 fn multiline_string_and_block_comment_thread_state() {
1100 let eco = Ecosystem::with_builtins();
1101 let spans = eco.highlighter_for_path("a.rs").highlight("/* a\nb */ x\n");
1102 covers("/* a\nb */ x\n", &spans);
1103 assert!(matches!(
1104 spans[0].class,
1105 HlClass::Comment { multiline: true }
1106 ));
1107 }
1108
1109 #[test]
1110 fn plain_text_is_one_plain_span() {
1111 let h = PlainHighlighter;
1112 let spans = h.highlight("hello");
1113 assert_eq!(spans.len(), 1);
1114 assert_eq!(spans[0].class, HlClass::Plain);
1115 }
1116
1117 fn lcg(state: &mut u64) -> u64 {
1122 *state = state
1123 .wrapping_mul(6_364_136_223_846_793_005)
1124 .wrapping_add(1_442_695_040_888_963_407);
1125 *state >> 33
1126 }
1127
1128 #[test]
1132 fn incremental_is_byte_identical_to_one_shot() {
1133 let eco = Ecosystem::with_builtins();
1134 let one_shot = eco.highlighter_for_path("f.rs");
1135 let mut cache = eco.incremental_highlighter_for_path("f.rs");
1136
1137 let alphabet: Vec<char> = "fn xy=42;{}\n/*/ \"ab\"//c".chars().collect();
1138 let mut text = String::from("fn main() {\n let x = 1;\n}\n");
1139 let mut seed = 0x1234_5678_9abc_def0u64;
1140
1141 for _ in 0..400 {
1142 let len = text.chars().count();
1144 let at = if len == 0 {
1145 0
1146 } else {
1147 (lcg(&mut seed) as usize) % (len + 1)
1148 };
1149 let byte_at = text.char_indices().nth(at).map_or(text.len(), |(b, _)| b);
1150 if len > 4 && lcg(&mut seed) % 2 == 0 {
1151 if let Some((b, c)) = text[byte_at..].char_indices().next() {
1153 let start = byte_at + b;
1154 text.replace_range(start..start + c.len_utf8(), "");
1155 }
1156 } else {
1157 let c = alphabet[(lcg(&mut seed) as usize) % alphabet.len()];
1158 text.insert(byte_at, c);
1159 }
1160
1161 let inc = cache.highlight(&text);
1162 let full = one_shot.highlight(&text);
1163 assert_eq!(inc, full, "incremental != one-shot for {text:?}");
1164 covers(&text, &inc);
1165 }
1166 }
1167
1168 #[test]
1170 fn idle_rehighlight_relexes_zero_lines() {
1171 let eco = Ecosystem::with_builtins();
1172 let mut cache = eco.incremental_highlighter_for_path("f.rs");
1173 let text = "fn a() {}\nfn b() {}\nfn c() {}\n";
1174 let _ = cache.highlight(text);
1175 let _ = cache.highlight(text); assert_eq!(cache.last_relexed(), 0, "idle re-render must re-lex nothing");
1177 }
1178
1179 #[test]
1183 fn single_line_edit_relexes_locally() {
1184 let eco = Ecosystem::with_builtins();
1185 let mut cache = eco.incremental_highlighter_for_path("f.rs");
1186 let mut text = String::new();
1187 for i in 0..60 {
1188 text.push_str(&format!("let v{i} = {i};\n"));
1189 }
1190 let _ = cache.highlight(&text); let edited = text.replacen("let v30 = 30;", "let v30 = 999;", 1);
1193 let _ = cache.highlight(&edited);
1194 assert_eq!(
1195 cache.last_relexed(),
1196 1,
1197 "a local edit must re-lex exactly its own line (state re-converges immediately)"
1198 );
1199 }
1200
1201 #[test]
1204 fn cross_line_state_change_propagates_then_converges() {
1205 let eco = Ecosystem::with_builtins();
1206 let mut cache = eco.incremental_highlighter_for_path("f.rs");
1207 let text = "let a = 1;\nlet b = 2;\nlet c = 3;\nlet d = 4;\n";
1208 let _ = cache.highlight(text);
1209 let edited = "let a = 1; /*\nstill comment\n*/ let c = 3;\nlet d = 4;\n";
1211 let inc = cache.highlight(edited);
1212 assert_eq!(inc, eco.highlighter_for_path("f.rs").highlight(edited));
1213 assert!(
1215 cache.last_relexed() <= 3,
1216 "re-lex must stop once the block comment closes and state reconverges"
1217 );
1218 }
1219}