settings: Use a derive macro for refine (#38451)

Conrad Irwin created

When we refactored settings to not pass JSON blobs around, we ended up
needing
to write *a lot* of code that just merged things (like json merge used
to do).

Use a derive macro to prevent typos in this logic.

Release Notes:

- N/A

Change summary

Cargo.lock                                             |  28 
Cargo.toml                                             |   3 
crates/agent2/src/tools/grep_tool.rs                   |  16 
crates/agent2/src/tools/list_directory_tool.rs         |  16 
crates/agent2/src/tools/read_file_tool.rs              |  16 
crates/agent_settings/src/agent_settings.rs            |  63 -
crates/agent_ui/src/agent_panel.rs                     |   2 
crates/assistant_tools/src/grep_tool.rs                |  16 
crates/assistant_tools/src/list_directory_tool.rs      |  16 
crates/assistant_tools/src/read_file_tool.rs           |  16 
crates/audio/src/audio_settings.rs                     |  14 
crates/auto_update/src/auto_update.rs                  |  16 
crates/call/src/call_settings.rs                       |  10 
crates/client/Cargo.toml                               |   1 
crates/client/src/client.rs                            |  42 
crates/collab_ui/src/panel_settings.rs                 |  35 
crates/collections/src/collections.rs                  |   1 
crates/dap/src/debugger_settings.rs                    |  24 
crates/editor/src/editor_settings.rs                   | 161 ---
crates/extension_host/src/extension_settings.rs        |  18 
crates/file_finder/src/file_finder_settings.rs         |  19 
crates/git_hosting_providers/src/settings.rs           |  17 
crates/git_ui/src/git_panel_settings.rs                |  22 
crates/go_to_line/src/cursor_position.rs               |   8 
crates/image_viewer/src/image_viewer_settings.rs       |  12 
crates/journal/Cargo.toml                              |   1 
crates/journal/src/journal.rs                          |  11 
crates/language/Cargo.toml                             |   1 
crates/language/src/buffer_tests.rs                    |   8 
crates/language/src/language_settings.rs               | 486 +-------
crates/language_models/src/settings.rs                 | 112 --
crates/languages/Cargo.toml                            |   1 
crates/languages/src/json.rs                           |  20 
crates/onboarding/src/ai_setup_page.rs                 |   2 
crates/outline_panel/src/outline_panel_settings.rs     |  33 
crates/project/src/agent_server_store.rs               |  22 
crates/project/src/project.rs                          |  11 
crates/project/src/project_settings.rs                 | 133 --
crates/project_panel/src/project_panel_settings.rs     |  51 
crates/recent_projects/src/remote_connections.rs       |  28 
crates/recent_projects/src/remote_servers.rs           |   4 
crates/repl/src/jupyter_settings.rs                    |  11 
crates/repl/src/repl_settings.rs                       |  12 
crates/settings/Cargo.toml                             |   2 
crates/settings/src/base_keymap_setting.rs             |  36 
crates/settings/src/merge_from.rs                      | 164 +++
crates/settings/src/settings.rs                        |   4 
crates/settings/src/settings_content.rs                | 136 +
crates/settings/src/settings_content/agent.rs          |  25 
crates/settings/src/settings_content/editor.rs         |  73 
crates/settings/src/settings_content/language.rs       | 128 +-
crates/settings/src/settings_content/language_model.rs |  75 
crates/settings/src/settings_content/project.rs        |  69 
crates/settings/src/settings_content/terminal.rs       |  31 
crates/settings/src/settings_content/theme.rs          |  50 
crates/settings/src/settings_content/workspace.rs      |  47 
crates/settings/src/settings_json.rs                   |  12 
crates/settings/src/settings_store.rs                  | 265 ++--
crates/settings_macros/Cargo.toml                      |   6 
crates/settings_macros/LICENSE-GPL                     |   0 
crates/settings_macros/src/settings_macros.rs          | 132 ++
crates/settings_ui_macros/src/settings_ui_macros.rs    | 612 ------------
crates/terminal/src/terminal_settings.rs               |  73 -
crates/theme/Cargo.toml                                |   1 
crates/theme/src/settings.rs                           | 172 ---
crates/title_bar/src/title_bar_settings.rs             |  23 
crates/util/src/schemars.rs                            |   5 
crates/util/src/util.rs                                |  18 
crates/vim/src/vim.rs                                  |  28 
crates/vim_mode_setting/src/vim_mode_setting.rs        |  16 
crates/workspace/src/item.rs                           |  30 
crates/workspace/src/workspace_settings.rs             |  75 -
crates/worktree/src/worktree_settings.rs               |  52 
crates/zlog_settings/src/zlog_settings.rs              |   8 
74 files changed, 1,089 insertions(+), 2,818 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -3144,7 +3144,6 @@ dependencies = [
  "release_channel",
  "rpc",
  "rustls-pki-types",
- "schemars 1.0.1",
  "serde",
  "serde_json",
  "serde_urlencoded",
@@ -9302,7 +9301,6 @@ dependencies = [
  "serde",
  "settings",
  "shellexpand 2.1.2",
- "util",
  "workspace",
  "workspace-hack",
 ]
@@ -9523,7 +9521,6 @@ dependencies = [
  "http_client",
  "imara-diff",
  "indoc",
- "inventory",
  "itertools 0.14.0",
  "log",
  "lsp",
@@ -15499,7 +15496,7 @@ dependencies = [
  "serde_path_to_error",
  "serde_repr",
  "serde_with",
- "settings_ui_macros",
+ "settings_macros",
  "smallvec",
  "tree-sitter",
  "tree-sitter-json",
@@ -15509,6 +15506,16 @@ dependencies = [
  "zlog",
 ]
 
+[[package]]
+name = "settings_macros"
+version = "0.1.0"
+dependencies = [
+ "quote",
+ "settings",
+ "syn 2.0.101",
+ "workspace-hack",
+]
+
 [[package]]
 name = "settings_profile_selector"
 version = "0.1.0"
@@ -15530,18 +15537,6 @@ dependencies = [
  "zed_actions",
 ]
 
-[[package]]
-name = "settings_ui_macros"
-version = "0.1.0"
-dependencies = [
- "heck 0.5.0",
- "proc-macro2",
- "quote",
- "settings",
- "syn 2.0.101",
- "workspace-hack",
-]
-
 [[package]]
 name = "sha1"
 version = "0.10.6"
@@ -17190,7 +17185,6 @@ dependencies = [
  "futures 0.3.31",
  "gpui",
  "indexmap 2.9.0",
- "inventory",
  "log",
  "palette",
  "parking_lot",

Cargo.toml 🔗

@@ -150,8 +150,8 @@ members = [
     "crates/semantic_version",
     "crates/session",
     "crates/settings",
+    "crates/settings_macros",
     "crates/settings_profile_selector",
-    "crates/settings_ui_macros",
     "crates/snippet",
     "crates/snippet_provider",
     "crates/snippets_ui",
@@ -383,7 +383,6 @@ semantic_version = { path = "crates/semantic_version" }
 session = { path = "crates/session" }
 settings = { path = "crates/settings" }
 settings_ui = { path = "crates/settings_ui" }
-settings_ui_macros = { path = "crates/settings_ui_macros" }
 snippet = { path = "crates/snippet" }
 snippet_provider = { path = "crates/snippet_provider" }
 snippets_ui = { path = "crates/snippets_ui" }

crates/agent2/src/tools/grep_tool.rs 🔗

@@ -834,11 +834,14 @@ mod tests {
                         "**/.secretdir".to_string(),
                         "**/.mymetadata".to_string(),
                     ]);
-                    settings.project.worktree.private_files = Some(vec![
-                        "**/.mysecrets".to_string(),
-                        "**/*.privatekey".to_string(),
-                        "**/*.mysensitive".to_string(),
-                    ]);
+                    settings.project.worktree.private_files = Some(
+                        vec![
+                            "**/.mysecrets".to_string(),
+                            "**/*.privatekey".to_string(),
+                            "**/*.mysensitive".to_string(),
+                        ]
+                        .into(),
+                    );
                 });
             });
         });
@@ -1064,7 +1067,8 @@ mod tests {
                 store.update_user_settings(cx, |settings| {
                     settings.project.worktree.file_scan_exclusions =
                         Some(vec!["**/.git".to_string(), "**/node_modules".to_string()]);
-                    settings.project.worktree.private_files = Some(vec!["**/.env".to_string()]);
+                    settings.project.worktree.private_files =
+                        Some(vec!["**/.env".to_string()].into());
                 });
             });
         });

crates/agent2/src/tools/list_directory_tool.rs 🔗

@@ -427,11 +427,14 @@ mod tests {
                         "**/.mymetadata".to_string(),
                         "**/.hidden_subdir".to_string(),
                     ]);
-                    settings.project.worktree.private_files = Some(vec![
-                        "**/.mysecrets".to_string(),
-                        "**/*.privatekey".to_string(),
-                        "**/*.mysensitive".to_string(),
-                    ]);
+                    settings.project.worktree.private_files = Some(
+                        vec![
+                            "**/.mysecrets".to_string(),
+                            "**/*.privatekey".to_string(),
+                            "**/*.mysensitive".to_string(),
+                        ]
+                        .into(),
+                    );
                 });
             });
         });
@@ -568,7 +571,8 @@ mod tests {
                 store.update_user_settings(cx, |settings| {
                     settings.project.worktree.file_scan_exclusions =
                         Some(vec!["**/.git".to_string(), "**/node_modules".to_string()]);
-                    settings.project.worktree.private_files = Some(vec!["**/.env".to_string()]);
+                    settings.project.worktree.private_files =
+                        Some(vec!["**/.env".to_string()].into());
                 });
             });
         });

crates/agent2/src/tools/read_file_tool.rs 🔗

@@ -593,11 +593,14 @@ mod test {
                         "**/.secretdir".to_string(),
                         "**/.mymetadata".to_string(),
                     ]);
-                    settings.project.worktree.private_files = Some(vec![
-                        "**/.mysecrets".to_string(),
-                        "**/*.privatekey".to_string(),
-                        "**/*.mysensitive".to_string(),
-                    ]);
+                    settings.project.worktree.private_files = Some(
+                        vec![
+                            "**/.mysecrets".to_string(),
+                            "**/*.privatekey".to_string(),
+                            "**/*.mysensitive".to_string(),
+                        ]
+                        .into(),
+                    );
                 });
             });
         });
@@ -804,7 +807,8 @@ mod test {
                 store.update_user_settings(cx, |settings| {
                     settings.project.worktree.file_scan_exclusions =
                         Some(vec!["**/.git".to_string(), "**/node_modules".to_string()]);
-                    settings.project.worktree.private_files = Some(vec!["**/.env".to_string()]);
+                    settings.project.worktree.private_files =
+                        Some(vec!["**/.env".to_string()].into());
                 });
             });
         });

crates/agent_settings/src/agent_settings.rs 🔗

@@ -11,7 +11,6 @@ use settings::{
     DefaultAgentView, DockPosition, LanguageModelParameters, LanguageModelSelection,
     NotifyWhenAgentWaiting, Settings, SettingsContent,
 };
-use util::MergeFrom;
 
 pub use crate::agent_profile::*;
 
@@ -147,7 +146,7 @@ impl Default for AgentProfileId {
 }
 
 impl Settings for AgentSettings {
-    fn from_defaults(content: &settings::SettingsContent, _cx: &mut App) -> Self {
+    fn from_settings(content: &settings::SettingsContent, _cx: &mut App) -> Self {
         let agent = content.agent.clone().unwrap();
         Self {
             enabled: agent.enabled.unwrap(),
@@ -183,66 +182,6 @@ impl Settings for AgentSettings {
         }
     }
 
-    fn refine(&mut self, content: &settings::SettingsContent, _: &mut App) {
-        let Some(value) = &content.agent else { return };
-        self.enabled.merge_from(&value.enabled);
-        self.button.merge_from(&value.button);
-        self.dock.merge_from(&value.dock);
-        self.default_width
-            .merge_from(&value.default_width.map(Into::into));
-        self.default_height
-            .merge_from(&value.default_height.map(Into::into));
-        self.default_model = value.default_model.clone().or(self.default_model.take());
-
-        self.inline_assistant_model = value
-            .inline_assistant_model
-            .clone()
-            .or(self.inline_assistant_model.take());
-        self.commit_message_model = value
-            .clone()
-            .commit_message_model
-            .or(self.commit_message_model.take());
-        self.thread_summary_model = value
-            .clone()
-            .thread_summary_model
-            .or(self.thread_summary_model.take());
-        self.inline_alternatives
-            .merge_from(&value.inline_alternatives.clone());
-        self.default_profile
-            .merge_from(&value.default_profile.clone().map(AgentProfileId));
-        self.default_view.merge_from(&value.default_view);
-        self.always_allow_tool_actions
-            .merge_from(&value.always_allow_tool_actions);
-        self.notify_when_agent_waiting
-            .merge_from(&value.notify_when_agent_waiting);
-        self.play_sound_when_agent_done
-            .merge_from(&value.play_sound_when_agent_done);
-        self.stream_edits.merge_from(&value.stream_edits);
-        self.single_file_review
-            .merge_from(&value.single_file_review);
-        self.preferred_completion_mode
-            .merge_from(&value.preferred_completion_mode.map(Into::into));
-        self.enable_feedback.merge_from(&value.enable_feedback);
-        self.expand_edit_card.merge_from(&value.expand_edit_card);
-        self.expand_terminal_card
-            .merge_from(&value.expand_terminal_card);
-        self.use_modifier_to_send
-            .merge_from(&value.use_modifier_to_send);
-
-        self.model_parameters
-            .extend_from_slice(&value.model_parameters);
-        self.message_editor_min_lines
-            .merge_from(&value.message_editor_min_lines);
-
-        if let Some(profiles) = value.profiles.as_ref() {
-            self.profiles.extend(
-                profiles
-                    .into_iter()
-                    .map(|(id, profile)| (AgentProfileId(id.clone()), profile.clone().into())),
-            );
-        }
-    }
-
     fn import_from_vscode(vscode: &settings::VsCodeSettings, current: &mut SettingsContent) {
         if let Some(b) = vscode
             .read_value("chat.agent.enabled")

crates/agent_ui/src/agent_panel.rs 🔗

@@ -1067,7 +1067,7 @@ impl AgentPanel {
                         let _ = settings
                             .theme
                             .agent_font_size
-                            .insert(Some(theme::clamp_font_size(agent_font_size).into()));
+                            .insert(theme::clamp_font_size(agent_font_size).into());
                     });
                 } else {
                     theme::adjust_agent_font_size(cx, |size| size + delta);

crates/assistant_tools/src/grep_tool.rs 🔗

@@ -856,11 +856,14 @@ mod tests {
                         "**/.secretdir".to_string(),
                         "**/.mymetadata".to_string(),
                     ]);
-                    settings.project.worktree.private_files = Some(vec![
-                        "**/.mysecrets".to_string(),
-                        "**/*.privatekey".to_string(),
-                        "**/*.mysensitive".to_string(),
-                    ]);
+                    settings.project.worktree.private_files = Some(
+                        vec![
+                            "**/.mysecrets".to_string(),
+                            "**/*.privatekey".to_string(),
+                            "**/*.mysensitive".to_string(),
+                        ]
+                        .into(),
+                    );
                 });
             });
         });
@@ -1160,7 +1163,8 @@ mod tests {
                 store.update_user_settings(cx, |settings| {
                     settings.project.worktree.file_scan_exclusions =
                         Some(vec!["**/.git".to_string(), "**/node_modules".to_string()]);
-                    settings.project.worktree.private_files = Some(vec!["**/.env".to_string()]);
+                    settings.project.worktree.private_files =
+                        Some(vec!["**/.env".to_string()].into());
                 });
             });
         });

crates/assistant_tools/src/list_directory_tool.rs 🔗

@@ -513,11 +513,14 @@ mod tests {
                         "**/.mymetadata".to_string(),
                         "**/.hidden_subdir".to_string(),
                     ]);
-                    settings.project.worktree.private_files = Some(vec![
-                        "**/.mysecrets".to_string(),
-                        "**/*.privatekey".to_string(),
-                        "**/*.mysensitive".to_string(),
-                    ]);
+                    settings.project.worktree.private_files = Some(
+                        vec![
+                            "**/.mysecrets".to_string(),
+                            "**/*.privatekey".to_string(),
+                            "**/*.mysensitive".to_string(),
+                        ]
+                        .into(),
+                    );
                 });
             });
         });
@@ -701,7 +704,8 @@ mod tests {
                 store.update_user_settings(cx, |settings| {
                     settings.project.worktree.file_scan_exclusions =
                         Some(vec!["**/.git".to_string(), "**/node_modules".to_string()]);
-                    settings.project.worktree.private_files = Some(vec!["**/.env".to_string()]);
+                    settings.project.worktree.private_files =
+                        Some(vec!["**/.env".to_string()].into());
                 });
             });
         });

crates/assistant_tools/src/read_file_tool.rs 🔗

@@ -684,11 +684,14 @@ mod test {
                         "**/.secretdir".to_string(),
                         "**/.mymetadata".to_string(),
                     ]);
-                    settings.project.worktree.private_files = Some(vec![
-                        "**/.mysecrets".to_string(),
-                        "**/*.privatekey".to_string(),
-                        "**/*.mysensitive".to_string(),
-                    ]);
+                    settings.project.worktree.private_files = Some(
+                        vec![
+                            "**/.mysecrets".to_string(),
+                            "**/*.privatekey".to_string(),
+                            "**/*.mysensitive".to_string(),
+                        ]
+                        .into(),
+                    );
                 });
             });
         });
@@ -970,7 +973,8 @@ mod test {
                 store.update_user_settings(cx, |settings| {
                     settings.project.worktree.file_scan_exclusions =
                         Some(vec!["**/.git".to_string(), "**/node_modules".to_string()]);
-                    settings.project.worktree.private_files = Some(vec!["**/.env".to_string()]);
+                    settings.project.worktree.private_files =
+                        Some(vec!["**/.env".to_string()].into());
                 });
             });
         });

crates/audio/src/audio_settings.rs 🔗

@@ -2,7 +2,6 @@ use std::sync::atomic::{AtomicBool, Ordering};
 
 use gpui::App;
 use settings::{Settings, SettingsStore};
-use util::MergeFrom as _;
 
 #[derive(Clone, Debug)]
 pub struct AudioSettings {
@@ -23,7 +22,7 @@ pub struct AudioSettings {
 
 /// Configuration of audio in Zed
 impl Settings for AudioSettings {
-    fn from_defaults(content: &settings::SettingsContent, _cx: &mut App) -> Self {
+    fn from_settings(content: &settings::SettingsContent, _cx: &mut App) -> Self {
         let audio = &content.audio.as_ref().unwrap();
         AudioSettings {
             control_input_volume: audio.control_input_volume.unwrap(),
@@ -32,17 +31,6 @@ impl Settings for AudioSettings {
         }
     }
 
-    fn refine(&mut self, content: &settings::SettingsContent, _cx: &mut App) {
-        let Some(audio) = content.audio.as_ref() else {
-            return;
-        };
-        self.control_input_volume
-            .merge_from(&audio.control_input_volume);
-        self.control_output_volume
-            .merge_from(&audio.control_output_volume);
-        self.rodio_audio.merge_from(&audio.rodio_audio);
-    }
-
     fn import_from_vscode(
         _vscode: &settings::VsCodeSettings,
         _current: &mut settings::SettingsContent,

crates/auto_update/src/auto_update.rs 🔗

@@ -9,7 +9,7 @@ use http_client::{AsyncBody, HttpClient, HttpClientWithUrl};
 use paths::remote_servers_dir;
 use release_channel::{AppCommitSha, ReleaseChannel};
 use serde::{Deserialize, Serialize};
-use settings::{Settings, SettingsContent, SettingsStore};
+use settings::{Settings, SettingsStore};
 use smol::{fs, io::AsyncReadExt};
 use smol::{fs::File, process::Command};
 use std::{
@@ -119,21 +119,9 @@ struct AutoUpdateSetting(bool);
 ///
 /// Default: true
 impl Settings for AutoUpdateSetting {
-    fn from_defaults(content: &settings::SettingsContent, _cx: &mut App) -> Self {
-        debug_assert_eq!(content.auto_update.unwrap(), true);
+    fn from_settings(content: &settings::SettingsContent, _cx: &mut App) -> Self {
         Self(content.auto_update.unwrap())
     }
-
-    fn refine(&mut self, content: &settings::SettingsContent, _cx: &mut App) {
-        if let Some(auto_update) = content.auto_update {
-            self.0 = auto_update;
-        }
-    }
-
-    fn import_from_vscode(_: &settings::VsCodeSettings, _: &mut SettingsContent) {
-        // We could match on vscode's update.mode here, but
-        // I think it's more important to have more people updating zed by default.
-    }
 }
 
 #[derive(Default)]

crates/call/src/call_settings.rs 🔗

@@ -1,6 +1,5 @@
 use gpui::App;
 use settings::Settings;
-use util::MergeFrom;
 
 #[derive(Debug)]
 pub struct CallSettings {
@@ -9,7 +8,7 @@ pub struct CallSettings {
 }
 
 impl Settings for CallSettings {
-    fn from_defaults(content: &settings::SettingsContent, _cx: &mut App) -> Self {
+    fn from_settings(content: &settings::SettingsContent, _cx: &mut App) -> Self {
         let call = content.calls.clone().unwrap();
         CallSettings {
             mute_on_join: call.mute_on_join.unwrap(),
@@ -17,13 +16,6 @@ impl Settings for CallSettings {
         }
     }
 
-    fn refine(&mut self, content: &settings::SettingsContent, _cx: &mut App) {
-        if let Some(call) = content.calls.clone() {
-            self.mute_on_join.merge_from(&call.mute_on_join);
-            self.share_on_join.merge_from(&call.share_on_join);
-        }
-    }
-
     fn import_from_vscode(
         _vscode: &settings::VsCodeSettings,
         _current: &mut settings::SettingsContent,

crates/client/Cargo.toml 🔗

@@ -41,7 +41,6 @@ rand.workspace = true
 regex.workspace = true
 release_channel.workspace = true
 rpc = { workspace = true, features = ["gpui"] }
-schemars.workspace = true
 serde.workspace = true
 serde_json.workspace = true
 serde_urlencoded.workspace = true

crates/client/src/client.rs 🔗

@@ -29,9 +29,8 @@ use proxy::connect_proxy_stream;
 use rand::prelude::*;
 use release_channel::{AppVersion, ReleaseChannel};
 use rpc::proto::{AnyTypedEnvelope, EnvelopedMessage, PeerId, RequestMessage};
-use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
-use settings::{Settings, SettingsContent, SettingsKey, SettingsUi};
+use settings::{Settings, SettingsContent};
 use std::{
     any::TypeId,
     convert::TryFrom,
@@ -50,7 +49,7 @@ use telemetry::Telemetry;
 use thiserror::Error;
 use tokio::net::TcpStream;
 use url::Url;
-use util::{ConnectionResult, MergeFrom, ResultExt};
+use util::{ConnectionResult, ResultExt};
 
 pub use rpc::*;
 pub use telemetry_events::Event;
@@ -102,7 +101,7 @@ pub struct ClientSettings {
 }
 
 impl Settings for ClientSettings {
-    fn from_defaults(content: &settings::SettingsContent, _cx: &mut App) -> Self {
+    fn from_settings(content: &settings::SettingsContent, _cx: &mut App) -> Self {
         if let Some(server_url) = &*ZED_SERVER_URL {
             return Self {
                 server_url: server_url.clone(),
@@ -112,23 +111,6 @@ impl Settings for ClientSettings {
             server_url: content.server_url.clone().unwrap(),
         }
     }
-
-    fn refine(&mut self, content: &settings::SettingsContent, _: &mut App) {
-        if ZED_SERVER_URL.is_some() {
-            return;
-        }
-        if let Some(server_url) = content.server_url.clone() {
-            self.server_url = server_url;
-        }
-    }
-
-    fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut SettingsContent) {}
-}
-
-#[derive(Default, Clone, Serialize, Deserialize, JsonSchema, SettingsUi, SettingsKey)]
-#[settings_key(None)]
-pub struct ProxySettingsContent {
-    proxy: Option<String>,
 }
 
 #[derive(Deserialize, Default)]
@@ -151,18 +133,12 @@ impl ProxySettings {
 }
 
 impl Settings for ProxySettings {
-    fn from_defaults(content: &settings::SettingsContent, _cx: &mut App) -> Self {
+    fn from_settings(content: &settings::SettingsContent, _cx: &mut App) -> Self {
         Self {
             proxy: content.proxy.clone(),
         }
     }
 
-    fn refine(&mut self, content: &settings::SettingsContent, _: &mut App) {
-        if let Some(proxy) = content.proxy.clone() {
-            self.proxy = Some(proxy)
-        }
-    }
-
     fn import_from_vscode(vscode: &settings::VsCodeSettings, current: &mut SettingsContent) {
         vscode.string_setting("http.proxy", &mut current.proxy);
     }
@@ -543,21 +519,13 @@ pub struct TelemetrySettings {
 }
 
 impl settings::Settings for TelemetrySettings {
-    fn from_defaults(content: &SettingsContent, _cx: &mut App) -> Self {
+    fn from_settings(content: &SettingsContent, _cx: &mut App) -> Self {
         Self {
             diagnostics: content.telemetry.as_ref().unwrap().diagnostics.unwrap(),
             metrics: content.telemetry.as_ref().unwrap().metrics.unwrap(),
         }
     }
 
-    fn refine(&mut self, content: &SettingsContent, _cx: &mut App) {
-        let Some(telemetry) = &content.telemetry else {
-            return;
-        };
-        self.diagnostics.merge_from(&telemetry.diagnostics);
-        self.metrics.merge_from(&telemetry.metrics);
-    }
-
     fn import_from_vscode(vscode: &settings::VsCodeSettings, current: &mut SettingsContent) {
         let mut telemetry = settings::TelemetrySettingsContent::default();
         vscode.enum_setting("telemetry.telemetryLevel", &mut telemetry.metrics, |s| {

crates/collab_ui/src/panel_settings.rs 🔗

@@ -1,7 +1,6 @@
 use gpui::Pixels;
 use settings::Settings;
 use ui::px;
-use util::MergeFrom as _;
 use workspace::dock::DockPosition;
 
 #[derive(Debug)]
@@ -19,7 +18,7 @@ pub struct NotificationPanelSettings {
 }
 
 impl Settings for CollaborationPanelSettings {
-    fn from_defaults(content: &settings::SettingsContent, _cx: &mut ui::App) -> Self {
+    fn from_settings(content: &settings::SettingsContent, _cx: &mut ui::App) -> Self {
         let panel = content.collaboration_panel.as_ref().unwrap();
 
         Self {
@@ -28,25 +27,10 @@ impl Settings for CollaborationPanelSettings {
             default_width: panel.default_width.map(px).unwrap(),
         }
     }
-
-    fn refine(&mut self, content: &settings::SettingsContent, _cx: &mut ui::App) {
-        if let Some(panel) = content.collaboration_panel.as_ref() {
-            self.button.merge_from(&panel.button);
-            self.default_width
-                .merge_from(&panel.default_width.map(Pixels::from));
-            self.dock.merge_from(&panel.dock.map(Into::into));
-        }
-    }
-
-    fn import_from_vscode(
-        _vscode: &settings::VsCodeSettings,
-        _content: &mut settings::SettingsContent,
-    ) {
-    }
 }
 
 impl Settings for NotificationPanelSettings {
-    fn from_defaults(content: &settings::SettingsContent, _cx: &mut ui::App) -> Self {
+    fn from_settings(content: &settings::SettingsContent, _cx: &mut ui::App) -> Self {
         let panel = content.notification_panel.as_ref().unwrap();
         return Self {
             button: panel.button.unwrap(),
@@ -54,19 +38,4 @@ impl Settings for NotificationPanelSettings {
             default_width: panel.default_width.map(px).unwrap(),
         };
     }
-
-    fn refine(&mut self, content: &settings::SettingsContent, _cx: &mut ui::App) {
-        let Some(panel) = content.notification_panel.as_ref() else {
-            return;
-        };
-        self.button.merge_from(&panel.button);
-        self.dock.merge_from(&panel.dock.map(Into::into));
-        self.default_width.merge_from(&panel.default_width.map(px));
-    }
-
-    fn import_from_vscode(
-        _vscode: &settings::VsCodeSettings,
-        _current: &mut settings::SettingsContent,
-    ) {
-    }
 }

crates/collections/src/collections.rs 🔗

@@ -22,6 +22,7 @@ pub type IndexMap<K, V> = indexmap::IndexMap<K, V>;
 #[cfg(not(feature = "test-support"))]
 pub type IndexSet<T> = indexmap::IndexSet<T>;
 
+pub use indexmap::Equivalent;
 pub use rustc_hash::FxHasher;
 pub use rustc_hash::{FxHashMap, FxHashSet};
 pub use std::collections::*;

crates/dap/src/debugger_settings.rs 🔗

@@ -1,7 +1,6 @@
 use dap_types::SteppingGranularity;
 use gpui::App;
 use settings::{Settings, SettingsContent};
-use util::MergeFrom;
 
 pub struct DebuggerSettings {
     /// Determines the stepping granularity.
@@ -35,7 +34,7 @@ pub struct DebuggerSettings {
 }
 
 impl Settings for DebuggerSettings {
-    fn from_defaults(content: &SettingsContent, _cx: &mut App) -> Self {
+    fn from_settings(content: &SettingsContent, _cx: &mut App) -> Self {
         let content = content.debugger.clone().unwrap();
         Self {
             stepping_granularity: dap_granularity_from_settings(
@@ -49,27 +48,6 @@ impl Settings for DebuggerSettings {
             dock: content.dock.unwrap(),
         }
     }
-
-    fn refine(&mut self, content: &SettingsContent, _cx: &mut App) {
-        let Some(content) = &content.debugger else {
-            return;
-        };
-        self.stepping_granularity.merge_from(
-            &content
-                .stepping_granularity
-                .map(dap_granularity_from_settings),
-        );
-        self.save_breakpoints.merge_from(&content.save_breakpoints);
-        self.button.merge_from(&content.button);
-        self.timeout.merge_from(&content.timeout);
-        self.log_dap_communications
-            .merge_from(&content.log_dap_communications);
-        self.format_dap_log_messages
-            .merge_from(&content.format_dap_log_messages);
-        self.dock.merge_from(&content.dock);
-    }
-
-    fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut SettingsContent) {}
 }
 
 fn dap_granularity_from_settings(

crates/editor/src/editor_settings.rs 🔗

@@ -12,7 +12,6 @@ pub use settings::{
 };
 use settings::{Settings, SettingsContent};
 use ui::scrollbars::{ScrollbarVisibility, ShowScrollbar};
-use util::MergeFrom;
 
 /// Imports from the VSCode settings at
 /// https://code.visualstudio.com/docs/reference/default-settings
@@ -190,7 +189,7 @@ impl ScrollbarVisibility for EditorSettings {
 }
 
 impl Settings for EditorSettings {
-    fn from_defaults(content: &settings::SettingsContent, _cx: &mut App) -> Self {
+    fn from_settings(content: &settings::SettingsContent, _cx: &mut App) -> Self {
         let editor = content.editor.clone();
         let scrollbar = editor.scrollbar.unwrap();
         let minimap = editor.minimap.unwrap();
@@ -238,7 +237,7 @@ impl Settings for EditorSettings {
                 display_in: minimap.display_in.unwrap(),
                 thumb: minimap.thumb.unwrap(),
                 thumb_border: minimap.thumb_border.unwrap(),
-                current_line_highlight: minimap.current_line_highlight.flatten(),
+                current_line_highlight: minimap.current_line_highlight,
                 max_width_columns: minimap.max_width_columns.unwrap(),
             },
             gutter: Gutter {
@@ -290,162 +289,6 @@ impl Settings for EditorSettings {
         }
     }
 
-    fn refine(&mut self, content: &settings::SettingsContent, _cx: &mut App) {
-        let editor = &content.editor;
-        self.cursor_blink.merge_from(&editor.cursor_blink);
-        if let Some(cursor_shape) = editor.cursor_shape {
-            self.cursor_shape = Some(cursor_shape.into())
-        }
-        self.current_line_highlight
-            .merge_from(&editor.current_line_highlight);
-        self.selection_highlight
-            .merge_from(&editor.selection_highlight);
-        self.rounded_selection.merge_from(&editor.rounded_selection);
-        self.lsp_highlight_debounce
-            .merge_from(&editor.lsp_highlight_debounce);
-        self.hover_popover_enabled
-            .merge_from(&editor.hover_popover_enabled);
-        self.hover_popover_delay
-            .merge_from(&editor.hover_popover_delay);
-        self.scroll_beyond_last_line
-            .merge_from(&editor.scroll_beyond_last_line);
-        self.vertical_scroll_margin
-            .merge_from(&editor.vertical_scroll_margin);
-        self.autoscroll_on_clicks
-            .merge_from(&editor.autoscroll_on_clicks);
-        self.horizontal_scroll_margin
-            .merge_from(&editor.horizontal_scroll_margin);
-        self.scroll_sensitivity
-            .merge_from(&editor.scroll_sensitivity);
-        self.fast_scroll_sensitivity
-            .merge_from(&editor.fast_scroll_sensitivity);
-        self.relative_line_numbers
-            .merge_from(&editor.relative_line_numbers);
-        self.seed_search_query_from_cursor
-            .merge_from(&editor.seed_search_query_from_cursor);
-        self.use_smartcase_search
-            .merge_from(&editor.use_smartcase_search);
-        self.multi_cursor_modifier
-            .merge_from(&editor.multi_cursor_modifier);
-        self.redact_private_values
-            .merge_from(&editor.redact_private_values);
-        self.expand_excerpt_lines
-            .merge_from(&editor.expand_excerpt_lines);
-        self.excerpt_context_lines
-            .merge_from(&editor.excerpt_context_lines);
-        self.middle_click_paste
-            .merge_from(&editor.middle_click_paste);
-        self.double_click_in_multibuffer
-            .merge_from(&editor.double_click_in_multibuffer);
-        self.search_wrap.merge_from(&editor.search_wrap);
-        self.auto_signature_help
-            .merge_from(&editor.auto_signature_help);
-        self.show_signature_help_after_edits
-            .merge_from(&editor.show_signature_help_after_edits);
-        self.go_to_definition_fallback
-            .merge_from(&editor.go_to_definition_fallback);
-        if let Some(hide_mouse) = editor.hide_mouse {
-            self.hide_mouse = Some(hide_mouse)
-        }
-        self.snippet_sort_order
-            .merge_from(&editor.snippet_sort_order);
-        if let Some(diagnostics_max_severity) = editor.diagnostics_max_severity {
-            self.diagnostics_max_severity = Some(diagnostics_max_severity.into());
-        }
-        self.inline_code_actions
-            .merge_from(&editor.inline_code_actions);
-        self.lsp_document_colors
-            .merge_from(&editor.lsp_document_colors);
-        self.minimum_contrast_for_highlights
-            .merge_from(&editor.minimum_contrast_for_highlights);
-
-        if let Some(status_bar) = &editor.status_bar {
-            self.status_bar
-                .active_language_button
-                .merge_from(&status_bar.active_language_button);
-            self.status_bar
-                .cursor_position_button
-                .merge_from(&status_bar.cursor_position_button);
-        }
-        if let Some(toolbar) = &editor.toolbar {
-            self.toolbar.breadcrumbs.merge_from(&toolbar.breadcrumbs);
-            self.toolbar
-                .quick_actions
-                .merge_from(&toolbar.quick_actions);
-            self.toolbar
-                .selections_menu
-                .merge_from(&toolbar.selections_menu);
-            self.toolbar.agent_review.merge_from(&toolbar.agent_review);
-            self.toolbar.code_actions.merge_from(&toolbar.code_actions);
-        }
-        if let Some(scrollbar) = &editor.scrollbar {
-            self.scrollbar
-                .show
-                .merge_from(&scrollbar.show.map(Into::into));
-            self.scrollbar.git_diff.merge_from(&scrollbar.git_diff);
-            self.scrollbar
-                .selected_text
-                .merge_from(&scrollbar.selected_text);
-            self.scrollbar
-                .selected_symbol
-                .merge_from(&scrollbar.selected_symbol);
-            self.scrollbar
-                .search_results
-                .merge_from(&scrollbar.search_results);
-            self.scrollbar
-                .diagnostics
-                .merge_from(&scrollbar.diagnostics);
-            self.scrollbar.cursors.merge_from(&scrollbar.cursors);
-            if let Some(axes) = &scrollbar.axes {
-                self.scrollbar.axes.horizontal.merge_from(&axes.horizontal);
-                self.scrollbar.axes.vertical.merge_from(&axes.vertical);
-            }
-        }
-        if let Some(minimap) = &editor.minimap {
-            self.minimap.show.merge_from(&minimap.show);
-            self.minimap.display_in.merge_from(&minimap.display_in);
-            self.minimap.thumb.merge_from(&minimap.thumb);
-            self.minimap.thumb_border.merge_from(&minimap.thumb_border);
-            self.minimap
-                .current_line_highlight
-                .merge_from(&minimap.current_line_highlight);
-            self.minimap
-                .max_width_columns
-                .merge_from(&minimap.max_width_columns);
-        }
-        if let Some(gutter) = &editor.gutter {
-            self.gutter
-                .min_line_number_digits
-                .merge_from(&gutter.min_line_number_digits);
-            self.gutter.line_numbers.merge_from(&gutter.line_numbers);
-            self.gutter.runnables.merge_from(&gutter.runnables);
-            self.gutter.breakpoints.merge_from(&gutter.breakpoints);
-            self.gutter.folds.merge_from(&gutter.folds);
-        }
-        if let Some(search) = &editor.search {
-            self.search.button.merge_from(&search.button);
-            self.search.whole_word.merge_from(&search.whole_word);
-            self.search
-                .case_sensitive
-                .merge_from(&search.case_sensitive);
-            self.search
-                .include_ignored
-                .merge_from(&search.include_ignored);
-            self.search.regex.merge_from(&search.regex);
-        }
-        if let Some(enabled) = editor.jupyter.as_ref().and_then(|jupyter| jupyter.enabled) {
-            self.jupyter.enabled = enabled;
-        }
-        if let Some(drag_and_drop_selection) = &editor.drag_and_drop_selection {
-            self.drag_and_drop_selection
-                .enabled
-                .merge_from(&drag_and_drop_selection.enabled);
-            self.drag_and_drop_selection
-                .delay
-                .merge_from(&drag_and_drop_selection.delay);
-        }
-    }
-
     fn import_from_vscode(vscode: &VsCodeSettings, current: &mut SettingsContent) {
         vscode.enum_setting(
             "editor.cursorBlinking",

crates/extension_host/src/extension_settings.rs 🔗

@@ -33,26 +33,10 @@ impl ExtensionSettings {
 }
 
 impl Settings for ExtensionSettings {
-    fn from_defaults(content: &settings::SettingsContent, _cx: &mut App) -> Self {
+    fn from_settings(content: &settings::SettingsContent, _cx: &mut App) -> Self {
         Self {
             auto_install_extensions: content.extension.auto_install_extensions.clone(),
             auto_update_extensions: content.extension.auto_update_extensions.clone(),
         }
     }
-
-    fn refine(&mut self, content: &settings::SettingsContent, _cx: &mut App) {
-        self.auto_install_extensions
-            .extend(content.extension.auto_install_extensions.clone());
-        self.auto_update_extensions
-            .extend(content.extension.auto_update_extensions.clone());
-    }
-
-    fn import_from_vscode(
-        _vscode: &settings::VsCodeSettings,
-        _current: &mut settings::SettingsContent,
-    ) {
-        // settingsSync.ignoredExtensions controls autoupdate for vscode extensions, but we
-        // don't have a mapping to zed-extensions. there's also extensions.autoCheckUpdates
-        // and extensions.autoUpdate which are global switches, we don't support those yet
-    }
 }

crates/file_finder/src/file_finder_settings.rs 🔗

@@ -1,7 +1,6 @@
 use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
 use settings::Settings;
-use util::MergeFrom;
 
 #[derive(Deserialize, Debug, Clone, Copy, PartialEq)]
 pub struct FileFinderSettings {
@@ -12,30 +11,16 @@ pub struct FileFinderSettings {
 }
 
 impl Settings for FileFinderSettings {
-    fn from_defaults(content: &settings::SettingsContent, _cx: &mut ui::App) -> Self {
+    fn from_settings(content: &settings::SettingsContent, _cx: &mut ui::App) -> Self {
         let file_finder = content.file_finder.as_ref().unwrap();
 
         Self {
             file_icons: file_finder.file_icons.unwrap(),
             modal_max_width: file_finder.modal_max_width.unwrap().into(),
             skip_focus_for_active_in_search: file_finder.skip_focus_for_active_in_search.unwrap(),
-            include_ignored: file_finder.include_ignored.flatten(),
+            include_ignored: file_finder.include_ignored,
         }
     }
-
-    fn refine(&mut self, content: &settings::SettingsContent, _cx: &mut ui::App) {
-        let Some(file_finder) = content.file_finder.as_ref() else {
-            return;
-        };
-
-        self.file_icons.merge_from(&file_finder.file_icons);
-        self.modal_max_width
-            .merge_from(&file_finder.modal_max_width.map(Into::into));
-        self.skip_focus_for_active_in_search
-            .merge_from(&file_finder.skip_focus_for_active_in_search);
-        self.include_ignored
-            .merge_from(&file_finder.include_ignored);
-    }
 }
 
 #[derive(Debug, PartialEq, Eq, Clone, Copy, Default, Serialize, Deserialize, JsonSchema)]

crates/git_hosting_providers/src/settings.rs 🔗

@@ -58,17 +58,14 @@ pub struct GitHostingProviderSettings {
 }
 
 impl Settings for GitHostingProviderSettings {
-    fn from_defaults(content: &settings::SettingsContent, _cx: &mut App) -> Self {
+    fn from_settings(content: &settings::SettingsContent, _cx: &mut App) -> Self {
         Self {
-            git_hosting_providers: content.project.git_hosting_providers.clone().unwrap(),
+            git_hosting_providers: content
+                .project
+                .git_hosting_providers
+                .clone()
+                .unwrap()
+                .into(),
         }
     }
-
-    fn refine(&mut self, content: &settings::SettingsContent, _: &mut App) {
-        if let Some(more) = &content.project.git_hosting_providers {
-            self.git_hosting_providers.extend_from_slice(&more.clone());
-        }
-    }
-
-    fn import_from_vscode(_: &settings::VsCodeSettings, _: &mut settings::SettingsContent) {}
 }

crates/git_ui/src/git_panel_settings.rs 🔗

@@ -7,7 +7,6 @@ use ui::{
     px,
     scrollbars::{ScrollbarVisibility, ShowScrollbar},
 };
-use util::MergeFrom;
 use workspace::dock::DockPosition;
 
 #[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
@@ -52,7 +51,7 @@ impl ScrollbarVisibility for GitPanelSettings {
 }
 
 impl Settings for GitPanelSettings {
-    fn from_defaults(content: &settings::SettingsContent, _cx: &mut ui::App) -> Self {
+    fn from_settings(content: &settings::SettingsContent, _cx: &mut ui::App) -> Self {
         let git_panel = content.git_panel.clone().unwrap();
         Self {
             button: git_panel.button.unwrap(),
@@ -68,25 +67,6 @@ impl Settings for GitPanelSettings {
         }
     }
 
-    fn refine(&mut self, content: &settings::SettingsContent, _cx: &mut ui::App) {
-        let Some(git_panel) = &content.git_panel else {
-            return;
-        };
-        self.button.merge_from(&git_panel.button);
-        self.dock.merge_from(&git_panel.dock.map(Into::into));
-        self.default_width
-            .merge_from(&git_panel.default_width.map(px));
-        self.status_style.merge_from(&git_panel.status_style);
-        self.fallback_branch_name
-            .merge_from(&git_panel.fallback_branch_name);
-        self.sort_by_path.merge_from(&git_panel.sort_by_path);
-        self.collapse_untracked_diff
-            .merge_from(&git_panel.collapse_untracked_diff);
-        if let Some(show) = git_panel.scrollbar.as_ref().and_then(|s| s.show) {
-            self.scrollbar.show = Some(show.into())
-        }
-    }
-
     fn import_from_vscode(vscode: &settings::VsCodeSettings, current: &mut SettingsContent) {
         if let Some(git_enabled) = vscode.read_bool("git.enabled") {
             current.git_panel.get_or_insert_default().button = Some(git_enabled);

crates/go_to_line/src/cursor_position.rs 🔗

@@ -7,7 +7,7 @@ use ui::{
     Button, ButtonCommon, Clickable, Context, FluentBuilder, IntoElement, LabelSize, ParentElement,
     Render, Tooltip, Window, div,
 };
-use util::{MergeFrom, paths::FILE_ROW_COLUMN_DELIMITER};
+use util::paths::FILE_ROW_COLUMN_DELIMITER;
 use workspace::{StatusItemView, Workspace, item::ItemHandle};
 
 #[derive(Copy, Clone, Debug, Default, PartialOrd, PartialEq)]
@@ -307,11 +307,7 @@ impl From<settings::LineIndicatorFormat> for LineIndicatorFormat {
 }
 
 impl Settings for LineIndicatorFormat {
-    fn from_defaults(content: &settings::SettingsContent, _cx: &mut App) -> Self {
+    fn from_settings(content: &settings::SettingsContent, _cx: &mut App) -> Self {
         content.line_indicator_format.unwrap().into()
     }
-
-    fn refine(&mut self, content: &settings::SettingsContent, _cx: &mut App) {
-        self.merge_from(&content.line_indicator_format.map(Into::into));
-    }
 }

crates/image_viewer/src/image_viewer_settings.rs 🔗

@@ -1,7 +1,6 @@
 use gpui::App;
 pub use settings::ImageFileSizeUnit;
 use settings::Settings;
-use util::MergeFrom;
 
 /// The settings for the image viewer.
 #[derive(Clone, Debug, Default)]
@@ -13,18 +12,9 @@ pub struct ImageViewerSettings {
 }
 
 impl Settings for ImageViewerSettings {
-    fn from_defaults(content: &settings::SettingsContent, _cx: &mut App) -> Self {
+    fn from_settings(content: &settings::SettingsContent, _cx: &mut App) -> Self {
         Self {
             unit: content.image_viewer.clone().unwrap().unit.unwrap(),
         }
     }
-
-    fn refine(&mut self, content: &settings::SettingsContent, _cx: &mut App) {
-        self.unit.merge_from(
-            &content
-                .image_viewer
-                .as_ref()
-                .and_then(|image_viewer| image_viewer.unit),
-        );
-    }
 }

crates/journal/Cargo.toml 🔗

@@ -23,7 +23,6 @@ settings.workspace = true
 shellexpand.workspace = true
 workspace.workspace = true
 workspace-hack.workspace = true
-util.workspace = true
 
 [dev-dependencies]
 editor = { workspace = true, features = ["test-support"] }

crates/journal/src/journal.rs 🔗

@@ -9,7 +9,6 @@ use std::{
     path::{Path, PathBuf},
     sync::Arc,
 };
-use util::MergeFrom;
 use workspace::{AppState, OpenVisible, Workspace};
 
 actions!(
@@ -34,7 +33,7 @@ pub struct JournalSettings {
 }
 
 impl settings::Settings for JournalSettings {
-    fn from_defaults(content: &settings::SettingsContent, _cx: &mut App) -> Self {
+    fn from_settings(content: &settings::SettingsContent, _cx: &mut App) -> Self {
         let journal = content.journal.clone().unwrap();
 
         Self {
@@ -42,14 +41,6 @@ impl settings::Settings for JournalSettings {
             hour_format: journal.hour_format.unwrap(),
         }
     }
-
-    fn refine(&mut self, content: &settings::SettingsContent, _cx: &mut App) {
-        let Some(journal) = content.journal.as_ref() else {
-            return;
-        };
-        self.path.merge_from(&journal.path);
-        self.hour_format.merge_from(&journal.hour_format);
-    }
 }
 
 pub fn init(_: Arc<AppState>, cx: &mut App) {

crates/language/Cargo.toml 🔗

@@ -39,7 +39,6 @@ globset.workspace = true
 gpui.workspace = true
 http_client.workspace = true
 imara-diff.workspace = true
-inventory.workspace = true
 itertools.workspace = true
 log.workspace = true
 lsp.workspace = true

crates/language/src/buffer_tests.rs 🔗

@@ -269,15 +269,15 @@ async fn test_language_for_file_with_custom_file_types(cx: &mut TestAppContext)
     cx.update(|cx| {
         init_settings(cx, |settings| {
             settings.file_types.extend([
-                ("TypeScript".into(), vec!["js".into()]),
+                ("TypeScript".into(), vec!["js".into()].into()),
                 (
                     "JavaScript".into(),
-                    vec!["*longer.ts".into(), "ecmascript".into()],
+                    vec!["*longer.ts".into(), "ecmascript".into()].into(),
                 ),
-                ("C++".into(), vec!["c".into(), "*.dev".into()]),
+                ("C++".into(), vec!["c".into(), "*.dev".into()].into()),
                 (
                     "Dockerfile".into(),
-                    vec!["Dockerfile".into(), "Dockerfile.*".into()],
+                    vec!["Dockerfile".into(), "Dockerfile.*".into()].into(),
                 ),
             ]);
         })

crates/language/src/language_settings.rs 🔗

@@ -9,22 +9,15 @@ use ec4rs::{
 use globset::{Glob, GlobMatcher, GlobSet, GlobSetBuilder};
 use gpui::{App, Modifiers};
 use itertools::{Either, Itertools};
-use schemars::json_schema;
 
 pub use settings::{
     CompletionSettingsContent, EditPredictionProvider, EditPredictionsMode, FormatOnSave,
     Formatter, FormatterList, InlayHintKind, LanguageSettingsContent, LspInsertMode,
     RewrapBehavior, SelectedFormatter, ShowWhitespaceSetting, SoftWrap, WordsCompletionMode,
 };
-use settings::{
-    IndentGuideSettingsContent, LanguageTaskSettingsContent, ParameterizedJsonSchema,
-    PrettierSettingsContent, Settings, SettingsContent, SettingsLocation, SettingsStore,
-    SettingsUi,
-};
+use settings::{ExtendingVec, Settings, SettingsContent, SettingsLocation, SettingsStore};
 use shellexpand;
 use std::{borrow::Cow, num::NonZeroU32, path::Path, sync::Arc};
-use util::MergeFrom;
-use util::{ResultExt, schemars::replace_subschema};
 
 /// Initializes the language settings.
 pub fn init(cx: &mut App) {
@@ -64,7 +57,6 @@ pub struct AllLanguageSettings {
     pub defaults: LanguageSettings,
     languages: HashMap<LanguageName, LanguageSettings>,
     pub(crate) file_types: FxHashMap<Arc<str>, GlobSet>,
-    pub(crate) file_globs: FxHashMap<Arc<str>, Vec<String>>,
 }
 
 /// The settings for a particular language.
@@ -189,18 +181,6 @@ pub struct CompletionSettings {
     pub lsp_insert_mode: LspInsertMode,
 }
 
-impl CompletionSettings {
-    pub fn merge_from(&mut self, src: &Option<CompletionSettingsContent>) {
-        let Some(src) = src else { return };
-        self.words.merge_from(&src.words);
-        self.words_min_length.merge_from(&src.words_min_length);
-        self.lsp.merge_from(&src.lsp);
-        self.lsp_fetch_timeout_ms
-            .merge_from(&src.lsp_fetch_timeout_ms);
-        self.lsp_insert_mode.merge_from(&src.lsp_insert_mode);
-    }
-}
-
 /// The settings for indent guides.
 #[derive(Debug, Clone, PartialEq)]
 pub struct IndentGuideSettings {
@@ -226,19 +206,6 @@ pub struct IndentGuideSettings {
     pub background_coloring: settings::IndentGuideBackgroundColoring,
 }
 
-impl IndentGuideSettings {
-    pub fn merge_from(&mut self, src: &Option<IndentGuideSettingsContent>) {
-        let Some(src) = src else { return };
-
-        self.enabled.merge_from(&src.enabled);
-        self.line_width.merge_from(&src.line_width);
-        self.active_line_width.merge_from(&src.active_line_width);
-        self.coloring.merge_from(&src.coloring);
-        self.background_coloring
-            .merge_from(&src.background_coloring);
-    }
-}
-
 #[derive(Debug, Clone)]
 pub struct LanguageTaskSettings {
     /// Extra task variables to set for a particular language.
@@ -254,17 +221,6 @@ pub struct LanguageTaskSettings {
     pub prefer_lsp: bool,
 }
 
-impl LanguageTaskSettings {
-    pub fn merge_from(&mut self, src: &Option<LanguageTaskSettingsContent>) {
-        let Some(src) = src.clone() else {
-            return;
-        };
-        self.variables.extend(src.variables);
-        self.enabled.merge_from(&src.enabled);
-        self.prefer_lsp.merge_from(&src.prefer_lsp);
-    }
-}
-
 /// Allows to enable/disable formatting with Prettier
 /// and configure default Prettier, used when no project-level Prettier installation is found.
 /// Prettier formatting is disabled by default.
@@ -285,16 +241,6 @@ pub struct PrettierSettings {
     pub options: HashMap<String, serde_json::Value>,
 }
 
-impl PrettierSettings {
-    pub fn merge_from(&mut self, src: &Option<PrettierSettingsContent>) {
-        let Some(src) = src.clone() else { return };
-        self.allowed.merge_from(&src.allowed);
-        self.parser = src.parser.clone();
-        self.plugins.extend(src.plugins);
-        self.options.extend(src.options);
-    }
-}
-
 impl LanguageSettings {
     /// A token representing the rest of the available language servers.
     const REST_OF_LANGUAGE_SERVERS: &'static str = "...";
@@ -413,14 +359,13 @@ impl InlayHintSettings {
 
 /// The settings for edit predictions, such as [GitHub Copilot](https://github.com/features/copilot)
 /// or [Supermaven](https://supermaven.com).
-#[derive(Clone, Debug, Default, SettingsUi)]
+#[derive(Clone, Debug, Default)]
 pub struct EditPredictionSettings {
     /// The provider that supplies edit predictions.
     pub provider: settings::EditPredictionProvider,
     /// A list of globs representing files that edit predictions should be disabled for.
     /// This list adds to a pre-existing, sensible default set of globs.
     /// Any additional ones you add are combined with them.
-    #[settings_ui(skip)]
     pub disabled_globs: Vec<DisabledGlob>,
     /// Configures how edit predictions are displayed in the buffer.
     pub mode: settings::EditPredictionsMode,
@@ -451,41 +396,16 @@ pub struct DisabledGlob {
     is_absolute: bool,
 }
 
-#[derive(Clone, Debug, Default, SettingsUi)]
+#[derive(Clone, Debug, Default)]
 pub struct CopilotSettings {
     /// HTTP/HTTPS proxy to use for Copilot.
-    #[settings_ui(skip)]
     pub proxy: Option<String>,
     /// Disable certificate verification for proxy (not recommended).
     pub proxy_no_verify: Option<bool>,
     /// Enterprise URI for Copilot.
-    #[settings_ui(skip)]
     pub enterprise_uri: Option<String>,
 }
 
-inventory::submit! {
-    ParameterizedJsonSchema {
-        add_and_get_ref: |generator, params, _cx| {
-            let language_settings_content_ref = generator
-                .subschema_for::<LanguageSettingsContent>()
-                .to_value();
-            replace_subschema::<settings::LanguageToSettingsMap>(generator, || json_schema!({
-                "type": "object",
-                "properties": params
-                    .language_names
-                    .iter()
-                    .map(|name| {
-                        (
-                            name.clone(),
-                            language_settings_content_ref.clone(),
-                        )
-                    })
-                    .collect::<serde_json::Map<_, _>>()
-            }))
-        }
-    }
-}
-
 impl AllLanguageSettings {
     /// Returns the [`LanguageSettings`] for the language with the specified name.
     pub fn language<'a>(
@@ -574,93 +494,99 @@ fn merge_with_editorconfig(settings: &mut LanguageSettings, cfg: &EditorconfigPr
 }
 
 impl settings::Settings for AllLanguageSettings {
-    fn from_defaults(content: &settings::SettingsContent, _cx: &mut App) -> Self {
+    fn from_settings(content: &settings::SettingsContent, _cx: &mut App) -> Self {
         let all_languages = &content.project.all_languages;
-        let defaults = all_languages.defaults.clone();
-        let inlay_hints = defaults.inlay_hints.unwrap();
-        let completions = defaults.completions.unwrap();
-        let prettier = defaults.prettier.unwrap();
-        let indent_guides = defaults.indent_guides.unwrap();
-        let tasks = defaults.tasks.unwrap();
-
-        let default_language_settings = LanguageSettings {
-            tab_size: defaults.tab_size.unwrap(),
-            hard_tabs: defaults.hard_tabs.unwrap(),
-            soft_wrap: defaults.soft_wrap.unwrap(),
-            preferred_line_length: defaults.preferred_line_length.unwrap(),
-            show_wrap_guides: defaults.show_wrap_guides.unwrap(),
-            wrap_guides: defaults.wrap_guides.unwrap(),
-            indent_guides: IndentGuideSettings {
-                enabled: indent_guides.enabled.unwrap(),
-                line_width: indent_guides.line_width.unwrap(),
-                active_line_width: indent_guides.active_line_width.unwrap(),
-                coloring: indent_guides.coloring.unwrap(),
-                background_coloring: indent_guides.background_coloring.unwrap(),
-            },
-            format_on_save: defaults.format_on_save.unwrap(),
-            remove_trailing_whitespace_on_save: defaults
-                .remove_trailing_whitespace_on_save
-                .unwrap(),
-            ensure_final_newline_on_save: defaults.ensure_final_newline_on_save.unwrap(),
-            formatter: defaults.formatter.unwrap(),
-            prettier: PrettierSettings {
-                allowed: prettier.allowed.unwrap(),
-                parser: prettier.parser,
-                plugins: prettier.plugins,
-                options: prettier.options,
-            },
-            jsx_tag_auto_close: defaults.jsx_tag_auto_close.unwrap().enabled.unwrap(),
-            enable_language_server: defaults.enable_language_server.unwrap(),
-            language_servers: defaults.language_servers.unwrap(),
-            allow_rewrap: defaults.allow_rewrap.unwrap(),
-            show_edit_predictions: defaults.show_edit_predictions.unwrap(),
-            edit_predictions_disabled_in: defaults.edit_predictions_disabled_in.unwrap(),
-            show_whitespaces: defaults.show_whitespaces.unwrap(),
-            whitespace_map: defaults.whitespace_map.unwrap(),
-            extend_comment_on_newline: defaults.extend_comment_on_newline.unwrap(),
-            inlay_hints: InlayHintSettings {
-                enabled: inlay_hints.enabled.unwrap(),
-                show_value_hints: inlay_hints.show_value_hints.unwrap(),
-                show_type_hints: inlay_hints.show_type_hints.unwrap(),
-                show_parameter_hints: inlay_hints.show_parameter_hints.unwrap(),
-                show_other_hints: inlay_hints.show_other_hints.unwrap(),
-                show_background: inlay_hints.show_background.unwrap(),
-                edit_debounce_ms: inlay_hints.edit_debounce_ms.unwrap(),
-                scroll_debounce_ms: inlay_hints.scroll_debounce_ms.unwrap(),
-                toggle_on_modifiers_press: inlay_hints.toggle_on_modifiers_press,
-            },
-            use_autoclose: defaults.use_autoclose.unwrap(),
-            use_auto_surround: defaults.use_auto_surround.unwrap(),
-            use_on_type_format: defaults.use_on_type_format.unwrap(),
-            auto_indent: defaults.auto_indent.unwrap(),
-            auto_indent_on_paste: defaults.auto_indent_on_paste.unwrap(),
-            always_treat_brackets_as_autoclosed: defaults
-                .always_treat_brackets_as_autoclosed
-                .unwrap(),
-            code_actions_on_format: defaults.code_actions_on_format.unwrap(),
-            linked_edits: defaults.linked_edits.unwrap(),
-            tasks: LanguageTaskSettings {
-                variables: tasks.variables,
-                enabled: tasks.enabled.unwrap(),
-                prefer_lsp: tasks.prefer_lsp.unwrap(),
-            },
-            show_completions_on_input: defaults.show_completions_on_input.unwrap(),
-            show_completion_documentation: defaults.show_completion_documentation.unwrap(),
-            completions: CompletionSettings {
-                words: completions.words.unwrap(),
-                words_min_length: completions.words_min_length.unwrap(),
-                lsp: completions.lsp.unwrap(),
-                lsp_fetch_timeout_ms: completions.lsp_fetch_timeout_ms.unwrap(),
-                lsp_insert_mode: completions.lsp_insert_mode.unwrap(),
-            },
-            debuggers: defaults.debuggers.unwrap(),
-        };
+
+        fn load_from_content(settings: LanguageSettingsContent) -> LanguageSettings {
+            let inlay_hints = settings.inlay_hints.unwrap();
+            let completions = settings.completions.unwrap();
+            let prettier = settings.prettier.unwrap();
+            let indent_guides = settings.indent_guides.unwrap();
+            let tasks = settings.tasks.unwrap();
+            LanguageSettings {
+                tab_size: settings.tab_size.unwrap(),
+                hard_tabs: settings.hard_tabs.unwrap(),
+                soft_wrap: settings.soft_wrap.unwrap(),
+                preferred_line_length: settings.preferred_line_length.unwrap(),
+                show_wrap_guides: settings.show_wrap_guides.unwrap(),
+                wrap_guides: settings.wrap_guides.unwrap(),
+                indent_guides: IndentGuideSettings {
+                    enabled: indent_guides.enabled.unwrap(),
+                    line_width: indent_guides.line_width.unwrap(),
+                    active_line_width: indent_guides.active_line_width.unwrap(),
+                    coloring: indent_guides.coloring.unwrap(),
+                    background_coloring: indent_guides.background_coloring.unwrap(),
+                },
+                format_on_save: settings.format_on_save.unwrap(),
+                remove_trailing_whitespace_on_save: settings
+                    .remove_trailing_whitespace_on_save
+                    .unwrap(),
+                ensure_final_newline_on_save: settings.ensure_final_newline_on_save.unwrap(),
+                formatter: settings.formatter.unwrap(),
+                prettier: PrettierSettings {
+                    allowed: prettier.allowed.unwrap(),
+                    parser: prettier.parser,
+                    plugins: prettier.plugins,
+                    options: prettier.options,
+                },
+                jsx_tag_auto_close: settings.jsx_tag_auto_close.unwrap().enabled.unwrap(),
+                enable_language_server: settings.enable_language_server.unwrap(),
+                language_servers: settings.language_servers.unwrap(),
+                allow_rewrap: settings.allow_rewrap.unwrap(),
+                show_edit_predictions: settings.show_edit_predictions.unwrap(),
+                edit_predictions_disabled_in: settings.edit_predictions_disabled_in.unwrap(),
+                show_whitespaces: settings.show_whitespaces.unwrap(),
+                whitespace_map: settings.whitespace_map.unwrap(),
+                extend_comment_on_newline: settings.extend_comment_on_newline.unwrap(),
+                inlay_hints: InlayHintSettings {
+                    enabled: inlay_hints.enabled.unwrap(),
+                    show_value_hints: inlay_hints.show_value_hints.unwrap(),
+                    show_type_hints: inlay_hints.show_type_hints.unwrap(),
+                    show_parameter_hints: inlay_hints.show_parameter_hints.unwrap(),
+                    show_other_hints: inlay_hints.show_other_hints.unwrap(),
+                    show_background: inlay_hints.show_background.unwrap(),
+                    edit_debounce_ms: inlay_hints.edit_debounce_ms.unwrap(),
+                    scroll_debounce_ms: inlay_hints.scroll_debounce_ms.unwrap(),
+                    toggle_on_modifiers_press: inlay_hints.toggle_on_modifiers_press,
+                },
+                use_autoclose: settings.use_autoclose.unwrap(),
+                use_auto_surround: settings.use_auto_surround.unwrap(),
+                use_on_type_format: settings.use_on_type_format.unwrap(),
+                auto_indent: settings.auto_indent.unwrap(),
+                auto_indent_on_paste: settings.auto_indent_on_paste.unwrap(),
+                always_treat_brackets_as_autoclosed: settings
+                    .always_treat_brackets_as_autoclosed
+                    .unwrap(),
+                code_actions_on_format: settings.code_actions_on_format.unwrap(),
+                linked_edits: settings.linked_edits.unwrap(),
+                tasks: LanguageTaskSettings {
+                    variables: tasks.variables,
+                    enabled: tasks.enabled.unwrap(),
+                    prefer_lsp: tasks.prefer_lsp.unwrap(),
+                },
+                show_completions_on_input: settings.show_completions_on_input.unwrap(),
+                show_completion_documentation: settings.show_completion_documentation.unwrap(),
+                completions: CompletionSettings {
+                    words: completions.words.unwrap(),
+                    words_min_length: completions.words_min_length.unwrap(),
+                    lsp: completions.lsp.unwrap(),
+                    lsp_fetch_timeout_ms: completions.lsp_fetch_timeout_ms.unwrap(),
+                    lsp_insert_mode: completions.lsp_insert_mode.unwrap(),
+                },
+                debuggers: settings.debuggers.unwrap(),
+            }
+        }
+
+        let default_language_settings = load_from_content(all_languages.defaults.clone());
 
         let mut languages = HashMap::default();
         for (language_name, settings) in &all_languages.languages.0 {
-            let mut language_settings = default_language_settings.clone();
-            merge_settings(&mut language_settings, settings);
-            languages.insert(LanguageName(language_name.clone()), language_settings);
+            let mut language_settings = all_languages.defaults.clone();
+            settings::merge_from::MergeFrom::merge_from(&mut language_settings, Some(settings));
+            languages.insert(
+                LanguageName(language_name.clone()),
+                load_from_content(language_settings),
+            );
         }
 
         let edit_prediction_provider = all_languages
@@ -688,17 +614,15 @@ impl settings::Settings for AllLanguageSettings {
         let enabled_in_text_threads = edit_predictions.enabled_in_text_threads.unwrap();
 
         let mut file_types: FxHashMap<Arc<str>, GlobSet> = FxHashMap::default();
-        let mut file_globs: FxHashMap<Arc<str>, Vec<String>> = FxHashMap::default();
 
         for (language, patterns) in &all_languages.file_types {
             let mut builder = GlobSetBuilder::new();
 
-            for pattern in patterns {
+            for pattern in &patterns.0 {
                 builder.add(Glob::new(pattern).unwrap());
             }
 
             file_types.insert(language.clone(), builder.build().unwrap());
-            file_globs.insert(language.clone(), patterns.clone());
         }
 
         Self {
@@ -725,110 +649,6 @@ impl settings::Settings for AllLanguageSettings {
             defaults: default_language_settings,
             languages,
             file_types,
-            file_globs,
-        }
-    }
-
-    fn refine(&mut self, content: &SettingsContent, _cx: &mut App) {
-        let all_languages = &content.project.all_languages;
-        if let Some(provider) = all_languages
-            .features
-            .as_ref()
-            .and_then(|f| f.edit_prediction_provider)
-        {
-            self.edit_predictions.provider = provider;
-        }
-
-        if let Some(edit_predictions) = all_languages.edit_predictions.as_ref() {
-            self.edit_predictions
-                .mode
-                .merge_from(&edit_predictions.mode);
-            self.edit_predictions
-                .enabled_in_text_threads
-                .merge_from(&edit_predictions.enabled_in_text_threads);
-
-            if let Some(disabled_globs) = edit_predictions.disabled_globs.as_ref() {
-                self.edit_predictions
-                    .disabled_globs
-                    .extend(disabled_globs.iter().filter_map(|g| {
-                        let expanded_g = shellexpand::tilde(g).into_owned();
-                        Some(DisabledGlob {
-                            matcher: globset::Glob::new(&expanded_g).ok()?.compile_matcher(),
-                            is_absolute: Path::new(&expanded_g).is_absolute(),
-                        })
-                    }));
-            }
-        }
-
-        if let Some(proxy) = all_languages
-            .edit_predictions
-            .as_ref()
-            .and_then(|settings| settings.copilot.as_ref()?.proxy.clone())
-        {
-            self.edit_predictions.copilot.proxy = Some(proxy);
-        }
-
-        if let Some(proxy_no_verify) = all_languages
-            .edit_predictions
-            .as_ref()
-            .and_then(|settings| settings.copilot.as_ref()?.proxy_no_verify)
-        {
-            self.edit_predictions.copilot.proxy_no_verify = Some(proxy_no_verify);
-        }
-
-        if let Some(enterprise_uri) = all_languages
-            .edit_predictions
-            .as_ref()
-            .and_then(|settings| settings.copilot.as_ref()?.enterprise_uri.clone())
-        {
-            self.edit_predictions.copilot.enterprise_uri = Some(enterprise_uri);
-        }
-
-        // A user's global settings override the default global settings and
-        // all default language-specific settings.
-        merge_settings(&mut self.defaults, &all_languages.defaults);
-        for language_settings in self.languages.values_mut() {
-            merge_settings(language_settings, &all_languages.defaults);
-        }
-
-        // A user's language-specific settings override default language-specific settings.
-        for (language_name, user_language_settings) in &all_languages.languages.0 {
-            merge_settings(
-                self.languages
-                    .entry(LanguageName(language_name.clone()))
-                    .or_insert_with(|| self.defaults.clone()),
-                user_language_settings,
-            );
-        }
-
-        for (language, patterns) in &all_languages.file_types {
-            let mut builder = GlobSetBuilder::new();
-
-            let default_value = self.file_globs.get(&language.clone());
-
-            // Merge the default value with the user's value.
-            if let Some(patterns) = default_value {
-                for pattern in patterns {
-                    if let Some(glob) = Glob::new(pattern).log_err() {
-                        builder.add(glob);
-                    }
-                }
-            }
-
-            for pattern in patterns {
-                if let Some(glob) = Glob::new(pattern).log_err() {
-                    builder.add(glob);
-                }
-            }
-
-            self.file_globs
-                .entry(language.clone())
-                .or_default()
-                .extend(patterns.clone());
-
-            if let Some(matcher) = builder.build().log_err() {
-                self.file_types.insert(language.clone(), matcher);
-            }
         }
     }
 
@@ -917,14 +737,14 @@ impl settings::Settings for AllLanguageSettings {
         // TODO: pull ^ out into helper and reuse for per-language settings
 
         // vscodes file association map is inverted from ours, so we flip the mapping before merging
-        let mut associations: HashMap<Arc<str>, Vec<String>> = HashMap::default();
+        let mut associations: HashMap<Arc<str>, ExtendingVec<String>> = HashMap::default();
         if let Some(map) = vscode
             .read_value("files.associations")
             .and_then(|v| v.as_object())
         {
             for (k, v) in map {
                 let Some(v) = v.as_str() else { continue };
-                associations.entry(v.into()).or_default().push(k.clone());
+                associations.entry(v.into()).or_default().0.push(k.clone());
             }
         }
 
@@ -957,121 +777,7 @@ impl settings::Settings for AllLanguageSettings {
     }
 }
 
-fn merge_settings(settings: &mut LanguageSettings, src: &LanguageSettingsContent) {
-    settings.tab_size.merge_from(&src.tab_size);
-    settings.tab_size = settings
-        .tab_size
-        .clamp(NonZeroU32::new(1).unwrap(), NonZeroU32::new(16).unwrap());
-
-    settings.hard_tabs.merge_from(&src.hard_tabs);
-    settings.soft_wrap.merge_from(&src.soft_wrap);
-    settings.use_autoclose.merge_from(&src.use_autoclose);
-    settings
-        .use_auto_surround
-        .merge_from(&src.use_auto_surround);
-    settings
-        .use_on_type_format
-        .merge_from(&src.use_on_type_format);
-    settings.auto_indent.merge_from(&src.auto_indent);
-    settings
-        .auto_indent_on_paste
-        .merge_from(&src.auto_indent_on_paste);
-    settings
-        .always_treat_brackets_as_autoclosed
-        .merge_from(&src.always_treat_brackets_as_autoclosed);
-    settings.show_wrap_guides.merge_from(&src.show_wrap_guides);
-    settings.wrap_guides.merge_from(&src.wrap_guides);
-    settings.indent_guides.merge_from(&src.indent_guides);
-    settings
-        .code_actions_on_format
-        .merge_from(&src.code_actions_on_format.clone());
-    settings.linked_edits.merge_from(&src.linked_edits);
-    settings.tasks.merge_from(&src.tasks);
-
-    settings
-        .preferred_line_length
-        .merge_from(&src.preferred_line_length);
-    settings.formatter.merge_from(&src.formatter.clone());
-    settings.prettier.merge_from(&src.prettier.clone());
-    settings
-        .jsx_tag_auto_close
-        .merge_from(&src.jsx_tag_auto_close.as_ref().and_then(|v| v.enabled));
-    settings
-        .format_on_save
-        .merge_from(&src.format_on_save.clone());
-    settings
-        .remove_trailing_whitespace_on_save
-        .merge_from(&src.remove_trailing_whitespace_on_save);
-    settings
-        .ensure_final_newline_on_save
-        .merge_from(&src.ensure_final_newline_on_save);
-    settings
-        .enable_language_server
-        .merge_from(&src.enable_language_server);
-    settings
-        .language_servers
-        .merge_from(&src.language_servers.clone());
-    settings.allow_rewrap.merge_from(&src.allow_rewrap);
-    settings
-        .show_edit_predictions
-        .merge_from(&src.show_edit_predictions);
-    settings
-        .edit_predictions_disabled_in
-        .merge_from(&src.edit_predictions_disabled_in.clone());
-    settings.show_whitespaces.merge_from(&src.show_whitespaces);
-    settings
-        .whitespace_map
-        .merge_from(&src.whitespace_map.clone());
-    settings
-        .extend_comment_on_newline
-        .merge_from(&src.extend_comment_on_newline);
-    if let Some(inlay_hints) = &src.inlay_hints {
-        settings
-            .inlay_hints
-            .enabled
-            .merge_from(&inlay_hints.enabled);
-        settings
-            .inlay_hints
-            .show_value_hints
-            .merge_from(&inlay_hints.show_value_hints);
-        settings
-            .inlay_hints
-            .show_type_hints
-            .merge_from(&inlay_hints.show_type_hints);
-        settings
-            .inlay_hints
-            .show_parameter_hints
-            .merge_from(&inlay_hints.show_parameter_hints);
-        settings
-            .inlay_hints
-            .show_other_hints
-            .merge_from(&inlay_hints.show_other_hints);
-        settings
-            .inlay_hints
-            .show_background
-            .merge_from(&inlay_hints.show_background);
-        settings
-            .inlay_hints
-            .edit_debounce_ms
-            .merge_from(&inlay_hints.edit_debounce_ms);
-        settings
-            .inlay_hints
-            .scroll_debounce_ms
-            .merge_from(&inlay_hints.scroll_debounce_ms);
-        if let Some(toggle_on_modifiers_press) = &inlay_hints.toggle_on_modifiers_press {
-            settings.inlay_hints.toggle_on_modifiers_press = Some(*toggle_on_modifiers_press);
-        }
-    }
-    settings
-        .show_completions_on_input
-        .merge_from(&src.show_completions_on_input);
-    settings
-        .show_completion_documentation
-        .merge_from(&src.show_completion_documentation);
-    settings.completions.merge_from(&src.completions);
-}
-
-#[derive(Default, Debug, Clone, PartialEq, Eq, SettingsUi)]
+#[derive(Default, Debug, Clone, PartialEq, Eq)]
 pub struct JsxTagAutoCloseSettings {
     /// Enables or disables auto-closing of JSX tags.
     pub enabled: bool,

crates/language_models/src/settings.rs 🔗

@@ -3,7 +3,6 @@ use std::sync::Arc;
 use collections::HashMap;
 use gpui::App;
 use settings::Settings;
-use util::MergeFrom;
 
 use crate::provider::{
     anthropic::AnthropicSettings, bedrock::AmazonBedrockSettings, cloud::ZedDotDevSettings,
@@ -37,7 +36,7 @@ pub struct AllLanguageModelSettings {
 impl settings::Settings for AllLanguageModelSettings {
     const PRESERVED_KEYS: Option<&'static [&'static str]> = Some(&["version"]);
 
-    fn from_defaults(content: &settings::SettingsContent, _cx: &mut App) -> Self {
+    fn from_settings(content: &settings::SettingsContent, _cx: &mut App) -> Self {
         let language_models = content.language_models.clone().unwrap();
         let anthropic = language_models.anthropic.unwrap();
         let bedrock = language_models.bedrock.unwrap();
@@ -118,113 +117,4 @@ impl settings::Settings for AllLanguageModelSettings {
             },
         }
     }
-
-    fn refine(&mut self, content: &settings::SettingsContent, _cx: &mut App) {
-        let Some(models) = content.language_models.as_ref() else {
-            return;
-        };
-
-        if let Some(anthropic) = models.anthropic.as_ref() {
-            self.anthropic
-                .available_models
-                .merge_from(&anthropic.available_models);
-            self.anthropic.api_url.merge_from(&anthropic.api_url);
-        }
-
-        if let Some(bedrock) = models.bedrock.clone() {
-            self.bedrock
-                .available_models
-                .merge_from(&bedrock.available_models);
-
-            if let Some(endpoint_url) = bedrock.endpoint_url {
-                self.bedrock.endpoint = Some(endpoint_url)
-            }
-
-            if let Some(region) = bedrock.region {
-                self.bedrock.region = Some(region)
-            }
-
-            if let Some(profile_name) = bedrock.profile {
-                self.bedrock.profile_name = Some(profile_name);
-            }
-
-            if let Some(auth_method) = bedrock.authentication_method {
-                self.bedrock.authentication_method = Some(auth_method.into());
-            }
-        }
-
-        if let Some(deepseek) = models.deepseek.as_ref() {
-            self.deepseek
-                .available_models
-                .merge_from(&deepseek.available_models);
-            self.deepseek.api_url.merge_from(&deepseek.api_url);
-        }
-
-        if let Some(google) = models.google.as_ref() {
-            self.google
-                .available_models
-                .merge_from(&google.available_models);
-            self.google.api_url.merge_from(&google.api_url);
-        }
-
-        if let Some(lmstudio) = models.lmstudio.as_ref() {
-            self.lmstudio
-                .available_models
-                .merge_from(&lmstudio.available_models);
-            self.lmstudio.api_url.merge_from(&lmstudio.api_url);
-        }
-
-        if let Some(mistral) = models.mistral.as_ref() {
-            self.mistral
-                .available_models
-                .merge_from(&mistral.available_models);
-            self.mistral.api_url.merge_from(&mistral.api_url);
-        }
-        if let Some(ollama) = models.ollama.as_ref() {
-            self.ollama
-                .available_models
-                .merge_from(&ollama.available_models);
-            self.ollama.api_url.merge_from(&ollama.api_url);
-        }
-        if let Some(open_router) = models.open_router.as_ref() {
-            self.open_router
-                .available_models
-                .merge_from(&open_router.available_models);
-            self.open_router.api_url.merge_from(&open_router.api_url);
-        }
-        if let Some(openai) = models.openai.as_ref() {
-            self.openai
-                .available_models
-                .merge_from(&openai.available_models);
-            self.openai.api_url.merge_from(&openai.api_url);
-        }
-        if let Some(openai_compatible) = models.openai_compatible.clone() {
-            for (name, value) in openai_compatible {
-                self.openai_compatible.insert(
-                    name,
-                    OpenAiCompatibleSettings {
-                        api_url: value.api_url,
-                        available_models: value.available_models,
-                    },
-                );
-            }
-        }
-        if let Some(vercel) = models.vercel.as_ref() {
-            self.vercel
-                .available_models
-                .merge_from(&vercel.available_models);
-            self.vercel.api_url.merge_from(&vercel.api_url);
-        }
-        if let Some(x_ai) = models.x_ai.as_ref() {
-            self.x_ai
-                .available_models
-                .merge_from(&x_ai.available_models);
-            self.x_ai.api_url.merge_from(&x_ai.api_url);
-        }
-        if let Some(zed_dot_dev) = models.zed_dot_dev.as_ref() {
-            self.zed_dot_dev
-                .available_models
-                .merge_from(&zed_dot_dev.available_models);
-        }
-    }
 }

crates/languages/Cargo.toml 🔗

@@ -74,6 +74,7 @@ snippet_provider.workspace = true
 url.workspace = true
 task.workspace = true
 tempfile.workspace = true
+theme.workspace = true
 toml.workspace = true
 tree-sitter = { workspace = true, optional = true }
 tree-sitter-bash = { workspace = true, optional = true }

crates/languages/src/json.rs 🔗

@@ -5,7 +5,7 @@ use async_trait::async_trait;
 use collections::HashMap;
 use dap::DapRegistry;
 use futures::StreamExt;
-use gpui::{App, AsyncApp, Task};
+use gpui::{App, AsyncApp, SharedString, Task};
 use http_client::github::{GitHubLspBinaryVersion, latest_github_release};
 use language::{
     ContextProvider, LanguageName, LanguageRegistry, LocalFile as _, LspAdapter,
@@ -29,6 +29,7 @@ use std::{
     sync::Arc,
 };
 use task::{AdapterSchemas, TaskTemplate, TaskTemplates, VariableName};
+use theme::ThemeRegistry;
 use util::{ResultExt, archive::extract_zip, fs::remove_matching, maybe, merge_json_value_into};
 
 use crate::PackageJsonData;
@@ -156,13 +157,20 @@ impl JsonLspAdapter {
     ) -> Value {
         let keymap_schema = KeymapFile::generate_json_schema_for_registered_actions(cx);
         let font_names = &cx.text_system().all_font_names();
-        let settings_schema = cx.global::<SettingsStore>().json_schema(
-            &SettingsJsonSchemaParams {
+        let theme_names = &ThemeRegistry::global(cx).list_names();
+        let icon_theme_names = &ThemeRegistry::global(cx)
+            .list_icon_themes()
+            .into_iter()
+            .map(|icon_theme| icon_theme.name)
+            .collect::<Vec<SharedString>>();
+        let settings_schema = cx
+            .global::<SettingsStore>()
+            .json_schema(&SettingsJsonSchemaParams {
                 language_names: &language_names,
                 font_names,
-            },
-            cx,
-        );
+                theme_names,
+                icon_theme_names,
+            });
 
         let tasks_schema = task::TaskTemplates::generate_json_schema();
         let debug_schema = task::DebugTaskFile::generate_json_schema(&adapter_schemas);

crates/onboarding/src/ai_setup_page.rs 🔗

@@ -265,7 +265,7 @@ pub(crate) fn render_ai_setup_page(
 
                     let fs = <dyn Fs>::global(cx);
                     update_settings_file(fs, cx, move |settings, _| {
-                        settings.disable_ai = Some(enabled);
+                        settings.disable_ai = Some(enabled.into());
                     });
                 },
             )

crates/outline_panel/src/outline_panel_settings.rs 🔗

@@ -2,7 +2,6 @@ use editor::EditorSettings;
 use gpui::{App, Pixels};
 pub use settings::{DockSide, Settings, ShowIndentGuides};
 use ui::scrollbars::{ScrollbarVisibility, ShowScrollbar};
-use util::MergeFrom;
 
 #[derive(Debug, Clone, Copy, PartialEq)]
 pub struct OutlinePanelSettings {
@@ -42,7 +41,7 @@ impl ScrollbarVisibility for OutlinePanelSettings {
 }
 
 impl Settings for OutlinePanelSettings {
-    fn from_defaults(content: &settings::SettingsContent, _cx: &mut App) -> Self {
+    fn from_settings(content: &settings::SettingsContent, _cx: &mut App) -> Self {
         let panel = content.outline_panel.as_ref().unwrap();
         Self {
             button: panel.button.unwrap(),
@@ -58,40 +57,12 @@ impl Settings for OutlinePanelSettings {
             auto_reveal_entries: panel.auto_reveal_entries.unwrap(),
             auto_fold_dirs: panel.auto_fold_dirs.unwrap(),
             scrollbar: ScrollbarSettings {
-                show: panel.scrollbar.unwrap().show.flatten().map(Into::into),
+                show: panel.scrollbar.unwrap().show.map(Into::into),
             },
             expand_outlines_with_depth: panel.expand_outlines_with_depth.unwrap(),
         }
     }
 
-    fn refine(&mut self, content: &settings::SettingsContent, _cx: &mut App) {
-        let Some(panel) = content.outline_panel.as_ref() else {
-            return;
-        };
-
-        self.button.merge_from(&panel.button);
-        self.default_width
-            .merge_from(&panel.default_width.map(Pixels::from));
-        self.dock.merge_from(&panel.dock);
-        self.file_icons.merge_from(&panel.file_icons);
-        self.folder_icons.merge_from(&panel.folder_icons);
-        self.git_status.merge_from(&panel.git_status);
-        self.indent_size.merge_from(&panel.indent_size);
-
-        if let Some(indent_guides) = panel.indent_guides.as_ref() {
-            self.indent_guides.show.merge_from(&indent_guides.show);
-        }
-
-        self.auto_reveal_entries
-            .merge_from(&panel.auto_reveal_entries);
-        self.auto_fold_dirs.merge_from(&panel.auto_fold_dirs);
-
-        if let Some(scrollbar) = panel.scrollbar.as_ref()
-            && let Some(show) = scrollbar.show.flatten()
-        {
-            self.scrollbar.show = Some(show.into())
-        }
-    }
     fn import_from_vscode(
         vscode: &settings::VsCodeSettings,
         current: &mut settings::SettingsContent,

crates/project/src/agent_server_store.rs 🔗

@@ -22,7 +22,7 @@ use rpc::{
 };
 use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
-use settings::{SettingsContent, SettingsKey, SettingsStore, SettingsUi};
+use settings::{SettingsContent, SettingsStore};
 use util::{ResultExt as _, debug_panic};
 
 use crate::ProjectEnvironment;
@@ -989,8 +989,7 @@ impl ExternalAgentServer for LocalCustomAgent {
 pub const GEMINI_NAME: &'static str = "gemini";
 pub const CLAUDE_CODE_NAME: &'static str = "claude";
 
-#[derive(Default, Clone, JsonSchema, Debug, SettingsUi, SettingsKey, PartialEq)]
-#[settings_key(key = "agent_servers")]
+#[derive(Default, Clone, JsonSchema, Debug, PartialEq)]
 pub struct AllAgentServersSettings {
     pub gemini: Option<BuiltinAgentServerSettings>,
     pub claude: Option<BuiltinAgentServerSettings>,
@@ -1063,7 +1062,7 @@ impl From<settings::CustomAgentServerSettings> for CustomAgentServerSettings {
 }
 
 impl settings::Settings for AllAgentServersSettings {
-    fn from_defaults(content: &settings::SettingsContent, _cx: &mut App) -> Self {
+    fn from_settings(content: &settings::SettingsContent, _cx: &mut App) -> Self {
         let agent_settings = content.agent_servers.clone().unwrap();
         Self {
             gemini: agent_settings.gemini.map(Into::into),
@@ -1076,20 +1075,5 @@ impl settings::Settings for AllAgentServersSettings {
         }
     }
 
-    fn refine(&mut self, content: &settings::SettingsContent, _cx: &mut App) {
-        let Some(content) = &content.agent_servers else {
-            return;
-        };
-        if let Some(gemini) = content.gemini.clone() {
-            self.gemini = Some(gemini.into())
-        };
-        if let Some(claude) = content.claude.clone() {
-            self.claude = Some(claude.into());
-        }
-        for (name, config) in content.custom.clone() {
-            self.custom.insert(name, config.into());
-        }
-    }
-
     fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut SettingsContent) {}
 }

crates/project/src/project.rs 🔗

@@ -971,23 +971,18 @@ pub enum PulledDiagnostics {
 /// Whether to disable all AI features in Zed.
 ///
 /// Default: false
-#[derive(Copy, Clone, Debug, settings::SettingsUi)]
+#[derive(Copy, Clone, Debug)]
 pub struct DisableAiSettings {
     pub disable_ai: bool,
 }
 
 impl settings::Settings for DisableAiSettings {
-    fn from_defaults(content: &settings::SettingsContent, _cx: &mut App) -> Self {
+    fn from_settings(content: &settings::SettingsContent, _cx: &mut App) -> Self {
         Self {
-            disable_ai: content.disable_ai.unwrap(),
+            disable_ai: content.disable_ai.unwrap().0,
         }
     }
 
-    fn refine(&mut self, content: &settings::SettingsContent, _cx: &mut App) {
-        // If disable_ai is true *in any file*, it is disabled.
-        self.disable_ai = self.disable_ai || content.disable_ai.unwrap_or(false);
-    }
-
     fn import_from_vscode(
         _vscode: &settings::VsCodeSettings,
         _current: &mut settings::SettingsContent,

crates/project/src/project_settings.rs 🔗

@@ -21,7 +21,7 @@ pub use settings::DirenvSettings;
 pub use settings::LspSettings;
 use settings::{
     DapSettingsContent, InvalidSettingsError, LocalSettingsKind, Settings, SettingsLocation,
-    SettingsStore, SettingsUi, parse_json_with_comments, watch_config_file,
+    SettingsStore, parse_json_with_comments, watch_config_file,
 };
 use std::{
     path::{Path, PathBuf},
@@ -29,7 +29,7 @@ use std::{
     time::Duration,
 };
 use task::{DebugTaskFile, TaskTemplates, VsCodeDebugTaskFile, VsCodeTaskFile};
-use util::{MergeFrom as _, ResultExt, serde::default_true};
+use util::{ResultExt, serde::default_true};
 use worktree::{PathChange, UpdatedEntriesSet, Worktree, WorktreeId};
 
 use crate::{
@@ -189,20 +189,7 @@ impl ContextServerSettings {
     }
 }
 
-#[derive(
-    Clone,
-    Copy,
-    Debug,
-    Eq,
-    PartialEq,
-    Ord,
-    PartialOrd,
-    Serialize,
-    Deserialize,
-    JsonSchema,
-    SettingsUi,
-)]
-#[serde(rename_all = "snake_case")]
+#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd)]
 pub enum DiagnosticSeverity {
     // No diagnostics are shown.
     Off,
@@ -444,7 +431,7 @@ pub struct LspPullDiagnosticsSettings {
 }
 
 impl Settings for ProjectSettings {
-    fn from_defaults(content: &settings::SettingsContent, _cx: &mut App) -> Self {
+    fn from_settings(content: &settings::SettingsContent, _cx: &mut App) -> Self {
         let project = &content.project.clone();
         let diagnostics = content.diagnostics.as_ref().unwrap();
         let lsp_pull_diagnostics = diagnostics.lsp_pull_diagnostics.as_ref().unwrap();
@@ -523,118 +510,6 @@ impl Settings for ProjectSettings {
         }
     }
 
-    fn refine(&mut self, content: &settings::SettingsContent, _cx: &mut App) {
-        let project = &content.project;
-        self.context_servers.extend(
-            project
-                .context_servers
-                .clone()
-                .into_iter()
-                .map(|(key, value)| (key, value.into())),
-        );
-        self.dap.extend(
-            project
-                .dap
-                .clone()
-                .into_iter()
-                .map(|(key, value)| (DebugAdapterName(key.into()), DapSettings::from(value))),
-        );
-        if let Some(diagnostics) = content.diagnostics.as_ref() {
-            if let Some(inline) = &diagnostics.inline {
-                self.diagnostics.inline.enabled.merge_from(&inline.enabled);
-                self.diagnostics
-                    .inline
-                    .update_debounce_ms
-                    .merge_from(&inline.update_debounce_ms);
-                self.diagnostics.inline.padding.merge_from(&inline.padding);
-                self.diagnostics
-                    .inline
-                    .min_column
-                    .merge_from(&inline.min_column);
-                if let Some(max_severity) = inline.max_severity {
-                    self.diagnostics.inline.max_severity = Some(max_severity.into())
-                }
-            }
-
-            self.diagnostics.button.merge_from(&diagnostics.button);
-            self.diagnostics
-                .include_warnings
-                .merge_from(&diagnostics.include_warnings);
-            if let Some(pull_diagnostics) = &diagnostics.lsp_pull_diagnostics {
-                self.diagnostics
-                    .lsp_pull_diagnostics
-                    .enabled
-                    .merge_from(&pull_diagnostics.enabled);
-                self.diagnostics
-                    .lsp_pull_diagnostics
-                    .debounce_ms
-                    .merge_from(&pull_diagnostics.debounce_ms);
-            }
-        }
-        if let Some(git) = content.git.as_ref() {
-            if let Some(branch_picker) = git.branch_picker.as_ref() {
-                self.git
-                    .branch_picker
-                    .show_author_name
-                    .merge_from(&branch_picker.show_author_name);
-            }
-            if let Some(inline_blame) = git.inline_blame.as_ref() {
-                self.git
-                    .inline_blame
-                    .enabled
-                    .merge_from(&inline_blame.enabled);
-                self.git
-                    .inline_blame
-                    .delay_ms
-                    .merge_from(&inline_blame.delay_ms.map(std::time::Duration::from_millis));
-                self.git
-                    .inline_blame
-                    .padding
-                    .merge_from(&inline_blame.padding);
-                self.git
-                    .inline_blame
-                    .min_column
-                    .merge_from(&inline_blame.min_column);
-                self.git
-                    .inline_blame
-                    .show_commit_summary
-                    .merge_from(&inline_blame.show_commit_summary);
-            }
-            self.git.git_gutter.merge_from(&git.git_gutter);
-            self.git.hunk_style.merge_from(&git.hunk_style);
-            if let Some(debounce) = git.gutter_debounce {
-                self.git.gutter_debounce = Some(debounce);
-            }
-        }
-        self.global_lsp_settings.button.merge_from(
-            &content
-                .global_lsp_settings
-                .as_ref()
-                .and_then(|settings| settings.button),
-        );
-        self.load_direnv
-            .merge_from(&content.project.load_direnv.clone());
-
-        for (key, value) in content.project.lsp.clone() {
-            self.lsp.insert(LanguageServerName(key.into()), value);
-        }
-
-        if let Some(node) = content.node.as_ref() {
-            self.node
-                .ignore_system_version
-                .merge_from(&node.ignore_system_version);
-            if let Some(path) = node.path.clone() {
-                self.node.path = Some(path);
-            }
-            if let Some(npm_path) = node.npm_path.clone() {
-                self.node.npm_path = Some(npm_path);
-            }
-        }
-        self.session
-            .restore_unsaved_buffers
-            .merge_from(&content.session.and_then(|s| s.restore_unsaved_buffers));
-    }
-
     fn import_from_vscode(
         vscode: &settings::VsCodeSettings,
         current: &mut settings::SettingsContent,

crates/project_panel/src/project_panel_settings.rs 🔗

@@ -10,7 +10,6 @@ use ui::{
     px,
     scrollbars::{ScrollbarVisibility, ShowScrollbar},
 };
-use util::MergeFrom;
 
 #[derive(Deserialize, Debug, Clone, Copy, PartialEq)]
 pub struct ProjectPanelSettings {
@@ -56,7 +55,7 @@ impl ScrollbarVisibility for ProjectPanelSettings {
 }
 
 impl Settings for ProjectPanelSettings {
-    fn from_defaults(content: &settings::SettingsContent, _cx: &mut ui::App) -> Self {
+    fn from_settings(content: &settings::SettingsContent, _cx: &mut ui::App) -> Self {
         let project_panel = content.project_panel.clone().unwrap();
         Self {
             button: project_panel.button.unwrap(),
@@ -76,12 +75,7 @@ impl Settings for ProjectPanelSettings {
             auto_fold_dirs: project_panel.auto_fold_dirs.unwrap(),
             starts_open: project_panel.starts_open.unwrap(),
             scrollbar: ScrollbarSettings {
-                show: project_panel
-                    .scrollbar
-                    .unwrap()
-                    .show
-                    .flatten()
-                    .map(Into::into),
+                show: project_panel.scrollbar.unwrap().show.map(Into::into),
             },
             show_diagnostics: project_panel.show_diagnostics.unwrap(),
             hide_root: project_panel.hide_root.unwrap(),
@@ -89,47 +83,6 @@ impl Settings for ProjectPanelSettings {
         }
     }
 
-    fn refine(&mut self, content: &SettingsContent, _cx: &mut ui::App) {
-        let Some(project_panel) = content.project_panel.as_ref() else {
-            return;
-        };
-        self.button.merge_from(&project_panel.button);
-        self.hide_gitignore
-            .merge_from(&project_panel.hide_gitignore);
-        self.default_width
-            .merge_from(&project_panel.default_width.map(px));
-        self.dock.merge_from(&project_panel.dock);
-        self.entry_spacing.merge_from(&project_panel.entry_spacing);
-        self.file_icons.merge_from(&project_panel.file_icons);
-        self.folder_icons.merge_from(&project_panel.folder_icons);
-        self.git_status.merge_from(&project_panel.git_status);
-        self.indent_size.merge_from(&project_panel.indent_size);
-        self.sticky_scroll.merge_from(&project_panel.sticky_scroll);
-        self.auto_reveal_entries
-            .merge_from(&project_panel.auto_reveal_entries);
-        self.auto_fold_dirs
-            .merge_from(&project_panel.auto_fold_dirs);
-        self.starts_open.merge_from(&project_panel.starts_open);
-        self.show_diagnostics
-            .merge_from(&project_panel.show_diagnostics);
-        self.hide_root.merge_from(&project_panel.hide_root);
-        self.drag_and_drop.merge_from(&project_panel.drag_and_drop);
-        if let Some(show) = project_panel
-            .indent_guides
-            .as_ref()
-            .and_then(|indent| indent.show)
-        {
-            self.indent_guides.show = show;
-        }
-        if let Some(show) = project_panel
-            .scrollbar
-            .as_ref()
-            .and_then(|scrollbar| scrollbar.show)
-        {
-            self.scrollbar.show = show.map(Into::into)
-        }
-    }
-
     fn import_from_vscode(vscode: &settings::VsCodeSettings, current: &mut SettingsContent) {
         if let Some(hide_gitignore) = vscode.read_bool("explorer.excludeGitIgnore") {
             current.project_panel.get_or_insert_default().hide_gitignore = Some(hide_gitignore);

crates/recent_projects/src/remote_connections.rs 🔗

@@ -19,29 +19,28 @@ use remote::{
     SshConnectionOptions,
 };
 pub use settings::SshConnection;
-use settings::{Settings, WslConnection};
+use settings::{ExtendingVec, Settings, WslConnection};
 use theme::ThemeSettings;
 use ui::{
     ActiveTheme, Color, CommonAnimationExt, Context, Icon, IconName, IconSize, InteractiveElement,
     IntoElement, Label, LabelCommon, Styled, Window, prelude::*,
 };
-use util::MergeFrom;
 use workspace::{AppState, ModalView, Workspace};
 
 pub struct SshSettings {
-    pub ssh_connections: Vec<SshConnection>,
-    pub wsl_connections: Vec<WslConnection>,
+    pub ssh_connections: ExtendingVec<SshConnection>,
+    pub wsl_connections: ExtendingVec<WslConnection>,
     /// Whether to read ~/.ssh/config for ssh connection sources.
     pub read_ssh_config: bool,
 }
 
 impl SshSettings {
     pub fn ssh_connections(&self) -> impl Iterator<Item = SshConnection> + use<> {
-        self.ssh_connections.clone().into_iter()
+        self.ssh_connections.clone().0.into_iter()
     }
 
     pub fn wsl_connections(&self) -> impl Iterator<Item = WslConnection> + use<> {
-        self.wsl_connections.clone().into_iter()
+        self.wsl_connections.clone().0.into_iter()
     }
 
     pub fn fill_connection_options_from_settings(&self, options: &mut SshConnectionOptions) {
@@ -104,25 +103,14 @@ impl From<WslConnection> for Connection {
 }
 
 impl Settings for SshSettings {
-    fn from_defaults(content: &settings::SettingsContent, _cx: &mut App) -> Self {
+    fn from_settings(content: &settings::SettingsContent, _cx: &mut App) -> Self {
         let remote = &content.remote;
         Self {
-            ssh_connections: remote.ssh_connections.clone().unwrap_or_default(),
-            wsl_connections: remote.wsl_connections.clone().unwrap_or_default(),
+            ssh_connections: remote.ssh_connections.clone().unwrap_or_default().into(),
+            wsl_connections: remote.wsl_connections.clone().unwrap_or_default().into(),
             read_ssh_config: remote.read_ssh_config.unwrap(),
         }
     }
-
-    fn refine(&mut self, content: &settings::SettingsContent, _cx: &mut App) {
-        if let Some(ssh_connections) = content.remote.ssh_connections.clone() {
-            self.ssh_connections.extend(ssh_connections)
-        }
-        if let Some(wsl_connections) = content.remote.wsl_connections.clone() {
-            self.wsl_connections.extend(wsl_connections)
-        }
-        self.read_ssh_config
-            .merge_from(&content.remote.read_ssh_config);
-    }
 }
 
 pub struct RemoteConnectionPrompt {

crates/recent_projects/src/remote_servers.rs 🔗

@@ -1885,7 +1885,7 @@ impl RemoteServerProjects {
         let ssh_settings = SshSettings::get_global(cx);
         let mut should_rebuild = false;
 
-        let ssh_connections_changed = ssh_settings.ssh_connections.iter().ne(state
+        let ssh_connections_changed = ssh_settings.ssh_connections.0.iter().ne(state
             .servers
             .iter()
             .filter_map(|server| match server {
@@ -1896,7 +1896,7 @@ impl RemoteServerProjects {
                 _ => None,
             }));
 
-        let wsl_connections_changed = ssh_settings.wsl_connections.iter().ne(state
+        let wsl_connections_changed = ssh_settings.wsl_connections.0.iter().ne(state
             .servers
             .iter()
             .filter_map(|server| match server {

crates/repl/src/jupyter_settings.rs 🔗

@@ -19,19 +19,10 @@ impl JupyterSettings {
 }
 
 impl Settings for JupyterSettings {
-    fn from_defaults(content: &settings::SettingsContent, _cx: &mut App) -> Self {
+    fn from_settings(content: &settings::SettingsContent, _cx: &mut App) -> Self {
         let jupyter = content.editor.jupyter.clone().unwrap();
         Self {
             kernel_selections: jupyter.kernel_selections.unwrap_or_default(),
         }
     }
-
-    fn refine(&mut self, content: &settings::SettingsContent, _cx: &mut App) {
-        let Some(jupyter) = content.editor.jupyter.as_ref() else {
-            return;
-        };
-        if let Some(kernel_selections) = jupyter.kernel_selections.clone() {
-            self.kernel_selections.extend(kernel_selections)
-        }
-    }
 }

crates/repl/src/repl_settings.rs 🔗

@@ -1,6 +1,5 @@
 use gpui::App;
 use settings::Settings;
-use util::MergeFrom;
 
 /// Settings for configuring REPL display and behavior.
 #[derive(Clone, Debug)]
@@ -18,7 +17,7 @@ pub struct ReplSettings {
 }
 
 impl Settings for ReplSettings {
-    fn from_defaults(content: &settings::SettingsContent, _cx: &mut App) -> Self {
+    fn from_settings(content: &settings::SettingsContent, _cx: &mut App) -> Self {
         let repl = content.repl.as_ref().unwrap();
 
         Self {
@@ -26,13 +25,4 @@ impl Settings for ReplSettings {
             max_columns: repl.max_columns.unwrap(),
         }
     }
-
-    fn refine(&mut self, content: &settings::SettingsContent, _cx: &mut App) {
-        let Some(repl) = content.repl.as_ref() else {
-            return;
-        };
-
-        self.max_columns.merge_from(&repl.max_columns);
-        self.max_lines.merge_from(&repl.max_lines);
-    }
 }

crates/settings/Cargo.toml 🔗

@@ -30,7 +30,7 @@ rust-embed.workspace = true
 schemars.workspace = true
 serde.workspace = true
 serde_json.workspace = true
-settings_ui_macros.workspace = true
+settings_macros = { path = "../settings_macros" }
 serde_json_lenient.workspace = true
 serde_repr.workspace = true
 serde_path_to_error.workspace = true

crates/settings/src/base_keymap_setting.rs 🔗

@@ -2,21 +2,17 @@ use std::fmt::{Display, Formatter};
 
 use crate::{
     self as settings,
-    settings_content::{self, BaseKeymapContent, SettingsContent},
+    settings_content::{BaseKeymapContent, SettingsContent},
 };
 use gpui::App;
 use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
-use serde_with::skip_serializing_none;
 use settings::{Settings, VsCodeSettings};
-use settings_ui_macros::{SettingsKey, SettingsUi};
 
 /// Base key bindings scheme. Base keymaps can be overridden with user keymaps.
 ///
 /// Default: VSCode
-#[derive(
-    Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Default, SettingsUi,
-)]
+#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Default)]
 pub enum BaseKeymap {
     #[default]
     VSCode,
@@ -134,37 +130,11 @@ impl BaseKeymap {
     }
 }
 
-#[derive(
-    Copy,
-    Clone,
-    Debug,
-    Serialize,
-    Deserialize,
-    JsonSchema,
-    PartialEq,
-    Eq,
-    Default,
-    SettingsUi,
-    SettingsKey,
-)]
-// extracted so that it can be an option, and still work with derive(SettingsUi)
-#[settings_key(None)]
-#[skip_serializing_none]
-pub struct BaseKeymapSetting {
-    pub base_keymap: Option<BaseKeymap>,
-}
-
 impl Settings for BaseKeymap {
-    fn from_defaults(s: &crate::settings_content::SettingsContent, _cx: &mut App) -> Self {
+    fn from_settings(s: &crate::settings_content::SettingsContent, _cx: &mut App) -> Self {
         s.base_keymap.unwrap().into()
     }
 
-    fn refine(&mut self, s: &settings_content::SettingsContent, _cx: &mut App) {
-        if let Some(base_keymap) = s.base_keymap {
-            *self = base_keymap.into();
-        };
-    }
-
     fn import_from_vscode(_vscode: &VsCodeSettings, current: &mut SettingsContent) {
         current.base_keymap = Some(BaseKeymapContent::VSCode);
     }

crates/settings/src/merge_from.rs 🔗

@@ -0,0 +1,164 @@
+use std::rc::Rc;
+
+/// Trait for recursively merging settings structures.
+///
+/// This trait allows settings objects to be merged from optional sources,
+/// where `None` values are ignored and `Some` values override existing values.
+///
+/// HashMaps, structs and similar types are merged by combining their contents key-wise,
+/// but all other types (including Vecs) are last-write-wins.
+/// (Though see also ExtendingVec and SaturatingBool)
+#[allow(unused)]
+pub trait MergeFrom {
+    /// Merge from an optional source of the same type.
+    /// If `other` is `None`, no changes are made.
+    /// If `other` is `Some(value)`, fields from `value` are merged into `self`.
+    fn merge_from(&mut self, other: Option<&Self>);
+}
+
+macro_rules! merge_from_overwrites {
+    ($($type:ty),+) => {
+        $(
+            impl MergeFrom for $type {
+                fn merge_from(&mut self, other: Option<&Self>) {
+                    if let Some(value) = other {
+                        *self = value.clone();
+                    }
+                }
+            }
+        )+
+    }
+}
+
+merge_from_overwrites!(
+    u16,
+    u32,
+    u64,
+    usize,
+    i16,
+    i32,
+    i64,
+    bool,
+    f64,
+    f32,
+    std::num::NonZeroUsize,
+    std::num::NonZeroU32,
+    String,
+    std::sync::Arc<str>,
+    gpui::SharedString,
+    std::path::PathBuf,
+    gpui::Modifiers,
+    gpui::FontFeatures
+);
+
+impl<T: Clone> MergeFrom for Vec<T> {
+    fn merge_from(&mut self, other: Option<&Self>) {
+        if let Some(other) = other {
+            *self = other.clone()
+        }
+    }
+}
+
+// Implementations for collections that extend/merge their contents
+impl<K, V> MergeFrom for collections::HashMap<K, V>
+where
+    K: Clone + std::hash::Hash + Eq,
+    V: Clone + MergeFrom,
+{
+    fn merge_from(&mut self, other: Option<&Self>) {
+        let Some(other) = other else { return };
+        for (k, v) in other {
+            if let Some(existing) = self.get_mut(k) {
+                existing.merge_from(Some(v));
+            } else {
+                self.insert(k.clone(), v.clone());
+            }
+        }
+    }
+}
+
+impl<K, V> MergeFrom for collections::BTreeMap<K, V>
+where
+    K: Clone + std::hash::Hash + Eq + Ord,
+    V: Clone + MergeFrom,
+{
+    fn merge_from(&mut self, other: Option<&Self>) {
+        let Some(other) = other else { return };
+        for (k, v) in other {
+            if let Some(existing) = self.get_mut(k) {
+                existing.merge_from(Some(v));
+            } else {
+                self.insert(k.clone(), v.clone());
+            }
+        }
+    }
+}
+
+impl<K, V> MergeFrom for collections::IndexMap<K, V>
+where
+    K: std::hash::Hash + Eq + Clone,
+    // Q: ?Sized + std::hash::Hash + collections::Equivalent<K> + Eq,
+    V: Clone + MergeFrom,
+{
+    fn merge_from(&mut self, other: Option<&Self>) {
+        let Some(other) = other else { return };
+        for (k, v) in other {
+            if let Some(existing) = self.get_mut(k) {
+                existing.merge_from(Some(v));
+            } else {
+                self.insert(k.clone(), v.clone());
+            }
+        }
+    }
+}
+
+impl<T> MergeFrom for collections::BTreeSet<T>
+where
+    T: Clone + Ord,
+{
+    fn merge_from(&mut self, other: Option<&Self>) {
+        let Some(other) = other else { return };
+        for item in other {
+            self.insert(item.clone());
+        }
+    }
+}
+
+impl<T> MergeFrom for collections::HashSet<T>
+where
+    T: Clone + std::hash::Hash + Eq,
+{
+    fn merge_from(&mut self, other: Option<&Self>) {
+        let Some(other) = other else { return };
+        for item in other {
+            self.insert(item.clone());
+        }
+    }
+}
+
+impl MergeFrom for serde_json::Value {
+    fn merge_from(&mut self, other: Option<&Self>) {
+        let Some(other) = other else { return };
+        match (self, other) {
+            (serde_json::Value::Object(this), serde_json::Value::Object(other)) => {
+                for (k, v) in other {
+                    if let Some(existing) = this.get_mut(k) {
+                        existing.merge_from(other.get(k));
+                    } else {
+                        this.insert(k.clone(), v.clone());
+                    }
+                }
+            }
+            (this, other) => *this = other.clone(),
+        }
+    }
+}
+
+impl<T: MergeFrom + Clone> MergeFrom for Rc<T> {
+    fn merge_from(&mut self, other: Option<&Self>) {
+        let Some(other) = other else { return };
+        let mut this: T = self.as_ref().clone();
+        this.merge_from(Some(other.as_ref()));
+        *self = Rc::new(this)
+    }
+}

crates/settings/src/settings.rs 🔗

@@ -1,6 +1,7 @@
 mod base_keymap_setting;
 mod editable_setting_control;
 mod keymap_file;
+pub mod merge_from;
 mod settings_content;
 mod settings_file;
 mod settings_json;
@@ -27,8 +28,7 @@ pub use settings_store::{
     InvalidSettingsError, LocalSettingsKind, Settings, SettingsKey, SettingsLocation, SettingsStore,
 };
 pub use settings_ui_core::*;
-// Re-export the derive macro
-pub use settings_ui_macros::{SettingsKey, SettingsUi};
+
 pub use vscode_import::{VsCodeSettings, VsCodeSettingsSource};
 
 #[derive(Clone, Debug, PartialEq)]

crates/settings/src/settings_content.rs 🔗

@@ -22,15 +22,16 @@ use release_channel::ReleaseChannel;
 use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
 use serde_with::skip_serializing_none;
+use settings_macros::MergeFrom;
 use std::collections::BTreeSet;
 use std::env;
 use std::sync::Arc;
 pub use util::serde::default_true;
 
-use crate::ActiveSettingsProfileName;
+use crate::{ActiveSettingsProfileName, merge_from};
 
 #[skip_serializing_none]
-#[derive(Debug, PartialEq, Default, Clone, Serialize, Deserialize, JsonSchema)]
+#[derive(Debug, PartialEq, Default, Clone, Serialize, Deserialize, JsonSchema, MergeFrom)]
 pub struct SettingsContent {
     #[serde(flatten)]
     pub project: ProjectSettingsContent,
@@ -153,7 +154,7 @@ pub struct SettingsContent {
     /// Whether to disable all AI features in Zed.
     ///
     /// Default: false
-    pub disable_ai: Option<bool>,
+    pub disable_ai: Option<SaturatingBool>,
 
     /// Settings related to Vim mode in Zed.
     pub vim: Option<VimSettingsContent>,
@@ -166,14 +167,14 @@ impl SettingsContent {
 }
 
 #[skip_serializing_none]
-#[derive(Debug, Default, Serialize, Deserialize, JsonSchema)]
+#[derive(Debug, Default, Serialize, Deserialize, JsonSchema, MergeFrom)]
 pub struct ServerSettingsContent {
     #[serde(flatten)]
     pub project: ProjectSettingsContent,
 }
 
 #[skip_serializing_none]
-#[derive(Debug, Default, PartialEq, Clone, Serialize, Deserialize, JsonSchema)]
+#[derive(Debug, Default, PartialEq, Clone, Serialize, Deserialize, JsonSchema, MergeFrom)]
 pub struct UserSettingsContent {
     #[serde(flatten)]
     pub content: Box<SettingsContent>,
@@ -225,7 +226,9 @@ impl UserSettingsContent {
 /// Base key bindings scheme. Base keymaps can be overridden with user keymaps.
 ///
 /// Default: VSCode
-#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Default)]
+#[derive(
+    Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq, Eq, Default,
+)]
 pub enum BaseKeymapContent {
     #[default]
     VSCode,
@@ -239,7 +242,7 @@ pub enum BaseKeymapContent {
 }
 
 #[skip_serializing_none]
-#[derive(Clone, PartialEq, Default, Serialize, Deserialize, JsonSchema, Debug)]
+#[derive(Clone, PartialEq, Default, Serialize, Deserialize, JsonSchema, MergeFrom, Debug)]
 pub struct TitleBarSettingsContent {
     /// Controls when the title bar is visible: "always" | "never" | "hide_in_full_screen".
     ///
@@ -275,7 +278,7 @@ pub struct TitleBarSettingsContent {
     pub show_menus: Option<bool>,
 }
 
-#[derive(Copy, Clone, PartialEq, Serialize, Deserialize, JsonSchema, Debug)]
+#[derive(Copy, Clone, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom, Debug)]
 #[serde(rename_all = "snake_case")]
 pub enum TitleBarVisibility {
     Always,
@@ -285,7 +288,7 @@ pub enum TitleBarVisibility {
 
 /// Configuration of audio in Zed.
 #[skip_serializing_none]
-#[derive(Clone, PartialEq, Default, Serialize, Deserialize, JsonSchema, Debug)]
+#[derive(Clone, PartialEq, Default, Serialize, Deserialize, JsonSchema, MergeFrom, Debug)]
 pub struct AudioSettingsContent {
     /// Opt into the new audio system.
     #[serde(rename = "experimental.rodio_audio", default)]
@@ -307,7 +310,7 @@ pub struct AudioSettingsContent {
 
 /// Control what info is collected by Zed.
 #[skip_serializing_none]
-#[derive(Default, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Debug)]
+#[derive(Default, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Debug, MergeFrom)]
 pub struct TelemetrySettingsContent {
     /// Send debug info like crash reports.
     ///
@@ -320,7 +323,7 @@ pub struct TelemetrySettingsContent {
 }
 
 #[skip_serializing_none]
-#[derive(Default, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Clone)]
+#[derive(Default, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Clone, MergeFrom)]
 pub struct DebuggerSettingsContent {
     /// Determines the stepping granularity.
     ///
@@ -353,7 +356,9 @@ pub struct DebuggerSettingsContent {
 }
 
 /// The granularity of one 'step' in the stepping requests `next`, `stepIn`, `stepOut`, and `stepBack`.
-#[derive(PartialEq, Eq, Debug, Hash, Clone, Copy, Deserialize, Serialize, JsonSchema)]
+#[derive(
+    PartialEq, Eq, Debug, Hash, Clone, Copy, Deserialize, Serialize, JsonSchema, MergeFrom,
+)]
 #[serde(rename_all = "snake_case")]
 pub enum SteppingGranularity {
     /// The step should allow the program to run until the current statement has finished executing.
@@ -366,7 +371,7 @@ pub enum SteppingGranularity {
     Instruction,
 }
 
-#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
+#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq, Eq)]
 #[serde(rename_all = "snake_case")]
 pub enum DockPosition {
     Left,
@@ -376,7 +381,7 @@ pub enum DockPosition {
 
 /// Settings for slash commands.
 #[skip_serializing_none]
-#[derive(Deserialize, Serialize, Debug, Default, Clone, JsonSchema, PartialEq, Eq)]
+#[derive(Deserialize, Serialize, Debug, Default, Clone, JsonSchema, MergeFrom, PartialEq, Eq)]
 pub struct SlashCommandSettings {
     /// Settings for the `/cargo-workspace` slash command.
     pub cargo_workspace: Option<CargoWorkspaceCommandSettings>,
@@ -384,7 +389,7 @@ pub struct SlashCommandSettings {
 
 /// Settings for the `/cargo-workspace` slash command.
 #[skip_serializing_none]
-#[derive(Deserialize, Serialize, Debug, Default, Clone, JsonSchema, PartialEq, Eq)]
+#[derive(Deserialize, Serialize, Debug, Default, Clone, JsonSchema, MergeFrom, PartialEq, Eq)]
 pub struct CargoWorkspaceCommandSettings {
     /// Whether `/cargo-workspace` is enabled.
     pub enabled: Option<bool>,
@@ -392,7 +397,7 @@ pub struct CargoWorkspaceCommandSettings {
 
 /// Configuration of voice calls in Zed.
 #[skip_serializing_none]
-#[derive(Clone, PartialEq, Default, Serialize, Deserialize, JsonSchema, Debug)]
+#[derive(Clone, PartialEq, Default, Serialize, Deserialize, JsonSchema, MergeFrom, Debug)]
 pub struct CallSettingsContent {
     /// Whether the microphone should be muted when joining a channel or a call.
     ///
@@ -406,7 +411,7 @@ pub struct CallSettingsContent {
 }
 
 #[skip_serializing_none]
-#[derive(Deserialize, Serialize, PartialEq, Debug, Default, Clone, JsonSchema)]
+#[derive(Deserialize, Serialize, PartialEq, Debug, Default, Clone, JsonSchema, MergeFrom)]
 pub struct ExtensionSettingsContent {
     /// The extensions that should be automatically installed by Zed.
     ///
@@ -421,7 +426,7 @@ pub struct ExtensionSettingsContent {
 }
 
 #[skip_serializing_none]
-#[derive(Clone, PartialEq, Default, Serialize, Deserialize, JsonSchema, Debug)]
+#[derive(Clone, PartialEq, Default, Serialize, Deserialize, JsonSchema, MergeFrom, Debug)]
 pub struct GitPanelSettingsContent {
     /// Whether to show the panel button in the status bar.
     ///
@@ -462,7 +467,9 @@ pub struct GitPanelSettingsContent {
     pub collapse_untracked_diff: Option<bool>,
 }
 
-#[derive(Default, Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
+#[derive(
+    Default, Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq, Eq,
+)]
 #[serde(rename_all = "snake_case")]
 pub enum StatusStyle {
     #[default]
@@ -471,13 +478,13 @@ pub enum StatusStyle {
 }
 
 #[skip_serializing_none]
-#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
+#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq, Eq)]
 pub struct ScrollbarSettings {
     pub show: Option<ShowScrollbar>,
 }
 
 #[skip_serializing_none]
-#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, PartialEq)]
+#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, MergeFrom, Debug, PartialEq)]
 pub struct NotificationPanelSettingsContent {
     /// Whether to show the panel button in the status bar.
     ///
@@ -494,7 +501,7 @@ pub struct NotificationPanelSettingsContent {
 }
 
 #[skip_serializing_none]
-#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, PartialEq)]
+#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, MergeFrom, Debug, PartialEq)]
 pub struct PanelSettingsContent {
     /// Whether to show the panel button in the status bar.
     ///
@@ -511,7 +518,7 @@ pub struct PanelSettingsContent {
 }
 
 #[skip_serializing_none]
-#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, PartialEq)]
+#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, MergeFrom, Debug, PartialEq)]
 pub struct MessageEditorSettings {
     /// Whether to automatically replace emoji shortcodes with emoji characters.
     /// For example: typing `:wave:` gets replaced with `👋`.
@@ -521,7 +528,7 @@ pub struct MessageEditorSettings {
 }
 
 #[skip_serializing_none]
-#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, PartialEq)]
+#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, MergeFrom, Debug, PartialEq)]
 pub struct FileFinderSettingsContent {
     /// Whether to show file icons in the file finder.
     ///
@@ -549,10 +556,12 @@ pub struct FileFinderSettingsContent {
     ///
     /// Default: None
     /// todo() -> Change this type to an enum
-    pub include_ignored: Option<Option<bool>>,
+    pub include_ignored: Option<bool>,
 }
 
-#[derive(Debug, PartialEq, Eq, Clone, Copy, Default, Serialize, Deserialize, JsonSchema)]
+#[derive(
+    Debug, PartialEq, Eq, Clone, Copy, Default, Serialize, Deserialize, JsonSchema, MergeFrom,
+)]
 #[serde(rename_all = "lowercase")]
 pub enum FileFinderWidthContent {
     #[default]
@@ -564,7 +573,7 @@ pub enum FileFinderWidthContent {
 }
 
 #[skip_serializing_none]
-#[derive(Clone, Default, Serialize, Deserialize, PartialEq, Debug, JsonSchema)]
+#[derive(Clone, Default, Serialize, Deserialize, PartialEq, Debug, JsonSchema, MergeFrom)]
 pub struct VimSettingsContent {
     pub default_mode: Option<ModeContent>,
     pub toggle_relative_line_numbers: Option<bool>,
@@ -575,7 +584,7 @@ pub struct VimSettingsContent {
     pub cursor_shape: Option<CursorShapeSettings>,
 }
 
-#[derive(Copy, Clone, Default, Serialize, Deserialize, JsonSchema, PartialEq, Debug)]
+#[derive(Copy, Clone, Default, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq, Debug)]
 #[serde(rename_all = "snake_case")]
 pub enum ModeContent {
     #[default]
@@ -585,7 +594,7 @@ pub enum ModeContent {
 }
 
 /// Controls when to use system clipboard.
-#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
+#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, MergeFrom)]
 #[serde(rename_all = "snake_case")]
 pub enum UseSystemClipboard {
     /// Don't use system clipboard.
@@ -598,7 +607,7 @@ pub enum UseSystemClipboard {
 
 /// The settings for cursor shape.
 #[skip_serializing_none]
-#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
+#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, MergeFrom)]
 pub struct CursorShapeSettings {
     /// Cursor shape for the normal mode.
     ///
@@ -620,7 +629,7 @@ pub struct CursorShapeSettings {
 
 /// Settings specific to journaling
 #[skip_serializing_none]
-#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
+#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq)]
 pub struct JournalSettingsContent {
     /// The path of the directory where journal entries are stored.
     ///
@@ -632,7 +641,7 @@ pub struct JournalSettingsContent {
     pub hour_format: Option<HourFormat>,
 }
 
-#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, PartialEq)]
+#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq)]
 #[serde(rename_all = "snake_case")]
 pub enum HourFormat {
     #[default]
@@ -641,7 +650,7 @@ pub enum HourFormat {
 }
 
 #[skip_serializing_none]
-#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, PartialEq)]
+#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, MergeFrom, Debug, PartialEq)]
 pub struct OutlinePanelSettingsContent {
     /// Whether to show the outline panel button in the status bar.
     ///
@@ -695,14 +704,14 @@ pub struct OutlinePanelSettingsContent {
     pub expand_outlines_with_depth: Option<usize>,
 }
 
-#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Copy, PartialEq)]
+#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, MergeFrom, Copy, PartialEq)]
 #[serde(rename_all = "snake_case")]
 pub enum DockSide {
     Left,
     Right,
 }
 
-#[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize, Serialize, JsonSchema)]
+#[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize, Serialize, JsonSchema, MergeFrom)]
 #[serde(rename_all = "snake_case")]
 pub enum ShowIndentGuides {
     Always,
@@ -710,13 +719,13 @@ pub enum ShowIndentGuides {
 }
 
 #[skip_serializing_none]
-#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
+#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq, Eq)]
 pub struct IndentGuidesSettingsContent {
     /// When to show the scrollbar in the outline panel.
     pub show: Option<ShowIndentGuides>,
 }
 
-#[derive(Clone, Copy, Default, PartialEq, Debug, JsonSchema, Deserialize, Serialize)]
+#[derive(Clone, Copy, Default, PartialEq, Debug, JsonSchema, MergeFrom, Deserialize, Serialize)]
 #[serde(rename_all = "snake_case")]
 pub enum LineIndicatorFormat {
     Short,
@@ -726,7 +735,7 @@ pub enum LineIndicatorFormat {
 
 /// The settings for the image viewer.
 #[skip_serializing_none]
-#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Default, PartialEq)]
+#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, MergeFrom, Default, PartialEq)]
 pub struct ImageViewerSettingsContent {
     /// The unit to use for displaying image file sizes.
     ///
@@ -735,7 +744,7 @@ pub struct ImageViewerSettingsContent {
 }
 
 #[skip_serializing_none]
-#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, Default, PartialEq)]
+#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, MergeFrom, Default, PartialEq)]
 #[serde(rename_all = "snake_case")]
 pub enum ImageFileSizeUnit {
     /// Displays file size in binary units (e.g., KiB, MiB).
@@ -746,7 +755,7 @@ pub enum ImageFileSizeUnit {
 }
 
 #[skip_serializing_none]
-#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, PartialEq)]
+#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq)]
 pub struct RemoteSettingsContent {
     pub ssh_connections: Option<Vec<SshConnection>>,
     pub wsl_connections: Option<Vec<WslConnection>>,
@@ -754,7 +763,7 @@ pub struct RemoteSettingsContent {
 }
 
 #[skip_serializing_none]
-#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, JsonSchema)]
+#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, JsonSchema, MergeFrom)]
 pub struct SshConnection {
     pub host: SharedString,
     pub username: Option<String>,
@@ -774,7 +783,7 @@ pub struct SshConnection {
     pub port_forwards: Option<Vec<SshPortForwardOption>>,
 }
 
-#[derive(Clone, Default, Serialize, Deserialize, PartialEq, JsonSchema, Debug)]
+#[derive(Clone, Default, Serialize, Deserialize, PartialEq, JsonSchema, MergeFrom, Debug)]
 pub struct WslConnection {
     pub distro_name: SharedString,
     pub user: Option<String>,
@@ -791,7 +800,7 @@ pub struct SshProject {
 }
 
 #[skip_serializing_none]
-#[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize, Serialize, JsonSchema)]
+#[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize, Serialize, JsonSchema, MergeFrom)]
 pub struct SshPortForwardOption {
     #[serde(skip_serializing_if = "Option::is_none")]
     pub local_host: Option<String>,
@@ -803,7 +812,7 @@ pub struct SshPortForwardOption {
 
 /// Settings for configuring REPL display and behavior.
 #[skip_serializing_none]
-#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
+#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)]
 pub struct ReplSettingsContent {
     /// Maximum number of lines to keep in REPL's scrollback buffer.
     /// Clamped with [4, 256] range.
@@ -816,3 +825,42 @@ pub struct ReplSettingsContent {
     /// Default: 128
     pub max_columns: Option<usize>,
 }
+
+#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
+pub struct ExtendingVec<T>(pub Vec<T>);
+
+impl<T> Into<Vec<T>> for ExtendingVec<T> {
+    fn into(self) -> Vec<T> {
+        self.0
+    }
+}
+impl<T> From<Vec<T>> for ExtendingVec<T> {
+    fn from(vec: Vec<T>) -> Self {
+        ExtendingVec(vec)
+    }
+}
+
+impl<T: Clone> merge_from::MergeFrom for ExtendingVec<T> {
+    fn merge_from(&mut self, other: Option<&Self>) {
+        if let Some(other) = other {
+            self.0.extend_from_slice(other.0.as_slice());
+        }
+    }
+}
+
+#[derive(Debug, Default, Copy, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
+pub struct SaturatingBool(pub bool);
+
+impl From<bool> for SaturatingBool {
+    fn from(value: bool) -> Self {
+        SaturatingBool(value)
+    }
+}
+
+impl merge_from::MergeFrom for SaturatingBool {
+    fn merge_from(&mut self, other: Option<&Self>) {
+        if let Some(other) = other {
+            self.0 |= other.0
+        }
+    }
+}

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

@@ -3,12 +3,13 @@ use gpui::SharedString;
 use schemars::{JsonSchema, json_schema};
 use serde::{Deserialize, Serialize};
 use serde_with::skip_serializing_none;
+use settings_macros::MergeFrom;
 use std::{borrow::Cow, path::PathBuf, sync::Arc};
 
 use crate::DockPosition;
 
 #[skip_serializing_none]
-#[derive(Clone, PartialEq, Serialize, Deserialize, JsonSchema, Debug, Default)]
+#[derive(Clone, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom, Debug, Default)]
 pub struct AgentSettingsContent {
     /// Whether the Agent is enabled.
     ///
@@ -168,7 +169,7 @@ impl AgentSettingsContent {
 }
 
 #[skip_serializing_none]
-#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, JsonSchema)]
+#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, JsonSchema, MergeFrom)]
 pub struct AgentProfileContent {
     pub name: Arc<str>,
     #[serde(default)]
@@ -180,12 +181,12 @@ pub struct AgentProfileContent {
 }
 
 #[skip_serializing_none]
-#[derive(Debug, PartialEq, Clone, Default, Serialize, Deserialize, JsonSchema)]
+#[derive(Debug, PartialEq, Clone, Default, Serialize, Deserialize, JsonSchema, MergeFrom)]
 pub struct ContextServerPresetContent {
     pub tools: IndexMap<Arc<str>, bool>,
 }
 
-#[derive(Copy, Clone, Default, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
+#[derive(Copy, Clone, Default, Debug, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)]
 #[serde(rename_all = "snake_case")]
 pub enum DefaultAgentView {
     #[default]
@@ -193,7 +194,7 @@ pub enum DefaultAgentView {
     TextThread,
 }
 
-#[derive(Copy, Clone, Default, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
+#[derive(Copy, Clone, Default, Debug, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq)]
 #[serde(rename_all = "snake_case")]
 pub enum NotifyWhenAgentWaiting {
     #[default]
@@ -203,13 +204,13 @@ pub enum NotifyWhenAgentWaiting {
 }
 
 #[skip_serializing_none]
-#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
+#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq)]
 pub struct LanguageModelSelection {
     pub provider: LanguageModelProviderSetting,
     pub model: String,
 }
 
-#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Default)]
+#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq, Default)]
 #[serde(rename_all = "snake_case")]
 pub enum CompletionMode {
     #[default]
@@ -219,14 +220,14 @@ pub enum CompletionMode {
 }
 
 #[skip_serializing_none]
-#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
+#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq)]
 pub struct LanguageModelParameters {
     pub provider: Option<LanguageModelProviderSetting>,
     pub model: Option<SharedString>,
     pub temperature: Option<f32>,
 }
 
-#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
+#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, MergeFrom)]
 pub struct LanguageModelProviderSetting(pub String);
 
 impl JsonSchema for LanguageModelProviderSetting {
@@ -277,7 +278,7 @@ impl From<&str> for LanguageModelProviderSetting {
 }
 
 #[skip_serializing_none]
-#[derive(Default, PartialEq, Deserialize, Serialize, Clone, JsonSchema, Debug)]
+#[derive(Default, PartialEq, Deserialize, Serialize, Clone, JsonSchema, MergeFrom, Debug)]
 pub struct AllAgentServersSettings {
     pub gemini: Option<BuiltinAgentServerSettings>,
     pub claude: Option<BuiltinAgentServerSettings>,
@@ -288,7 +289,7 @@ pub struct AllAgentServersSettings {
 }
 
 #[skip_serializing_none]
-#[derive(Default, Deserialize, Serialize, Clone, JsonSchema, Debug, PartialEq)]
+#[derive(Default, Deserialize, Serialize, Clone, JsonSchema, MergeFrom, Debug, PartialEq)]
 pub struct BuiltinAgentServerSettings {
     /// Absolute path to a binary to be used when launching this agent.
     ///
@@ -320,7 +321,7 @@ pub struct BuiltinAgentServerSettings {
 }
 
 #[skip_serializing_none]
-#[derive(Deserialize, Serialize, Clone, JsonSchema, Debug, PartialEq)]
+#[derive(Deserialize, Serialize, Clone, JsonSchema, MergeFrom, Debug, PartialEq)]
 pub struct CustomAgentServerSettings {
     #[serde(rename = "command")]
     pub path: PathBuf,

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

@@ -4,11 +4,12 @@ use collections::HashMap;
 use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
 use serde_with::skip_serializing_none;
+use settings_macros::MergeFrom;
 
 use crate::{DiagnosticSeverityContent, ShowScrollbar};
 
 #[skip_serializing_none]
-#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
+#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)]
 pub struct EditorSettingsContent {
     /// Whether the cursor blinks in the editor.
     ///
@@ -194,7 +195,7 @@ pub struct EditorSettingsContent {
 
 // Status bar related settings
 #[skip_serializing_none]
-#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
+#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq, Eq)]
 pub struct StatusBarContent {
     /// Whether to display the active language button in the status bar.
     ///
@@ -208,7 +209,7 @@ pub struct StatusBarContent {
 
 // Toolbar related settings
 #[skip_serializing_none]
-#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
+#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq, Eq)]
 pub struct ToolbarContent {
     /// Whether to display breadcrumbs in the editor toolbar.
     ///
@@ -235,7 +236,7 @@ pub struct ToolbarContent {
 
 /// Scrollbar related settings
 #[skip_serializing_none]
-#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Default)]
+#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq, Default)]
 pub struct ScrollbarContent {
     /// When to show the scrollbar in the editor.
     ///
@@ -271,7 +272,7 @@ pub struct ScrollbarContent {
 
 /// Minimap related settings
 #[skip_serializing_none]
-#[derive(Clone, Default, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
+#[derive(Clone, Default, Debug, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq)]
 pub struct MinimapContent {
     /// When to show the minimap in the editor.
     ///
@@ -296,7 +297,7 @@ pub struct MinimapContent {
     /// How to highlight the current line in the minimap.
     ///
     /// Default: inherits editor line highlights setting
-    pub current_line_highlight: Option<Option<CurrentLineHighlight>>,
+    pub current_line_highlight: Option<CurrentLineHighlight>,
 
     /// Maximum number of columns to display in the minimap.
     ///
@@ -306,7 +307,7 @@ pub struct MinimapContent {
 
 /// Forcefully enable or disable the scrollbar for each axis
 #[skip_serializing_none]
-#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Default)]
+#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq, Default)]
 pub struct ScrollbarAxesContent {
     /// When false, forcefully disables the horizontal scrollbar. Otherwise, obey other settings.
     ///
@@ -321,7 +322,7 @@ pub struct ScrollbarAxesContent {
 
 /// Gutter related settings
 #[skip_serializing_none]
-#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
+#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq, Eq)]
 pub struct GutterContent {
     /// Whether to show line numbers in the gutter.
     ///
@@ -346,7 +347,9 @@ pub struct GutterContent {
 }
 
 /// How to render LSP `textDocument/documentColor` colors in the editor.
-#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
+#[derive(
+    Copy, Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema, MergeFrom,
+)]
 #[serde(rename_all = "snake_case")]
 pub enum DocumentColorsRenderMode {
     /// Do not query and render document colors.
@@ -360,7 +363,7 @@ pub enum DocumentColorsRenderMode {
     Background,
 }
 
-#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
+#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, MergeFrom)]
 #[serde(rename_all = "snake_case")]
 pub enum CurrentLineHighlight {
     // Don't highlight the current line.
@@ -374,7 +377,7 @@ pub enum CurrentLineHighlight {
 }
 
 /// When to populate a new search's query based on the text under the cursor.
-#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
+#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, MergeFrom)]
 #[serde(rename_all = "snake_case")]
 pub enum SeedQuerySetting {
     /// Always populate the search query with the word under the cursor.
@@ -386,7 +389,9 @@ pub enum SeedQuerySetting {
 }
 
 /// What to do when multibuffer is double clicked in some of its excerpts (parts of singleton buffers).
-#[derive(Default, Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
+#[derive(
+    Default, Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, MergeFrom,
+)]
 #[serde(rename_all = "snake_case")]
 pub enum DoubleClickInMultibuffer {
     /// Behave as a regular buffer and select the whole word.
@@ -400,7 +405,9 @@ pub enum DoubleClickInMultibuffer {
 /// When to show the minimap thumb.
 ///
 /// Default: always
-#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
+#[derive(
+    Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq, Eq,
+)]
 #[serde(rename_all = "snake_case")]
 pub enum MinimapThumb {
     /// Show the minimap thumb only when the mouse is hovering over the minimap.
@@ -413,7 +420,9 @@ pub enum MinimapThumb {
 /// Defines the border style for the minimap's scrollbar thumb.
 ///
 /// Default: left_open
-#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
+#[derive(
+    Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq, Eq,
+)]
 #[serde(rename_all = "snake_case")]
 pub enum MinimapThumbBorder {
     /// Displays a border on all sides of the thumb.
@@ -432,7 +441,7 @@ pub enum MinimapThumbBorder {
 /// Which diagnostic indicators to show in the scrollbar.
 ///
 /// Default: all
-#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
+#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq, Eq)]
 #[serde(rename_all = "lowercase")]
 pub enum ScrollbarDiagnostics {
     /// Show all diagnostic levels: hint, information, warnings, error.
@@ -450,7 +459,7 @@ pub enum ScrollbarDiagnostics {
 /// The key to use for adding multiple cursors
 ///
 /// Default: alt
-#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
+#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq, Eq)]
 #[serde(rename_all = "snake_case")]
 pub enum MultiCursorModifier {
     Alt,
@@ -461,7 +470,7 @@ pub enum MultiCursorModifier {
 /// Whether the editor will scroll beyond the last line.
 ///
 /// Default: one_page
-#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
+#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq, Eq)]
 #[serde(rename_all = "snake_case")]
 pub enum ScrollBeyondLastLine {
     /// The editor will not scroll beyond the last line.
@@ -475,7 +484,9 @@ pub enum ScrollBeyondLastLine {
 }
 
 /// The shape of a selection cursor.
-#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
+#[derive(
+    Copy, Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema, MergeFrom,
+)]
 #[serde(rename_all = "snake_case")]
 pub enum CursorShape {
     /// A vertical bar
@@ -490,7 +501,9 @@ pub enum CursorShape {
 }
 
 /// What to do when go to definition yields no results.
-#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
+#[derive(
+    Copy, Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema, MergeFrom,
+)]
 #[serde(rename_all = "snake_case")]
 pub enum GoToDefinitionFallback {
     /// Disables the fallback.
@@ -503,7 +516,9 @@ pub enum GoToDefinitionFallback {
 /// Determines when the mouse cursor should be hidden in an editor or input box.
 ///
 /// Default: on_typing_and_movement
-#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
+#[derive(
+    Copy, Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema, MergeFrom,
+)]
 #[serde(rename_all = "snake_case")]
 pub enum HideMouseMode {
     /// Never hide the mouse cursor
@@ -518,7 +533,9 @@ pub enum HideMouseMode {
 /// Determines how snippets are sorted relative to other completion items.
 ///
 /// Default: inline
-#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
+#[derive(
+    Copy, Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema, MergeFrom,
+)]
 #[serde(rename_all = "snake_case")]
 pub enum SnippetSortOrder {
     /// Place snippets at the top of the completion list
@@ -534,7 +551,7 @@ pub enum SnippetSortOrder {
 
 /// Default options for buffer and project search items.
 #[skip_serializing_none]
-#[derive(Clone, Default, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
+#[derive(Clone, Default, Debug, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq, Eq)]
 pub struct SearchSettingsContent {
     /// Whether to show the project search button in the status bar.
     pub button: Option<bool>,
@@ -545,7 +562,7 @@ pub struct SearchSettingsContent {
 }
 
 #[skip_serializing_none]
-#[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
+#[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, MergeFrom)]
 #[serde(rename_all = "snake_case")]
 pub struct JupyterContent {
     /// Whether the Jupyter feature is enabled.
@@ -561,7 +578,7 @@ pub struct JupyterContent {
 
 /// Whether to allow drag and drop text selection in buffer.
 #[skip_serializing_none]
-#[derive(Clone, Default, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
+#[derive(Clone, Default, Debug, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq, Eq)]
 pub struct DragAndDropSelectionContent {
     /// When true, enables drag and drop text selection in buffer.
     ///
@@ -577,7 +594,9 @@ pub struct DragAndDropSelectionContent {
 /// When to show the minimap in the editor.
 ///
 /// Default: never
-#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
+#[derive(
+    Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq, Eq,
+)]
 #[serde(rename_all = "snake_case")]
 pub enum ShowMinimap {
     /// Follow the visibility of the scrollbar.
@@ -592,7 +611,9 @@ pub enum ShowMinimap {
 /// Where to show the minimap in the editor.
 ///
 /// Default: all_editors
-#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
+#[derive(
+    Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq, Eq,
+)]
 #[serde(rename_all = "snake_case")]
 pub enum DisplayIn {
     /// Show on all open editors.

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

@@ -8,10 +8,10 @@ use serde::{
     de::{self, IntoDeserializer, MapAccess, SeqAccess, Visitor},
 };
 use serde_with::skip_serializing_none;
+use settings_macros::MergeFrom;
 use std::sync::Arc;
-use util::schemars::replace_subschema;
 
-use crate::ParameterizedJsonSchema;
+use crate::{ExtendingVec, merge_from};
 
 #[skip_serializing_none]
 #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
@@ -31,12 +31,50 @@ pub struct AllLanguageSettingsContent {
     /// Settings for associating file extensions and filenames
     /// with languages.
     #[serde(default)]
-    pub file_types: HashMap<Arc<str>, Vec<String>>,
+    pub file_types: HashMap<Arc<str>, ExtendingVec<String>>,
+}
+
+fn merge_option<T: merge_from::MergeFrom + Clone>(this: &mut Option<T>, other: Option<&T>) {
+    let Some(other) = other else { return };
+    if let Some(this) = this {
+        this.merge_from(Some(other));
+    } else {
+        this.replace(other.clone());
+    }
+}
+
+impl merge_from::MergeFrom for AllLanguageSettingsContent {
+    fn merge_from(&mut self, other: Option<&Self>) {
+        let Some(other) = other else { return };
+        self.file_types.merge_from(Some(&other.file_types));
+        merge_option(&mut self.features, other.features.as_ref());
+        merge_option(&mut self.edit_predictions, other.edit_predictions.as_ref());
+
+        // A user's global settings override the default global settings and
+        // all default language-specific settings.
+        //
+        self.defaults.merge_from(Some(&other.defaults));
+        for language_settings in self.languages.0.values_mut() {
+            language_settings.merge_from(Some(&other.defaults));
+        }
+
+        // A user's language-specific settings override default language-specific settings.
+        for (language_name, user_language_settings) in &other.languages.0 {
+            if let Some(existing) = self.languages.0.get_mut(language_name) {
+                existing.merge_from(Some(&user_language_settings));
+            } else {
+                let mut new_settings = self.defaults.clone();
+                new_settings.merge_from(Some(&user_language_settings));
+
+                self.languages.0.insert(language_name.clone(), new_settings);
+            }
+        }
+    }
 }
 
 /// The settings for enabling/disabling features.
 #[skip_serializing_none]
-#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize, JsonSchema)]
+#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize, JsonSchema, MergeFrom)]
 #[serde(rename_all = "snake_case")]
 pub struct FeaturesContent {
     /// Determines which edit prediction provider to use.
@@ -44,7 +82,9 @@ pub struct FeaturesContent {
 }
 
 /// The provider that supplies edit predictions.
-#[derive(Copy, Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize, JsonSchema)]
+#[derive(
+    Copy, Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom,
+)]
 #[serde(rename_all = "snake_case")]
 pub enum EditPredictionProvider {
     None,
@@ -56,7 +96,7 @@ pub enum EditPredictionProvider {
 
 /// The contents of the edit prediction settings.
 #[skip_serializing_none]
-#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, PartialEq)]
+#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq)]
 pub struct EditPredictionSettingsContent {
     /// A list of globs representing files that edit predictions should be disabled for.
     /// This list adds to a pre-existing, sensible default set of globs.
@@ -73,7 +113,7 @@ pub struct EditPredictionSettingsContent {
 }
 
 #[skip_serializing_none]
-#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, PartialEq)]
+#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq)]
 pub struct CopilotSettingsContent {
     /// HTTP/HTTPS proxy to use for Copilot.
     ///
@@ -90,7 +130,9 @@ pub struct CopilotSettingsContent {
 }
 
 /// The mode in which edit predictions should be displayed.
-#[derive(Copy, Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize, JsonSchema)]
+#[derive(
+    Copy, Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom,
+)]
 #[serde(rename_all = "snake_case")]
 pub enum EditPredictionsMode {
     /// If provider supports it, display inline when holding modifier key (e.g., alt).
@@ -104,7 +146,7 @@ pub enum EditPredictionsMode {
 }
 
 /// Controls the soft-wrapping behavior in the editor.
-#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
+#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, MergeFrom)]
 #[serde(rename_all = "snake_case")]
 pub enum SoftWrap {
     /// Prefer a single line generally, unless an overly long line is encountered.
@@ -122,7 +164,7 @@ pub enum SoftWrap {
 
 /// The settings for a particular language.
 #[skip_serializing_none]
-#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
+#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)]
 pub struct LanguageSettingsContent {
     /// How many columns a tab should occupy.
     ///
@@ -289,7 +331,7 @@ pub struct LanguageSettingsContent {
 }
 
 /// Controls how whitespace should be displayedin the editor.
-#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
+#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, MergeFrom)]
 #[serde(rename_all = "snake_case")]
 pub enum ShowWhitespaceSetting {
     /// Draw whitespace only for the selected text.
@@ -310,7 +352,7 @@ pub enum ShowWhitespaceSetting {
 }
 
 #[skip_serializing_none]
-#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, PartialEq)]
+#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq)]
 pub struct WhitespaceMap {
     pub space: Option<String>,
     pub tab: Option<String>,
@@ -331,7 +373,7 @@ impl WhitespaceMap {
 }
 
 /// The behavior of `editor::Rewrap`.
-#[derive(Debug, PartialEq, Clone, Copy, Default, Serialize, Deserialize, JsonSchema)]
+#[derive(Debug, PartialEq, Clone, Copy, Default, Serialize, Deserialize, JsonSchema, MergeFrom)]
 #[serde(rename_all = "snake_case")]
 pub enum RewrapBehavior {
     /// Only rewrap within comments.
@@ -344,7 +386,7 @@ pub enum RewrapBehavior {
 }
 
 #[skip_serializing_none]
-#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
+#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, MergeFrom)]
 pub struct JsxTagAutoCloseSettingsContent {
     /// Enables or disables auto-closing of JSX tags.
     pub enabled: Option<bool>,
@@ -352,7 +394,7 @@ pub struct JsxTagAutoCloseSettingsContent {
 
 /// The settings for inlay hints.
 #[skip_serializing_none]
-#[derive(Clone, Default, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
+#[derive(Clone, Default, Debug, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq, Eq)]
 pub struct InlayHintSettingsContent {
     /// Global switch to toggle hints on and off.
     ///
@@ -434,7 +476,7 @@ impl InlayHintKind {
 
 /// Controls how completions are processed for this language.
 #[skip_serializing_none]
-#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema, Default)]
+#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema, MergeFrom, Default)]
 #[serde(rename_all = "snake_case")]
 pub struct CompletionSettingsContent {
     /// Controls how words are completed.
@@ -462,7 +504,7 @@ pub struct CompletionSettingsContent {
     pub lsp_insert_mode: Option<LspInsertMode>,
 }
 
-#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
+#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, MergeFrom)]
 #[serde(rename_all = "snake_case")]
 pub enum LspInsertMode {
     /// Replaces text before the cursor, using the `insert` range described in the LSP specification.
@@ -478,7 +520,7 @@ pub enum LspInsertMode {
 }
 
 /// Controls how document's words are completed.
-#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
+#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, MergeFrom)]
 #[serde(rename_all = "snake_case")]
 pub enum WordsCompletionMode {
     /// Always fetch document's words for completions along with LSP completions.
@@ -495,7 +537,7 @@ pub enum WordsCompletionMode {
 /// and configure default Prettier, used when no project-level Prettier installation is found.
 /// Prettier formatting is disabled by default.
 #[skip_serializing_none]
-#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
+#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, MergeFrom)]
 pub struct PrettierSettingsContent {
     /// Enables or disables formatting with Prettier for a given language.
     pub allowed: Option<bool>,
@@ -515,7 +557,7 @@ pub struct PrettierSettingsContent {
 }
 
 /// Controls the behavior of formatting files when they are saved.
-#[derive(Debug, Clone, PartialEq, Eq)]
+#[derive(Debug, Clone, PartialEq, Eq, MergeFrom)]
 pub enum FormatOnSave {
     /// Files should be formatted on save.
     On,
@@ -614,7 +656,7 @@ impl<'de> Deserialize<'de> for FormatOnSave {
 }
 
 /// Controls which formatter should be used when formatting code.
-#[derive(Clone, Debug, Default, PartialEq, Eq)]
+#[derive(Clone, Debug, Default, PartialEq, Eq, MergeFrom)]
 pub enum SelectedFormatter {
     /// Format files using Zed's Prettier integration (if applicable),
     /// or falling back to formatting via language server.
@@ -710,7 +752,7 @@ impl<'de> Deserialize<'de> for SelectedFormatter {
 }
 
 /// Controls which formatters should be used when formatting code.
-#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
+#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, MergeFrom)]
 #[serde(untagged)]
 pub enum FormatterList {
     Single(Formatter),
@@ -733,7 +775,7 @@ impl AsRef<[Formatter]> for FormatterList {
 }
 
 /// Controls which formatter should be used when formatting code. If there are multiple formatters, they are executed in the order of declaration.
-#[derive(Clone, Default, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
+#[derive(Clone, Default, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, MergeFrom)]
 #[serde(rename_all = "snake_case")]
 pub enum Formatter {
     /// Format code using the current language server.
@@ -754,7 +796,7 @@ pub enum Formatter {
 
 /// The settings for indent guides.
 #[skip_serializing_none]
-#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
+#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, MergeFrom)]
 pub struct IndentGuideSettingsContent {
     /// Whether to display indent guides in the editor.
     ///
@@ -780,7 +822,7 @@ pub struct IndentGuideSettingsContent {
 
 /// The task settings for a particular language.
 #[skip_serializing_none]
-#[derive(Debug, Clone, Deserialize, PartialEq, Serialize, JsonSchema)]
+#[derive(Debug, Clone, Deserialize, PartialEq, Serialize, JsonSchema, MergeFrom)]
 pub struct LanguageTaskSettingsContent {
     /// Extra task variables to set for a particular language.
     #[serde(default)]
@@ -796,37 +838,15 @@ pub struct LanguageTaskSettingsContent {
     pub prefer_lsp: Option<bool>,
 }
 
-/// Map from language name to settings. Its `ParameterizedJsonSchema` allows only known language
-/// names in the keys.
+/// Map from language name to settings.
 #[skip_serializing_none]
-#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
+#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)]
 pub struct LanguageToSettingsMap(pub HashMap<SharedString, LanguageSettingsContent>);
 
-inventory::submit! {
-    ParameterizedJsonSchema {
-        add_and_get_ref: |generator, params, _cx| {
-            let language_settings_content_ref = generator
-                .subschema_for::<LanguageSettingsContent>()
-                .to_value();
-            replace_subschema::<LanguageToSettingsMap>(generator, || json_schema!({
-                "type": "object",
-                "properties": params
-                    .language_names
-                    .iter()
-                    .map(|name| {
-                        (
-                            name.clone(),
-                            language_settings_content_ref.clone(),
-                        )
-                    })
-                    .collect::<serde_json::Map<_, _>>()
-            }))
-        }
-    }
-}
-
 /// Determines how indent guides are colored.
-#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
+#[derive(
+    Default, Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, MergeFrom,
+)]
 #[serde(rename_all = "snake_case")]
 pub enum IndentGuideColoring {
     /// Do not render any lines for indent guides.
@@ -839,7 +859,9 @@ pub enum IndentGuideColoring {
 }
 
 /// Determines how indent guide backgrounds are colored.
-#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
+#[derive(
+    Default, Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, MergeFrom,
+)]
 #[serde(rename_all = "snake_case")]
 pub enum IndentGuideBackgroundColoring {
     /// Do not render any background for indent guides.

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

@@ -2,11 +2,12 @@ use collections::HashMap;
 use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
 use serde_with::skip_serializing_none;
+use settings_macros::MergeFrom;
 
 use std::sync::Arc;
 
 #[skip_serializing_none]
-#[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)]
+#[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema, MergeFrom)]
 pub struct AllLanguageModelSettingsContent {
     pub anthropic: Option<AnthropicSettingsContent>,
     pub bedrock: Option<AmazonBedrockSettingsContent>,
@@ -25,14 +26,14 @@ pub struct AllLanguageModelSettingsContent {
 }
 
 #[skip_serializing_none]
-#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)]
+#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema, MergeFrom)]
 pub struct AnthropicSettingsContent {
     pub api_url: Option<String>,
     pub available_models: Option<Vec<AnthropicAvailableModel>>,
 }
 
 #[skip_serializing_none]
-#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
+#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)]
 pub struct AnthropicAvailableModel {
     /// The model's name in the Anthropic API. e.g. claude-3-5-sonnet-latest, claude-3-opus-20240229, etc
     pub name: String,
@@ -53,7 +54,7 @@ pub struct AnthropicAvailableModel {
 }
 
 #[skip_serializing_none]
-#[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)]
+#[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema, MergeFrom)]
 pub struct AmazonBedrockSettingsContent {
     pub available_models: Option<Vec<BedrockAvailableModel>>,
     pub endpoint_url: Option<String>,
@@ -63,7 +64,7 @@ pub struct AmazonBedrockSettingsContent {
 }
 
 #[skip_serializing_none]
-#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
+#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)]
 pub struct BedrockAvailableModel {
     pub name: String,
     pub display_name: Option<String>,
@@ -74,7 +75,7 @@ pub struct BedrockAvailableModel {
     pub mode: Option<ModelMode>,
 }
 
-#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
+#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)]
 pub enum BedrockAuthMethodContent {
     #[serde(rename = "named_profile")]
     NamedProfile,
@@ -86,14 +87,14 @@ pub enum BedrockAuthMethodContent {
 }
 
 #[skip_serializing_none]
-#[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)]
+#[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema, MergeFrom)]
 pub struct OllamaSettingsContent {
     pub api_url: Option<String>,
     pub available_models: Option<Vec<OllamaAvailableModel>>,
 }
 
 #[skip_serializing_none]
-#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
+#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)]
 pub struct OllamaAvailableModel {
     /// The model name in the Ollama API (e.g. "llama3.2:latest")
     pub name: String,
@@ -111,7 +112,7 @@ pub struct OllamaAvailableModel {
     pub supports_thinking: Option<bool>,
 }
 
-#[derive(Clone, Serialize, Deserialize, Debug, Eq, PartialEq, JsonSchema)]
+#[derive(Clone, Serialize, Deserialize, Debug, Eq, PartialEq, JsonSchema, MergeFrom)]
 #[serde(untagged)]
 pub enum KeepAlive {
     /// Keep model alive for N seconds
@@ -134,14 +135,14 @@ impl Default for KeepAlive {
 }
 
 #[skip_serializing_none]
-#[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)]
+#[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema, MergeFrom)]
 pub struct LmStudioSettingsContent {
     pub api_url: Option<String>,
     pub available_models: Option<Vec<LmStudioAvailableModel>>,
 }
 
 #[skip_serializing_none]
-#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
+#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)]
 pub struct LmStudioAvailableModel {
     pub name: String,
     pub display_name: Option<String>,
@@ -151,14 +152,14 @@ pub struct LmStudioAvailableModel {
 }
 
 #[skip_serializing_none]
-#[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)]
+#[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema, MergeFrom)]
 pub struct DeepseekSettingsContent {
     pub api_url: Option<String>,
     pub available_models: Option<Vec<DeepseekAvailableModel>>,
 }
 
 #[skip_serializing_none]
-#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
+#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)]
 pub struct DeepseekAvailableModel {
     pub name: String,
     pub display_name: Option<String>,
@@ -167,14 +168,14 @@ pub struct DeepseekAvailableModel {
 }
 
 #[skip_serializing_none]
-#[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)]
+#[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema, MergeFrom)]
 pub struct MistralSettingsContent {
     pub api_url: Option<String>,
     pub available_models: Option<Vec<MistralAvailableModel>>,
 }
 
 #[skip_serializing_none]
-#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
+#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)]
 pub struct MistralAvailableModel {
     pub name: String,
     pub display_name: Option<String>,
@@ -187,14 +188,14 @@ pub struct MistralAvailableModel {
 }
 
 #[skip_serializing_none]
-#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)]
+#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema, MergeFrom)]
 pub struct OpenAiSettingsContent {
     pub api_url: Option<String>,
     pub available_models: Option<Vec<OpenAiAvailableModel>>,
 }
 
 #[skip_serializing_none]
-#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
+#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)]
 pub struct OpenAiAvailableModel {
     pub name: String,
     pub display_name: Option<String>,
@@ -204,7 +205,7 @@ pub struct OpenAiAvailableModel {
     pub reasoning_effort: Option<OpenAiReasoningEffort>,
 }
 
-#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, JsonSchema)]
+#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, JsonSchema, MergeFrom)]
 #[serde(rename_all = "lowercase")]
 pub enum OpenAiReasoningEffort {
     Minimal,
@@ -214,14 +215,14 @@ pub enum OpenAiReasoningEffort {
 }
 
 #[skip_serializing_none]
-#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)]
+#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema, MergeFrom)]
 pub struct OpenAiCompatibleSettingsContent {
     pub api_url: String,
     pub available_models: Vec<OpenAiCompatibleAvailableModel>,
 }
 
 #[skip_serializing_none]
-#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
+#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)]
 pub struct OpenAiCompatibleAvailableModel {
     pub name: String,
     pub display_name: Option<String>,
@@ -233,7 +234,7 @@ pub struct OpenAiCompatibleAvailableModel {
 }
 
 #[skip_serializing_none]
-#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
+#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)]
 pub struct OpenAiCompatibleModelCapabilities {
     pub tools: bool,
     pub images: bool,
@@ -253,14 +254,14 @@ impl Default for OpenAiCompatibleModelCapabilities {
 }
 
 #[skip_serializing_none]
-#[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)]
+#[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema, MergeFrom)]
 pub struct VercelSettingsContent {
     pub api_url: Option<String>,
     pub available_models: Option<Vec<VercelAvailableModel>>,
 }
 
 #[skip_serializing_none]
-#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
+#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)]
 pub struct VercelAvailableModel {
     pub name: String,
     pub display_name: Option<String>,
@@ -270,14 +271,14 @@ pub struct VercelAvailableModel {
 }
 
 #[skip_serializing_none]
-#[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)]
+#[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema, MergeFrom)]
 pub struct GoogleSettingsContent {
     pub api_url: Option<String>,
     pub available_models: Option<Vec<GoogleAvailableModel>>,
 }
 
 #[skip_serializing_none]
-#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
+#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)]
 pub struct GoogleAvailableModel {
     pub name: String,
     pub display_name: Option<String>,
@@ -286,14 +287,14 @@ pub struct GoogleAvailableModel {
 }
 
 #[skip_serializing_none]
-#[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)]
+#[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema, MergeFrom)]
 pub struct XAiSettingsContent {
     pub api_url: Option<String>,
     pub available_models: Option<Vec<XaiAvailableModel>>,
 }
 
 #[skip_serializing_none]
-#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
+#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)]
 pub struct XaiAvailableModel {
     pub name: String,
     pub display_name: Option<String>,
@@ -303,13 +304,13 @@ pub struct XaiAvailableModel {
 }
 
 #[skip_serializing_none]
-#[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)]
+#[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema, MergeFrom)]
 pub struct ZedDotDevSettingsContent {
     pub available_models: Option<Vec<ZedDotDevAvailableModel>>,
 }
 
 #[skip_serializing_none]
-#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
+#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)]
 pub struct ZedDotDevAvailableModel {
     /// The provider of the language model.
     pub provider: ZedDotDevAvailableProvider,
@@ -336,7 +337,7 @@ pub struct ZedDotDevAvailableModel {
     pub mode: Option<ModelMode>,
 }
 
-#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
+#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)]
 #[serde(rename_all = "lowercase")]
 pub enum ZedDotDevAvailableProvider {
     Anthropic,
@@ -345,14 +346,14 @@ pub enum ZedDotDevAvailableProvider {
 }
 
 #[skip_serializing_none]
-#[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)]
+#[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema, MergeFrom)]
 pub struct OpenRouterSettingsContent {
     pub api_url: Option<String>,
     pub available_models: Option<Vec<OpenRouterAvailableModel>>,
 }
 
 #[skip_serializing_none]
-#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
+#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)]
 pub struct OpenRouterAvailableModel {
     pub name: String,
     pub display_name: Option<String>,
@@ -366,7 +367,7 @@ pub struct OpenRouterAvailableModel {
 }
 
 #[skip_serializing_none]
-#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
+#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)]
 pub struct OpenRouterProvider {
     order: Option<Vec<String>>,
     #[serde(default = "default_true")]
@@ -381,7 +382,7 @@ pub struct OpenRouterProvider {
     sort: Option<String>,
 }
 
-#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
+#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)]
 #[serde(rename_all = "lowercase")]
 pub enum DataCollection {
     Allow,
@@ -400,14 +401,16 @@ fn default_true() -> bool {
 
 /// Configuration for caching language model messages.
 #[skip_serializing_none]
-#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
+#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)]
 pub struct LanguageModelCacheConfiguration {
     pub max_cache_anchors: usize,
     pub should_speculate: bool,
     pub min_total_token: u64,
 }
 
-#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
+#[derive(
+    Copy, Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema, MergeFrom,
+)]
 #[serde(tag = "type", rename_all = "lowercase")]
 pub enum ModelMode {
     #[default]

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

@@ -4,12 +4,13 @@ use collections::{BTreeMap, HashMap};
 use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
 use serde_with::skip_serializing_none;
+use settings_macros::MergeFrom;
 use util::serde::default_true;
 
-use crate::{AllLanguageSettingsContent, SlashCommandSettings};
+use crate::{AllLanguageSettingsContent, ExtendingVec, SlashCommandSettings};
 
 #[skip_serializing_none]
-#[derive(Debug, PartialEq, Clone, Default, Serialize, Deserialize, JsonSchema)]
+#[derive(Debug, PartialEq, Clone, Default, Serialize, Deserialize, JsonSchema, MergeFrom)]
 pub struct ProjectSettingsContent {
     #[serde(flatten)]
     pub all_languages: AllLanguageSettingsContent,
@@ -43,11 +44,11 @@ pub struct ProjectSettingsContent {
     pub slash_commands: Option<SlashCommandSettings>,
 
     /// The list of custom Git hosting providers.
-    pub git_hosting_providers: Option<Vec<GitHostingProviderConfig>>,
+    pub git_hosting_providers: Option<ExtendingVec<GitHostingProviderConfig>>,
 }
 
 #[skip_serializing_none]
-#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
+#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)]
 pub struct WorktreeSettingsContent {
     /// The displayed name of this project. If not set, the root directory name
     /// will be displayed.
@@ -81,11 +82,11 @@ pub struct WorktreeSettingsContent {
 
     /// Treat the files matching these globs as `.env` files.
     /// Default: ["**/.env*", "**/*.pem", "**/*.key", "**/*.cert", "**/*.crt", "**/secrets.yml"]
-    pub private_files: Option<Vec<String>>,
+    pub private_files: Option<ExtendingVec<String>>,
 }
 
 #[skip_serializing_none]
-#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, Hash)]
+#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, MergeFrom, Hash)]
 #[serde(rename_all = "snake_case")]
 pub struct LspSettings {
     pub binary: Option<BinarySettings>,
@@ -112,7 +113,9 @@ impl Default for LspSettings {
 }
 
 #[skip_serializing_none]
-#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema, Hash)]
+#[derive(
+    Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema, MergeFrom, Hash,
+)]
 pub struct BinarySettings {
     pub path: Option<String>,
     pub arguments: Option<Vec<String>>,
@@ -121,7 +124,9 @@ pub struct BinarySettings {
 }
 
 #[skip_serializing_none]
-#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema, Hash)]
+#[derive(
+    Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema, MergeFrom, Hash,
+)]
 pub struct FetchSettings {
     // Whether to consider pre-releases for fetching
     pub pre_release: Option<bool>,
@@ -129,7 +134,7 @@ pub struct FetchSettings {
 
 /// Common language server settings.
 #[skip_serializing_none]
-#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, MergeFrom)]
 pub struct GlobalLspSettingsContent {
     /// Whether to show the LSP servers button in the status bar.
     ///
@@ -138,7 +143,7 @@ pub struct GlobalLspSettingsContent {
 }
 
 #[skip_serializing_none]
-#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
+#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema, MergeFrom)]
 #[serde(rename_all = "snake_case")]
 pub struct DapSettingsContent {
     pub binary: Option<String>,
@@ -147,7 +152,9 @@ pub struct DapSettingsContent {
 }
 
 #[skip_serializing_none]
-#[derive(Default, Copy, Clone, PartialEq, Eq, Debug, Serialize, Deserialize, JsonSchema)]
+#[derive(
+    Default, Copy, Clone, PartialEq, Eq, Debug, Serialize, Deserialize, JsonSchema, MergeFrom,
+)]
 pub struct SessionSettingsContent {
     /// Whether or not to restore unsaved buffers on restart.
     ///
@@ -158,7 +165,7 @@ pub struct SessionSettingsContent {
     pub restore_unsaved_buffers: Option<bool>,
 }
 
-#[derive(Deserialize, Serialize, Clone, PartialEq, Eq, JsonSchema, Debug)]
+#[derive(Deserialize, Serialize, Clone, PartialEq, Eq, JsonSchema, MergeFrom, Debug)]
 #[serde(tag = "source", rename_all = "snake_case")]
 pub enum ContextServerSettingsContent {
     Custom {
@@ -198,7 +205,7 @@ impl ContextServerSettingsContent {
 }
 
 #[skip_serializing_none]
-#[derive(Deserialize, Serialize, Clone, PartialEq, Eq, JsonSchema)]
+#[derive(Deserialize, Serialize, Clone, PartialEq, Eq, JsonSchema, MergeFrom)]
 pub struct ContextServerCommand {
     #[serde(rename = "command")]
     pub path: PathBuf,
@@ -234,7 +241,7 @@ impl std::fmt::Debug for ContextServerCommand {
 }
 
 #[skip_serializing_none]
-#[derive(Copy, Clone, Debug, PartialEq, Default, Serialize, Deserialize, JsonSchema)]
+#[derive(Copy, Clone, Debug, PartialEq, Default, Serialize, Deserialize, JsonSchema, MergeFrom)]
 pub struct GitSettings {
     /// Whether or not to show the git gutter.
     ///
@@ -259,7 +266,7 @@ pub struct GitSettings {
     pub hunk_style: Option<GitHunkStyleSetting>,
 }
 
-#[derive(Clone, Copy, Debug, PartialEq, Default, Serialize, Deserialize, JsonSchema)]
+#[derive(Clone, Copy, Debug, PartialEq, Default, Serialize, Deserialize, JsonSchema, MergeFrom)]
 #[serde(rename_all = "snake_case")]
 pub enum GitGutterSetting {
     /// Show git gutter in tracked files.
@@ -270,7 +277,7 @@ pub enum GitGutterSetting {
 }
 
 #[skip_serializing_none]
-#[derive(Clone, Copy, Debug, PartialEq, Default, Serialize, Deserialize, JsonSchema)]
+#[derive(Clone, Copy, Debug, PartialEq, Default, Serialize, Deserialize, JsonSchema, MergeFrom)]
 #[serde(rename_all = "snake_case")]
 pub struct InlineBlameSettings {
     /// Whether or not to show git blame data inline in
@@ -299,7 +306,7 @@ pub struct InlineBlameSettings {
 }
 
 #[skip_serializing_none]
-#[derive(Clone, Copy, PartialEq, Debug, Serialize, Deserialize, JsonSchema)]
+#[derive(Clone, Copy, PartialEq, Debug, Serialize, Deserialize, JsonSchema, MergeFrom)]
 #[serde(rename_all = "snake_case")]
 pub struct BranchPickerSettingsContent {
     /// Whether to show author name as part of the commit information.
@@ -308,7 +315,7 @@ pub struct BranchPickerSettingsContent {
     pub show_author_name: Option<bool>,
 }
 
-#[derive(Clone, Copy, PartialEq, Debug, Default, Serialize, Deserialize, JsonSchema)]
+#[derive(Clone, Copy, PartialEq, Debug, Default, Serialize, Deserialize, JsonSchema, MergeFrom)]
 #[serde(rename_all = "snake_case")]
 pub enum GitHunkStyleSetting {
     /// Show unstaged hunks with a filled background and staged hunks hollow.
@@ -319,7 +326,7 @@ pub enum GitHunkStyleSetting {
 }
 
 #[skip_serializing_none]
-#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
+#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema, MergeFrom)]
 pub struct DiagnosticsSettingsContent {
     /// Whether to show the project diagnostics button in the status bar.
     pub button: Option<bool>,
@@ -335,7 +342,7 @@ pub struct DiagnosticsSettingsContent {
 }
 
 #[skip_serializing_none]
-#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
+#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq, Eq)]
 pub struct LspPullDiagnosticsSettingsContent {
     /// Whether to pull for diagnostics or not.
     ///
@@ -349,7 +356,7 @@ pub struct LspPullDiagnosticsSettingsContent {
 }
 
 #[skip_serializing_none]
-#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize, JsonSchema, Eq)]
+#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom, Eq)]
 pub struct InlineDiagnosticsSettingsContent {
     /// Whether or not to show inline diagnostics
     ///
@@ -376,7 +383,7 @@ pub struct InlineDiagnosticsSettingsContent {
 }
 
 #[skip_serializing_none]
-#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
+#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema, MergeFrom)]
 pub struct NodeBinarySettings {
     /// The path to the Node binary.
     pub path: Option<String>,
@@ -386,7 +393,7 @@ pub struct NodeBinarySettings {
     pub ignore_system_version: Option<bool>,
 }
 
-#[derive(Clone, PartialEq, Debug, Default, Serialize, Deserialize, JsonSchema)]
+#[derive(Clone, PartialEq, Debug, Default, Serialize, Deserialize, JsonSchema, MergeFrom)]
 #[serde(rename_all = "snake_case")]
 pub enum DirenvSettings {
     /// Load direnv configuration through a shell hook
@@ -397,7 +404,17 @@ pub enum DirenvSettings {
 }
 
 #[derive(
-    Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize, JsonSchema,
+    Clone,
+    Copy,
+    Debug,
+    Eq,
+    PartialEq,
+    Ord,
+    PartialOrd,
+    Serialize,
+    Deserialize,
+    JsonSchema,
+    MergeFrom,
 )]
 #[serde(rename_all = "snake_case")]
 pub enum DiagnosticSeverityContent {
@@ -412,7 +429,7 @@ pub enum DiagnosticSeverityContent {
 
 /// A custom Git hosting provider.
 #[skip_serializing_none]
-#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, JsonSchema)]
+#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, JsonSchema, MergeFrom)]
 pub struct GitHostingProviderConfig {
     /// The type of the provider.
     ///
@@ -426,7 +443,7 @@ pub struct GitHostingProviderConfig {
     pub name: String,
 }
 
-#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)]
 #[serde(rename_all = "snake_case")]
 pub enum GitHostingProviderKind {
     Github,

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

@@ -5,11 +5,12 @@ use gpui::{AbsoluteLength, FontFeatures, SharedString, px};
 use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
 use serde_with::skip_serializing_none;
+use settings_macros::MergeFrom;
 
 use crate::FontFamilyName;
 
 #[skip_serializing_none]
-#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize, JsonSchema)]
+#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize, JsonSchema, MergeFrom)]
 pub struct TerminalSettingsContent {
     /// What shell to use when opening a terminal.
     ///
@@ -127,7 +128,7 @@ pub struct TerminalSettingsContent {
 }
 
 /// Shell configuration to open the terminal with.
-#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
+#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema, MergeFrom)]
 #[serde(rename_all = "snake_case")]
 pub enum Shell {
     /// Use the system's default terminal configuration in /etc/passwd
@@ -146,7 +147,7 @@ pub enum Shell {
     },
 }
 
-#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
+#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, MergeFrom)]
 #[serde(rename_all = "snake_case")]
 pub enum WorkingDirectory {
     /// Use the current file's project directory.  Will Fallback to the
@@ -163,15 +164,15 @@ pub enum WorkingDirectory {
 }
 
 #[skip_serializing_none]
-#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
+#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq, Eq)]
 pub struct ScrollbarSettingsContent {
     /// When to show the scrollbar in the terminal.
     ///
     /// Default: inherits editor scrollbar settings
-    pub show: Option<Option<ShowScrollbar>>,
+    pub show: Option<ShowScrollbar>,
 }
 
-#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema, Default)]
+#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema, MergeFrom, Default)]
 #[serde(rename_all = "snake_case")]
 pub enum TerminalLineHeight {
     /// Use a line height that's comfortable for reading, 1.618
@@ -198,7 +199,7 @@ impl TerminalLineHeight {
 /// When to show the scrollbar.
 ///
 /// Default: auto
-#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
+#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq, Eq)]
 #[serde(rename_all = "snake_case")]
 pub enum ShowScrollbar {
     /// Show the scrollbar if there's important information or
@@ -212,7 +213,9 @@ pub enum ShowScrollbar {
     Never,
 }
 
-#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
+#[derive(
+    Clone, Copy, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema, MergeFrom,
+)]
 #[serde(rename_all = "snake_case")]
 pub enum CursorShapeContent {
     /// Cursor is a block like `█`.
@@ -226,7 +229,7 @@ pub enum CursorShapeContent {
     Hollow,
 }
 
-#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
+#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, MergeFrom)]
 #[serde(rename_all = "snake_case")]
 pub enum TerminalBlink {
     /// Never blink the cursor, ignoring the terminal mode.
@@ -238,7 +241,7 @@ pub enum TerminalBlink {
     On,
 }
 
-#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
+#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, MergeFrom)]
 #[serde(rename_all = "snake_case")]
 pub enum AlternateScroll {
     On,
@@ -247,7 +250,7 @@ pub enum AlternateScroll {
 
 // Toolbar related settings
 #[skip_serializing_none]
-#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
+#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq, Eq)]
 pub struct TerminalToolbarContent {
     /// Whether to display the terminal title in breadcrumbs inside the terminal pane.
     /// Only shown if the terminal title is not empty.
@@ -259,7 +262,7 @@ pub struct TerminalToolbarContent {
     pub breadcrumbs: Option<bool>,
 }
 
-#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
+#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)]
 #[serde(rename_all = "snake_case")]
 pub enum VenvSettings {
     #[default]
@@ -297,7 +300,7 @@ impl VenvSettings {
     }
 }
 
-#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
+#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq, Eq)]
 #[serde(rename_all = "snake_case")]
 pub enum TerminalDockPosition {
     Left,
@@ -305,7 +308,7 @@ pub enum TerminalDockPosition {
     Right,
 }
 
-#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
+#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)]
 #[serde(rename_all = "snake_case")]
 pub enum ActivateScript {
     #[default]

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

@@ -4,6 +4,7 @@ use schemars::{JsonSchema, JsonSchema_repr};
 use serde::{Deserialize, Deserializer, Serialize};
 use serde_json::Value;
 use serde_repr::{Deserialize_repr, Serialize_repr};
+use settings_macros::MergeFrom;
 use std::sync::Arc;
 
 use serde_with::skip_serializing_none;
@@ -11,7 +12,7 @@ use serde_with::skip_serializing_none;
 /// Settings for rendering text in UI and text buffers.
 
 #[skip_serializing_none]
-#[derive(Clone, PartialEq, Debug, Default, Serialize, Deserialize, JsonSchema)]
+#[derive(Clone, PartialEq, Debug, Default, Serialize, Deserialize, JsonSchema, MergeFrom)]
 pub struct ThemeSettingsContent {
     /// The default font size for text in the UI.
     #[serde(default)]
@@ -53,7 +54,7 @@ pub struct ThemeSettingsContent {
     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>>,
+    pub agent_font_size: Option<f32>,
     /// The name of the Zed theme to use.
     #[serde(default)]
     pub theme: Option<ThemeSelection>,
@@ -93,7 +94,7 @@ fn default_font_fallbacks() -> Option<FontFallbacks> {
 }
 
 /// Represents the selection of a theme, which can be either static or dynamic.
-#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
+#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq, Eq)]
 #[serde(untagged)]
 pub enum ThemeSelection {
     /// A static theme selection, represented by a single theme name.
@@ -111,7 +112,7 @@ pub enum ThemeSelection {
 }
 
 /// Represents the selection of an icon theme, which can be either static or dynamic.
-#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
+#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq, Eq)]
 #[serde(untagged)]
 pub enum IconThemeSelection {
     /// A static icon theme selection, represented by a single icon theme name.
@@ -134,7 +135,9 @@ pub enum IconThemeSelection {
 /// `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)]
+#[derive(
+    Debug, PartialEq, Eq, Clone, Copy, Default, Serialize, Deserialize, JsonSchema, MergeFrom,
+)]
 #[serde(rename_all = "snake_case")]
 pub enum ThemeMode {
     /// Use the specified `light` theme.
@@ -163,6 +166,7 @@ pub enum ThemeMode {
     Serialize,
     Deserialize,
     JsonSchema,
+    MergeFrom,
 )]
 #[serde(rename_all = "snake_case")]
 pub enum UiDensity {
@@ -190,15 +194,14 @@ impl UiDensity {
     }
 }
 
-/// Newtype for font family name. Its `ParameterizedJsonSchema` lists the font families known at
-/// runtime.
+/// Font family name.
 #[skip_serializing_none]
-#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
+#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq, Eq)]
 #[serde(transparent)]
 pub struct FontFamilyName(pub Arc<str>);
 
 /// The buffer's line height.
-#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, JsonSchema, Default)]
+#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, JsonSchema, MergeFrom, Default)]
 #[serde(rename_all = "snake_case")]
 pub enum BufferLineHeight {
     /// A less dense line height.
@@ -226,7 +229,7 @@ where
 
 /// The content of a serialized theme.
 #[skip_serializing_none]
-#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema, PartialEq)]
+#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq)]
 #[serde(default)]
 pub struct ThemeStyleContent {
     #[serde(default, rename = "background.appearance")]
@@ -249,31 +252,30 @@ pub struct ThemeStyleContent {
     pub syntax: IndexMap<String, HighlightStyleContent>,
 }
 
-#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
+#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq)]
 pub struct AccentContent(pub Option<String>);
 
-#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
+#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq)]
 pub struct PlayerColorContent {
     pub cursor: Option<String>,
     pub background: Option<String>,
     pub selection: Option<String>,
 }
 
-/// Newtype for a theme name. Its `ParameterizedJsonSchema` lists the theme names known at runtime.
+/// Theme name.
 #[skip_serializing_none]
-#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
+#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq, Eq)]
 #[serde(transparent)]
 pub struct ThemeName(pub Arc<str>);
 
-/// Newtype for a icon theme name. Its `ParameterizedJsonSchema` lists the icon theme names known at
-/// runtime.
+/// Icon Theme Name
 #[skip_serializing_none]
-#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
+#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq, Eq)]
 #[serde(transparent)]
 pub struct IconThemeName(pub Arc<str>);
 
 #[skip_serializing_none]
-#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema, PartialEq)]
+#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq)]
 #[serde(default)]
 pub struct ThemeColorsContent {
     /// Border color. Used for most borders, is usually a high contrast color.
@@ -778,7 +780,7 @@ pub struct ThemeColorsContent {
 }
 
 #[skip_serializing_none]
-#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema, PartialEq)]
+#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq)]
 #[serde(default)]
 pub struct HighlightStyleContent {
     pub color: Option<String>,
@@ -812,7 +814,7 @@ where
 }
 
 #[skip_serializing_none]
-#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema, PartialEq)]
+#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq)]
 #[serde(default)]
 pub struct StatusColorsContent {
     /// Indicates some kind of conflict, like a file changed on disk while it was open, or
@@ -958,7 +960,7 @@ pub struct StatusColorsContent {
 }
 
 /// The background appearance of the window.
-#[derive(Debug, PartialEq, Clone, Copy, Serialize, Deserialize, JsonSchema)]
+#[derive(Debug, PartialEq, Clone, Copy, Serialize, Deserialize, JsonSchema, MergeFrom)]
 #[serde(rename_all = "snake_case")]
 pub enum WindowBackgroundContent {
     Opaque,
@@ -976,7 +978,7 @@ impl Into<gpui::WindowBackgroundAppearance> for WindowBackgroundContent {
     }
 }
 
-#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, PartialEq)]
+#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq)]
 #[serde(rename_all = "snake_case")]
 pub enum FontStyleContent {
     Normal,
@@ -994,7 +996,9 @@ impl From<FontStyleContent> for FontStyle {
     }
 }
 
-#[derive(Debug, Clone, Copy, Serialize_repr, Deserialize_repr, JsonSchema_repr, PartialEq)]
+#[derive(
+    Debug, Clone, Copy, Serialize_repr, Deserialize_repr, JsonSchema_repr, PartialEq, MergeFrom,
+)]
 #[repr(u16)]
 pub enum FontWeightContent {
     Thin = 100,

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

@@ -4,11 +4,12 @@ use collections::HashMap;
 use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
 use serde_with::skip_serializing_none;
+use settings_macros::MergeFrom;
 
 use crate::{DockPosition, DockSide, ScrollbarSettingsContent, ShowIndentGuides};
 
 #[skip_serializing_none]
-#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize, JsonSchema)]
+#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize, JsonSchema, MergeFrom)]
 pub struct WorkspaceSettingsContent {
     /// Active pane styling settings.
     pub active_pane_modifiers: Option<ActivePanelModifiers>,
@@ -108,7 +109,7 @@ pub struct WorkspaceSettingsContent {
 }
 
 #[skip_serializing_none]
-#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
+#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)]
 pub struct ItemSettingsContent {
     /// Whether to show the Git file status on a tab item.
     ///
@@ -138,7 +139,7 @@ pub struct ItemSettingsContent {
 }
 
 #[skip_serializing_none]
-#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
+#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)]
 pub struct PreviewTabsSettingsContent {
     /// Whether to show opened editors as preview tabs.
     /// Preview tabs do not stay open, are reused until explicitly set to be kept open opened (via double-click or editing) and show file names in italic.
@@ -155,7 +156,7 @@ pub struct PreviewTabsSettingsContent {
     pub enable_preview_from_code_navigation: Option<bool>,
 }
 
-#[derive(Copy, Clone, Debug, PartialEq, Default, Serialize, Deserialize, JsonSchema)]
+#[derive(Copy, Clone, Debug, PartialEq, Default, Serialize, Deserialize, JsonSchema, MergeFrom)]
 #[serde(rename_all = "lowercase")]
 pub enum ClosePosition {
     Left,
@@ -163,7 +164,7 @@ pub enum ClosePosition {
     Right,
 }
 
-#[derive(Copy, Clone, Debug, PartialEq, Default, Serialize, Deserialize, JsonSchema)]
+#[derive(Copy, Clone, Debug, PartialEq, Default, Serialize, Deserialize, JsonSchema, MergeFrom)]
 #[serde(rename_all = "lowercase")]
 pub enum ShowCloseButton {
     Always,
@@ -172,7 +173,9 @@ pub enum ShowCloseButton {
     Hidden,
 }
 
-#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
+#[derive(
+    Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq, Eq,
+)]
 #[serde(rename_all = "snake_case")]
 pub enum ShowDiagnostics {
     #[default]
@@ -181,7 +184,7 @@ pub enum ShowDiagnostics {
     All,
 }
 
-#[derive(Copy, Clone, Debug, PartialEq, Default, Serialize, Deserialize, JsonSchema)]
+#[derive(Copy, Clone, Debug, PartialEq, Default, Serialize, Deserialize, JsonSchema, MergeFrom)]
 #[serde(rename_all = "snake_case")]
 pub enum ActivateOnClose {
     #[default]
@@ -191,7 +194,7 @@ pub enum ActivateOnClose {
 }
 
 #[skip_serializing_none]
-#[derive(Copy, Clone, PartialEq, Debug, Default, Serialize, Deserialize, JsonSchema)]
+#[derive(Copy, Clone, PartialEq, Debug, Default, Serialize, Deserialize, JsonSchema, MergeFrom)]
 #[serde(rename_all = "snake_case")]
 pub struct ActivePanelModifiers {
     /// Size of the border surrounding the active pane.
@@ -209,7 +212,7 @@ pub struct ActivePanelModifiers {
     pub inactive_opacity: Option<f32>,
 }
 
-#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, PartialEq, JsonSchema)]
+#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, PartialEq, JsonSchema, MergeFrom)]
 #[serde(rename_all = "snake_case")]
 pub enum BottomDockLayout {
     /// Contained between the left and right docks
@@ -223,7 +226,7 @@ pub enum BottomDockLayout {
     RightAligned,
 }
 
-#[derive(Copy, Clone, PartialEq, Default, Serialize, Deserialize, JsonSchema, Debug)]
+#[derive(Copy, Clone, PartialEq, Default, Serialize, Deserialize, JsonSchema, MergeFrom, Debug)]
 #[serde(rename_all = "snake_case")]
 pub enum CloseWindowWhenNoItems {
     /// Match platform conventions by default, so "on" on macOS and "off" everywhere else
@@ -245,7 +248,9 @@ impl CloseWindowWhenNoItems {
     }
 }
 
-#[derive(Copy, Clone, PartialEq, Eq, Default, Serialize, Deserialize, JsonSchema, Debug)]
+#[derive(
+    Copy, Clone, PartialEq, Eq, Default, Serialize, Deserialize, JsonSchema, MergeFrom, Debug,
+)]
 #[serde(rename_all = "snake_case")]
 pub enum RestoreOnStartupBehavior {
     /// Always start with an empty editor
@@ -258,7 +263,7 @@ pub enum RestoreOnStartupBehavior {
 }
 
 #[skip_serializing_none]
-#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, PartialEq)]
+#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, MergeFrom, Debug, PartialEq)]
 pub struct TabBarSettingsContent {
     /// Whether or not to show the tab bar in the editor.
     ///
@@ -274,7 +279,7 @@ pub struct TabBarSettingsContent {
     pub show_tab_bar_buttons: Option<bool>,
 }
 
-#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
+#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, MergeFrom)]
 #[serde(rename_all = "snake_case")]
 pub enum AutosaveSetting {
     /// Disable autosave.
@@ -298,14 +303,14 @@ impl AutosaveSetting {
     }
 }
 
-#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
+#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, MergeFrom)]
 #[serde(rename_all = "snake_case")]
 pub enum PaneSplitDirectionHorizontal {
     Up,
     Down,
 }
 
-#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
+#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, MergeFrom)]
 #[serde(rename_all = "snake_case")]
 pub enum PaneSplitDirectionVertical {
     Left,
@@ -313,7 +318,7 @@ pub enum PaneSplitDirectionVertical {
 }
 
 #[skip_serializing_none]
-#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
+#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq)]
 #[serde(rename_all = "snake_case")]
 pub struct CenteredLayoutSettings {
     /// The relative width of the left padding of the central pane from the
@@ -328,7 +333,7 @@ pub struct CenteredLayoutSettings {
     pub right_padding: Option<f32>,
 }
 
-#[derive(Copy, Clone, Default, Serialize, Deserialize, JsonSchema, PartialEq, Debug)]
+#[derive(Copy, Clone, Default, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq, Debug)]
 #[serde(rename_all = "snake_case")]
 pub enum OnLastWindowClosed {
     /// Match platform conventions by default, so don't quit on macOS, and quit on other platforms
@@ -348,7 +353,7 @@ impl OnLastWindowClosed {
 }
 
 #[skip_serializing_none]
-#[derive(Clone, PartialEq, Default, Serialize, Deserialize, JsonSchema, Debug)]
+#[derive(Clone, PartialEq, Default, Serialize, Deserialize, JsonSchema, MergeFrom, Debug)]
 pub struct ProjectPanelSettingsContent {
     /// Whether to show the project panel button in the status bar.
     ///
@@ -423,7 +428,9 @@ pub struct ProjectPanelSettingsContent {
     pub drag_and_drop: Option<bool>,
 }
 
-#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
+#[derive(
+    Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq, Eq,
+)]
 #[serde(rename_all = "snake_case")]
 pub enum ProjectPanelEntrySpacing {
     /// Comfortable spacing of entries.
@@ -434,7 +441,7 @@ pub enum ProjectPanelEntrySpacing {
 }
 
 #[skip_serializing_none]
-#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
+#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq, Eq)]
 pub struct ProjectPanelIndentGuidesSettings {
     pub show: Option<ShowIndentGuides>,
 }

crates/settings/src/settings_json.rs 🔗

@@ -1,5 +1,5 @@
 use anyhow::Result;
-use gpui::App;
+use gpui::SharedString;
 use serde::{Serialize, de::DeserializeOwned};
 use serde_json::Value;
 use std::{ops::Range, sync::LazyLock};
@@ -10,16 +10,10 @@ use util::RangeExt;
 pub struct SettingsJsonSchemaParams<'a> {
     pub language_names: &'a [String],
     pub font_names: &'a [String],
+    pub theme_names: &'a [SharedString],
+    pub icon_theme_names: &'a [SharedString],
 }
 
-/// Value registered which specifies JSON schemas that are generated at runtime.
-pub struct ParameterizedJsonSchema {
-    pub add_and_get_ref:
-        fn(&mut schemars::SchemaGenerator, &SettingsJsonSchemaParams, &App) -> schemars::Schema,
-}
-
-inventory::collect!(ParameterizedJsonSchema);
-
 pub fn update_value_in_json_text<'a>(
     text: &mut String,
     key_path: &mut Vec<&'a str>,

crates/settings/src/settings_store.rs 🔗

@@ -10,7 +10,7 @@ use futures::{
 use gpui::{App, AsyncApp, BorrowAppContext, Global, SharedString, Task, UpdateGlobal};
 
 use paths::{EDITORCONFIG_NAME, local_settings_file_relative_path, task_file_name};
-use schemars::JsonSchema;
+use schemars::{JsonSchema, json_schema};
 use serde_json::Value;
 use smallvec::SmallVec;
 use std::{
@@ -18,16 +18,23 @@ use std::{
     fmt::Debug,
     ops::Range,
     path::{Path, PathBuf},
+    rc::Rc,
     str::{self, FromStr},
     sync::Arc,
 };
-use util::{ResultExt as _, schemars::DefaultDenyUnknownFields};
+use util::{
+    ResultExt as _,
+    schemars::{DefaultDenyUnknownFields, replace_subschema},
+};
 
 pub type EditorconfigProperties = ec4rs::Properties;
 
 use crate::{
-    ActiveSettingsProfileName, ParameterizedJsonSchema, SettingsJsonSchemaParams, SettingsUiEntry,
-    VsCodeSettings, WorktreeId, parse_json_with_comments, replace_value_in_json_text,
+    ActiveSettingsProfileName, FontFamilyName, IconThemeName, LanguageSettingsContent,
+    LanguageToSettingsMap, SettingsJsonSchemaParams, SettingsUiEntry, ThemeName, VsCodeSettings,
+    WorktreeId,
+    merge_from::MergeFrom,
+    parse_json_with_comments, replace_value_in_json_text,
     settings_content::{
         ExtensionsSettingsContent, ProjectSettingsContent, ServerSettingsContent, SettingsContent,
         UserSettingsContent,
@@ -58,15 +65,10 @@ pub trait Settings: 'static + Send + Sync + Sized {
     const PRESERVED_KEYS: Option<&'static [&'static str]> = None;
 
     /// Read the value from default.json.
+    ///
     /// This function *should* panic if default values are missing,
     /// and you should add a default to default.json for documentation.
-    fn from_defaults(content: &SettingsContent, cx: &mut App) -> Self;
-
-    /// Update the value based on the content from the current file.
-    ///
-    /// This function *should not* panic if there are problems, as the
-    /// content of user-provided settings files may be incomplete or invalid.
-    fn refine(&mut self, content: &SettingsContent, cx: &mut App);
+    fn from_settings(content: &SettingsContent, cx: &mut App) -> Self;
 
     fn missing_default() -> anyhow::Error {
         anyhow::anyhow!("missing default for: {}", std::any::type_name::<Self>())
@@ -140,12 +142,15 @@ pub struct SettingsLocation<'a> {
 /// A set of strongly-typed setting values defined via multiple config files.
 pub struct SettingsStore {
     setting_values: HashMap<TypeId, Box<dyn AnySettingValue>>,
-    default_settings: Box<SettingsContent>,
+    default_settings: Rc<SettingsContent>,
     user_settings: Option<UserSettingsContent>,
     global_settings: Option<Box<SettingsContent>>,
 
     extension_settings: Option<Box<SettingsContent>>,
     server_settings: Option<Box<SettingsContent>>,
+
+    merged_settings: Rc<SettingsContent>,
+
     local_settings: BTreeMap<(WorktreeId, Arc<Path>), SettingsContent>,
     raw_editorconfig_settings: BTreeMap<(WorktreeId, Arc<Path>), (String, Option<Editorconfig>)>,
 
@@ -193,8 +198,7 @@ struct SettingValue<T> {
 trait AnySettingValue: 'static + Send + Sync {
     fn setting_type_name(&self) -> &'static str;
 
-    fn from_default(&self, s: &SettingsContent, cx: &mut App) -> Box<dyn Any>;
-    fn refine(&self, value: &mut dyn Any, s: &[&SettingsContent], cx: &mut App);
+    fn from_settings(&self, s: &SettingsContent, cx: &mut App) -> Box<dyn Any>;
 
     fn value_for_path(&self, path: Option<SettingsLocation>) -> &dyn Any;
     fn all_local_values(&self) -> Vec<(WorktreeId, Arc<Path>, &dyn Any)>;
@@ -210,14 +214,17 @@ trait AnySettingValue: 'static + Send + Sync {
 impl SettingsStore {
     pub fn new(cx: &App, default_settings: &str) -> Self {
         let (setting_file_updates_tx, mut setting_file_updates_rx) = mpsc::unbounded();
-        let default_settings = parse_json_with_comments(default_settings).unwrap();
+        let default_settings: Rc<SettingsContent> =
+            parse_json_with_comments(default_settings).unwrap();
         Self {
             setting_values: Default::default(),
-            default_settings,
+            default_settings: default_settings.clone(),
             global_settings: None,
             server_settings: None,
             user_settings: None,
             extension_settings: None,
+
+            merged_settings: default_settings,
             local_settings: BTreeMap::default(),
             raw_editorconfig_settings: BTreeMap::default(),
             setting_file_updates_tx,
@@ -257,38 +264,7 @@ impl SettingsStore {
             global_value: None,
             local_values: Vec::new(),
         }));
-
-        let mut refinements = Vec::default();
-
-        if let Some(extension_settings) = self.extension_settings.as_deref() {
-            refinements.push(extension_settings)
-        }
-
-        if let Some(global_settings) = self.global_settings.as_deref() {
-            refinements.push(global_settings)
-        }
-
-        if let Some(user_settings) = self.user_settings.as_ref() {
-            refinements.push(&user_settings.content);
-            if let Some(release_channel) = user_settings.for_release_channel() {
-                refinements.push(release_channel)
-            }
-            if let Some(os) = user_settings.for_os() {
-                refinements.push(os)
-            }
-            if let Some(profile) = user_settings.for_profile(cx) {
-                refinements.push(profile)
-            }
-        }
-
-        if let Some(server_settings) = self.server_settings.as_ref() {
-            refinements.push(server_settings)
-        }
-        let mut value = T::from_defaults(&self.default_settings, cx);
-        for refinement in refinements {
-            value.refine(refinement, cx)
-        }
-
+        let value = T::from_settings(&self.merged_settings, cx);
         setting_value.set_global_value(Box::new(value));
     }
 
@@ -831,19 +807,56 @@ impl SettingsStore {
             })
     }
 
-    pub fn json_schema(&self, schema_params: &SettingsJsonSchemaParams, cx: &App) -> Value {
+    pub fn json_schema(&self, params: &SettingsJsonSchemaParams) -> Value {
         let mut generator = schemars::generate::SchemaSettings::draft2019_09()
             .with_transform(DefaultDenyUnknownFields)
             .into_generator();
 
-        let schema = UserSettingsContent::json_schema(&mut generator);
+        UserSettingsContent::json_schema(&mut generator);
+
+        let language_settings_content_ref = generator
+            .subschema_for::<LanguageSettingsContent>()
+            .to_value();
+        replace_subschema::<LanguageToSettingsMap>(&mut generator, || {
+            json_schema!({
+                "type": "object",
+                "properties": params
+                    .language_names
+                    .iter()
+                    .map(|name| {
+                        (
+                            name.clone(),
+                            language_settings_content_ref.clone(),
+                        )
+                    })
+                    .collect::<serde_json::Map<_, _>>()
+            })
+        });
 
-        // add schemas which are determined at runtime
-        for parameterized_json_schema in inventory::iter::<ParameterizedJsonSchema>() {
-            (parameterized_json_schema.add_and_get_ref)(&mut generator, schema_params, cx);
-        }
+        replace_subschema::<FontFamilyName>(&mut generator, || {
+            json_schema!({
+                "type": "string",
+                "enum": params.font_names,
+            })
+        });
+
+        replace_subschema::<ThemeName>(&mut generator, || {
+            json_schema!({
+                "type": "string",
+                "enum": params.theme_names,
+            })
+        });
 
-        schema.to_value()
+        replace_subschema::<IconThemeName>(&mut generator, || {
+            json_schema!({
+                "type": "string",
+                "enum": params.icon_theme_names,
+            })
+        });
+
+        generator
+            .root_schema_for::<UserSettingsContent>()
+            .to_value()
     }
 
     fn recompute_values(
@@ -852,74 +865,62 @@ impl SettingsStore {
         cx: &mut App,
     ) -> std::result::Result<(), InvalidSettingsError> {
         // Reload the global and local values for every setting.
-        let mut project_settings_stack = Vec::<&SettingsContent>::new();
+        let mut project_settings_stack = Vec::<SettingsContent>::new();
         let mut paths_stack = Vec::<Option<(WorktreeId, &Path)>>::new();
 
-        let mut refinements = Vec::default();
-
-        if let Some(extension_settings) = self.extension_settings.as_deref() {
-            refinements.push(extension_settings)
-        }
-
-        if let Some(global_settings) = self.global_settings.as_deref() {
-            refinements.push(global_settings)
-        }
-
-        if let Some(user_settings) = self.user_settings.as_ref() {
-            refinements.push(&user_settings.content);
-            if let Some(release_channel) = user_settings.for_release_channel() {
-                refinements.push(release_channel)
+        if changed_local_path.is_none() {
+            let mut merged = self.default_settings.as_ref().clone();
+            merged.merge_from(self.extension_settings.as_deref());
+            merged.merge_from(self.global_settings.as_deref());
+            if let Some(user_settings) = self.user_settings.as_ref() {
+                merged.merge_from(Some(&user_settings.content));
+                merged.merge_from(user_settings.for_release_channel());
+                merged.merge_from(user_settings.for_os());
+                merged.merge_from(user_settings.for_profile(cx));
             }
-            if let Some(os) = user_settings.for_os() {
-                refinements.push(os)
-            }
-            if let Some(profile) = user_settings.for_profile(cx) {
-                refinements.push(profile)
-            }
-        }
-
-        if let Some(server_settings) = self.server_settings.as_ref() {
-            refinements.push(server_settings)
-        }
+            merged.merge_from(self.server_settings.as_deref());
+            self.merged_settings = Rc::new(merged);
 
-        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_default(&self.default_settings, cx);
-                setting_value.refine(value.as_mut(), &refinements, cx);
+            for setting_value in self.setting_values.values_mut() {
+                let value = setting_value.from_settings(&self.merged_settings, cx);
                 setting_value.set_global_value(value);
             }
+        }
 
-            // Reload the local values for the setting.
-            paths_stack.clear();
-            project_settings_stack.clear();
-            for ((root_id, directory_path), local_settings) in &self.local_settings {
-                // Build a stack of all of the local values for that setting.
-                while let Some(prev_entry) = paths_stack.last() {
-                    if let Some((prev_root_id, prev_path)) = prev_entry
-                        && (root_id != prev_root_id || !directory_path.starts_with(prev_path))
-                    {
-                        paths_stack.pop();
-                        project_settings_stack.pop();
-                        continue;
-                    }
-                    break;
+        for ((root_id, directory_path), local_settings) in &self.local_settings {
+            // Build a stack of all of the local values for that setting.
+            while let Some(prev_entry) = paths_stack.last() {
+                if let Some((prev_root_id, prev_path)) = prev_entry
+                    && (root_id != prev_root_id || !directory_path.starts_with(prev_path))
+                {
+                    paths_stack.pop();
+                    project_settings_stack.pop();
+                    continue;
                 }
+                break;
+            }
 
-                paths_stack.push(Some((*root_id, directory_path.as_ref())));
-                project_settings_stack.push(local_settings);
+            paths_stack.push(Some((*root_id, directory_path.as_ref())));
+            let mut merged_local_settings = if let Some(deepest) = project_settings_stack.last() {
+                (*deepest).clone()
+            } else {
+                self.merged_settings.as_ref().clone()
+            };
+            merged_local_settings.merge_from(Some(local_settings));
 
-                // If a local settings file changed, then avoid recomputing local
-                // settings for any path outside of that directory.
-                if changed_local_path.is_some_and(|(changed_root_id, changed_local_path)| {
-                    *root_id != changed_root_id || !directory_path.starts_with(changed_local_path)
-                }) {
-                    continue;
-                }
+            project_settings_stack.push(merged_local_settings);
+
+            // If a local settings file changed, then avoid recomputing local
+            // settings for any path outside of that directory.
+            if changed_local_path.is_some_and(|(changed_root_id, changed_local_path)| {
+                *root_id != changed_root_id || !directory_path.starts_with(changed_local_path)
+            }) {
+                continue;
+            }
 
-                let mut value = setting_value.from_default(&self.default_settings, cx);
-                setting_value.refine(value.as_mut(), &refinements, cx);
-                setting_value.refine(value.as_mut(), &project_settings_stack, cx);
+            for setting_value in self.setting_values.values_mut() {
+                let value =
+                    setting_value.from_settings(&project_settings_stack.last().unwrap(), cx);
                 setting_value.set_local_value(*root_id, directory_path.clone(), value);
             }
         }
@@ -1001,15 +1002,8 @@ impl Debug for SettingsStore {
 }
 
 impl<T: Settings> AnySettingValue for SettingValue<T> {
-    fn from_default(&self, s: &SettingsContent, cx: &mut App) -> Box<dyn Any> {
-        Box::new(T::from_defaults(s, cx)) as _
-    }
-
-    fn refine(&self, value: &mut dyn Any, refinements: &[&SettingsContent], cx: &mut App) {
-        let value = value.downcast_mut::<T>().unwrap();
-        for refinement in refinements {
-            value.refine(refinement, cx)
-        }
+    fn from_settings(&self, s: &SettingsContent, cx: &mut App) -> Box<dyn Any> {
+        Box::new(T::from_settings(s, cx)) as _
     }
 
     fn setting_type_name(&self) -> &'static str {
@@ -1072,7 +1066,6 @@ mod tests {
 
     use super::*;
     use unindent::Unindent;
-    use util::MergeFrom;
 
     #[derive(Debug, PartialEq)]
     struct AutoUpdateSetting {
@@ -1080,19 +1073,11 @@ mod tests {
     }
 
     impl Settings for AutoUpdateSetting {
-        fn from_defaults(content: &SettingsContent, _: &mut App) -> Self {
+        fn from_settings(content: &SettingsContent, _: &mut App) -> Self {
             AutoUpdateSetting {
                 auto_update: content.auto_update.unwrap(),
             }
         }
-
-        fn refine(&mut self, content: &SettingsContent, _: &mut App) {
-            if let Some(auto_update) = content.auto_update {
-                self.auto_update = auto_update;
-            }
-        }
-
-        fn import_from_vscode(_: &VsCodeSettings, _: &mut SettingsContent) {}
     }
 
     #[derive(Debug, PartialEq)]
@@ -1102,7 +1087,7 @@ mod tests {
     }
 
     impl Settings for TitleBarSettings {
-        fn from_defaults(content: &SettingsContent, _: &mut App) -> Self {
+        fn from_settings(content: &SettingsContent, _: &mut App) -> Self {
             let content = content.title_bar.clone().unwrap();
             TitleBarSettings {
                 show: content.show.unwrap(),
@@ -1110,13 +1095,6 @@ mod tests {
             }
         }
 
-        fn refine(&mut self, content: &SettingsContent, _: &mut App) {
-            let Some(content) = content.title_bar.as_ref() else {
-                return;
-            };
-            self.show.merge_from(&content.show)
-        }
-
         fn import_from_vscode(vscode: &VsCodeSettings, content: &mut SettingsContent) {
             let mut show = None;
 
@@ -1138,7 +1116,7 @@ mod tests {
     }
 
     impl Settings for DefaultLanguageSettings {
-        fn from_defaults(content: &SettingsContent, _: &mut App) -> Self {
+        fn from_settings(content: &SettingsContent, _: &mut App) -> Self {
             let content = &content.project.all_languages.defaults;
             DefaultLanguageSettings {
                 tab_size: content.tab_size.unwrap(),
@@ -1146,13 +1124,6 @@ mod tests {
             }
         }
 
-        fn refine(&mut self, content: &SettingsContent, _: &mut App) {
-            let content = &content.project.all_languages.defaults;
-            self.tab_size.merge_from(&content.tab_size);
-            self.preferred_line_length
-                .merge_from(&content.preferred_line_length);
-        }
-
         fn import_from_vscode(vscode: &VsCodeSettings, content: &mut SettingsContent) {
             let content = &mut content.project.all_languages.defaults;
 

crates/settings_ui_macros/Cargo.toml → crates/settings_macros/Cargo.toml 🔗

@@ -1,12 +1,12 @@
 [package]
-name = "settings_ui_macros"
+name = "settings_macros"
 version = "0.1.0"
 edition.workspace = true
 publish.workspace = true
 license = "GPL-3.0-or-later"
 
 [lib]
-path = "src/settings_ui_macros.rs"
+path = "src/settings_macros.rs"
 proc-macro = true
 
 [lints]
@@ -16,8 +16,6 @@ workspace = true
 default = []
 
 [dependencies]
-heck.workspace = true
-proc-macro2.workspace = true
 quote.workspace = true
 syn.workspace = true
 workspace-hack.workspace = true

crates/settings_macros/src/settings_macros.rs 🔗

@@ -0,0 +1,132 @@
+use proc_macro::TokenStream;
+use quote::quote;
+use syn::{Data, DeriveInput, Fields, Type, parse_macro_input};
+
+/// Derives the `MergeFrom` trait for a struct.
+///
+/// This macro automatically implements `MergeFrom` by calling `merge_from`
+/// on all fields in the struct. For `Option<T>` fields, it merges by taking
+/// the `other` value when `self` is `None`. For other types, it recursively
+/// calls `merge_from` on the field.
+///
+/// # Example
+///
+/// ```ignore
+/// #[derive(Clone, MergeFrom)]
+/// struct MySettings {
+///     field1: Option<String>,
+///     field2: SomeOtherSettings,
+/// }
+/// ```
+#[proc_macro_derive(MergeFrom)]
+pub fn derive_merge_from(input: TokenStream) -> TokenStream {
+    let input = parse_macro_input!(input as DeriveInput);
+
+    let name = &input.ident;
+    let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
+
+    let merge_body = match &input.data {
+        Data::Struct(data_struct) => match &data_struct.fields {
+            Fields::Named(fields) => {
+                let field_merges = fields.named.iter().map(|field| {
+                    let field_name = &field.ident;
+                    let field_type = &field.ty;
+
+                    if is_option_type(field_type) {
+                        // For Option<T> fields, merge by taking the other value if self is None
+                        quote! {
+                            if let Some(other_value) = other.#field_name.as_ref() {
+                                if self.#field_name.is_none() {
+                                    self.#field_name = Some(other_value.clone());
+                                } else if let Some(self_value) = self.#field_name.as_mut() {
+                                    self_value.merge_from(Some(other_value));
+                                }
+                            }
+                        }
+                    } else {
+                        // For non-Option fields, recursively call merge_from
+                        quote! {
+                            self.#field_name.merge_from(Some(&other.#field_name));
+                        }
+                    }
+                });
+
+                quote! {
+                    if let Some(other) = other {
+                        #(#field_merges)*
+                    }
+                }
+            }
+            Fields::Unnamed(fields) => {
+                let field_merges = fields.unnamed.iter().enumerate().map(|(i, field)| {
+                    let field_index = syn::Index::from(i);
+                    let field_type = &field.ty;
+
+                    if is_option_type(field_type) {
+                        // For Option<T> fields, merge by taking the other value if self is None
+                        quote! {
+                            if let Some(other_value) = other.#field_index.as_ref() {
+                                if self.#field_index.is_none() {
+                                    self.#field_index = Some(other_value.clone());
+                                } else if let Some(self_value) = self.#field_index.as_mut() {
+                                    self_value.merge_from(Some(other_value));
+                                }
+                            }
+                        }
+                    } else {
+                        // For non-Option fields, recursively call merge_from
+                        quote! {
+                            self.#field_index.merge_from(Some(&other.#field_index));
+                        }
+                    }
+                });
+
+                quote! {
+                    if let Some(other) = other {
+                        #(#field_merges)*
+                    }
+                }
+            }
+            Fields::Unit => {
+                quote! {
+                    // No fields to merge for unit structs
+                }
+            }
+        },
+        Data::Enum(_) => {
+            quote! {
+               if let Some(other) = other {
+                   *self = other.clone();
+               }
+            }
+        }
+        Data::Union(_) => {
+            panic!("MergeFrom cannot be derived for unions");
+        }
+    };
+
+    let expanded = quote! {
+        impl #impl_generics crate::merge_from::MergeFrom for #name #ty_generics #where_clause {
+            fn merge_from(&mut self, other: ::core::option::Option<&Self>) {
+                use crate::merge_from::MergeFrom as _;
+                #merge_body
+            }
+        }
+    };
+
+    TokenStream::from(expanded)
+}
+
+/// Check if a type is `Option<T>`
+fn is_option_type(ty: &Type) -> bool {
+    match ty {
+        Type::Path(type_path) => {
+            if let Some(segment) = type_path.path.segments.last() {
+                segment.ident == "Option"
+            } else {
+                false
+            }
+        }
+        _ => false,
+    }
+}

crates/settings_ui_macros/src/settings_ui_macros.rs 🔗

@@ -1,612 +0,0 @@
-use heck::{ToSnakeCase as _, ToTitleCase as _};
-use proc_macro2::TokenStream;
-use quote::{ToTokens, quote};
-use syn::{Data, DeriveInput, LitStr, Token, parse_macro_input};
-
-/// Derive macro for the `SettingsUi` marker trait.
-///
-/// This macro automatically implements the `SettingsUi` trait for the annotated type.
-/// The `SettingsUi` trait is a marker trait used to indicate that a type can be
-/// displayed in the settings UI.
-///
-/// # Example
-///
-/// ```
-/// use settings_ui_macros::SettingsUi;
-///
-/// #[derive(SettingsUi)]
-/// #[settings_ui(group = "Standard")]
-/// struct MySettings {
-///     enabled: bool,
-///     count: usize,
-/// }
-/// ```
-#[proc_macro_derive(SettingsUi, attributes(settings_ui))]
-pub fn derive_settings_ui(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
-    let input = parse_macro_input!(input as DeriveInput);
-    let name = &input.ident;
-
-    // Handle generic parameters if present
-    let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
-
-    let mut group_name = Option::<String>::None;
-    let mut path_name = Option::<String>::None;
-
-    for attr in &input.attrs {
-        if attr.path().is_ident("settings_ui") {
-            attr.parse_nested_meta(|meta| {
-                if meta.path.is_ident("group") {
-                    if group_name.is_some() {
-                        return Err(meta.error("Only one 'group' path can be specified"));
-                    }
-                    meta.input.parse::<Token![=]>()?;
-                    let lit: LitStr = meta.input.parse()?;
-                    group_name = Some(lit.value());
-                } else if meta.path.is_ident("path") {
-                    // todo(settings_ui) rely entirely on settings_key, remove path attribute
-                    if path_name.is_some() {
-                        return Err(meta.error("Only one 'path' can be specified, either with `path` in `settings_ui` or with `settings_key`"));
-                    }
-                    meta.input.parse::<Token![=]>()?;
-                    let lit: LitStr = meta.input.parse()?;
-                    path_name = Some(lit.value());
-                } else if meta.path.is_ident("render") {
-                    // Just consume the tokens even if we don't use them here
-                    meta.input.parse::<Token![=]>()?;
-                    let _lit: LitStr = meta.input.parse()?;
-                }
-                Ok(())
-            })
-            .unwrap_or_else(|e| panic!("in #[settings_ui] attribute: {}", e));
-        } else if let Some(settings_key) = parse_setting_key_attr(attr) {
-            // todo(settings_ui) either remove fallback key or handle it here
-            if path_name.is_some() && settings_key.key.is_some() {
-                panic!("Both 'path' and 'settings_key' are specified. Must specify only one");
-            }
-            path_name = settings_key.key;
-        }
-    }
-
-    let doc_str = parse_documentation_from_attrs(&input.attrs);
-
-    let ui_item_fn_body = generate_ui_item_body(group_name.as_ref(), &input);
-
-    // todo(settings_ui): make group name optional, repurpose group as tag indicating item is group, and have "title" tag for custom title
-    let title = group_name.unwrap_or(input.ident.to_string().to_title_case());
-
-    let ui_entry_fn_body = map_ui_item_to_entry(
-        path_name.as_deref(),
-        &title,
-        doc_str.as_deref(),
-        quote! { Self },
-    );
-
-    let expanded = quote! {
-        impl #impl_generics settings::SettingsUi for #name #ty_generics #where_clause {
-            fn settings_ui_item() -> settings::SettingsUiItem {
-                #ui_item_fn_body
-            }
-
-            fn settings_ui_entry() -> settings::SettingsUiEntry {
-                #ui_entry_fn_body
-            }
-        }
-    };
-
-    proc_macro::TokenStream::from(expanded)
-}
-
-fn extract_type_from_option(ty: TokenStream) -> TokenStream {
-    match option_inner_type(ty.clone()) {
-        Some(inner_type) => inner_type,
-        None => ty,
-    }
-}
-
-fn option_inner_type(ty: TokenStream) -> Option<TokenStream> {
-    let ty = syn::parse2::<syn::Type>(ty).ok()?;
-    let syn::Type::Path(path) = ty else {
-        return None;
-    };
-    let segment = path.path.segments.last()?;
-    if segment.ident != "Option" {
-        return None;
-    }
-    let syn::PathArguments::AngleBracketed(args) = &segment.arguments else {
-        return None;
-    };
-    let arg = args.args.first()?;
-    let syn::GenericArgument::Type(ty) = arg else {
-        return None;
-    };
-    return Some(ty.to_token_stream());
-}
-
-fn map_ui_item_to_entry(
-    path: Option<&str>,
-    title: &str,
-    doc_str: Option<&str>,
-    ty: TokenStream,
-) -> TokenStream {
-    // todo(settings_ui): does quote! just work with options?
-    let path = path.map_or_else(|| quote! {None}, |path| quote! {Some(#path)});
-    let doc_str = doc_str.map_or_else(|| quote! {None}, |doc_str| quote! {Some(#doc_str)});
-    let item = ui_item_from_type(ty);
-    quote! {
-        settings::SettingsUiEntry {
-            title: #title,
-            path: #path,
-            item: #item,
-            documentation: #doc_str,
-        }
-    }
-}
-
-fn ui_item_from_type(ty: TokenStream) -> TokenStream {
-    let ty = extract_type_from_option(ty);
-    return trait_method_call(ty, quote! {settings::SettingsUi}, quote! {settings_ui_item});
-}
-
-fn trait_method_call(
-    ty: TokenStream,
-    trait_name: TokenStream,
-    method_name: TokenStream,
-) -> TokenStream {
-    // doing the <ty as settings::SettingsUi> makes the error message better:
-    //  -> "#ty Doesn't implement settings::SettingsUi" instead of "no item "settings_ui_item" for #ty"
-    // and ensures safety against name conflicts
-    //
-    // todo(settings_ui): Turn `Vec<T>` into `Vec::<T>` here as well
-    quote! {
-        <#ty as #trait_name>::#method_name()
-    }
-}
-
-fn generate_ui_item_body(group_name: Option<&String>, input: &syn::DeriveInput) -> TokenStream {
-    match (group_name, &input.data) {
-        (_, Data::Union(_)) => unimplemented!("Derive SettingsUi for Unions"),
-        (None, Data::Struct(_)) => quote! {
-            settings::SettingsUiItem::None
-        },
-        (Some(_), Data::Struct(data_struct)) => {
-            let parent_serde_attrs = parse_serde_attributes(&input.attrs);
-            item_group_from_fields(&data_struct.fields, &parent_serde_attrs)
-        }
-        (None, Data::Enum(data_enum)) => {
-            let serde_attrs = parse_serde_attributes(&input.attrs);
-            let render_as = parse_render_as(&input.attrs);
-            let length = data_enum.variants.len();
-
-            let mut variants = Vec::with_capacity(length);
-            let mut labels = Vec::with_capacity(length);
-
-            for variant in &data_enum.variants {
-                // todo(settings_ui): Can #[serde(rename = )] be on enum variants?
-                let ident = variant.ident.clone().to_string();
-                let variant_name = serde_attrs.rename_all.apply(&ident);
-                let title = variant_name.to_title_case();
-
-                variants.push(variant_name);
-                labels.push(title);
-            }
-
-            let is_not_union = data_enum.variants.iter().all(|v| v.fields.is_empty());
-            if is_not_union {
-                return match render_as {
-                    RenderAs::ToggleGroup if length > 6 => {
-                        panic!("Can't set toggle group with more than six entries");
-                    }
-                    RenderAs::ToggleGroup => {
-                        quote! {
-                            settings::SettingsUiItem::Single(settings::SettingsUiItemSingle::ToggleGroup{ variants: &[#(#variants),*], labels: &[#(#labels),*] })
-                        }
-                    }
-                    RenderAs::Default => {
-                        quote! {
-                            settings::SettingsUiItem::Single(settings::SettingsUiItemSingle::DropDown{ variants: &[#(#variants),*], labels: &[#(#labels),*] })
-                        }
-                    }
-                };
-            }
-            // else: Union!
-            let enum_name = &input.ident;
-
-            let options = data_enum.variants.iter().map(|variant| {
-                if variant.fields.is_empty() {
-                    return quote! {None};
-                }
-                let name = &variant.ident;
-                let item = item_group_from_fields(&variant.fields, &serde_attrs);
-                // todo(settings_ui): documentation
-                return quote! {
-                    Some(settings::SettingsUiEntry {
-                        path: None,
-                        title: stringify!(#name),
-                        documentation: None,
-                        item: #item,
-                    })
-                };
-            });
-            let defaults = data_enum.variants.iter().map(|variant| {
-                let variant_name = &variant.ident;
-                if variant.fields.is_empty() {
-                    quote! {
-                        serde_json::to_value(#enum_name::#variant_name).expect("Failed to serialize default value for #enum_name::#variant_name")
-                    }
-                } else {
-                    let fields = variant.fields.iter().enumerate().map(|(index, field)| {
-                        let field_name = field.ident.as_ref().map_or_else(|| syn::Index::from(index).into_token_stream(), |ident| ident.to_token_stream());
-                        let field_type_is_option = option_inner_type(field.ty.to_token_stream()).is_some();
-                        let field_default = if field_type_is_option {
-                            quote! {
-                                None
-                            }
-                        } else {
-                            quote! {
-                                ::std::default::Default::default()
-                            }
-                        };
-
-                        quote!{
-                            #field_name: #field_default
-                        }
-                    });
-                    quote! {
-                        serde_json::to_value(#enum_name::#variant_name {
-                            #(#fields),*
-                        }).expect("Failed to serialize default value for #enum_name::#variant_name")
-                    }
-                }
-            });
-            // todo(settings_ui): Identify #[default] attr and use it for index, defaulting to 0
-            let default_variant_index: usize = 0;
-            let determine_option_fn = {
-                let match_arms = data_enum
-                    .variants
-                    .iter()
-                    .enumerate()
-                    .map(|(index, variant)| {
-                        let variant_name = &variant.ident;
-                        quote! {
-                            Ok(#variant_name {..}) => #index
-                        }
-                    });
-                quote! {
-                    |value: &serde_json::Value, _cx: &gpui::App| -> usize {
-                        use #enum_name::*;
-                        match serde_json::from_value::<#enum_name>(value.clone()) {
-                            #(#match_arms),*,
-                            Err(_) => #default_variant_index,
-                        }
-                    }
-                }
-            };
-            // todo(settings_ui) should probably always use toggle group for unions, dropdown makes less sense
-            return quote! {
-                settings::SettingsUiItem::Union(settings::SettingsUiItemUnion {
-                    defaults: Box::new([#(#defaults),*]),
-                    labels: &[#(#labels),*],
-                    options: Box::new([#(#options),*]),
-                    determine_option: #determine_option_fn,
-                })
-            };
-            // panic!("Unhandled");
-        }
-        // todo(settings_ui) discriminated unions
-        (_, Data::Enum(_)) => quote! {
-            settings::SettingsUiItem::None
-        },
-    }
-}
-
-fn item_group_from_fields(fields: &syn::Fields, parent_serde_attrs: &SerdeOptions) -> TokenStream {
-    let group_items = fields
-        .iter()
-        .filter(|field| {
-            !field.attrs.iter().any(|attr| {
-                let mut has_skip = false;
-                if attr.path().is_ident("settings_ui") {
-                    let _ = attr.parse_nested_meta(|meta| {
-                        if meta.path.is_ident("skip") {
-                            has_skip = true;
-                        }
-                        Ok(())
-                    });
-                }
-
-                has_skip
-            })
-        })
-        .map(|field| {
-            let field_serde_attrs = parse_serde_attributes(&field.attrs);
-            let name = field.ident.as_ref().map(ToString::to_string);
-            let title = name.as_ref().map_or_else(
-                || "todo(settings_ui): Titles for tuple fields".to_string(),
-                |name| name.to_title_case(),
-            );
-            let doc_str = parse_documentation_from_attrs(&field.attrs);
-
-            (
-                title,
-                doc_str,
-                name.filter(|_| !field_serde_attrs.flatten).map(|name| {
-                    parent_serde_attrs.apply_rename_to_field(&field_serde_attrs, &name)
-                }),
-                field.ty.to_token_stream(),
-            )
-        })
-        // todo(settings_ui): Re-format field name as nice title, and support setting different title with attr
-        .map(|(title, doc_str, path, ty)| {
-            map_ui_item_to_entry(path.as_deref(), &title, doc_str.as_deref(), ty)
-        });
-
-    quote! {
-        settings::SettingsUiItem::Group(settings::SettingsUiItemGroup{ items: vec![#(#group_items),*] })
-    }
-}
-
-struct SerdeOptions {
-    rename_all: SerdeRenameAll,
-    rename: Option<String>,
-    flatten: bool,
-    untagged: bool,
-    _alias: Option<String>, // todo(settings_ui)
-}
-
-#[derive(PartialEq)]
-enum SerdeRenameAll {
-    Lowercase,
-    SnakeCase,
-    None,
-}
-
-impl SerdeRenameAll {
-    fn apply(&self, name: &str) -> String {
-        match self {
-            SerdeRenameAll::Lowercase => name.to_lowercase(),
-            SerdeRenameAll::SnakeCase => name.to_snake_case(),
-            SerdeRenameAll::None => name.to_string(),
-        }
-    }
-}
-
-impl SerdeOptions {
-    fn apply_rename_to_field(&self, field_options: &Self, name: &str) -> String {
-        // field renames take precedence over struct rename all cases
-        if let Some(rename) = &field_options.rename {
-            return rename.clone();
-        }
-        return self.rename_all.apply(name);
-    }
-}
-
-enum RenderAs {
-    ToggleGroup,
-    Default,
-}
-
-fn parse_render_as(attrs: &[syn::Attribute]) -> RenderAs {
-    let mut render_as = RenderAs::Default;
-
-    for attr in attrs {
-        if !attr.path().is_ident("settings_ui") {
-            continue;
-        }
-
-        attr.parse_nested_meta(|meta| {
-            if meta.path.is_ident("render") {
-                meta.input.parse::<Token![=]>()?;
-                let lit = meta.input.parse::<LitStr>()?.value();
-
-                if lit == "toggle_group" {
-                    render_as = RenderAs::ToggleGroup;
-                } else {
-                    return Err(meta.error(format!("invalid `render` attribute: {}", lit)));
-                }
-            }
-            Ok(())
-        })
-        .unwrap();
-    }
-
-    render_as
-}
-
-fn parse_serde_attributes(attrs: &[syn::Attribute]) -> SerdeOptions {
-    let mut options = SerdeOptions {
-        rename_all: SerdeRenameAll::None,
-        rename: None,
-        flatten: false,
-        untagged: false,
-        _alias: None,
-    };
-
-    for attr in attrs {
-        if !attr.path().is_ident("serde") {
-            continue;
-        }
-        attr.parse_nested_meta(|meta| {
-            if meta.path.is_ident("rename_all") {
-                meta.input.parse::<Token![=]>()?;
-                let lit = meta.input.parse::<LitStr>()?.value();
-
-                if options.rename_all != SerdeRenameAll::None {
-                    return Err(meta.error("duplicate `rename_all` attribute"));
-                } else if lit == "lowercase" {
-                    options.rename_all = SerdeRenameAll::Lowercase;
-                } else if lit == "snake_case" {
-                    options.rename_all = SerdeRenameAll::SnakeCase;
-                } else {
-                    return Err(meta.error(format!("invalid `rename_all` attribute: {}", lit)));
-                }
-                // todo(settings_ui): Other options?
-            } else if meta.path.is_ident("flatten") {
-                options.flatten = true;
-            } else if meta.path.is_ident("rename") {
-                if options.rename.is_some() {
-                    return Err(meta.error("Can only have one rename attribute"));
-                }
-
-                meta.input.parse::<Token![=]>()?;
-                let lit = meta.input.parse::<LitStr>()?.value();
-                options.rename = Some(lit);
-            } else if meta.path.is_ident("untagged") {
-                options.untagged = true;
-            }
-            Ok(())
-        })
-        .unwrap();
-    }
-
-    return options;
-}
-
-fn parse_documentation_from_attrs(attrs: &[syn::Attribute]) -> Option<String> {
-    let mut doc_str = Option::<String>::None;
-    for attr in attrs {
-        if attr.path().is_ident("doc") {
-            // /// ...
-            // becomes
-            // #[doc = "..."]
-            use syn::{Expr::Lit, ExprLit, Lit::Str, Meta, MetaNameValue};
-            if let Meta::NameValue(MetaNameValue {
-                value:
-                    Lit(ExprLit {
-                        lit: Str(ref lit_str),
-                        ..
-                    }),
-                ..
-            }) = attr.meta
-            {
-                let doc = lit_str.value();
-                let doc_str = doc_str.get_or_insert_default();
-                doc_str.push_str(doc.trim());
-                doc_str.push('\n');
-            }
-        }
-    }
-    return doc_str;
-}
-
-struct SettingsKey {
-    key: Option<String>,
-    fallback_key: Option<String>,
-}
-
-fn parse_setting_key_attr(attr: &syn::Attribute) -> Option<SettingsKey> {
-    if !attr.path().is_ident("settings_key") {
-        return None;
-    }
-
-    let mut settings_key = SettingsKey {
-        key: None,
-        fallback_key: None,
-    };
-
-    let mut found_none = false;
-
-    attr.parse_nested_meta(|meta| {
-        if meta.path.is_ident("None") {
-            found_none = true;
-        } else if meta.path.is_ident("key") {
-            if settings_key.key.is_some() {
-                return Err(meta.error("Only one 'group' path can be specified"));
-            }
-            meta.input.parse::<Token![=]>()?;
-            let lit: LitStr = meta.input.parse()?;
-            settings_key.key = Some(lit.value());
-        } else if meta.path.is_ident("fallback_key") {
-            if found_none {
-                return Err(meta.error("Cannot specify 'fallback_key' and 'None'"));
-            }
-
-            if settings_key.fallback_key.is_some() {
-                return Err(meta.error("Only one 'fallback_key' can be specified"));
-            }
-
-            meta.input.parse::<Token![=]>()?;
-            let lit: LitStr = meta.input.parse()?;
-            settings_key.fallback_key = Some(lit.value());
-        }
-        Ok(())
-    })
-    .unwrap_or_else(|e| panic!("in #[settings_key] attribute: {}", e));
-
-    if found_none && settings_key.fallback_key.is_some() {
-        panic!("in #[settings_key] attribute: Cannot specify 'None' and 'fallback_key'");
-    }
-    if found_none && settings_key.key.is_some() {
-        panic!("in #[settings_key] attribute: Cannot specify 'None' and 'key'");
-    }
-    if !found_none && settings_key.key.is_none() {
-        panic!("in #[settings_key] attribute: 'key' must be specified");
-    }
-
-    return Some(settings_key);
-}
-
-#[proc_macro_derive(SettingsKey, attributes(settings_key))]
-pub fn derive_settings_key(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
-    let input = parse_macro_input!(input as DeriveInput);
-    let name = &input.ident;
-
-    // Handle generic parameters if present
-    let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
-
-    let mut settings_key = Option::<SettingsKey>::None;
-
-    for attr in &input.attrs {
-        let parsed_settings_key = parse_setting_key_attr(attr);
-        if parsed_settings_key.is_some() && settings_key.is_some() {
-            panic!("Duplicate #[settings_key] attribute");
-        }
-        settings_key = settings_key.or(parsed_settings_key);
-    }
-
-    let Some(SettingsKey { key, fallback_key }) = settings_key else {
-        panic!("Missing #[settings_key] attribute");
-    };
-
-    let key = key.map_or_else(|| quote! {None}, |key| quote! {Some(#key)});
-    let fallback_key = fallback_key.map_or_else(
-        || quote! {None},
-        |fallback_key| quote! {Some(#fallback_key)},
-    );
-
-    let expanded = quote! {
-        impl #impl_generics settings::SettingsKey for #name #ty_generics #where_clause {
-            const KEY: Option<&'static str> = #key;
-
-            const FALLBACK_KEY: Option<&'static str> = #fallback_key;
-        };
-    };
-
-    proc_macro::TokenStream::from(expanded)
-}
-
-#[cfg(test)]
-mod tests {
-    use syn::{Attribute, parse_quote};
-
-    use super::*;
-
-    #[test]
-    fn test_extract_key() {
-        let input: Attribute = parse_quote!(
-            #[settings_key(key = "my_key")]
-        );
-        let settings_key = parse_setting_key_attr(&input).unwrap();
-        assert_eq!(settings_key.key, Some("my_key".to_string()));
-        assert_eq!(settings_key.fallback_key, None);
-    }
-
-    #[test]
-    fn test_empty_key() {
-        let input: Attribute = parse_quote!(
-            #[settings_key(None)]
-        );
-        let settings_key = parse_setting_key_attr(&input).unwrap();
-        assert_eq!(settings_key.key, None);
-        assert_eq!(settings_key.fallback_key, None);
-    }
-}

crates/terminal/src/terminal_settings.rs 🔗

@@ -13,7 +13,6 @@ use settings::{
 };
 use task::Shell;
 use theme::FontFamilyName;
-use util::MergeFrom;
 
 #[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
 pub struct Toolbar {
@@ -73,7 +72,7 @@ fn settings_shell_to_task_shell(shell: settings::Shell) -> Shell {
 }
 
 impl settings::Settings for TerminalSettings {
-    fn from_defaults(content: &settings::SettingsContent, _cx: &mut App) -> Self {
+    fn from_settings(content: &settings::SettingsContent, _cx: &mut App) -> Self {
         let content = content.terminal.clone().unwrap();
         TerminalSettings {
             shell: settings_shell_to_task_shell(content.shell.unwrap()),
@@ -108,80 +107,12 @@ impl settings::Settings for TerminalSettings {
                 breadcrumbs: content.toolbar.unwrap().breadcrumbs.unwrap(),
             },
             scrollbar: ScrollbarSettings {
-                show: content.scrollbar.unwrap().show.flatten(),
+                show: content.scrollbar.unwrap().show,
             },
             minimum_contrast: content.minimum_contrast.unwrap(),
         }
     }
 
-    fn refine(&mut self, content: &settings::SettingsContent, _cx: &mut App) {
-        let Some(content) = &content.terminal else {
-            return;
-        };
-        self.shell
-            .merge_from(&content.shell.clone().map(settings_shell_to_task_shell));
-        self.working_directory
-            .merge_from(&content.working_directory);
-        if let Some(font_size) = content.font_size.map(px) {
-            self.font_size = Some(font_size)
-        }
-        if let Some(font_family) = content.font_family.clone() {
-            self.font_family = Some(font_family);
-        }
-        if let Some(fallbacks) = content.font_fallbacks.clone() {
-            self.font_fallbacks = Some(FontFallbacks::from_fonts(
-                fallbacks
-                    .into_iter()
-                    .map(|family| family.0.to_string())
-                    .collect(),
-            ))
-        }
-        if let Some(font_features) = content.font_features.clone() {
-            self.font_features = Some(font_features)
-        }
-        if let Some(font_weight) = content.font_weight {
-            self.font_weight = Some(FontWeight(font_weight));
-        }
-        self.line_height.merge_from(&content.line_height);
-        if let Some(env) = &content.env {
-            for (key, value) in env {
-                self.env.insert(key.clone(), value.clone());
-            }
-        }
-        if let Some(cursor_shape) = content.cursor_shape {
-            self.cursor_shape = Some(cursor_shape.into())
-        }
-        self.blinking.merge_from(&content.blinking);
-        self.alternate_scroll.merge_from(&content.alternate_scroll);
-        self.option_as_meta.merge_from(&content.option_as_meta);
-        self.copy_on_select.merge_from(&content.copy_on_select);
-        self.keep_selection_on_copy
-            .merge_from(&content.keep_selection_on_copy);
-        self.button.merge_from(&content.button);
-        self.dock.merge_from(&content.dock);
-        self.default_width
-            .merge_from(&content.default_width.map(px));
-        self.default_height
-            .merge_from(&content.default_height.map(px));
-        self.detect_venv.merge_from(&content.detect_venv);
-        if let Some(max_scroll_history_lines) = content.max_scroll_history_lines {
-            self.max_scroll_history_lines = Some(max_scroll_history_lines)
-        }
-        self.toolbar.breadcrumbs.merge_from(
-            &content
-                .toolbar
-                .as_ref()
-                .and_then(|toolbar| toolbar.breadcrumbs),
-        );
-        self.scrollbar.show.merge_from(
-            &content
-                .scrollbar
-                .as_ref()
-                .and_then(|scrollbar| scrollbar.show),
-        );
-        self.minimum_contrast.merge_from(&content.minimum_contrast);
-    }
-
     fn import_from_vscode(vscode: &settings::VsCodeSettings, content: &mut SettingsContent) {
         let mut default = TerminalSettingsContent::default();
         let current = content.terminal.as_mut().unwrap_or(&mut default);

crates/theme/Cargo.toml 🔗

@@ -24,7 +24,6 @@ fs.workspace = true
 futures.workspace = true
 gpui.workspace = true
 indexmap.workspace = true
-inventory.workspace = true
 log.workspace = true
 palette = { workspace = true, default-features = false, features = ["std"] }
 parking_lot.workspace = true

crates/theme/src/settings.rs 🔗

@@ -7,17 +7,16 @@ use crate::{
 use collections::HashMap;
 use derive_more::{Deref, DerefMut};
 use gpui::{
-    App, Context, Font, FontFallbacks, 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 schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
 pub use settings::{FontFamilyName, IconThemeName, ThemeMode, ThemeName};
-use settings::{ParameterizedJsonSchema, Settings, SettingsContent};
+use settings::{Settings, SettingsContent};
 use std::sync::Arc;
-use util::schemars::replace_subschema;
-use util::{MergeFrom, ResultExt as _};
+use util::ResultExt as _;
 
 const MIN_FONT_SIZE: Pixels = px(6.0);
 const MAX_FONT_SIZE: Pixels = px(100.0);
@@ -270,45 +269,6 @@ pub struct AgentFontSize(Pixels);
 
 impl Global for AgentFontSize {}
 
-inventory::submit! {
-    ParameterizedJsonSchema {
-        add_and_get_ref: |generator, _params, cx| {
-            replace_subschema::<settings::ThemeName>(generator, || json_schema!({
-                "type": "string",
-                "enum": ThemeRegistry::global(cx).list_names(),
-            }))
-        }
-    }
-}
-
-inventory::submit! {
-    ParameterizedJsonSchema {
-        add_and_get_ref: |generator, _params, cx| {
-            replace_subschema::<settings::IconThemeName>(generator, || json_schema!({
-                "type": "string",
-                "enum": ThemeRegistry::global(cx)
-                    .list_icon_themes()
-                    .into_iter()
-                    .map(|icon_theme| icon_theme.name)
-                    .collect::<Vec<SharedString>>(),
-            }))
-        }
-    }
-}
-
-inventory::submit! {
-    ParameterizedJsonSchema {
-        add_and_get_ref: |generator, params, _cx| {
-            replace_subschema::<settings::FontFamilyName>(generator, || {
-                json_schema!({
-                    "type": "string",
-                    "enum": params.font_names,
-                })
-            })
-        }
-    }
-}
-
 /// Represents the selection of a theme, which can be either static or dynamic.
 #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
 #[serde(untagged)]
@@ -807,20 +767,20 @@ pub fn font_fallbacks_from_settings(
 }
 
 impl settings::Settings for ThemeSettings {
-    fn from_defaults(content: &settings::SettingsContent, cx: &mut App) -> Self {
+    fn from_settings(content: &settings::SettingsContent, cx: &mut App) -> Self {
         let content = &content.theme;
         // todo(settings_refactor). This should *not* require cx...
         let themes = ThemeRegistry::default_global(cx);
         let system_appearance = SystemAppearance::default_global(cx);
         let theme_selection: ThemeSelection = content.theme.clone().unwrap().into();
         let icon_theme_selection: IconThemeSelection = content.icon_theme.clone().unwrap().into();
-        Self {
-            ui_font_size: content.ui_font_size.unwrap().into(),
+        let mut this = Self {
+            ui_font_size: clamp_font_size(content.ui_font_size.unwrap().into()),
             ui_font: Font {
                 family: content.ui_font_family.as_ref().unwrap().0.clone().into(),
                 features: content.ui_font_features.clone().unwrap(),
                 fallbacks: font_fallbacks_from_settings(content.ui_font_fallbacks.clone()),
-                weight: content.ui_font_weight.map(FontWeight).unwrap(),
+                weight: clamp_font_weight(content.ui_font_weight.unwrap()),
                 style: Default::default(),
             },
             buffer_font: Font {
@@ -833,12 +793,12 @@ impl settings::Settings for ThemeSettings {
                     .into(),
                 features: content.buffer_font_features.clone().unwrap(),
                 fallbacks: font_fallbacks_from_settings(content.buffer_font_fallbacks.clone()),
-                weight: content.buffer_font_weight.map(FontWeight).unwrap(),
+                weight: clamp_font_weight(content.buffer_font_weight.unwrap()),
                 style: FontStyle::default(),
             },
-            buffer_font_size: content.buffer_font_size.unwrap().into(),
+            buffer_font_size: clamp_font_size(content.buffer_font_size.unwrap().into()),
             buffer_line_height: content.buffer_line_height.unwrap().into(),
-            agent_font_size: content.agent_font_size.flatten().map(Into::into),
+            agent_font_size: content.agent_font_size.map(Into::into),
             active_theme: themes
                 .get(theme_selection.theme(*system_appearance))
                 .or(themes.get(&zed_default_dark().name))
@@ -852,110 +812,10 @@ impl settings::Settings for ThemeSettings {
                 .unwrap(),
             icon_theme_selection: Some(icon_theme_selection),
             ui_density: content.ui_density.unwrap_or_default().into(),
-            unnecessary_code_fade: content.unnecessary_code_fade.unwrap(),
-        }
-    }
-
-    fn refine(&mut self, content: &SettingsContent, cx: &mut App) {
-        let value = &content.theme;
-
-        let themes = ThemeRegistry::default_global(cx);
-        let system_appearance = SystemAppearance::default_global(cx);
-
-        self.ui_density
-            .merge_from(&value.ui_density.map(Into::into));
-
-        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);
-        }
-
-        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.theme {
-            self.theme_selection = Some(value.clone().into());
-
-            let theme_name = self
-                .theme_selection
-                .as_ref()
-                .unwrap()
-                .theme(*system_appearance);
-
-            match themes.get(theme_name) {
-                Ok(theme) => {
-                    self.active_theme = theme;
-                }
-                Err(err @ ThemeNotFoundError(_)) => {
-                    if themes.extensions_loaded() {
-                        log::error!("{err}");
-                    }
-                }
-            }
-        }
-
-        self.experimental_theme_overrides
-            .clone_from(&value.experimental_theme_overrides);
-        self.theme_overrides.clone_from(&value.theme_overrides);
-
-        self.apply_theme_overrides();
-
-        if let Some(value) = &value.icon_theme {
-            self.icon_theme_selection = Some(value.clone().into());
-
-            let icon_theme_name = self
-                .icon_theme_selection
-                .as_ref()
-                .unwrap()
-                .icon_theme(*system_appearance);
-
-            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}");
-                    }
-                }
-            }
-        }
-
-        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)),
-        );
-
-        self.buffer_line_height
-            .merge_from(&value.buffer_line_height.map(Into::into));
-
-        // 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);
+            unnecessary_code_fade: content.unnecessary_code_fade.unwrap().clamp(0.0, 0.9),
+        };
+        this.apply_theme_overrides();
+        this
     }
 
     fn import_from_vscode(vscode: &settings::VsCodeSettings, current: &mut SettingsContent) {

crates/title_bar/src/title_bar_settings.rs 🔗

@@ -1,7 +1,6 @@
 pub use settings::TitleBarVisibility;
 use settings::{Settings, SettingsContent};
 use ui::App;
-use util::MergeFrom;
 
 #[derive(Copy, Clone, Debug)]
 pub struct TitleBarSettings {
@@ -16,7 +15,7 @@ pub struct TitleBarSettings {
 }
 
 impl Settings for TitleBarSettings {
-    fn from_defaults(s: &SettingsContent, _: &mut App) -> Self {
+    fn from_settings(s: &SettingsContent, _: &mut App) -> Self {
         let content = s.title_bar.clone().unwrap();
         TitleBarSettings {
             show: content.show.unwrap(),
@@ -29,24 +28,4 @@ impl Settings for TitleBarSettings {
             show_menus: content.show_menus.unwrap(),
         }
     }
-
-    fn refine(&mut self, s: &SettingsContent, _: &mut App) {
-        let Some(content) = &s.title_bar else {
-            return;
-        };
-
-        self.show.merge_from(&content.show);
-        self.show_branch_icon.merge_from(&content.show_branch_icon);
-        self.show_onboarding_banner
-            .merge_from(&content.show_onboarding_banner);
-        self.show_user_picture
-            .merge_from(&content.show_user_picture);
-        self.show_branch_name.merge_from(&content.show_branch_name);
-        self.show_project_items
-            .merge_from(&content.show_project_items);
-        self.show_sign_in.merge_from(&content.show_sign_in);
-        self.show_menus.merge_from(&content.show_menus);
-    }
-
-    fn import_from_vscode(_: &settings::VsCodeSettings, _: &mut SettingsContent) {}
 }

crates/util/src/schemars.rs 🔗

@@ -18,9 +18,8 @@ pub fn replace_subschema<T: JsonSchema>(
     let schema_name = T::schema_name();
     let definitions = generator.definitions_mut();
     assert!(!definitions.contains_key(&format!("{schema_name}2")));
-    if definitions.contains_key(schema_name.as_ref()) {
-        definitions.insert(schema_name.to_string(), schema().to_value());
-    }
+    assert!(definitions.contains_key(schema_name.as_ref()));
+    definitions.insert(schema_name.to_string(), schema().to_value());
     schemars::Schema::new_ref(format!("{DEFS_PATH}{schema_name}"))
 }
 

crates/util/src/util.rs 🔗

@@ -1390,21 +1390,3 @@ Line 3"#
         assert_eq!(result[1], (10..15, "world")); // '🦀' is 4 bytes
     }
 }
-
-pub fn refine<T: Clone>(dest: &mut T, src: &Option<T>) {
-    if let Some(src) = src {
-        *dest = src.clone()
-    }
-}
-
-pub trait MergeFrom: Sized + Clone {
-    fn merge_from(&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();
-        }
-    }
-}

crates/vim/src/vim.rs 🔗

@@ -45,7 +45,6 @@ use std::{mem, ops::Range, sync::Arc};
 use surrounds::SurroundsType;
 use theme::ThemeSettings;
 use ui::{IntoElement, SharedString, px};
-use util::MergeFrom;
 use vim_mode_setting::HelixModeSetting;
 use vim_mode_setting::VimModeSetting;
 use workspace::{self, Pane, Workspace};
@@ -1845,7 +1844,7 @@ impl From<settings::ModeContent> for Mode {
 }
 
 impl Settings for VimSettings {
-    fn from_defaults(content: &settings::SettingsContent, _cx: &mut App) -> Self {
+    fn from_settings(content: &settings::SettingsContent, _cx: &mut App) -> Self {
         let vim = content.vim.clone().unwrap();
         Self {
             default_mode: vim.default_mode.unwrap().into(),
@@ -1857,29 +1856,4 @@ impl Settings for VimSettings {
             cursor_shape: vim.cursor_shape.unwrap().into(),
         }
     }
-
-    fn refine(&mut self, content: &settings::SettingsContent, _cx: &mut App) {
-        let Some(vim) = content.vim.as_ref() else {
-            return;
-        };
-        self.default_mode
-            .merge_from(&vim.default_mode.map(Into::into));
-        self.toggle_relative_line_numbers
-            .merge_from(&vim.toggle_relative_line_numbers);
-        self.use_system_clipboard
-            .merge_from(&vim.use_system_clipboard);
-        self.use_smartcase_find.merge_from(&vim.use_smartcase_find);
-        self.custom_digraphs.merge_from(&vim.custom_digraphs);
-        self.highlight_on_yank_duration
-            .merge_from(&vim.highlight_on_yank_duration);
-        self.cursor_shape
-            .merge_from(&vim.cursor_shape.map(Into::into));
-    }
-
-    fn import_from_vscode(
-        _vscode: &settings::VsCodeSettings,
-        _current: &mut settings::SettingsContent,
-    ) {
-        // TODO: translate vim extension settings
-    }
 }

crates/vim_mode_setting/src/vim_mode_setting.rs 🔗

@@ -16,16 +16,10 @@ pub fn init(cx: &mut App) {
 pub struct VimModeSetting(pub bool);
 
 impl Settings for VimModeSetting {
-    fn from_defaults(content: &SettingsContent, _cx: &mut App) -> Self {
+    fn from_settings(content: &SettingsContent, _cx: &mut App) -> Self {
         Self(content.vim_mode.unwrap())
     }
 
-    fn refine(&mut self, content: &SettingsContent, _cx: &mut App) {
-        if let Some(vim_mode) = content.vim_mode {
-            self.0 = vim_mode;
-        }
-    }
-
     fn import_from_vscode(_vscode: &settings::VsCodeSettings, _content: &mut SettingsContent) {
         // TODO: could possibly check if any of the `vim.<foo>` keys are set?
     }
@@ -34,15 +28,9 @@ impl Settings for VimModeSetting {
 pub struct HelixModeSetting(pub bool);
 
 impl Settings for HelixModeSetting {
-    fn from_defaults(content: &SettingsContent, _cx: &mut App) -> Self {
+    fn from_settings(content: &SettingsContent, _cx: &mut App) -> Self {
         Self(content.helix_mode.unwrap())
     }
 
-    fn refine(&mut self, content: &SettingsContent, _cx: &mut App) {
-        if let Some(helix_mode) = content.helix_mode {
-            self.0 = helix_mode;
-        }
-    }
-
     fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut SettingsContent) {}
 }

crates/workspace/src/item.rs 🔗

@@ -30,7 +30,7 @@ use std::{
 };
 use theme::Theme;
 use ui::{Color, Icon, IntoElement, Label, LabelCommon};
-use util::{MergeFrom as _, ResultExt};
+use util::ResultExt;
 
 pub const LEADER_UPDATE_THROTTLE: Duration = Duration::from_millis(200);
 
@@ -65,7 +65,7 @@ pub struct PreviewTabsSettings {
 }
 
 impl Settings for ItemSettings {
-    fn from_defaults(content: &settings::SettingsContent, _cx: &mut App) -> Self {
+    fn from_settings(content: &settings::SettingsContent, _cx: &mut App) -> Self {
         let tabs = content.tabs.as_ref().unwrap();
         Self {
             git_status: tabs.git_status.unwrap(),
@@ -77,18 +77,6 @@ impl Settings for ItemSettings {
         }
     }
 
-    fn refine(&mut self, content: &settings::SettingsContent, _cx: &mut App) {
-        let Some(tabs) = content.tabs.as_ref() else {
-            return;
-        };
-        self.git_status.merge_from(&tabs.git_status);
-        self.close_position.merge_from(&tabs.close_position);
-        self.activate_on_close.merge_from(&tabs.activate_on_close);
-        self.file_icons.merge_from(&tabs.file_icons);
-        self.show_diagnostics.merge_from(&tabs.show_diagnostics);
-        self.show_close_button.merge_from(&tabs.show_close_button);
-    }
-
     fn import_from_vscode(
         vscode: &settings::VsCodeSettings,
         current: &mut settings::SettingsContent,
@@ -125,7 +113,7 @@ impl Settings for ItemSettings {
 }
 
 impl Settings for PreviewTabsSettings {
-    fn from_defaults(content: &settings::SettingsContent, _cx: &mut App) -> Self {
+    fn from_settings(content: &settings::SettingsContent, _cx: &mut App) -> Self {
         let preview_tabs = content.preview_tabs.as_ref().unwrap();
         Self {
             enabled: preview_tabs.enabled.unwrap(),
@@ -136,18 +124,6 @@ impl Settings for PreviewTabsSettings {
         }
     }
 
-    fn refine(&mut self, content: &settings::SettingsContent, _cx: &mut App) {
-        let Some(preview_tabs) = content.preview_tabs.as_ref() else {
-            return;
-        };
-
-        self.enabled.merge_from(&preview_tabs.enabled);
-        self.enable_preview_from_file_finder
-            .merge_from(&preview_tabs.enable_preview_from_file_finder);
-        self.enable_preview_from_code_navigation
-            .merge_from(&preview_tabs.enable_preview_from_code_navigation);
-    }
-
     fn import_from_vscode(
         vscode: &settings::VsCodeSettings,
         current: &mut settings::SettingsContent,

crates/workspace/src/workspace_settings.rs 🔗

@@ -10,7 +10,6 @@ pub use settings::{
     BottomDockLayout, PaneSplitDirectionHorizontal, PaneSplitDirectionVertical,
     RestoreOnStartupBehavior,
 };
-use util::MergeFrom as _;
 
 pub struct WorkspaceSettings {
     pub active_pane_modifiers: ActivePanelModifiers,
@@ -63,7 +62,7 @@ pub struct TabBarSettings {
 }
 
 impl Settings for WorkspaceSettings {
-    fn from_defaults(content: &settings::SettingsContent, _cx: &mut App) -> Self {
+    fn from_settings(content: &settings::SettingsContent, _cx: &mut App) -> Self {
         let workspace = &content.workspace;
         Self {
             active_pane_modifiers: ActivePanelModifiers {
@@ -111,65 +110,6 @@ impl Settings for WorkspaceSettings {
         }
     }
 
-    fn refine(&mut self, content: &settings::SettingsContent, _cx: &mut App) {
-        let workspace = &content.workspace;
-        if let Some(border_size) = workspace
-            .active_pane_modifiers
-            .and_then(|modifier| modifier.border_size)
-        {
-            self.active_pane_modifiers.border_size = Some(border_size);
-        }
-
-        if let Some(inactive_opacity) = workspace
-            .active_pane_modifiers
-            .and_then(|modifier| modifier.inactive_opacity)
-        {
-            self.active_pane_modifiers.inactive_opacity = Some(inactive_opacity);
-        }
-
-        self.bottom_dock_layout
-            .merge_from(&workspace.bottom_dock_layout);
-        self.pane_split_direction_horizontal
-            .merge_from(&workspace.pane_split_direction_horizontal);
-        self.pane_split_direction_vertical
-            .merge_from(&workspace.pane_split_direction_vertical);
-        self.centered_layout.merge_from(&workspace.centered_layout);
-        self.confirm_quit.merge_from(&workspace.confirm_quit);
-        self.show_call_status_icon
-            .merge_from(&workspace.show_call_status_icon);
-        self.autosave.merge_from(&workspace.autosave);
-        self.restore_on_startup
-            .merge_from(&workspace.restore_on_startup);
-        self.restore_on_file_reopen
-            .merge_from(&workspace.restore_on_file_reopen);
-        self.drop_target_size
-            .merge_from(&workspace.drop_target_size);
-        self.use_system_path_prompts
-            .merge_from(&workspace.use_system_path_prompts);
-        self.use_system_prompts
-            .merge_from(&workspace.use_system_prompts);
-        self.command_aliases
-            .extend(workspace.command_aliases.clone());
-        if let Some(max_tabs) = workspace.max_tabs {
-            self.max_tabs = Some(max_tabs);
-        }
-        self.when_closing_with_no_tabs
-            .merge_from(&workspace.when_closing_with_no_tabs);
-        self.on_last_window_closed
-            .merge_from(&workspace.on_last_window_closed);
-        self.resize_all_panels_in_dock.merge_from(
-            &workspace
-                .resize_all_panels_in_dock
-                .as_ref()
-                .map(|resize| resize.clone().into_iter().map(Into::into).collect()),
-        );
-        self.close_on_file_delete
-            .merge_from(&workspace.close_on_file_delete);
-        self.use_system_window_tabs
-            .merge_from(&workspace.use_system_window_tabs);
-        self.zoomed_padding.merge_from(&workspace.zoomed_padding);
-    }
-
     fn import_from_vscode(
         vscode: &settings::VsCodeSettings,
         current: &mut settings::SettingsContent,
@@ -257,7 +197,7 @@ impl Settings for WorkspaceSettings {
 }
 
 impl Settings for TabBarSettings {
-    fn from_defaults(content: &settings::SettingsContent, _cx: &mut App) -> Self {
+    fn from_settings(content: &settings::SettingsContent, _cx: &mut App) -> Self {
         let tab_bar = content.tab_bar.clone().unwrap();
         TabBarSettings {
             show: tab_bar.show.unwrap(),
@@ -266,17 +206,6 @@ impl Settings for TabBarSettings {
         }
     }
 
-    fn refine(&mut self, content: &settings::SettingsContent, _cx: &mut App) {
-        let Some(tab_bar) = &content.tab_bar else {
-            return;
-        };
-        self.show.merge_from(&tab_bar.show);
-        self.show_nav_history_buttons
-            .merge_from(&tab_bar.show_nav_history_buttons);
-        self.show_tab_bar_buttons
-            .merge_from(&tab_bar.show_tab_bar_buttons);
-    }
-
     fn import_from_vscode(
         vscode: &settings::VsCodeSettings,
         current: &mut settings::SettingsContent,

crates/worktree/src/worktree_settings.rs 🔗

@@ -31,11 +31,11 @@ impl WorktreeSettings {
 }
 
 impl Settings for WorktreeSettings {
-    fn from_defaults(content: &settings::SettingsContent, _cx: &mut App) -> Self {
+    fn from_settings(content: &settings::SettingsContent, _cx: &mut App) -> Self {
         let worktree = content.project.worktree.clone();
         let file_scan_exclusions = worktree.file_scan_exclusions.unwrap();
         let file_scan_inclusions = worktree.file_scan_inclusions.unwrap();
-        let private_files = worktree.private_files.unwrap();
+        let private_files = worktree.private_files.unwrap().0;
         let parsed_file_scan_inclusions: Vec<String> = file_scan_inclusions
             .iter()
             .flat_map(|glob| {
@@ -49,54 +49,16 @@ impl Settings for WorktreeSettings {
         Self {
             project_name: None,
             file_scan_exclusions: path_matchers(file_scan_exclusions, "file_scan_exclusions")
-                .unwrap(),
+                .log_err()
+                .unwrap_or_default(),
             file_scan_inclusions: path_matchers(
                 parsed_file_scan_inclusions,
                 "file_scan_inclusions",
             )
             .unwrap(),
-            private_files: path_matchers(private_files, "private_files").unwrap(),
-        }
-    }
-
-    fn refine(&mut self, content: &SettingsContent, _cx: &mut App) {
-        let worktree = &content.project.worktree;
-
-        if let Some(project_name) = worktree.project_name.clone() {
-            self.project_name = Some(project_name);
-        }
-
-        if let Some(mut private_files) = worktree.private_files.clone() {
-            let sources = self.private_files.sources();
-            private_files.extend_from_slice(sources);
-            if let Some(matchers) = path_matchers(private_files, "private_files").log_err() {
-                self.private_files = matchers;
-            }
-        }
-
-        if let Some(file_scan_exclusions) = worktree.file_scan_exclusions.clone() {
-            if let Some(matchers) =
-                path_matchers(file_scan_exclusions, "file_scan_exclusions").log_err()
-            {
-                self.file_scan_exclusions = matchers
-            }
-        }
-
-        if let Some(file_scan_inclusions) = worktree.file_scan_inclusions.clone() {
-            let parsed_file_scan_inclusions: Vec<String> = file_scan_inclusions
-                .iter()
-                .flat_map(|glob| {
-                    Path::new(glob)
-                        .ancestors()
-                        .map(|a| a.to_string_lossy().into())
-                })
-                .filter(|p: &String| !p.is_empty())
-                .collect();
-            if let Some(matchers) =
-                path_matchers(parsed_file_scan_inclusions, "file_scan_inclusions").log_err()
-            {
-                self.file_scan_inclusions = matchers
-            }
+            private_files: path_matchers(private_files, "private_files")
+                .log_err()
+                .unwrap_or_default(),
         }
     }
 

crates/zlog_settings/src/zlog_settings.rs 🔗

@@ -24,17 +24,11 @@ pub struct ZlogSettings {
 }
 
 impl Settings for ZlogSettings {
-    fn from_defaults(content: &settings::SettingsContent, _: &mut App) -> Self {
+    fn from_settings(content: &settings::SettingsContent, _: &mut App) -> Self {
         ZlogSettings {
             scopes: content.log.clone().unwrap(),
         }
     }
 
-    fn refine(&mut self, content: &settings::SettingsContent, _: &mut App) {
-        if let Some(log) = &content.log {
-            self.scopes.extend(log.clone());
-        }
-    }
-
     fn import_from_vscode(_: &settings::VsCodeSettings, _: &mut settings::SettingsContent) {}
 }