Start on new theme::ThemeRegistry

Nathan Sobo and Max Brunsfeld created

Co-Authored-By: Max Brunsfeld <maxbrunsfeld@gmail.com>

Change summary

zed/src/theme.rs | 304 +++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 301 insertions(+), 3 deletions(-)

Detailed changes

zed/src/theme.rs 🔗

@@ -1,7 +1,21 @@
-use gpui::color::Color;
-use gpui::elements::{ContainerStyle, LabelStyle};
-use gpui::fonts::Properties as FontProperties;
+use anyhow::{anyhow, Context, Result};
+use gpui::{
+    color::Color,
+    elements::{ContainerStyle, LabelStyle},
+    fonts::Properties as FontProperties,
+    AssetSource,
+};
+use json::{Map, Value};
+use parking_lot::Mutex;
 use serde::Deserialize;
+use serde_json as json;
+use std::{cmp::Ordering, collections::HashMap, sync::Arc};
+
+pub struct ThemeRegistry {
+    assets: Box<dyn AssetSource>,
+    themes: Mutex<HashMap<String, Arc<Theme>>>,
+    theme_data: Mutex<HashMap<String, Arc<Map<String, Value>>>>,
+}
 
 #[derive(Debug, Default)]
 pub struct Theme {
@@ -78,3 +92,287 @@ impl Default for Editor {
         }
     }
 }
+
+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>> {
+        todo!()
+        // 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<Map<String, Value>>> {
+        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_data: Map<String, Value> = toml::from_slice(source_code.as_ref())
+            .with_context(|| format!("failed to parse {}.toml", name))?;
+
+        // If this theme extends another base theme, deeply merge it into the base theme's data
+        if let Some(base_name) = theme_data
+            .get("extends")
+            .and_then(|name| name.as_str())
+            .map(str::to_string)
+        {
+            let mut base_theme_data = self
+                .load(&base_name)
+                .with_context(|| format!("failed to load base theme {}", base_name))?
+                .as_ref()
+                .clone();
+            deep_merge_json(&mut base_theme_data, theme_data);
+            theme_data = base_theme_data;
+        }
+
+        // Evaluate `extends` fields in styles
+        let mut directives = Vec::new();
+        let mut key_path = Vec::new();
+        for (key, value) in theme_data.iter() {
+            if value.is_array() || value.is_object() {
+                key_path.push(Key::Object(key.clone()));
+                find_extensions(value, &mut key_path, &mut directives);
+                key_path.pop();
+            }
+        }
+        directives.sort_unstable();
+        for ExtendDirective {
+            source_path,
+            target_path,
+        } in directives
+        {
+            let source = value_at(&mut theme_data, &source_path)?.clone();
+            let target = value_at(&mut theme_data, &target_path)?;
+            if let Value::Object(source_object) = source {
+                deep_merge_json(target.as_object_mut().unwrap(), source_object);
+            }
+        }
+
+        // Evaluate any variables
+        if let Some((key, variables)) = theme_data.remove_entry("variables") {
+            if let Some(variables) = variables.as_object() {
+                for value in theme_data.values_mut() {
+                    evaluate_variables(value, &variables, &mut Vec::new())?;
+                }
+            }
+            theme_data.insert(key, variables);
+        }
+
+        let result = Arc::new(theme_data);
+        self.theme_data
+            .lock()
+            .insert(name.to_string(), result.clone());
+
+        Ok(result)
+    }
+}
+
+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 {
+            if let Some(base_object) = base.get_mut(&key).and_then(|value| value.as_object_mut()) {
+                deep_merge_json(base_object, extension_object);
+            } else {
+                base.insert(key, Value::Object(extension_object));
+            }
+        } else {
+            base.insert(key, extension_value);
+        }
+    }
+}
+
+#[derive(Clone, PartialEq, Eq)]
+enum Key {
+    Array(usize),
+    Object(String),
+}
+
+#[derive(PartialEq, Eq)]
+struct ExtendDirective {
+    source_path: Vec<Key>,
+    target_path: Vec<Key>,
+}
+
+impl Ord for ExtendDirective {
+    fn cmp(&self, other: &Self) -> Ordering {
+        if self.target_path.starts_with(&other.source_path)
+            || other.source_path.starts_with(&self.target_path)
+        {
+            Ordering::Less
+        } else if other.target_path.starts_with(&self.source_path)
+            || self.source_path.starts_with(&other.target_path)
+        {
+            Ordering::Greater
+        } else {
+            Ordering::Equal
+        }
+    }
+}
+
+impl PartialOrd for ExtendDirective {
+    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
+        Some(self.cmp(other))
+    }
+}
+
+fn find_extensions(value: &Value, key_path: &mut Vec<Key>, directives: &mut Vec<ExtendDirective>) {
+    match value {
+        Value::Array(vec) => {
+            for (ix, value) in vec.iter().enumerate() {
+                key_path.push(Key::Array(ix));
+                find_extensions(value, key_path, directives);
+                key_path.pop();
+            }
+        }
+        Value::Object(map) => {
+            for (key, value) in map.iter() {
+                if key == "extends" {
+                    if let Some(source_path) = value.as_str() {
+                        directives.push(ExtendDirective {
+                            source_path: source_path
+                                .split(".")
+                                .map(|key| Key::Object(key.to_string()))
+                                .collect(),
+                            target_path: key_path.clone(),
+                        });
+                    }
+                } else if value.is_array() || value.is_object() {
+                    key_path.push(Key::Object(key.to_string()));
+                    find_extensions(value, key_path, directives);
+                    key_path.pop();
+                }
+            }
+        }
+        _ => {}
+    }
+}
+
+fn value_at<'a>(object: &'a mut Map<String, Value>, key_path: &Vec<Key>) -> Result<&'a mut Value> {
+    let mut key_path = key_path.iter();
+    if let Some(Key::Object(first_key)) = key_path.next() {
+        let mut cur_value = object.get_mut(first_key);
+        for key in key_path {
+            if let Some(value) = cur_value {
+                match key {
+                    Key::Array(ix) => cur_value = value.get_mut(ix),
+                    Key::Object(key) => cur_value = value.get_mut(key),
+                }
+            } else {
+                return Err(anyhow!("invalid key path"));
+            }
+        }
+        cur_value.ok_or_else(|| anyhow!("invalid key path"))
+    } else {
+        Err(anyhow!("invalid key path"))
+    }
+}
+
+fn evaluate_variables(
+    value: &mut Value,
+    variables: &Map<String, Value>,
+    stack: &mut Vec<String>,
+) -> Result<()> {
+    match value {
+        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() {
+                        *value = definition;
+                        evaluate_variables(value, 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
+}