Move remaining theme-related code and tests from settings mod to theme mod

Nathan Sobo created

Change summary

zed/src/settings.rs | 483 ----------------------------------------------
zed/src/theme.rs    | 261 +++++++++++++++++++++++++
2 files changed, 266 insertions(+), 478 deletions(-)

Detailed changes

zed/src/settings.rs 🔗

@@ -1,20 +1,10 @@
-use anyhow::{anyhow, Context, Result};
-use gpui::{
-    color::Color,
-    font_cache::{FamilyId, FontCache},
-    fonts::{Properties as FontProperties, Style as FontStyle, Weight as FontWeight},
-    AssetSource,
-};
-use parking_lot::Mutex;
-use postage::watch;
-use serde::{de::value::MapDeserializer, Deserialize};
-use serde_json::Value;
-use std::{collections::HashMap, sync::Arc};
-
 use crate::theme;
-pub use theme::Theme;
+use anyhow::Result;
+use gpui::font_cache::{FamilyId, FontCache};
+use postage::watch;
+use std::sync::Arc;
 
-const DEFAULT_STYLE_ID: StyleId = StyleId(u32::MAX);
+pub use theme::{StyleId, Theme, ThemeMap, ThemeRegistry};
 
 #[derive(Clone)]
 pub struct Settings {
@@ -26,32 +16,6 @@ pub struct Settings {
     pub theme: Arc<Theme>,
 }
 
-pub struct ThemeRegistry {
-    assets: Box<dyn AssetSource>,
-    themes: Mutex<HashMap<String, Arc<Theme>>>,
-    theme_data: Mutex<HashMap<String, Arc<ThemeToml>>>,
-}
-
-#[derive(Deserialize)]
-struct ThemeToml {
-    #[serde(default)]
-    extends: Option<String>,
-    #[serde(default)]
-    variables: HashMap<String, Value>,
-    #[serde(default)]
-    ui: HashMap<String, Value>,
-    #[serde(default)]
-    editor: HashMap<String, Value>,
-    #[serde(default)]
-    syntax: HashMap<String, Value>,
-}
-
-#[derive(Clone, Debug)]
-pub struct ThemeMap(Arc<[StyleId]>);
-
-#[derive(Clone, Copy, Debug)]
-pub struct StyleId(u32);
-
 impl Settings {
     pub fn new(font_cache: &FontCache) -> Result<Self> {
         Self::new_with_theme(font_cache, Arc::new(Theme::default()))
@@ -74,182 +38,6 @@ impl Settings {
     }
 }
 
-impl ThemeRegistry {
-    pub fn new(source: impl AssetSource) -> Arc<Self> {
-        Arc::new(Self {
-            assets: Box::new(source),
-            themes: Default::default(),
-            theme_data: Default::default(),
-        })
-    }
-
-    pub fn list(&self) -> impl Iterator<Item = String> {
-        self.assets.list("themes/").into_iter().filter_map(|path| {
-            let filename = path.strip_prefix("themes/")?;
-            let theme_name = filename.strip_suffix(".toml")?;
-            if theme_name.starts_with('_') {
-                None
-            } else {
-                Some(theme_name.to_string())
-            }
-        })
-    }
-
-    pub fn get(&self, name: &str) -> Result<Arc<Theme>> {
-        if let Some(theme) = self.themes.lock().get(name) {
-            return Ok(theme.clone());
-        }
-
-        let theme_toml = self.load(name)?;
-        let mut syntax = Vec::<(String, Color, FontProperties)>::new();
-        for (key, style) in theme_toml.syntax.iter() {
-            let mut color = Color::default();
-            let mut properties = FontProperties::new();
-            match style {
-                Value::Object(object) => {
-                    if let Some(value) = object.get("color") {
-                        color = serde_json::from_value(value.clone())?;
-                    }
-                    if let Some(Value::Bool(true)) = object.get("italic") {
-                        properties.style = FontStyle::Italic;
-                    }
-                    properties.weight = deserialize_weight(object.get("weight"))?;
-                }
-                _ => {
-                    color = serde_json::from_value(style.clone())?;
-                }
-            }
-            match syntax.binary_search_by_key(&key, |e| &e.0) {
-                Ok(i) | Err(i) => {
-                    syntax.insert(i, (key.to_string(), color, properties));
-                }
-            }
-        }
-
-        let theme = Arc::new(Theme {
-            ui: theme::Ui::deserialize(MapDeserializer::new(theme_toml.ui.clone().into_iter()))?,
-            editor: theme::Editor::deserialize(MapDeserializer::new(
-                theme_toml.editor.clone().into_iter(),
-            ))?,
-            syntax,
-        });
-
-        self.themes.lock().insert(name.to_string(), theme.clone());
-        Ok(theme)
-    }
-
-    fn load(&self, name: &str) -> Result<Arc<ThemeToml>> {
-        if let Some(data) = self.theme_data.lock().get(name) {
-            return Ok(data.clone());
-        }
-
-        let asset_path = format!("themes/{}.toml", name);
-        let source_code = self
-            .assets
-            .load(&asset_path)
-            .with_context(|| format!("failed to load theme file {}", asset_path))?;
-
-        let mut theme_toml: ThemeToml = toml::from_slice(source_code.as_ref())
-            .with_context(|| format!("failed to parse {}.toml", name))?;
-
-        // If this theme extends another base theme, merge in the raw data from the base theme.
-        if let Some(base_name) = theme_toml.extends.as_ref() {
-            let base_theme_toml = self
-                .load(base_name)
-                .with_context(|| format!("failed to load base theme {}", base_name))?;
-            merge_map(&mut theme_toml.ui, &base_theme_toml.ui);
-            merge_map(&mut theme_toml.editor, &base_theme_toml.editor);
-            merge_map(&mut theme_toml.syntax, &base_theme_toml.syntax);
-            merge_map(&mut theme_toml.variables, &base_theme_toml.variables);
-        }
-
-        // Substitute any variable references for their definitions.
-        let values = theme_toml
-            .ui
-            .values_mut()
-            .chain(theme_toml.editor.values_mut())
-            .chain(theme_toml.syntax.values_mut());
-        let mut name_stack = Vec::new();
-        for value in values {
-            name_stack.clear();
-            evaluate_variables(value, &theme_toml.variables, &mut name_stack)?;
-        }
-
-        let result = Arc::new(theme_toml);
-        self.theme_data
-            .lock()
-            .insert(name.to_string(), result.clone());
-        Ok(result)
-    }
-}
-
-impl Theme {
-    pub fn syntax_style(&self, id: StyleId) -> (Color, FontProperties) {
-        self.syntax
-            .get(id.0 as usize)
-            .map_or((self.editor.text, FontProperties::new()), |entry| {
-                (entry.1, entry.2)
-            })
-    }
-
-    #[cfg(test)]
-    pub fn syntax_style_name(&self, id: StyleId) -> Option<&str> {
-        self.syntax.get(id.0 as usize).map(|e| e.0.as_str())
-    }
-}
-
-impl ThemeMap {
-    pub fn new(capture_names: &[String], theme: &Theme) -> 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.
-        ThemeMap(
-            capture_names
-                .iter()
-                .map(|capture_name| {
-                    theme
-                        .syntax
-                        .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_STYLE_ID, |(i, _)| StyleId(i as u32))
-                })
-                .collect(),
-        )
-    }
-
-    pub fn get(&self, capture_id: u32) -> StyleId {
-        self.0
-            .get(capture_id as usize)
-            .copied()
-            .unwrap_or(DEFAULT_STYLE_ID)
-    }
-}
-
-impl Default for ThemeMap {
-    fn default() -> Self {
-        Self(Arc::new([]))
-    }
-}
-
-impl Default for StyleId {
-    fn default() -> Self {
-        DEFAULT_STYLE_ID
-    }
-}
-
 pub fn channel(
     font_cache: &FontCache,
 ) -> Result<(watch::Sender<Settings>, watch::Receiver<Settings>)> {
@@ -265,264 +53,3 @@ pub fn channel_with_themes(
         themes.get("dark").expect("failed to load default theme"),
     )?))
 }
-
-fn deserialize_weight(weight: Option<&Value>) -> Result<FontWeight> {
-    match weight {
-        None => return Ok(FontWeight::NORMAL),
-        Some(Value::Number(number)) => {
-            if let Some(weight) = number.as_f64() {
-                return Ok(FontWeight(weight as f32));
-            }
-        }
-        Some(Value::String(s)) => match s.as_str() {
-            "normal" => return Ok(FontWeight::NORMAL),
-            "bold" => return Ok(FontWeight::BOLD),
-            "light" => return Ok(FontWeight::LIGHT),
-            "semibold" => return Ok(FontWeight::SEMIBOLD),
-            _ => {}
-        },
-        _ => {}
-    }
-    Err(anyhow!("Invalid weight {}", weight.unwrap()))
-}
-
-fn evaluate_variables(
-    expr: &mut Value,
-    variables: &HashMap<String, Value>,
-    stack: &mut Vec<String>,
-) -> Result<()> {
-    match expr {
-        Value::String(s) => {
-            if let Some(name) = s.strip_prefix("$") {
-                if stack.iter().any(|e| e == name) {
-                    Err(anyhow!("variable {} is defined recursively", name))?;
-                }
-                if validate_variable_name(name) {
-                    stack.push(name.to_string());
-                    if let Some(definition) = variables.get(name).cloned() {
-                        *expr = definition;
-                        evaluate_variables(expr, variables, stack)?;
-                    }
-                    stack.pop();
-                }
-            }
-        }
-        Value::Array(a) => {
-            for value in a.iter_mut() {
-                evaluate_variables(value, variables, stack)?;
-            }
-        }
-        Value::Object(object) => {
-            for value in object.values_mut() {
-                evaluate_variables(value, variables, stack)?;
-            }
-        }
-        _ => {}
-    }
-    Ok(())
-}
-
-fn validate_variable_name(name: &str) -> bool {
-    let mut chars = name.chars();
-    if let Some(first) = chars.next() {
-        if first.is_alphabetic() || first == '_' {
-            if chars.all(|c| c.is_alphanumeric() || c == '_') {
-                return true;
-            }
-        }
-    }
-    false
-}
-
-fn merge_map(left: &mut HashMap<String, Value>, right: &HashMap<String, Value>) {
-    for (name, value) in right {
-        if !left.contains_key(name) {
-            left.insert(name.clone(), value.clone());
-        }
-    }
-}
-
-#[cfg(test)]
-mod tests {
-    use super::*;
-
-    #[test]
-    fn test_parse_simple_theme() {
-        let assets = TestAssets(&[(
-            "themes/my-theme.toml",
-            r#"
-            [ui.tab.active]
-            background = 0x100000
-
-            [editor]
-            background = 0x00ed00
-            line_number = 0xdddddd
-
-            [syntax]
-            "beta.two" = 0xAABBCC
-            "alpha.one" = {color = 0x112233, weight = "bold"}
-            "gamma.three" = {weight = "light", italic = true}
-            "#,
-        )]);
-
-        let registry = ThemeRegistry::new(assets);
-        let theme = registry.get("my-theme").unwrap();
-
-        assert_eq!(
-            theme.ui.active_tab.container.background_color,
-            Some(Color::from_u32(0x100000ff))
-        );
-        assert_eq!(theme.editor.background, Color::from_u32(0x00ed00ff));
-        assert_eq!(theme.editor.line_number, Color::from_u32(0xddddddff));
-        assert_eq!(
-            theme.syntax,
-            &[
-                (
-                    "alpha.one".to_string(),
-                    Color::from_u32(0x112233ff),
-                    *FontProperties::new().weight(FontWeight::BOLD)
-                ),
-                (
-                    "beta.two".to_string(),
-                    Color::from_u32(0xaabbccff),
-                    *FontProperties::new().weight(FontWeight::NORMAL)
-                ),
-                (
-                    "gamma.three".to_string(),
-                    Color::from_u32(0x00000000),
-                    *FontProperties::new()
-                        .weight(FontWeight::LIGHT)
-                        .style(FontStyle::Italic),
-                ),
-            ]
-        );
-    }
-
-    #[test]
-    fn test_parse_extended_theme() {
-        let assets = TestAssets(&[
-            (
-                "themes/_base.toml",
-                r#"
-                abstract = true
-
-                [ui.tab]
-                background = 0x111111
-                text = "$variable_1"
-
-                [editor]
-                background = 0x222222
-                default_text = "$variable_2"
-                "#,
-            ),
-            (
-                "themes/light.toml",
-                r#"
-                extends = "_base"
-
-                [variables]
-                variable_1 = 0x333333
-                variable_2 = 0x444444
-
-                [ui.tab]
-                background = 0x555555
-
-                [editor]
-                background = 0x666666
-                "#,
-            ),
-            (
-                "themes/dark.toml",
-                r#"
-                extends = "_base"
-
-                [variables]
-                variable_1 = 0x555555
-                variable_2 = 0x666666
-                "#,
-            ),
-        ]);
-
-        let registry = ThemeRegistry::new(assets);
-        let theme = registry.get("light").unwrap();
-
-        assert_eq!(
-            theme.ui.tab.container.background_color,
-            Some(Color::from_u32(0x555555ff))
-        );
-        assert_eq!(theme.ui.tab.label.color, Color::from_u32(0x333333ff));
-        assert_eq!(theme.editor.background, Color::from_u32(0x666666ff));
-        assert_eq!(theme.editor.text, Color::from_u32(0x444444ff));
-
-        assert_eq!(
-            registry.list().collect::<Vec<_>>(),
-            &["light".to_string(), "dark".to_string()]
-        );
-    }
-
-    #[test]
-    fn test_parse_empty_theme() {
-        let assets = TestAssets(&[("themes/my-theme.toml", "")]);
-        let registry = ThemeRegistry::new(assets);
-        registry.get("my-theme").unwrap();
-    }
-
-    #[test]
-    fn test_theme_map() {
-        let theme = Theme {
-            ui: Default::default(),
-            editor: Default::default(),
-            syntax: [
-                ("function", Color::from_u32(0x100000ff)),
-                ("function.method", Color::from_u32(0x200000ff)),
-                ("function.async", Color::from_u32(0x300000ff)),
-                ("variable.builtin.self.rust", Color::from_u32(0x400000ff)),
-                ("variable.builtin", Color::from_u32(0x500000ff)),
-                ("variable", Color::from_u32(0x600000ff)),
-            ]
-            .iter()
-            .map(|e| (e.0.to_string(), e.1, FontProperties::new()))
-            .collect(),
-        };
-
-        let capture_names = &[
-            "function.special".to_string(),
-            "function.async.rust".to_string(),
-            "variable.builtin.self".to_string(),
-        ];
-
-        let map = ThemeMap::new(capture_names, &theme);
-        assert_eq!(theme.syntax_style_name(map.get(0)), Some("function"));
-        assert_eq!(theme.syntax_style_name(map.get(1)), Some("function.async"));
-        assert_eq!(
-            theme.syntax_style_name(map.get(2)),
-            Some("variable.builtin")
-        );
-    }
-
-    struct TestAssets(&'static [(&'static str, &'static str)]);
-
-    impl AssetSource for TestAssets {
-        fn load(&self, path: &str) -> Result<std::borrow::Cow<[u8]>> {
-            if let Some(row) = self.0.iter().find(|e| e.0 == path) {
-                Ok(row.1.as_bytes().into())
-            } else {
-                Err(anyhow!("no such path {}", path))
-            }
-        }
-
-        fn list(&self, prefix: &str) -> Vec<std::borrow::Cow<'static, str>> {
-            self.0
-                .iter()
-                .copied()
-                .filter_map(|(path, _)| {
-                    if path.starts_with(prefix) {
-                        Some(path.into())
-                    } else {
-                        None
-                    }
-                })
-                .collect()
-        }
-    }
-}

zed/src/theme.rs 🔗

@@ -11,12 +11,20 @@ use serde::{de, Deserialize, Deserializer};
 use serde_json as json;
 use std::{cmp::Ordering, collections::HashMap, sync::Arc};
 
+const DEFAULT_STYLE_ID: StyleId = StyleId(u32::MAX);
+
 pub struct ThemeRegistry {
     assets: Box<dyn AssetSource>,
     themes: Mutex<HashMap<String, Arc<Theme>>>,
     theme_data: Mutex<HashMap<String, Arc<Value>>>,
 }
 
+#[derive(Clone, Debug)]
+pub struct ThemeMap(Arc<[StyleId]>);
+
+#[derive(Clone, Copy, Debug)]
+pub struct StyleId(u32);
+
 #[derive(Debug, Default, Deserialize)]
 pub struct Theme {
     pub ui: Ui,
@@ -204,6 +212,73 @@ impl ThemeRegistry {
     }
 }
 
+impl Theme {
+    pub fn syntax_style(&self, id: StyleId) -> (Color, FontProperties) {
+        self.syntax
+            .get(id.0 as usize)
+            .map_or((self.editor.text, FontProperties::new()), |entry| {
+                (entry.1, entry.2)
+            })
+    }
+
+    #[cfg(test)]
+    pub fn syntax_style_name(&self, id: StyleId) -> Option<&str> {
+        self.syntax.get(id.0 as usize).map(|e| e.0.as_str())
+    }
+}
+
+impl ThemeMap {
+    pub fn new(capture_names: &[String], theme: &Theme) -> 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.
+        ThemeMap(
+            capture_names
+                .iter()
+                .map(|capture_name| {
+                    theme
+                        .syntax
+                        .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_STYLE_ID, |(i, _)| StyleId(i as u32))
+                })
+                .collect(),
+        )
+    }
+
+    pub fn get(&self, capture_id: u32) -> StyleId {
+        self.0
+            .get(capture_id as usize)
+            .copied()
+            .unwrap_or(DEFAULT_STYLE_ID)
+    }
+}
+
+impl Default for ThemeMap {
+    fn default() -> Self {
+        Self(Arc::new([]))
+    }
+}
+
+impl Default for StyleId {
+    fn default() -> Self {
+        DEFAULT_STYLE_ID
+    }
+}
+
 fn deep_merge_json(base: &mut Map<String, Value>, extension: Map<String, Value>) {
     for (key, extension_value) in extension {
         if let Value::Object(extension_object) = extension_value {
@@ -384,3 +459,189 @@ where
 
     Ok(result)
 }
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use gpui::fonts::{Properties as FontProperties, Style as FontStyle, Weight as FontWeight};
+
+    #[test]
+    fn test_parse_simple_theme() {
+        let assets = TestAssets(&[(
+            "themes/my-theme.toml",
+            r#"
+            [ui.tab.active]
+            background = 0x100000
+
+            [editor]
+            background = 0x00ed00
+            line_number = 0xdddddd
+
+            [syntax]
+            "beta.two" = 0xAABBCC
+            "alpha.one" = {color = 0x112233, weight = "bold"}
+            "gamma.three" = {weight = "light", italic = true}
+            "#,
+        )]);
+
+        let registry = ThemeRegistry::new(assets);
+        let theme = registry.get("my-theme").unwrap();
+
+        assert_eq!(
+            theme.ui.active_tab.container.background_color,
+            Some(Color::from_u32(0x100000ff))
+        );
+        assert_eq!(theme.editor.background, Color::from_u32(0x00ed00ff));
+        assert_eq!(theme.editor.line_number, Color::from_u32(0xddddddff));
+        assert_eq!(
+            theme.syntax,
+            &[
+                (
+                    "alpha.one".to_string(),
+                    Color::from_u32(0x112233ff),
+                    *FontProperties::new().weight(FontWeight::BOLD)
+                ),
+                (
+                    "beta.two".to_string(),
+                    Color::from_u32(0xaabbccff),
+                    *FontProperties::new().weight(FontWeight::NORMAL)
+                ),
+                (
+                    "gamma.three".to_string(),
+                    Color::from_u32(0x00000000),
+                    *FontProperties::new()
+                        .weight(FontWeight::LIGHT)
+                        .style(FontStyle::Italic),
+                ),
+            ]
+        );
+    }
+
+    #[test]
+    fn test_parse_extended_theme() {
+        let assets = TestAssets(&[
+            (
+                "themes/_base.toml",
+                r#"
+                abstract = true
+
+                [ui.tab]
+                background = 0x111111
+                text = "$variable_1"
+
+                [editor]
+                background = 0x222222
+                default_text = "$variable_2"
+                "#,
+            ),
+            (
+                "themes/light.toml",
+                r#"
+                extends = "_base"
+
+                [variables]
+                variable_1 = 0x333333
+                variable_2 = 0x444444
+
+                [ui.tab]
+                background = 0x555555
+
+                [editor]
+                background = 0x666666
+                "#,
+            ),
+            (
+                "themes/dark.toml",
+                r#"
+                extends = "_base"
+
+                [variables]
+                variable_1 = 0x555555
+                variable_2 = 0x666666
+                "#,
+            ),
+        ]);
+
+        let registry = ThemeRegistry::new(assets);
+        let theme = registry.get("light").unwrap();
+
+        assert_eq!(
+            theme.ui.tab.container.background_color,
+            Some(Color::from_u32(0x555555ff))
+        );
+        assert_eq!(theme.ui.tab.label.color, Color::from_u32(0x333333ff));
+        assert_eq!(theme.editor.background, Color::from_u32(0x666666ff));
+        assert_eq!(theme.editor.text, Color::from_u32(0x444444ff));
+
+        assert_eq!(
+            registry.list().collect::<Vec<_>>(),
+            &["light".to_string(), "dark".to_string()]
+        );
+    }
+
+    #[test]
+    fn test_parse_empty_theme() {
+        let assets = TestAssets(&[("themes/my-theme.toml", "")]);
+        let registry = ThemeRegistry::new(assets);
+        registry.get("my-theme").unwrap();
+    }
+
+    #[test]
+    fn test_theme_map() {
+        let theme = Theme {
+            ui: Default::default(),
+            editor: Default::default(),
+            syntax: [
+                ("function", Color::from_u32(0x100000ff)),
+                ("function.method", Color::from_u32(0x200000ff)),
+                ("function.async", Color::from_u32(0x300000ff)),
+                ("variable.builtin.self.rust", Color::from_u32(0x400000ff)),
+                ("variable.builtin", Color::from_u32(0x500000ff)),
+                ("variable", Color::from_u32(0x600000ff)),
+            ]
+            .iter()
+            .map(|e| (e.0.to_string(), e.1, FontProperties::new()))
+            .collect(),
+        };
+
+        let capture_names = &[
+            "function.special".to_string(),
+            "function.async.rust".to_string(),
+            "variable.builtin.self".to_string(),
+        ];
+
+        let map = ThemeMap::new(capture_names, &theme);
+        assert_eq!(theme.syntax_style_name(map.get(0)), Some("function"));
+        assert_eq!(theme.syntax_style_name(map.get(1)), Some("function.async"));
+        assert_eq!(
+            theme.syntax_style_name(map.get(2)),
+            Some("variable.builtin")
+        );
+    }
+
+    struct TestAssets(&'static [(&'static str, &'static str)]);
+
+    impl AssetSource for TestAssets {
+        fn load(&self, path: &str) -> Result<std::borrow::Cow<[u8]>> {
+            if let Some(row) = self.0.iter().find(|e| e.0 == path) {
+                Ok(row.1.as_bytes().into())
+            } else {
+                Err(anyhow!("no such path {}", path))
+            }
+        }
+
+        fn list(&self, prefix: &str) -> Vec<std::borrow::Cow<'static, str>> {
+            self.0
+                .iter()
+                .copied()
+                .filter_map(|(path, _)| {
+                    if path.starts_with(prefix) {
+                        Some(path.into())
+                    } else {
+                        None
+                    }
+                })
+                .collect()
+        }
+    }
+}