1use crate::ui::theme::get_syntax_theme_for_ui_theme;
35use anstyle::Style as AnstyleStyle;
36use anstyle_syntect::to_anstyle;
37use once_cell::sync::Lazy;
38use syntect::highlighting::{Highlighter, Theme, ThemeSet};
39use syntect::parsing::{Scope, SyntaxReference, SyntaxSet};
40use syntect::util::LinesWithEndings;
41use tracing::warn;
42
43const DEFAULT_THEME_NAME: &str = "base16-ocean.dark";
45
46const MAX_INPUT_SIZE_BYTES: usize = 512 * 1024;
48
49const MAX_INPUT_LINES: usize = 10_000;
51
52static SHARED_SYNTAX_SET: Lazy<SyntaxSet> = Lazy::new(SyntaxSet::load_defaults_newlines);
54
55static SHARED_THEME_SET: Lazy<ThemeSet> = Lazy::new(|| match ThemeSet::load_defaults() {
57 defaults if !defaults.themes.is_empty() => defaults,
58 _ => {
59 warn!("Failed to load default syntax highlighting themes");
60 ThemeSet {
61 themes: Default::default(),
62 }
63 }
64});
65
66#[inline]
68pub fn syntax_set() -> &'static SyntaxSet {
69 &SHARED_SYNTAX_SET
70}
71
72#[inline]
74pub fn find_syntax_by_token(token: &str) -> &'static SyntaxReference {
75 SHARED_SYNTAX_SET
76 .find_syntax_by_token(token)
77 .unwrap_or_else(|| SHARED_SYNTAX_SET.find_syntax_plain_text())
78}
79
80#[inline]
82pub fn find_syntax_by_name(name: &str) -> Option<&'static SyntaxReference> {
83 SHARED_SYNTAX_SET.find_syntax_by_name(name)
84}
85
86#[inline]
88pub fn find_syntax_by_extension(ext: &str) -> Option<&'static SyntaxReference> {
89 SHARED_SYNTAX_SET.find_syntax_by_extension(ext)
90}
91
92#[inline]
94pub fn find_syntax_plain_text() -> &'static SyntaxReference {
95 SHARED_SYNTAX_SET.find_syntax_plain_text()
96}
97
98fn fallback_theme() -> Theme {
99 SHARED_THEME_SET
100 .themes
101 .values()
102 .next()
103 .cloned()
104 .unwrap_or_default()
105}
106
107fn plain_text_line_segments(code: &str) -> Vec<Vec<(syntect::highlighting::Style, String)>> {
108 let mut result = Vec::new();
109 let mut ends_with_newline = false;
110 for line in LinesWithEndings::from(code) {
111 ends_with_newline = line.ends_with('\n');
112 let trimmed = line.trim_end_matches('\n');
113 result.push(vec![(
114 syntect::highlighting::Style::default(),
115 trimmed.to_string(),
116 )]);
117 }
118
119 if ends_with_newline {
120 result.push(Vec::new());
121 }
122
123 result
124}
125
126pub fn load_theme(theme_name: &str, _cache: bool) -> Theme {
135 if let Some(theme) = SHARED_THEME_SET.themes.get(theme_name) {
136 theme.clone()
137 } else {
138 warn!(
139 theme = theme_name,
140 "Unknown syntax highlighting theme, falling back to default"
141 );
142 fallback_theme()
143 }
144}
145
146#[inline]
148pub fn default_theme_name() -> String {
149 DEFAULT_THEME_NAME.to_string()
150}
151
152pub fn available_themes() -> Vec<String> {
154 SHARED_THEME_SET.themes.keys().cloned().collect()
155}
156
157#[inline]
159pub fn should_highlight(code: &str) -> bool {
160 code.len() <= MAX_INPUT_SIZE_BYTES && code.lines().count() <= MAX_INPUT_LINES
161}
162
163#[inline]
168pub fn get_active_syntax_theme() -> &'static str {
169 get_syntax_theme_for_ui_theme(&crate::ui::theme::active_theme_id())
170}
171
172#[inline]
174pub fn get_syntax_theme(theme: &str) -> &'static str {
175 get_syntax_theme_for_ui_theme(theme)
176}
177
178#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
183pub struct DiffScopeBackgroundRgbs {
184 pub inserted: Option<(u8, u8, u8)>,
185 pub deleted: Option<(u8, u8, u8)>,
186}
187
188pub fn diff_scope_background_rgbs() -> DiffScopeBackgroundRgbs {
190 let theme_name = get_active_syntax_theme();
191 let theme = load_theme(theme_name, true);
192 diff_scope_background_rgbs_for_theme(&theme)
193}
194
195fn diff_scope_background_rgbs_for_theme(theme: &Theme) -> DiffScopeBackgroundRgbs {
196 let highlighter = Highlighter::new(theme);
197 let inserted = scope_background_rgb(&highlighter, "markup.inserted")
198 .or_else(|| scope_background_rgb(&highlighter, "diff.inserted"));
199 let deleted = scope_background_rgb(&highlighter, "markup.deleted")
200 .or_else(|| scope_background_rgb(&highlighter, "diff.deleted"));
201 DiffScopeBackgroundRgbs { inserted, deleted }
202}
203
204fn scope_background_rgb(highlighter: &Highlighter<'_>, scope_name: &str) -> Option<(u8, u8, u8)> {
205 let scope = Scope::new(scope_name).ok()?;
206 let background = highlighter.style_mod_for_stack(&[scope]).background?;
207 Some((background.r, background.g, background.b))
208}
209
210#[inline]
211fn select_syntax(language: Option<&str>) -> &'static SyntaxReference {
212 language
213 .map(find_syntax_by_token)
214 .unwrap_or_else(find_syntax_plain_text)
215}
216
217pub fn highlight_code_to_line_segments(
222 code: &str,
223 language: Option<&str>,
224 theme_name: &str,
225) -> Vec<Vec<(syntect::highlighting::Style, String)>> {
226 if !should_highlight(code) {
227 return plain_text_line_segments(code);
228 }
229
230 let syntax = select_syntax(language);
231 let theme = load_theme(theme_name, true);
232 let mut highlighter = syntect::easy::HighlightLines::new(syntax, &theme);
233 let mut result = Vec::new();
234 let mut ends_with_newline = false;
235
236 for line in LinesWithEndings::from(code) {
237 ends_with_newline = line.ends_with('\n');
238 let trimmed = line.trim_end_matches('\n');
239 let segments = match highlighter.highlight_line(trimmed, syntax_set()) {
240 Ok(ranges) => ranges
241 .into_iter()
242 .map(|(style, text)| (style, text.to_string()))
243 .collect(),
244 Err(_) => vec![(syntect::highlighting::Style::default(), trimmed.to_string())],
245 };
246 result.push(segments);
247 }
248
249 if ends_with_newline {
250 result.push(Vec::new());
251 }
252
253 result
254}
255
256pub fn highlight_code_to_anstyle_line_segments(
258 code: &str,
259 language: Option<&str>,
260 theme_name: &str,
261 strip_background: bool,
262) -> Vec<Vec<(AnstyleStyle, String)>> {
263 highlight_code_to_line_segments(code, language, theme_name)
264 .into_iter()
265 .map(|ranges| {
266 ranges
267 .into_iter()
268 .filter(|(_, text)| !text.is_empty())
269 .map(|(style, text)| {
270 let mut anstyle = to_anstyle(style);
271 if strip_background {
272 anstyle = anstyle.bg_color(None);
273 }
274 (anstyle, text)
275 })
276 .collect()
277 })
278 .collect()
279}
280
281pub fn highlight_line_to_anstyle_segments(
283 line: &str,
284 language: Option<&str>,
285 theme_name: &str,
286 strip_background: bool,
287) -> Option<Vec<(AnstyleStyle, String)>> {
288 highlight_code_to_anstyle_line_segments(line, language, theme_name, strip_background)
289 .into_iter()
290 .next()
291}
292
293pub fn highlight_code_to_segments(
307 code: &str,
308 language: Option<&str>,
309 theme_name: &str,
310) -> Vec<(syntect::highlighting::Style, String)> {
311 highlight_code_to_line_segments(code, language, theme_name)
312 .into_iter()
313 .flatten()
314 .collect()
315}
316
317pub fn highlight_line_for_diff(
321 line: &str,
322 language: Option<&str>,
323 theme_name: &str,
324) -> Option<Vec<(syntect::highlighting::Style, String)>> {
325 highlight_code_to_line_segments(line, language, theme_name)
326 .into_iter()
327 .next()
328}
329
330pub fn highlight_code_to_ansi(code: &str, language: Option<&str>, theme_name: &str) -> String {
332 let segments = highlight_code_to_segments(code, language, theme_name);
333 let mut output = String::with_capacity(code.len() + segments.len() * 10);
334
335 for (style, text) in segments {
336 let ansi_style = to_anstyle(style);
337 output.push_str(&ansi_style.to_string());
338 output.push_str(&text);
339 output.push_str("\x1b[0m"); }
341
342 output
343}
344
345#[cfg(test)]
346mod tests {
347 use super::*;
348 use std::str::FromStr;
349 use syntect::highlighting::Color as SyntectColor;
350 use syntect::highlighting::ScopeSelectors;
351 use syntect::highlighting::StyleModifier;
352 use syntect::highlighting::ThemeItem;
353 use syntect::highlighting::ThemeSettings;
354
355 fn theme_item(scope: &str, background: Option<(u8, u8, u8)>) -> ThemeItem {
356 ThemeItem {
357 scope: ScopeSelectors::from_str(scope).expect("scope selector should parse"),
358 style: StyleModifier {
359 background: background.map(|(r, g, b)| SyntectColor { r, g, b, a: 255 }),
360 ..StyleModifier::default()
361 },
362 }
363 }
364
365 #[test]
366 fn test_syntax_set_loaded() {
367 let ss = syntax_set();
368 assert!(!ss.syntaxes().is_empty());
369 }
370
371 #[test]
372 fn test_find_syntax_by_token() {
373 let rust = find_syntax_by_token("rust");
374 assert!(rust.name.contains("Rust"));
375 }
376
377 #[test]
378 fn test_should_highlight_guardrails() {
379 assert!(should_highlight("fn main() {}"));
380 assert!(!should_highlight(&"x".repeat(MAX_INPUT_SIZE_BYTES + 1)));
381 }
382
383 #[test]
384 fn test_get_active_syntax_theme() {
385 let theme = get_active_syntax_theme();
386 assert!(!theme.is_empty());
387 }
388
389 #[test]
390 fn test_highlight_code_to_segments() {
391 let segments =
392 highlight_code_to_segments("fn main() {}", Some("rust"), "base16-ocean.dark");
393 assert!(!segments.is_empty());
394 }
395
396 #[test]
397 fn test_theme_loading_stable() {
398 let theme1 = load_theme("base16-ocean.dark", true);
399 let theme2 = load_theme("base16-ocean.dark", true);
400 assert_eq!(theme1.name, theme2.name);
401 }
402
403 #[test]
404 fn diff_scope_backgrounds_prefer_markup_scope_then_diff_fallback() {
405 let theme = Theme {
406 settings: ThemeSettings::default(),
407 scopes: vec![
408 theme_item("markup.inserted", Some((10, 20, 30))),
409 theme_item("diff.deleted", Some((40, 50, 60))),
410 ],
411 ..Theme::default()
412 };
413
414 let rgbs = diff_scope_background_rgbs_for_theme(&theme);
415 assert_eq!(
416 rgbs,
417 DiffScopeBackgroundRgbs {
418 inserted: Some((10, 20, 30)),
419 deleted: Some((40, 50, 60)),
420 }
421 );
422 }
423
424 #[test]
425 fn diff_scope_backgrounds_return_none_when_scopes_do_not_match() {
426 let theme = Theme {
427 settings: ThemeSettings::default(),
428 scopes: vec![theme_item("constant.numeric", Some((1, 2, 3)))],
429 ..Theme::default()
430 };
431
432 let rgbs = diff_scope_background_rgbs_for_theme(&theme);
433 assert_eq!(
434 rgbs,
435 DiffScopeBackgroundRgbs {
436 inserted: None,
437 deleted: None,
438 }
439 );
440 }
441
442 #[test]
443 fn diff_scope_backgrounds_fall_back_to_diff_scopes() {
444 let theme = Theme {
445 settings: ThemeSettings::default(),
446 scopes: vec![
447 theme_item("diff.inserted", Some((16, 32, 48))),
448 theme_item("diff.deleted", Some((64, 80, 96))),
449 ],
450 ..Theme::default()
451 };
452
453 let rgbs = diff_scope_background_rgbs_for_theme(&theme);
454 assert_eq!(
455 rgbs,
456 DiffScopeBackgroundRgbs {
457 inserted: Some((16, 32, 48)),
458 deleted: Some((64, 80, 96)),
459 }
460 );
461 }
462}