1use crate::model::buffer::Buffer;
20use crate::primitives::grammar::GrammarRegistry;
21use crate::primitives::highlighter::{
22 highlight_color, HighlightCategory, HighlightSpan, Highlighter, Language,
23};
24use crate::view::theme::Theme;
25use std::ops::Range;
26use std::path::Path;
27use std::sync::Arc;
28use syntect::parsing::SyntaxSet;
29
30fn scope_to_category(scope: &str) -> Option<HighlightCategory> {
32 let scope_lower = scope.to_lowercase();
33
34 if scope_lower.starts_with("comment") {
36 return Some(HighlightCategory::Comment);
37 }
38
39 if scope_lower.starts_with("string") {
41 return Some(HighlightCategory::String);
42 }
43
44 if scope_lower.starts_with("markup.heading") || scope_lower.starts_with("entity.name.section") {
48 return Some(HighlightCategory::Keyword); }
50 if scope_lower.starts_with("markup.bold") {
52 return Some(HighlightCategory::Constant); }
54 if scope_lower.starts_with("markup.italic") {
56 return Some(HighlightCategory::Variable); }
58 if scope_lower.starts_with("markup.raw") || scope_lower.starts_with("markup.inline.raw") {
60 return Some(HighlightCategory::String); }
62 if scope_lower.starts_with("markup.underline.link") {
64 return Some(HighlightCategory::Function); }
66 if scope_lower.starts_with("markup.underline") {
68 return Some(HighlightCategory::Function);
69 }
70 if scope_lower.starts_with("markup.quote") {
72 return Some(HighlightCategory::Comment); }
74 if scope_lower.starts_with("markup.list") {
76 return Some(HighlightCategory::Operator); }
78 if scope_lower.starts_with("markup.strikethrough") {
80 return Some(HighlightCategory::Comment); }
82
83 if scope_lower.starts_with("keyword.control")
85 || scope_lower.starts_with("keyword.other")
86 || scope_lower.starts_with("keyword.declaration")
87 || scope_lower.starts_with("keyword")
88 {
89 if !scope_lower.starts_with("keyword.operator") {
91 return Some(HighlightCategory::Keyword);
92 }
93 }
94
95 if scope_lower.starts_with("keyword.operator") || scope_lower.starts_with("punctuation") {
97 return Some(HighlightCategory::Operator);
98 }
99
100 if scope_lower.starts_with("entity.name.function")
102 || scope_lower.starts_with("support.function")
103 || scope_lower.starts_with("meta.function-call")
104 || scope_lower.starts_with("variable.function")
105 {
106 return Some(HighlightCategory::Function);
107 }
108
109 if scope_lower.starts_with("entity.name.type")
111 || scope_lower.starts_with("entity.name.class")
112 || scope_lower.starts_with("entity.name.struct")
113 || scope_lower.starts_with("entity.name.enum")
114 || scope_lower.starts_with("entity.name.interface")
115 || scope_lower.starts_with("entity.name.trait")
116 || scope_lower.starts_with("support.type")
117 || scope_lower.starts_with("support.class")
118 || scope_lower.starts_with("storage.type")
119 {
120 return Some(HighlightCategory::Type);
121 }
122
123 if scope_lower.starts_with("storage.modifier") {
125 return Some(HighlightCategory::Keyword);
126 }
127
128 if scope_lower.starts_with("constant.numeric")
130 || scope_lower.starts_with("constant.language.boolean")
131 {
132 return Some(HighlightCategory::Number);
133 }
134 if scope_lower.starts_with("constant") {
135 return Some(HighlightCategory::Constant);
136 }
137
138 if scope_lower.starts_with("variable.parameter")
140 || scope_lower.starts_with("variable.other")
141 || scope_lower.starts_with("variable.language")
142 {
143 return Some(HighlightCategory::Variable);
144 }
145
146 if scope_lower.starts_with("entity.name.tag")
148 || scope_lower.starts_with("support.other.property")
149 || scope_lower.starts_with("meta.object-literal.key")
150 || scope_lower.starts_with("variable.other.property")
151 || scope_lower.starts_with("variable.other.object.property")
152 {
153 return Some(HighlightCategory::Property);
154 }
155
156 if scope_lower.starts_with("entity.other.attribute")
158 || scope_lower.starts_with("meta.attribute")
159 || scope_lower.starts_with("entity.name.decorator")
160 {
161 return Some(HighlightCategory::Attribute);
162 }
163
164 if scope_lower.starts_with("variable") {
166 return Some(HighlightCategory::Variable);
167 }
168
169 None
170}
171
172#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
174pub enum HighlighterPreference {
175 #[default]
178 Auto,
179 TreeSitter,
181 TextMate,
183}
184
185#[derive(Default)]
187pub enum HighlightEngine {
188 TreeSitter(Box<Highlighter>),
190 TextMate(Box<TextMateEngine>),
192 #[default]
194 None,
195}
196
197pub struct TextMateEngine {
202 syntax_set: Arc<SyntaxSet>,
203 syntax_index: usize,
204 cache: Option<TextMateCache>,
205 last_buffer_len: usize,
206 ts_language: Option<Language>,
209}
210
211#[derive(Debug, Clone)]
212struct TextMateCache {
213 range: Range<usize>,
214 spans: Vec<CachedSpan>,
215}
216
217#[derive(Debug, Clone)]
218struct CachedSpan {
219 range: Range<usize>,
220 category: crate::primitives::highlighter::HighlightCategory,
221}
222
223const MAX_PARSE_BYTES: usize = 1024 * 1024;
225
226impl TextMateEngine {
227 pub fn new(syntax_set: Arc<SyntaxSet>, syntax_index: usize) -> Self {
229 Self {
230 syntax_set,
231 syntax_index,
232 cache: None,
233 last_buffer_len: 0,
234 ts_language: None,
235 }
236 }
237
238 pub fn with_language(
240 syntax_set: Arc<SyntaxSet>,
241 syntax_index: usize,
242 ts_language: Option<Language>,
243 ) -> Self {
244 Self {
245 syntax_set,
246 syntax_index,
247 cache: None,
248 last_buffer_len: 0,
249 ts_language,
250 }
251 }
252
253 pub fn language(&self) -> Option<&Language> {
255 self.ts_language.as_ref()
256 }
257
258 pub fn highlight_viewport(
263 &mut self,
264 buffer: &Buffer,
265 viewport_start: usize,
266 viewport_end: usize,
267 theme: &Theme,
268 context_bytes: usize,
269 ) -> Vec<HighlightSpan> {
270 use syntect::parsing::{ParseState, ScopeStack};
271
272 if let Some(cache) = &self.cache {
274 if cache.range.start <= viewport_start
275 && cache.range.end >= viewport_end
276 && self.last_buffer_len == buffer.len()
277 {
278 return cache
279 .spans
280 .iter()
281 .filter(|span| {
282 span.range.start < viewport_end && span.range.end > viewport_start
283 })
284 .map(|span| HighlightSpan {
285 range: span.range.clone(),
286 color: highlight_color(span.category, theme),
287 })
288 .collect();
289 }
290 }
291
292 let parse_start = viewport_start.saturating_sub(context_bytes);
294 let parse_end = (viewport_end + context_bytes).min(buffer.len());
295
296 if parse_end <= parse_start || parse_end - parse_start > MAX_PARSE_BYTES {
297 return Vec::new();
298 }
299
300 let syntax = &self.syntax_set.syntaxes()[self.syntax_index];
301 let mut state = ParseState::new(syntax);
302 let mut spans = Vec::new();
303
304 let content = buffer.slice_bytes(parse_start..parse_end);
306 let content_str = match std::str::from_utf8(&content) {
307 Ok(s) => s,
308 Err(_) => return Vec::new(),
309 };
310
311 let content_bytes = content_str.as_bytes();
314 let mut pos = 0;
315 let mut current_offset = parse_start;
316 let mut current_scopes = ScopeStack::new();
317
318 while pos < content_bytes.len() {
319 let line_start = pos;
320 let mut line_end = pos;
321
322 while line_end < content_bytes.len() {
324 if content_bytes[line_end] == b'\n' {
325 line_end += 1;
326 break;
327 } else if content_bytes[line_end] == b'\r' {
328 if line_end + 1 < content_bytes.len() && content_bytes[line_end + 1] == b'\n' {
329 line_end += 2; } else {
331 line_end += 1; }
333 break;
334 }
335 line_end += 1;
336 }
337
338 let line_bytes = &content_bytes[line_start..line_end];
340 let actual_line_byte_len = line_bytes.len();
341
342 let line_str = match std::str::from_utf8(line_bytes) {
344 Ok(s) => s,
345 Err(_) => {
346 pos = line_end;
347 current_offset += actual_line_byte_len;
348 continue;
349 }
350 };
351
352 let line_content = line_str.trim_end_matches(&['\r', '\n'][..]);
354 let line_for_syntect = if line_end < content_bytes.len() || line_str.ends_with('\n') {
355 format!("{}\n", line_content)
356 } else {
357 line_content.to_string()
358 };
359
360 let ops = match state.parse_line(&line_for_syntect, &self.syntax_set) {
361 Ok(ops) => ops,
362 Err(_) => {
363 pos = line_end;
364 current_offset += actual_line_byte_len;
365 continue;
366 }
367 };
368
369 let mut syntect_offset = 0;
373 let line_content_len = line_content.len();
374
375 for (op_offset, op) in ops {
376 let clamped_op_offset = op_offset.min(line_content_len);
378 if clamped_op_offset > syntect_offset {
379 if let Some(category) = Self::scope_stack_to_category(¤t_scopes) {
380 let byte_start = current_offset + syntect_offset;
381 let byte_end = current_offset + clamped_op_offset;
382 if byte_start < byte_end {
383 spans.push(CachedSpan {
384 range: byte_start..byte_end,
385 category,
386 });
387 }
388 }
389 }
390 syntect_offset = clamped_op_offset;
391
392 let _ = current_scopes.apply(&op);
393 }
394
395 if syntect_offset < line_content_len {
397 if let Some(category) = Self::scope_stack_to_category(¤t_scopes) {
398 let byte_start = current_offset + syntect_offset;
399 let byte_end = current_offset + line_content_len;
400 if byte_start < byte_end {
401 spans.push(CachedSpan {
402 range: byte_start..byte_end,
403 category,
404 });
405 }
406 }
407 }
408
409 pos = line_end;
411 current_offset += actual_line_byte_len;
412 }
413
414 Self::merge_adjacent_spans(&mut spans);
416
417 self.cache = Some(TextMateCache {
419 range: parse_start..parse_end,
420 spans: spans.clone(),
421 });
422 self.last_buffer_len = buffer.len();
423
424 spans
426 .into_iter()
427 .filter(|span| span.range.start < viewport_end && span.range.end > viewport_start)
428 .map(|span| HighlightSpan {
429 range: span.range,
430 color: highlight_color(span.category, theme),
431 })
432 .collect()
433 }
434
435 fn scope_stack_to_category(scopes: &syntect::parsing::ScopeStack) -> Option<HighlightCategory> {
437 for scope in scopes.as_slice().iter().rev() {
438 let scope_str = scope.build_string();
439 if let Some(cat) = scope_to_category(&scope_str) {
440 return Some(cat);
441 }
442 }
443 None
444 }
445
446 fn merge_adjacent_spans(spans: &mut Vec<CachedSpan>) {
448 if spans.len() < 2 {
449 return;
450 }
451
452 let mut write_idx = 0;
453 for read_idx in 1..spans.len() {
454 if spans[write_idx].category == spans[read_idx].category
455 && spans[write_idx].range.end == spans[read_idx].range.start
456 {
457 spans[write_idx].range.end = spans[read_idx].range.end;
458 } else {
459 write_idx += 1;
460 if write_idx != read_idx {
461 spans[write_idx] = spans[read_idx].clone();
462 }
463 }
464 }
465 spans.truncate(write_idx + 1);
466 }
467
468 pub fn invalidate_range(&mut self, edit_range: Range<usize>) {
470 if let Some(cache) = &self.cache {
471 if edit_range.start < cache.range.end && edit_range.end > cache.range.start {
472 self.cache = None;
473 }
474 }
475 }
476
477 pub fn invalidate_all(&mut self) {
479 self.cache = None;
480 }
481
482 pub fn syntax_name(&self) -> &str {
484 &self.syntax_set.syntaxes()[self.syntax_index].name
485 }
486}
487
488impl HighlightEngine {
489 pub fn for_file(path: &Path, registry: &GrammarRegistry) -> Self {
494 Self::for_file_with_preference(path, registry, HighlighterPreference::Auto)
495 }
496
497 pub fn for_file_with_languages(
504 path: &Path,
505 registry: &GrammarRegistry,
506 languages: &std::collections::HashMap<String, crate::config::LanguageConfig>,
507 ) -> Self {
508 Self::for_file_with_languages_and_preference(
509 path,
510 registry,
511 languages,
512 HighlighterPreference::Auto,
513 )
514 }
515
516 pub fn for_file_with_languages_and_preference(
518 path: &Path,
519 registry: &GrammarRegistry,
520 languages: &std::collections::HashMap<String, crate::config::LanguageConfig>,
521 preference: HighlighterPreference,
522 ) -> Self {
523 match preference {
524 HighlighterPreference::Auto | HighlighterPreference::TextMate => {
527 Self::textmate_for_file_with_languages(path, registry, languages)
528 }
529 HighlighterPreference::TreeSitter => {
530 if let Some(lang) = Language::from_path(path) {
531 if let Ok(highlighter) = Highlighter::new(lang) {
532 return Self::TreeSitter(Box::new(highlighter));
533 }
534 }
535 Self::None
536 }
537 }
538 }
539
540 pub fn for_file_with_preference(
542 path: &Path,
543 registry: &GrammarRegistry,
544 preference: HighlighterPreference,
545 ) -> Self {
546 match preference {
547 HighlighterPreference::Auto | HighlighterPreference::TextMate => {
550 Self::textmate_for_file(path, registry)
551 }
552 HighlighterPreference::TreeSitter => {
553 if let Some(lang) = Language::from_path(path) {
554 if let Ok(highlighter) = Highlighter::new(lang) {
555 return Self::TreeSitter(Box::new(highlighter));
556 }
557 }
558 Self::None
559 }
560 }
561 }
562
563 fn textmate_for_file(path: &Path, registry: &GrammarRegistry) -> Self {
565 let syntax_set = registry.syntax_set_arc();
566
567 let ts_language = Language::from_path(path);
569
570 if let Some(syntax) = registry.find_syntax_for_file(path) {
572 if let Some(index) = syntax_set
574 .syntaxes()
575 .iter()
576 .position(|s| s.name == syntax.name)
577 {
578 return Self::TextMate(Box::new(TextMateEngine::with_language(
579 syntax_set,
580 index,
581 ts_language,
582 )));
583 }
584 }
585
586 if let Some(lang) = ts_language {
589 if let Ok(highlighter) = Highlighter::new(lang) {
590 tracing::debug!(
591 "No TextMate grammar for {:?}, falling back to tree-sitter",
592 path.extension()
593 );
594 return Self::TreeSitter(Box::new(highlighter));
595 }
596 }
597
598 Self::None
599 }
600
601 fn textmate_for_file_with_languages(
603 path: &Path,
604 registry: &GrammarRegistry,
605 languages: &std::collections::HashMap<String, crate::config::LanguageConfig>,
606 ) -> Self {
607 let syntax_set = registry.syntax_set_arc();
608
609 let ts_language = Language::from_path(path);
611
612 if let Some(syntax) = registry.find_syntax_for_file_with_languages(path, languages) {
614 if let Some(index) = syntax_set
616 .syntaxes()
617 .iter()
618 .position(|s| s.name == syntax.name)
619 {
620 return Self::TextMate(Box::new(TextMateEngine::with_language(
621 syntax_set,
622 index,
623 ts_language,
624 )));
625 }
626 }
627
628 if let Some(lang) = ts_language {
631 if let Ok(highlighter) = Highlighter::new(lang) {
632 tracing::debug!(
633 "No TextMate grammar for {:?}, falling back to tree-sitter",
634 path.extension()
635 );
636 return Self::TreeSitter(Box::new(highlighter));
637 }
638 }
639
640 Self::None
641 }
642
643 pub fn highlight_viewport(
648 &mut self,
649 buffer: &Buffer,
650 viewport_start: usize,
651 viewport_end: usize,
652 theme: &Theme,
653 context_bytes: usize,
654 ) -> Vec<HighlightSpan> {
655 match self {
656 Self::TreeSitter(h) => {
657 h.highlight_viewport(buffer, viewport_start, viewport_end, theme, context_bytes)
658 }
659 Self::TextMate(h) => {
660 h.highlight_viewport(buffer, viewport_start, viewport_end, theme, context_bytes)
661 }
662 Self::None => Vec::new(),
663 }
664 }
665
666 pub fn invalidate_range(&mut self, edit_range: Range<usize>) {
668 match self {
669 Self::TreeSitter(h) => h.invalidate_range(edit_range),
670 Self::TextMate(h) => h.invalidate_range(edit_range),
671 Self::None => {}
672 }
673 }
674
675 pub fn invalidate_all(&mut self) {
677 match self {
678 Self::TreeSitter(h) => h.invalidate_all(),
679 Self::TextMate(h) => h.invalidate_all(),
680 Self::None => {}
681 }
682 }
683
684 pub fn has_highlighting(&self) -> bool {
686 !matches!(self, Self::None)
687 }
688
689 pub fn backend_name(&self) -> &str {
691 match self {
692 Self::TreeSitter(_) => "tree-sitter",
693 Self::TextMate(_) => "textmate",
694 Self::None => "none",
695 }
696 }
697
698 pub fn syntax_name(&self) -> Option<&str> {
700 match self {
701 Self::TreeSitter(_) => None, Self::TextMate(h) => Some(h.syntax_name()),
703 Self::None => None,
704 }
705 }
706
707 pub fn language(&self) -> Option<&Language> {
710 match self {
711 Self::TreeSitter(h) => Some(h.language()),
712 Self::TextMate(h) => h.language(),
713 Self::None => None,
714 }
715 }
716}
717
718pub fn highlight_string(
724 code: &str,
725 lang_hint: &str,
726 registry: &GrammarRegistry,
727 theme: &Theme,
728) -> Vec<HighlightSpan> {
729 use syntect::parsing::{ParseState, ScopeStack};
730
731 let syntax = match registry.syntax_set().find_syntax_by_token(lang_hint) {
733 Some(s) => s,
734 None => return Vec::new(),
735 };
736
737 let syntax_set = registry.syntax_set();
738 let mut state = ParseState::new(syntax);
739 let mut spans = Vec::new();
740 let mut current_scopes = ScopeStack::new();
741 let mut current_offset = 0;
742
743 for line in code.split_inclusive('\n') {
745 let line_start = current_offset;
746 let line_len = line.len();
747
748 let line_content = line.trim_end_matches(&['\r', '\n'][..]);
750 let line_for_syntect = if line.ends_with('\n') {
751 format!("{}\n", line_content)
752 } else {
753 line_content.to_string()
754 };
755
756 let ops = match state.parse_line(&line_for_syntect, syntax_set) {
757 Ok(ops) => ops,
758 Err(_) => {
759 current_offset += line_len;
760 continue;
761 }
762 };
763
764 let mut syntect_offset = 0;
765 let line_content_len = line_content.len();
766
767 for (op_offset, op) in ops {
768 let clamped_op_offset = op_offset.min(line_content_len);
769 if clamped_op_offset > syntect_offset {
770 if let Some(category) = scope_stack_to_category(¤t_scopes) {
771 let byte_start = line_start + syntect_offset;
772 let byte_end = line_start + clamped_op_offset;
773 if byte_start < byte_end {
774 spans.push(HighlightSpan {
775 range: byte_start..byte_end,
776 color: highlight_color(category, theme),
777 });
778 }
779 }
780 }
781 syntect_offset = clamped_op_offset;
782 let _ = current_scopes.apply(&op);
783 }
784
785 if syntect_offset < line_content_len {
787 if let Some(category) = scope_stack_to_category(¤t_scopes) {
788 let byte_start = line_start + syntect_offset;
789 let byte_end = line_start + line_content_len;
790 if byte_start < byte_end {
791 spans.push(HighlightSpan {
792 range: byte_start..byte_end,
793 color: highlight_color(category, theme),
794 });
795 }
796 }
797 }
798
799 current_offset += line_len;
800 }
801
802 merge_adjacent_highlight_spans(&mut spans);
804
805 spans
806}
807
808fn scope_stack_to_category(scopes: &syntect::parsing::ScopeStack) -> Option<HighlightCategory> {
810 for scope in scopes.as_slice().iter().rev() {
811 let scope_str = scope.build_string();
812 if let Some(cat) = scope_to_category(&scope_str) {
813 return Some(cat);
814 }
815 }
816 None
817}
818
819fn merge_adjacent_highlight_spans(spans: &mut Vec<HighlightSpan>) {
821 if spans.len() < 2 {
822 return;
823 }
824
825 let mut write_idx = 0;
826 for read_idx in 1..spans.len() {
827 if spans[write_idx].color == spans[read_idx].color
828 && spans[write_idx].range.end == spans[read_idx].range.start
829 {
830 spans[write_idx].range.end = spans[read_idx].range.end;
831 } else {
832 write_idx += 1;
833 if write_idx != read_idx {
834 spans[write_idx] = spans[read_idx].clone();
835 }
836 }
837 }
838 spans.truncate(write_idx + 1);
839}
840
841#[cfg(test)]
842mod tests {
843 use super::*;
844 use crate::view::theme;
845
846 #[test]
847 fn test_highlighter_preference_default() {
848 let pref = HighlighterPreference::default();
849 assert_eq!(pref, HighlighterPreference::Auto);
850 }
851
852 #[test]
853 fn test_highlight_engine_default() {
854 let engine = HighlightEngine::default();
855 assert!(!engine.has_highlighting());
856 assert_eq!(engine.backend_name(), "none");
857 }
858
859 #[test]
860 fn test_textmate_backend_selection() {
861 let registry =
862 GrammarRegistry::load(&crate::primitives::grammar::LocalGrammarLoader::new());
863
864 let engine = HighlightEngine::for_file(Path::new("test.rs"), ®istry);
866 assert_eq!(engine.backend_name(), "textmate");
867 assert!(engine.language().is_some());
869
870 let engine = HighlightEngine::for_file(Path::new("test.py"), ®istry);
871 assert_eq!(engine.backend_name(), "textmate");
872 assert!(engine.language().is_some());
873
874 let engine = HighlightEngine::for_file(Path::new("test.js"), ®istry);
875 assert_eq!(engine.backend_name(), "textmate");
876 assert!(engine.language().is_some());
877
878 let engine = HighlightEngine::for_file(Path::new("test.ts"), ®istry);
880 assert_eq!(engine.backend_name(), "tree-sitter");
881 assert!(engine.language().is_some());
882
883 let engine = HighlightEngine::for_file(Path::new("test.tsx"), ®istry);
884 assert_eq!(engine.backend_name(), "tree-sitter");
885 assert!(engine.language().is_some());
886 }
887
888 #[test]
889 fn test_tree_sitter_explicit_preference() {
890 let registry =
891 GrammarRegistry::load(&crate::primitives::grammar::LocalGrammarLoader::new());
892
893 let engine = HighlightEngine::for_file_with_preference(
895 Path::new("test.rs"),
896 ®istry,
897 HighlighterPreference::TreeSitter,
898 );
899 assert_eq!(engine.backend_name(), "tree-sitter");
900 }
901
902 #[test]
903 fn test_unknown_extension() {
904 let registry =
905 GrammarRegistry::load(&crate::primitives::grammar::LocalGrammarLoader::new());
906
907 let engine = HighlightEngine::for_file(Path::new("test.unknown_xyz_123"), ®istry);
909 let _ = engine.backend_name();
912 }
913
914 #[test]
915 fn test_highlight_viewport_empty_buffer_no_panic() {
916 let registry =
925 GrammarRegistry::load(&crate::primitives::grammar::LocalGrammarLoader::new());
926
927 let mut engine = HighlightEngine::for_file(Path::new("test.rs"), ®istry);
928
929 let buffer = Buffer::from_str("", 0);
931 let theme = Theme::load_builtin(theme::THEME_LIGHT).unwrap();
932
933 if let HighlightEngine::TextMate(ref mut tm) = engine {
937 let spans = tm.highlight_viewport(&buffer, 100, 200, &theme, 10);
939 assert!(spans.is_empty());
940 }
941 }
942
943 #[test]
947 fn test_textmate_engine_crlf_byte_offsets() {
948 let registry =
949 GrammarRegistry::load(&crate::primitives::grammar::LocalGrammarLoader::new());
950
951 let mut engine = HighlightEngine::for_file(Path::new("test.java"), ®istry);
952
953 let content = b"public\r\npublic\r\npublic\r\n";
959 let buffer = Buffer::from_bytes(content.to_vec());
960 let theme = Theme::load_builtin(theme::THEME_LIGHT).unwrap();
961
962 if let HighlightEngine::TextMate(ref mut tm) = engine {
963 let spans = tm.highlight_viewport(&buffer, 0, content.len(), &theme, 0);
965
966 eprintln!(
973 "Spans: {:?}",
974 spans.iter().map(|s| &s.range).collect::<Vec<_>>()
975 );
976
977 let has_span_at = |start: usize, end: usize| -> bool {
979 spans
980 .iter()
981 .any(|s| s.range.start <= start && s.range.end >= end)
982 };
983
984 assert!(
986 has_span_at(0, 6),
987 "Should have span covering bytes 0-6 (line 1 'public'). Spans: {:?}",
988 spans.iter().map(|s| &s.range).collect::<Vec<_>>()
989 );
990
991 assert!(
994 has_span_at(8, 14),
995 "Should have span covering bytes 8-14 (line 2 'public'). \
996 If this fails, CRLF offset drift is occurring. Spans: {:?}",
997 spans.iter().map(|s| &s.range).collect::<Vec<_>>()
998 );
999
1000 assert!(
1003 has_span_at(16, 22),
1004 "Should have span covering bytes 16-22 (line 3 'public'). \
1005 If this fails, CRLF offset drift is occurring. Spans: {:?}",
1006 spans.iter().map(|s| &s.range).collect::<Vec<_>>()
1007 );
1008 } else {
1009 panic!("Expected TextMate engine for .java file");
1010 }
1011 }
1012
1013 #[test]
1014 fn test_git_rebase_todo_highlighting() {
1015 let registry =
1016 GrammarRegistry::load(&crate::primitives::grammar::LocalGrammarLoader::new());
1017
1018 let engine = HighlightEngine::for_file(Path::new("git-rebase-todo"), ®istry);
1020 assert_eq!(engine.backend_name(), "textmate");
1021 assert!(engine.has_highlighting());
1022 }
1023
1024 #[test]
1025 fn test_git_commit_message_highlighting() {
1026 let registry =
1027 GrammarRegistry::load(&crate::primitives::grammar::LocalGrammarLoader::new());
1028
1029 let engine = HighlightEngine::for_file(Path::new("COMMIT_EDITMSG"), ®istry);
1031 assert_eq!(engine.backend_name(), "textmate");
1032 assert!(engine.has_highlighting());
1033
1034 let engine = HighlightEngine::for_file(Path::new("MERGE_MSG"), ®istry);
1036 assert_eq!(engine.backend_name(), "textmate");
1037 assert!(engine.has_highlighting());
1038 }
1039
1040 #[test]
1041 fn test_gitignore_highlighting() {
1042 let registry =
1043 GrammarRegistry::load(&crate::primitives::grammar::LocalGrammarLoader::new());
1044
1045 let engine = HighlightEngine::for_file(Path::new(".gitignore"), ®istry);
1047 assert_eq!(engine.backend_name(), "textmate");
1048 assert!(engine.has_highlighting());
1049
1050 let engine = HighlightEngine::for_file(Path::new(".dockerignore"), ®istry);
1052 assert_eq!(engine.backend_name(), "textmate");
1053 assert!(engine.has_highlighting());
1054 }
1055
1056 #[test]
1057 fn test_gitconfig_highlighting() {
1058 let registry =
1059 GrammarRegistry::load(&crate::primitives::grammar::LocalGrammarLoader::new());
1060
1061 let engine = HighlightEngine::for_file(Path::new(".gitconfig"), ®istry);
1063 assert_eq!(engine.backend_name(), "textmate");
1064 assert!(engine.has_highlighting());
1065
1066 let engine = HighlightEngine::for_file(Path::new(".gitmodules"), ®istry);
1068 assert_eq!(engine.backend_name(), "textmate");
1069 assert!(engine.has_highlighting());
1070 }
1071
1072 #[test]
1073 fn test_gitattributes_highlighting() {
1074 let registry =
1075 GrammarRegistry::load(&crate::primitives::grammar::LocalGrammarLoader::new());
1076
1077 let engine = HighlightEngine::for_file(Path::new(".gitattributes"), ®istry);
1079 assert_eq!(engine.backend_name(), "textmate");
1080 assert!(engine.has_highlighting());
1081 }
1082}