git_iris/studio/components/
syntax.rs1use 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
12static SYNTAX_SET: std::sync::LazyLock<SyntaxSet> =
14 std::sync::LazyLock::new(SyntaxSet::load_defaults_newlines);
15
16static THEME_SET: std::sync::LazyLock<ThemeSet> = std::sync::LazyLock::new(ThemeSet::load_defaults);
18
19pub struct SyntaxHighlighter {
21 syntax: Option<&'static SyntaxReference>,
22}
23
24impl SyntaxHighlighter {
25 pub fn for_extension(ext: &str) -> Self {
27 let syntax = SYNTAX_SET.find_syntax_by_extension(ext);
28 Self { syntax }
29 }
30
31 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 pub fn is_available(&self) -> bool {
39 self.syntax.is_some()
40 }
41
42 pub fn highlight_line(&self, line: &str) -> Vec<(Style, String)> {
44 let Some(syntax) = self.syntax else {
45 return vec![(
47 Style::default().fg(theme::text_primary_color()),
48 line.to_string(),
49 )];
50 };
51
52 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 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 pub fn highlight_lines(&self, lines: &[String]) -> Vec<Vec<(Style, String)>> {
82 lines.iter().map(|line| self.highlight_line(line)).collect()
83 }
84}
85
86fn 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
104fn syntect_color_to_silkcircuit(color: syntect::highlighting::Color) -> Color {
107 let r = color.r;
109 let g = color.g;
110 let b = color.b;
111
112 let saturation = color_saturation(r, g, b);
117 let luminance = color_luminance(r, g, b);
118
119 if is_purple_ish(r, g, b) {
121 return theme::accent_primary();
122 }
123
124 if is_green_ish(r, g, b) && saturation > 0.3 {
126 return theme::success_color();
127 }
128
129 if is_orange_ish(r, g, b) {
131 return theme::accent_tertiary();
132 }
133
134 if is_cyan_ish(r, g, b) {
136 return theme::accent_secondary();
137 }
138
139 if is_yellow_ish(r, g, b) {
141 return theme::warning_color();
142 }
143
144 if saturation < 0.15 && luminance < 0.6 {
146 return theme::text_muted_color();
147 }
148
149 if luminance > 0.2 {
151 Color::Rgb(r, g, b)
152 } else {
153 theme::text_secondary_color()
154 }
155}
156
157fn 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 r > 150 && g < 150 && b > 150
172}
173
174fn is_green_ish(r: u8, g: u8, b: u8) -> bool {
175 g > r && g > b && g > 120
177}
178
179fn is_orange_ish(r: u8, g: u8, b: u8) -> bool {
180 r > 180 && g > 80 && g < 180 && b < 150
182}
183
184fn is_cyan_ish(r: u8, g: u8, b: u8) -> bool {
185 r < 150 && g > 150 && b > 150
187}
188
189fn is_yellow_ish(r: u8, g: u8, b: u8) -> bool {
190 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}