Skip to main content

slt/
syntax.rs

1//! Tree-sitter based syntax highlighting.
2//!
3//! When one of the `syntax-*` features is enabled,
4//! [`crate::syntax::highlight_code`] uses tree-sitter grammars for accurate,
5//! language-aware highlighting. Without those features the function always
6//! returns `None` so callers can fall back to the built-in keyword
7//! highlighter.
8
9use 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/// Ordered list of tree-sitter highlight capture names.
33///
34/// The index of each name corresponds to the `Highlight` index
35/// returned by `HighlightEvent::HighlightStart`.
36#[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/// Return a cached `HighlightConfiguration` for `lang`, or `None` if the
122/// language is unsupported or the corresponding feature is not enabled.
123#[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/// Map a tree-sitter highlight capture name to an SLT [`Style`].
493///
494/// Colorful tokens resolve through the active theme's
495/// [`SyntaxPalette`](crate::SyntaxPalette) (`theme.syntax.*`), so code blocks
496/// adopt the selected theme instead of a hardcoded scheme. Neutral tokens
497/// (comments, operators, plain variables, punctuation) resolve through
498/// [`Theme::text`] / [`Theme::text_dim`].
499#[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    // SAFETY: SLT runs a single-threaded synchronous event loop.
560    // Re-entrant highlight calls are architecturally impossible.
561    // If an async runtime is added later, revisit this (see issue #113).
562    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/// Highlight source code using tree-sitter.
699///
700/// Returns `Some(lines)` where each line is a `Vec<(text, style)>` of
701/// styled segments, or `None` if:
702/// - The language is not recognised
703/// - The corresponding `syntax-*` feature is not enabled
704/// - Parsing fails
705///
706/// Callers should fall back to the built-in keyword highlighter when
707/// `None` is returned.
708///
709/// # Example
710///
711/// ```ignore
712/// let lines = slt::syntax::highlight_code("let x = 1;", "rust", &theme);
713/// ```
714#[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                    // Split by newlines to produce per-line segments
773                    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
822/// Highlight source code using the active theme, reusing a bounded preparation
823/// cache for repeated content/language/theme combinations.
824pub 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/// Returns `true` if tree-sitter highlighting is available for `lang`.
917///
918/// This checks both that the corresponding `syntax-*` feature is enabled
919/// and that the language string is recognised.
920#[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" should be in the first line's segments
1023        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        // Keyword styles should differ between dark and light
1102        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        // The `let` keyword should adopt each theme's syntax palette rather
1111        // than a hardcoded One Dark color.
1112        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        // The two themes resolve to different keyword colors — proving the
1126        // old hardcoded One Dark purple is no longer used.
1127        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        // The code text still renders.
1144        tb.assert_contains("fn");
1145        tb.assert_contains("main");
1146
1147        // Some keyword cell adopts Tokyo Night's keyword color, and the old
1148        // hardcoded One Dark purple is absent from the buffer.
1149        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    /// Regression test for issue #113:
1282    /// `highlight_code()` must not panic on repeated calls (thread_local HIGHLIGHTER reuse).
1283    #[cfg(feature = "syntax-rust")]
1284    #[test]
1285    fn highlight_reuse_does_not_panic() {
1286        let theme = Theme::dark();
1287        // Call twice with the same language — exercises HIGHLIGHTER.with borrow_mut reuse.
1288        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        // Other parallel syntax tests may legitimately evict this bounded
1304        // global cache between calls. Verify value reuse semantics here and
1305        // pointer reuse deterministically in the cache-unit test below.
1306        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    /// Regression test for issue #113:
1337    /// Multiple calls across different languages must all return Some.
1338    #[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}