language: Improve highlight map resolution (#52183)

Finn Evers , Piotr Osiewicz , and Gaauwe Rombouts created

This PR refactors the highlight map capture name resolution to be faster
and more predictable. Speficically,
- it changes the capture name matching to explicit prefix matching
(e.g., `function.call.whatever.jsx` will now be matched by only
`function`, `function.call`, `function.call.whatever` and
`function.call.whatever.jsx`). This matches the behavior VSCode has
- resolving highlights is now much more efficient, as we now look up
captures in a BTreeMap as opposed to searching in a Vector for these.

This substantially improves the performance for resolving capture names
against themes. With the benchmark added here for creating the
HighlightMap, we see quite some improvements:
```
Running benches/highlight_map.rs (target/release/deps/highlight_map-f99da68650aac85b)
HighlightMap::new/small_captures/small_theme
                        time:   [161.90 ns 162.70 ns 163.55 ns]
                        change: [-39.027% -38.352% -37.742%] (p = 0.00 < 0.05)
                        Performance has improved.
Found 3 outliers among 100 measurements (3.00%)
  3 (3.00%) high mild
HighlightMap::new/small_captures/large_theme
                        time:   [231.37 ns 233.02 ns 234.70 ns]
                        change: [-91.570% -91.516% -91.464%] (p = 0.00 < 0.05)
                        Performance has improved.
HighlightMap::new/large_captures/small_theme
                        time:   [991.82 ns 994.94 ns 998.50 ns]
                        change: [-50.670% -50.443% -50.220%] (p = 0.00 < 0.05)
                        Performance has improved.
Found 5 outliers among 100 measurements (5.00%)
  5 (5.00%) high mild
HighlightMap::new/large_captures/large_theme
                        time:   [1.6528 ยตs 1.6650 ยตs 1.6784 ยตs]
                        change: [-91.684% -91.637% -91.593%] (p = 0.00 < 0.05)
                        Performance has improved.
Found 1 outliers among 100 measurements (1.00%)
  1 (1.00%) low mild
```

For large themes and many capture names, the revised approach is much
faster.

With that in place, we can also add better fallbacks whenever we change
tokens, since e.g. a change from `@variable` to `@preproc` would
previously cause tokens to not be highlighted at all, whereas now we can
add fallbacks for such cases more efficiently. I'll add this later on to
this PR.


## Self-Review Checklist

<!-- Check before requesting review: -->
- [X] I've reviewed my own diff for quality, security, and reliability
- [ ] Unsafe blocks (if any) have justifying comments
- [ ] The content is consistent with the [UI/UX
checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
- [X] Tests cover the new/changed behavior
- [X] Performance impact has been considered and is acceptable

Release Notes:

- Improved resolution speed of theme highlight capture names. This might
change highlighting in some rare edge cases, but should overall make
highlighting more predicatable. Theme captures will now follow a strict
prefix matching, so e.g. function.call.decorator.jsx` will now be
matched by only `function`, `function.call`, `function.call.decorator`
and `function.call.decorator.jsx` with the most specific capture always
taking precedence.

---------

Co-authored-by: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com>
Co-authored-by: Gaauwe Rombouts <mail@grombouts.nl>

Change summary

Cargo.lock                                              |   1 
crates/debugger_ui/src/session/running/variable_list.rs |   7 
crates/editor/src/editor.rs                             |   6 
crates/editor/src/semantic_tokens.rs                    |   5 
crates/language/Cargo.toml                              |   5 
crates/language/benches/highlight_map.rs                | 144 +++++++++++
crates/language/src/highlight_map.rs                    |  34 -
crates/language_tools/src/highlights_tree_view.rs       |   4 
crates/onboarding/src/theme_preview.rs                  |  14 
crates/theme/src/fallback_themes.rs                     | 126 ++++----
crates/theme/src/styles/syntax.rs                       | 125 ++++++---
crates/theme/src/theme.rs                               |  49 +--
12 files changed, 346 insertions(+), 174 deletions(-)

Detailed changes

Cargo.lock ๐Ÿ”—

@@ -9331,6 +9331,7 @@ dependencies = [
  "async-trait",
  "clock",
  "collections",
+ "criterion",
  "ctor",
  "diffy",
  "ec4rs",

crates/debugger_ui/src/session/running/variable_list.rs ๐Ÿ”—

@@ -1076,7 +1076,12 @@ impl VariableList {
         presentation_hint: Option<&VariablePresentationHint>,
         cx: &Context<Self>,
     ) -> VariableColor {
-        let syntax_color_for = |name| cx.theme().syntax().get(name).color;
+        let syntax_color_for = |name| {
+            cx.theme()
+                .syntax()
+                .style_for_name(name)
+                .and_then(|style| style.color)
+        };
         let name = if self.disabled {
             Some(Color::Disabled.color(cx))
         } else {

crates/editor/src/editor.rs ๐Ÿ”—

@@ -601,7 +601,11 @@ pub fn make_inlay_hints_style(cx: &App) -> HighlightStyle {
         .inlay_hints
         .show_background;
 
-    let mut style = cx.theme().syntax().get("hint");
+    let mut style = cx
+        .theme()
+        .syntax()
+        .style_for_name("hint")
+        .unwrap_or_default();
 
     if style.color.is_none() {
         style.color = Some(cx.theme().status().hint);

crates/editor/src/semantic_tokens.rs ๐Ÿ”—

@@ -377,7 +377,10 @@ fn convert_token(
     for rule in matching {
         empty = false;
 
-        let style = rule.style.iter().find_map(|style| theme.get_opt(style));
+        let style = rule
+            .style
+            .iter()
+            .find_map(|style| theme.style_for_name(style));
 
         macro_rules! overwrite {
             (

crates/language/Cargo.toml ๐Ÿ”—

@@ -101,6 +101,11 @@ toml.workspace = true
 unindent.workspace = true
 util = { workspace = true, features = ["test-support"] }
 zlog.workspace = true
+criterion.workspace = true
+
+[[bench]]
+name = "highlight_map"
+harness = false
 
 [package.metadata.cargo-machete]
 ignored = ["tracing"]

crates/language/benches/highlight_map.rs ๐Ÿ”—

@@ -0,0 +1,144 @@
+use criterion::{BenchmarkId, Criterion, black_box, criterion_group, criterion_main};
+use gpui::rgba;
+use language::HighlightMap;
+use theme::SyntaxTheme;
+
+fn syntax_theme(highlight_names: &[&str]) -> SyntaxTheme {
+    SyntaxTheme::new(highlight_names.iter().enumerate().map(|(i, name)| {
+        let r = ((i * 37) % 256) as u8;
+        let g = ((i * 53) % 256) as u8;
+        let b = ((i * 71) % 256) as u8;
+        let color = rgba(u32::from_be_bytes([r, g, b, 0xff]));
+        (name.to_string(), color.into())
+    }))
+}
+
+static SMALL_THEME_KEYS: &[&str] = &[
+    "comment", "function", "keyword", "string", "type", "variable",
+];
+
+static LARGE_THEME_KEYS: &[&str] = &[
+    "attribute",
+    "boolean",
+    "comment",
+    "comment.doc",
+    "constant",
+    "constant.builtin",
+    "constructor",
+    "embedded",
+    "emphasis",
+    "emphasis.strong",
+    "function",
+    "function.builtin",
+    "function.method",
+    "function.method.builtin",
+    "function.special.definition",
+    "keyword",
+    "keyword.control",
+    "keyword.control.conditional",
+    "keyword.control.import",
+    "keyword.control.repeat",
+    "keyword.control.return",
+    "keyword.modifier",
+    "keyword.operator",
+    "label",
+    "link_text",
+    "link_uri",
+    "number",
+    "operator",
+    "property",
+    "punctuation",
+    "punctuation.bracket",
+    "punctuation.delimiter",
+    "punctuation.list_marker",
+    "punctuation.special",
+    "string",
+    "string.escape",
+    "string.regex",
+    "string.special",
+    "string.special.symbol",
+    "tag",
+    "text.literal",
+    "title",
+    "type",
+    "type.builtin",
+    "type.super",
+    "variable",
+    "variable.builtin",
+    "variable.member",
+    "variable.parameter",
+    "variable.special",
+];
+
+static SMALL_CAPTURE_NAMES: &[&str] = &[
+    "function",
+    "keyword",
+    "string.escape",
+    "type.builtin",
+    "variable.builtin",
+];
+
+static LARGE_CAPTURE_NAMES: &[&str] = &[
+    "attribute",
+    "boolean",
+    "comment",
+    "comment.doc",
+    "constant",
+    "constant.builtin",
+    "constructor",
+    "function",
+    "function.builtin",
+    "function.method",
+    "keyword",
+    "keyword.control",
+    "keyword.control.conditional",
+    "keyword.control.import",
+    "keyword.modifier",
+    "keyword.operator",
+    "label",
+    "number",
+    "operator",
+    "property",
+    "punctuation.bracket",
+    "punctuation.delimiter",
+    "punctuation.special",
+    "string",
+    "string.escape",
+    "string.regex",
+    "string.special",
+    "tag",
+    "type",
+    "type.builtin",
+    "variable",
+    "variable.builtin",
+    "variable.member",
+    "variable.parameter",
+];
+
+fn bench_highlight_map_new(c: &mut Criterion) {
+    let mut group = c.benchmark_group("HighlightMap::new");
+
+    for (capture_label, capture_names) in [
+        ("small_captures", SMALL_CAPTURE_NAMES as &[&str]),
+        ("large_captures", LARGE_CAPTURE_NAMES as &[&str]),
+    ] {
+        for (theme_label, theme_keys) in [
+            ("small_theme", SMALL_THEME_KEYS as &[&str]),
+            ("large_theme", LARGE_THEME_KEYS as &[&str]),
+        ] {
+            let theme = syntax_theme(theme_keys);
+            group.bench_with_input(
+                BenchmarkId::new(capture_label, theme_label),
+                &(capture_names, &theme),
+                |b, (capture_names, theme)| {
+                    b.iter(|| HighlightMap::new(black_box(capture_names), black_box(theme)));
+                },
+            );
+        }
+    }
+
+    group.finish();
+}
+
+criterion_group!(benches, bench_highlight_map_new);
+criterion_main!(benches);

crates/language/src/highlight_map.rs ๐Ÿ”—

@@ -11,7 +11,7 @@ pub struct HighlightId(pub u32);
 const DEFAULT_SYNTAX_HIGHLIGHT_ID: HighlightId = HighlightId(u32::MAX);
 
 impl HighlightMap {
-    pub(crate) fn new(capture_names: &[&str], theme: &SyntaxTheme) -> Self {
+    pub fn new(capture_names: &[&str], theme: &SyntaxTheme) -> Self {
         // For each capture name in the highlight query, find the longest
         // key in the theme's syntax styles that matches all of the
         // dot-separated components of the capture name.
@@ -20,23 +20,8 @@ impl HighlightMap {
                 .iter()
                 .map(|capture_name| {
                     theme
-                        .highlights
-                        .iter()
-                        .enumerate()
-                        .filter_map(|(i, (key, _))| {
-                            let mut len = 0;
-                            let capture_parts = capture_name.split('.');
-                            for key_part in key.split('.') {
-                                if capture_parts.clone().any(|part| part == key_part) {
-                                    len += 1;
-                                } else {
-                                    return None;
-                                }
-                            }
-                            Some((i, len))
-                        })
-                        .max_by_key(|(_, len)| *len)
-                        .map_or(DEFAULT_SYNTAX_HIGHLIGHT_ID, |(i, _)| HighlightId(i as u32))
+                        .highlight_id(capture_name)
+                        .map_or(DEFAULT_SYNTAX_HIGHLIGHT_ID, HighlightId)
                 })
                 .collect(),
         )
@@ -59,11 +44,11 @@ impl HighlightId {
     }
 
     pub fn style(&self, theme: &SyntaxTheme) -> Option<HighlightStyle> {
-        theme.highlights.get(self.0 as usize).map(|entry| entry.1)
+        theme.get(self.0 as usize).cloned()
     }
 
     pub fn name<'a>(&self, theme: &'a SyntaxTheme) -> Option<&'a str> {
-        theme.highlights.get(self.0 as usize).map(|e| e.0.as_str())
+        theme.get_capture_name(self.0 as usize)
     }
 }
 
@@ -86,8 +71,8 @@ mod tests {
 
     #[test]
     fn test_highlight_map() {
-        let theme = SyntaxTheme {
-            highlights: [
+        let theme = SyntaxTheme::new(
+            [
                 ("function", rgba(0x100000ff)),
                 ("function.method", rgba(0x200000ff)),
                 ("function.async", rgba(0x300000ff)),
@@ -96,9 +81,8 @@ mod tests {
                 ("variable", rgba(0x600000ff)),
             ]
             .iter()
-            .map(|(name, color)| (name.to_string(), (*color).into()))
-            .collect(),
-        };
+            .map(|(name, color)| (name.to_string(), (*color).into())),
+        );
 
         let capture_names = &[
             "function.special",

crates/language_tools/src/highlights_tree_view.rs ๐Ÿ”—

@@ -375,7 +375,9 @@ impl HighlightsTreeView {
                                             rule.style
                                                 .iter()
                                                 .find(|style_name| {
-                                                    semantic_theme.get_opt(style_name).is_some()
+                                                    semantic_theme
+                                                        .style_for_name(style_name)
+                                                        .is_some()
                                                 })
                                                 .map(|style_name| {
                                                     SharedString::from(style_name.clone())

crates/onboarding/src/theme_preview.rs ๐Ÿ”—

@@ -87,13 +87,13 @@ impl ThemePreviewTile {
         let colors = theme.colors();
         let syntax = theme.syntax();
 
-        let keyword_color = syntax.get("keyword").color;
-        let function_color = syntax.get("function").color;
-        let string_color = syntax.get("string").color;
-        let comment_color = syntax.get("comment").color;
-        let variable_color = syntax.get("variable").color;
-        let type_color = syntax.get("type").color;
-        let punctuation_color = syntax.get("punctuation").color;
+        let keyword_color = syntax.style_for_name("keyword").and_then(|s| s.color);
+        let function_color = syntax.style_for_name("function").and_then(|s| s.color);
+        let string_color = syntax.style_for_name("string").and_then(|s| s.color);
+        let comment_color = syntax.style_for_name("comment").and_then(|s| s.color);
+        let variable_color = syntax.style_for_name("variable").and_then(|s| s.color);
+        let type_color = syntax.style_for_name("type").and_then(|s| s.color);
+        let punctuation_color = syntax.style_for_name("punctuation").and_then(|s| s.color);
 
         let syntax_colors = [
             keyword_color,

crates/theme/src/fallback_themes.rs ๐Ÿ”—

@@ -314,70 +314,68 @@ pub(crate) fn zed_default_dark() -> Theme {
                 warning_border: yellow,
             },
             player,
-            syntax: Arc::new(SyntaxTheme {
-                highlights: vec![
-                    ("attribute".into(), purple.into()),
-                    ("boolean".into(), orange.into()),
-                    ("comment".into(), gray.into()),
-                    ("comment.doc".into(), gray.into()),
-                    ("constant".into(), yellow.into()),
-                    ("constructor".into(), blue.into()),
-                    ("embedded".into(), HighlightStyle::default()),
-                    (
-                        "emphasis".into(),
-                        HighlightStyle {
-                            font_style: Some(FontStyle::Italic),
-                            ..HighlightStyle::default()
-                        },
-                    ),
-                    (
-                        "emphasis.strong".into(),
-                        HighlightStyle {
-                            font_weight: Some(FontWeight::BOLD),
-                            ..HighlightStyle::default()
-                        },
-                    ),
-                    ("enum".into(), teal.into()),
-                    ("function".into(), blue.into()),
-                    ("function.method".into(), blue.into()),
-                    ("function.definition".into(), blue.into()),
-                    ("hint".into(), blue.into()),
-                    ("keyword".into(), purple.into()),
-                    ("label".into(), HighlightStyle::default()),
-                    ("link_text".into(), blue.into()),
-                    (
-                        "link_uri".into(),
-                        HighlightStyle {
-                            color: Some(teal),
-                            font_style: Some(FontStyle::Italic),
-                            ..HighlightStyle::default()
-                        },
-                    ),
-                    ("number".into(), orange.into()),
-                    ("operator".into(), HighlightStyle::default()),
-                    ("predictive".into(), HighlightStyle::default()),
-                    ("preproc".into(), HighlightStyle::default()),
-                    ("primary".into(), HighlightStyle::default()),
-                    ("property".into(), red.into()),
-                    ("punctuation".into(), HighlightStyle::default()),
-                    ("punctuation.bracket".into(), HighlightStyle::default()),
-                    ("punctuation.delimiter".into(), HighlightStyle::default()),
-                    ("punctuation.list_marker".into(), HighlightStyle::default()),
-                    ("punctuation.special".into(), HighlightStyle::default()),
-                    ("string".into(), green.into()),
-                    ("string.escape".into(), HighlightStyle::default()),
-                    ("string.regex".into(), red.into()),
-                    ("string.special".into(), HighlightStyle::default()),
-                    ("string.special.symbol".into(), HighlightStyle::default()),
-                    ("tag".into(), HighlightStyle::default()),
-                    ("text.literal".into(), HighlightStyle::default()),
-                    ("title".into(), HighlightStyle::default()),
-                    ("type".into(), teal.into()),
-                    ("variable".into(), HighlightStyle::default()),
-                    ("variable.special".into(), red.into()),
-                    ("variant".into(), HighlightStyle::default()),
-                ],
-            }),
+            syntax: Arc::new(SyntaxTheme::new(vec![
+                ("attribute".into(), purple.into()),
+                ("boolean".into(), orange.into()),
+                ("comment".into(), gray.into()),
+                ("comment.doc".into(), gray.into()),
+                ("constant".into(), yellow.into()),
+                ("constructor".into(), blue.into()),
+                ("embedded".into(), HighlightStyle::default()),
+                (
+                    "emphasis".into(),
+                    HighlightStyle {
+                        font_style: Some(FontStyle::Italic),
+                        ..HighlightStyle::default()
+                    },
+                ),
+                (
+                    "emphasis.strong".into(),
+                    HighlightStyle {
+                        font_weight: Some(FontWeight::BOLD),
+                        ..HighlightStyle::default()
+                    },
+                ),
+                ("enum".into(), teal.into()),
+                ("function".into(), blue.into()),
+                ("function.method".into(), blue.into()),
+                ("function.definition".into(), blue.into()),
+                ("hint".into(), blue.into()),
+                ("keyword".into(), purple.into()),
+                ("label".into(), HighlightStyle::default()),
+                ("link_text".into(), blue.into()),
+                (
+                    "link_uri".into(),
+                    HighlightStyle {
+                        color: Some(teal),
+                        font_style: Some(FontStyle::Italic),
+                        ..HighlightStyle::default()
+                    },
+                ),
+                ("number".into(), orange.into()),
+                ("operator".into(), HighlightStyle::default()),
+                ("predictive".into(), HighlightStyle::default()),
+                ("preproc".into(), HighlightStyle::default()),
+                ("primary".into(), HighlightStyle::default()),
+                ("property".into(), red.into()),
+                ("punctuation".into(), HighlightStyle::default()),
+                ("punctuation.bracket".into(), HighlightStyle::default()),
+                ("punctuation.delimiter".into(), HighlightStyle::default()),
+                ("punctuation.list_marker".into(), HighlightStyle::default()),
+                ("punctuation.special".into(), HighlightStyle::default()),
+                ("string".into(), green.into()),
+                ("string.escape".into(), HighlightStyle::default()),
+                ("string.regex".into(), red.into()),
+                ("string.special".into(), HighlightStyle::default()),
+                ("string.special.symbol".into(), HighlightStyle::default()),
+                ("tag".into(), HighlightStyle::default()),
+                ("text.literal".into(), HighlightStyle::default()),
+                ("title".into(), HighlightStyle::default()),
+                ("type".into(), teal.into()),
+                ("variable".into(), HighlightStyle::default()),
+                ("variable.special".into(), red.into()),
+                ("variant".into(), HighlightStyle::default()),
+            ])),
         },
     }
 }

crates/theme/src/styles/syntax.rs ๐Ÿ”—

@@ -1,15 +1,38 @@
 #![allow(missing_docs)]
 
-use std::sync::Arc;
+use std::{
+    collections::{BTreeMap, btree_map::Entry},
+    sync::Arc,
+};
 
-use gpui::{HighlightStyle, Hsla};
+use gpui::HighlightStyle;
+#[cfg(any(test, feature = "test-support"))]
+use gpui::Hsla;
 
 #[derive(Debug, PartialEq, Eq, Clone, Default)]
 pub struct SyntaxTheme {
-    pub highlights: Vec<(String, HighlightStyle)>,
+    pub(self) highlights: Vec<HighlightStyle>,
+    pub(self) capture_name_map: BTreeMap<String, usize>,
 }
 
 impl SyntaxTheme {
+    pub fn new(highlights: impl IntoIterator<Item = (String, HighlightStyle)>) -> Self {
+        let (capture_names, highlights) = highlights.into_iter().unzip();
+
+        Self {
+            capture_name_map: Self::create_capture_name_map(capture_names),
+            highlights,
+        }
+    }
+
+    fn create_capture_name_map(highlights: Vec<String>) -> BTreeMap<String, usize> {
+        highlights
+            .into_iter()
+            .enumerate()
+            .map(|(i, key)| (key, i))
+            .collect()
+    }
+
     #[cfg(any(test, feature = "test-support"))]
     pub fn new_test(colors: impl IntoIterator<Item = (&'static str, Hsla)>) -> Self {
         Self::new_test_styles(colors.into_iter().map(|(key, color)| {
@@ -27,34 +50,45 @@ impl SyntaxTheme {
     pub fn new_test_styles(
         colors: impl IntoIterator<Item = (&'static str, HighlightStyle)>,
     ) -> Self {
-        Self {
-            highlights: colors
+        Self::new(
+            colors
                 .into_iter()
-                .map(|(key, style)| (key.to_owned(), style))
-                .collect(),
-        }
+                .map(|(key, style)| (key.to_owned(), style)),
+        )
     }
 
-    pub fn get(&self, name: &str) -> HighlightStyle {
-        self.highlights
-            .iter()
-            .find_map(|entry| if entry.0 == name { Some(entry.1) } else { None })
-            .unwrap_or_default()
+    pub fn get(&self, highlight_index: usize) -> Option<&HighlightStyle> {
+        self.highlights.get(highlight_index)
     }
 
-    pub fn get_opt(&self, name: &str) -> Option<HighlightStyle> {
-        self.highlights
-            .iter()
-            .find_map(|entry| if entry.0 == name { Some(entry.1) } else { None })
+    pub fn style_for_name(&self, name: &str) -> Option<HighlightStyle> {
+        self.capture_name_map
+            .get(name)
+            .map(|highlight_idx| self.highlights[*highlight_idx])
     }
 
-    pub fn color(&self, name: &str) -> Hsla {
-        self.get(name).color.unwrap_or_default()
+    pub fn get_capture_name(&self, idx: usize) -> Option<&str> {
+        self.capture_name_map
+            .iter()
+            .find(|(_, value)| **value == idx)
+            .map(|(key, _)| key.as_ref())
     }
 
-    pub fn highlight_id(&self, name: &str) -> Option<u32> {
-        let ix = self.highlights.iter().position(|entry| entry.0 == name)?;
-        Some(ix as u32)
+    pub fn highlight_id(&self, capture_name: &str) -> Option<u32> {
+        self.capture_name_map
+            .range::<str, _>((
+                capture_name.split(".").next().map_or(
+                    std::ops::Bound::Included(capture_name),
+                    std::ops::Bound::Included,
+                ),
+                std::ops::Bound::Included(capture_name),
+            ))
+            .rfind(|(prefix, _)| {
+                capture_name
+                    .strip_prefix(*prefix)
+                    .is_some_and(|remainder| remainder.is_empty() || remainder.starts_with('.'))
+            })
+            .map(|(_, index)| *index as u32)
     }
 
     /// Returns a new [`Arc<SyntaxTheme>`] with the given syntax styles merged in.
@@ -63,33 +97,36 @@ impl SyntaxTheme {
             return base;
         }
 
-        let mut merged_highlights = base.highlights.clone();
+        let mut base = Arc::try_unwrap(base).unwrap_or_else(|base| (*base).clone());
 
         for (name, highlight) in user_syntax_styles {
-            if let Some((_, existing_highlight)) = merged_highlights
-                .iter_mut()
-                .find(|(existing_name, _)| existing_name == &name)
-            {
-                existing_highlight.color = highlight.color.or(existing_highlight.color);
-                existing_highlight.font_weight =
-                    highlight.font_weight.or(existing_highlight.font_weight);
-                existing_highlight.font_style =
-                    highlight.font_style.or(existing_highlight.font_style);
-                existing_highlight.background_color = highlight
-                    .background_color
-                    .or(existing_highlight.background_color);
-                existing_highlight.underline = highlight.underline.or(existing_highlight.underline);
-                existing_highlight.strikethrough =
-                    highlight.strikethrough.or(existing_highlight.strikethrough);
-                existing_highlight.fade_out = highlight.fade_out.or(existing_highlight.fade_out);
-            } else {
-                merged_highlights.push((name, highlight));
+            match base.capture_name_map.entry(name) {
+                Entry::Occupied(entry) => {
+                    if let Some(existing_highlight) = base.highlights.get_mut(*entry.get()) {
+                        existing_highlight.color = highlight.color.or(existing_highlight.color);
+                        existing_highlight.font_weight =
+                            highlight.font_weight.or(existing_highlight.font_weight);
+                        existing_highlight.font_style =
+                            highlight.font_style.or(existing_highlight.font_style);
+                        existing_highlight.background_color = highlight
+                            .background_color
+                            .or(existing_highlight.background_color);
+                        existing_highlight.underline =
+                            highlight.underline.or(existing_highlight.underline);
+                        existing_highlight.strikethrough =
+                            highlight.strikethrough.or(existing_highlight.strikethrough);
+                        existing_highlight.fade_out =
+                            highlight.fade_out.or(existing_highlight.fade_out);
+                    }
+                }
+                Entry::Vacant(vacant) => {
+                    vacant.insert(base.highlights.len());
+                    base.highlights.push(highlight);
+                }
             }
         }
 
-        Arc::new(Self {
-            highlights: merged_highlights,
-        })
+        Arc::new(base)
     }
 }
 

crates/theme/src/theme.rs ๐Ÿ”—

@@ -258,30 +258,25 @@ impl ThemeFamily {
         };
         refined_accent_colors.merge(&theme.style.accents);
 
-        let syntax_highlights = theme
-            .style
-            .syntax
-            .iter()
-            .map(|(syntax_token, highlight)| {
-                (
-                    syntax_token.clone(),
-                    HighlightStyle {
-                        color: highlight
-                            .color
-                            .as_ref()
-                            .and_then(|color| try_parse_color(color).ok()),
-                        background_color: highlight
-                            .background_color
-                            .as_ref()
-                            .and_then(|color| try_parse_color(color).ok()),
-                        font_style: highlight.font_style.map(|s| s.into_gpui()),
-                        font_weight: highlight.font_weight.map(|w| w.into_gpui()),
-                        ..Default::default()
-                    },
-                )
-            })
-            .collect::<Vec<_>>();
-        let syntax_theme = SyntaxTheme::merge(Arc::new(SyntaxTheme::default()), syntax_highlights);
+        let syntax_highlights = theme.style.syntax.iter().map(|(syntax_token, highlight)| {
+            (
+                syntax_token.clone(),
+                HighlightStyle {
+                    color: highlight
+                        .color
+                        .as_ref()
+                        .and_then(|color| try_parse_color(color).ok()),
+                    background_color: highlight
+                        .background_color
+                        .as_ref()
+                        .and_then(|color| try_parse_color(color).ok()),
+                    font_style: highlight.font_style.map(|s| s.into_gpui()),
+                    font_weight: highlight.font_weight.map(|w| w.into_gpui()),
+                    ..Default::default()
+                },
+            )
+        });
+        let syntax_theme = Arc::new(SyntaxTheme::new(syntax_highlights));
 
         let window_background_appearance = theme
             .style
@@ -381,12 +376,6 @@ impl Theme {
         &self.styles.status
     }
 
-    /// Returns the color for the syntax node with the given name.
-    #[inline(always)]
-    pub fn syntax_color(&self, name: &str) -> Hsla {
-        self.syntax().color(name)
-    }
-
     /// Returns the [`Appearance`] for the theme.
     #[inline(always)]
     pub fn appearance(&self) -> Appearance {