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 { syntax_set: two_face::syntax::extra_newlines(), cache: Mutex::new(HashMap::new()) }
32    }
33
34    /// Syntax-highlights `code`, caching the result by `(lang, code)`.
35    pub fn highlight(&self, code: &str, lang: &str, theme: &Theme) -> Vec<Line> {
36        if let Some(cached) = self.cache.lock().unwrap().get(lang).and_then(|m| m.get(code)) {
37            return cached.clone();
38        }
39
40        let lines = self.render_highlighted_lines(code, lang, theme);
41        self.cache.lock().unwrap().entry(lang.to_string()).or_default().insert(code.to_string(), lines.clone());
42
43        lines
44    }
45}
46
47impl SyntaxHighlighter {
48    fn render_highlighted_lines(&self, code: &str, lang: &str, theme: &Theme) -> Vec<Line> {
49        let syntax = find_syntax_for_hint(&self.syntax_set, lang);
50
51        let Some(syntax) = syntax else {
52            return plain_code_lines(code, theme);
53        };
54
55        let syntect_theme = theme.syntect_theme();
56        let mut h = HighlightLines::new(syntax, syntect_theme);
57        let mut lines = Vec::new();
58
59        for source_line in code.lines() {
60            let Ok(ranges) = h.highlight_line(source_line, &self.syntax_set) else {
61                lines.push(Line::with_style(source_line, Style::fg(theme.code_fg())));
62                continue;
63            };
64
65            let mut line = Line::default();
66            for (syntect_style, text) in ranges {
67                line.push_span(Span::with_style(text, syntect_to_wisp_style(syntect_style)));
68            }
69            lines.push(line);
70        }
71
72        lines
73    }
74}
75
76fn plain_code_lines(code: &str, theme: &Theme) -> Vec<Line> {
77    let style = Style::fg(theme.code_fg());
78    code.lines().map(|line| Line::with_style(line, style)).collect()
79}