1use crate::style::{Style, Theme};
10
11#[cfg(any(
12 feature = "syntax-rust",
13 feature = "syntax-python",
14 feature = "syntax-javascript",
15 feature = "syntax-typescript",
16 feature = "syntax-go",
17 feature = "syntax-bash",
18 feature = "syntax-json",
19 feature = "syntax-toml",
20 feature = "syntax-c",
21 feature = "syntax-cpp",
22 feature = "syntax-java",
23 feature = "syntax-ruby",
24 feature = "syntax-css",
25 feature = "syntax-html",
26 feature = "syntax-yaml",
27))]
28use crate::style::SyntaxPalette;
29
30type HighlightedLines = Vec<Vec<(String, Style)>>;
31
32#[cfg(any(
37 feature = "syntax-rust",
38 feature = "syntax-python",
39 feature = "syntax-javascript",
40 feature = "syntax-typescript",
41 feature = "syntax-go",
42 feature = "syntax-bash",
43 feature = "syntax-json",
44 feature = "syntax-toml",
45 feature = "syntax-c",
46 feature = "syntax-cpp",
47 feature = "syntax-java",
48 feature = "syntax-ruby",
49 feature = "syntax-css",
50 feature = "syntax-html",
51 feature = "syntax-yaml",
52))]
53const HIGHLIGHT_NAMES: &[&str] = &[
54 "attribute",
55 "comment",
56 "constant",
57 "constant.builtin",
58 "constructor",
59 "embedded",
60 "function",
61 "function.builtin",
62 "function.macro",
63 "keyword",
64 "module",
65 "number",
66 "operator",
67 "property",
68 "property.builtin",
69 "punctuation",
70 "punctuation.bracket",
71 "punctuation.delimiter",
72 "punctuation.special",
73 "string",
74 "string.special",
75 "tag",
76 "type",
77 "type.builtin",
78 "variable",
79 "variable.builtin",
80 "variable.parameter",
81];
82
83#[cfg(any(
84 feature = "syntax-rust",
85 feature = "syntax-python",
86 feature = "syntax-javascript",
87 feature = "syntax-typescript",
88 feature = "syntax-go",
89 feature = "syntax-bash",
90 feature = "syntax-json",
91 feature = "syntax-toml",
92 feature = "syntax-c",
93 feature = "syntax-cpp",
94 feature = "syntax-java",
95 feature = "syntax-ruby",
96 feature = "syntax-css",
97 feature = "syntax-html",
98 feature = "syntax-yaml",
99))]
100use std::sync::OnceLock;
101
102#[cfg(any(
103 feature = "syntax-rust",
104 feature = "syntax-python",
105 feature = "syntax-javascript",
106 feature = "syntax-typescript",
107 feature = "syntax-go",
108 feature = "syntax-bash",
109 feature = "syntax-json",
110 feature = "syntax-toml",
111 feature = "syntax-c",
112 feature = "syntax-cpp",
113 feature = "syntax-java",
114 feature = "syntax-ruby",
115 feature = "syntax-css",
116 feature = "syntax-html",
117 feature = "syntax-yaml",
118))]
119use tree_sitter_highlight::HighlightConfiguration;
120
121#[cfg(any(
124 feature = "syntax-rust",
125 feature = "syntax-python",
126 feature = "syntax-javascript",
127 feature = "syntax-typescript",
128 feature = "syntax-go",
129 feature = "syntax-bash",
130 feature = "syntax-json",
131 feature = "syntax-toml",
132 feature = "syntax-c",
133 feature = "syntax-cpp",
134 feature = "syntax-java",
135 feature = "syntax-ruby",
136 feature = "syntax-css",
137 feature = "syntax-html",
138 feature = "syntax-yaml",
139))]
140fn get_config(lang: &str) -> Option<&'static HighlightConfiguration> {
141 match lang {
142 #[cfg(feature = "syntax-rust")]
143 "rust" | "rs" => {
144 static CFG: OnceLock<Option<HighlightConfiguration>> = OnceLock::new();
145 CFG.get_or_init(|| {
146 HighlightConfiguration::new(
147 tree_sitter_rust::LANGUAGE.into(),
148 "rust",
149 tree_sitter_rust::HIGHLIGHTS_QUERY,
150 tree_sitter_rust::INJECTIONS_QUERY,
151 "",
152 )
153 .ok()
154 .map(|mut c| {
155 c.configure(HIGHLIGHT_NAMES);
156 c
157 })
158 })
159 .as_ref()
160 }
161
162 #[cfg(feature = "syntax-python")]
163 "python" | "py" => {
164 static CFG: OnceLock<Option<HighlightConfiguration>> = OnceLock::new();
165 CFG.get_or_init(|| {
166 HighlightConfiguration::new(
167 tree_sitter_python::LANGUAGE.into(),
168 "python",
169 tree_sitter_python::HIGHLIGHTS_QUERY,
170 "",
171 "",
172 )
173 .ok()
174 .map(|mut c| {
175 c.configure(HIGHLIGHT_NAMES);
176 c
177 })
178 })
179 .as_ref()
180 }
181
182 #[cfg(feature = "syntax-javascript")]
183 "javascript" | "js" | "jsx" => {
184 static CFG: OnceLock<Option<HighlightConfiguration>> = OnceLock::new();
185 CFG.get_or_init(|| {
186 HighlightConfiguration::new(
187 tree_sitter_javascript::LANGUAGE.into(),
188 "javascript",
189 tree_sitter_javascript::HIGHLIGHT_QUERY,
190 tree_sitter_javascript::INJECTIONS_QUERY,
191 tree_sitter_javascript::LOCALS_QUERY,
192 )
193 .ok()
194 .map(|mut c| {
195 c.configure(HIGHLIGHT_NAMES);
196 c
197 })
198 })
199 .as_ref()
200 }
201
202 #[cfg(feature = "syntax-go")]
203 "go" | "golang" => {
204 static CFG: OnceLock<Option<HighlightConfiguration>> = OnceLock::new();
205 CFG.get_or_init(|| {
206 HighlightConfiguration::new(
207 tree_sitter_go::LANGUAGE.into(),
208 "go",
209 tree_sitter_go::HIGHLIGHTS_QUERY,
210 "",
211 "",
212 )
213 .ok()
214 .map(|mut c| {
215 c.configure(HIGHLIGHT_NAMES);
216 c
217 })
218 })
219 .as_ref()
220 }
221
222 #[cfg(feature = "syntax-bash")]
223 "bash" | "sh" | "shell" | "zsh" => {
224 static CFG: OnceLock<Option<HighlightConfiguration>> = OnceLock::new();
225 CFG.get_or_init(|| {
226 HighlightConfiguration::new(
227 tree_sitter_bash::LANGUAGE.into(),
228 "bash",
229 tree_sitter_bash::HIGHLIGHT_QUERY,
230 "",
231 "",
232 )
233 .ok()
234 .map(|mut c| {
235 c.configure(HIGHLIGHT_NAMES);
236 c
237 })
238 })
239 .as_ref()
240 }
241
242 #[cfg(feature = "syntax-json")]
243 "json" | "jsonc" => {
244 static CFG: OnceLock<Option<HighlightConfiguration>> = OnceLock::new();
245 CFG.get_or_init(|| {
246 HighlightConfiguration::new(
247 tree_sitter_json::LANGUAGE.into(),
248 "json",
249 tree_sitter_json::HIGHLIGHTS_QUERY,
250 "",
251 "",
252 )
253 .ok()
254 .map(|mut c| {
255 c.configure(HIGHLIGHT_NAMES);
256 c
257 })
258 })
259 .as_ref()
260 }
261
262 #[cfg(feature = "syntax-toml")]
263 "toml" => {
264 static CFG: OnceLock<Option<HighlightConfiguration>> = OnceLock::new();
265 CFG.get_or_init(|| {
266 HighlightConfiguration::new(
267 tree_sitter_toml_ng::LANGUAGE.into(),
268 "toml",
269 tree_sitter_toml_ng::HIGHLIGHTS_QUERY,
270 "",
271 "",
272 )
273 .ok()
274 .map(|mut c| {
275 c.configure(HIGHLIGHT_NAMES);
276 c
277 })
278 })
279 .as_ref()
280 }
281
282 #[cfg(feature = "syntax-c")]
283 "c" | "h" => {
284 static CFG: OnceLock<Option<HighlightConfiguration>> = OnceLock::new();
285 CFG.get_or_init(|| {
286 HighlightConfiguration::new(
287 tree_sitter_c::LANGUAGE.into(),
288 "c",
289 tree_sitter_c::HIGHLIGHT_QUERY,
290 "",
291 "",
292 )
293 .ok()
294 .map(|mut c| {
295 c.configure(HIGHLIGHT_NAMES);
296 c
297 })
298 })
299 .as_ref()
300 }
301
302 #[cfg(feature = "syntax-cpp")]
303 "cpp" | "c++" | "cxx" | "cc" | "hpp" => {
304 static CFG: OnceLock<Option<HighlightConfiguration>> = OnceLock::new();
305 CFG.get_or_init(|| {
306 #[cfg(feature = "syntax-c")]
307 let highlights = {
308 let mut combined = String::with_capacity(
309 tree_sitter_c::HIGHLIGHT_QUERY.len()
310 + tree_sitter_cpp::HIGHLIGHT_QUERY.len()
311 + 1,
312 );
313 combined.push_str(tree_sitter_c::HIGHLIGHT_QUERY);
314 combined.push('\n');
315 combined.push_str(tree_sitter_cpp::HIGHLIGHT_QUERY);
316 combined
317 };
318 #[cfg(not(feature = "syntax-c"))]
319 let highlights = tree_sitter_cpp::HIGHLIGHT_QUERY.to_string();
320
321 HighlightConfiguration::new(
322 tree_sitter_cpp::LANGUAGE.into(),
323 "cpp",
324 &highlights,
325 "",
326 "",
327 )
328 .ok()
329 .map(|mut c| {
330 c.configure(HIGHLIGHT_NAMES);
331 c
332 })
333 })
334 .as_ref()
335 }
336
337 #[cfg(feature = "syntax-typescript")]
338 "typescript" | "ts" => {
339 static CFG: OnceLock<Option<HighlightConfiguration>> = OnceLock::new();
340 CFG.get_or_init(|| {
341 let highlights = [
342 tree_sitter_javascript::HIGHLIGHT_QUERY,
343 tree_sitter_typescript::HIGHLIGHTS_QUERY,
344 ]
345 .join("\n");
346 HighlightConfiguration::new(
347 tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into(),
348 "typescript",
349 &highlights,
350 "",
351 tree_sitter_typescript::LOCALS_QUERY,
352 )
353 .ok()
354 .map(|mut c| {
355 c.configure(HIGHLIGHT_NAMES);
356 c
357 })
358 })
359 .as_ref()
360 }
361
362 #[cfg(feature = "syntax-typescript")]
363 "tsx" => {
364 static CFG: OnceLock<Option<HighlightConfiguration>> = OnceLock::new();
365 CFG.get_or_init(|| {
366 let highlights = [
367 tree_sitter_javascript::HIGHLIGHT_QUERY,
368 tree_sitter_javascript::JSX_HIGHLIGHT_QUERY,
369 tree_sitter_typescript::HIGHLIGHTS_QUERY,
370 ]
371 .join("\n");
372 HighlightConfiguration::new(
373 tree_sitter_typescript::LANGUAGE_TSX.into(),
374 "tsx",
375 &highlights,
376 "",
377 tree_sitter_typescript::LOCALS_QUERY,
378 )
379 .ok()
380 .map(|mut c| {
381 c.configure(HIGHLIGHT_NAMES);
382 c
383 })
384 })
385 .as_ref()
386 }
387
388 #[cfg(feature = "syntax-java")]
389 "java" => {
390 static CFG: OnceLock<Option<HighlightConfiguration>> = OnceLock::new();
391 CFG.get_or_init(|| {
392 HighlightConfiguration::new(
393 tree_sitter_java::LANGUAGE.into(),
394 "java",
395 tree_sitter_java::HIGHLIGHTS_QUERY,
396 "",
397 "",
398 )
399 .ok()
400 .map(|mut c| {
401 c.configure(HIGHLIGHT_NAMES);
402 c
403 })
404 })
405 .as_ref()
406 }
407
408 #[cfg(feature = "syntax-ruby")]
409 "ruby" | "rb" => {
410 static CFG: OnceLock<Option<HighlightConfiguration>> = OnceLock::new();
411 CFG.get_or_init(|| {
412 HighlightConfiguration::new(
413 tree_sitter_ruby::LANGUAGE.into(),
414 "ruby",
415 tree_sitter_ruby::HIGHLIGHTS_QUERY,
416 "",
417 tree_sitter_ruby::LOCALS_QUERY,
418 )
419 .ok()
420 .map(|mut c| {
421 c.configure(HIGHLIGHT_NAMES);
422 c
423 })
424 })
425 .as_ref()
426 }
427
428 #[cfg(feature = "syntax-css")]
429 "css" => {
430 static CFG: OnceLock<Option<HighlightConfiguration>> = OnceLock::new();
431 CFG.get_or_init(|| {
432 HighlightConfiguration::new(
433 tree_sitter_css::LANGUAGE.into(),
434 "css",
435 tree_sitter_css::HIGHLIGHTS_QUERY,
436 "",
437 "",
438 )
439 .ok()
440 .map(|mut c| {
441 c.configure(HIGHLIGHT_NAMES);
442 c
443 })
444 })
445 .as_ref()
446 }
447
448 #[cfg(feature = "syntax-html")]
449 "html" | "htm" => {
450 static CFG: OnceLock<Option<HighlightConfiguration>> = OnceLock::new();
451 CFG.get_or_init(|| {
452 HighlightConfiguration::new(
453 tree_sitter_html::LANGUAGE.into(),
454 "html",
455 tree_sitter_html::HIGHLIGHTS_QUERY,
456 tree_sitter_html::INJECTIONS_QUERY,
457 "",
458 )
459 .ok()
460 .map(|mut c| {
461 c.configure(HIGHLIGHT_NAMES);
462 c
463 })
464 })
465 .as_ref()
466 }
467
468 #[cfg(feature = "syntax-yaml")]
469 "yaml" | "yml" => {
470 static CFG: OnceLock<Option<HighlightConfiguration>> = OnceLock::new();
471 CFG.get_or_init(|| {
472 HighlightConfiguration::new(
473 tree_sitter_yaml::LANGUAGE.into(),
474 "yaml",
475 tree_sitter_yaml::HIGHLIGHTS_QUERY,
476 "",
477 "",
478 )
479 .ok()
480 .map(|mut c| {
481 c.configure(HIGHLIGHT_NAMES);
482 c
483 })
484 })
485 .as_ref()
486 }
487
488 _ => None,
489 }
490}
491
492#[cfg(any(
500 feature = "syntax-rust",
501 feature = "syntax-python",
502 feature = "syntax-javascript",
503 feature = "syntax-typescript",
504 feature = "syntax-go",
505 feature = "syntax-bash",
506 feature = "syntax-json",
507 feature = "syntax-toml",
508 feature = "syntax-c",
509 feature = "syntax-cpp",
510 feature = "syntax-java",
511 feature = "syntax-ruby",
512 feature = "syntax-css",
513 feature = "syntax-html",
514 feature = "syntax-yaml",
515))]
516fn highlight_name_to_style(name: &str, theme: &Theme) -> Style {
517 let syntax = &theme.syntax;
518 match name {
519 "keyword" => Style::new().fg(syntax.keyword),
520 "string" | "string.special" => Style::new().fg(syntax.string),
521 "comment" => Style::new().fg(theme.text_dim).italic(),
522 "number" => Style::new().fg(syntax.number),
523 "constant" | "constant.builtin" => Style::new().fg(syntax.constant),
524 "function" | "function.builtin" => Style::new().fg(syntax.function),
525 "function.macro" => Style::new().fg(syntax.macro_),
526 "type" | "type.builtin" | "constructor" => Style::new().fg(syntax.type_),
527 "variable.builtin" => Style::new().fg(syntax.tag),
528 "property" | "property.builtin" => Style::new().fg(syntax.property),
529 "tag" => Style::new().fg(syntax.tag),
530 "attribute" => Style::new().fg(syntax.constant),
531 "module" | "embedded" | "operator" | "variable" | "variable.parameter" => {
532 Style::new().fg(theme.text)
533 }
534 "punctuation" | "punctuation.bracket" | "punctuation.delimiter" | "punctuation.special" => {
535 Style::new().fg(theme.text_dim)
536 }
537 _ => Style::new().fg(theme.text),
538 }
539}
540
541#[cfg(any(
542 feature = "syntax-rust",
543 feature = "syntax-python",
544 feature = "syntax-javascript",
545 feature = "syntax-typescript",
546 feature = "syntax-go",
547 feature = "syntax-bash",
548 feature = "syntax-json",
549 feature = "syntax-toml",
550 feature = "syntax-c",
551 feature = "syntax-cpp",
552 feature = "syntax-java",
553 feature = "syntax-ruby",
554 feature = "syntax-css",
555 feature = "syntax-html",
556 feature = "syntax-yaml",
557))]
558thread_local! {
559 static HIGHLIGHTER: std::cell::RefCell<tree_sitter_highlight::Highlighter> =
563 std::cell::RefCell::new(tree_sitter_highlight::Highlighter::new());
564}
565
566#[cfg(any(
567 feature = "syntax-rust",
568 feature = "syntax-python",
569 feature = "syntax-javascript",
570 feature = "syntax-typescript",
571 feature = "syntax-go",
572 feature = "syntax-bash",
573 feature = "syntax-json",
574 feature = "syntax-toml",
575 feature = "syntax-c",
576 feature = "syntax-cpp",
577 feature = "syntax-java",
578 feature = "syntax-ruby",
579 feature = "syntax-css",
580 feature = "syntax-html",
581 feature = "syntax-yaml",
582))]
583#[derive(Clone, Copy, PartialEq, Eq)]
584struct SyntaxThemeKey {
585 text: crate::style::Color,
586 text_dim: crate::style::Color,
587 syntax: SyntaxPalette,
588}
589
590#[cfg(any(
591 feature = "syntax-rust",
592 feature = "syntax-python",
593 feature = "syntax-javascript",
594 feature = "syntax-typescript",
595 feature = "syntax-go",
596 feature = "syntax-bash",
597 feature = "syntax-json",
598 feature = "syntax-toml",
599 feature = "syntax-c",
600 feature = "syntax-cpp",
601 feature = "syntax-java",
602 feature = "syntax-ruby",
603 feature = "syntax-css",
604 feature = "syntax-html",
605 feature = "syntax-yaml",
606))]
607struct SyntaxCacheEntry {
608 content_hash: u64,
609 code: Box<str>,
610 lang: Box<str>,
611 theme: SyntaxThemeKey,
612 lines: std::sync::Arc<HighlightedLines>,
613}
614
615#[cfg(any(
616 feature = "syntax-rust",
617 feature = "syntax-python",
618 feature = "syntax-javascript",
619 feature = "syntax-typescript",
620 feature = "syntax-go",
621 feature = "syntax-bash",
622 feature = "syntax-json",
623 feature = "syntax-toml",
624 feature = "syntax-c",
625 feature = "syntax-cpp",
626 feature = "syntax-java",
627 feature = "syntax-ruby",
628 feature = "syntax-css",
629 feature = "syntax-html",
630 feature = "syntax-yaml",
631))]
632#[derive(Default)]
633struct SyntaxCache {
634 entries: std::collections::VecDeque<SyntaxCacheEntry>,
635 source_bytes: usize,
636}
637
638#[cfg(any(
639 feature = "syntax-rust",
640 feature = "syntax-python",
641 feature = "syntax-javascript",
642 feature = "syntax-typescript",
643 feature = "syntax-go",
644 feature = "syntax-bash",
645 feature = "syntax-json",
646 feature = "syntax-toml",
647 feature = "syntax-c",
648 feature = "syntax-cpp",
649 feature = "syntax-java",
650 feature = "syntax-ruby",
651 feature = "syntax-css",
652 feature = "syntax-html",
653 feature = "syntax-yaml",
654))]
655impl SyntaxCache {
656 const MAX_ENTRIES: usize = 64;
657 const MAX_SOURCE_BYTES: usize = 2 * 1024 * 1024;
658
659 fn get(
660 &mut self,
661 content_hash: u64,
662 code: &str,
663 lang: &str,
664 theme: SyntaxThemeKey,
665 ) -> Option<std::sync::Arc<HighlightedLines>> {
666 let index = self.entries.iter().position(|entry| {
667 entry.content_hash == content_hash
668 && entry.code.as_ref() == code
669 && entry.lang.as_ref() == lang
670 && entry.theme == theme
671 })?;
672 let entry = self.entries.remove(index)?;
673 let lines = std::sync::Arc::clone(&entry.lines);
674 self.entries.push_back(entry);
675 Some(lines)
676 }
677
678 fn insert(&mut self, entry: SyntaxCacheEntry) {
679 let source_bytes = entry.code.len().saturating_add(entry.lang.len());
680 if source_bytes > Self::MAX_SOURCE_BYTES {
681 return;
682 }
683 while self.entries.len() >= Self::MAX_ENTRIES
684 || self.source_bytes.saturating_add(source_bytes) > Self::MAX_SOURCE_BYTES
685 {
686 let Some(evicted) = self.entries.pop_front() else {
687 break;
688 };
689 self.source_bytes = self
690 .source_bytes
691 .saturating_sub(evicted.code.len().saturating_add(evicted.lang.len()));
692 }
693 self.source_bytes = self.source_bytes.saturating_add(source_bytes);
694 self.entries.push_back(entry);
695 }
696}
697
698#[cfg(any(
715 feature = "syntax-rust",
716 feature = "syntax-python",
717 feature = "syntax-javascript",
718 feature = "syntax-typescript",
719 feature = "syntax-go",
720 feature = "syntax-bash",
721 feature = "syntax-json",
722 feature = "syntax-toml",
723 feature = "syntax-c",
724 feature = "syntax-cpp",
725 feature = "syntax-java",
726 feature = "syntax-ruby",
727 feature = "syntax-css",
728 feature = "syntax-html",
729 feature = "syntax-yaml",
730))]
731fn highlight_code_uncached(code: &str, lang: &str, theme: &Theme) -> Option<HighlightedLines> {
732 #[cfg(any(
733 feature = "syntax-rust",
734 feature = "syntax-python",
735 feature = "syntax-javascript",
736 feature = "syntax-typescript",
737 feature = "syntax-go",
738 feature = "syntax-bash",
739 feature = "syntax-json",
740 feature = "syntax-toml",
741 feature = "syntax-c",
742 feature = "syntax-cpp",
743 feature = "syntax-java",
744 feature = "syntax-ruby",
745 feature = "syntax-css",
746 feature = "syntax-html",
747 feature = "syntax-yaml",
748 ))]
749 {
750 use tree_sitter_highlight::HighlightEvent;
751
752 let config = get_config(lang)?;
753 let highlights = HIGHLIGHTER.with(|cell| {
754 let mut highlighter = cell.borrow_mut();
755 highlighter
756 .highlight(config, code.as_bytes(), None, |_| None)
757 .ok()
758 .map(|iter| iter.collect::<Vec<_>>())
759 })?;
760 let highlights = highlights.into_iter();
761
762 let default_style = Style::new().fg(theme.text);
763 let mut result: Vec<Vec<(String, Style)>> = Vec::new();
764 let mut current_line: Vec<(String, Style)> = Vec::new();
765 let mut style_stack: Vec<Style> = vec![default_style];
766
767 for event in highlights {
768 match event.ok()? {
769 HighlightEvent::Source { start, end } => {
770 let text = &code[start..end];
771 let style = *style_stack.last().unwrap_or(&default_style);
772 for (i, part) in text.split('\n').enumerate() {
774 if i > 0 {
775 result.push(std::mem::take(&mut current_line));
776 }
777 if !part.is_empty() {
778 current_line.push((part.to_string(), style));
779 }
780 }
781 }
782 HighlightEvent::HighlightStart(highlight) => {
783 let name = HIGHLIGHT_NAMES.get(highlight.0).copied().unwrap_or("");
784 let style = highlight_name_to_style(name, theme);
785 style_stack.push(style);
786 }
787 HighlightEvent::HighlightEnd => {
788 style_stack.pop();
789 }
790 }
791 }
792
793 if !current_line.is_empty() {
794 result.push(current_line);
795 }
796
797 Some(result)
798 }
799
800 #[cfg(not(any(
801 feature = "syntax-rust",
802 feature = "syntax-python",
803 feature = "syntax-javascript",
804 feature = "syntax-typescript",
805 feature = "syntax-go",
806 feature = "syntax-bash",
807 feature = "syntax-json",
808 feature = "syntax-toml",
809 feature = "syntax-c",
810 feature = "syntax-cpp",
811 feature = "syntax-java",
812 feature = "syntax-ruby",
813 feature = "syntax-css",
814 feature = "syntax-html",
815 feature = "syntax-yaml",
816 )))]
817 {
818 None
819 }
820}
821
822pub fn highlight_code(code: &str, lang: &str, theme: &Theme) -> Option<Vec<Vec<(String, Style)>>> {
825 highlight_code_cached(code, lang, theme).map(|lines| lines.as_ref().clone())
826}
827
828pub(crate) fn highlight_code_cached(
829 code: &str,
830 lang: &str,
831 theme: &Theme,
832) -> Option<std::sync::Arc<HighlightedLines>> {
833 #[cfg(any(
834 feature = "syntax-rust",
835 feature = "syntax-python",
836 feature = "syntax-javascript",
837 feature = "syntax-typescript",
838 feature = "syntax-go",
839 feature = "syntax-bash",
840 feature = "syntax-json",
841 feature = "syntax-toml",
842 feature = "syntax-c",
843 feature = "syntax-cpp",
844 feature = "syntax-java",
845 feature = "syntax-ruby",
846 feature = "syntax-css",
847 feature = "syntax-html",
848 feature = "syntax-yaml",
849 ))]
850 {
851 use std::hash::{Hash, Hasher};
852 use std::sync::{Mutex, OnceLock};
853
854 static CACHE: OnceLock<Mutex<SyntaxCache>> = OnceLock::new();
855 const MAX_CACHEABLE_CODE_BYTES: usize = 256 * 1024;
856
857 let theme_key = SyntaxThemeKey {
858 text: theme.text,
859 text_dim: theme.text_dim,
860 syntax: theme.syntax,
861 };
862 let mut hasher = std::collections::hash_map::DefaultHasher::new();
863 code.hash(&mut hasher);
864 let content_hash = hasher.finish();
865
866 if code.len() <= MAX_CACHEABLE_CODE_BYTES {
867 let cache = CACHE.get_or_init(|| Mutex::new(SyntaxCache::default()));
868 let mut cache = cache
869 .lock()
870 .unwrap_or_else(|poisoned| poisoned.into_inner());
871 if let Some(lines) = cache.get(content_hash, code, lang, theme_key) {
872 return Some(lines);
873 }
874 }
875
876 let lines = std::sync::Arc::new(highlight_code_uncached(code, lang, theme)?);
877 if code.len() <= MAX_CACHEABLE_CODE_BYTES {
878 let cache = CACHE.get_or_init(|| Mutex::new(SyntaxCache::default()));
879 let mut cache = cache
880 .lock()
881 .unwrap_or_else(|poisoned| poisoned.into_inner());
882 cache.insert(SyntaxCacheEntry {
883 content_hash,
884 code: code.into(),
885 lang: lang.into(),
886 theme: theme_key,
887 lines: std::sync::Arc::clone(&lines),
888 });
889 }
890 Some(lines)
891 }
892
893 #[cfg(not(any(
894 feature = "syntax-rust",
895 feature = "syntax-python",
896 feature = "syntax-javascript",
897 feature = "syntax-typescript",
898 feature = "syntax-go",
899 feature = "syntax-bash",
900 feature = "syntax-json",
901 feature = "syntax-toml",
902 feature = "syntax-c",
903 feature = "syntax-cpp",
904 feature = "syntax-java",
905 feature = "syntax-ruby",
906 feature = "syntax-css",
907 feature = "syntax-html",
908 feature = "syntax-yaml",
909 )))]
910 {
911 let _ = (code, lang, theme);
912 None
913 }
914}
915
916#[allow(unused_variables)]
921pub fn is_language_supported(lang: &str) -> bool {
922 #[cfg(any(
923 feature = "syntax-rust",
924 feature = "syntax-python",
925 feature = "syntax-javascript",
926 feature = "syntax-typescript",
927 feature = "syntax-go",
928 feature = "syntax-bash",
929 feature = "syntax-json",
930 feature = "syntax-toml",
931 feature = "syntax-c",
932 feature = "syntax-cpp",
933 feature = "syntax-java",
934 feature = "syntax-ruby",
935 feature = "syntax-css",
936 feature = "syntax-html",
937 feature = "syntax-yaml",
938 ))]
939 {
940 get_config(lang).is_some()
941 }
942 #[cfg(not(any(
943 feature = "syntax-rust",
944 feature = "syntax-python",
945 feature = "syntax-javascript",
946 feature = "syntax-typescript",
947 feature = "syntax-go",
948 feature = "syntax-bash",
949 feature = "syntax-json",
950 feature = "syntax-toml",
951 feature = "syntax-c",
952 feature = "syntax-cpp",
953 feature = "syntax-java",
954 feature = "syntax-ruby",
955 feature = "syntax-css",
956 feature = "syntax-html",
957 feature = "syntax-yaml",
958 )))]
959 {
960 false
961 }
962}
963
964#[cfg(test)]
965mod tests {
966 #![allow(clippy::unwrap_used)]
967 use super::*;
968 use crate::style::Theme;
969
970 #[test]
971 fn highlight_returns_none_for_unknown_lang() {
972 let theme = Theme::dark();
973 assert!(highlight_code("let x = 1;", "brainfuck", &theme).is_none());
974 }
975
976 #[test]
977 fn is_language_supported_unknown() {
978 assert!(!is_language_supported("haskell"));
979 }
980
981 #[cfg(any(
982 feature = "syntax-rust",
983 feature = "syntax-python",
984 feature = "syntax-javascript",
985 feature = "syntax-typescript",
986 feature = "syntax-go",
987 feature = "syntax-bash",
988 feature = "syntax-json",
989 feature = "syntax-toml",
990 feature = "syntax-c",
991 feature = "syntax-cpp",
992 feature = "syntax-java",
993 feature = "syntax-ruby",
994 feature = "syntax-css",
995 feature = "syntax-html",
996 feature = "syntax-yaml",
997 ))]
998 #[test]
999 fn number_and_constant_captures_use_distinct_palette_entries() {
1000 let mut theme = Theme::dark();
1001 theme.syntax.number = crate::style::Color::Rgb(1, 2, 3);
1002 theme.syntax.constant = crate::style::Color::Rgb(4, 5, 6);
1003
1004 assert_eq!(
1005 highlight_name_to_style("number", &theme).fg,
1006 Some(theme.syntax.number)
1007 );
1008 assert_eq!(
1009 highlight_name_to_style("constant", &theme).fg,
1010 Some(theme.syntax.constant)
1011 );
1012 }
1013
1014 #[cfg(feature = "syntax-rust")]
1015 #[test]
1016 fn highlight_rust_basic() {
1017 let theme = Theme::dark();
1018 let result = highlight_code("let x = 1;", "rust", &theme);
1019 assert!(result.is_some());
1020 let lines = result.unwrap();
1021 assert_eq!(lines.len(), 1);
1022 let flat: String = lines[0].iter().map(|(t, _)| t.as_str()).collect();
1024 assert!(flat.contains("let"));
1025 assert!(flat.contains("1"));
1026 }
1027
1028 #[cfg(feature = "syntax-rust")]
1029 #[test]
1030 fn highlight_rust_multiline() {
1031 let theme = Theme::dark();
1032 let code = "fn main() {\n println!(\"hello\");\n}";
1033 let result = highlight_code(code, "rust", &theme).unwrap();
1034 assert_eq!(result.len(), 3);
1035 }
1036
1037 #[cfg(feature = "syntax-rust")]
1038 #[test]
1039 fn highlight_rust_rs_alias() {
1040 let theme = Theme::dark();
1041 assert!(highlight_code("let x = 1;", "rs", &theme).is_some());
1042 }
1043
1044 #[cfg(feature = "syntax-python")]
1045 #[test]
1046 fn highlight_python_basic() {
1047 let theme = Theme::dark();
1048 let result = highlight_code("def foo():\n return 42", "python", &theme);
1049 assert!(result.is_some());
1050 let lines = result.unwrap();
1051 assert_eq!(lines.len(), 2);
1052 }
1053
1054 #[cfg(feature = "syntax-javascript")]
1055 #[test]
1056 fn highlight_javascript_basic() {
1057 let theme = Theme::dark();
1058 let result = highlight_code("const x = () => 42;", "js", &theme);
1059 assert!(result.is_some());
1060 }
1061
1062 #[cfg(feature = "syntax-bash")]
1063 #[test]
1064 fn highlight_bash_basic() {
1065 let theme = Theme::dark();
1066 let result = highlight_code("echo \"hello\"", "sh", &theme);
1067 assert!(result.is_some());
1068 }
1069
1070 #[cfg(feature = "syntax-json")]
1071 #[test]
1072 fn highlight_json_basic() {
1073 let theme = Theme::dark();
1074 let result = highlight_code("{\"key\": 42}", "json", &theme);
1075 assert!(result.is_some());
1076 }
1077
1078 #[cfg(feature = "syntax-toml")]
1079 #[test]
1080 fn highlight_toml_basic() {
1081 let theme = Theme::dark();
1082 let result = highlight_code("[package]\nname = \"slt\"", "toml", &theme);
1083 assert!(result.is_some());
1084 }
1085
1086 #[cfg(feature = "syntax-go")]
1087 #[test]
1088 fn highlight_go_basic() {
1089 let theme = Theme::dark();
1090 let result = highlight_code("package main\nfunc main() {}", "go", &theme);
1091 assert!(result.is_some());
1092 }
1093
1094 #[cfg(feature = "syntax-rust")]
1095 #[test]
1096 fn highlight_light_theme_differs() {
1097 let dark = Theme::dark();
1098 let light = Theme::light();
1099 let dark_result = highlight_code("let x = 1;", "rust", &dark).unwrap();
1100 let light_result = highlight_code("let x = 1;", "rust", &light).unwrap();
1101 let dark_styles: Vec<Style> = dark_result[0].iter().map(|(_, s)| *s).collect();
1103 let light_styles: Vec<Style> = light_result[0].iter().map(|(_, s)| *s).collect();
1104 assert_ne!(dark_styles, light_styles);
1105 }
1106
1107 #[cfg(feature = "syntax-rust")]
1108 #[test]
1109 fn highlight_keyword_uses_theme_palette() {
1110 let nord = Theme::nord();
1113 let catppuccin = Theme::catppuccin();
1114
1115 let kw_fg = |theme: &Theme| -> crate::style::Color {
1116 let line = highlight_code("let x = 1;", "rust", theme).unwrap();
1117 line[0]
1118 .iter()
1119 .find_map(|(text, style)| (text.as_str() == "let").then_some(style.fg.unwrap()))
1120 .expect("`let` keyword segment present")
1121 };
1122
1123 assert_eq!(kw_fg(&nord), nord.syntax.keyword);
1124 assert_eq!(kw_fg(&catppuccin), catppuccin.syntax.keyword);
1125 assert_ne!(nord.syntax.keyword, catppuccin.syntax.keyword);
1128 }
1129
1130 #[cfg(feature = "syntax-rust")]
1131 #[test]
1132 fn code_block_renders_with_theme_syntax_palette() {
1133 use crate::style::Theme;
1134 use crate::test_utils::TestBackend;
1135
1136 let theme = Theme::tokyo_night();
1137 let mut tb = TestBackend::new(40, 8);
1138 tb.render(|ui| {
1139 ui.set_theme(theme);
1140 let _ = ui.code_block("fn main() {}").lang("rust").show();
1141 });
1142
1143 tb.assert_contains("fn");
1145 tb.assert_contains("main");
1146
1147 let one_dark_keyword = crate::style::Color::Rgb(198, 120, 221);
1150 let buffer = tb.buffer();
1151 let mut saw_theme_keyword = false;
1152 for y in 0..tb.height() {
1153 for x in 0..tb.width() {
1154 let fg = buffer.get(x, y).style.fg;
1155 assert_ne!(
1156 fg,
1157 Some(one_dark_keyword),
1158 "One Dark keyword color must not appear under Tokyo Night"
1159 );
1160 if fg == Some(theme.syntax.keyword) {
1161 saw_theme_keyword = true;
1162 }
1163 }
1164 }
1165 assert!(
1166 saw_theme_keyword,
1167 "expected a cell colored with Tokyo Night's keyword color"
1168 );
1169 }
1170
1171 #[cfg(feature = "syntax-rust")]
1172 #[test]
1173 fn highlight_incomplete_code_does_not_panic() {
1174 let theme = Theme::dark();
1175 let result = highlight_code("fn main( {", "rust", &theme);
1176 assert!(result.is_some());
1177 }
1178
1179 #[cfg(feature = "syntax-c")]
1180 #[test]
1181 fn highlight_c_basic() {
1182 let theme = Theme::dark();
1183 assert!(
1184 highlight_code("#include <stdio.h>\nint main() { return 0; }", "c", &theme).is_some()
1185 );
1186 }
1187
1188 #[cfg(feature = "syntax-cpp")]
1189 #[test]
1190 fn highlight_cpp_basic() {
1191 let theme = Theme::dark();
1192 assert!(highlight_code("class Foo { public: void bar(); };", "cpp", &theme).is_some());
1193 }
1194
1195 #[cfg(feature = "syntax-typescript")]
1196 #[test]
1197 fn highlight_typescript_basic() {
1198 let theme = Theme::dark();
1199 let lines = highlight_code("const x: number = 42;", "ts", &theme).unwrap();
1200 let number = lines
1201 .iter()
1202 .flatten()
1203 .find(|(text, _)| text == "42")
1204 .expect("numeric capture");
1205 assert_eq!(number.1.fg, Some(theme.syntax.number));
1206 }
1207
1208 #[cfg(feature = "syntax-typescript")]
1209 #[test]
1210 fn highlight_tsx_basic() {
1211 let theme = Theme::dark();
1212 let lines = highlight_code(
1213 "const App = () => <div data-count={42}>hello</div>;",
1214 "tsx",
1215 &theme,
1216 )
1217 .unwrap();
1218 let flat: String = lines
1219 .iter()
1220 .flatten()
1221 .map(|(text, _)| text.as_str())
1222 .collect();
1223 assert!(flat.contains("data-count"));
1224 assert!(flat.contains("42"));
1225 }
1226
1227 #[cfg(feature = "syntax-java")]
1228 #[test]
1229 fn highlight_java_basic() {
1230 let theme = Theme::dark();
1231 assert!(
1232 highlight_code(
1233 "public class Main { public static void main(String[] args) {} }",
1234 "java",
1235 &theme
1236 )
1237 .is_some()
1238 );
1239 }
1240
1241 #[cfg(feature = "syntax-ruby")]
1242 #[test]
1243 fn highlight_ruby_basic() {
1244 let theme = Theme::dark();
1245 let lines = highlight_code(
1246 "answer = 42\ndef hello\n puts 'world'\nend",
1247 "ruby",
1248 &theme,
1249 )
1250 .unwrap();
1251 let flat: String = lines
1252 .iter()
1253 .flatten()
1254 .map(|(text, _)| text.as_str())
1255 .collect();
1256 assert!(flat.contains("answer"));
1257 assert!(flat.contains("42"));
1258 }
1259
1260 #[cfg(feature = "syntax-css")]
1261 #[test]
1262 fn highlight_css_basic() {
1263 let theme = Theme::dark();
1264 assert!(highlight_code("body { color: red; }", "css", &theme).is_some());
1265 }
1266
1267 #[cfg(feature = "syntax-html")]
1268 #[test]
1269 fn highlight_html_basic() {
1270 let theme = Theme::dark();
1271 assert!(highlight_code("<div class=\"test\">hello</div>", "html", &theme).is_some());
1272 }
1273
1274 #[cfg(feature = "syntax-yaml")]
1275 #[test]
1276 fn highlight_yaml_basic() {
1277 let theme = Theme::dark();
1278 assert!(highlight_code("name: slt\nversion: 0.14", "yaml", &theme).is_some());
1279 }
1280
1281 #[cfg(feature = "syntax-rust")]
1284 #[test]
1285 fn highlight_reuse_does_not_panic() {
1286 let theme = Theme::dark();
1287 let first = highlight_code("let x = 1;", "rust", &theme);
1289 let second = highlight_code("fn foo() {}", "rust", &theme);
1290 assert!(first.is_some(), "first call should succeed");
1291 assert!(second.is_some(), "second call should succeed");
1292 }
1293
1294 #[cfg(feature = "syntax-rust")]
1295 #[test]
1296 fn highlight_preparation_cache_reuses_arc_and_invalidates_on_theme() {
1297 let dark = Theme::dark();
1298 let light = Theme::light();
1299 let first = highlight_code_cached("let cache_probe = 17;", "rust", &dark).unwrap();
1300 let second = highlight_code_cached("let cache_probe = 17;", "rust", &dark).unwrap();
1301 let themed = highlight_code_cached("let cache_probe = 17;", "rust", &light).unwrap();
1302
1303 assert_eq!(first.as_ref(), second.as_ref());
1307 assert_ne!(first.as_ref(), themed.as_ref());
1308 }
1309
1310 #[cfg(feature = "syntax-rust")]
1311 #[test]
1312 fn syntax_cache_returns_the_stored_arc() {
1313 let theme = Theme::dark();
1314 let theme_key = SyntaxThemeKey {
1315 text: theme.text,
1316 text_dim: theme.text_dim,
1317 syntax: theme.syntax,
1318 };
1319 let lines = std::sync::Arc::new(vec![vec![(
1320 "cached".to_string(),
1321 Style::new().fg(theme.text),
1322 )]]);
1323 let mut cache = SyntaxCache::default();
1324 cache.insert(SyntaxCacheEntry {
1325 content_hash: 7,
1326 code: "probe".into(),
1327 lang: "rust".into(),
1328 theme: theme_key,
1329 lines: std::sync::Arc::clone(&lines),
1330 });
1331
1332 let cached = cache.get(7, "probe", "rust", theme_key).unwrap();
1333 assert!(std::sync::Arc::ptr_eq(&lines, &cached));
1334 }
1335
1336 #[cfg(all(feature = "syntax-rust", feature = "syntax-python"))]
1339 #[test]
1340 fn highlight_reuse_across_languages() {
1341 let theme = Theme::dark();
1342 let r1 = highlight_code("let x = 1;", "rust", &theme);
1343 let r2 = highlight_code("def foo(): pass", "python", &theme);
1344 let r3 = highlight_code("fn bar() {}", "rust", &theme);
1345 assert!(r1.is_some());
1346 assert!(r2.is_some());
1347 assert!(r3.is_some());
1348 }
1349}