vtcode_tui/ui/
syntax_highlight.rs1use 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}