Skip to main content

boltz_syntax_theme/
syntax_theme.rs

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(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}