1use std::cell::RefCell;
12use std::collections::BTreeSet;
13use lru::LruCache;
14use std::num::NonZeroUsize;
15use std::ops::Range;
16use std::path::Path;
17use std::sync::Arc;
18
19use syntect::{
20 easy::HighlightLines,
21 highlighting::{FontStyle, HighlightState, Highlighter, Style, ThemeSet},
22 parsing::{ParseState, SyntaxReference, SyntaxSet},
23};
24
25use once_cell::sync::Lazy;
26
27use crate::editor::buffer::Version;
28
29static SYNTAX_SET: Lazy<SyntaxSet> = Lazy::new(SyntaxSet::load_defaults_nonewlines);
30static THEME_SET: Lazy<ThemeSet> = Lazy::new(ThemeSet::load_defaults);
31
32pub const DEFAULT_CHECKPOINT_INTERVAL: usize = 100;
33
34#[derive(Debug, Clone, PartialEq)]
35pub struct Token {
36 pub range: Range<usize>,
37 pub style: Style,
38}
39
40impl Token {
41 pub fn new(start: usize, end: usize, style: Style) -> Self {
42 Self {
43 range: start..end,
44 style,
45 }
46 }
47}
48
49#[derive(Debug, Clone)]
50pub struct StyledSpan {
51 pub range: Range<usize>,
53 pub style: Style,
54}
55
56#[derive(Debug, Clone)]
57struct Checkpoint {
58 line: usize,
59 highlight_state: HighlightState,
60 parse_state: ParseState,
61}
62
63#[derive(Debug, Clone)]
64struct CachedLine {
65 version: Version,
66 tokens: Arc<Vec<Token>>,
67}
68
69#[derive(Clone)]
79pub struct HighlighterState {
80 pub theme_name: String,
81 pub syntax_name: String,
82 pub checkpoint_interval: usize,
83 checkpoints: Vec<Checkpoint>,
84}
85
86const _: () = {
89 fn _assert_send<T: Send>() {}
90 fn _check() {
91 _assert_send::<HighlightState>();
92 _assert_send::<ParseState>();
93 _assert_send::<Checkpoint>();
94 _assert_send::<HighlighterState>();
95 }
96};
97
98
99#[derive(Debug)]
100pub struct SyntaxHighlighter {
101 pub theme_name: String,
102 pub syntax_name: String,
103 checkpoint_interval: usize,
104 checkpoints: RefCell<Vec<Checkpoint>>,
105 line_cache: RefCell<LruCache<usize, CachedLine>>,
111 cache_line_index: RefCell<BTreeSet<usize>>,
115}
116
117impl Default for SyntaxHighlighter {
118 fn default() -> Self {
119 Self::new()
120 }
121}
122
123impl Clone for SyntaxHighlighter {
124 fn clone(&self) -> Self {
125 Self {
126 theme_name: self.theme_name.clone(),
127 syntax_name: self.syntax_name.clone(),
128 checkpoint_interval: self.checkpoint_interval,
129 checkpoints: RefCell::new(self.checkpoints.borrow().clone()),
130 line_cache: RefCell::new(LruCache::new(NonZeroUsize::new(10_000).unwrap())),
131 cache_line_index: RefCell::new(BTreeSet::new()),
132 }
133 }
134}
135
136impl SyntaxHighlighter {
137 pub fn new() -> Self {
138 Self {
139 theme_name: "base16-ocean.dark".to_string(),
140 syntax_name: "Plain Text".to_string(),
141 checkpoint_interval: DEFAULT_CHECKPOINT_INTERVAL,
142 checkpoints: RefCell::new(Vec::new()),
143 line_cache: RefCell::new(LruCache::new(NonZeroUsize::new(10_000).unwrap())),
144 cache_line_index: RefCell::new(BTreeSet::new()),
145 }
146 }
147
148 pub fn for_path(path: &Path) -> Self {
149 let syntax_name = SYNTAX_SET
150 .find_syntax_for_file(path)
151 .ok()
152 .flatten()
153 .map(|s| s.name.clone())
154 .unwrap_or_else(|| "Plain Text".to_string());
155
156 Self::new().with_syntax(&syntax_name)
157 }
158
159 pub fn plain() -> Self {
160 Self::new()
161 }
162
163 pub fn with_theme(mut self, theme_name: &str) -> Self {
164 self.theme_name = theme_name.to_string();
165 self
166 }
167
168 pub fn with_syntax(mut self, syntax_name: &str) -> Self {
169 self.syntax_name = syntax_name.to_string();
170 self
171 }
172
173 pub fn snapshot_state(&self) -> HighlighterState {
182 HighlighterState {
183 theme_name: self.theme_name.clone(),
184 syntax_name: self.syntax_name.clone(),
185 checkpoint_interval: self.checkpoint_interval,
186 checkpoints: self.checkpoints.borrow().clone(),
187 }
188 }
189
190 pub fn from_state(state: HighlighterState) -> Self {
195 Self {
196 theme_name: state.theme_name,
197 syntax_name: state.syntax_name,
198 checkpoint_interval: state.checkpoint_interval,
199 checkpoints: RefCell::new(state.checkpoints),
200 line_cache: RefCell::new(LruCache::new(NonZeroUsize::new(10_000).unwrap())),
201 cache_line_index: RefCell::new(BTreeSet::new()),
202 }
203 }
204
205 fn get_syntax(&self) -> &SyntaxReference {
206 SYNTAX_SET
207 .find_syntax_by_name(&self.syntax_name)
208 .unwrap_or_else(|| SYNTAX_SET.find_syntax_plain_text())
209 }
210
211 pub fn clear_cache(&self) {
212 self.checkpoints.borrow_mut().clear();
213 self.line_cache.borrow_mut().clear();
214 self.cache_line_index.borrow_mut().clear();
215 }
216
217 pub fn invalidate_from(&self, from_line: usize) {
218 self.checkpoints.borrow_mut().retain(|c| c.line < from_line);
222
223 let mut cache = self.line_cache.borrow_mut();
224 let mut index = self.cache_line_index.borrow_mut();
225 let to_remove: Vec<usize> = index.range(from_line..).copied().collect();
228 for k in to_remove {
229 cache.pop(&k);
230 index.remove(&k);
231 }
232 }
233
234 fn get_or_create_checkpoint(&self, lines: &[String], line: usize) -> Checkpoint {
235 let interval = self.checkpoint_interval;
236 let checkpoint_line = (line / interval) * interval;
237
238 if let Some(cp) = self
239 .checkpoints
240 .borrow()
241 .iter()
242 .find(|c| c.line == checkpoint_line)
243 .cloned()
244 {
245 return cp;
246 }
247
248 let checkpoints_borrow = self.checkpoints.borrow();
249
250 let prev_checkpoint = checkpoints_borrow
251 .iter()
252 .filter(|c| c.line < checkpoint_line)
253 .max_by_key(|c| c.line)
254 .cloned();
255
256 let theme = &THEME_SET.themes[&self.theme_name];
257 let highlighter = Highlighter::new(theme);
258
259 let (highlight_state, parse_state, start_line) = if let Some(ref prev) = prev_checkpoint {
260 (
261 prev.highlight_state.clone(),
262 prev.parse_state.clone(),
263 prev.line,
264 )
265 } else {
266 let parse = ParseState::new(self.get_syntax());
271 let high =
272 HighlightState::new(&highlighter, syntect::parsing::ScopeStack::new());
273 (high, parse, 0)
274 };
275
276 drop(checkpoints_borrow); let mut h = HighlightLines::from_state(theme, highlight_state, parse_state);
279
280 for i in start_line..checkpoint_line {
281 if i < lines.len() {
282 let _ = h.highlight_line(&lines[i], &SYNTAX_SET);
283 }
284 }
285
286 let (highlight_state, parse_state) = h.state();
287
288 let checkpoint = Checkpoint {
289 line: checkpoint_line,
290 highlight_state,
291 parse_state,
292 };
293
294 let mut checkpoints = self.checkpoints.borrow_mut();
295 let mut insert_pos = checkpoints
296 .iter()
297 .position(|c| c.line > checkpoint_line)
298 .unwrap_or(checkpoints.len());
299 checkpoints.insert(insert_pos, checkpoint);
300
301 const MAX_CHECKPOINTS: usize = 256;
304 if checkpoints.len() > MAX_CHECKPOINTS {
305 let mut distances: Vec<(usize, usize)> = checkpoints
307 .iter()
308 .enumerate()
309 .map(|(i, c)| (i, c.line.abs_diff(checkpoint_line)))
310 .collect();
311 distances.sort_by_key(|&(_, d)| d);
313 use std::collections::HashSet;
314 let keep: HashSet<usize> = distances.into_iter().take(MAX_CHECKPOINTS).map(|(i, _)| i).collect();
315
316 let mut kept = Vec::with_capacity(MAX_CHECKPOINTS);
318 for (i, cp) in checkpoints.iter().enumerate() {
319 if keep.contains(&i) {
320 kept.push(cp.clone());
321 }
322 }
323 *checkpoints = kept;
324
325 insert_pos = checkpoints
327 .iter()
328 .position(|c| c.line == checkpoint_line)
329 .unwrap_or(checkpoints.len());
330 }
331
332 checkpoints[insert_pos].clone()
333 }
334
335 fn get_cached_line(&self, line: usize, version: Version) -> Option<Arc<Vec<Token>>> {
336 let mut cache = self.line_cache.borrow_mut();
337 match cache.get(&line) {
338 Some(cached) if cached.version == version => Some(cached.tokens.clone()),
339 _ => None,
340 }
341 }
342
343 fn cache_line(&self, line: usize, version: Version, tokens: Arc<Vec<Token>>) {
344 let mut cache = self.line_cache.borrow_mut();
345 let mut index = self.cache_line_index.borrow_mut();
346 if let Some((evicted_key, _)) = cache.push(line, CachedLine { version, tokens })
347 && evicted_key != line {
348 index.remove(&evicted_key);
350 }
351 index.insert(line);
352 }
353
354 pub fn highlight_line_at(&self, lines: &[String], line: usize, version: Version) -> Arc<Vec<Token>> {
355 if let Some(tokens) = self.get_cached_line(line, version) {
356 return tokens;
357 }
358
359 let checkpoint = self.get_or_create_checkpoint(lines, line);
360 let theme = &THEME_SET.themes[&self.theme_name];
361 let mut h = HighlightLines::from_state(
362 theme,
363 checkpoint.highlight_state.clone(),
364 checkpoint.parse_state.clone(),
365 );
366
367 for i in checkpoint.line..line {
368 if i < lines.len() {
369 let _ = h.highlight_line(&lines[i], &SYNTAX_SET);
370 }
371 }
372
373 let highlighted = if line < lines.len() {
374 h.highlight_line(&lines[line], &SYNTAX_SET)
375 .unwrap_or_default()
376 } else {
377 Vec::new()
378 };
379
380 let tokens_vec = self.tokens_from_highlight(&highlighted);
381 let tokens_arc = Arc::new(tokens_vec);
382
383 self.cache_line(line, version, tokens_arc.clone());
384
385 tokens_arc
386 }
387
388 fn tokens_from_highlight(&self, highlighted: &[(Style, &str)]) -> Vec<Token> {
389 let mut tokens = Vec::new();
390 let mut byte_offset = 0;
391
392 for (style, text) in highlighted {
393 let end = byte_offset + text.len();
394 tokens.push(Token::new(byte_offset, end, *style));
395 byte_offset = end;
396 }
397 tokens
398 }
399
400 pub fn highlight_range(
401 &self,
402 all_lines: &[String],
403 version: Version,
404 start_row: usize,
405 count: usize,
406 ) -> Vec<Vec<StyledSpan>> {
407 let end_row = (start_row + count).min(all_lines.len());
408
409 (start_row..end_row)
410 .map(|i| {
411 let tokens = self.highlight_line_at(all_lines, i, version);
412 self.tokens_to_spans(&all_lines[i], tokens.as_ref())
413 })
414 .collect()
415 }
416
417 pub fn highlight_tokens(
418 &self,
419 all_lines: &[String],
420 version: Version,
421 start_line: usize,
422 count: usize,
423 ) -> Vec<Arc<Vec<Token>>> {
424 let end_line = (start_line + count).min(all_lines.len());
425
426 (start_line..end_line)
427 .map(|i| self.highlight_line_at(all_lines, i, version))
428 .collect()
429 }
430
431 pub fn highlight_tokens_cancellable(
435 &self,
436 all_lines: &[String],
437 version: Version,
438 start_line: usize,
439 count: usize,
440 is_cancelled: impl Fn() -> bool,
441 ) -> Vec<Arc<Vec<Token>>> {
442 let end_line = (start_line + count).min(all_lines.len());
443 let mut results = Vec::new();
444 for i in start_line..end_line {
445 if is_cancelled() {
446 return results;
447 }
448 results.push(self.highlight_line_at(all_lines, i, version));
449 }
450 results
451 }
452
453 pub fn highlight_lines_batch(
462 &self,
463 all_lines: &[String],
464 version: Version,
465 requested_lines: &[usize],
466 ) -> Vec<(usize, Arc<Vec<Token>>)> {
467 if requested_lines.is_empty() {
468 return Vec::new();
469 }
470
471 let interval = self.checkpoint_interval;
472 let theme = &THEME_SET.themes[&self.theme_name];
473 let mut results = Vec::with_capacity(requested_lines.len());
474
475 let mut parse_pos: usize = 0;
479 let mut h: Option<HighlightLines> = None;
480
481 for &line in requested_lines {
482 if line >= all_lines.len() {
483 results.push((line, Arc::new(Vec::new())));
484 continue;
485 }
486
487 if let Some(tokens) = self.get_cached_line(line, version) {
488 results.push((line, tokens));
489 h = None;
491 continue;
492 }
493
494 let can_continue =
496 h.is_some() && parse_pos <= line && (line - parse_pos) <= interval;
497
498 if !can_continue {
499 let checkpoint = self.get_or_create_checkpoint(all_lines, line);
500 h = Some(HighlightLines::from_state(
501 theme,
502 checkpoint.highlight_state.clone(),
503 checkpoint.parse_state.clone(),
504 ));
505 parse_pos = checkpoint.line;
506 }
507
508 let highlighter = h.as_mut().unwrap();
509
510 while parse_pos < line {
512 if parse_pos < all_lines.len() {
513 let _ = highlighter.highlight_line(&all_lines[parse_pos], &SYNTAX_SET);
514 }
515 parse_pos += 1;
516 }
517
518 let highlighted = highlighter
520 .highlight_line(&all_lines[line], &SYNTAX_SET)
521 .unwrap_or_default();
522 parse_pos = line + 1;
523
524 let tokens_vec = self.tokens_from_highlight(&highlighted);
525 let tokens_arc = Arc::new(tokens_vec);
526 self.cache_line(line, version, tokens_arc.clone());
527 results.push((line, tokens_arc));
528 }
529
530 results
531 }
532
533 pub fn tokens_to_spans(&self, _line: &str, tokens: &[Token]) -> Vec<StyledSpan> {
534 tokens
537 .iter()
538 .map(|t| StyledSpan { range: t.range.clone(), style: t.style })
539 .collect()
540 }
541}
542
543#[inline]
544pub fn to_ratatui_color(c: syntect::highlighting::Color) -> ratatui::style::Color {
545 ratatui::style::Color::Rgb(c.r, c.g, c.b)
546}
547
548pub fn to_ratatui_style(style: &Style) -> ratatui::style::Style {
549 use ratatui::style::Modifier;
550 let mut s = ratatui::style::Style::default().fg(to_ratatui_color(style.foreground));
551 if style.font_style.contains(FontStyle::BOLD) {
552 s = s.add_modifier(Modifier::BOLD);
553 }
554 if style.font_style.contains(FontStyle::ITALIC) {
555 s = s.add_modifier(Modifier::ITALIC);
556 }
557 if style.font_style.contains(FontStyle::UNDERLINE) {
558 s = s.add_modifier(Modifier::UNDERLINED);
559 }
560 s
561}
562
563pub const BRACE_PAIRS: &[(char, char)] = &[('{', '}'), ('(', ')'), ('[', ']')];
564
565pub fn find_matching_brace(
568 lines: &[String],
569 cursor_row: usize,
570 cursor_col: usize,
571) -> Option<(usize, usize)> {
572 let line = lines.get(cursor_row)?;
573
574 let ch = line.chars().nth(cursor_col)?;
576
577 let (open, close, forward) = BRACE_PAIRS.iter().find_map(|&(o, c)| {
578 if ch == o {
579 Some((o, c, true))
580 } else if ch == c {
581 Some((o, c, false))
582 } else {
583 None
584 }
585 })?;
586
587 if forward {
588 let mut depth = 0i32;
590 for (row, line) in lines.iter().enumerate().skip(cursor_row) {
591 let start_char = if row == cursor_row { cursor_col } else { 0 };
592 for (char_idx, c) in line.chars().enumerate().skip(start_char) {
593 if c == open {
594 depth += 1;
595 }
596 if c == close {
597 depth -= 1;
598 if depth == 0 && (row, char_idx) != (cursor_row, cursor_col) {
599 return Some((row, char_idx));
600 }
601 }
602 }
603 }
604 } else {
605 let mut depth = 0i32;
607 for row in (0..=cursor_row).rev() {
608 let line = &lines[row];
609 let chars: Vec<char> = line.chars().collect();
610 let end_char = if row == cursor_row {
611 cursor_col + 1
612 } else {
613 chars.len()
614 };
615
616 for (char_idx, c) in chars.into_iter().enumerate().take(end_char).rev() {
617 if c == close {
618 depth += 1;
619 }
620 if c == open {
621 depth -= 1;
622 if depth == 0 && (row, char_idx) != (cursor_row, cursor_col) {
623 return Some((row, char_idx));
624 }
625 }
626 }
627 }
628 }
629 None
630}
631
632#[cfg(test)]
633mod tests {
634 use super::*;
635
636 #[test]
637 fn brace_match_forward() {
638 let lines = vec!["fn f() {".to_string(), " x".to_string(), "}".to_string()];
639 assert_eq!(find_matching_brace(&lines, 0, 7), Some((2, 0)));
640 }
641
642 #[test]
643 fn brace_match_backward() {
644 let lines = vec!["fn f() {".to_string(), " x".to_string(), "}".to_string()];
645 assert_eq!(find_matching_brace(&lines, 2, 0), Some((0, 7)));
646 }
647
648 #[test]
649 fn brace_match_nested() {
650 let lines = vec!["if (a && (b || c)) {".to_string(), "}".to_string()];
651 assert_eq!(find_matching_brace(&lines, 0, 3), Some((0, 17)));
652 }
653
654 #[test]
655 fn no_match_for_plain_char() {
656 let lines = vec!["let x = 1;".to_string()];
657 assert_eq!(find_matching_brace(&lines, 0, 4), None);
658 }
659
660 #[test]
661 fn brace_match_with_utf8() {
662 let lines = vec!["∞ ()".to_string()];
665 assert_eq!(find_matching_brace(&lines, 0, 2), Some((0, 3)));
667 assert_eq!(find_matching_brace(&lines, 0, 3), Some((0, 2)));
668 }
669
670 #[test]
671 fn tokens_to_spans_with_utf8_characters() {
672 let highlighter = SyntaxHighlighter::new();
673 let line = "∞ code";
674 let tokens = vec![
681 Token::new(0, 3, Style::default()), Token::new(3, 4, Style::default()), Token::new(4, 8, Style::default()), ];
685
686 let spans = highlighter.tokens_to_spans(line, &tokens);
687 assert_eq!(spans.len(), 3);
688 assert_eq!(&line[spans[0].range.start..spans[0].range.end], "∞");
689 assert_eq!(&line[spans[1].range.start..spans[1].range.end], " ");
690 assert_eq!(&line[spans[2].range.start..spans[2].range.end], "code");
691 }
692
693 #[test]
694 fn token_creation() {
695 let token = Token::new(0, 5, Style::default());
696 assert_eq!(token.range, 0..5);
697 }
698
699 #[test]
700 fn highlighting_produces_tokens() {
701 let highlighter = SyntaxHighlighter::new();
702 let lines = vec!["fn main() {}".to_string()];
703 let tokens = highlighter.highlight_line_at(&lines, 0, Version::new());
704 assert!(!tokens.is_empty());
705 }
706
707 #[test]
708 fn cache_works() {
709 let highlighter = SyntaxHighlighter::new();
710 let version = Version::new();
711 let lines = vec!["fn main() {}".to_string()];
712
713 let tokens1 = highlighter.highlight_line_at(&lines, 0, version);
714 let tokens2 = highlighter.highlight_line_at(&lines, 0, version);
715
716 assert_eq!(tokens1.len(), tokens2.len());
717 }
718
719 #[test]
720 fn checkpoint_interval() {
721 let highlighter = SyntaxHighlighter::new();
722 let mut h = highlighter.clone();
723 h.checkpoint_interval = 50;
724
725 let version = Version::new();
726 let lines: Vec<String> = (0..200).map(|i| format!("line {}", i)).collect();
727
728 for i in 0..200 {
729 h.highlight_line_at(&lines, i, version);
730 }
731
732 let checkpoints = h.checkpoints.borrow();
733 assert!(!checkpoints.is_empty());
734 assert!(checkpoints.iter().all(|c| c.line % 50 == 0));
735 }
736
737 #[test]
738 fn checkpoint_eviction_keeps_nearby_checkpoints() {
739 let mut h = SyntaxHighlighter::new();
742 h.checkpoint_interval = 1;
743 let version = Version::new();
744 let lines: Vec<String> = (0..300).map(|i| format!("line {}", i)).collect();
745
746 for i in 0..300 {
749 if i == 150 { continue; }
750 h.highlight_line_at(&lines, i, version);
751 }
752
753 h.highlight_line_at(&lines, 150, version);
755
756 let checkpoints = h.checkpoints.borrow();
757 assert!(checkpoints.len() <= 256);
759
760 let cp_lines: Vec<usize> = checkpoints.iter().map(|c| c.line).collect();
761 assert!(cp_lines.contains(&150));
762
763 let min = *cp_lines.first().unwrap();
766 let max = *cp_lines.last().unwrap();
767 assert!(min <= 150 && 150 <= max);
768 assert!(max - min < 256);
769 }
770
771 #[test]
772 fn spans_from_highlight_cover_entire_line() {
773 let h = SyntaxHighlighter::new();
774 let version = Version::new();
775 let lines = vec!["fn test_span_cover() { let x = 1; }".to_string()];
776
777 let tokens = h.highlight_line_at(&lines, 0, version);
778 let spans = h.tokens_to_spans(&lines[0], tokens.as_ref());
779
780 let mut reconstructed = String::new();
781 for s in &spans {
782 reconstructed.push_str(&lines[0][s.range.start..s.range.end]);
783 }
784
785 assert_eq!(reconstructed, lines[0]);
786 }
787
788 #[test]
789 fn invalidate_from_boundary_behaviour() {
790 let mut h = SyntaxHighlighter::new();
792 h.checkpoint_interval = 50;
793 let version = Version::new();
794 let lines: Vec<String> = (0..200).map(|i| format!("line {}", i)).collect();
795
796 for i in 0..200 {
798 h.highlight_line_at(&lines, i, version);
799 }
800
801 let cp_lines: Vec<usize> = h.checkpoints.borrow().iter().map(|c| c.line).collect();
803 assert!(cp_lines.contains(&100));
804
805 h.invalidate_from(100);
807 let cp_after: Vec<usize> = h.checkpoints.borrow().iter().map(|c| c.line).collect();
808 assert!(!cp_after.contains(&100));
810 assert!(cp_after.iter().all(|&l| l < 100));
811
812 for i in 0..200 {
814 h.highlight_line_at(&lines, i, version);
815 }
816
817 h.invalidate_from(101);
820 let cp_after_mid: Vec<usize> = h.checkpoints.borrow().iter().map(|c| c.line).collect();
821 assert!(cp_after_mid.contains(&100));
822 }
823
824 #[test]
829 fn cache_miss_on_version_change() {
830 let h = SyntaxHighlighter::new();
831 let lines = vec!["fn main() {}".to_string()];
832 let v1 = Version::from_raw(0);
833 let v2 = Version::from_raw(1);
834
835 let t1 = h.highlight_line_at(&lines, 0, v1);
836 let t2 = h.highlight_line_at(&lines, 0, v2);
837
838 assert_eq!(t1.len(), t2.len());
841 }
842
843 #[test]
844 fn cache_hit_returns_same_arc() {
845 let h = SyntaxHighlighter::new();
846 let version = Version::new();
847 let lines = vec!["let x = 42;".to_string()];
848
849 let t1 = h.highlight_line_at(&lines, 0, version);
850 let t2 = h.highlight_line_at(&lines, 0, version);
851
852 assert!(Arc::ptr_eq(&t1, &t2));
854 }
855
856 #[test]
857 fn clear_cache_forces_recomputation() {
858 let h = SyntaxHighlighter::new();
859 let version = Version::new();
860 let lines = vec!["let x = 42;".to_string()];
861
862 let t1 = h.highlight_line_at(&lines, 0, version);
863 h.clear_cache();
864 let t2 = h.highlight_line_at(&lines, 0, version);
865
866 assert!(!Arc::ptr_eq(&t1, &t2));
868 assert_eq!(t1.len(), t2.len());
870 }
871
872 #[test]
873 fn clear_cache_removes_all_checkpoints() {
874 let mut h = SyntaxHighlighter::new();
875 h.checkpoint_interval = 10;
876 let version = Version::new();
877 let lines: Vec<String> = (0..50).map(|i| format!("line {}", i)).collect();
878
879 for i in 0..50 {
880 h.highlight_line_at(&lines, i, version);
881 }
882 assert!(!h.checkpoints.borrow().is_empty());
883
884 h.clear_cache();
885 assert!(h.checkpoints.borrow().is_empty());
886 }
887
888 #[test]
889 fn lru_eviction_does_not_panic() {
890 let h = SyntaxHighlighter::new();
893 let version = Version::new();
894 let lines: Vec<String> = (0..10_200).map(|i| format!("// line {}", i)).collect();
895
896 for i in 0..10_200 {
897 h.highlight_line_at(&lines, i, version);
898 }
899 }
901
902 #[test]
907 fn invalidate_from_zero_clears_line_cache_entirely() {
908 let h = SyntaxHighlighter::new();
909 let version = Version::new();
910 let lines: Vec<String> = (0..10).map(|i| format!("let x{} = {};", i, i)).collect();
911
912 for i in 0..10 {
914 h.highlight_line_at(&lines, i, version);
915 }
916
917 h.invalidate_from(0);
918
919 for (i, _line) in lines.iter().enumerate().take(10) {
921 let before_ptr = {
922 h.highlight_line_at(&lines, i, version)
924 };
925 let after_ptr = h.highlight_line_at(&lines, i, version);
929 assert!(Arc::ptr_eq(&before_ptr, &after_ptr), "line {} should now be cached", i);
930 }
931 }
932
933 #[test]
934 fn invalidate_from_preserves_lines_before_edit_point() {
935 let h = SyntaxHighlighter::new();
936 let version = Version::new();
937 let lines: Vec<String> = (0..20).map(|i| format!("let x{} = {};", i, i)).collect();
938
939 let arcs_before: Vec<_> = (0..20)
941 .map(|i| h.highlight_line_at(&lines, i, version))
942 .collect();
943
944 h.invalidate_from(10);
946
947 for (i, _line) in arcs_before.iter().enumerate().take(10) {
949 let after = h.highlight_line_at(&lines, i, version);
950 assert!(
951 Arc::ptr_eq(&arcs_before[i], &after),
952 "line {} before invalidation point should still be cached", i
953 );
954 }
955 }
956
957 #[test]
958 fn invalidate_from_removes_lines_at_and_after_edit_point() {
959 let h = SyntaxHighlighter::new();
960 let version = Version::new();
961 let lines: Vec<String> = (0..20).map(|i| format!("let x{} = {};", i, i)).collect();
962
963 let arcs_before: Vec<_> = (0..20)
964 .map(|i| h.highlight_line_at(&lines, i, version))
965 .collect();
966
967 h.invalidate_from(10);
968
969 for (i, _line) in arcs_before.iter().enumerate().take(20).skip(10) {
972 let after = h.highlight_line_at(&lines, i, version);
973 assert!(
974 !Arc::ptr_eq(&arcs_before[i], &after),
975 "line {} at/after invalidation point should be evicted", i
976 );
977 }
978 }
979
980 #[test]
981 fn invalidate_from_exact_boundary_removes_that_line() {
982 let h = SyntaxHighlighter::new();
983 let version = Version::new();
984 let lines = vec!["fn a() {}".to_string(), "fn b() {}".to_string()];
985
986 let arc0 = h.highlight_line_at(&lines, 0, version);
987 let arc1 = h.highlight_line_at(&lines, 1, version);
988
989 h.invalidate_from(1);
991
992 let after0 = h.highlight_line_at(&lines, 0, version);
994 assert!(Arc::ptr_eq(&arc0, &after0), "line 0 must remain cached");
995
996 let after1 = h.highlight_line_at(&lines, 1, version);
998 assert!(!Arc::ptr_eq(&arc1, &after1), "line 1 must be evicted");
999 }
1000
1001 #[test]
1006 fn edit_at_beginning_invalidates_all_lines() {
1007 let h = SyntaxHighlighter::new();
1008 let version = Version::new();
1009 let mut lines: Vec<String> = (0..10).map(|i| format!("let x{} = {};", i, i)).collect();
1010
1011 let arcs_before: Vec<_> = (0..10)
1012 .map(|i| h.highlight_line_at(&lines, i, version))
1013 .collect();
1014
1015 lines.insert(0, "// new comment".to_string());
1017 h.invalidate_from(0);
1018
1019 for (i, _line) in arcs_before.iter().enumerate().take(10) {
1022 let after = h.highlight_line_at(&lines, i, version);
1023 assert!(!Arc::ptr_eq(&arcs_before[i], &after),
1024 "line {} must be recomputed after edit at line 0", i);
1025 }
1026 }
1027
1028 #[test]
1029 fn edit_at_middle_preserves_preceding_lines() {
1030 let h = SyntaxHighlighter::new();
1031 let version = Version::new();
1032 let mut lines: Vec<String> = (0..20).map(|i| format!("// line {}", i)).collect();
1033
1034 let arcs_before: Vec<_> = (0..20)
1035 .map(|i| h.highlight_line_at(&lines, i, version))
1036 .collect();
1037
1038 lines[10] = "// CHANGED".to_string();
1040 h.invalidate_from(10);
1041
1042 for (i, _line) in arcs_before.iter().enumerate().take(10) {
1044 let after = h.highlight_line_at(&lines, i, version);
1045 assert!(Arc::ptr_eq(&arcs_before[i], &after),
1046 "line {} before edit should still be cached", i);
1047 }
1048
1049 for (i, _line) in arcs_before.iter().enumerate().take(20).skip(10) {
1051 let after = h.highlight_line_at(&lines, i, version);
1052 assert!(!Arc::ptr_eq(&arcs_before[i], &after),
1053 "line {} at/after edit point should be recomputed", i);
1054 }
1055 }
1056
1057 #[test]
1058 fn edit_at_last_line_preserves_rest() {
1059 let h = SyntaxHighlighter::new();
1060 let version = Version::new();
1061 let mut lines: Vec<String> = (0..5).map(|i| format!("// line {}", i)).collect();
1062
1063 let arcs: Vec<_> = (0..5)
1064 .map(|i| h.highlight_line_at(&lines, i, version))
1065 .collect();
1066
1067 lines[4] = "// CHANGED LAST".to_string();
1068 h.invalidate_from(4);
1069
1070 for (i, _line) in arcs.iter().enumerate().take(4) {
1071 let after = h.highlight_line_at(&lines, i, version);
1072 assert!(Arc::ptr_eq(&arcs[i], &after), "line {} must stay cached", i);
1073 }
1074
1075 let after4 = h.highlight_line_at(&lines, 4, version);
1076 assert!(!Arc::ptr_eq(&arcs[4], &after4), "last line must be recomputed");
1077 }
1078
1079 #[test]
1080 fn sequential_edits_produce_consistent_results() {
1081 let h = SyntaxHighlighter::new();
1083 let version = Version::new();
1084 let mut lines: Vec<String> = (0..30).map(|i| format!("let v{} = {};", i, i)).collect();
1085
1086 for i in 0..30 {
1088 h.highlight_line_at(&lines, i, version);
1089 }
1090
1091 lines[25] = "let v25 = 9999;".to_string();
1093 h.invalidate_from(25);
1094 for i in 0..30 {
1095 let t = h.highlight_line_at(&lines, i, version);
1096 assert!(!t.is_empty() || lines[i].is_empty(), "line {} should produce tokens", i);
1097 }
1098
1099 lines[10] = "let v10 = 8888;".to_string();
1101 h.invalidate_from(10);
1102 for i in 0..30 {
1103 let t = h.highlight_line_at(&lines, i, version);
1104 assert!(!t.is_empty() || lines[i].is_empty());
1105 }
1106
1107 lines[0] = "// file header".to_string();
1109 h.invalidate_from(0);
1110 for i in 0..30 {
1111 let t = h.highlight_line_at(&lines, i, version);
1112 assert!(!t.is_empty() || lines[i].is_empty());
1113 }
1114 }
1115
1116 #[test]
1121 fn scroll_down_highlights_new_lines() {
1122 let h = SyntaxHighlighter::new();
1123 let version = Version::new();
1124 let lines: Vec<String> = (0..100).map(|i| format!("// line {}", i)).collect();
1125
1126 for i in 0..40 {
1128 h.highlight_line_at(&lines, i, version);
1129 }
1130
1131 let second_screen: Vec<_> = (40..80)
1133 .map(|i| h.highlight_line_at(&lines, i, version))
1134 .collect();
1135
1136 assert_eq!(second_screen.len(), 40);
1137 for tokens in &second_screen {
1138 assert!(!tokens.is_empty());
1139 }
1140 }
1141
1142 #[test]
1143 fn scroll_back_up_uses_cache() {
1144 let h = SyntaxHighlighter::new();
1145 let version = Version::new();
1146 let lines: Vec<String> = (0..100).map(|i| format!("// line {}", i)).collect();
1147
1148 let first_screen: Vec<_> = (0..40)
1150 .map(|i| h.highlight_line_at(&lines, i, version))
1151 .collect();
1152
1153 for i in 40..80 {
1155 h.highlight_line_at(&lines, i, version);
1156 }
1157
1158 for (i, _line) in first_screen.iter().enumerate().take(40) {
1160 let after = h.highlight_line_at(&lines, i, version);
1161 assert!(Arc::ptr_eq(&first_screen[i], &after),
1162 "line {} should be cache-hit on scroll back", i);
1163 }
1164 }
1165
1166 #[test]
1167 fn highlight_window_jump_no_panic() {
1168 let h = SyntaxHighlighter::new();
1170 let version = Version::new();
1171 let lines: Vec<String> = (0..6000).map(|i| format!("// line {}", i)).collect();
1172
1173 for i in 0..40 {
1175 h.highlight_line_at(&lines, i, version);
1176 }
1177
1178 for i in 5000..5040 {
1180 let t = h.highlight_line_at(&lines, i, version);
1181 assert!(!t.is_empty());
1182 }
1183 }
1184
1185 #[test]
1190 fn highlight_range_returns_correct_count() {
1191 let h = SyntaxHighlighter::new();
1192 let version = Version::new();
1193 let lines: Vec<String> = (0..20).map(|i| format!("let x{} = {};", i, i)).collect();
1194
1195 let spans = h.highlight_range(&lines, version, 5, 10);
1196 assert_eq!(spans.len(), 10, "should return exactly 10 rows");
1197 }
1198
1199 #[test]
1200 fn highlight_range_zero_count_returns_empty() {
1201 let h = SyntaxHighlighter::new();
1202 let version = Version::new();
1203 let lines = vec!["let x = 1;".to_string()];
1204
1205 let spans = h.highlight_range(&lines, version, 0, 0);
1206 assert!(spans.is_empty());
1207 }
1208
1209 #[test]
1210 fn highlight_range_clamped_to_buffer_length() {
1211 let h = SyntaxHighlighter::new();
1212 let version = Version::new();
1213 let lines: Vec<String> = (0..5).map(|i| format!("// {}", i)).collect();
1214
1215 let spans = h.highlight_range(&lines, version, 0, 100);
1217 assert_eq!(spans.len(), 5);
1218 }
1219
1220 #[test]
1221 fn highlight_range_spans_cover_full_line() {
1222 let h = SyntaxHighlighter::new().with_syntax("Rust");
1223 let version = Version::new();
1224 let lines = vec![
1225 "fn main() {".to_string(),
1226 " let x = 42;".to_string(),
1227 "}".to_string(),
1228 ];
1229
1230 let all_spans = h.highlight_range(&lines, version, 0, 3);
1231 for (row, spans) in all_spans.iter().enumerate() {
1232 let mut reconstructed = String::new();
1233 for s in spans {
1234 reconstructed.push_str(&lines[row][s.range.start..s.range.end]);
1235 }
1236 assert_eq!(reconstructed, lines[row], "spans must reconstruct line {}", row);
1237 }
1238 }
1239
1240 #[test]
1241 fn highlight_tokens_matches_individual_highlight_line_at() {
1242 let h = SyntaxHighlighter::new().with_syntax("Rust");
1243 let version = Version::new();
1244 let lines: Vec<String> = (0..10).map(|i| format!("let v{} = {};", i, i)).collect();
1245
1246 let batch = h.highlight_tokens(&lines, version, 0, 10);
1247
1248 let h2 = SyntaxHighlighter::new().with_syntax("Rust");
1250 for (i, _line) in batch.iter().enumerate().take(10) {
1251 let individual = h2.highlight_line_at(&lines, i, version);
1252 assert_eq!(
1253 batch[i].len(), individual.len(),
1254 "batch and individual token counts must match for line {}", i
1255 );
1256 }
1257 }
1258
1259 #[test]
1260 fn highlight_tokens_beyond_bounds_clamped() {
1261 let h = SyntaxHighlighter::new();
1262 let version = Version::new();
1263 let lines = vec!["one".to_string(), "two".to_string()];
1264
1265 let tokens = h.highlight_tokens(&lines, version, 0, 1000);
1266 assert_eq!(tokens.len(), 2, "should clamp to buffer length");
1267 }
1268
1269 #[test]
1274 fn batch_matches_individual() {
1275 let h1 = SyntaxHighlighter::new().with_syntax("Rust");
1276 let version = Version::new();
1277 let lines: Vec<String> = (0..20).map(|i| format!("let v{} = {};", i, i)).collect();
1278
1279 let requested: Vec<usize> = (0..20).collect();
1280 let batch = h1.highlight_lines_batch(&lines, version, &requested);
1281
1282 let h2 = SyntaxHighlighter::new().with_syntax("Rust");
1283 for (line, tokens) in &batch {
1284 let individual = h2.highlight_line_at(&lines, *line, version);
1285 assert_eq!(
1286 tokens.len(), individual.len(),
1287 "batch and individual token counts must match for line {}", line
1288 );
1289 }
1290 }
1291
1292 #[test]
1293 fn batch_empty_request() {
1294 let h = SyntaxHighlighter::new();
1295 let version = Version::new();
1296 let lines = vec!["hello".to_string()];
1297 let result = h.highlight_lines_batch(&lines, version, &[]);
1298 assert!(result.is_empty());
1299 }
1300
1301 #[test]
1302 fn batch_out_of_bounds_returns_empty_tokens() {
1303 let h = SyntaxHighlighter::new();
1304 let version = Version::new();
1305 let lines = vec!["hello".to_string()];
1306 let result = h.highlight_lines_batch(&lines, version, &[0, 5, 100]);
1307 assert_eq!(result.len(), 3);
1308 assert!(!result[0].1.is_empty()); assert!(result[1].1.is_empty()); assert!(result[2].1.is_empty()); }
1312
1313 #[test]
1314 fn batch_with_gaps() {
1315 let h1 = SyntaxHighlighter::new().with_syntax("Rust");
1316 let version = Version::new();
1317 let lines: Vec<String> = (0..200).map(|i| format!("let v{} = {};", i, i)).collect();
1318
1319 let requested = vec![0, 5, 50, 100, 150, 199];
1320 let batch = h1.highlight_lines_batch(&lines, version, &requested);
1321
1322 let h2 = SyntaxHighlighter::new().with_syntax("Rust");
1323 for (line, tokens) in &batch {
1324 let individual = h2.highlight_line_at(&lines, *line, version);
1325 assert_eq!(
1326 tokens.len(), individual.len(),
1327 "batch and individual must match for line {}", line
1328 );
1329 }
1330 }
1331
1332 #[test]
1333 fn batch_reuses_state_for_adjacent_lines() {
1334 let h1 = SyntaxHighlighter::new().with_syntax("Rust");
1337 let version = Version::new();
1338 let lines: Vec<String> = (0..50).map(|i| format!("fn f{}() {{ }}", i)).collect();
1339
1340 let requested: Vec<usize> = (10..30).collect();
1341 let batch = h1.highlight_lines_batch(&lines, version, &requested);
1342
1343 assert_eq!(batch.len(), 20);
1344 let h2 = SyntaxHighlighter::new().with_syntax("Rust");
1345 for (line, tokens) in &batch {
1346 let individual = h2.highlight_line_at(&lines, *line, version);
1347 assert_eq!(tokens.len(), individual.len(), "mismatch at line {}", line);
1348 }
1349 }
1350
1351 #[test]
1356 fn cancellable_no_cancel_same_as_normal() {
1357 let h = SyntaxHighlighter::new();
1358 let version = Version::new();
1359 let lines: Vec<String> = (0..10).map(|i| format!("// line {}", i)).collect();
1360
1361 let normal = h.highlight_tokens(&lines, version, 0, 10);
1362 let h2 = SyntaxHighlighter::new();
1364 let cancellable = h2.highlight_tokens_cancellable(&lines, version, 0, 10, || false);
1365
1366 assert_eq!(normal.len(), cancellable.len());
1367 for i in 0..normal.len() {
1368 assert_eq!(normal[i].len(), cancellable[i].len(),
1369 "token count for line {} must match", i);
1370 }
1371 }
1372
1373 #[test]
1374 fn cancellable_cancel_before_start_returns_empty() {
1375 let h = SyntaxHighlighter::new();
1376 let version = Version::new();
1377 let lines: Vec<String> = (0..10).map(|i| format!("// line {}", i)).collect();
1378
1379 let result = h.highlight_tokens_cancellable(&lines, version, 0, 10, || true);
1380 assert!(result.is_empty(), "cancelled before start must return empty vec");
1381 }
1382
1383 #[test]
1384 fn cancellable_cancel_mid_way_returns_partial() {
1385 use std::sync::atomic::{AtomicBool, Ordering};
1386 use std::sync::Arc;
1387
1388 let h = SyntaxHighlighter::new();
1392 let version = Version::new();
1393 let lines: Vec<String> = (0..50).map(|i| format!("// line {}", i)).collect();
1394 let cancelled = Arc::new(AtomicBool::new(false));
1395 let cancelled_clone = cancelled.clone();
1396
1397 let handle = std::thread::spawn(move || {
1401 std::thread::sleep(std::time::Duration::from_micros(1));
1402 cancelled_clone.store(true, Ordering::SeqCst);
1403 });
1404
1405 let result = h.highlight_tokens_cancellable(
1406 &lines, version, 0, 50,
1407 || cancelled.load(Ordering::SeqCst),
1408 );
1409 handle.join().unwrap();
1410
1411 assert!(result.len() <= 50, "result cannot exceed total lines");
1413 }
1414
1415 #[test]
1420 fn empty_lines_array_returns_empty() {
1421 let h = SyntaxHighlighter::new();
1422 let version = Version::new();
1423 let lines: Vec<String> = vec![];
1424
1425 let tokens = h.highlight_line_at(&lines, 0, version);
1427 assert!(tokens.is_empty());
1428
1429 let range = h.highlight_range(&lines, version, 0, 10);
1430 assert!(range.is_empty());
1431 }
1432
1433 #[test]
1434 fn out_of_bounds_line_returns_empty_tokens() {
1435 let h = SyntaxHighlighter::new();
1436 let version = Version::new();
1437 let lines = vec!["let x = 1;".to_string()];
1438
1439 let tokens = h.highlight_line_at(&lines, 999, version);
1440 assert!(tokens.is_empty(), "OOB line must return empty tokens, not panic");
1441 }
1442
1443 #[test]
1444 fn single_empty_string_line_no_panic() {
1445 let h = SyntaxHighlighter::new();
1446 let version = Version::new();
1447 let lines = vec!["".to_string()];
1448
1449 let tokens = h.highlight_line_at(&lines, 0, version);
1450 let _ = tokens;
1452 }
1453
1454 #[test]
1455 fn very_long_line_no_panic() {
1456 let h = SyntaxHighlighter::new().with_syntax("Rust");
1457 let version = Version::new();
1458 let long_line = "x".repeat(100_000);
1459 let lines = vec![format!("let s = \"{}\";", long_line)];
1460
1461 let tokens = h.highlight_line_at(&lines, 0, version);
1462 assert!(!tokens.is_empty());
1463 }
1464
1465 #[test]
1466 fn unicode_heavy_line_no_panic() {
1467 let h = SyntaxHighlighter::new();
1468 let version = Version::new();
1469 let lines = vec!["// ∞ ★ 𝓤𝓷𝓲𝓬𝓸𝓭𝓮 αβγδ 日本語 العربية".to_string()];
1470
1471 let tokens = h.highlight_line_at(&lines, 0, version);
1472 assert!(!tokens.is_empty());
1473 }
1474
1475 #[test]
1476 fn line_of_only_whitespace_no_panic() {
1477 let h = SyntaxHighlighter::new();
1478 let version = Version::new();
1479 let lines = vec![" \t ".to_string()];
1480
1481 let _ = h.highlight_line_at(&lines, 0, version);
1482 }
1483
1484 #[test]
1485 fn null_bytes_in_line_no_panic() {
1486 let h = SyntaxHighlighter::new();
1488 let version = Version::new();
1489 let lines = vec!["normal text".to_string()];
1490 let _ = h.highlight_line_at(&lines, 0, version);
1491 }
1492
1493 #[test]
1498 fn for_path_rs_detects_rust_syntax() {
1499 use std::path::Path;
1500 let h = SyntaxHighlighter::for_path(Path::new("src/main.rs"));
1501 assert_eq!(h.syntax_name, "Rust");
1502 }
1503
1504 #[test]
1505 fn for_path_unknown_extension_uses_plain_text() {
1506 use std::path::Path;
1507 let h = SyntaxHighlighter::for_path(Path::new("file.xyzdoesnotexist123"));
1508 assert_eq!(h.syntax_name, "Plain Text");
1509 }
1510
1511 #[test]
1512 fn snapshot_state_roundtrip_preserves_config() {
1513 let original = SyntaxHighlighter::new()
1514 .with_theme("base16-ocean.dark")
1515 .with_syntax("Rust");
1516
1517 let state = original.snapshot_state();
1518 assert_eq!(state.theme_name, "base16-ocean.dark");
1519 assert_eq!(state.syntax_name, "Rust");
1520
1521 let restored = SyntaxHighlighter::from_state(state);
1522 assert_eq!(restored.theme_name, "base16-ocean.dark");
1523 assert_eq!(restored.syntax_name, "Rust");
1524 }
1525
1526 #[test]
1527 fn snapshot_state_carries_accumulated_checkpoints() {
1528 let mut h = SyntaxHighlighter::new();
1529 h.checkpoint_interval = 10;
1530 let version = Version::new();
1531 let lines: Vec<String> = (0..30).map(|i| format!("// line {}", i)).collect();
1532
1533 for i in 0..30 {
1534 h.highlight_line_at(&lines, i, version);
1535 }
1536
1537 let state = h.snapshot_state();
1538 assert!(!state.checkpoints.is_empty());
1540 }
1541
1542 #[test]
1543 fn from_state_has_empty_line_cache() {
1544 let mut h = SyntaxHighlighter::new();
1547 h.checkpoint_interval = 1;
1548 let version = Version::new();
1549 let lines = vec!["fn test() {}".to_string()];
1550
1551 let arc_original = h.highlight_line_at(&lines, 0, version);
1552 let state = h.snapshot_state();
1553
1554 let h2 = SyntaxHighlighter::from_state(state);
1555 let arc_restored = h2.highlight_line_at(&lines, 0, version);
1556
1557 assert!(!Arc::ptr_eq(&arc_original, &arc_restored));
1559 assert_eq!(arc_original.len(), arc_restored.len());
1560 }
1561
1562 #[test]
1567 fn to_ratatui_color_converts_rgb() {
1568 use syntect::highlighting::Color;
1569 let c = Color { r: 255, g: 128, b: 0, a: 255 };
1570 let ratatui_color = to_ratatui_color(c);
1571 assert_eq!(ratatui_color, ratatui::style::Color::Rgb(255, 128, 0));
1572 }
1573
1574 #[test]
1575 fn to_ratatui_style_plain_produces_no_modifiers() {
1576 use syntect::highlighting::Style;
1577 let style = Style::default();
1578 let ratatui_style = to_ratatui_style(&style);
1579 assert_eq!(ratatui_style.add_modifier, ratatui::style::Modifier::empty());
1581 }
1582
1583 #[test]
1584 fn to_ratatui_style_bold_sets_modifier() {
1585 use syntect::highlighting::{FontStyle, Style};
1586 let style = Style { font_style: FontStyle::BOLD, ..Default::default() };
1587 let ratatui_style = to_ratatui_style(&style);
1588 assert!(ratatui_style.add_modifier.contains(ratatui::style::Modifier::BOLD));
1589 }
1590
1591 #[test]
1596 fn tokens_to_spans_empty_tokens_gives_empty_spans() {
1597 let h = SyntaxHighlighter::new();
1598 let spans = h.tokens_to_spans("", &[]);
1599 assert!(spans.is_empty());
1600 }
1601
1602 #[test]
1603 fn highlight_with_rust_syntax_produces_more_tokens_than_plain() {
1604 let rust_h = SyntaxHighlighter::new().with_syntax("Rust");
1607 let plain_h = SyntaxHighlighter::new(); let version = Version::new();
1609 let lines = vec!["fn main() { let x = 42; }".to_string()];
1610
1611 let rust_tokens = rust_h.highlight_line_at(&lines, 0, version);
1612 let plain_tokens = plain_h.highlight_line_at(&lines, 0, version);
1613
1614 assert!(
1615 rust_tokens.len() > plain_tokens.len(),
1616 "Rust highlighting should produce more token splits than plain text"
1617 );
1618 }
1619
1620 #[test]
1621 fn sequential_lines_produce_valid_spans() {
1622 let h = SyntaxHighlighter::new().with_syntax("Rust");
1624 let version = Version::new();
1625 let lines = vec![
1626 "fn add(a: i32, b: i32) -> i32 {".to_string(),
1627 " a + b".to_string(),
1628 "}".to_string(),
1629 ];
1630
1631 for (i, line) in lines.iter().enumerate() {
1632 let tokens = h.highlight_line_at(&lines, i, version);
1633 let spans = h.tokens_to_spans(line, tokens.as_ref());
1634 let mut reconstructed = String::new();
1635 for s in &spans {
1636 reconstructed.push_str(&line[s.range.start..s.range.end]);
1637 }
1638 assert_eq!(reconstructed, *line, "spans must reconstruct line {}", i);
1639 }
1640 }
1641
1642 #[test]
1647 fn realistic_editing_session() {
1648 let h = SyntaxHighlighter::new().with_syntax("Rust");
1651 let version = Version::new();
1652 let mut lines: Vec<String> = vec![
1653 "use std::collections::HashMap;".to_string(),
1654 "".to_string(),
1655 "fn process(data: &[u8]) -> HashMap<u32, Vec<u8>> {".to_string(),
1656 " let mut map = HashMap::new();".to_string(),
1657 " for (i, &b) in data.iter().enumerate() {".to_string(),
1658 " map.entry(b as u32).or_default().push(i as u8);".to_string(),
1659 " }".to_string(),
1660 " map".to_string(),
1661 "}".to_string(),
1662 "".to_string(),
1663 "fn main() {".to_string(),
1664 " let data = vec![1, 2, 3, 1, 2];".to_string(),
1665 " let result = process(&data);".to_string(),
1666 " println!(\"{:?}\", result);".to_string(),
1667 "}".to_string(),
1668 ];
1669
1670 for i in 0..lines.len() {
1672 let t = h.highlight_line_at(&lines, i, version);
1673 assert!(!t.is_empty() || lines[i].is_empty(), "line {} must be highlighted", i);
1674 }
1675
1676 lines[5] = " map.entry(b as u32).or_insert_with(Vec::new).push(i as u8);".to_string();
1678 h.invalidate_from(5);
1679
1680 for i in 0..lines.len() {
1682 let t = h.highlight_line_at(&lines, i, version);
1683 assert!(!t.is_empty() || lines[i].is_empty());
1684 }
1685
1686 lines.push("// end of file".to_string());
1692 h.invalidate_from(lines.len() - 1);
1693
1694 let last = h.highlight_line_at(&lines, lines.len() - 1, version);
1695 assert!(!last.is_empty());
1696 }
1697
1698 #[test]
1699 fn many_small_edits_at_same_line_remain_correct() {
1700 let h = SyntaxHighlighter::new().with_syntax("Rust");
1702 let version = Version::new();
1703 let mut lines = vec!["fn main".to_string()];
1704
1705 let suffixes = ["(", ") {", "\n let x = 0;", "\n}"];
1706 for suffix in &suffixes {
1707 lines[0].push_str(suffix);
1708 h.invalidate_from(0);
1709 let t = h.highlight_line_at(&lines, 0, version);
1710 assert!(!t.is_empty(), "tokens must be non-empty after appending '{}'", suffix);
1711 }
1712 }
1713}