Skip to main content

tui/syntax_highlighting/
syntax_highlighter.rs

1use super::syntect_bridge::{find_syntax_for_hint, syntect_to_wisp_style};
2use crate::line::Line;
3use crate::span::Span;
4use crate::style::Style;
5use crate::theme::Theme;
6use std::collections::HashMap;
7use std::sync::Mutex;
8use syntect::easy::HighlightLines;
9use syntect::parsing::SyntaxSet;
10
11/// Unified syntax-highlighting facade.
12///
13/// Results are cached by `(lang, code)` so repeated re-renders
14/// (e.g. during streaming) skip the expensive syntect pass.
15///
16/// The cache uses interior mutability (`Mutex`) so `highlight`
17/// works through shared references (`&self`).
18pub struct SyntaxHighlighter {
19    syntax_set: SyntaxSet,
20    cache: Mutex<HashMap<String, HashMap<String, Vec<Line>>>>,
21}
22
23impl Default for SyntaxHighlighter {
24    fn default() -> Self {
25        Self::new()
26    }
27}
28
29impl SyntaxHighlighter {
30    pub fn new() -> Self {
31        Self {
32            syntax_set: two_face::syntax::extra_newlines(),
33            cache: Mutex::new(HashMap::new()),
34        }
35    }
36
37    /// Syntax-highlights `code`, caching the result by `(lang, code)`.
38    pub fn highlight(&self, code: &str, lang: &str, theme: &Theme) -> Vec<Line> {
39        if let Some(cached) = self
40            .cache
41            .lock()
42            .unwrap()
43            .get(lang)
44            .and_then(|m| m.get(code))
45        {
46            return cached.clone();
47        }
48
49        let lines = self.render_highlighted_lines(code, lang, theme);
50        self.cache
51            .lock()
52            .unwrap()
53            .entry(lang.to_string())
54            .or_default()
55            .insert(code.to_string(), lines.clone());
56
57        lines
58    }
59}
60
61impl SyntaxHighlighter {
62    fn render_highlighted_lines(&self, code: &str, lang: &str, theme: &Theme) -> Vec<Line> {
63        let syntax = find_syntax_for_hint(&self.syntax_set, lang);
64
65        let Some(syntax) = syntax else {
66            return plain_code_lines(code, theme);
67        };
68
69        let syntect_theme = theme.syntect_theme();
70        let mut h = HighlightLines::new(syntax, syntect_theme);
71        let mut lines = Vec::new();
72
73        for source_line in code.lines() {
74            let Ok(ranges) = h.highlight_line(source_line, &self.syntax_set) else {
75                lines.push(Line::with_style(source_line, Style::fg(theme.code_fg())));
76                continue;
77            };
78
79            let mut line = Line::default();
80            for (syntect_style, text) in ranges {
81                line.push_span(Span::with_style(text, syntect_to_wisp_style(syntect_style)));
82            }
83            lines.push(line);
84        }
85
86        lines
87    }
88}
89
90fn plain_code_lines(code: &str, theme: &Theme) -> Vec<Line> {
91    let style = Style::fg(theme.code_fg());
92    code.lines()
93        .map(|line| Line::with_style(line, style))
94        .collect()
95}