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