syntax.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    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}