1use gpui::{Font, FontStyle, FontWeight, Hsla, SharedString, TextRun};
2use std::cell::RefCell;
3use std::collections::HashMap;
4use std::rc::Rc;
5use syntect::highlighting::{HighlightIterator, HighlightState, Highlighter, Style, ThemeSet};
6use syntect::parsing::{ParseState, ScopeStack, SyntaxSet};
7
8struct SyntaxHighlighterInner {
9 syntax_set: SyntaxSet,
10 theme_set: ThemeSet,
11 current_theme: String,
12 parse_states: HashMap<String, ParseState>,
13 highlight_states: HashMap<String, HighlightState>,
14}
15
16#[derive(Clone)]
17pub struct SyntaxHighlighter {
18 inner: Rc<RefCell<SyntaxHighlighterInner>>,
19}
20
21impl SyntaxHighlighter {
22 pub fn new() -> Self {
23 let syntax_set = SyntaxSet::load_defaults_newlines();
24 let theme_set = ThemeSet::load_defaults();
25
26 let current_theme = theme_set
28 .themes
29 .keys()
30 .next()
31 .cloned()
32 .unwrap_or_else(|| "Default".to_string());
33
34 Self {
35 inner: Rc::new(RefCell::new(SyntaxHighlighterInner {
36 syntax_set,
37 theme_set,
38 current_theme,
39 parse_states: HashMap::new(),
40 highlight_states: HashMap::new(),
41 })),
42 }
43 }
44
45 pub fn set_theme(&mut self, theme_name: &str) {
46 let mut inner = self.inner.borrow_mut();
47 if inner.theme_set.themes.contains_key(theme_name) {
48 inner.current_theme = theme_name.to_string();
49 inner.highlight_states.clear();
50 }
51 }
52
53 pub fn available_themes(&self) -> Vec<String> {
54 self.inner
55 .borrow()
56 .theme_set
57 .themes
58 .keys()
59 .cloned()
60 .collect()
61 }
62
63 pub fn detect_language(&self, text: &str, file_extension: Option<&str>) -> Option<String> {
64 let inner = self.inner.borrow();
65 if let Some(ext) = file_extension {
66 if let Some(syntax) = inner.syntax_set.find_syntax_by_extension(ext) {
67 return Some(syntax.name.clone());
68 }
69 }
70
71 inner
72 .syntax_set
73 .find_syntax_by_first_line(text)
74 .map(|s| s.name.clone())
75 }
76
77 pub fn clear_state_from_line(&mut self, line_number: usize, language: &str) {
80 let mut inner = self.inner.borrow_mut();
81
82 if line_number == 0 {
87 inner.parse_states.remove(language);
88 }
89
90 let cache_key = format!("{}-{}", language, inner.current_theme);
92 if line_number == 0 {
93 inner.highlight_states.remove(&cache_key);
94 }
95 }
96
97 pub fn reset_state(&mut self) {
100 let mut inner = self.inner.borrow_mut();
101 inner.parse_states.clear();
102 inner.highlight_states.clear();
103 }
104
105 pub fn highlight_line(
106 &mut self,
107 line: &str,
108 language: &str,
109 line_number: usize,
110 font_family: SharedString,
111 _font_size: f32,
112 ) -> Vec<TextRun> {
113 let mut inner = self.inner.borrow_mut();
114
115 let has_syntax = inner.syntax_set.find_syntax_by_name(language).is_some();
117 if !has_syntax {
118 return vec![TextRun {
120 len: line.len(),
121 font: Font {
122 family: font_family,
123 features: Default::default(),
124 weight: FontWeight::NORMAL,
125 style: FontStyle::Normal,
126 fallbacks: Default::default(),
127 },
128 color: gpui::rgb(0xcccccc).into(),
129 background_color: None,
130 underline: None,
131 strikethrough: None,
132 }];
133 }
134
135 let cache_key = format!("{}-{}", language, inner.current_theme);
136 let parse_state_key = language.to_string();
137
138 if line_number == 0 {
140 inner.parse_states.remove(&parse_state_key);
141 inner.highlight_states.remove(&cache_key);
142 }
143
144 let syntax = inner
146 .syntax_set
147 .find_syntax_by_name(language)
148 .expect("syntax should exist after check above");
149
150 let mut parse_state = if line_number == 0 {
151 ParseState::new(syntax)
152 } else if let Some(state) = inner.parse_states.get(&parse_state_key) {
153 state.clone()
154 } else {
155 ParseState::new(syntax)
156 };
157
158 let theme = inner
160 .theme_set
161 .themes
162 .get(&inner.current_theme)
163 .or_else(|| inner.theme_set.themes.values().next());
164
165 if theme.is_none() {
166 return vec![TextRun {
168 len: line.len(),
169 font: Font {
170 family: font_family,
171 features: Default::default(),
172 weight: FontWeight::NORMAL,
173 style: FontStyle::Normal,
174 fallbacks: Default::default(),
175 },
176 color: gpui::rgb(0xcccccc).into(),
177 background_color: None,
178 underline: None,
179 strikethrough: None,
180 }];
181 }
182
183 let theme = theme.expect("theme should exist after check above");
184 let highlighter = Highlighter::new(theme);
185
186 let ops = parse_state
187 .parse_line(line, &inner.syntax_set)
188 .unwrap_or_default();
189
190 let mut highlight_state = if line_number == 0 {
191 HighlightState::new(&highlighter, ScopeStack::new())
192 } else if let Some(state) = inner.highlight_states.get(&cache_key) {
193 state.clone()
194 } else {
195 HighlightState::new(&highlighter, ScopeStack::new())
196 };
197
198 let mut text_runs = Vec::new();
199 let mut current_pos = 0;
200
201 let ranges: Vec<(Style, usize, usize)> =
202 HighlightIterator::new(&mut highlight_state, &ops, line, &highlighter)
203 .map(|(style, text)| {
204 let start = current_pos;
205 let end = current_pos + text.len();
206 current_pos = end;
207 (style, start, end)
208 })
209 .collect();
210
211 for (style, start, end) in ranges {
212 let len = end - start;
213 if len == 0 {
214 continue;
215 }
216
217 let color = style_to_hsla(style);
218 let (weight, font_style) = get_font_style(style);
219
220 text_runs.push(TextRun {
221 len,
222 font: Font {
223 family: font_family.clone(),
224 features: Default::default(),
225 weight,
226 style: font_style,
227 fallbacks: Default::default(),
228 },
229 color,
230 background_color: if style.background != style.foreground {
231 Some(style_color_to_hsla(style.background))
232 } else {
233 None
234 },
235 underline: if style
236 .font_style
237 .contains(syntect::highlighting::FontStyle::UNDERLINE)
238 {
239 Some(Default::default())
240 } else {
241 None
242 },
243 strikethrough: None,
244 });
245 }
246
247 if text_runs.is_empty() {
248 text_runs.push(TextRun {
249 len: line.len(),
250 font: Font {
251 family: font_family,
252 features: Default::default(),
253 weight: FontWeight::NORMAL,
254 style: FontStyle::Normal,
255 fallbacks: Default::default(),
256 },
257 color: gpui::rgb(0xcccccc).into(),
258 background_color: None,
259 underline: None,
260 strikethrough: None,
261 });
262 }
263
264 let new_parse_state = parse_state
266 .parse_line(line, &inner.syntax_set)
267 .map(|_| parse_state.clone())
268 .unwrap_or_else(|_| ParseState::new(syntax));
269 inner.parse_states.insert(parse_state_key, new_parse_state);
270
271 inner.highlight_states.insert(cache_key, highlight_state);
273
274 text_runs
275 }
276
277 pub fn get_theme_background(&self) -> Hsla {
278 let inner = self.inner.borrow();
279 inner
280 .theme_set
281 .themes
282 .get(&inner.current_theme)
283 .and_then(|theme| theme.settings.background)
284 .map(style_color_to_hsla)
285 .unwrap_or_else(|| gpui::rgb(0x1e1e1e).into())
286 }
287
288 pub fn get_theme_foreground(&self) -> Hsla {
289 let inner = self.inner.borrow();
290 inner
291 .theme_set
292 .themes
293 .get(&inner.current_theme)
294 .and_then(|theme| theme.settings.foreground)
295 .map(style_color_to_hsla)
296 .unwrap_or_else(|| gpui::rgb(0xcccccc).into())
297 }
298
299 pub fn get_theme_gutter_background(&self) -> Hsla {
300 let inner = self.inner.borrow();
301 inner
302 .theme_set
303 .themes
304 .get(&inner.current_theme)
305 .and_then(|theme| {
306 theme.settings.gutter.map(style_color_to_hsla).or_else(|| {
307 theme.settings.background.map(|bg| {
308 let mut hsla: Hsla = style_color_to_hsla(bg);
310 hsla.l = (hsla.l * 0.95).max(0.0);
311 hsla
312 })
313 })
314 })
315 .unwrap_or_else(|| gpui::rgb(0x252525).into())
316 }
317
318 pub fn get_theme_line_highlight(&self) -> Hsla {
319 let inner = self.inner.borrow();
320 inner
321 .theme_set
322 .themes
323 .get(&inner.current_theme)
324 .and_then(|theme| theme.settings.line_highlight)
325 .map(|color| {
326 let mut hsla = style_color_to_hsla(color);
327 hsla.a = hsla.a.min(0.3); hsla
329 })
330 .unwrap_or_else(|| gpui::rgba(0x2a2a2aff).into())
331 }
332
333 pub fn get_theme_selection(&self) -> Hsla {
334 let inner = self.inner.borrow();
335 inner
336 .theme_set
337 .themes
338 .get(&inner.current_theme)
339 .and_then(|theme| theme.settings.selection)
340 .map(|color| {
341 let mut hsla = style_color_to_hsla(color);
342 hsla.a = hsla.a.min(0.5); hsla
344 })
345 .unwrap_or_else(|| gpui::rgba(0x3e4451aa).into())
346 }
347
348 #[allow(dead_code)]
351 pub fn load_theme_from_file(&mut self, path: &str) -> Result<(), String> {
352 use std::fs::File;
353 use std::io::BufReader;
354
355 let file = File::open(path).map_err(|e| format!("Failed to open theme file: {}", e))?;
356 let reader = BufReader::new(file);
357
358 let theme = syntect::highlighting::ThemeSet::load_from_reader(&mut BufReader::new(reader))
359 .map_err(|e| format!("Failed to parse theme: {}", e))?;
360
361 let theme_name = std::path::Path::new(path)
362 .file_stem()
363 .and_then(|s| s.to_str())
364 .unwrap_or("custom")
365 .to_string();
366
367 let mut inner = self.inner.borrow_mut();
368 inner.theme_set.themes.insert(theme_name.clone(), theme);
369 inner.current_theme = theme_name;
370
371 Ok(())
372 }
373
374 #[allow(dead_code)]
377 pub fn load_syntax_from_file(&mut self, path: &str) -> Result<(), String> {
378 let mut inner = self.inner.borrow_mut();
379 let mut builder = syntect::parsing::SyntaxSetBuilder::new();
380 builder
381 .add_from_folder(path, true)
382 .map_err(|e| format!("Failed to load syntax: {}", e))?;
383
384 for _syntax in inner.syntax_set.syntaxes() {
386 builder.add_plain_text_syntax();
387 }
388
389 inner.syntax_set = builder.build();
390 inner.parse_states.clear();
391 inner.highlight_states.clear();
392
393 Ok(())
394 }
395}
396
397fn style_color_to_hsla(color: syntect::highlighting::Color) -> Hsla {
398 gpui::rgba(
399 ((color.r as u32) << 24)
400 | ((color.g as u32) << 16)
401 | ((color.b as u32) << 8)
402 | (color.a as u32),
403 )
404 .into()
405}
406
407fn style_to_hsla(style: Style) -> Hsla {
408 style_color_to_hsla(style.foreground)
409}
410
411fn get_font_style(style: Style) -> (FontWeight, FontStyle) {
412 let weight = if style
413 .font_style
414 .contains(syntect::highlighting::FontStyle::BOLD)
415 {
416 FontWeight::BOLD
417 } else {
418 FontWeight::NORMAL
419 };
420
421 let font_style = if style
422 .font_style
423 .contains(syntect::highlighting::FontStyle::ITALIC)
424 {
425 FontStyle::Italic
426 } else {
427 FontStyle::Normal
428 };
429
430 (weight, font_style)
431}
432
433impl Default for SyntaxHighlighter {
434 fn default() -> Self {
435 Self::new()
436 }
437}
438
439