1#![allow(missing_docs)]
2
3use std::{
4 collections::{BTreeMap, btree_map::Entry},
5 sync::Arc,
6};
7
8use gpui::HighlightStyle;
9#[cfg(any(test, feature = "test-support"))]
10use gpui::Hsla;
11
12#[derive(Debug, PartialEq, Eq, Clone, Default)]
13pub struct SyntaxTheme {
14 highlights: Vec<HighlightStyle>,
15 capture_name_map: BTreeMap<String, usize>,
16}
17
18impl SyntaxTheme {
19 pub fn new(highlights: impl IntoIterator<Item = (String, HighlightStyle)>) -> Self {
20 let (capture_names, highlights) = highlights.into_iter().unzip();
21
22 Self {
23 capture_name_map: Self::create_capture_name_map(capture_names),
24 highlights,
25 }
26 }
27
28 fn create_capture_name_map(highlights: Vec<String>) -> BTreeMap<String, usize> {
29 highlights
30 .into_iter()
31 .enumerate()
32 .map(|(i, key)| (key, i))
33 .collect()
34 }
35
36 #[cfg(any(test, feature = "test-support"))]
37 pub fn new_test(colors: impl IntoIterator<Item = (&'static str, Hsla)>) -> Self {
38 Self::new_test_styles(colors.into_iter().map(|(key, color)| {
39 (
40 key,
41 HighlightStyle {
42 color: Some(color),
43 ..Default::default()
44 },
45 )
46 }))
47 }
48
49 #[cfg(any(test, feature = "test-support"))]
50 pub fn new_test_styles(
51 colors: impl IntoIterator<Item = (&'static str, HighlightStyle)>,
52 ) -> Self {
53 Self::new(
54 colors
55 .into_iter()
56 .map(|(key, style)| (key.to_owned(), style)),
57 )
58 }
59
60 pub fn get(&self, highlight_index: impl Into<usize>) -> Option<&HighlightStyle> {
61 self.highlights.get(highlight_index.into())
62 }
63
64 pub fn style_for_name(&self, name: &str) -> Option<HighlightStyle> {
65 self.capture_name_map
66 .get(name)
67 .map(|highlight_idx| self.highlights[*highlight_idx])
68 }
69
70 pub fn get_capture_name(&self, idx: impl Into<usize>) -> Option<&str> {
71 let idx = idx.into();
72 self.capture_name_map
73 .iter()
74 .find(|(_, value)| **value == idx)
75 .map(|(key, _)| key.as_ref())
76 }
77
78 pub fn highlight_id(&self, capture_name: &str) -> Option<u32> {
79 self.capture_name_map
80 .range::<str, _>((
81 capture_name.split(".").next().map_or(
82 std::ops::Bound::Included(capture_name),
83 std::ops::Bound::Included,
84 ),
85 std::ops::Bound::Included(capture_name),
86 ))
87 .rfind(|(prefix, _)| {
88 capture_name
89 .strip_prefix(*prefix)
90 .is_some_and(|remainder| remainder.is_empty() || remainder.starts_with('.'))
91 })
92 .map(|(_, index)| *index as u32)
93 }
94
95 /// Returns a new [`Arc<SyntaxTheme>`] with the given syntax styles merged in.
96 pub fn merge(base: Arc<Self>, user_syntax_styles: Vec<(String, HighlightStyle)>) -> Arc<Self> {
97 if user_syntax_styles.is_empty() {
98 return base;
99 }
100
101 let mut base = Arc::try_unwrap(base).unwrap_or_else(|base| (*base).clone());
102
103 for (name, highlight) in user_syntax_styles {
104 match base.capture_name_map.entry(name) {
105 Entry::Occupied(entry) => {
106 if let Some(existing_highlight) = base.highlights.get_mut(*entry.get()) {
107 existing_highlight.color = highlight.color.or(existing_highlight.color);
108 existing_highlight.font_weight =
109 highlight.font_weight.or(existing_highlight.font_weight);
110 existing_highlight.font_style =
111 highlight.font_style.or(existing_highlight.font_style);
112 existing_highlight.background_color = highlight
113 .background_color
114 .or(existing_highlight.background_color);
115 existing_highlight.underline =
116 highlight.underline.or(existing_highlight.underline);
117 existing_highlight.strikethrough =
118 highlight.strikethrough.or(existing_highlight.strikethrough);
119 existing_highlight.fade_out =
120 highlight.fade_out.or(existing_highlight.fade_out);
121 }
122 }
123 Entry::Vacant(vacant) => {
124 vacant.insert(base.highlights.len());
125 base.highlights.push(highlight);
126 }
127 }
128 }
129
130 Arc::new(base)
131 }
132}
133
134#[cfg(feature = "bundled-themes")]
135mod bundled_themes {
136 use std::collections::BTreeMap;
137 use std::sync::Arc;
138
139 use gpui::{FontStyle, FontWeight, HighlightStyle, Hsla, Rgba, rgb};
140 use serde::Deserialize;
141
142 use super::SyntaxTheme;
143
144 #[derive(Deserialize)]
145 struct ThemeFile {
146 themes: Vec<ThemeEntry>,
147 }
148
149 #[derive(Deserialize)]
150 struct ThemeEntry {
151 name: String,
152 style: ThemeStyle,
153 }
154
155 #[derive(Deserialize)]
156 struct ThemeStyle {
157 syntax: BTreeMap<String, SyntaxStyleEntry>,
158 }
159
160 #[derive(Deserialize)]
161 struct SyntaxStyleEntry {
162 color: Option<String>,
163 font_weight: Option<f32>,
164 font_style: Option<String>,
165 }
166
167 impl SyntaxStyleEntry {
168 fn to_highlight_style(&self) -> HighlightStyle {
169 HighlightStyle {
170 color: self.color.as_deref().map(hex_to_hsla),
171 font_weight: self.font_weight.map(FontWeight),
172 font_style: self.font_style.as_deref().and_then(|s| match s {
173 "italic" => Some(FontStyle::Italic),
174 "normal" => Some(FontStyle::Normal),
175 "oblique" => Some(FontStyle::Oblique),
176 _ => None,
177 }),
178 ..Default::default()
179 }
180 }
181 }
182
183 fn hex_to_hsla(hex: &str) -> Hsla {
184 let hex = hex.trim_start_matches('#');
185 let rgba: Rgba = match hex.len() {
186 6 => rgb(u32::from_str_radix(hex, 16).unwrap_or(0)),
187 8 => {
188 let value = u32::from_str_radix(hex, 16).unwrap_or(0);
189 Rgba {
190 r: ((value >> 24) & 0xff) as f32 / 255.0,
191 g: ((value >> 16) & 0xff) as f32 / 255.0,
192 b: ((value >> 8) & 0xff) as f32 / 255.0,
193 a: (value & 0xff) as f32 / 255.0,
194 }
195 }
196 _ => rgb(0),
197 };
198 rgba.into()
199 }
200
201 fn load_theme(json: &str, theme_name: &str) -> Arc<SyntaxTheme> {
202 let theme_file: ThemeFile = serde_json::from_str(json).expect("failed to parse theme JSON");
203 let theme_entry = theme_file
204 .themes
205 .iter()
206 .find(|entry| entry.name == theme_name)
207 .unwrap_or_else(|| panic!("theme {theme_name:?} not found in theme JSON"));
208
209 let highlights = theme_entry
210 .style
211 .syntax
212 .iter()
213 .map(|(name, entry)| (name.clone(), entry.to_highlight_style()));
214
215 Arc::new(SyntaxTheme::new(highlights))
216 }
217
218 impl SyntaxTheme {
219 /// Load the "One Dark" syntax theme from the bundled theme JSON.
220 pub fn one_dark() -> Arc<Self> {
221 load_theme(
222 include_str!("../../../assets/themes/one/one.json"),
223 "One Dark",
224 )
225 }
226 }
227}
228
229#[cfg(test)]
230mod tests {
231 use gpui::FontStyle;
232
233 use super::*;
234
235 #[test]
236 fn test_syntax_theme_merge() {
237 // Merging into an empty `SyntaxTheme` keeps all the user-defined styles.
238 let syntax_theme = SyntaxTheme::merge(
239 Arc::new(SyntaxTheme::new_test([])),
240 vec![
241 (
242 "foo".to_string(),
243 HighlightStyle {
244 color: Some(gpui::red()),
245 ..Default::default()
246 },
247 ),
248 (
249 "foo.bar".to_string(),
250 HighlightStyle {
251 color: Some(gpui::green()),
252 ..Default::default()
253 },
254 ),
255 ],
256 );
257 assert_eq!(
258 syntax_theme,
259 Arc::new(SyntaxTheme::new_test([
260 ("foo", gpui::red()),
261 ("foo.bar", gpui::green())
262 ]))
263 );
264
265 // Merging empty user-defined styles keeps all the base styles.
266 let syntax_theme = SyntaxTheme::merge(
267 Arc::new(SyntaxTheme::new_test([
268 ("foo", gpui::blue()),
269 ("foo.bar", gpui::red()),
270 ])),
271 Vec::new(),
272 );
273 assert_eq!(
274 syntax_theme,
275 Arc::new(SyntaxTheme::new_test([
276 ("foo", gpui::blue()),
277 ("foo.bar", gpui::red())
278 ]))
279 );
280
281 let syntax_theme = SyntaxTheme::merge(
282 Arc::new(SyntaxTheme::new_test([
283 ("foo", gpui::red()),
284 ("foo.bar", gpui::green()),
285 ])),
286 vec![(
287 "foo.bar".to_string(),
288 HighlightStyle {
289 color: Some(gpui::yellow()),
290 ..Default::default()
291 },
292 )],
293 );
294 assert_eq!(
295 syntax_theme,
296 Arc::new(SyntaxTheme::new_test([
297 ("foo", gpui::red()),
298 ("foo.bar", gpui::yellow())
299 ]))
300 );
301
302 let syntax_theme = SyntaxTheme::merge(
303 Arc::new(SyntaxTheme::new_test([
304 ("foo", gpui::red()),
305 ("foo.bar", gpui::green()),
306 ])),
307 vec![(
308 "foo.bar".to_string(),
309 HighlightStyle {
310 font_style: Some(FontStyle::Italic),
311 ..Default::default()
312 },
313 )],
314 );
315 assert_eq!(
316 syntax_theme,
317 Arc::new(SyntaxTheme::new_test_styles([
318 (
319 "foo",
320 HighlightStyle {
321 color: Some(gpui::red()),
322 ..Default::default()
323 }
324 ),
325 (
326 "foo.bar",
327 HighlightStyle {
328 color: Some(gpui::green()),
329 font_style: Some(FontStyle::Italic),
330 ..Default::default()
331 }
332 )
333 ]))
334 );
335 }
336}