Get new theme structure working

Max Brunsfeld and Nathan Sobo created

* Fix precedence of extends directives
* Always group color with font properties for text theming

Co-Authored-By: Nathan Sobo <nathan@zed.dev>

Change summary

gpui/src/color.rs                      |  25 ++
gpui/src/elements/label.rs             |  63 +++---
gpui/src/fonts.rs                      |  96 ++++++---
zed/assets/themes/_base.toml           |  35 ++-
zed/assets/themes/dark.toml            |  32 +-
zed/src/editor.rs                      |  14 
zed/src/editor/buffer.rs               |  16 
zed/src/editor/display_map.rs          |  30 --
zed/src/editor/display_map/fold_map.rs |   8 
zed/src/editor/display_map/tab_map.rs  |   6 
zed/src/editor/display_map/wrap_map.rs |   8 
zed/src/file_finder.rs                 |  19 +-
zed/src/language.rs                    |  16 
zed/src/settings.rs                    |  11 
zed/src/theme.rs                       | 250 +++++++++++----------------
15 files changed, 309 insertions(+), 320 deletions(-)

Detailed changes

gpui/src/color.rs 🔗

@@ -1,11 +1,15 @@
 use std::{
+    borrow::Cow,
     fmt,
     ops::{Deref, DerefMut},
 };
 
 use crate::json::ToJson;
 use pathfinder_color::ColorU;
-use serde::{Deserialize, Deserializer};
+use serde::{
+    de::{self, Unexpected},
+    Deserialize, Deserializer,
+};
 use serde_json::json;
 
 #[derive(Clone, Copy, Default, PartialEq, Eq, Hash)]
@@ -39,13 +43,20 @@ impl<'de> Deserialize<'de> for Color {
     where
         D: Deserializer<'de>,
     {
-        let mut rgba = u32::deserialize(deserializer)?;
-
-        if rgba <= 0xFFFFFF {
-            rgba = (rgba << 8) + 0xFF;
+        let literal: Cow<str> = Deserialize::deserialize(deserializer)?;
+        if let Some(digits) = literal.strip_prefix('#') {
+            if let Ok(value) = u32::from_str_radix(digits, 16) {
+                if digits.len() == 6 {
+                    return Ok(Color::from_u32((value << 8) | 0xFF));
+                } else if digits.len() == 8 {
+                    return Ok(Color::from_u32(value));
+                }
+            }
         }
-
-        Ok(Self::from_u32(rgba))
+        Err(de::Error::invalid_value(
+            Unexpected::Str(literal.as_ref()),
+            &"#RRGGBB[AA]",
+        ))
     }
 }
 

gpui/src/elements/label.rs 🔗

@@ -1,7 +1,7 @@
 use crate::{
     color::Color,
     font_cache::FamilyId,
-    fonts::{deserialize_font_properties, deserialize_option_font_properties, FontId, Properties},
+    fonts::{FontId, TextStyle},
     geometry::{
         rect::RectF,
         vector::{vec2f, Vector2F},
@@ -25,14 +25,8 @@ pub struct Label {
 
 #[derive(Clone, Debug, Default, Deserialize)]
 pub struct LabelStyle {
-    #[serde(default = "Color::black")]
-    pub color: Color,
-    #[serde(default)]
-    pub highlight_color: Option<Color>,
-    #[serde(default, deserialize_with = "deserialize_font_properties")]
-    pub font_properties: Properties,
-    #[serde(default, deserialize_with = "deserialize_option_font_properties")]
-    pub highlight_font_properties: Option<Properties>,
+    pub text: TextStyle,
+    pub highlight_text: Option<TextStyle>,
 }
 
 impl Label {
@@ -52,7 +46,7 @@ impl Label {
     }
 
     pub fn with_default_color(mut self, color: Color) -> Self {
-        self.style.color = color;
+        self.style.text.color = color;
         self
     }
 
@@ -67,13 +61,18 @@ impl Label {
         font_id: FontId,
     ) -> SmallVec<[(usize, FontId, Color); 8]> {
         if self.highlight_indices.is_empty() {
-            return smallvec![(self.text.len(), font_id, self.style.color)];
+            return smallvec![(self.text.len(), font_id, self.style.text.color)];
         }
 
         let highlight_font_id = self
             .style
-            .highlight_font_properties
-            .and_then(|properties| font_cache.select_font(self.family_id, &properties).ok())
+            .highlight_text
+            .as_ref()
+            .and_then(|style| {
+                font_cache
+                    .select_font(self.family_id, &style.font_properties)
+                    .ok()
+            })
             .unwrap_or(font_id);
 
         let mut highlight_indices = self.highlight_indices.iter().copied().peekable();
@@ -81,11 +80,16 @@ impl Label {
 
         for (char_ix, c) in self.text.char_indices() {
             let mut font_id = font_id;
-            let mut color = self.style.color;
+            let mut color = self.style.text.color;
             if let Some(highlight_ix) = highlight_indices.peek() {
                 if char_ix == *highlight_ix {
                     font_id = highlight_font_id;
-                    color = self.style.highlight_color.unwrap_or(self.style.color);
+                    color = self
+                        .style
+                        .highlight_text
+                        .as_ref()
+                        .unwrap_or(&self.style.text)
+                        .color;
                     highlight_indices.next();
                 }
             }
@@ -121,7 +125,7 @@ impl Element for Label {
     ) -> (Vector2F, Self::LayoutState) {
         let font_id = cx
             .font_cache
-            .select_font(self.family_id, &self.style.font_properties)
+            .select_font(self.family_id, &self.style.text.font_properties)
             .unwrap();
         let runs = self.compute_runs(&cx.font_cache, font_id);
         let line =
@@ -185,40 +189,43 @@ impl Element for Label {
 impl ToJson for LabelStyle {
     fn to_json(&self) -> Value {
         json!({
-            "default_color": self.color.to_json(),
-            "default_font_properties": self.font_properties.to_json(),
-            "highlight_color": self.highlight_color.to_json(),
-            "highlight_font_properties": self.highlight_font_properties.to_json(),
+            "text": self.text.to_json(),
+            "highlight_text": self.highlight_text
+                .as_ref()
+                .map_or(serde_json::Value::Null, |style| style.to_json())
         })
     }
 }
 
 #[cfg(test)]
 mod tests {
-    use font_kit::properties::Weight;
-
     use super::*;
+    use crate::fonts::{Properties as FontProperties, Weight};
 
     #[crate::test(self)]
     fn test_layout_label_with_highlights(cx: &mut crate::MutableAppContext) {
         let menlo = cx.font_cache().load_family(&["Menlo"]).unwrap();
         let menlo_regular = cx
             .font_cache()
-            .select_font(menlo, &Properties::new())
+            .select_font(menlo, &FontProperties::new())
             .unwrap();
         let menlo_bold = cx
             .font_cache()
-            .select_font(menlo, Properties::new().weight(Weight::BOLD))
+            .select_font(menlo, FontProperties::new().weight(Weight::BOLD))
             .unwrap();
         let black = Color::black();
         let red = Color::new(255, 0, 0, 255);
 
         let label = Label::new(".αβγδε.ⓐⓑⓒⓓⓔ.abcde.".to_string(), menlo, 12.0)
             .with_style(&LabelStyle {
-                color: black,
-                highlight_color: Some(red),
-                highlight_font_properties: Some(*Properties::new().weight(Weight::BOLD)),
-                ..Default::default()
+                text: TextStyle {
+                    color: black,
+                    font_properties: Default::default(),
+                },
+                highlight_text: Some(TextStyle {
+                    color: red,
+                    font_properties: *FontProperties::new().weight(Weight::BOLD),
+                }),
             })
             .with_highlights(vec![
                 ".α".len(),

gpui/src/fonts.rs 🔗

@@ -1,15 +1,25 @@
-use crate::json::{json, ToJson};
+use crate::{
+    color::Color,
+    json::{json, ToJson},
+};
 pub use font_kit::{
     metrics::Metrics,
     properties::{Properties, Stretch, Style, Weight},
 };
-use serde::{Deserialize, Deserializer};
+use serde::{de, Deserialize};
+use serde_json::Value;
 
 #[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
 pub struct FontId(pub usize);
 
 pub type GlyphId = u32;
 
+#[derive(Clone, Debug, Default, PartialEq, Eq)]
+pub struct TextStyle {
+    pub color: Color,
+    pub font_properties: Properties,
+}
+
 #[allow(non_camel_case_types)]
 #[derive(Deserialize)]
 enum WeightJson {
@@ -25,16 +35,53 @@ enum WeightJson {
 }
 
 #[derive(Deserialize)]
-struct PropertiesJson {
+struct TextStyleJson {
+    color: Color,
     weight: Option<WeightJson>,
     #[serde(default)]
     italic: bool,
 }
 
-impl Into<Properties> for PropertiesJson {
-    fn into(self) -> Properties {
-        let mut result = Properties::new();
-        result.weight = match self.weight.unwrap_or(WeightJson::normal) {
+impl<'de> Deserialize<'de> for TextStyle {
+    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+    where
+        D: serde::Deserializer<'de>,
+    {
+        let json = Value::deserialize(deserializer)?;
+        if json.is_object() {
+            let style_json: TextStyleJson =
+                serde_json::from_value(json).map_err(de::Error::custom)?;
+            Ok(style_json.into())
+        } else {
+            Ok(Self {
+                color: serde_json::from_value(json).map_err(de::Error::custom)?,
+                font_properties: Properties::new(),
+            })
+        }
+    }
+}
+
+impl From<Color> for TextStyle {
+    fn from(color: Color) -> Self {
+        Self {
+            color,
+            font_properties: Default::default(),
+        }
+    }
+}
+
+impl ToJson for TextStyle {
+    fn to_json(&self) -> Value {
+        json!({
+            "color": self.color.to_json(),
+            "font_properties": self.font_properties.to_json(),
+        })
+    }
+}
+
+impl Into<TextStyle> for TextStyleJson {
+    fn into(self) -> TextStyle {
+        let weight = match self.weight.unwrap_or(WeightJson::normal) {
             WeightJson::thin => Weight::THIN,
             WeightJson::extra_light => Weight::EXTRA_LIGHT,
             WeightJson::light => Weight::LIGHT,
@@ -45,37 +92,18 @@ impl Into<Properties> for PropertiesJson {
             WeightJson::extra_bold => Weight::EXTRA_BOLD,
             WeightJson::black => Weight::BLACK,
         };
-        if self.italic {
-            result.style = Style::Italic;
+        let style = if self.italic {
+            Style::Italic
+        } else {
+            Style::Normal
+        };
+        TextStyle {
+            color: self.color,
+            font_properties: *Properties::new().weight(weight).style(style),
         }
-        result
     }
 }
 
-pub fn deserialize_option_font_properties<'de, D>(
-    deserializer: D,
-) -> Result<Option<Properties>, D::Error>
-where
-    D: Deserializer<'de>,
-{
-    let json: Option<PropertiesJson> = Deserialize::deserialize(deserializer)?;
-    Ok(json.map(Into::into))
-}
-
-pub fn deserialize_font_properties<'de, D>(deserializer: D) -> Result<Properties, D::Error>
-where
-    D: Deserializer<'de>,
-{
-    let json: PropertiesJson = Deserialize::deserialize(deserializer)?;
-    Ok(json.into())
-}
-
-pub fn font_properties_from_json(
-    value: serde_json::Value,
-) -> Result<Properties, serde_json::Error> {
-    Ok(serde_json::from_value::<PropertiesJson>(value)?.into())
-}
-
 impl ToJson for Properties {
     fn to_json(&self) -> crate::json::Value {
         json!({

zed/assets/themes/_base.toml 🔗

@@ -3,34 +3,35 @@ background = "$elevation_1"
 
 [ui.tab]
 background = "$elevation_2"
-color = "$text_dull"
-border.color = 0x000000
-icon_close = 0x383839
-icon_dirty = 0x556de8
-icon_conflict = 0xe45349
+text = "$text_dull"
+border.color = "#000000"
+icon_close = "#383839"
+icon_dirty = "#556de8"
+icon_conflict = "#e45349"
 
 [ui.active_tab]
-extends = ".."
+extends = "ui.tab"
 background = "$elevation_3"
-color = "$text_bright"
+text = "$text_bright"
 
 [ui.selector]
 background = "$elevation_4"
+text = "$text_bright"
 padding = { top = 6.0, bottom = 6.0, left = 6.0, right = 6.0 }
 margin.top = 12.0
 corner_radius = 6.0
-shadow = { offset = [0.0, 0.0], blur = 12.0, color = 0x00000088 }
+shadow = { offset = [0.0, 0.0], blur = 12.0, color = "#00000088" }
 
 [ui.selector.item]
-background = 0x424344
-text = 0xcccccc
-highlight_text = 0x18a3ff
-highlight_font_properties = { weight = "bold" }
-border = { color = 0x000000, width = 1.0 }
+background = "#424344"
+text = "#cccccc"
+highlight_text = { color = "#18a3ff", weight = "bold" }
+border = { color = "#000000", width = 1.0 }
+padding = { top = 6.0, bottom = 6.0, left = 6.0, right = 6.0 }
 
 [ui.selector.active_item]
-extends = ".."
-background = 0x094771
+extends = "ui.selector.item"
+background = "#094771"
 
 [editor]
 background = "$elevation_3"
@@ -40,6 +41,6 @@ line_number = "$text_dull"
 line_number_active = "$text_bright"
 text = "$text_normal"
 replicas = [
-    { selection = 0x264f78, cursor = "$text_bright" },
-    { selection = 0x504f31, cursor = 0xfcf154 },
+    { selection = "#264f78", cursor = "$text_bright" },
+    { selection = "#504f31", cursor = "#fcf154" },
 ]

zed/assets/themes/dark.toml 🔗

@@ -1,21 +1,21 @@
 extends = "_base"
 
 [variables]
-elevation_1 = 0x050101
-elevation_2 = 0x131415
-elevation_3 = 0x1c1d1e
-elevation_4 = 0x3a3b3c
-text_dull = 0x5a5a5b
-text_bright = 0xffffff
-text_normal = 0xd4d4d4
+elevation_1 = "#050101"
+elevation_2 = "#131415"
+elevation_3 = "#1c1d1e"
+elevation_4 = "#3a3b3c"
+text_dull = "#5a5a5b"
+text_bright = "#ffffff"
+text_normal = "#d4d4d4"
 
 [syntax]
-keyword = 0xc586c0
-function = 0xdcdcaa
-string = 0xcb8f77
-type = 0x4ec9b0
-number = 0xb5cea8
-comment = 0x6a9955
-property = 0x4e94ce
-variant = 0x4fc1ff
-constant = 0x9cdcfe
+keyword = { color = "#c586c0", weight = "bold" }
+function = "#dcdcaa"
+string = "#cb8f77"
+type = "#4ec9b0"
+number = "#b5cea8"
+comment = "#6a9955"
+property = "#4e94ce"
+variant = "#4fc1ff"
+constant = "#9cdcfe"

zed/src/editor.rs 🔗

@@ -4,7 +4,7 @@ mod element;
 pub mod movement;
 
 use crate::{
-    settings::{Settings, StyleId, Theme},
+    settings::{HighlightId, Settings, Theme},
     time::ReplicaId,
     util::{post_inc, Bias},
     workspace,
@@ -2419,7 +2419,7 @@ impl Snapshot {
             .display_snapshot
             .highlighted_chunks_for_rows(rows.clone());
 
-        'outer: for (chunk, style_ix) in chunks.chain(Some(("\n", StyleId::default()))) {
+        'outer: for (chunk, style_ix) in chunks.chain(Some(("\n", HighlightId::default()))) {
             for (ix, mut line_chunk) in chunk.split('\n').enumerate() {
                 if ix > 0 {
                     layouts.push(layout_cache.layout_str(&line, self.font_size, &styles));
@@ -2433,12 +2433,12 @@ impl Snapshot {
                 }
 
                 if !line_chunk.is_empty() && !line_exceeded_max_len {
-                    let (color, font_properties) = self.theme.syntax_style(style_ix);
+                    let style = self.theme.highlight_style(style_ix);
                     // Avoid a lookup if the font properties match the previous ones.
-                    let font_id = if font_properties == prev_font_properties {
+                    let font_id = if style.font_properties == prev_font_properties {
                         prev_font_id
                     } else {
-                        font_cache.select_font(self.font_family, &font_properties)?
+                        font_cache.select_font(self.font_family, &style.font_properties)?
                     };
 
                     if line.len() + line_chunk.len() > MAX_LINE_LEN {
@@ -2451,9 +2451,9 @@ impl Snapshot {
                     }
 
                     line.push_str(line_chunk);
-                    styles.push((line_chunk.len(), font_id, color));
+                    styles.push((line_chunk.len(), font_id, style.color));
                     prev_font_id = font_id;
-                    prev_font_properties = font_properties;
+                    prev_font_properties = style.font_properties;
                 }
             }
         }

zed/src/editor/buffer.rs 🔗

@@ -16,7 +16,7 @@ use zrpc::proto;
 use crate::{
     language::{Language, Tree},
     operation_queue::{self, OperationQueue},
-    settings::{StyleId, ThemeMap},
+    settings::{HighlightId, HighlightMap},
     sum_tree::{self, FilterCursor, SumTree},
     time::{self, ReplicaId},
     util::Bias,
@@ -1985,7 +1985,7 @@ impl Snapshot {
                     captures,
                     next_capture: None,
                     stack: Default::default(),
-                    theme_mapping: language.theme_mapping(),
+                    highlight_map: language.highlight_map(),
                 }),
             }
         } else {
@@ -2316,8 +2316,8 @@ impl<'a> tree_sitter::TextProvider<'a> for TextProvider<'a> {
 struct Highlights<'a> {
     captures: tree_sitter::QueryCaptures<'a, 'a, TextProvider<'a>>,
     next_capture: Option<(tree_sitter::QueryMatch<'a, 'a>, usize)>,
-    stack: Vec<(usize, StyleId)>,
-    theme_mapping: ThemeMap,
+    stack: Vec<(usize, HighlightId)>,
+    highlight_map: HighlightMap,
 }
 
 pub struct HighlightedChunks<'a> {
@@ -2341,7 +2341,7 @@ impl<'a> HighlightedChunks<'a> {
                     if offset < next_capture_end {
                         highlights.stack.push((
                             next_capture_end,
-                            highlights.theme_mapping.get(capture.index),
+                            highlights.highlight_map.get(capture.index),
                         ));
                     }
                     highlights.next_capture.take();
@@ -2357,7 +2357,7 @@ impl<'a> HighlightedChunks<'a> {
 }
 
 impl<'a> Iterator for HighlightedChunks<'a> {
-    type Item = (&'a str, StyleId);
+    type Item = (&'a str, HighlightId);
 
     fn next(&mut self) -> Option<Self::Item> {
         let mut next_capture_start = usize::MAX;
@@ -2381,7 +2381,7 @@ impl<'a> Iterator for HighlightedChunks<'a> {
                     next_capture_start = capture.node.start_byte();
                     break;
                 } else {
-                    let style_id = highlights.theme_mapping.get(capture.index);
+                    let style_id = highlights.highlight_map.get(capture.index);
                     highlights.stack.push((capture.node.end_byte(), style_id));
                     highlights.next_capture = highlights.captures.next();
                 }
@@ -2391,7 +2391,7 @@ impl<'a> Iterator for HighlightedChunks<'a> {
         if let Some(chunk) = self.chunks.peek() {
             let chunk_start = self.range.start;
             let mut chunk_end = (self.chunks.offset() + chunk.len()).min(next_capture_start);
-            let mut style_id = StyleId::default();
+            let mut style_id = HighlightId::default();
             if let Some((parent_capture_end, parent_style_id)) =
                 self.highlights.as_ref().and_then(|h| h.stack.last())
             {

zed/src/editor/display_map.rs 🔗

@@ -654,16 +654,8 @@ mod tests {
         .unwrap();
         let theme = Theme {
             syntax: vec![
-                (
-                    "mod.body".to_string(),
-                    Color::from_u32(0xff0000ff),
-                    Default::default(),
-                ),
-                (
-                    "fn.name".to_string(),
-                    Color::from_u32(0x00ff00ff),
-                    Default::default(),
-                ),
+                ("mod.body".to_string(), Color::from_u32(0xff0000ff).into()),
+                ("fn.name".to_string(), Color::from_u32(0x00ff00ff).into()),
             ],
             ..Default::default()
         };
@@ -676,7 +668,7 @@ mod tests {
             grammar: grammar.clone(),
             highlight_query,
             brackets_query: tree_sitter::Query::new(grammar, "").unwrap(),
-            theme_mapping: Default::default(),
+            highlight_map: Default::default(),
         });
         lang.set_theme(&theme);
 
@@ -752,16 +744,8 @@ mod tests {
         .unwrap();
         let theme = Theme {
             syntax: vec![
-                (
-                    "mod.body".to_string(),
-                    Color::from_u32(0xff0000ff),
-                    Default::default(),
-                ),
-                (
-                    "fn.name".to_string(),
-                    Color::from_u32(0x00ff00ff),
-                    Default::default(),
-                ),
+                ("mod.body".to_string(), Color::from_u32(0xff0000ff).into()),
+                ("fn.name".to_string(), Color::from_u32(0x00ff00ff).into()),
             ],
             ..Default::default()
         };
@@ -774,7 +758,7 @@ mod tests {
             grammar: grammar.clone(),
             highlight_query,
             brackets_query: tree_sitter::Query::new(grammar, "").unwrap(),
-            theme_mapping: Default::default(),
+            highlight_map: Default::default(),
         });
         lang.set_theme(&theme);
 
@@ -953,7 +937,7 @@ mod tests {
         let mut snapshot = map.update(cx, |map, cx| map.snapshot(cx));
         let mut chunks: Vec<(String, Option<&str>)> = Vec::new();
         for (chunk, style_id) in snapshot.highlighted_chunks_for_rows(rows) {
-            let style_name = theme.syntax_style_name(style_id);
+            let style_name = theme.highlight_name(style_id);
             if let Some((last_chunk, last_style_name)) = chunks.last_mut() {
                 if style_name == *last_style_name {
                     last_chunk.push_str(chunk);

zed/src/editor/display_map/fold_map.rs 🔗

@@ -4,7 +4,7 @@ use super::{
 };
 use crate::{
     editor::buffer,
-    settings::StyleId,
+    settings::HighlightId,
     sum_tree::{self, Cursor, FilterCursor, SumTree},
     time,
     util::Bias,
@@ -1004,12 +1004,12 @@ impl<'a> Iterator for Chunks<'a> {
 pub struct HighlightedChunks<'a> {
     transform_cursor: Cursor<'a, Transform, FoldOffset, usize>,
     buffer_chunks: buffer::HighlightedChunks<'a>,
-    buffer_chunk: Option<(usize, &'a str, StyleId)>,
+    buffer_chunk: Option<(usize, &'a str, HighlightId)>,
     buffer_offset: usize,
 }
 
 impl<'a> Iterator for HighlightedChunks<'a> {
-    type Item = (&'a str, StyleId);
+    type Item = (&'a str, HighlightId);
 
     fn next(&mut self) -> Option<Self::Item> {
         let transform = if let Some(item) = self.transform_cursor.item() {
@@ -1031,7 +1031,7 @@ impl<'a> Iterator for HighlightedChunks<'a> {
                 self.transform_cursor.next(&());
             }
 
-            return Some((output_text, StyleId::default()));
+            return Some((output_text, HighlightId::default()));
         }
 
         // Retrieve a chunk from the current location in the buffer.

zed/src/editor/display_map/tab_map.rs 🔗

@@ -1,7 +1,7 @@
 use parking_lot::Mutex;
 
 use super::fold_map::{self, FoldEdit, FoldPoint, Snapshot as FoldSnapshot};
-use crate::{editor::rope, settings::StyleId, util::Bias};
+use crate::{editor::rope, settings::HighlightId, util::Bias};
 use std::{mem, ops::Range};
 
 pub struct TabMap(Mutex<Snapshot>);
@@ -416,14 +416,14 @@ impl<'a> Iterator for Chunks<'a> {
 pub struct HighlightedChunks<'a> {
     fold_chunks: fold_map::HighlightedChunks<'a>,
     chunk: &'a str,
-    style_id: StyleId,
+    style_id: HighlightId,
     column: usize,
     tab_size: usize,
     skip_leading_tab: bool,
 }
 
 impl<'a> Iterator for HighlightedChunks<'a> {
-    type Item = (&'a str, StyleId);
+    type Item = (&'a str, HighlightId);
 
     fn next(&mut self) -> Option<Self::Item> {
         if self.chunk.is_empty() {

zed/src/editor/display_map/wrap_map.rs 🔗

@@ -5,7 +5,7 @@ use super::{
 };
 use crate::{
     editor::Point,
-    settings::StyleId,
+    settings::HighlightId,
     sum_tree::{self, Cursor, SumTree},
     util::Bias,
     Settings,
@@ -59,7 +59,7 @@ pub struct Chunks<'a> {
 pub struct HighlightedChunks<'a> {
     input_chunks: tab_map::HighlightedChunks<'a>,
     input_chunk: &'a str,
-    style_id: StyleId,
+    style_id: HighlightId,
     output_position: WrapPoint,
     max_output_row: u32,
     transforms: Cursor<'a, Transform, WrapPoint, TabPoint>,
@@ -487,7 +487,7 @@ impl Snapshot {
         HighlightedChunks {
             input_chunks: self.tab_snapshot.highlighted_chunks(input_start..input_end),
             input_chunk: "",
-            style_id: StyleId::default(),
+            style_id: HighlightId::default(),
             output_position: output_start,
             max_output_row: rows.end,
             transforms,
@@ -670,7 +670,7 @@ impl<'a> Iterator for Chunks<'a> {
 }
 
 impl<'a> Iterator for HighlightedChunks<'a> {
-    type Item = (&'a str, StyleId);
+    type Item = (&'a str, HighlightId);
 
     fn next(&mut self) -> Option<Self::Item> {
         if self.output_position.row() >= self.max_output_row {

zed/src/file_finder.rs 🔗

@@ -141,11 +141,15 @@ impl FileFinder {
         index: usize,
         cx: &AppContext,
     ) -> Option<ElementBox> {
+        let selected_index = self.selected_index();
         let settings = self.settings.borrow();
-        let theme = &settings.theme.ui;
+        let style = if index == selected_index {
+            &settings.theme.ui.selector.active_item
+        } else {
+            &settings.theme.ui.selector.item
+        };
         self.labels_for_match(path_match, cx).map(
             |(file_name, file_name_positions, full_path, full_path_positions)| {
-                let selected_index = self.selected_index();
                 let container = Container::new(
                     Flex::row()
                         .with_child(
@@ -170,7 +174,7 @@ impl FileFinder {
                                             settings.ui_font_family,
                                             settings.ui_font_size,
                                         )
-                                        .with_style(&theme.selector.label)
+                                        .with_style(&style.label)
                                         .with_highlights(file_name_positions)
                                         .boxed(),
                                     )
@@ -180,7 +184,7 @@ impl FileFinder {
                                             settings.ui_font_family,
                                             settings.ui_font_size,
                                         )
-                                        .with_style(&theme.selector.label)
+                                        .with_style(&style.label)
                                         .with_highlights(full_path_positions)
                                         .boxed(),
                                     )
@@ -190,12 +194,7 @@ impl FileFinder {
                         )
                         .boxed(),
                 )
-                .with_uniform_padding(6.0)
-                .with_style(if index == selected_index {
-                    &theme.selector.active_item.container
-                } else {
-                    &theme.selector.item.container
-                });
+                .with_style(&style.container);
 
                 let entry = (path_match.tree_id, path_match.path.clone());
                 EventHandler::new(container.boxed())

zed/src/language.rs 🔗

@@ -1,4 +1,4 @@
-use crate::settings::{Theme, ThemeMap};
+use crate::settings::{HighlightMap, Theme};
 use parking_lot::Mutex;
 use rust_embed::RustEmbed;
 use serde::Deserialize;
@@ -27,7 +27,7 @@ pub struct Language {
     pub grammar: Grammar,
     pub highlight_query: Query,
     pub brackets_query: Query,
-    pub theme_mapping: Mutex<ThemeMap>,
+    pub highlight_map: Mutex<HighlightMap>,
 }
 
 pub struct LanguageRegistry {
@@ -35,12 +35,12 @@ pub struct LanguageRegistry {
 }
 
 impl Language {
-    pub fn theme_mapping(&self) -> ThemeMap {
-        self.theme_mapping.lock().clone()
+    pub fn highlight_map(&self) -> HighlightMap {
+        self.highlight_map.lock().clone()
     }
 
     pub fn set_theme(&self, theme: &Theme) {
-        *self.theme_mapping.lock() = ThemeMap::new(self.highlight_query.capture_names(), theme);
+        *self.highlight_map.lock() = HighlightMap::new(self.highlight_query.capture_names(), theme);
     }
 }
 
@@ -53,7 +53,7 @@ impl LanguageRegistry {
             grammar,
             highlight_query: Self::load_query(grammar, "rust/highlights.scm"),
             brackets_query: Self::load_query(grammar, "rust/brackets.scm"),
-            theme_mapping: Mutex::new(ThemeMap::default()),
+            highlight_map: Mutex::new(HighlightMap::default()),
         };
 
         Self {
@@ -114,7 +114,7 @@ mod tests {
                     grammar,
                     highlight_query: Query::new(grammar, "").unwrap(),
                     brackets_query: Query::new(grammar, "").unwrap(),
-                    theme_mapping: Default::default(),
+                    highlight_map: Default::default(),
                 }),
                 Arc::new(Language {
                     config: LanguageConfig {
@@ -125,7 +125,7 @@ mod tests {
                     grammar,
                     highlight_query: Query::new(grammar, "").unwrap(),
                     brackets_query: Query::new(grammar, "").unwrap(),
-                    theme_mapping: Default::default(),
+                    highlight_map: Default::default(),
                 }),
             ],
         };

zed/src/settings.rs 🔗

@@ -4,7 +4,7 @@ use gpui::font_cache::{FamilyId, FontCache};
 use postage::watch;
 use std::sync::Arc;
 
-pub use theme::{StyleId, Theme, ThemeMap, ThemeRegistry};
+pub use theme::{HighlightId, HighlightMap, Theme, ThemeRegistry};
 
 #[derive(Clone)]
 pub struct Settings {
@@ -48,8 +48,13 @@ pub fn channel_with_themes(
     font_cache: &FontCache,
     themes: &ThemeRegistry,
 ) -> Result<(watch::Sender<Settings>, watch::Receiver<Settings>)> {
+    let theme = match themes.get("dark") {
+        Ok(theme) => dbg!(theme),
+        Err(err) => {
+            panic!("failed to deserialize default theme: {:?}", err)
+        }
+    };
     Ok(watch::channel_with(Settings::new_with_theme(
-        font_cache,
-        themes.get("dark").expect("failed to load default theme"),
+        font_cache, theme,
     )?))
 }

zed/src/theme.rs 🔗

@@ -2,16 +2,16 @@ use anyhow::{anyhow, Context, Result};
 use gpui::{
     color::Color,
     elements::{ContainerStyle, LabelStyle},
-    fonts::{font_properties_from_json, Properties as FontProperties},
+    fonts::TextStyle,
     AssetSource,
 };
 use json::{Map, Value};
 use parking_lot::Mutex;
-use serde::{de, Deserialize, Deserializer};
+use serde::{Deserialize, Deserializer};
 use serde_json as json;
 use std::{cmp::Ordering, collections::HashMap, sync::Arc};
 
-const DEFAULT_STYLE_ID: StyleId = StyleId(u32::MAX);
+const DEFAULT_HIGHLIGHT_ID: HighlightId = HighlightId(u32::MAX);
 
 pub struct ThemeRegistry {
     assets: Box<dyn AssetSource>,
@@ -20,17 +20,17 @@ pub struct ThemeRegistry {
 }
 
 #[derive(Clone, Debug)]
-pub struct ThemeMap(Arc<[StyleId]>);
+pub struct HighlightMap(Arc<[HighlightId]>);
 
 #[derive(Clone, Copy, Debug)]
-pub struct StyleId(u32);
+pub struct HighlightId(u32);
 
 #[derive(Debug, Default, Deserialize)]
 pub struct Theme {
     pub ui: Ui,
     pub editor: Editor,
     #[serde(deserialize_with = "deserialize_syntax_theme")]
-    pub syntax: Vec<(String, Color, FontProperties)>,
+    pub syntax: Vec<(String, TextStyle)>,
 }
 
 #[derive(Debug, Default, Deserialize)]
@@ -180,6 +180,7 @@ impl ThemeRegistry {
         }
         // If you extend something with an extend directive, process the source's extend directive first
         directives.sort_unstable();
+
         // Now update objects to include the fields of objects they extend
         for ExtendDirective {
             source_path,
@@ -188,8 +189,11 @@ impl ThemeRegistry {
         {
             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);
+            if let (Value::Object(mut source_object), Value::Object(target_object)) =
+                (source, target.take())
+            {
+                deep_merge_json(&mut source_object, target_object);
+                *target = Value::Object(source_object);
             }
         }
 
@@ -213,26 +217,28 @@ impl ThemeRegistry {
 }
 
 impl Theme {
-    pub fn syntax_style(&self, id: StyleId) -> (Color, FontProperties) {
+    pub fn highlight_style(&self, id: HighlightId) -> TextStyle {
         self.syntax
             .get(id.0 as usize)
-            .map_or((self.editor.text, FontProperties::new()), |entry| {
-                (entry.1, entry.2)
+            .map(|entry| entry.1.clone())
+            .unwrap_or_else(|| TextStyle {
+                color: self.editor.text,
+                font_properties: Default::default(),
             })
     }
 
     #[cfg(test)]
-    pub fn syntax_style_name(&self, id: StyleId) -> Option<&str> {
+    pub fn highlight_name(&self, id: HighlightId) -> Option<&str> {
         self.syntax.get(id.0 as usize).map(|e| e.0.as_str())
     }
 }
 
-impl ThemeMap {
+impl HighlightMap {
     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(
+        HighlightMap(
             capture_names
                 .iter()
                 .map(|capture_name| {
@@ -240,7 +246,7 @@ impl ThemeMap {
                         .syntax
                         .iter()
                         .enumerate()
-                        .filter_map(|(i, (key, _, _))| {
+                        .filter_map(|(i, (key, _))| {
                             let mut len = 0;
                             let capture_parts = capture_name.split('.');
                             for key_part in key.split('.') {
@@ -253,29 +259,29 @@ impl ThemeMap {
                             Some((i, len))
                         })
                         .max_by_key(|(_, len)| *len)
-                        .map_or(DEFAULT_STYLE_ID, |(i, _)| StyleId(i as u32))
+                        .map_or(DEFAULT_HIGHLIGHT_ID, |(i, _)| HighlightId(i as u32))
                 })
                 .collect(),
         )
     }
 
-    pub fn get(&self, capture_id: u32) -> StyleId {
+    pub fn get(&self, capture_id: u32) -> HighlightId {
         self.0
             .get(capture_id as usize)
             .copied()
-            .unwrap_or(DEFAULT_STYLE_ID)
+            .unwrap_or(DEFAULT_HIGHLIGHT_ID)
     }
 }
 
-impl Default for ThemeMap {
+impl Default for HighlightMap {
     fn default() -> Self {
         Self(Arc::new([]))
     }
 }
 
-impl Default for StyleId {
+impl Default for HighlightId {
     fn default() -> Self {
-        DEFAULT_STYLE_ID
+        DEFAULT_HIGHLIGHT_ID
     }
 }
 
@@ -293,13 +299,13 @@ fn deep_merge_json(base: &mut Map<String, Value>, extension: Map<String, Value>)
     }
 }
 
-#[derive(Clone, PartialEq, Eq)]
+#[derive(Debug, Clone, PartialEq, Eq)]
 enum Key {
     Array(usize),
     Object(String),
 }
 
-#[derive(PartialEq, Eq)]
+#[derive(Debug, PartialEq, Eq)]
 struct ExtendDirective {
     source_path: Vec<Key>,
     target_path: Vec<Key>,
@@ -429,30 +435,17 @@ fn validate_variable_name(name: &str) -> bool {
 
 pub fn deserialize_syntax_theme<'de, D>(
     deserializer: D,
-) -> Result<Vec<(String, Color, FontProperties)>, D::Error>
+) -> Result<Vec<(String, TextStyle)>, D::Error>
 where
     D: Deserializer<'de>,
 {
-    let mut result = Vec::<(String, Color, FontProperties)>::new();
+    let mut result = Vec::<(String, TextStyle)>::new();
 
-    let syntax_data: Map<String, Value> = Deserialize::deserialize(deserializer)?;
+    let syntax_data: HashMap<String, TextStyle> = Deserialize::deserialize(deserializer)?;
     for (key, style) in syntax_data {
-        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()).map_err(de::Error::custom)?;
-                }
-                properties = font_properties_from_json(style).map_err(de::Error::custom)?;
-            }
-            _ => {
-                color = serde_json::from_value(style.clone()).map_err(de::Error::custom)?;
-            }
-        }
-        match result.binary_search_by(|(needle, _, _)| needle.cmp(&key)) {
+        match result.binary_search_by(|(needle, _)| needle.cmp(&key)) {
             Ok(i) | Err(i) => {
-                result.insert(i, (key, color, properties));
+                result.insert(i, (key, style));
             }
         }
     }
@@ -463,131 +456,95 @@ where
 #[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() {
+    fn test_theme_extension() {
         let assets = TestAssets(&[
             (
                 "themes/_base.toml",
-                r#"
-                abstract = true
+                r##"
+                [ui.active_tab]
+                extends = "ui.tab"
+                border.color = "#666666"
+                text = "$bright_text"
 
                 [ui.tab]
-                background = 0x111111
-                text = "$variable_1"
+                extends = "ui.element"
+                text = "$dull_text"
+
+                [ui.element]
+                background = "#111111"
+                border = {width = 2.0, color = "#00000000"}
 
                 [editor]
-                background = 0x222222
-                default_text = "$variable_2"
-                "#,
+                background = "#222222"
+                default_text = "$regular_text"
+                "##,
             ),
             (
                 "themes/light.toml",
-                r#"
+                r##"
                 extends = "_base"
 
                 [variables]
-                variable_1 = 0x333333
-                variable_2 = 0x444444
-
-                [ui.tab]
-                background = 0x555555
+                bright_text = "#ffffff"
+                regular_text = "#eeeeee"
+                dull_text = "#dddddd"
 
                 [editor]
-                background = 0x666666
-                "#,
-            ),
-            (
-                "themes/dark.toml",
-                r#"
-                extends = "_base"
-
-                [variables]
-                variable_1 = 0x555555
-                variable_2 = 0x666666
-                "#,
+                background = "#232323"
+                "##,
             ),
         ]);
 
         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));
-
+        let theme_data = registry.load("light").unwrap();
         assert_eq!(
-            registry.list().collect::<Vec<_>>(),
-            &["light".to_string(), "dark".to_string()]
+            theme_data.as_ref(),
+            &serde_json::json!({
+              "ui": {
+                "active_tab": {
+                  "background": "#111111",
+                  "border": {
+                    "width": 2.0,
+                    "color": "#666666"
+                  },
+                  "extends": "ui.tab",
+                  "text": "#ffffff"
+                },
+                "tab": {
+                  "background": "#111111",
+                  "border": {
+                    "width": 2.0,
+                    "color": "#00000000"
+                  },
+                  "extends": "ui.element",
+                  "text": "#dddddd"
+                },
+                "element": {
+                  "background": "#111111",
+                  "border": {
+                    "width": 2.0,
+                    "color": "#00000000"
+                  }
+                }
+              },
+              "editor": {
+                "background": "#232323",
+                "default_text": "#eeeeee"
+              },
+              "extends": "_base",
+              "variables": {
+                "bright_text": "#ffffff",
+                "regular_text": "#eeeeee",
+                "dull_text": "#dddddd"
+              }
+            })
         );
     }
 
     #[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() {
+    fn test_highlight_map() {
         let theme = Theme {
             ui: Default::default(),
             editor: Default::default(),
@@ -600,7 +557,7 @@ mod tests {
                 ("variable", Color::from_u32(0x600000ff)),
             ]
             .iter()
-            .map(|e| (e.0.to_string(), e.1, FontProperties::new()))
+            .map(|(name, color)| (name.to_string(), (*color).into()))
             .collect(),
         };
 
@@ -610,13 +567,10 @@ mod tests {
             "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")
-        );
+        let map = HighlightMap::new(capture_names, &theme);
+        assert_eq!(theme.highlight_name(map.get(0)), Some("function"));
+        assert_eq!(theme.highlight_name(map.get(1)), Some("function.async"));
+        assert_eq!(theme.highlight_name(map.get(2)), Some("variable.builtin"));
     }
 
     struct TestAssets(&'static [(&'static str, &'static str)]);