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: usize) -> Option<&HighlightStyle> {
61 self.highlights.get(highlight_index)
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: usize) -> Option<&str> {
71 self.capture_name_map
72 .iter()
73 .find(|(_, value)| **value == idx)
74 .map(|(key, _)| key.as_ref())
75 }
76
77 pub fn highlight_id(&self, capture_name: &str) -> Option<u32> {
78 self.capture_name_map
79 .range::<str, _>((
80 capture_name.split(".").next().map_or(
81 std::ops::Bound::Included(capture_name),
82 std::ops::Bound::Included,
83 ),
84 std::ops::Bound::Included(capture_name),
85 ))
86 .rfind(|(prefix, _)| {
87 capture_name
88 .strip_prefix(*prefix)
89 .is_some_and(|remainder| remainder.is_empty() || remainder.starts_with('.'))
90 })
91 .map(|(_, index)| *index as u32)
92 }
93
94 /// Returns a new [`Arc<SyntaxTheme>`] with the given syntax styles merged in.
95 pub fn merge(base: Arc<Self>, user_syntax_styles: Vec<(String, HighlightStyle)>) -> Arc<Self> {
96 if user_syntax_styles.is_empty() {
97 return base;
98 }
99
100 let mut base = Arc::try_unwrap(base).unwrap_or_else(|base| (*base).clone());
101
102 for (name, highlight) in user_syntax_styles {
103 match base.capture_name_map.entry(name) {
104 Entry::Occupied(entry) => {
105 if let Some(existing_highlight) = base.highlights.get_mut(*entry.get()) {
106 existing_highlight.color = highlight.color.or(existing_highlight.color);
107 existing_highlight.font_weight =
108 highlight.font_weight.or(existing_highlight.font_weight);
109 existing_highlight.font_style =
110 highlight.font_style.or(existing_highlight.font_style);
111 existing_highlight.background_color = highlight
112 .background_color
113 .or(existing_highlight.background_color);
114 existing_highlight.underline =
115 highlight.underline.or(existing_highlight.underline);
116 existing_highlight.strikethrough =
117 highlight.strikethrough.or(existing_highlight.strikethrough);
118 existing_highlight.fade_out =
119 highlight.fade_out.or(existing_highlight.fade_out);
120 }
121 }
122 Entry::Vacant(vacant) => {
123 vacant.insert(base.highlights.len());
124 base.highlights.push(highlight);
125 }
126 }
127 }
128
129 Arc::new(base)
130 }
131}
132
133#[cfg(test)]
134mod tests {
135 use gpui::FontStyle;
136
137 use super::*;
138
139 #[test]
140 fn test_syntax_theme_merge() {
141 // Merging into an empty `SyntaxTheme` keeps all the user-defined styles.
142 let syntax_theme = SyntaxTheme::merge(
143 Arc::new(SyntaxTheme::new_test([])),
144 vec![
145 (
146 "foo".to_string(),
147 HighlightStyle {
148 color: Some(gpui::red()),
149 ..Default::default()
150 },
151 ),
152 (
153 "foo.bar".to_string(),
154 HighlightStyle {
155 color: Some(gpui::green()),
156 ..Default::default()
157 },
158 ),
159 ],
160 );
161 assert_eq!(
162 syntax_theme,
163 Arc::new(SyntaxTheme::new_test([
164 ("foo", gpui::red()),
165 ("foo.bar", gpui::green())
166 ]))
167 );
168
169 // Merging empty user-defined styles keeps all the base styles.
170 let syntax_theme = SyntaxTheme::merge(
171 Arc::new(SyntaxTheme::new_test([
172 ("foo", gpui::blue()),
173 ("foo.bar", gpui::red()),
174 ])),
175 Vec::new(),
176 );
177 assert_eq!(
178 syntax_theme,
179 Arc::new(SyntaxTheme::new_test([
180 ("foo", gpui::blue()),
181 ("foo.bar", gpui::red())
182 ]))
183 );
184
185 let syntax_theme = SyntaxTheme::merge(
186 Arc::new(SyntaxTheme::new_test([
187 ("foo", gpui::red()),
188 ("foo.bar", gpui::green()),
189 ])),
190 vec![(
191 "foo.bar".to_string(),
192 HighlightStyle {
193 color: Some(gpui::yellow()),
194 ..Default::default()
195 },
196 )],
197 );
198 assert_eq!(
199 syntax_theme,
200 Arc::new(SyntaxTheme::new_test([
201 ("foo", gpui::red()),
202 ("foo.bar", gpui::yellow())
203 ]))
204 );
205
206 let syntax_theme = SyntaxTheme::merge(
207 Arc::new(SyntaxTheme::new_test([
208 ("foo", gpui::red()),
209 ("foo.bar", gpui::green()),
210 ])),
211 vec![(
212 "foo.bar".to_string(),
213 HighlightStyle {
214 font_style: Some(FontStyle::Italic),
215 ..Default::default()
216 },
217 )],
218 );
219 assert_eq!(
220 syntax_theme,
221 Arc::new(SyntaxTheme::new_test_styles([
222 (
223 "foo",
224 HighlightStyle {
225 color: Some(gpui::red()),
226 ..Default::default()
227 }
228 ),
229 (
230 "foo.bar",
231 HighlightStyle {
232 color: Some(gpui::green()),
233 font_style: Some(FontStyle::Italic),
234 ..Default::default()
235 }
236 )
237 ]))
238 );
239 }
240}