Skip to main content

vtcode_tui/ui/
syntax_highlight.rs

1use once_cell::sync::Lazy;
2use std::collections::HashMap;
3use syntect::highlighting::{Theme, ThemeSet};
4use syntect::parsing::{SyntaxReference, SyntaxSet};
5use tracing::warn;
6
7const MAX_THEME_CACHE_SIZE: usize = 32;
8const DEFAULT_THEME_NAME: &str = "base16-ocean.dark";
9
10static SHARED_SYNTAX_SET: Lazy<SyntaxSet> = Lazy::new(SyntaxSet::load_defaults_newlines);
11
12static SHARED_THEME_CACHE: Lazy<parking_lot::RwLock<HashMap<String, Theme>>> = Lazy::new(|| {
13    match ThemeSet::load_defaults() {
14        defaults if !defaults.themes.is_empty() => {
15            let mut entries: Vec<(String, Theme)> = defaults.themes.into_iter().collect();
16            if entries.len() > MAX_THEME_CACHE_SIZE {
17                entries.truncate(MAX_THEME_CACHE_SIZE);
18            }
19            let themes: HashMap<_, _> = entries.into_iter().collect();
20            parking_lot::RwLock::new(themes)
21        }
22        _ => {
23            warn!(
24                "Failed to load default syntax highlighting themes; syntax highlighting will be disabled"
25            );
26            parking_lot::RwLock::new(HashMap::new())
27        }
28    }
29});
30
31pub fn syntax_set() -> &'static SyntaxSet {
32    &SHARED_SYNTAX_SET
33}
34
35pub fn find_syntax_by_token(token: &str) -> &'static SyntaxReference {
36    SHARED_SYNTAX_SET
37        .find_syntax_by_token(token)
38        .unwrap_or_else(|| SHARED_SYNTAX_SET.find_syntax_plain_text())
39}
40
41pub fn find_syntax_by_name(name: &str) -> Option<&'static SyntaxReference> {
42    SHARED_SYNTAX_SET.find_syntax_by_name(name)
43}
44
45pub fn find_syntax_by_extension(ext: &str) -> Option<&'static SyntaxReference> {
46    SHARED_SYNTAX_SET.find_syntax_by_extension(ext)
47}
48
49pub fn find_syntax_plain_text() -> &'static SyntaxReference {
50    SHARED_SYNTAX_SET.find_syntax_plain_text()
51}
52
53pub fn load_theme(theme_name: &str, cache: bool) -> Theme {
54    if let Some(theme) = SHARED_THEME_CACHE.read().get(theme_name).cloned() {
55        return theme;
56    }
57
58    let defaults = ThemeSet::load_defaults();
59    if let Some(theme) = defaults.themes.get(theme_name).cloned() {
60        if cache {
61            let mut guard = SHARED_THEME_CACHE.write();
62            if guard.len() >= MAX_THEME_CACHE_SIZE
63                && let Some(first_key) = guard.keys().next().cloned()
64            {
65                guard.remove(&first_key);
66            }
67            guard.insert(theme_name.to_owned(), theme.clone());
68        }
69        theme
70    } else {
71        warn!(
72            theme = theme_name,
73            "Unknown syntax highlighting theme, falling back to first available theme"
74        );
75        if defaults.themes.is_empty() {
76            warn!("No syntax highlighting themes available at all");
77            Theme::default()
78        } else {
79            defaults
80                .themes
81                .into_iter()
82                .next()
83                .map(|(_, theme)| theme)
84                .unwrap_or_default()
85        }
86    }
87}
88
89pub fn default_theme_name() -> String {
90    DEFAULT_THEME_NAME.to_string()
91}
92
93pub fn available_themes() -> Vec<String> {
94    SHARED_THEME_CACHE.read().keys().cloned().collect()
95}
96
97#[cfg(test)]
98mod tests {
99    use super::*;
100
101    #[test]
102    fn test_syntax_set_loaded() {
103        let ss = syntax_set();
104        assert!(!ss.syntaxes().is_empty(), "Syntax set should not be empty");
105    }
106
107    #[test]
108    fn test_find_syntax_by_token() {
109        let rust = find_syntax_by_token("rust");
110        assert!(rust.name.contains("Rust"), "Should find Rust syntax");
111    }
112
113    #[test]
114    fn test_find_syntax_plain_text() {
115        let plain = find_syntax_plain_text();
116        assert!(
117            plain.name.contains("Plain Text"),
118            "Should find Plain Text syntax"
119        );
120    }
121
122    #[test]
123    fn test_load_default_theme() {
124        let theme = load_theme("base16-ocean.dark", false);
125        assert!(theme.name.is_some());
126    }
127
128    #[test]
129    fn test_load_unknown_theme_falls_back() {
130        let theme = load_theme("nonexistent-theme-xyz", false);
131        assert!(theme.name.is_some());
132    }
133
134    #[test]
135    fn test_theme_caching() {
136        let theme1 = load_theme("base16-ocean.dark", true);
137        let theme2 = load_theme("base16-ocean.dark", true);
138        assert_eq!(theme1.name, theme2.name);
139    }
140}