Add support for theme family-specific syntax mapping overrides (#3551)

Marshall Bowers created

This PR adds support for adding a specific set of mappings from Zed
syntax tokens to VS Code scopes for a particular theme family.

We can use this as a fallback when we aren't otherwise able to rely on
the mappings in the theme importer, as sometimes it isn't possible to
make a specific enough matcher that works across all of the themes.

Release Notes:

- N/A

Change summary

assets/themes/src/vscode/rose-pine/family.json |  5 +++
crates/theme2/src/themes/rose_pine.rs          |  6 ++--
crates/theme_importer/Cargo.toml               |  2 
crates/theme_importer/src/main.rs              | 15 ++++++++++++
crates/theme_importer/src/vscode/converter.rs  | 23 ++++++++++++++++---
crates/theme_importer/src/vscode/syntax.rs     |  2 
6 files changed, 42 insertions(+), 11 deletions(-)

Detailed changes

crates/theme2/src/themes/rose_pine.rs 🔗

@@ -106,7 +106,7 @@ pub fn rose_pine() -> UserThemeFamily {
                             (
                                 "function".into(),
                                 UserHighlightStyle {
-                                    color: Some(rgba(0xe0def4ff).into()),
+                                    color: Some(rgba(0xebbcbaff).into()),
                                     ..Default::default()
                                 },
                             ),
@@ -283,7 +283,7 @@ pub fn rose_pine() -> UserThemeFamily {
                             (
                                 "function".into(),
                                 UserHighlightStyle {
-                                    color: Some(rgba(0xe0def4ff).into()),
+                                    color: Some(rgba(0xea9a97ff).into()),
                                     ..Default::default()
                                 },
                             ),
@@ -460,7 +460,7 @@ pub fn rose_pine() -> UserThemeFamily {
                             (
                                 "function".into(),
                                 UserHighlightStyle {
-                                    color: Some(rgba(0x575279ff).into()),
+                                    color: Some(rgba(0xd7827eff).into()),
                                     ..Default::default()
                                 },
                             ),

crates/theme_importer/Cargo.toml 🔗

@@ -10,7 +10,7 @@ publish = false
 anyhow.workspace = true
 convert_case = "0.6.0"
 gpui = { package = "gpui2", path = "../gpui2" }
-indexmap = "1.6.2"
+indexmap = { version = "1.6.2", features = ["serde"] }
 json_comments = "0.2.2"
 log.workspace = true
 palette = { version = "0.7.3", default-features = false, features = ["std"] }

crates/theme_importer/src/main.rs 🔗

@@ -12,6 +12,7 @@ use std::str::FromStr;
 use anyhow::{anyhow, Context, Result};
 use convert_case::{Case, Casing};
 use gpui::serde_json;
+use indexmap::IndexMap;
 use json_comments::StripComments;
 use log::LevelFilter;
 use serde::Deserialize;
@@ -27,6 +28,14 @@ struct FamilyMetadata {
     pub name: String,
     pub author: String,
     pub themes: Vec<ThemeMetadata>,
+
+    /// Overrides for specific syntax tokens.
+    ///
+    /// Use this to ensure certain Zed syntax tokens are matched
+    /// to an exact set of scopes when it is not otherwise possible
+    /// to rely on the default mappings in the theme importer.
+    #[serde(default)]
+    pub syntax: IndexMap<String, Vec<String>>,
 }
 
 #[derive(Debug, Clone, Copy, Deserialize)]
@@ -127,7 +136,11 @@ fn main() -> Result<()> {
             let vscode_theme: VsCodeTheme = serde_json::from_reader(theme_without_comments)
                 .context(format!("failed to parse theme {theme_file_path:?}"))?;
 
-            let converter = VsCodeThemeConverter::new(vscode_theme, theme_metadata);
+            let converter = VsCodeThemeConverter::new(
+                vscode_theme,
+                theme_metadata,
+                family_metadata.syntax.clone(),
+            );
 
             let theme = converter.convert()?;
 

crates/theme_importer/src/vscode/converter.rs 🔗

@@ -9,7 +9,7 @@ use theme::{
 
 use crate::color::try_parse_color;
 use crate::util::Traverse;
-use crate::vscode::VsCodeTheme;
+use crate::vscode::{VsCodeTheme, VsCodeTokenScope};
 use crate::ThemeMetadata;
 
 use super::ZedSyntaxToken;
@@ -32,13 +32,19 @@ pub(crate) fn try_parse_font_style(font_style: &str) -> Option<UserFontStyle> {
 pub struct VsCodeThemeConverter {
     theme: VsCodeTheme,
     theme_metadata: ThemeMetadata,
+    syntax_overrides: IndexMap<String, Vec<String>>,
 }
 
 impl VsCodeThemeConverter {
-    pub fn new(theme: VsCodeTheme, theme_metadata: ThemeMetadata) -> Self {
+    pub fn new(
+        theme: VsCodeTheme,
+        theme_metadata: ThemeMetadata,
+        syntax_overrides: IndexMap<String, Vec<String>>,
+    ) -> Self {
         Self {
             theme,
             theme_metadata,
+            syntax_overrides,
         }
     }
 
@@ -291,8 +297,17 @@ impl VsCodeThemeConverter {
         let mut highlight_styles = IndexMap::new();
 
         for syntax_token in ZedSyntaxToken::iter() {
-            let best_match = syntax_token
-                .find_best_token_color_match(&self.theme.token_colors)
+            let override_match = self
+                .syntax_overrides
+                .get(&syntax_token.to_string())
+                .and_then(|scope| {
+                    self.theme.token_colors.iter().find(|token_color| {
+                        token_color.scope == Some(VsCodeTokenScope::Many(scope.clone()))
+                    })
+                });
+
+            let best_match = override_match
+                .or_else(|| syntax_token.find_best_token_color_match(&self.theme.token_colors))
                 .or_else(|| {
                     syntax_token.fallbacks().iter().find_map(|fallback| {
                         fallback.find_best_token_color_match(&self.theme.token_colors)

crates/theme_importer/src/vscode/syntax.rs 🔗

@@ -2,7 +2,7 @@ use indexmap::IndexMap;
 use serde::Deserialize;
 use strum::EnumIter;
 
-#[derive(Debug, Deserialize)]
+#[derive(Debug, PartialEq, Eq, Deserialize)]
 #[serde(untagged)]
 pub enum VsCodeTokenScope {
     One(String),