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/// Ordered list of tree-sitter highlight capture names.
12///
13/// The index of each name corresponds to the `Highlight` index
14/// returned by `HighlightEvent::HighlightStart`.
15#[cfg(any(
16    feature = "syntax-rust",
17    feature = "syntax-python",
18    feature = "syntax-javascript",
19    feature = "syntax-typescript",
20    feature = "syntax-go",
21    feature = "syntax-bash",
22    feature = "syntax-json",
23    feature = "syntax-toml",
24    feature = "syntax-c",
25    feature = "syntax-cpp",
26    feature = "syntax-java",
27    feature = "syntax-ruby",
28    feature = "syntax-css",
29    feature = "syntax-html",
30    feature = "syntax-yaml",
31))]
32const HIGHLIGHT_NAMES: &[&str] = &[
33    "attribute",
34    "comment",
35    "constant",
36    "constant.builtin",
37    "constructor",
38    "embedded",
39    "function",
40    "function.builtin",
41    "function.macro",
42    "keyword",
43    "module",
44    "number",
45    "operator",
46    "property",
47    "property.builtin",
48    "punctuation",
49    "punctuation.bracket",
50    "punctuation.delimiter",
51    "punctuation.special",
52    "string",
53    "string.special",
54    "tag",
55    "type",
56    "type.builtin",
57    "variable",
58    "variable.builtin",
59    "variable.parameter",
60];
61
62#[cfg(any(
63    feature = "syntax-rust",
64    feature = "syntax-python",
65    feature = "syntax-javascript",
66    feature = "syntax-typescript",
67    feature = "syntax-go",
68    feature = "syntax-bash",
69    feature = "syntax-json",
70    feature = "syntax-toml",
71    feature = "syntax-c",
72    feature = "syntax-cpp",
73    feature = "syntax-java",
74    feature = "syntax-ruby",
75    feature = "syntax-css",
76    feature = "syntax-html",
77    feature = "syntax-yaml",
78))]
79use std::sync::OnceLock;
80
81#[cfg(any(
82    feature = "syntax-rust",
83    feature = "syntax-python",
84    feature = "syntax-javascript",
85    feature = "syntax-typescript",
86    feature = "syntax-go",
87    feature = "syntax-bash",
88    feature = "syntax-json",
89    feature = "syntax-toml",
90    feature = "syntax-c",
91    feature = "syntax-cpp",
92    feature = "syntax-java",
93    feature = "syntax-ruby",
94    feature = "syntax-css",
95    feature = "syntax-html",
96    feature = "syntax-yaml",
97))]
98use tree_sitter_highlight::HighlightConfiguration;
99
100/// Return a cached `HighlightConfiguration` for `lang`, or `None` if the
101/// language is unsupported or the corresponding feature is not enabled.
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))]
119fn get_config(lang: &str) -> Option<&'static HighlightConfiguration> {
120    match lang {
121        #[cfg(feature = "syntax-rust")]
122        "rust" | "rs" => {
123            static CFG: OnceLock<Option<HighlightConfiguration>> = OnceLock::new();
124            CFG.get_or_init(|| {
125                HighlightConfiguration::new(
126                    tree_sitter_rust::LANGUAGE.into(),
127                    "rust",
128                    tree_sitter_rust::HIGHLIGHTS_QUERY,
129                    tree_sitter_rust::INJECTIONS_QUERY,
130                    "",
131                )
132                .ok()
133                .map(|mut c| {
134                    c.configure(HIGHLIGHT_NAMES);
135                    c
136                })
137            })
138            .as_ref()
139        }
140
141        #[cfg(feature = "syntax-python")]
142        "python" | "py" => {
143            static CFG: OnceLock<Option<HighlightConfiguration>> = OnceLock::new();
144            CFG.get_or_init(|| {
145                HighlightConfiguration::new(
146                    tree_sitter_python::LANGUAGE.into(),
147                    "python",
148                    tree_sitter_python::HIGHLIGHTS_QUERY,
149                    "",
150                    "",
151                )
152                .ok()
153                .map(|mut c| {
154                    c.configure(HIGHLIGHT_NAMES);
155                    c
156                })
157            })
158            .as_ref()
159        }
160
161        #[cfg(feature = "syntax-javascript")]
162        "javascript" | "js" | "jsx" => {
163            static CFG: OnceLock<Option<HighlightConfiguration>> = OnceLock::new();
164            CFG.get_or_init(|| {
165                HighlightConfiguration::new(
166                    tree_sitter_javascript::LANGUAGE.into(),
167                    "javascript",
168                    tree_sitter_javascript::HIGHLIGHT_QUERY,
169                    tree_sitter_javascript::INJECTIONS_QUERY,
170                    tree_sitter_javascript::LOCALS_QUERY,
171                )
172                .ok()
173                .map(|mut c| {
174                    c.configure(HIGHLIGHT_NAMES);
175                    c
176                })
177            })
178            .as_ref()
179        }
180
181        #[cfg(feature = "syntax-go")]
182        "go" | "golang" => {
183            static CFG: OnceLock<Option<HighlightConfiguration>> = OnceLock::new();
184            CFG.get_or_init(|| {
185                HighlightConfiguration::new(
186                    tree_sitter_go::LANGUAGE.into(),
187                    "go",
188                    tree_sitter_go::HIGHLIGHTS_QUERY,
189                    "",
190                    "",
191                )
192                .ok()
193                .map(|mut c| {
194                    c.configure(HIGHLIGHT_NAMES);
195                    c
196                })
197            })
198            .as_ref()
199        }
200
201        #[cfg(feature = "syntax-bash")]
202        "bash" | "sh" | "shell" | "zsh" => {
203            static CFG: OnceLock<Option<HighlightConfiguration>> = OnceLock::new();
204            CFG.get_or_init(|| {
205                HighlightConfiguration::new(
206                    tree_sitter_bash::LANGUAGE.into(),
207                    "bash",
208                    tree_sitter_bash::HIGHLIGHT_QUERY,
209                    "",
210                    "",
211                )
212                .ok()
213                .map(|mut c| {
214                    c.configure(HIGHLIGHT_NAMES);
215                    c
216                })
217            })
218            .as_ref()
219        }
220
221        #[cfg(feature = "syntax-json")]
222        "json" | "jsonc" => {
223            static CFG: OnceLock<Option<HighlightConfiguration>> = OnceLock::new();
224            CFG.get_or_init(|| {
225                HighlightConfiguration::new(
226                    tree_sitter_json::LANGUAGE.into(),
227                    "json",
228                    tree_sitter_json::HIGHLIGHTS_QUERY,
229                    "",
230                    "",
231                )
232                .ok()
233                .map(|mut c| {
234                    c.configure(HIGHLIGHT_NAMES);
235                    c
236                })
237            })
238            .as_ref()
239        }
240
241        #[cfg(feature = "syntax-toml")]
242        "toml" => {
243            static CFG: OnceLock<Option<HighlightConfiguration>> = OnceLock::new();
244            CFG.get_or_init(|| {
245                HighlightConfiguration::new(
246                    tree_sitter_toml_ng::LANGUAGE.into(),
247                    "toml",
248                    tree_sitter_toml_ng::HIGHLIGHTS_QUERY,
249                    "",
250                    "",
251                )
252                .ok()
253                .map(|mut c| {
254                    c.configure(HIGHLIGHT_NAMES);
255                    c
256                })
257            })
258            .as_ref()
259        }
260
261        #[cfg(feature = "syntax-c")]
262        "c" | "h" => {
263            static CFG: OnceLock<Option<HighlightConfiguration>> = OnceLock::new();
264            CFG.get_or_init(|| {
265                HighlightConfiguration::new(
266                    tree_sitter_c::LANGUAGE.into(),
267                    "c",
268                    tree_sitter_c::HIGHLIGHT_QUERY,
269                    "",
270                    "",
271                )
272                .ok()
273                .map(|mut c| {
274                    c.configure(HIGHLIGHT_NAMES);
275                    c
276                })
277            })
278            .as_ref()
279        }
280
281        #[cfg(feature = "syntax-cpp")]
282        "cpp" | "c++" | "cxx" | "cc" | "hpp" => {
283            static CFG: OnceLock<Option<HighlightConfiguration>> = OnceLock::new();
284            CFG.get_or_init(|| {
285                #[cfg(feature = "syntax-c")]
286                let highlights = {
287                    let mut combined = String::with_capacity(
288                        tree_sitter_c::HIGHLIGHT_QUERY.len()
289                            + tree_sitter_cpp::HIGHLIGHT_QUERY.len()
290                            + 1,
291                    );
292                    combined.push_str(tree_sitter_c::HIGHLIGHT_QUERY);
293                    combined.push('\n');
294                    combined.push_str(tree_sitter_cpp::HIGHLIGHT_QUERY);
295                    combined
296                };
297                #[cfg(not(feature = "syntax-c"))]
298                let highlights = tree_sitter_cpp::HIGHLIGHT_QUERY.to_string();
299
300                HighlightConfiguration::new(
301                    tree_sitter_cpp::LANGUAGE.into(),
302                    "cpp",
303                    &highlights,
304                    "",
305                    "",
306                )
307                .ok()
308                .map(|mut c| {
309                    c.configure(HIGHLIGHT_NAMES);
310                    c
311                })
312            })
313            .as_ref()
314        }
315
316        #[cfg(feature = "syntax-typescript")]
317        "typescript" | "ts" => {
318            static CFG: OnceLock<Option<HighlightConfiguration>> = OnceLock::new();
319            CFG.get_or_init(|| {
320                HighlightConfiguration::new(
321                    tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into(),
322                    "typescript",
323                    tree_sitter_typescript::HIGHLIGHTS_QUERY,
324                    tree_sitter_typescript::LOCALS_QUERY,
325                    "",
326                )
327                .ok()
328                .map(|mut c| {
329                    c.configure(HIGHLIGHT_NAMES);
330                    c
331                })
332            })
333            .as_ref()
334        }
335
336        #[cfg(feature = "syntax-typescript")]
337        "tsx" => {
338            static CFG: OnceLock<Option<HighlightConfiguration>> = OnceLock::new();
339            CFG.get_or_init(|| {
340                HighlightConfiguration::new(
341                    tree_sitter_typescript::LANGUAGE_TSX.into(),
342                    "tsx",
343                    tree_sitter_typescript::HIGHLIGHTS_QUERY,
344                    tree_sitter_typescript::LOCALS_QUERY,
345                    "",
346                )
347                .ok()
348                .map(|mut c| {
349                    c.configure(HIGHLIGHT_NAMES);
350                    c
351                })
352            })
353            .as_ref()
354        }
355
356        #[cfg(feature = "syntax-java")]
357        "java" => {
358            static CFG: OnceLock<Option<HighlightConfiguration>> = OnceLock::new();
359            CFG.get_or_init(|| {
360                HighlightConfiguration::new(
361                    tree_sitter_java::LANGUAGE.into(),
362                    "java",
363                    tree_sitter_java::HIGHLIGHTS_QUERY,
364                    "",
365                    "",
366                )
367                .ok()
368                .map(|mut c| {
369                    c.configure(HIGHLIGHT_NAMES);
370                    c
371                })
372            })
373            .as_ref()
374        }
375
376        #[cfg(feature = "syntax-ruby")]
377        "ruby" | "rb" => {
378            static CFG: OnceLock<Option<HighlightConfiguration>> = OnceLock::new();
379            CFG.get_or_init(|| {
380                HighlightConfiguration::new(
381                    tree_sitter_ruby::LANGUAGE.into(),
382                    "ruby",
383                    tree_sitter_ruby::HIGHLIGHTS_QUERY,
384                    tree_sitter_ruby::LOCALS_QUERY,
385                    "",
386                )
387                .ok()
388                .map(|mut c| {
389                    c.configure(HIGHLIGHT_NAMES);
390                    c
391                })
392            })
393            .as_ref()
394        }
395
396        #[cfg(feature = "syntax-css")]
397        "css" => {
398            static CFG: OnceLock<Option<HighlightConfiguration>> = OnceLock::new();
399            CFG.get_or_init(|| {
400                HighlightConfiguration::new(
401                    tree_sitter_css::LANGUAGE.into(),
402                    "css",
403                    tree_sitter_css::HIGHLIGHTS_QUERY,
404                    "",
405                    "",
406                )
407                .ok()
408                .map(|mut c| {
409                    c.configure(HIGHLIGHT_NAMES);
410                    c
411                })
412            })
413            .as_ref()
414        }
415
416        #[cfg(feature = "syntax-html")]
417        "html" | "htm" => {
418            static CFG: OnceLock<Option<HighlightConfiguration>> = OnceLock::new();
419            CFG.get_or_init(|| {
420                HighlightConfiguration::new(
421                    tree_sitter_html::LANGUAGE.into(),
422                    "html",
423                    tree_sitter_html::HIGHLIGHTS_QUERY,
424                    tree_sitter_html::INJECTIONS_QUERY,
425                    "",
426                )
427                .ok()
428                .map(|mut c| {
429                    c.configure(HIGHLIGHT_NAMES);
430                    c
431                })
432            })
433            .as_ref()
434        }
435
436        #[cfg(feature = "syntax-yaml")]
437        "yaml" | "yml" => {
438            static CFG: OnceLock<Option<HighlightConfiguration>> = OnceLock::new();
439            CFG.get_or_init(|| {
440                HighlightConfiguration::new(
441                    tree_sitter_yaml::LANGUAGE.into(),
442                    "yaml",
443                    tree_sitter_yaml::HIGHLIGHTS_QUERY,
444                    "",
445                    "",
446                )
447                .ok()
448                .map(|mut c| {
449                    c.configure(HIGHLIGHT_NAMES);
450                    c
451                })
452            })
453            .as_ref()
454        }
455
456        _ => None,
457    }
458}
459
460/// Map a tree-sitter highlight capture name to an SLT [`Style`].
461///
462/// Colorful tokens resolve through the active theme's
463/// [`SyntaxPalette`](crate::SyntaxPalette) (`theme.syntax.*`), so code blocks
464/// adopt the selected theme instead of a hardcoded scheme. Neutral tokens
465/// (comments, operators, plain variables, punctuation) resolve through
466/// [`Theme::text`] / [`Theme::text_dim`].
467#[cfg(any(
468    feature = "syntax-rust",
469    feature = "syntax-python",
470    feature = "syntax-javascript",
471    feature = "syntax-typescript",
472    feature = "syntax-go",
473    feature = "syntax-bash",
474    feature = "syntax-json",
475    feature = "syntax-toml",
476    feature = "syntax-c",
477    feature = "syntax-cpp",
478    feature = "syntax-java",
479    feature = "syntax-ruby",
480    feature = "syntax-css",
481    feature = "syntax-html",
482    feature = "syntax-yaml",
483))]
484fn highlight_name_to_style(name: &str, theme: &Theme) -> Style {
485    let syntax = &theme.syntax;
486    match name {
487        "keyword" => Style::new().fg(syntax.keyword),
488        "string" | "string.special" => Style::new().fg(syntax.string),
489        "comment" => Style::new().fg(theme.text_dim).italic(),
490        "number" | "constant" | "constant.builtin" => Style::new().fg(syntax.constant),
491        "function" | "function.builtin" => Style::new().fg(syntax.function),
492        "function.macro" => Style::new().fg(syntax.macro_),
493        "type" | "type.builtin" | "constructor" => Style::new().fg(syntax.type_),
494        "variable.builtin" => Style::new().fg(syntax.tag),
495        "property" | "property.builtin" => Style::new().fg(syntax.property),
496        "tag" => Style::new().fg(syntax.tag),
497        "attribute" => Style::new().fg(syntax.constant),
498        "module" | "embedded" | "operator" | "variable" | "variable.parameter" => {
499            Style::new().fg(theme.text)
500        }
501        "punctuation" | "punctuation.bracket" | "punctuation.delimiter" | "punctuation.special" => {
502            Style::new().fg(theme.text_dim)
503        }
504        _ => Style::new().fg(theme.text),
505    }
506}
507
508#[cfg(any(
509    feature = "syntax-rust",
510    feature = "syntax-python",
511    feature = "syntax-javascript",
512    feature = "syntax-typescript",
513    feature = "syntax-go",
514    feature = "syntax-bash",
515    feature = "syntax-json",
516    feature = "syntax-toml",
517    feature = "syntax-c",
518    feature = "syntax-cpp",
519    feature = "syntax-java",
520    feature = "syntax-ruby",
521    feature = "syntax-css",
522    feature = "syntax-html",
523    feature = "syntax-yaml",
524))]
525thread_local! {
526    // SAFETY: SLT runs a single-threaded synchronous event loop.
527    // Re-entrant highlight calls are architecturally impossible.
528    // If an async runtime is added later, revisit this (see issue #113).
529    static HIGHLIGHTER: std::cell::RefCell<tree_sitter_highlight::Highlighter> =
530        std::cell::RefCell::new(tree_sitter_highlight::Highlighter::new());
531}
532
533/// Highlight source code using tree-sitter.
534///
535/// Returns `Some(lines)` where each line is a `Vec<(text, style)>` of
536/// styled segments, or `None` if:
537/// - The language is not recognised
538/// - The corresponding `syntax-*` feature is not enabled
539/// - Parsing fails
540///
541/// Callers should fall back to the built-in keyword highlighter when
542/// `None` is returned.
543///
544/// # Example
545///
546/// ```ignore
547/// let lines = slt::syntax::highlight_code("let x = 1;", "rust", &theme);
548/// ```
549#[allow(unused_variables)]
550pub fn highlight_code(code: &str, lang: &str, theme: &Theme) -> Option<Vec<Vec<(String, Style)>>> {
551    #[cfg(any(
552        feature = "syntax-rust",
553        feature = "syntax-python",
554        feature = "syntax-javascript",
555        feature = "syntax-typescript",
556        feature = "syntax-go",
557        feature = "syntax-bash",
558        feature = "syntax-json",
559        feature = "syntax-toml",
560        feature = "syntax-c",
561        feature = "syntax-cpp",
562        feature = "syntax-java",
563        feature = "syntax-ruby",
564        feature = "syntax-css",
565        feature = "syntax-html",
566        feature = "syntax-yaml",
567    ))]
568    {
569        use tree_sitter_highlight::HighlightEvent;
570
571        let config = get_config(lang)?;
572        let highlights = HIGHLIGHTER.with(|cell| {
573            let mut highlighter = cell.borrow_mut();
574            highlighter
575                .highlight(config, code.as_bytes(), None, |_| None)
576                .ok()
577                .map(|iter| iter.collect::<Vec<_>>())
578        })?;
579        let highlights = highlights.into_iter();
580
581        let default_style = Style::new().fg(theme.text);
582        let mut result: Vec<Vec<(String, Style)>> = Vec::new();
583        let mut current_line: Vec<(String, Style)> = Vec::new();
584        let mut style_stack: Vec<Style> = vec![default_style];
585
586        for event in highlights {
587            match event.ok()? {
588                HighlightEvent::Source { start, end } => {
589                    let text = &code[start..end];
590                    let style = *style_stack.last().unwrap_or(&default_style);
591                    // Split by newlines to produce per-line segments
592                    for (i, part) in text.split('\n').enumerate() {
593                        if i > 0 {
594                            result.push(std::mem::take(&mut current_line));
595                        }
596                        if !part.is_empty() {
597                            current_line.push((part.to_string(), style));
598                        }
599                    }
600                }
601                HighlightEvent::HighlightStart(highlight) => {
602                    let name = HIGHLIGHT_NAMES.get(highlight.0).copied().unwrap_or("");
603                    let style = highlight_name_to_style(name, theme);
604                    style_stack.push(style);
605                }
606                HighlightEvent::HighlightEnd => {
607                    style_stack.pop();
608                }
609            }
610        }
611
612        if !current_line.is_empty() {
613            result.push(current_line);
614        }
615
616        Some(result)
617    }
618
619    #[cfg(not(any(
620        feature = "syntax-rust",
621        feature = "syntax-python",
622        feature = "syntax-javascript",
623        feature = "syntax-typescript",
624        feature = "syntax-go",
625        feature = "syntax-bash",
626        feature = "syntax-json",
627        feature = "syntax-toml",
628        feature = "syntax-c",
629        feature = "syntax-cpp",
630        feature = "syntax-java",
631        feature = "syntax-ruby",
632        feature = "syntax-css",
633        feature = "syntax-html",
634        feature = "syntax-yaml",
635    )))]
636    {
637        None
638    }
639}
640
641/// Returns `true` if tree-sitter highlighting is available for `lang`.
642///
643/// This checks both that the corresponding `syntax-*` feature is enabled
644/// and that the language string is recognised.
645#[allow(unused_variables)]
646pub fn is_language_supported(lang: &str) -> bool {
647    #[cfg(any(
648        feature = "syntax-rust",
649        feature = "syntax-python",
650        feature = "syntax-javascript",
651        feature = "syntax-typescript",
652        feature = "syntax-go",
653        feature = "syntax-bash",
654        feature = "syntax-json",
655        feature = "syntax-toml",
656        feature = "syntax-c",
657        feature = "syntax-cpp",
658        feature = "syntax-java",
659        feature = "syntax-ruby",
660        feature = "syntax-css",
661        feature = "syntax-html",
662        feature = "syntax-yaml",
663    ))]
664    {
665        get_config(lang).is_some()
666    }
667    #[cfg(not(any(
668        feature = "syntax-rust",
669        feature = "syntax-python",
670        feature = "syntax-javascript",
671        feature = "syntax-typescript",
672        feature = "syntax-go",
673        feature = "syntax-bash",
674        feature = "syntax-json",
675        feature = "syntax-toml",
676        feature = "syntax-c",
677        feature = "syntax-cpp",
678        feature = "syntax-java",
679        feature = "syntax-ruby",
680        feature = "syntax-css",
681        feature = "syntax-html",
682        feature = "syntax-yaml",
683    )))]
684    {
685        false
686    }
687}
688
689#[cfg(test)]
690mod tests {
691    #![allow(clippy::unwrap_used)]
692    use super::*;
693    use crate::style::Theme;
694
695    #[test]
696    fn highlight_returns_none_for_unknown_lang() {
697        let theme = Theme::dark();
698        assert!(highlight_code("let x = 1;", "brainfuck", &theme).is_none());
699    }
700
701    #[test]
702    fn is_language_supported_unknown() {
703        assert!(!is_language_supported("haskell"));
704    }
705
706    #[cfg(feature = "syntax-rust")]
707    #[test]
708    fn highlight_rust_basic() {
709        let theme = Theme::dark();
710        let result = highlight_code("let x = 1;", "rust", &theme);
711        assert!(result.is_some());
712        let lines = result.unwrap();
713        assert_eq!(lines.len(), 1);
714        // "let" should be in the first line's segments
715        let flat: String = lines[0].iter().map(|(t, _)| t.as_str()).collect();
716        assert!(flat.contains("let"));
717        assert!(flat.contains("1"));
718    }
719
720    #[cfg(feature = "syntax-rust")]
721    #[test]
722    fn highlight_rust_multiline() {
723        let theme = Theme::dark();
724        let code = "fn main() {\n    println!(\"hello\");\n}";
725        let result = highlight_code(code, "rust", &theme).unwrap();
726        assert_eq!(result.len(), 3);
727    }
728
729    #[cfg(feature = "syntax-rust")]
730    #[test]
731    fn highlight_rust_rs_alias() {
732        let theme = Theme::dark();
733        assert!(highlight_code("let x = 1;", "rs", &theme).is_some());
734    }
735
736    #[cfg(feature = "syntax-python")]
737    #[test]
738    fn highlight_python_basic() {
739        let theme = Theme::dark();
740        let result = highlight_code("def foo():\n    return 42", "python", &theme);
741        assert!(result.is_some());
742        let lines = result.unwrap();
743        assert_eq!(lines.len(), 2);
744    }
745
746    #[cfg(feature = "syntax-javascript")]
747    #[test]
748    fn highlight_javascript_basic() {
749        let theme = Theme::dark();
750        let result = highlight_code("const x = () => 42;", "js", &theme);
751        assert!(result.is_some());
752    }
753
754    #[cfg(feature = "syntax-bash")]
755    #[test]
756    fn highlight_bash_basic() {
757        let theme = Theme::dark();
758        let result = highlight_code("echo \"hello\"", "sh", &theme);
759        assert!(result.is_some());
760    }
761
762    #[cfg(feature = "syntax-json")]
763    #[test]
764    fn highlight_json_basic() {
765        let theme = Theme::dark();
766        let result = highlight_code("{\"key\": 42}", "json", &theme);
767        assert!(result.is_some());
768    }
769
770    #[cfg(feature = "syntax-toml")]
771    #[test]
772    fn highlight_toml_basic() {
773        let theme = Theme::dark();
774        let result = highlight_code("[package]\nname = \"slt\"", "toml", &theme);
775        assert!(result.is_some());
776    }
777
778    #[cfg(feature = "syntax-go")]
779    #[test]
780    fn highlight_go_basic() {
781        let theme = Theme::dark();
782        let result = highlight_code("package main\nfunc main() {}", "go", &theme);
783        assert!(result.is_some());
784    }
785
786    #[cfg(feature = "syntax-rust")]
787    #[test]
788    fn highlight_light_theme_differs() {
789        let dark = Theme::dark();
790        let light = Theme::light();
791        let dark_result = highlight_code("let x = 1;", "rust", &dark).unwrap();
792        let light_result = highlight_code("let x = 1;", "rust", &light).unwrap();
793        // Keyword styles should differ between dark and light
794        let dark_styles: Vec<Style> = dark_result[0].iter().map(|(_, s)| *s).collect();
795        let light_styles: Vec<Style> = light_result[0].iter().map(|(_, s)| *s).collect();
796        assert_ne!(dark_styles, light_styles);
797    }
798
799    #[cfg(feature = "syntax-rust")]
800    #[test]
801    fn highlight_keyword_uses_theme_palette() {
802        // The `let` keyword should adopt each theme's syntax palette rather
803        // than a hardcoded One Dark color.
804        let nord = Theme::nord();
805        let catppuccin = Theme::catppuccin();
806
807        let kw_fg = |theme: &Theme| -> crate::style::Color {
808            let line = highlight_code("let x = 1;", "rust", theme).unwrap();
809            line[0]
810                .iter()
811                .find_map(|(text, style)| (text.as_str() == "let").then_some(style.fg.unwrap()))
812                .expect("`let` keyword segment present")
813        };
814
815        assert_eq!(kw_fg(&nord), nord.syntax.keyword);
816        assert_eq!(kw_fg(&catppuccin), catppuccin.syntax.keyword);
817        // The two themes resolve to different keyword colors — proving the
818        // old hardcoded One Dark purple is no longer used.
819        assert_ne!(nord.syntax.keyword, catppuccin.syntax.keyword);
820    }
821
822    #[cfg(feature = "syntax-rust")]
823    #[test]
824    fn code_block_renders_with_theme_syntax_palette() {
825        use crate::style::Theme;
826        use crate::test_utils::TestBackend;
827
828        let theme = Theme::tokyo_night();
829        let mut tb = TestBackend::new(40, 8);
830        tb.render(|ui| {
831            ui.set_theme(theme);
832            let _ = ui.code_block("fn main() {}").lang("rust").show();
833        });
834
835        // The code text still renders.
836        tb.assert_contains("fn");
837        tb.assert_contains("main");
838
839        // Some keyword cell adopts Tokyo Night's keyword color, and the old
840        // hardcoded One Dark purple is absent from the buffer.
841        let one_dark_keyword = crate::style::Color::Rgb(198, 120, 221);
842        let buffer = tb.buffer();
843        let mut saw_theme_keyword = false;
844        for y in 0..tb.height() {
845            for x in 0..tb.width() {
846                let fg = buffer.get(x, y).style.fg;
847                assert_ne!(
848                    fg,
849                    Some(one_dark_keyword),
850                    "One Dark keyword color must not appear under Tokyo Night"
851                );
852                if fg == Some(theme.syntax.keyword) {
853                    saw_theme_keyword = true;
854                }
855            }
856        }
857        assert!(
858            saw_theme_keyword,
859            "expected a cell colored with Tokyo Night's keyword color"
860        );
861    }
862
863    #[cfg(feature = "syntax-rust")]
864    #[test]
865    fn highlight_incomplete_code_does_not_panic() {
866        let theme = Theme::dark();
867        let result = highlight_code("fn main( {", "rust", &theme);
868        assert!(result.is_some());
869    }
870
871    #[cfg(feature = "syntax-c")]
872    #[test]
873    fn highlight_c_basic() {
874        let theme = Theme::dark();
875        assert!(
876            highlight_code("#include <stdio.h>\nint main() { return 0; }", "c", &theme).is_some()
877        );
878    }
879
880    #[cfg(feature = "syntax-cpp")]
881    #[test]
882    fn highlight_cpp_basic() {
883        let theme = Theme::dark();
884        assert!(highlight_code("class Foo { public: void bar(); };", "cpp", &theme).is_some());
885    }
886
887    #[cfg(feature = "syntax-typescript")]
888    #[test]
889    fn highlight_typescript_basic() {
890        let theme = Theme::dark();
891        assert!(highlight_code("const x: number = 42;", "ts", &theme).is_some());
892    }
893
894    #[cfg(feature = "syntax-typescript")]
895    #[test]
896    fn highlight_tsx_basic() {
897        let theme = Theme::dark();
898        assert!(highlight_code("const App = () => <div>hello</div>;", "tsx", &theme).is_some());
899    }
900
901    #[cfg(feature = "syntax-java")]
902    #[test]
903    fn highlight_java_basic() {
904        let theme = Theme::dark();
905        assert!(highlight_code(
906            "public class Main { public static void main(String[] args) {} }",
907            "java",
908            &theme
909        )
910        .is_some());
911    }
912
913    #[cfg(feature = "syntax-ruby")]
914    #[test]
915    fn highlight_ruby_basic() {
916        let theme = Theme::dark();
917        assert!(highlight_code("def hello\n  puts 'world'\nend", "ruby", &theme).is_some());
918    }
919
920    #[cfg(feature = "syntax-css")]
921    #[test]
922    fn highlight_css_basic() {
923        let theme = Theme::dark();
924        assert!(highlight_code("body { color: red; }", "css", &theme).is_some());
925    }
926
927    #[cfg(feature = "syntax-html")]
928    #[test]
929    fn highlight_html_basic() {
930        let theme = Theme::dark();
931        assert!(highlight_code("<div class=\"test\">hello</div>", "html", &theme).is_some());
932    }
933
934    #[cfg(feature = "syntax-yaml")]
935    #[test]
936    fn highlight_yaml_basic() {
937        let theme = Theme::dark();
938        assert!(highlight_code("name: slt\nversion: 0.14", "yaml", &theme).is_some());
939    }
940
941    /// Regression test for issue #113:
942    /// `highlight_code()` must not panic on repeated calls (thread_local HIGHLIGHTER reuse).
943    #[cfg(feature = "syntax-rust")]
944    #[test]
945    fn highlight_reuse_does_not_panic() {
946        let theme = Theme::dark();
947        // Call twice with the same language — exercises HIGHLIGHTER.with borrow_mut reuse.
948        let first = highlight_code("let x = 1;", "rust", &theme);
949        let second = highlight_code("fn foo() {}", "rust", &theme);
950        assert!(first.is_some(), "first call should succeed");
951        assert!(second.is_some(), "second call should succeed");
952    }
953
954    /// Regression test for issue #113:
955    /// Multiple calls across different languages must all return Some.
956    #[cfg(all(feature = "syntax-rust", feature = "syntax-python"))]
957    #[test]
958    fn highlight_reuse_across_languages() {
959        let theme = Theme::dark();
960        let r1 = highlight_code("let x = 1;", "rust", &theme);
961        let r2 = highlight_code("def foo(): pass", "python", &theme);
962        let r3 = highlight_code("fn bar() {}", "rust", &theme);
963        assert!(r1.is_some());
964        assert!(r2.is_some());
965        assert!(r3.is_some());
966    }
967}