git_iris/studio/components/
syntax.rs

1//! Syntax highlighting for code view using `SilkCircuit` colors
2//!
3//! Maps syntect token types to our theme colors for a cohesive look.
4
5use ratatui::style::{Color, Modifier, Style};
6use syntect::easy::HighlightLines;
7use syntect::highlighting::{FontStyle, Style as SyntectStyle, ThemeSet};
8use syntect::parsing::{SyntaxReference, SyntaxSet};
9
10use crate::studio::theme;
11
12/// Global syntax set - loaded once
13static SYNTAX_SET: std::sync::LazyLock<SyntaxSet> =
14    std::sync::LazyLock::new(SyntaxSet::load_defaults_newlines);
15
16/// Global theme set - load default themes for syntax highlighting
17static THEME_SET: std::sync::LazyLock<ThemeSet> = std::sync::LazyLock::new(ThemeSet::load_defaults);
18
19/// Syntax highlighter with caching
20pub struct SyntaxHighlighter {
21    syntax: Option<&'static SyntaxReference>,
22}
23
24impl SyntaxHighlighter {
25    /// Create a new highlighter for the given file extension
26    pub fn for_extension(ext: &str) -> Self {
27        let syntax = SYNTAX_SET.find_syntax_by_extension(ext);
28        Self { syntax }
29    }
30
31    /// Create a new highlighter for the given file path
32    pub fn for_path(path: &std::path::Path) -> Self {
33        let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("");
34        Self::for_extension(ext)
35    }
36
37    /// Check if syntax highlighting is available
38    pub fn is_available(&self) -> bool {
39        self.syntax.is_some()
40    }
41
42    /// Highlight a single line, returning styled spans
43    pub fn highlight_line(&self, line: &str) -> Vec<(Style, String)> {
44        let Some(syntax) = self.syntax else {
45            // No syntax highlighting - return plain
46            return vec![(
47                Style::default().fg(theme::text_primary_color()),
48                line.to_string(),
49            )];
50        };
51
52        // Try to get a dark theme, fallback to any available theme, or give up
53        let Some(theme) = THEME_SET
54            .themes
55            .get("base16-ocean.dark")
56            .or_else(|| THEME_SET.themes.get("InspiredGitHub"))
57            .or_else(|| THEME_SET.themes.values().next())
58        else {
59            // No themes available - return plain text
60            return vec![(
61                Style::default().fg(theme::text_primary_color()),
62                line.to_string(),
63            )];
64        };
65
66        let mut highlighter = HighlightLines::new(syntax, theme);
67
68        match highlighter.highlight_line(line, &SYNTAX_SET) {
69            Ok(ranges) => ranges
70                .into_iter()
71                .map(|(style, text)| (syntect_to_ratatui(style), text.to_string()))
72                .collect(),
73            Err(_) => vec![(
74                Style::default().fg(theme::text_primary_color()),
75                line.to_string(),
76            )],
77        }
78    }
79
80    /// Highlight multiple lines
81    pub fn highlight_lines(&self, lines: &[String]) -> Vec<Vec<(Style, String)>> {
82        lines.iter().map(|line| self.highlight_line(line)).collect()
83    }
84}
85
86/// Convert syntect style to ratatui style with `SilkCircuit` color mapping
87fn syntect_to_ratatui(style: SyntectStyle) -> Style {
88    let fg = syntect_color_to_silkcircuit(style.foreground);
89    let mut ratatui_style = Style::default().fg(fg);
90
91    if style.font_style.contains(FontStyle::BOLD) {
92        ratatui_style = ratatui_style.add_modifier(Modifier::BOLD);
93    }
94    if style.font_style.contains(FontStyle::ITALIC) {
95        ratatui_style = ratatui_style.add_modifier(Modifier::ITALIC);
96    }
97    if style.font_style.contains(FontStyle::UNDERLINE) {
98        ratatui_style = ratatui_style.add_modifier(Modifier::UNDERLINED);
99    }
100
101    ratatui_style
102}
103
104/// Map syntect colors to `SilkCircuit` palette
105/// This creates a cohesive look by mapping token colors to our theme
106fn syntect_color_to_silkcircuit(color: syntect::highlighting::Color) -> Color {
107    // Extract RGB values
108    let r = color.r;
109    let g = color.g;
110    let b = color.b;
111
112    // Map common syntax highlighting colors to SilkCircuit palette
113    // We analyze the color characteristics and map to our theme
114
115    // Very bright/saturated colors -> map to our accent colors
116    let saturation = color_saturation(r, g, b);
117    let luminance = color_luminance(r, g, b);
118
119    // Keywords, control flow (often purple/magenta in themes)
120    if is_purple_ish(r, g, b) {
121        return theme::accent_primary();
122    }
123
124    // Strings (often green/teal)
125    if is_green_ish(r, g, b) && saturation > 0.3 {
126        return theme::success_color();
127    }
128
129    // Numbers, constants (often orange/coral)
130    if is_orange_ish(r, g, b) {
131        return theme::accent_tertiary();
132    }
133
134    // Functions, methods (often cyan/blue)
135    if is_cyan_ish(r, g, b) {
136        return theme::accent_secondary();
137    }
138
139    // Types, classes (often yellow)
140    if is_yellow_ish(r, g, b) {
141        return theme::warning_color();
142    }
143
144    // Comments (usually gray/dim)
145    if saturation < 0.15 && luminance < 0.6 {
146        return theme::text_muted_color();
147    }
148
149    // Default: use original color if it's reasonably visible
150    if luminance > 0.2 {
151        Color::Rgb(r, g, b)
152    } else {
153        theme::text_secondary_color()
154    }
155}
156
157// Color analysis helpers
158
159fn color_saturation(r: u8, g: u8, b: u8) -> f32 {
160    let max = f32::from(r.max(g).max(b));
161    let min = f32::from(r.min(g).min(b));
162    if max == 0.0 { 0.0 } else { (max - min) / max }
163}
164
165fn color_luminance(r: u8, g: u8, b: u8) -> f32 {
166    (0.299 * f32::from(r) + 0.587 * f32::from(g) + 0.114 * f32::from(b)) / 255.0
167}
168
169fn is_purple_ish(r: u8, g: u8, b: u8) -> bool {
170    // Purple: high red, low green, high blue
171    r > 150 && g < 150 && b > 150
172}
173
174fn is_green_ish(r: u8, g: u8, b: u8) -> bool {
175    // Green: low red, high green, variable blue
176    g > r && g > b && g > 120
177}
178
179fn is_orange_ish(r: u8, g: u8, b: u8) -> bool {
180    // Orange/coral: high red, medium green, low blue
181    r > 180 && g > 80 && g < 180 && b < 150
182}
183
184fn is_cyan_ish(r: u8, g: u8, b: u8) -> bool {
185    // Cyan: low red, high green, high blue
186    r < 150 && g > 150 && b > 150
187}
188
189fn is_yellow_ish(r: u8, g: u8, b: u8) -> bool {
190    // Yellow: high red, high green, low blue
191    r > 180 && g > 180 && b < 150
192}
193
194#[cfg(test)]
195mod tests {
196    use super::*;
197
198    #[test]
199    fn test_highlighter_rust() {
200        let highlighter = SyntaxHighlighter::for_extension("rs");
201        assert!(highlighter.is_available());
202
203        let spans = highlighter.highlight_line("fn main() { }");
204        assert!(!spans.is_empty());
205    }
206
207    #[test]
208    fn test_highlighter_unknown() {
209        let highlighter = SyntaxHighlighter::for_extension("xyz_unknown");
210        assert!(!highlighter.is_available());
211    }
212}