diff --git a/zed/src/settings.rs b/zed/src/settings.rs index 42b4afa3d3..ad4bb80e00 100644 --- a/zed/src/settings.rs +++ b/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, } -pub struct ThemeRegistry { - assets: Box, - themes: Mutex>>, - theme_data: Mutex>>, -} - -#[derive(Deserialize)] -struct ThemeToml { - #[serde(default)] - extends: Option, - #[serde(default)] - variables: HashMap, - #[serde(default)] - ui: HashMap, - #[serde(default)] - editor: HashMap, - #[serde(default)] - syntax: HashMap, -} - -#[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::new_with_theme(font_cache, Arc::new(Theme::default())) @@ -74,182 +38,6 @@ impl Settings { } } -impl ThemeRegistry { - pub fn new(source: impl AssetSource) -> Arc { - Arc::new(Self { - assets: Box::new(source), - themes: Default::default(), - theme_data: Default::default(), - }) - } - - pub fn list(&self) -> impl Iterator { - 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> { - 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> { - 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, watch::Receiver)> { @@ -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 { - 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, - stack: &mut Vec, -) -> 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, right: &HashMap) { - 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::>(), - &["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> { - 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> { - self.0 - .iter() - .copied() - .filter_map(|(path, _)| { - if path.starts_with(prefix) { - Some(path.into()) - } else { - None - } - }) - .collect() - } - } -} diff --git a/zed/src/theme.rs b/zed/src/theme.rs index 60dbe10d70..0dcccc52f0 100644 --- a/zed/src/theme.rs +++ b/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, themes: Mutex>>, theme_data: Mutex>>, } +#[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, extension: Map) { 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::>(), + &["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> { + 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> { + self.0 + .iter() + .copied() + .filter_map(|(path, _)| { + if path.starts_with(prefix) { + Some(path.into()) + } else { + None + } + }) + .collect() + } + } +}