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 pub(self) highlights: Vec<HighlightStyle>,
15 pub(self) 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(test)]
135mod tests {
136 use gpui::FontStyle;
137
138 use super::*;
139
140 #[test]
141 fn test_syntax_theme_merge() {
142 // Merging into an empty `SyntaxTheme` keeps all the user-defined styles.
143 let syntax_theme = SyntaxTheme::merge(
144 Arc::new(SyntaxTheme::new_test([])),
145 vec![
146 (
147 "foo".to_string(),
148 HighlightStyle {
149 color: Some(gpui::red()),
150 ..Default::default()
151 },
152 ),
153 (
154 "foo.bar".to_string(),
155 HighlightStyle {
156 color: Some(gpui::green()),
157 ..Default::default()
158 },
159 ),
160 ],
161 );
162 assert_eq!(
163 syntax_theme,
164 Arc::new(SyntaxTheme::new_test([
165 ("foo", gpui::red()),
166 ("foo.bar", gpui::green())
167 ]))
168 );
169
170 // Merging empty user-defined styles keeps all the base styles.
171 let syntax_theme = SyntaxTheme::merge(
172 Arc::new(SyntaxTheme::new_test([
173 ("foo", gpui::blue()),
174 ("foo.bar", gpui::red()),
175 ])),
176 Vec::new(),
177 );
178 assert_eq!(
179 syntax_theme,
180 Arc::new(SyntaxTheme::new_test([
181 ("foo", gpui::blue()),
182 ("foo.bar", gpui::red())
183 ]))
184 );
185
186 let syntax_theme = SyntaxTheme::merge(
187 Arc::new(SyntaxTheme::new_test([
188 ("foo", gpui::red()),
189 ("foo.bar", gpui::green()),
190 ])),
191 vec![(
192 "foo.bar".to_string(),
193 HighlightStyle {
194 color: Some(gpui::yellow()),
195 ..Default::default()
196 },
197 )],
198 );
199 assert_eq!(
200 syntax_theme,
201 Arc::new(SyntaxTheme::new_test([
202 ("foo", gpui::red()),
203 ("foo.bar", gpui::yellow())
204 ]))
205 );
206
207 let syntax_theme = SyntaxTheme::merge(
208 Arc::new(SyntaxTheme::new_test([
209 ("foo", gpui::red()),
210 ("foo.bar", gpui::green()),
211 ])),
212 vec![(
213 "foo.bar".to_string(),
214 HighlightStyle {
215 font_style: Some(FontStyle::Italic),
216 ..Default::default()
217 },
218 )],
219 );
220 assert_eq!(
221 syntax_theme,
222 Arc::new(SyntaxTheme::new_test_styles([
223 (
224 "foo",
225 HighlightStyle {
226 color: Some(gpui::red()),
227 ..Default::default()
228 }
229 ),
230 (
231 "foo.bar",
232 HighlightStyle {
233 color: Some(gpui::green()),
234 font_style: Some(FontStyle::Italic),
235 ..Default::default()
236 }
237 )
238 ]))
239 );
240 }
241}