Port theme settings :yolo:

Conrad Irwin and Ben Kunkle created

Co-authored-by: Ben Kunkle <ben@zed.dev>

Change summary

crates/settings/src/base_keymap_setting.rs    |    5 
crates/settings/src/settings_content.rs       |    3 
crates/settings/src/settings_content/theme.rs |   89 +
crates/settings/src/settings_store.rs         |   55 
crates/theme/src/schema.rs                    | 1100 +++-----------------
crates/theme/src/settings.rs                  |  755 +++++---------
crates/theme/src/styles/accents.rs            |    2 
crates/theme/src/styles/players.rs            |    2 
crates/theme/src/theme.rs                     |    5 
crates/title_bar/src/title_bar_settings.rs    |    2 
crates/util/src/util.rs                       |    8 
11 files changed, 599 insertions(+), 1,427 deletions(-)

Detailed changes

crates/settings/src/base_keymap_setting.rs 🔗

@@ -4,6 +4,7 @@ use crate::{
     self as settings,
     settings_content::{self, BaseKeymapContent, SettingsContent},
 };
+use gpui::App;
 use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
 use settings::{Settings, VsCodeSettings};
@@ -138,11 +139,11 @@ pub struct BaseKeymapSetting {
 }
 
 impl Settings for BaseKeymap {
-    fn from_file(s: &crate::settings_content::SettingsContent) -> Option<Self> {
+    fn from_file(s: &crate::settings_content::SettingsContent, _cx: &mut App) -> Option<Self> {
         s.base_keymap.map(Into::into)
     }
 
-    fn refine(&mut self, s: &settings_content::SettingsContent) {
+    fn refine(&mut self, s: &settings_content::SettingsContent, _cx: &mut App) {
         if let Some(base_keymap) = s.base_keymap {
             *self = base_keymap.into();
         };

crates/settings/src/settings_content.rs 🔗

@@ -18,6 +18,9 @@ pub struct SettingsContent {
     #[serde(flatten)]
     pub project: ProjectSettingsContent,
 
+    #[serde(flatten)]
+    pub theme: ThemeSettingsContent,
+
     pub base_keymap: Option<BaseKeymapContent>,
 
     pub auto_update: Option<bool>,

crates/settings/src/settings_content/theme.rs 🔗

@@ -1,5 +1,5 @@
 use collections::{HashMap, IndexMap};
-use gpui::{FontFallbacks, FontFeatures};
+use gpui::{FontFallbacks, FontFeatures, FontStyle, FontWeight};
 use schemars::{JsonSchema, JsonSchema_repr, json_schema};
 use serde::{Deserialize, Deserializer, Serialize};
 use serde_json::Value;
@@ -999,6 +999,16 @@ pub enum WindowBackgroundContent {
     Blurred,
 }
 
+impl Into<gpui::WindowBackgroundAppearance> for WindowBackgroundContent {
+    fn into(self) -> gpui::WindowBackgroundAppearance {
+        match self {
+            WindowBackgroundContent::Opaque => gpui::WindowBackgroundAppearance::Opaque,
+            WindowBackgroundContent::Transparent => gpui::WindowBackgroundAppearance::Transparent,
+            WindowBackgroundContent::Blurred => gpui::WindowBackgroundAppearance::Blurred,
+        }
+    }
+}
+
 #[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, PartialEq)]
 #[serde(rename_all = "snake_case")]
 pub enum FontStyleContent {
@@ -1007,6 +1017,16 @@ pub enum FontStyleContent {
     Oblique,
 }
 
+impl From<FontStyleContent> for FontStyle {
+    fn from(value: FontStyleContent) -> Self {
+        match value {
+            FontStyleContent::Normal => FontStyle::Normal,
+            FontStyleContent::Italic => FontStyle::Italic,
+            FontStyleContent::Oblique => FontStyle::Oblique,
+        }
+    }
+}
+
 #[derive(Debug, Clone, Copy, Serialize_repr, Deserialize_repr, JsonSchema_repr, PartialEq)]
 #[repr(u16)]
 pub enum FontWeightContent {
@@ -1020,3 +1040,70 @@ pub enum FontWeightContent {
     ExtraBold = 800,
     Black = 900,
 }
+
+impl From<FontWeightContent> for FontWeight {
+    fn from(value: FontWeightContent) -> Self {
+        match value {
+            FontWeightContent::Thin => FontWeight::THIN,
+            FontWeightContent::ExtraLight => FontWeight::EXTRA_LIGHT,
+            FontWeightContent::Light => FontWeight::LIGHT,
+            FontWeightContent::Normal => FontWeight::NORMAL,
+            FontWeightContent::Medium => FontWeight::MEDIUM,
+            FontWeightContent::Semibold => FontWeight::SEMIBOLD,
+            FontWeightContent::Bold => FontWeight::BOLD,
+            FontWeightContent::ExtraBold => FontWeight::EXTRA_BOLD,
+            FontWeightContent::Black => FontWeight::BLACK,
+        }
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use serde_json::json;
+
+    #[test]
+    fn test_buffer_line_height_deserialize_valid() {
+        assert_eq!(
+            serde_json::from_value::<BufferLineHeight>(json!("comfortable")).unwrap(),
+            BufferLineHeight::Comfortable
+        );
+        assert_eq!(
+            serde_json::from_value::<BufferLineHeight>(json!("standard")).unwrap(),
+            BufferLineHeight::Standard
+        );
+        assert_eq!(
+            serde_json::from_value::<BufferLineHeight>(json!({"custom": 1.0})).unwrap(),
+            BufferLineHeight::Custom(1.0)
+        );
+        assert_eq!(
+            serde_json::from_value::<BufferLineHeight>(json!({"custom": 1.5})).unwrap(),
+            BufferLineHeight::Custom(1.5)
+        );
+    }
+
+    #[test]
+    fn test_buffer_line_height_deserialize_invalid() {
+        assert!(
+            serde_json::from_value::<BufferLineHeight>(json!({"custom": 0.99}))
+                .err()
+                .unwrap()
+                .to_string()
+                .contains("buffer_line_height.custom must be at least 1.0")
+        );
+        assert!(
+            serde_json::from_value::<BufferLineHeight>(json!({"custom": 0.0}))
+                .err()
+                .unwrap()
+                .to_string()
+                .contains("buffer_line_height.custom must be at least 1.0")
+        );
+        assert!(
+            serde_json::from_value::<BufferLineHeight>(json!({"custom": -1.0}))
+                .err()
+                .unwrap()
+                .to_string()
+                .contains("buffer_line_height.custom must be at least 1.0")
+        );
+    }
+}

crates/settings/src/settings_store.rs 🔗

@@ -21,16 +21,13 @@ use std::{
     str::{self, FromStr},
     sync::Arc,
 };
-use util::{
-    ResultExt as _, merge_non_null_json_value_into,
-};
+use util::{ResultExt as _, merge_non_null_json_value_into};
 
 pub type EditorconfigProperties = ec4rs::Properties;
 
 use crate::{
-    ActiveSettingsProfileName, SettingsJsonSchemaParams, SettingsUiEntry,
-    VsCodeSettings, WorktreeId, parse_json_with_comments,
-    replace_value_in_json_text,
+    ActiveSettingsProfileName, SettingsJsonSchemaParams, SettingsUiEntry, VsCodeSettings,
+    WorktreeId, parse_json_with_comments, replace_value_in_json_text,
     settings_content::{
         ExtensionsSettingsContent, ProjectSettingsContent, ServerSettingsContent, SettingsContent,
         UserSettingsContent,
@@ -60,9 +57,9 @@ pub trait Settings: 'static + Send + Sync + Sized {
     /// user settings match the current version of the settings.
     const PRESERVED_KEYS: Option<&'static [&'static str]> = None;
 
-    fn from_file(content: &SettingsContent) -> Option<Self>;
+    fn from_file(content: &SettingsContent, cx: &mut App) -> Option<Self>;
 
-    fn refine(&mut self, content: &SettingsContent);
+    fn refine(&mut self, content: &SettingsContent, cx: &mut App);
 
     fn missing_default() -> anyhow::Error {
         anyhow::anyhow!("missing default for: {}", std::any::type_name::<Self>())
@@ -252,8 +249,8 @@ struct SettingValue<T> {
 trait AnySettingValue: 'static + Send + Sync {
     fn setting_type_name(&self) -> &'static str;
 
-    fn from_file(&self, s: &SettingsContent) -> Option<Box<dyn Any>>;
-    fn refine(&self, value: &mut dyn Any, s: &[&SettingsContent]);
+    fn from_file(&self, s: &SettingsContent, cx: &mut App) -> Option<Box<dyn Any>>;
+    fn refine(&self, value: &mut dyn Any, s: &[&SettingsContent], cx: &mut App);
 
     fn value_for_path(&self, path: Option<SettingsLocation>) -> &dyn Any;
     fn all_local_values(&self) -> Vec<(WorktreeId, Arc<Path>, &dyn Any)>;
@@ -347,9 +344,9 @@ impl SettingsStore {
             refinements.push(server_settings)
         }
         // todo!() unwrap...
-        let mut value = T::from_file(&self.default_settings).unwrap();
+        let mut value = T::from_file(&self.default_settings, cx).unwrap();
         for refinement in refinements {
-            value.refine(refinement)
+            value.refine(refinement, cx)
         }
 
         setting_value.set_global_value(Box::new(value));
@@ -1114,8 +1111,8 @@ impl SettingsStore {
         for setting_value in self.setting_values.values_mut() {
             // If the global settings file changed, reload the global value for the field.
             if changed_local_path.is_none() {
-                let mut value = setting_value.from_file(&self.default_settings).unwrap();
-                setting_value.refine(value.as_mut(), &refinements);
+                let mut value = setting_value.from_file(&self.default_settings, cx).unwrap();
+                setting_value.refine(value.as_mut(), &refinements, cx);
                 setting_value.set_global_value(value);
             }
 
@@ -1137,7 +1134,7 @@ impl SettingsStore {
 
                 // NOTE: this kind of condition existing in the old code too,
                 // but is there a problem when a setting is removed from a file?
-                if setting_value.from_file(local_settings).is_some() {
+                if setting_value.from_file(local_settings, cx).is_some() {
                     paths_stack.push(Some((*root_id, directory_path.as_ref())));
                     project_settings_stack.push(local_settings);
 
@@ -1150,9 +1147,9 @@ impl SettingsStore {
                         continue;
                     }
 
-                    let mut value = setting_value.from_file(&self.default_settings).unwrap();
-                    setting_value.refine(value.as_mut(), &refinements);
-                    setting_value.refine(value.as_mut(), &project_settings_stack);
+                    let mut value = setting_value.from_file(&self.default_settings, cx).unwrap();
+                    setting_value.refine(value.as_mut(), &refinements, cx);
+                    setting_value.refine(value.as_mut(), &project_settings_stack, cx);
                     setting_value.set_local_value(*root_id, directory_path.clone(), value);
                 }
             }
@@ -1235,16 +1232,16 @@ impl Debug for SettingsStore {
 }
 
 impl<T: Settings> AnySettingValue for SettingValue<T> {
-    fn from_file(&self, s: &SettingsContent) -> Option<Box<dyn Any>> {
+    fn from_file(&self, s: &SettingsContent, cx: &mut App) -> Option<Box<dyn Any>> {
         (type_name::<T>(), TypeId::of::<T>());
-        T::from_file(s).map(|result| Box::new(result) as _)
+        T::from_file(s, cx).map(|result| Box::new(result) as _)
     }
 
-    fn refine(&self, value: &mut dyn Any, refinements: &[&SettingsContent]) {
+    fn refine(&self, value: &mut dyn Any, refinements: &[&SettingsContent], cx: &mut App) {
         (type_name::<T>(), TypeId::of::<T>());
         let value = value.downcast_mut::<T>().unwrap();
         for refinement in refinements {
-            value.refine(refinement)
+            value.refine(refinement, cx)
         }
     }
 
@@ -1338,7 +1335,7 @@ impl<T: Settings> AnySettingValue for SettingValue<T> {
 #[cfg(test)]
 mod tests {
     use crate::{
-        TitleBarSettingsContent, TitleBarVisibilityContent, VsCodeSettingsSource,
+        TitleBarSettingsContent, TitleBarVisibilityContent, VsCodeSettingsSource, default_settings,
         settings_content::LanguageSettingsContent, test_settings,
     };
 
@@ -1348,7 +1345,7 @@ mod tests {
     use serde::Deserialize;
     use settings_ui_macros::{SettingsKey, SettingsUi};
     use unindent::Unindent;
-    use util::Refine;
+    use util::MergeFrom;
 
     #[derive(Debug, PartialEq)]
     struct AutoUpdateSetting {
@@ -1356,13 +1353,13 @@ mod tests {
     }
 
     impl Settings for AutoUpdateSetting {
-        fn from_file(content: &SettingsContent) -> Option<Self> {
+        fn from_file(content: &SettingsContent, _: &mut App) -> Option<Self> {
             content
                 .auto_update
                 .map(|auto_update| AutoUpdateSetting { auto_update })
         }
 
-        fn refine(&mut self, content: &SettingsContent) {
+        fn refine(&mut self, content: &SettingsContent, _: &mut App) {
             if let Some(auto_update) = content.auto_update {
                 self.auto_update = auto_update;
             }
@@ -1377,18 +1374,18 @@ mod tests {
     }
 
     impl Settings for TitleBarSettings {
-        fn from_file(content: &SettingsContent) -> Option<Self> {
+        fn from_file(content: &SettingsContent, _: &mut App) -> Option<Self> {
             let content = content.title_bar?;
             Some(TitleBarSettings {
                 show: content.show?,
             })
         }
 
-        fn refine(&mut self, content: &SettingsContent) {
+        fn refine(&mut self, content: &SettingsContent, _: &mut App) {
             let Some(content) = content.title_bar else {
                 return;
             };
-            self.show.refine(&content.show)
+            self.show.merge_from(&content.show)
         }
 
         fn import_from_vscode(_: &VsCodeSettings, _: &mut SettingsContent) {}

crates/theme/src/schema.rs 🔗

@@ -11,21 +11,6 @@ use serde_repr::{Deserialize_repr, Serialize_repr};
 
 use crate::{StatusColorsRefinement, ThemeColorsRefinement};
 
-pub(crate) fn try_parse_color(color: &str) -> Result<Hsla> {
-    let rgba = gpui::Rgba::try_from(color)?;
-    let rgba = palette::rgb::Srgba::from_components((rgba.r, rgba.g, rgba.b, rgba.a));
-    let hsla = palette::Hsla::from_color(rgba);
-
-    let hsla = gpui::hsla(
-        hsla.hue.into_positive_degrees() / 360.,
-        hsla.saturation,
-        hsla.lightness,
-        hsla.alpha,
-    );
-
-    Ok(hsla)
-}
-
 fn ensure_non_opaque(color: Hsla) -> Hsla {
     const MAXIMUM_OPACITY: f32 = 0.7;
     if color.a <= MAXIMUM_OPACITY {
@@ -81,7 +66,7 @@ pub struct ThemeFamilyContent {
 pub struct ThemeContent {
     pub name: String,
     pub appearance: AppearanceContent,
-    pub style: ThemeStyleContent,
+    pub style: settings::ThemeStyleContent,
 }
 
 /// The content of a serialized theme.
@@ -95,909 +80,214 @@ pub struct ThemeStyleContent {
     pub accents: Vec<AccentContent>,
 
     #[serde(flatten, default)]
-    pub colors: ThemeColorsContent,
+    pub colors: settings::ThemeColorsContent,
 
     #[serde(flatten, default)]
-    pub status: StatusColorsContent,
+    pub status: settings::StatusColorsContent,
 
     #[serde(default)]
     pub players: Vec<PlayerColorContent>,
 
     /// The styles for syntax nodes.
     #[serde(default)]
-    pub syntax: IndexMap<String, HighlightStyleContent>,
+    pub syntax: IndexMap<String, settings::HighlightStyleContent>,
 }
 
-impl ThemeStyleContent {
-    /// Returns a [`ThemeColorsRefinement`] based on the colors in the [`ThemeContent`].
-    #[inline(always)]
-    pub fn theme_colors_refinement(&self) -> ThemeColorsRefinement {
-        self.colors
-            .theme_colors_refinement(&self.status_colors_refinement())
-    }
-
-    /// Returns a [`StatusColorsRefinement`] based on the colors in the [`ThemeContent`].
-    #[inline(always)]
-    pub fn status_colors_refinement(&self) -> StatusColorsRefinement {
-        self.status.status_colors_refinement()
-    }
-
-    /// Returns the syntax style overrides in the [`ThemeContent`].
-    pub fn syntax_overrides(&self) -> Vec<(String, HighlightStyle)> {
-        self.syntax
-            .iter()
-            .map(|(key, style)| {
-                (
-                    key.clone(),
-                    HighlightStyle {
-                        color: style
-                            .color
-                            .as_ref()
-                            .and_then(|color| try_parse_color(color).ok()),
-                        background_color: style
-                            .background_color
-                            .as_ref()
-                            .and_then(|color| try_parse_color(color).ok()),
-                        font_style: style.font_style.map(FontStyle::from),
-                        font_weight: style.font_weight.map(FontWeight::from),
-                        ..Default::default()
-                    },
-                )
-            })
-            .collect()
-    }
+/// Returns the syntax style overrides in the [`ThemeContent`].
+pub fn syntax_overrides(this: &settings::ThemeStyleContent) -> Vec<(String, HighlightStyle)> {
+    this.syntax
+        .iter()
+        .map(|(key, style)| {
+            (
+                key.clone(),
+                HighlightStyle {
+                    color: style
+                        .color
+                        .as_ref()
+                        .and_then(|color| try_parse_color(color).ok()),
+                    background_color: style
+                        .background_color
+                        .as_ref()
+                        .and_then(|color| try_parse_color(color).ok()),
+                    font_style: style.font_style.map(FontStyle::from),
+                    font_weight: style.font_weight.map(FontWeight::from),
+                    ..Default::default()
+                },
+            )
+        })
+        .collect()
 }
 
-pub(crate) fn try_parse_color(color: &str) -> Result<Hsla> {
-    let rgba = gpui::Rgba::try_from(color)?;
-    let rgba = palette::rgb::Srgba::from_components((rgba.r, rgba.g, rgba.b, rgba.a));
-    let hsla = palette::Hsla::from_color(rgba);
-
-    let hsla = gpui::hsla(
-        hsla.hue.into_positive_degrees() / 360.,
-        hsla.saturation,
-        hsla.lightness,
-        hsla.alpha,
-    );
-
-    Ok(hsla)
-}
-
-impl ThemeColorsContent {
-    /// Returns a [`ThemeColorsRefinement`] based on the colors in the [`ThemeColorsContent`].
-    pub fn theme_colors_refinement(
-        &self,
-        status_colors: &StatusColorsRefinement,
-    ) -> ThemeColorsRefinement {
-        let border = self
-            .border
-            .as_ref()
-            .and_then(|color| try_parse_color(color).ok());
-        let editor_document_highlight_read_background = self
-            .editor_document_highlight_read_background
-            .as_ref()
-            .and_then(|color| try_parse_color(color).ok());
-        let scrollbar_thumb_background = self
-            .scrollbar_thumb_background
+pub fn status_colors_refinement(colors: &settings::StatusColorsContent) -> StatusColorsRefinement {
+    StatusColorsRefinement {
+        conflict: colors
+            .conflict
             .as_ref()
-            .and_then(|color| try_parse_color(color).ok())
-            .or_else(|| {
-                self.deprecated_scrollbar_thumb_background
-                    .as_ref()
-                    .and_then(|color| try_parse_color(color).ok())
-            });
-        let scrollbar_thumb_hover_background = self
-            .scrollbar_thumb_hover_background
-            .as_ref()
-            .and_then(|color| try_parse_color(color).ok());
-        let scrollbar_thumb_active_background = self
-            .scrollbar_thumb_active_background
+            .and_then(|color| try_parse_color(color).ok()),
+        conflict_background: colors
+            .conflict_background
             .as_ref()
-            .and_then(|color| try_parse_color(color).ok())
-            .or(scrollbar_thumb_background);
-        let scrollbar_thumb_border = self
-            .scrollbar_thumb_border
-            .as_ref()
-            .and_then(|color| try_parse_color(color).ok());
-        let element_hover = self
-            .element_hover
-            .as_ref()
-            .and_then(|color| try_parse_color(color).ok());
-        let panel_background = self
-            .panel_background
-            .as_ref()
-            .and_then(|color| try_parse_color(color).ok());
-        ThemeColorsRefinement {
-            border,
-            border_variant: self
-                .border_variant
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            border_focused: self
-                .border_focused
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            border_selected: self
-                .border_selected
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            border_transparent: self
-                .border_transparent
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            border_disabled: self
-                .border_disabled
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            elevated_surface_background: self
-                .elevated_surface_background
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            surface_background: self
-                .surface_background
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            background: self
-                .background
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            element_background: self
-                .element_background
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            element_hover,
-            element_active: self
-                .element_active
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            element_selected: self
-                .element_selected
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            element_disabled: self
-                .element_disabled
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            element_selection_background: self
-                .element_selection_background
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            drop_target_background: self
-                .drop_target_background
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            drop_target_border: self
-                .drop_target_border
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            ghost_element_background: self
-                .ghost_element_background
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            ghost_element_hover: self
-                .ghost_element_hover
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            ghost_element_active: self
-                .ghost_element_active
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            ghost_element_selected: self
-                .ghost_element_selected
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            ghost_element_disabled: self
-                .ghost_element_disabled
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            text: self
-                .text
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            text_muted: self
-                .text_muted
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            text_placeholder: self
-                .text_placeholder
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            text_disabled: self
-                .text_disabled
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            text_accent: self
-                .text_accent
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            icon: self
-                .icon
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            icon_muted: self
-                .icon_muted
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            icon_disabled: self
-                .icon_disabled
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            icon_placeholder: self
-                .icon_placeholder
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            icon_accent: self
-                .icon_accent
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            debugger_accent: self
-                .debugger_accent
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            status_bar_background: self
-                .status_bar_background
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            title_bar_background: self
-                .title_bar_background
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            title_bar_inactive_background: self
-                .title_bar_inactive_background
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            toolbar_background: self
-                .toolbar_background
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            tab_bar_background: self
-                .tab_bar_background
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            tab_inactive_background: self
-                .tab_inactive_background
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            tab_active_background: self
-                .tab_active_background
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            search_match_background: self
-                .search_match_background
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            panel_background,
-            panel_focused_border: self
-                .panel_focused_border
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            panel_indent_guide: self
-                .panel_indent_guide
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            panel_indent_guide_hover: self
-                .panel_indent_guide_hover
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            panel_indent_guide_active: self
-                .panel_indent_guide_active
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            panel_overlay_background: self
-                .panel_overlay_background
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok())
-                .or(panel_background.map(ensure_opaque)),
-            panel_overlay_hover: self
-                .panel_overlay_hover
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok())
-                .or(panel_background
-                    .zip(element_hover)
-                    .map(|(panel_bg, hover_bg)| panel_bg.blend(hover_bg))
-                    .map(ensure_opaque)),
-            pane_focused_border: self
-                .pane_focused_border
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            pane_group_border: self
-                .pane_group_border
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok())
-                .or(border),
-            scrollbar_thumb_background,
-            scrollbar_thumb_hover_background,
-            scrollbar_thumb_active_background,
-            scrollbar_thumb_border,
-            scrollbar_track_background: self
-                .scrollbar_track_background
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            scrollbar_track_border: self
-                .scrollbar_track_border
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            minimap_thumb_background: self
-                .minimap_thumb_background
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok())
-                .or(scrollbar_thumb_background.map(ensure_non_opaque)),
-            minimap_thumb_hover_background: self
-                .minimap_thumb_hover_background
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok())
-                .or(scrollbar_thumb_hover_background.map(ensure_non_opaque)),
-            minimap_thumb_active_background: self
-                .minimap_thumb_active_background
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok())
-                .or(scrollbar_thumb_active_background.map(ensure_non_opaque)),
-            minimap_thumb_border: self
-                .minimap_thumb_border
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok())
-                .or(scrollbar_thumb_border),
-            editor_foreground: self
-                .editor_foreground
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            editor_background: self
-                .editor_background
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            editor_gutter_background: self
-                .editor_gutter_background
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            editor_subheader_background: self
-                .editor_subheader_background
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            editor_active_line_background: self
-                .editor_active_line_background
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            editor_highlighted_line_background: self
-                .editor_highlighted_line_background
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            editor_debugger_active_line_background: self
-                .editor_debugger_active_line_background
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            editor_line_number: self
-                .editor_line_number
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            editor_hover_line_number: self
-                .editor_hover_line_number
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            editor_active_line_number: self
-                .editor_active_line_number
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            editor_invisible: self
-                .editor_invisible
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            editor_wrap_guide: self
-                .editor_wrap_guide
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            editor_active_wrap_guide: self
-                .editor_active_wrap_guide
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            editor_indent_guide: self
-                .editor_indent_guide
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            editor_indent_guide_active: self
-                .editor_indent_guide_active
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            editor_document_highlight_read_background,
-            editor_document_highlight_write_background: self
-                .editor_document_highlight_write_background
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            editor_document_highlight_bracket_background: self
-                .editor_document_highlight_bracket_background
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok())
-                // Fall back to `editor.document_highlight.read_background`, for backwards compatibility.
-                .or(editor_document_highlight_read_background),
-            terminal_background: self
-                .terminal_background
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            terminal_ansi_background: self
-                .terminal_ansi_background
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            terminal_foreground: self
-                .terminal_foreground
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            terminal_bright_foreground: self
-                .terminal_bright_foreground
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            terminal_dim_foreground: self
-                .terminal_dim_foreground
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            terminal_ansi_black: self
-                .terminal_ansi_black
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            terminal_ansi_bright_black: self
-                .terminal_ansi_bright_black
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            terminal_ansi_dim_black: self
-                .terminal_ansi_dim_black
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            terminal_ansi_red: self
-                .terminal_ansi_red
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            terminal_ansi_bright_red: self
-                .terminal_ansi_bright_red
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            terminal_ansi_dim_red: self
-                .terminal_ansi_dim_red
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            terminal_ansi_green: self
-                .terminal_ansi_green
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            terminal_ansi_bright_green: self
-                .terminal_ansi_bright_green
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            terminal_ansi_dim_green: self
-                .terminal_ansi_dim_green
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            terminal_ansi_yellow: self
-                .terminal_ansi_yellow
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            terminal_ansi_bright_yellow: self
-                .terminal_ansi_bright_yellow
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            terminal_ansi_dim_yellow: self
-                .terminal_ansi_dim_yellow
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            terminal_ansi_blue: self
-                .terminal_ansi_blue
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            terminal_ansi_bright_blue: self
-                .terminal_ansi_bright_blue
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            terminal_ansi_dim_blue: self
-                .terminal_ansi_dim_blue
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            terminal_ansi_magenta: self
-                .terminal_ansi_magenta
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            terminal_ansi_bright_magenta: self
-                .terminal_ansi_bright_magenta
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            terminal_ansi_dim_magenta: self
-                .terminal_ansi_dim_magenta
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            terminal_ansi_cyan: self
-                .terminal_ansi_cyan
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            terminal_ansi_bright_cyan: self
-                .terminal_ansi_bright_cyan
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            terminal_ansi_dim_cyan: self
-                .terminal_ansi_dim_cyan
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            terminal_ansi_white: self
-                .terminal_ansi_white
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            terminal_ansi_bright_white: self
-                .terminal_ansi_bright_white
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            terminal_ansi_dim_white: self
-                .terminal_ansi_dim_white
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            link_text_hover: self
-                .link_text_hover
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            version_control_added: self
-                .version_control_added
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok())
-                // Fall back to `created`, for backwards compatibility.
-                .or(status_colors.created),
-            version_control_deleted: self
-                .version_control_deleted
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok())
-                // Fall back to `deleted`, for backwards compatibility.
-                .or(status_colors.deleted),
-            version_control_modified: self
-                .version_control_modified
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok())
-                // Fall back to `modified`, for backwards compatibility.
-                .or(status_colors.modified),
-            version_control_renamed: self
-                .version_control_renamed
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok())
-                // Fall back to `modified`, for backwards compatibility.
-                .or(status_colors.modified),
-            version_control_conflict: self
-                .version_control_conflict
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok())
-                // Fall back to `ignored`, for backwards compatibility.
-                .or(status_colors.ignored),
-            version_control_ignored: self
-                .version_control_ignored
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok())
-                // Fall back to `conflict`, for backwards compatibility.
-                .or(status_colors.ignored),
-            #[allow(deprecated)]
-            version_control_conflict_marker_ours: self
-                .version_control_conflict_marker_ours
-                .as_ref()
-                .or(self.version_control_conflict_ours_background.as_ref())
-                .and_then(|color| try_parse_color(color).ok()),
-            #[allow(deprecated)]
-            version_control_conflict_marker_theirs: self
-                .version_control_conflict_marker_theirs
-                .as_ref()
-                .or(self.version_control_conflict_theirs_background.as_ref())
-                .and_then(|color| try_parse_color(color).ok()),
-        }
-    }
-}
-
-#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema, PartialEq)]
-#[serde(default)]
-pub struct StatusColorsContent {
-    /// Indicates some kind of conflict, like a file changed on disk while it was open, or
-    /// merge conflicts in a Git repository.
-    #[serde(rename = "conflict")]
-    pub conflict: Option<String>,
-
-    #[serde(rename = "conflict.background")]
-    pub conflict_background: Option<String>,
-
-    #[serde(rename = "conflict.border")]
-    pub conflict_border: Option<String>,
-
-    /// Indicates something new, like a new file added to a Git repository.
-    #[serde(rename = "created")]
-    pub created: Option<String>,
-
-    #[serde(rename = "created.background")]
-    pub created_background: Option<String>,
-
-    #[serde(rename = "created.border")]
-    pub created_border: Option<String>,
-
-    /// Indicates that something no longer exists, like a deleted file.
-    #[serde(rename = "deleted")]
-    pub deleted: Option<String>,
-
-    #[serde(rename = "deleted.background")]
-    pub deleted_background: Option<String>,
-
-    #[serde(rename = "deleted.border")]
-    pub deleted_border: Option<String>,
-
-    /// Indicates a system error, a failed operation or a diagnostic error.
-    #[serde(rename = "error")]
-    pub error: Option<String>,
-
-    #[serde(rename = "error.background")]
-    pub error_background: Option<String>,
-
-    #[serde(rename = "error.border")]
-    pub error_border: Option<String>,
-
-    /// Represents a hidden status, such as a file being hidden in a file tree.
-    #[serde(rename = "hidden")]
-    pub hidden: Option<String>,
-
-    #[serde(rename = "hidden.background")]
-    pub hidden_background: Option<String>,
-
-    #[serde(rename = "hidden.border")]
-    pub hidden_border: Option<String>,
-
-    /// Indicates a hint or some kind of additional information.
-    #[serde(rename = "hint")]
-    pub hint: Option<String>,
-
-    #[serde(rename = "hint.background")]
-    pub hint_background: Option<String>,
-
-    #[serde(rename = "hint.border")]
-    pub hint_border: Option<String>,
-
-    /// Indicates that something is deliberately ignored, such as a file or operation ignored by Git.
-    #[serde(rename = "ignored")]
-    pub ignored: Option<String>,
-
-    #[serde(rename = "ignored.background")]
-    pub ignored_background: Option<String>,
-
-    #[serde(rename = "ignored.border")]
-    pub ignored_border: Option<String>,
-
-    /// Represents informational status updates or messages.
-    #[serde(rename = "info")]
-    pub info: Option<String>,
-
-    #[serde(rename = "info.background")]
-    pub info_background: Option<String>,
-
-    #[serde(rename = "info.border")]
-    pub info_border: Option<String>,
-
-    /// Indicates a changed or altered status, like a file that has been edited.
-    #[serde(rename = "modified")]
-    pub modified: Option<String>,
-
-    #[serde(rename = "modified.background")]
-    pub modified_background: Option<String>,
-
-    #[serde(rename = "modified.border")]
-    pub modified_border: Option<String>,
-
-    /// Indicates something that is predicted, like automatic code completion, or generated code.
-    #[serde(rename = "predictive")]
-    pub predictive: Option<String>,
-
-    #[serde(rename = "predictive.background")]
-    pub predictive_background: Option<String>,
-
-    #[serde(rename = "predictive.border")]
-    pub predictive_border: Option<String>,
-
-    /// Represents a renamed status, such as a file that has been renamed.
-    #[serde(rename = "renamed")]
-    pub renamed: Option<String>,
-
-    #[serde(rename = "renamed.background")]
-    pub renamed_background: Option<String>,
-
-    #[serde(rename = "renamed.border")]
-    pub renamed_border: Option<String>,
-
-    /// Indicates a successful operation or task completion.
-    #[serde(rename = "success")]
-    pub success: Option<String>,
-
-    #[serde(rename = "success.background")]
-    pub success_background: Option<String>,
-
-    #[serde(rename = "success.border")]
-    pub success_border: Option<String>,
-
-    /// Indicates some kind of unreachable status, like a block of code that can never be reached.
-    #[serde(rename = "unreachable")]
-    pub unreachable: Option<String>,
-
-    #[serde(rename = "unreachable.background")]
-    pub unreachable_background: Option<String>,
-
-    #[serde(rename = "unreachable.border")]
-    pub unreachable_border: Option<String>,
-
-    /// Represents a warning status, like an operation that is about to fail.
-    #[serde(rename = "warning")]
-    pub warning: Option<String>,
-
-    #[serde(rename = "warning.background")]
-    pub warning_background: Option<String>,
-
-    #[serde(rename = "warning.border")]
-    pub warning_border: Option<String>,
-}
-
-impl StatusColorsContent {
-    /// Returns a [`StatusColorsRefinement`] based on the colors in the [`StatusColorsContent`].
-    pub fn status_colors_refinement(&self) -> StatusColorsRefinement {
-        StatusColorsRefinement {
-            conflict: self
-                .conflict
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            conflict_background: self
-                .conflict_background
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            conflict_border: self
-                .conflict_border
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            created: self
-                .created
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            created_background: self
-                .created_background
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            created_border: self
-                .created_border
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            deleted: self
-                .deleted
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            deleted_background: self
-                .deleted_background
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            deleted_border: self
-                .deleted_border
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            error: self
-                .error
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            error_background: self
-                .error_background
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            error_border: self
-                .error_border
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            hidden: self
-                .hidden
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            hidden_background: self
-                .hidden_background
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            hidden_border: self
-                .hidden_border
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            hint: self
-                .hint
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            hint_background: self
-                .hint_background
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            hint_border: self
-                .hint_border
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            ignored: self
-                .ignored
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            ignored_background: self
-                .ignored_background
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            ignored_border: self
-                .ignored_border
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            info: self
-                .info
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            info_background: self
-                .info_background
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            info_border: self
-                .info_border
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            modified: self
-                .modified
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            modified_background: self
-                .modified_background
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            modified_border: self
-                .modified_border
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            predictive: self
-                .predictive
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            predictive_background: self
-                .predictive_background
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            predictive_border: self
-                .predictive_border
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            renamed: self
-                .renamed
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            renamed_background: self
-                .renamed_background
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            renamed_border: self
-                .renamed_border
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            success: self
-                .success
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            success_background: self
-                .success_background
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            success_border: self
-                .success_border
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            unreachable: self
-                .unreachable
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            unreachable_background: self
-                .unreachable_background
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            unreachable_border: self
-                .unreachable_border
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            warning: self
-                .warning
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            warning_background: self
-                .warning_background
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            warning_border: self
-                .warning_border
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-        }
+            .and_then(|color| try_parse_color(color).ok()),
+        conflict_border: colors
+            .conflict_border
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        created: colors
+            .created
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        created_background: colors
+            .created_background
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        created_border: colors
+            .created_border
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        deleted: colors
+            .deleted
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        deleted_background: colors
+            .deleted_background
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        deleted_border: colors
+            .deleted_border
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        error: colors
+            .error
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        error_background: colors
+            .error_background
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        error_border: colors
+            .error_border
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        hidden: colors
+            .hidden
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        hidden_background: colors
+            .hidden_background
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        hidden_border: colors
+            .hidden_border
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        hint: colors
+            .hint
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        hint_background: colors
+            .hint_background
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        hint_border: colors
+            .hint_border
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        ignored: colors
+            .ignored
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        ignored_background: colors
+            .ignored_background
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        ignored_border: colors
+            .ignored_border
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        info: colors
+            .info
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        info_background: colors
+            .info_background
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        info_border: colors
+            .info_border
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        modified: colors
+            .modified
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        modified_background: colors
+            .modified_background
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        modified_border: colors
+            .modified_border
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        predictive: colors
+            .predictive
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        predictive_background: colors
+            .predictive_background
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        predictive_border: colors
+            .predictive_border
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        renamed: colors
+            .renamed
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        renamed_background: colors
+            .renamed_background
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        renamed_border: colors
+            .renamed_border
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        success: colors
+            .success
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        success_background: colors
+            .success_background
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        success_border: colors
+            .success_border
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        unreachable: colors
+            .unreachable
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        unreachable_background: colors
+            .unreachable_background
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        unreachable_border: colors
+            .unreachable_border
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        warning: colors
+            .warning
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        warning_background: colors
+            .warning_background
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        warning_border: colors
+            .warning_border
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
     }
 }
 

crates/theme/src/settings.rs 🔗

@@ -1,22 +1,25 @@
 use crate::fallback_themes::zed_default_dark;
 use crate::{
     Appearance, DEFAULT_ICON_THEME_NAME, IconTheme, IconThemeNotFoundError, SyntaxTheme, Theme,
-    ThemeNotFoundError, ThemeRegistry, ThemeStyleContent,
+    ThemeColorsRefinement, ThemeNotFoundError, ThemeRegistry, ThemeStyleContent,
+    status_colors_refinement, syntax_overrides, theme_colors_refinement,
 };
-use anyhow::Result;
 use collections::HashMap;
 use derive_more::{Deref, DerefMut};
 use gpui::{
-    App, Context, Font, FontFallbacks, FontFeatures, FontStyle, FontWeight, Global, Pixels,
-    SharedString, Subscription, Window, px,
+    App, Context, Font, FontFallbacks, FontStyle, FontWeight, Global, Pixels, Subscription, Window,
+    px,
 };
 use refineable::Refineable;
 use schemars::{JsonSchema, json_schema};
 use serde::{Deserialize, Serialize};
-use settings::{ParameterizedJsonSchema, Settings, SettingsKey, SettingsSources, SettingsUi};
+use settings::{
+    FontFamilyName, IconThemeName, ParameterizedJsonSchema, Settings, SettingsContent, ThemeMode,
+    ThemeName,
+};
 use std::sync::Arc;
-use util::ResultExt as _;
 use util::schemars::replace_subschema;
+use util::{MergeFrom, ResultExt as _};
 
 const MIN_FONT_SIZE: Pixels = px(6.0);
 const MAX_FONT_SIZE: Pixels = px(100.0);
@@ -119,16 +122,16 @@ pub struct ThemeSettings {
     /// Manual overrides for the active theme.
     ///
     /// Note: This setting is still experimental. See [this tracking issue](https://github.com/zed-industries/zed/issues/18078)
-    pub experimental_theme_overrides: Option<ThemeStyleContent>,
+    pub experimental_theme_overrides: Option<settings::ThemeStyleContent>,
     /// Manual overrides per theme
-    pub theme_overrides: HashMap<String, ThemeStyleContent>,
+    pub theme_overrides: HashMap<String, settings::ThemeStyleContent>,
     /// The current icon theme selection.
     pub icon_theme_selection: Option<IconThemeSelection>,
     /// The active icon theme.
     pub active_icon_theme: Arc<IconTheme>,
     /// The density of the UI.
     /// Note: This setting is still experimental. See [this tracking issue](
-    pub ui_density: UiDensity,
+    pub ui_density: settings::UiDensity,
     /// The amount of fading applied to unnecessary code.
     pub unnecessary_code_fade: f32,
 }
@@ -277,24 +280,15 @@ pub enum ThemeSelection {
     },
 }
 
-// TODO: Rename ThemeMode -> ThemeAppearanceMode
-/// The mode use to select a theme.
-///
-/// `Light` and `Dark` will select their respective themes.
-///
-/// `System` will select the theme based on the system's appearance.
-#[derive(Debug, PartialEq, Eq, Clone, Copy, Default, Serialize, Deserialize, JsonSchema)]
-#[serde(rename_all = "snake_case")]
-pub enum ThemeMode {
-    /// Use the specified `light` theme.
-    Light,
-
-    /// Use the specified `dark` theme.
-    Dark,
-
-    /// Use the theme based on the system's appearance.
-    #[default]
-    System,
+impl From<settings::ThemeSelection> for ThemeSelection {
+    fn from(selection: settings::ThemeSelection) -> Self {
+        match selection {
+            settings::ThemeSelection::Static(theme) => ThemeSelection::Static(theme),
+            settings::ThemeSelection::Dynamic { mode, light, dark } => {
+                ThemeSelection::Dynamic { mode, light, dark }
+            }
+        }
+    }
 }
 
 impl ThemeSelection {
@@ -323,15 +317,13 @@ impl ThemeSelection {
 }
 
 /// Represents the selection of an icon theme, which can be either static or dynamic.
-#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
-#[serde(untagged)]
+#[derive(Clone, Debug, PartialEq, Eq)]
 pub enum IconThemeSelection {
     /// A static icon theme selection, represented by a single icon theme name.
     Static(IconThemeName),
     /// A dynamic icon theme selection, which can change based on the [`ThemeMode`].
     Dynamic {
         /// The mode used to determine which theme to use.
-        #[serde(default)]
         mode: ThemeMode,
         /// The icon theme to use for light mode.
         light: IconThemeName,
@@ -340,6 +332,17 @@ pub enum IconThemeSelection {
     },
 }
 
+impl From<settings::IconThemeSelection> for IconThemeSelection {
+    fn from(selection: settings::IconThemeSelection) -> Self {
+        match selection {
+            settings::IconThemeSelection::Static(theme) => IconThemeSelection::Static(theme),
+            settings::IconThemeSelection::Dynamic { mode, light, dark } => {
+                IconThemeSelection::Dynamic { mode, light, dark }
+            }
+        }
+    }
+}
+
 impl IconThemeSelection {
     /// Returns the icon theme name based on the given [`Appearance`].
     pub fn icon_theme(&self, system_appearance: Appearance) -> &str {
@@ -365,189 +368,105 @@ impl IconThemeSelection {
     }
 }
 
-/// Settings for rendering text in UI and text buffers.
-#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, SettingsUi, SettingsKey)]
-#[settings_key(None)]
-pub struct ThemeSettingsContent {
-    /// The default font size for text in the UI.
-    #[serde(default)]
-    pub ui_font_size: Option<f32>,
-    /// The name of a font to use for rendering in the UI.
-    #[serde(default)]
-    pub ui_font_family: Option<FontFamilyName>,
-    /// The font fallbacks to use for rendering in the UI.
-    #[serde(default)]
-    #[schemars(default = "default_font_fallbacks")]
-    #[schemars(extend("uniqueItems" = true))]
-    pub ui_font_fallbacks: Option<Vec<FontFamilyName>>,
-    /// The OpenType features to enable for text in the UI.
-    #[serde(default)]
-    #[schemars(default = "default_font_features")]
-    pub ui_font_features: Option<FontFeatures>,
-    /// The weight of the UI font in CSS units from 100 to 900.
-    #[serde(default)]
-    pub ui_font_weight: Option<f32>,
-    /// The name of a font to use for rendering in text buffers.
-    #[serde(default)]
-    pub buffer_font_family: Option<FontFamilyName>,
-    /// The font fallbacks to use for rendering in text buffers.
-    #[serde(default)]
-    #[schemars(extend("uniqueItems" = true))]
-    pub buffer_font_fallbacks: Option<Vec<FontFamilyName>>,
-    /// The default font size for rendering in text buffers.
-    #[serde(default)]
-    pub buffer_font_size: Option<f32>,
-    /// The weight of the editor font in CSS units from 100 to 900.
-    #[serde(default)]
-    pub buffer_font_weight: Option<f32>,
-    /// The buffer's line height.
-    #[serde(default)]
-    pub buffer_line_height: Option<BufferLineHeight>,
-    /// The OpenType features to enable for rendering in text buffers.
-    #[serde(default)]
-    #[schemars(default = "default_font_features")]
-    pub buffer_font_features: Option<FontFeatures>,
-    /// The font size for the agent panel. Falls back to the UI font size if unset.
-    #[serde(default)]
-    pub agent_font_size: Option<Option<f32>>,
-    /// The name of the Zed theme to use.
-    #[serde(default)]
-    pub theme: Option<ThemeSelection>,
-    /// The name of the icon theme to use.
-    #[serde(default)]
-    pub icon_theme: Option<IconThemeSelection>,
-
-    /// UNSTABLE: Expect many elements to be broken.
-    ///
-    // Controls the density of the UI.
-    #[serde(rename = "unstable.ui_density", default)]
-    pub ui_density: Option<UiDensity>,
-
-    /// How much to fade out unused code.
-    #[serde(default)]
-    pub unnecessary_code_fade: Option<f32>,
-
-    /// EXPERIMENTAL: Overrides for the current theme.
-    ///
-    /// These values will override the ones on the current theme specified in `theme`.
-    #[serde(rename = "experimental.theme_overrides", default)]
-    pub experimental_theme_overrides: Option<ThemeStyleContent>,
-
-    /// Overrides per theme
-    ///
-    /// These values will override the ones on the specified theme
-    #[serde(default)]
-    pub theme_overrides: HashMap<String, ThemeStyleContent>,
-}
-
-fn default_font_features() -> Option<FontFeatures> {
-    Some(FontFeatures::default())
-}
-
-fn default_font_fallbacks() -> Option<FontFallbacks> {
-    Some(FontFallbacks::default())
-}
-
-impl ThemeSettingsContent {
-    /// Sets the theme for the given appearance to the theme with the specified name.
-    pub fn set_theme(&mut self, theme_name: impl Into<Arc<str>>, appearance: Appearance) {
-        if let Some(selection) = self.theme.as_mut() {
-            let theme_to_update = match selection {
-                ThemeSelection::Static(theme) => theme,
-                ThemeSelection::Dynamic { mode, light, dark } => match mode {
-                    ThemeMode::Light => light,
-                    ThemeMode::Dark => dark,
-                    ThemeMode::System => match appearance {
-                        Appearance::Light => light,
-                        Appearance::Dark => dark,
-                    },
-                },
-            };
-
-            *theme_to_update = ThemeName(theme_name.into());
-        } else {
-            self.theme = Some(ThemeSelection::Static(ThemeName(theme_name.into())));
-        }
-    }
-
-    /// Sets the icon theme for the given appearance to the icon theme with the specified name.
-    pub fn set_icon_theme(&mut self, icon_theme_name: String, appearance: Appearance) {
-        if let Some(selection) = self.icon_theme.as_mut() {
-            let icon_theme_to_update = match selection {
-                IconThemeSelection::Static(theme) => theme,
-                IconThemeSelection::Dynamic { mode, light, dark } => match mode {
-                    ThemeMode::Light => light,
-                    ThemeMode::Dark => dark,
-                    ThemeMode::System => match appearance {
-                        Appearance::Light => light,
-                        Appearance::Dark => dark,
-                    },
-                },
-            };
-
-            *icon_theme_to_update = IconThemeName(icon_theme_name.into());
-        } else {
-            self.icon_theme = Some(IconThemeSelection::Static(IconThemeName(
-                icon_theme_name.into(),
-            )));
-        }
-    }
-
-    /// Sets the mode for the theme.
-    pub fn set_mode(&mut self, mode: ThemeMode) {
-        if let Some(selection) = self.theme.as_mut() {
-            match selection {
-                ThemeSelection::Static(theme) => {
-                    // If the theme was previously set to a single static theme,
-                    // we don't know whether it was a light or dark theme, so we
-                    // just use it for both.
-                    self.theme = Some(ThemeSelection::Dynamic {
-                        mode,
-                        light: theme.clone(),
-                        dark: theme.clone(),
-                    });
-                }
-                ThemeSelection::Dynamic {
-                    mode: mode_to_update,
-                    ..
-                } => *mode_to_update = mode,
-            }
-        } else {
-            self.theme = Some(ThemeSelection::Dynamic {
-                mode,
-                light: ThemeName(ThemeSettings::DEFAULT_LIGHT_THEME.into()),
-                dark: ThemeName(ThemeSettings::DEFAULT_DARK_THEME.into()),
-            });
-        }
-
-        if let Some(selection) = self.icon_theme.as_mut() {
-            match selection {
-                IconThemeSelection::Static(icon_theme) => {
-                    // If the icon theme was previously set to a single static
-                    // theme, we don't know whether it was a light or dark
-                    // theme, so we just use it for both.
-                    self.icon_theme = Some(IconThemeSelection::Dynamic {
-                        mode,
-                        light: icon_theme.clone(),
-                        dark: icon_theme.clone(),
-                    });
-                }
-                IconThemeSelection::Dynamic {
-                    mode: mode_to_update,
-                    ..
-                } => *mode_to_update = mode,
-            }
-        } else {
-            self.icon_theme = Some(IconThemeSelection::Static(IconThemeName(
-                DEFAULT_ICON_THEME_NAME.into(),
-            )));
-        }
-    }
-}
+// impl ThemeSettingsContent {
+//     /// Sets the theme for the given appearance to the theme with the specified name.
+//     pub fn set_theme(&mut self, theme_name: impl Into<Arc<str>>, appearance: Appearance) {
+//         if let Some(selection) = self.theme.as_mut() {
+//             let theme_to_update = match selection {
+//                 ThemeSelection::Static(theme) => theme,
+//                 ThemeSelection::Dynamic { mode, light, dark } => match mode {
+//                     ThemeMode::Light => light,
+//                     ThemeMode::Dark => dark,
+//                     ThemeMode::System => match appearance {
+//                         Appearance::Light => light,
+//                         Appearance::Dark => dark,
+//                     },
+//                 },
+//             };
+
+//             *theme_to_update = ThemeName(theme_name.into());
+//         } else {
+//             self.theme = Some(ThemeSelection::Static(ThemeName(theme_name.into())));
+//         }
+//     }
+
+//     /// Sets the icon theme for the given appearance to the icon theme with the specified name.
+//     pub fn set_icon_theme(&mut self, icon_theme_name: String, appearance: Appearance) {
+//         if let Some(selection) = self.icon_theme.as_mut() {
+//             let icon_theme_to_update = match selection {
+//                 IconThemeSelection::Static(theme) => theme,
+//                 IconThemeSelection::Dynamic { mode, light, dark } => match mode {
+//                     ThemeMode::Light => light,
+//                     ThemeMode::Dark => dark,
+//                     ThemeMode::System => match appearance {
+//                         Appearance::Light => light,
+//                         Appearance::Dark => dark,
+//                     },
+//                 },
+//             };
+
+//             *icon_theme_to_update = IconThemeName(icon_theme_name.into());
+//         } else {
+//             self.icon_theme = Some(IconThemeSelection::Static(IconThemeName(
+//                 icon_theme_name.into(),
+//             )));
+//         }
+//     }
+
+//     /// Sets the mode for the theme.
+//     pub fn set_mode(&mut self, mode: ThemeMode) {
+//         if let Some(selection) = self.theme.as_mut() {
+//             match selection {
+//                 ThemeSelection::Static(theme) => {
+//                     // If the theme was previously set to a single static theme,
+//                     // we don't know whether it was a light or dark theme, so we
+//                     // just use it for both.
+//                     self.theme = Some(ThemeSelection::Dynamic {
+//                         mode,
+//                         light: theme.clone(),
+//                         dark: theme.clone(),
+//                     });
+//                 }
+//                 ThemeSelection::Dynamic {
+//                     mode: mode_to_update,
+//                     ..
+//                 } => *mode_to_update = mode,
+//             }
+//         } else {
+//             self.theme = Some(ThemeSelection::Dynamic {
+//                 mode,
+//                 light: ThemeName(ThemeSettings::DEFAULT_LIGHT_THEME.into()),
+//                 dark: ThemeName(ThemeSettings::DEFAULT_DARK_THEME.into()),
+//             });
+//         }
+
+//         if let Some(selection) = self.icon_theme.as_mut() {
+//             match selection {
+//                 IconThemeSelection::Static(icon_theme) => {
+//                     // If the icon theme was previously set to a single static
+//                     // theme, we don't know whether it was a light or dark
+//                     // theme, so we just use it for both.
+//                     self.icon_theme = Some(IconThemeSelection::Dynamic {
+//                         mode,
+//                         light: icon_theme.clone(),
+//                         dark: icon_theme.clone(),
+//                     });
+//                 }
+//                 IconThemeSelection::Dynamic {
+//                     mode: mode_to_update,
+//                     ..
+//                 } => *mode_to_update = mode,
+//             }
+//         } else {
+//             self.icon_theme = Some(IconThemeSelection::Static(IconThemeName(
+//                 DEFAULT_ICON_THEME_NAME.into(),
+//             )));
+//         }
+//     }
+// }
 
 /// The buffer's line height.
-#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, JsonSchema, Default)]
-#[serde(rename_all = "snake_case")]
+#[derive(Clone, Copy, Debug, PartialEq, Default)]
 pub enum BufferLineHeight {
     /// A less dense line height.
     #[default]
@@ -555,21 +474,19 @@ pub enum BufferLineHeight {
     /// The default line height.
     Standard,
     /// A custom line height, where 1.0 is the font's height. Must be at least 1.0.
-    Custom(#[serde(deserialize_with = "deserialize_line_height")] f32),
+    Custom(f32),
 }
 
-fn deserialize_line_height<'de, D>(deserializer: D) -> Result<f32, D::Error>
-where
-    D: serde::Deserializer<'de>,
-{
-    let value = f32::deserialize(deserializer)?;
-    if value < 1.0 {
-        return Err(serde::de::Error::custom(
-            "buffer_line_height.custom must be at least 1.0",
-        ));
+impl From<settings::BufferLineHeight> for BufferLineHeight {
+    fn from(value: settings::BufferLineHeight) -> Self {
+        match value {
+            settings::BufferLineHeight::Comfortable => BufferLineHeight::Comfortable,
+            settings::BufferLineHeight::Standard => BufferLineHeight::Standard,
+            settings::BufferLineHeight::Custom(line_height) => {
+                BufferLineHeight::Custom(line_height)
+            }
+        }
     }
-
-    Ok(value)
 }
 
 impl BufferLineHeight {
@@ -681,24 +598,22 @@ impl ThemeSettings {
         }
     }
 
-    fn modify_theme(base_theme: &mut Theme, theme_overrides: &ThemeStyleContent) {
+    fn modify_theme(base_theme: &mut Theme, theme_overrides: &settings::ThemeStyleContent) {
         if let Some(window_background_appearance) = theme_overrides.window_background_appearance {
             base_theme.styles.window_background_appearance = window_background_appearance.into();
         }
+        let status_color_refinement = status_colors_refinement(&theme_overrides.status);
 
-        base_theme
-            .styles
-            .colors
-            .refine(&theme_overrides.theme_colors_refinement());
-        base_theme
-            .styles
-            .status
-            .refine(&theme_overrides.status_colors_refinement());
+        base_theme.styles.colors.refine(&theme_colors_refinement(
+            &theme_overrides.colors,
+            &status_color_refinement,
+        ));
+        base_theme.styles.status.refine(&status_color_refinement);
         base_theme.styles.player.merge(&theme_overrides.players);
         base_theme.styles.accents.merge(&theme_overrides.accents);
         base_theme.styles.syntax = SyntaxTheme::merge(
             base_theme.styles.syntax.clone(),
-            theme_overrides.syntax_overrides(),
+            syntax_overrides(&theme_overrides),
         );
     }
 
@@ -818,292 +733,170 @@ fn clamp_font_weight(weight: f32) -> FontWeight {
     FontWeight(weight.clamp(100., 950.))
 }
 
+fn font_fallbacks_from_settings(
+    fallbacks: Option<Vec<settings::FontFamilyName>>,
+) -> Option<FontFallbacks> {
+    fallbacks.map(|fallbacks| {
+        FontFallbacks::from_fonts(
+            fallbacks
+                .into_iter()
+                .map(|font_family| font_family.0.to_string())
+                .collect(),
+        )
+    })
+}
+
 impl settings::Settings for ThemeSettings {
-    fn load(sources: SettingsSources<Self::FileContent>, cx: &mut App) -> Result<Self> {
+    fn from_file(content: &settings::SettingsContent, cx: &mut App) -> Option<Self> {
+        let content = &content.theme;
         let themes = ThemeRegistry::default_global(cx);
         let system_appearance = SystemAppearance::default_global(cx);
-
-        fn font_fallbacks_from_settings(
-            fallbacks: Option<Vec<FontFamilyName>>,
-        ) -> Option<FontFallbacks> {
-            fallbacks.map(|fallbacks| {
-                FontFallbacks::from_fonts(
-                    fallbacks
-                        .into_iter()
-                        .map(|font_family| font_family.0.to_string())
-                        .collect(),
-                )
-            })
-        }
-
-        let defaults = sources.default;
-        let mut this = Self {
-            ui_font_size: defaults.ui_font_size.unwrap().into(),
+        let theme_selection: ThemeSelection = content.theme.clone()?.into();
+        let icon_theme_selection: IconThemeSelection = content.icon_theme.clone()?.into();
+        let this = Self {
+            ui_font_size: content.ui_font_size?.into(),
             ui_font: Font {
-                family: defaults.ui_font_family.as_ref().unwrap().0.clone().into(),
-                features: defaults.ui_font_features.clone().unwrap(),
-                fallbacks: font_fallbacks_from_settings(defaults.ui_font_fallbacks.clone()),
-                weight: defaults.ui_font_weight.map(FontWeight).unwrap(),
+                family: content.ui_font_family.as_ref()?.0.clone().into(),
+                features: content.ui_font_features.clone()?,
+                fallbacks: font_fallbacks_from_settings(content.ui_font_fallbacks.clone()),
+                weight: content.ui_font_weight.map(FontWeight)?,
                 style: Default::default(),
             },
             buffer_font: Font {
-                family: defaults
-                    .buffer_font_family
-                    .as_ref()
-                    .unwrap()
-                    .0
-                    .clone()
-                    .into(),
-                features: defaults.buffer_font_features.clone().unwrap(),
-                fallbacks: font_fallbacks_from_settings(defaults.buffer_font_fallbacks.clone()),
-                weight: defaults.buffer_font_weight.map(FontWeight).unwrap(),
+                family: content.buffer_font_family.as_ref()?.0.clone().into(),
+                features: content.buffer_font_features.clone()?,
+                fallbacks: font_fallbacks_from_settings(content.buffer_font_fallbacks.clone()),
+                weight: content.buffer_font_weight.map(FontWeight)?,
                 style: FontStyle::default(),
             },
-            buffer_font_size: defaults.buffer_font_size.unwrap().into(),
-            buffer_line_height: defaults.buffer_line_height.unwrap(),
-            agent_font_size: defaults.agent_font_size.flatten().map(Into::into),
-            theme_selection: defaults.theme.clone(),
+            buffer_font_size: content.buffer_font_size?.into(),
+            buffer_line_height: content.buffer_line_height?.into(),
+            agent_font_size: content.agent_font_size.flatten().map(Into::into),
             active_theme: themes
-                .get(defaults.theme.as_ref().unwrap().theme(*system_appearance))
+                .get(theme_selection.theme(*system_appearance))
                 .or(themes.get(&zed_default_dark().name))
                 .unwrap(),
+            theme_selection: Some(theme_selection),
             experimental_theme_overrides: None,
             theme_overrides: HashMap::default(),
-            icon_theme_selection: defaults.icon_theme.clone(),
-            active_icon_theme: defaults
-                .icon_theme
-                .as_ref()
-                .and_then(|selection| {
-                    themes
-                        .get_icon_theme(selection.icon_theme(*system_appearance))
-                        .ok()
-                })
-                .unwrap_or_else(|| themes.get_icon_theme(DEFAULT_ICON_THEME_NAME).unwrap()),
-            ui_density: defaults.ui_density.unwrap_or(UiDensity::Default),
-            unnecessary_code_fade: defaults.unnecessary_code_fade.unwrap_or(0.0),
+            active_icon_theme: themes
+                .get_icon_theme(icon_theme_selection.icon_theme(*system_appearance))
+                .ok()?,
+            icon_theme_selection: Some(icon_theme_selection),
+            ui_density: content.ui_density?,
+            unnecessary_code_fade: content.unnecessary_code_fade?,
         };
 
-        for value in sources
-            .user
-            .into_iter()
-            .chain(sources.release_channel)
-            .chain(sources.operating_system)
-            .chain(sources.profile)
-            .chain(sources.server)
-        {
-            if let Some(value) = value.ui_density {
-                this.ui_density = value;
-            }
-
-            if let Some(value) = value.buffer_font_family.clone() {
-                this.buffer_font.family = value.0.into();
-            }
-            if let Some(value) = value.buffer_font_features.clone() {
-                this.buffer_font.features = value;
-            }
-            if let Some(value) = value.buffer_font_fallbacks.clone() {
-                this.buffer_font.fallbacks = font_fallbacks_from_settings(Some(value));
-            }
-            if let Some(value) = value.buffer_font_weight {
-                this.buffer_font.weight = clamp_font_weight(value);
-            }
+        Some(this)
+    }
 
-            if let Some(value) = value.ui_font_family.clone() {
-                this.ui_font.family = value.0.into();
-            }
-            if let Some(value) = value.ui_font_features.clone() {
-                this.ui_font.features = value;
-            }
-            if let Some(value) = value.ui_font_fallbacks.clone() {
-                this.ui_font.fallbacks = font_fallbacks_from_settings(Some(value));
-            }
-            if let Some(value) = value.ui_font_weight {
-                this.ui_font.weight = clamp_font_weight(value);
-            }
+    fn refine(&mut self, content: &SettingsContent, cx: &mut App) {
+        let value = &content.theme;
 
-            if let Some(value) = &value.theme {
-                this.theme_selection = Some(value.clone());
+        let themes = ThemeRegistry::default_global(cx);
+        let system_appearance = SystemAppearance::default_global(cx);
 
-                let theme_name = value.theme(*system_appearance);
+        self.ui_density.merge_from(&value.ui_density);
 
-                match themes.get(theme_name) {
-                    Ok(theme) => {
-                        this.active_theme = theme;
-                    }
-                    Err(err @ ThemeNotFoundError(_)) => {
-                        if themes.extensions_loaded() {
-                            log::error!("{err}");
-                        }
-                    }
-                }
-            }
+        if let Some(value) = value.buffer_font_family.clone() {
+            self.buffer_font.family = value.0.into();
+        }
+        if let Some(value) = value.buffer_font_features.clone() {
+            self.buffer_font.features = value;
+        }
+        if let Some(value) = value.buffer_font_fallbacks.clone() {
+            self.buffer_font.fallbacks = font_fallbacks_from_settings(Some(value));
+        }
+        if let Some(value) = value.buffer_font_weight {
+            self.buffer_font.weight = clamp_font_weight(value);
+        }
 
-            this.experimental_theme_overrides
-                .clone_from(&value.experimental_theme_overrides);
-            this.theme_overrides.clone_from(&value.theme_overrides);
-            this.apply_theme_overrides();
+        if let Some(value) = value.ui_font_family.clone() {
+            self.ui_font.family = value.0.into();
+        }
+        if let Some(value) = value.ui_font_features.clone() {
+            self.ui_font.features = value;
+        }
+        if let Some(value) = value.ui_font_fallbacks.clone() {
+            self.ui_font.fallbacks = font_fallbacks_from_settings(Some(value));
+        }
+        if let Some(value) = value.ui_font_weight {
+            self.ui_font.weight = clamp_font_weight(value);
+        }
 
-            if let Some(value) = &value.icon_theme {
-                this.icon_theme_selection = Some(value.clone());
+        if let Some(value) = &value.theme {
+            self.theme_selection = Some(value.clone().into());
 
-                let icon_theme_name = value.icon_theme(*system_appearance);
+            let theme_name = self
+                .theme_selection
+                .as_ref()
+                .unwrap()
+                .theme(*system_appearance);
 
-                match themes.get_icon_theme(icon_theme_name) {
-                    Ok(icon_theme) => {
-                        this.active_icon_theme = icon_theme;
-                    }
-                    Err(err @ IconThemeNotFoundError(_)) => {
-                        if themes.extensions_loaded() {
-                            log::error!("{err}");
-                        }
+            match themes.get(theme_name) {
+                Ok(theme) => {
+                    self.active_theme = theme;
+                }
+                Err(err @ ThemeNotFoundError(_)) => {
+                    if themes.extensions_loaded() {
+                        log::error!("{err}");
                     }
                 }
             }
-
-            merge(
-                &mut this.ui_font_size,
-                value.ui_font_size.map(Into::into).map(clamp_font_size),
-            );
-            merge(
-                &mut this.buffer_font_size,
-                value.buffer_font_size.map(Into::into).map(clamp_font_size),
-            );
-            merge(
-                &mut this.agent_font_size,
-                value
-                    .agent_font_size
-                    .map(|value| value.map(Into::into).map(clamp_font_size)),
-            );
-
-            merge(&mut this.buffer_line_height, value.buffer_line_height);
-
-            // Clamp the `unnecessary_code_fade` to ensure text can't disappear entirely.
-            merge(&mut this.unnecessary_code_fade, value.unnecessary_code_fade);
-            this.unnecessary_code_fade = this.unnecessary_code_fade.clamp(0.0, 0.9);
         }
 
-        Ok(this)
-    }
+        self.experimental_theme_overrides
+            .clone_from(&value.experimental_theme_overrides);
+        self.theme_overrides.clone_from(&value.theme_overrides);
 
-    fn import_from_vscode(vscode: &settings::VsCodeSettings, current: &mut Self::FileContent) {
-        vscode.f32_setting("editor.fontWeight", &mut current.buffer_font_weight);
-        vscode.f32_setting("editor.fontSize", &mut current.buffer_font_size);
-        if let Some(font) = vscode.read_string("editor.font") {
-            current.buffer_font_family = Some(FontFamilyName(font.into()));
-        }
-        // TODO: possibly map editor.fontLigatures to buffer_font_features?
-    }
-}
+        self.apply_theme_overrides();
 
-/// Newtype for a theme name. Its `ParameterizedJsonSchema` lists the theme names known at runtime.
-#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
-#[serde(transparent)]
-pub struct ThemeName(pub Arc<str>);
-
-inventory::submit! {
-    ParameterizedJsonSchema {
-        add_and_get_ref: |generator, _params, cx| {
-            replace_subschema::<ThemeName>(generator, || json_schema!({
-                "type": "string",
-                "enum": ThemeRegistry::global(cx).list_names(),
-            }))
-        }
-    }
-}
+        if let Some(value) = &value.icon_theme {
+            self.icon_theme_selection = Some(value.clone().into());
 
-/// Newtype for a icon theme name. Its `ParameterizedJsonSchema` lists the icon theme names known at
-/// runtime.
-#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
-#[serde(transparent)]
-pub struct IconThemeName(pub Arc<str>);
-
-inventory::submit! {
-    ParameterizedJsonSchema {
-        add_and_get_ref: |generator, _params, cx| {
-            replace_subschema::<IconThemeName>(generator, || json_schema!({
-                "type": "string",
-                "enum": ThemeRegistry::global(cx)
-                    .list_icon_themes()
-                    .into_iter()
-                    .map(|icon_theme| icon_theme.name)
-                    .collect::<Vec<SharedString>>(),
-            }))
-        }
-    }
-}
+            let icon_theme_name = self
+                .icon_theme_selection
+                .as_ref()
+                .unwrap()
+                .icon_theme(*system_appearance);
 
-/// Newtype for font family name. Its `ParameterizedJsonSchema` lists the font families known at
-/// runtime.
-#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
-#[serde(transparent)]
-pub struct FontFamilyName(pub Arc<str>);
-
-inventory::submit! {
-    ParameterizedJsonSchema {
-        add_and_get_ref: |generator, params, _cx| {
-            replace_subschema::<FontFamilyName>(generator, || {
-                json_schema!({
-                    "type": "string",
-                    "enum": params.font_names,
-                })
-            })
+            match themes.get_icon_theme(icon_theme_name) {
+                Ok(icon_theme) => {
+                    self.active_icon_theme = icon_theme;
+                }
+                Err(err @ IconThemeNotFoundError(_)) => {
+                    if themes.extensions_loaded() {
+                        log::error!("{err}");
+                    }
+                }
+            }
         }
-    }
-}
 
-fn merge<T: Copy>(target: &mut T, value: Option<T>) {
-    if let Some(value) = value {
-        *target = value;
-    }
-}
+        self.ui_font_size
+            .merge_from(&value.ui_font_size.map(Into::into).map(clamp_font_size));
+        self.buffer_font_size
+            .merge_from(&value.buffer_font_size.map(Into::into).map(clamp_font_size));
+        self.agent_font_size.merge_from(
+            &value
+                .agent_font_size
+                .map(|value| value.map(Into::into).map(clamp_font_size)),
+        );
 
-#[cfg(test)]
-mod tests {
-    use super::*;
-    use serde_json::json;
+        self.buffer_line_height
+            .merge_from(&value.buffer_line_height.map(Into::into));
 
-    #[test]
-    fn test_buffer_line_height_deserialize_valid() {
-        assert_eq!(
-            serde_json::from_value::<BufferLineHeight>(json!("comfortable")).unwrap(),
-            BufferLineHeight::Comfortable
-        );
-        assert_eq!(
-            serde_json::from_value::<BufferLineHeight>(json!("standard")).unwrap(),
-            BufferLineHeight::Standard
-        );
-        assert_eq!(
-            serde_json::from_value::<BufferLineHeight>(json!({"custom": 1.0})).unwrap(),
-            BufferLineHeight::Custom(1.0)
-        );
-        assert_eq!(
-            serde_json::from_value::<BufferLineHeight>(json!({"custom": 1.5})).unwrap(),
-            BufferLineHeight::Custom(1.5)
-        );
+        // Clamp the `unnecessary_code_fade` to ensure text can't disappear entirely.
+        self.unnecessary_code_fade
+            .merge_from(&value.unnecessary_code_fade);
+        self.unnecessary_code_fade = self.unnecessary_code_fade.clamp(0.0, 0.9);
     }
 
-    #[test]
-    fn test_buffer_line_height_deserialize_invalid() {
-        assert!(
-            serde_json::from_value::<BufferLineHeight>(json!({"custom": 0.99}))
-                .err()
-                .unwrap()
-                .to_string()
-                .contains("buffer_line_height.custom must be at least 1.0")
-        );
-        assert!(
-            serde_json::from_value::<BufferLineHeight>(json!({"custom": 0.0}))
-                .err()
-                .unwrap()
-                .to_string()
-                .contains("buffer_line_height.custom must be at least 1.0")
-        );
-        assert!(
-            serde_json::from_value::<BufferLineHeight>(json!({"custom": -1.0}))
-                .err()
-                .unwrap()
-                .to_string()
-                .contains("buffer_line_height.custom must be at least 1.0")
-        );
+    fn import_from_vscode(vscode: &settings::VsCodeSettings, current: &mut SettingsContent) {
+        vscode.f32_setting("editor.fontWeight", &mut current.theme.buffer_font_weight);
+        vscode.f32_setting("editor.fontSize", &mut current.theme.buffer_font_size);
+        if let Some(font) = vscode.read_string("editor.font") {
+            current.theme.buffer_font_family = Some(FontFamilyName(font.into()));
+        }
+        // TODO: possibly map editor.fontLigatures to buffer_font_features?
     }
 }

crates/theme/src/styles/accents.rs 🔗

@@ -66,7 +66,7 @@ impl AccentColors {
     }
 
     /// Merges the given accent colors into this [`AccentColors`] instance.
-    pub fn merge(&mut self, accent_colors: &[AccentContent]) {
+    pub fn merge(&mut self, accent_colors: &[settings::AccentContent]) {
         if accent_colors.is_empty() {
             return;
         }

crates/theme/src/styles/players.rs 🔗

@@ -152,7 +152,7 @@ impl PlayerColors {
     }
 
     /// Merges the given player colors into this [`PlayerColors`] instance.
-    pub fn merge(&mut self, user_player_colors: &[PlayerColorContent]) {
+    pub fn merge(&mut self, user_player_colors: &[settings::PlayerColorContent]) {
         if user_player_colors.is_empty() {
             return;
         }

crates/theme/src/theme.rs 🔗

@@ -178,7 +178,7 @@ impl ThemeFamily {
             AppearanceContent::Light => StatusColors::light(),
             AppearanceContent::Dark => StatusColors::dark(),
         };
-        let mut status_colors_refinement = theme.style.status_colors_refinement();
+        let mut status_colors_refinement = status_colors_refinement(&theme.style.status);
         apply_status_color_defaults(&mut status_colors_refinement);
         refined_status_colors.refine(&status_colors_refinement);
 
@@ -192,7 +192,8 @@ impl ThemeFamily {
             AppearanceContent::Light => ThemeColors::light(),
             AppearanceContent::Dark => ThemeColors::dark(),
         };
-        let mut theme_colors_refinement = theme.style.theme_colors_refinement();
+        let mut theme_colors_refinement =
+            theme_colors_refinement(&theme.style.colors, &status_colors_refinement);
         apply_theme_color_defaults(&mut theme_colors_refinement, &refined_player_colors);
         refined_theme_colors.refine(&theme_colors_refinement);
 

crates/title_bar/src/title_bar_settings.rs 🔗

@@ -38,7 +38,7 @@ impl Settings for TitleBarSettings {
         }
     }
 
-    fn refine(&mut self, s: &SettingsContent) {
+    fn refine(&mut self, s: &SettingsContent, _: &mut App) {
         let Some(content) = s.title_bar else {
             return
         }

crates/util/src/util.rs 🔗

@@ -1388,12 +1388,12 @@ pub fn refine<T: Clone>(dest: &mut T, src: &Option<T>) {
     }
 }
 
-pub trait Refine: Sized + Clone {
-    fn refine(&mut self, src: &Option<Self>);
+pub trait MergeFrom: Sized + Clone {
+    fn merge_from(&mut self, src: &Option<Self>);
 }
 
-impl<T: Clone> Refine for T {
-    fn refine(&mut self, src: &Option<Self>) {
+impl<T: Clone> MergeFrom for T {
+    fn merge_from(&mut self, src: &Option<Self>) {
         if let Some(src) = src {
             *self = src.clone();
         }