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