project

Conrad Irwin created

Change summary

crates/context_server/src/context_server.rs     |   4 
crates/git_hosting_providers/src/settings.rs    |   4 
crates/project/src/context_server_store.rs      |  37 +-
crates/project/src/project.rs                   |  49 +++-
crates/project/src/project_settings.rs          | 184 +++++++++++-------
crates/project/src/project_tests.rs             |   4 
crates/settings/src/settings_content.rs         |  49 +---
crates/settings/src/settings_content/agent.rs   |  10 
crates/settings/src/settings_content/project.rs |  70 ++++--
crates/settings/src/settings_content/theme.rs   |   2 
10 files changed, 234 insertions(+), 179 deletions(-)

Detailed changes

crates/context_server/src/context_server.rs 🔗

@@ -12,13 +12,9 @@ use std::{fmt::Display, path::PathBuf};
 
 use anyhow::Result;
 use client::Client;
-use collections::HashMap;
 use gpui::AsyncApp;
 use parking_lot::RwLock;
-use schemars::JsonSchema;
-use serde::{Deserialize, Serialize};
 pub use settings::ContextServerCommand;
-use util::redact::should_redact;
 
 #[derive(Debug, Clone, PartialEq, Eq, Hash)]
 pub struct ContextServerId(pub Arc<str>);

crates/git_hosting_providers/src/settings.rs 🔗

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

crates/project/src/context_server_store.rs 🔗

@@ -915,7 +915,7 @@ mod tests {
             set_context_server_configuration(
                 vec![(
                     server_1_id.0.clone(),
-                    ContextServerSettings::Extension {
+                    settings::ContextServerSettingsContent::Extension {
                         enabled: true,
                         settings: json!({
                             "somevalue": false
@@ -934,7 +934,7 @@ mod tests {
             set_context_server_configuration(
                 vec![(
                     server_1_id.0.clone(),
-                    ContextServerSettings::Extension {
+                    settings::ContextServerSettingsContent::Extension {
                         enabled: true,
                         settings: json!({
                             "somevalue": false
@@ -961,7 +961,7 @@ mod tests {
                 vec![
                     (
                         server_1_id.0.clone(),
-                        ContextServerSettings::Extension {
+                        settings::ContextServerSettingsContent::Extension {
                             enabled: true,
                             settings: json!({
                                 "somevalue": false
@@ -970,7 +970,7 @@ mod tests {
                     ),
                     (
                         server_2_id.0.clone(),
-                        ContextServerSettings::Custom {
+                        settings::ContextServerSettingsContent::Custom {
                             enabled: true,
                             command: ContextServerCommand {
                                 path: "somebinary".into(),
@@ -1002,7 +1002,7 @@ mod tests {
                 vec![
                     (
                         server_1_id.0.clone(),
-                        ContextServerSettings::Extension {
+                        settings::ContextServerSettingsContent::Extension {
                             enabled: true,
                             settings: json!({
                                 "somevalue": false
@@ -1011,7 +1011,7 @@ mod tests {
                     ),
                     (
                         server_2_id.0.clone(),
-                        ContextServerSettings::Custom {
+                        settings::ContextServerSettingsContent::Custom {
                             enabled: true,
                             command: ContextServerCommand {
                                 path: "somebinary".into(),
@@ -1027,6 +1027,7 @@ mod tests {
 
             cx.run_until_parked();
         }
+        dbg!("hi");
 
         // Ensure that mcp-2 is removed once it is removed from the settings
         {
@@ -1038,7 +1039,7 @@ mod tests {
             set_context_server_configuration(
                 vec![(
                     server_1_id.0.clone(),
-                    ContextServerSettings::Extension {
+                    settings::ContextServerSettingsContent::Extension {
                         enabled: true,
                         settings: json!({
                             "somevalue": false
@@ -1054,6 +1055,7 @@ mod tests {
                 assert_eq!(store.read(cx).status_for_server(&server_2_id), None);
             });
         }
+        dbg!("bye");
 
         // Ensure that nothing happens if the settings do not change
         {
@@ -1061,7 +1063,7 @@ mod tests {
             set_context_server_configuration(
                 vec![(
                     server_1_id.0.clone(),
-                    ContextServerSettings::Extension {
+                    settings::ContextServerSettingsContent::Extension {
                         enabled: true,
                         settings: json!({
                             "somevalue": false
@@ -1147,7 +1149,7 @@ mod tests {
             set_context_server_configuration(
                 vec![(
                     server_1_id.0.clone(),
-                    ContextServerSettings::Custom {
+                    settings::ContextServerSettingsContent::Custom {
                         enabled: false,
                         command: ContextServerCommand {
                             path: "somebinary".into(),
@@ -1176,7 +1178,7 @@ mod tests {
             set_context_server_configuration(
                 vec![(
                     server_1_id.0.clone(),
-                    ContextServerSettings::Custom {
+                    settings::ContextServerSettingsContent::Custom {
                         enabled: true,
                         command: ContextServerCommand {
                             path: "somebinary".into(),
@@ -1194,18 +1196,17 @@ mod tests {
     }
 
     fn set_context_server_configuration(
-        context_servers: Vec<(Arc<str>, ContextServerSettings)>,
+        context_servers: Vec<(Arc<str>, settings::ContextServerSettingsContent)>,
         cx: &mut TestAppContext,
     ) {
         cx.update(|cx| {
             SettingsStore::update_global(cx, |store, cx| {
-                let mut settings = ProjectSettings::default();
-                for (id, config) in context_servers {
-                    settings.context_servers.insert(id, config);
-                }
-                store
-                    .set_user_settings(&serde_json::to_string(&settings).unwrap(), cx)
-                    .unwrap();
+                store.update_user_settings(cx, |content| {
+                    content.project.context_servers.clear();
+                    for (id, config) in context_servers {
+                        content.project.context_servers.insert(id, config);
+                    }
+                });
             })
         });
     }

crates/project/src/project.rs 🔗

@@ -985,7 +985,7 @@ impl settings::Settings for DisableAiSettings {
 
     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.project.disable_ai.unwrap_or(false);
+        self.disable_ai = self.disable_ai || content.disable_ai.unwrap_or(false);
     }
 
     fn import_from_vscode(
@@ -3244,9 +3244,22 @@ impl Project {
     ) {
         self.buffers_needing_diff.insert(buffer.downgrade());
         let first_insertion = self.buffers_needing_diff.len() == 1;
-
         let settings = ProjectSettings::get_global(cx);
-        let delay = settings.git.gutter_debounce;
+        let delay = if let Some(delay) = settings.git.gutter_debounce {
+            delay
+        } else {
+            if first_insertion {
+                let this = cx.weak_entity();
+                cx.defer(move |cx| {
+                    if let Some(this) = this.upgrade() {
+                        this.update(cx, |this, cx| {
+                            this.recalculate_buffer_diffs(cx).detach();
+                        });
+                    }
+                });
+            }
+            return;
+        };
 
         const MIN_DELAY: u64 = 50;
         let delay = delay.max(MIN_DELAY);
@@ -5632,32 +5645,42 @@ mod disable_ai_settings_tests {
     #[gpui::test]
     async fn test_disable_ai_settings_security(cx: &mut TestAppContext) {
         cx.update(|cx| {
-            let mut store = SettingsStore::new(cx, &settings::test_settings());
-            store.register_setting::<DisableAiSettings>(cx);
+            settings::init(cx);
+            Project::init_settings(cx);
+
             // Test 1: Default is false (AI enabled)
             assert!(
                 !DisableAiSettings::get_global(cx).disable_ai,
                 "Default should allow AI"
             );
+        });
 
-            let disable_true = serde_json::json!({
-                "disable_ai": true
-            })
-            .to_string();
-            let disable_false = serde_json::json!({
-                "disable_ai": false
-            })
-            .to_string();
+        let disable_true = serde_json::json!({
+            "disable_ai": true
+        })
+        .to_string();
+        let disable_false = serde_json::json!({
+            "disable_ai": false
+        })
+        .to_string();
 
+        cx.update_global::<SettingsStore, _>(|store, cx| {
             store.set_user_settings(&disable_false, cx).unwrap();
             store.set_global_settings(&disable_true, cx).unwrap();
+        });
+        cx.update(|cx| {
             assert!(
                 DisableAiSettings::get_global(cx).disable_ai,
                 "Local false cannot override global true"
             );
+        });
 
+        cx.update_global::<SettingsStore, _>(|store, cx| {
             store.set_global_settings(&disable_false, cx).unwrap();
             store.set_user_settings(&disable_true, cx).unwrap();
+        });
+
+        cx.update(|cx| {
             assert!(
                 DisableAiSettings::get_global(cx).disable_ai,
                 "Local false cannot override global true"

crates/project/src/project_settings.rs 🔗

@@ -1,14 +1,10 @@
 use anyhow::Context as _;
-use clock::Global;
 use collections::HashMap;
 use context_server::ContextServerCommand;
 use dap::adapters::DebugAdapterName;
 use fs::Fs;
 use futures::StreamExt as _;
-use gpui::{
-    App, AsyncApp, BorrowAppContext, Context, Entity, EventEmitter, SharedString, Subscription,
-    Task,
-};
+use gpui::{App, AsyncApp, BorrowAppContext, Context, Entity, EventEmitter, Subscription, Task};
 use lsp::LanguageServerName;
 use paths::{
     EDITORCONFIG_NAME, local_debug_file_relative_path, local_settings_file_relative_path,
@@ -17,16 +13,17 @@ use paths::{
 };
 use rpc::{
     AnyProtoClient, TypedEnvelope,
-    proto::{self, FromProto, Message, REMOTE_SERVER_PROJECT_ID, ToProto},
+    proto::{self, FromProto, REMOTE_SERVER_PROJECT_ID, ToProto},
 };
 use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
+pub use settings::DirenvSettings;
+pub use settings::LspSettings;
 use settings::{
-    InvalidSettingsError, LocalSettingsKind, Settings, SettingsKey, SettingsLocation,
-    SettingsStore, SettingsUi, parse_json_with_comments, watch_config_file,
+    InvalidSettingsError, LocalSettingsKind, Settings, SettingsLocation, SettingsStore, SettingsUi,
+    parse_json_with_comments, watch_config_file,
 };
 use std::{
-    collections::BTreeMap,
     path::{Path, PathBuf},
     sync::Arc,
     time::Duration,
@@ -51,13 +48,13 @@ pub struct ProjectSettings {
     /// name to the lsp value.
     /// Default: null
     // todo! should these hash map types be Map<key, SettingsContent> or Map<Key, Settings>
-    pub lsp: HashMap<LanguageServerName, settings::LspSettingsContent>,
+    pub lsp: HashMap<LanguageServerName, settings::LspSettings>,
 
     /// Common language server settings.
     pub global_lsp_settings: GlobalLspSettings,
 
     /// Configuration for Debugger-related features
-    pub dap: HashMap<DebugAdapterName, DapSettings>,
+    pub dap: HashMap<DebugAdapterName, settings::DapSettings>,
 
     /// Settings for context servers used for AI-related features.
     pub context_servers: HashMap<Arc<str>, ContextServerSettings>,
@@ -78,10 +75,35 @@ pub struct ProjectSettings {
     pub session: SessionSettings,
 }
 
-#[derive(Debug, Clone, Default, PartialEq)]
-pub struct DapSettings {
-    pub binary: String,
-    pub args: Vec<String>,
+#[derive(Copy, Clone, Debug)]
+pub struct SessionSettings {
+    /// Whether or not to restore unsaved buffers on restart.
+    ///
+    /// If this is true, user won't be prompted whether to save/discard
+    /// dirty files when closing the application.
+    ///
+    /// Default: true
+    pub restore_unsaved_buffers: bool,
+}
+
+#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
+pub struct NodeBinarySettings {
+    /// The path to the Node binary.
+    pub path: Option<String>,
+    /// The path to the npm binary Zed should use (defaults to `.path/../npm`).
+    pub npm_path: Option<String>,
+    /// If enabled, Zed will download its own copy of Node.
+    pub ignore_system_version: bool,
+}
+
+impl From<settings::NodeBinarySettings> for NodeBinarySettings {
+    fn from(settings: settings::NodeBinarySettings) -> Self {
+        Self {
+            path: settings.path,
+            npm_path: settings.npm_path,
+            ignore_system_version: settings.ignore_system_version.unwrap_or(false),
+        }
+    }
 }
 
 /// Common language server settings.
@@ -281,7 +303,7 @@ pub struct GitSettings {
     /// Sets the debounce threshold (in milliseconds) after which changes are reflected in the git gutter.
     ///
     /// Default: null
-    pub gutter_debounce: u64,
+    pub gutter_debounce: Option<u64>,
     /// Whether or not to show git blame data inline in
     /// the currently focused line.
     ///
@@ -415,7 +437,7 @@ pub struct InlineDiagnosticsSettings {
     /// Default: 0
     pub min_column: u32,
 
-    pub max_severity: DiagnosticSeverity,
+    pub max_severity: Option<DiagnosticSeverity>,
 }
 
 #[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
@@ -432,7 +454,7 @@ pub struct LspPullDiagnosticsSettings {
 }
 
 impl Settings for ProjectSettings {
-    fn from_defaults(content: &settings::SettingsContent, cx: &mut App) -> Self {
+    fn from_defaults(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();
@@ -441,7 +463,7 @@ impl Settings for ProjectSettings {
         let git = content.git.as_ref().unwrap();
         let git_settings = GitSettings {
             git_gutter: git.git_gutter.unwrap(),
-            gutter_debounce: git.gutter_debounce.unwrap(),
+            gutter_debounce: git.gutter_debounce,
             inline_blame: {
                 let inline = git.inline_blame.unwrap();
                 InlineBlameSettings {
@@ -474,21 +496,18 @@ impl Settings for ProjectSettings {
                 .map(|(key, value)| (LanguageServerName(key.into()), value.into()))
                 .collect(),
             global_lsp_settings: GlobalLspSettings {
-                button: content.global_lsp_settings.unwrap().button.unwrap(),
+                button: content
+                    .global_lsp_settings
+                    .as_ref()
+                    .unwrap()
+                    .button
+                    .unwrap(),
             },
             dap: project
                 .dap
                 .clone()
                 .into_iter()
-                .map(|(key, value)| {
-                    (
-                        DebugAdapterName(key.into()),
-                        DapSettings {
-                            binary: value.binary.unwrap(),
-                            args: value.args,
-                        },
-                    )
-                })
+                .map(|(key, value)| (DebugAdapterName(key.into()), value))
                 .collect(),
             diagnostics: DiagnosticsSettings {
                 button: diagnostics.button.unwrap(),
@@ -502,17 +521,19 @@ impl Settings for ProjectSettings {
                     update_debounce_ms: inline_diagnostics.update_debounce_ms.unwrap(),
                     padding: inline_diagnostics.padding.unwrap(),
                     min_column: inline_diagnostics.min_column.unwrap(),
-                    max_severity: inline_diagnostics.max_severity.unwrap().into(),
+                    max_severity: inline_diagnostics.max_severity.map(Into::into),
                 },
             },
             git: git_settings,
-            node: content.node.clone(),
-            load_direnv: project.load_direnv.unwrap(),
-            session: content.session.clone(),
+            node: content.node.clone().unwrap().into(),
+            load_direnv: project.load_direnv.clone().unwrap(),
+            session: SessionSettings {
+                restore_unsaved_buffers: content.session.unwrap().restore_unsaved_buffers.unwrap(),
+            },
         }
     }
 
-    fn refine(&mut self, content: &settings::SettingsContent, cx: &mut App) {
+    fn refine(&mut self, content: &settings::SettingsContent, _cx: &mut App) {
         let project = &content.project;
         self.context_servers.extend(
             project
@@ -521,16 +542,14 @@ impl Settings for ProjectSettings {
                 .into_iter()
                 .map(|(key, value)| (key, value.into())),
         );
-        self.dap
-            .extend(project.dap.clone().into_iter().filter_map(|(key, value)| {
-                Some((
-                    DebugAdapterName(key.into()),
-                    DapSettings {
-                        binary: value.binary?,
-                        args: value.args,
-                    },
-                ))
-            }));
+        dbg!(&self.context_servers);
+        self.dap.extend(
+            project
+                .dap
+                .clone()
+                .into_iter()
+                .filter_map(|(key, value)| Some((DebugAdapterName(key.into()), value))),
+        );
         if let Some(diagnostics) = content.diagnostics.as_ref() {
             if let Some(inline) = &diagnostics.inline {
                 self.diagnostics.inline.enabled.merge_from(&inline.enabled);
@@ -543,10 +562,9 @@ impl Settings for ProjectSettings {
                     .inline
                     .min_column
                     .merge_from(&inline.min_column);
-                self.diagnostics
-                    .inline
-                    .max_severity
-                    .merge_from(&inline.max_severity.map(Into::into));
+                if let Some(max_severity) = inline.max_severity {
+                    self.diagnostics.inline.max_severity = Some(max_severity.into())
+                }
             }
 
             self.diagnostics.button.merge_from(&diagnostics.button);
@@ -595,18 +613,37 @@ impl Settings for ProjectSettings {
             }
             self.git.git_gutter.merge_from(&git.git_gutter);
             self.git.hunk_style.merge_from(&git.hunk_style);
-            self.git.gutter_debounce.merge_from(&git.gutter_debounce);
+            if let Some(debounce) = git.gutter_debounce {
+                self.git.gutter_debounce = Some(debounce);
+            }
         }
-        self.global_lsp_settings = content.global_lsp_settings.clone();
-        self.load_direnv = content.project.load_direnv.clone();
-        self.lsp.extend(
-            content
-                .project
-                .lsp
-                .clone()
-                .into_iter()
-                .map(|(key, value)| (key, lsp_settings)),
+        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(
@@ -750,17 +787,19 @@ impl SettingsObserver {
             if let Some(upstream_client) = upstream_client {
                 let mut user_settings = None;
                 user_settings_watcher = Some(cx.observe_global::<SettingsStore>(move |_, cx| {
-                    let new_settings = cx.global::<SettingsStore>().raw_user_settings();
-                    if Some(new_settings) != user_settings.as_ref() {
-                        if let Some(new_settings_string) = serde_json::to_string(new_settings).ok()
-                        {
-                            user_settings = new_settings.clone();
-                            upstream_client
-                                .send(proto::UpdateUserSettings {
-                                    project_id: REMOTE_SERVER_PROJECT_ID,
-                                    contents: new_settings_string,
-                                })
-                                .log_err();
+                    if let Some(new_settings) = cx.global::<SettingsStore>().raw_user_settings() {
+                        if Some(new_settings) != user_settings.as_ref() {
+                            if let Some(new_settings_string) =
+                                serde_json::to_string(new_settings).ok()
+                            {
+                                user_settings = Some(new_settings.clone());
+                                upstream_client
+                                    .send(proto::UpdateUserSettings {
+                                        project_id: REMOTE_SERVER_PROJECT_ID,
+                                        contents: new_settings_string,
+                                    })
+                                    .log_err();
+                            }
                         }
                     }
                 }));
@@ -870,10 +909,9 @@ impl SettingsObserver {
         envelope: TypedEnvelope<proto::UpdateUserSettings>,
         cx: AsyncApp,
     ) -> anyhow::Result<()> {
-        let new_settings = serde_json::from_str::<serde_json::Value>(&envelope.payload.contents)
-            .with_context(|| {
-                format!("deserializing {} user settings", envelope.payload.contents)
-            })?;
+        let new_settings = serde_json::from_str(&envelope.payload.contents).with_context(|| {
+            format!("deserializing {} user settings", envelope.payload.contents)
+        })?;
         cx.update_global(|settings_store: &mut SettingsStore, cx| {
             settings_store
                 .set_raw_user_settings(new_settings, cx)

crates/project/src/project_tests.rs 🔗

@@ -8769,8 +8769,8 @@ async fn test_rescan_with_gitignore(cx: &mut gpui::TestAppContext) {
     init_test(cx);
     cx.update(|cx| {
         cx.update_global::<SettingsStore, _>(|store, cx| {
-            store.update_user_settings::<WorktreeSettings>(cx, |project_settings| {
-                project_settings.file_scan_exclusions = Some(Vec::new());
+            store.update_user_settings(cx, |settings| {
+                settings.project.worktree.file_scan_exclusions = Some(Vec::new());
             });
         });
     });

crates/settings/src/settings_content.rs 🔗

@@ -19,7 +19,7 @@ pub use util::serde::default_true;
 
 use crate::ActiveSettingsProfileName;
 
-#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema)]
+#[derive(Debug, PartialEq, Default, Clone, Serialize, Deserialize, JsonSchema)]
 pub struct SettingsContent {
     #[serde(flatten)]
     pub project: ProjectSettingsContent,
@@ -45,9 +45,6 @@ pub struct SettingsContent {
     /// Configuration for Git-related features
     pub git: Option<GitSettings>,
 
-    /// The list of custom Git hosting providers.
-    pub git_hosting_providers: Option<Vec<GitHostingProviderConfig>>,
-
     /// Common language server settings.
     pub global_lsp_settings: Option<GlobalLspSettingsContent>,
 
@@ -70,7 +67,7 @@ pub struct SettingsContent {
     pub server_url: Option<String>,
 
     /// Configuration for session-related features
-    pub session: Option<SessionSettings>,
+    pub session: Option<SessionSettingsContent>,
     /// Control what info is collected by Zed.
     pub telemetry: Option<TelemetrySettingsContent>,
 
@@ -86,6 +83,11 @@ pub struct SettingsContent {
 
     // Settings related to calls in Zed
     pub calls: Option<CallSettingsContent>,
+
+    /// Whether to disable all AI features in Zed.
+    ///
+    /// Default: false
+    pub disable_ai: Option<bool>,
 }
 
 impl SettingsContent {
@@ -101,7 +103,7 @@ pub struct ServerSettingsContent {
     pub project: ProjectSettingsContent,
 }
 
-#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema)]
+#[derive(Debug, Default, PartialEq, Clone, Serialize, Deserialize, JsonSchema)]
 pub struct UserSettingsContent {
     #[serde(flatten)]
     pub content: SettingsContent,
@@ -211,7 +213,7 @@ pub enum TitleBarVisibilityContent {
 }
 
 /// Configuration of audio in Zed.
-#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
+#[derive(Clone, PartialEq, Default, Serialize, Deserialize, JsonSchema, Debug)]
 pub struct AudioSettingsContent {
     /// Opt into the new audio system.
     #[serde(rename = "experimental.rodio_audio", default)]
@@ -231,31 +233,8 @@ pub struct AudioSettingsContent {
     pub control_output_volume: Option<bool>,
 }
 
-/// A custom Git hosting provider.
-#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
-pub struct GitHostingProviderConfig {
-    /// The type of the provider.
-    ///
-    /// Must be one of `github`, `gitlab`, or `bitbucket`.
-    pub provider: GitHostingProviderKind,
-
-    /// The base URL for the provider (e.g., "https://code.corp.big.com").
-    pub base_url: String,
-
-    /// The display name for the provider (e.g., "BigCorp GitHub").
-    pub name: String,
-}
-
-#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
-#[serde(rename_all = "snake_case")]
-pub enum GitHostingProviderKind {
-    Github,
-    Gitlab,
-    Bitbucket,
-}
-
 /// Control what info is collected by Zed.
-#[derive(Default, Clone, Serialize, Deserialize, JsonSchema, Debug)]
+#[derive(Default, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Debug)]
 pub struct TelemetrySettingsContent {
     /// Send debug info like crash reports.
     ///
@@ -267,7 +246,7 @@ pub struct TelemetrySettingsContent {
     pub metrics: Option<bool>,
 }
 
-#[derive(Debug, Serialize, Deserialize, JsonSchema, Clone)]
+#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Clone)]
 pub struct DebuggerSettingsContent {
     /// Determines the stepping granularity.
     ///
@@ -322,21 +301,21 @@ pub enum DockPosition {
 }
 
 /// Settings for slash commands.
-#[derive(Deserialize, Serialize, Debug, Default, Clone, JsonSchema)]
+#[derive(Deserialize, Serialize, Debug, Default, Clone, JsonSchema, PartialEq, Eq)]
 pub struct SlashCommandSettings {
     /// Settings for the `/cargo-workspace` slash command.
     pub cargo_workspace: Option<CargoWorkspaceCommandSettings>,
 }
 
 /// Settings for the `/cargo-workspace` slash command.
-#[derive(Deserialize, Serialize, Debug, Default, Clone, JsonSchema)]
+#[derive(Deserialize, Serialize, Debug, Default, Clone, JsonSchema, PartialEq, Eq)]
 pub struct CargoWorkspaceCommandSettings {
     /// Whether `/cargo-workspace` is enabled.
     pub enabled: Option<bool>,
 }
 
 /// Configuration of voice calls in Zed.
-#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
+#[derive(Clone, PartialEq, Default, Serialize, Deserialize, JsonSchema, Debug)]
 pub struct CallSettingsContent {
     /// Whether the microphone should be muted when joining a channel or a call.
     ///

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

@@ -4,7 +4,7 @@ use schemars::{JsonSchema, json_schema};
 use serde::{Deserialize, Serialize};
 use std::{borrow::Cow, path::PathBuf, sync::Arc};
 
-#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug, Default)]
+#[derive(Clone, PartialEq, Serialize, Deserialize, JsonSchema, Debug, Default)]
 pub struct AgentSettingsContent {
     /// Whether the Agent is enabled.
     ///
@@ -174,7 +174,7 @@ pub struct ContextServerPresetContent {
     pub tools: IndexMap<Arc<str>, bool>,
 }
 
-#[derive(Copy, Clone, Default, Debug, Serialize, Deserialize, JsonSchema)]
+#[derive(Copy, Clone, Default, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
 #[serde(rename_all = "snake_case")]
 pub enum AgentDockPosition {
     Left,
@@ -183,7 +183,7 @@ pub enum AgentDockPosition {
     Bottom,
 }
 
-#[derive(Copy, Clone, Default, Debug, Serialize, Deserialize, JsonSchema)]
+#[derive(Copy, Clone, Default, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
 #[serde(rename_all = "snake_case")]
 pub enum DefaultAgentView {
     #[default]
@@ -191,7 +191,7 @@ pub enum DefaultAgentView {
     TextThread,
 }
 
-#[derive(Copy, Clone, Default, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
+#[derive(Copy, Clone, Default, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
 #[serde(rename_all = "snake_case")]
 pub enum NotifyWhenAgentWaiting {
     #[default]
@@ -263,7 +263,7 @@ impl From<&str> for LanguageModelProviderSetting {
     }
 }
 
-#[derive(Default, Deserialize, Serialize, Clone, JsonSchema, Debug, PartialEq)]
+#[derive(Default, PartialEq, Deserialize, Serialize, Clone, JsonSchema, Debug)]
 pub struct AllAgentServersSettings {
     pub gemini: Option<BuiltinAgentServerSettings>,
     pub claude: Option<BuiltinAgentServerSettings>,

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

@@ -5,7 +5,7 @@ use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
 use util::serde::default_true;
 
-use crate::AllLanguageSettingsContent;
+use crate::{AllLanguageSettingsContent, SlashCommandSettings};
 
 #[derive(Debug, PartialEq, Clone, Default, Serialize, Deserialize, JsonSchema)]
 pub struct ProjectSettingsContent {
@@ -24,11 +24,11 @@ pub struct ProjectSettingsContent {
     /// name to the lsp value.
     /// Default: null
     #[serde(default)]
-    pub lsp: HashMap<Arc<str>, LspSettingsContent>,
+    pub lsp: HashMap<Arc<str>, LspSettings>,
 
     /// Configuration for Debugger-related features
     #[serde(default)]
-    pub dap: HashMap<Arc<str>, DapSettingsContent>,
+    pub dap: HashMap<Arc<str>, DapSettings>,
 
     /// Settings for context servers used for AI-related features.
     #[serde(default)]
@@ -39,6 +39,9 @@ pub struct ProjectSettingsContent {
 
     /// Settings for slash commands.
     pub slash_commands: Option<SlashCommandSettings>,
+
+    /// The list of custom Git hosting providers.
+    pub git_hosting_providers: Option<Vec<GitHostingProviderConfig>>,
 }
 
 #[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
@@ -80,7 +83,7 @@ pub struct WorktreeSettingsContent {
 
 #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, Hash)]
 #[serde(rename_all = "snake_case")]
-pub struct LspSettingsContent {
+pub struct LspSettings {
     pub binary: Option<BinarySettings>,
     pub initialization_options: Option<serde_json::Value>,
     pub settings: Option<serde_json::Value>,
@@ -92,7 +95,7 @@ pub struct LspSettingsContent {
     pub fetch: Option<FetchSettings>,
 }
 
-impl Default for LspSettingsContent {
+impl Default for LspSettings {
     fn default() -> Self {
         Self {
             binary: None,
@@ -119,7 +122,7 @@ pub struct FetchSettings {
 }
 
 /// Common language server settings.
-#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
 pub struct GlobalLspSettingsContent {
     /// Whether to show the LSP servers button in the status bar.
     ///
@@ -128,31 +131,23 @@ pub struct GlobalLspSettingsContent {
 }
 
 // todo! binary is actually just required, shouldn't be an option
-#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
+#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
 #[serde(rename_all = "snake_case")]
-pub struct DapSettingsContent {
+pub struct DapSettings {
     pub binary: Option<String>,
     #[serde(default)]
     pub args: Vec<String>,
 }
 
-#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
-pub struct SessionSettings {
+#[derive(Copy, Clone, PartialEq, Eq, Debug, Serialize, Deserialize, JsonSchema)]
+pub struct SessionSettingsContent {
     /// Whether or not to restore unsaved buffers on restart.
     ///
     /// If this is true, user won't be prompted whether to save/discard
     /// dirty files when closing the application.
     ///
     /// Default: true
-    pub restore_unsaved_buffers: bool,
-}
-
-impl Default for SessionSettings {
-    fn default() -> Self {
-        Self {
-            restore_unsaved_buffers: true,
-        }
-    }
+    pub restore_unsaved_buffers: Option<bool>,
 }
 
 #[derive(Deserialize, Serialize, Clone, PartialEq, Eq, JsonSchema, Debug)]
@@ -213,7 +208,7 @@ impl std::fmt::Debug for ContextServerCommand {
     }
 }
 
-#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
+#[derive(Copy, Clone, Debug, PartialEq, Default, Serialize, Deserialize, JsonSchema)]
 pub struct GitSettings {
     /// Whether or not to show the git gutter.
     ///
@@ -238,7 +233,7 @@ pub struct GitSettings {
     pub hunk_style: Option<GitHunkStyleSetting>,
 }
 
-#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema)]
+#[derive(Clone, Copy, Debug, PartialEq, Default, Serialize, Deserialize, JsonSchema)]
 #[serde(rename_all = "snake_case")]
 pub enum GitGutterSetting {
     /// Show git gutter in tracked files.
@@ -248,7 +243,7 @@ pub enum GitGutterSetting {
     Hide,
 }
 
-#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema)]
+#[derive(Clone, Copy, Debug, PartialEq, Default, Serialize, Deserialize, JsonSchema)]
 #[serde(rename_all = "snake_case")]
 pub struct InlineBlameSettings {
     /// Whether or not to show git blame data inline in
@@ -276,7 +271,7 @@ pub struct InlineBlameSettings {
     pub show_commit_summary: Option<bool>,
 }
 
-#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema)]
+#[derive(Clone, Copy, PartialEq, Debug, Serialize, Deserialize, JsonSchema)]
 #[serde(rename_all = "snake_case")]
 pub struct BranchPickerSettingsContent {
     /// Whether to show author name as part of the commit information.
@@ -285,7 +280,7 @@ pub struct BranchPickerSettingsContent {
     pub show_author_name: Option<bool>,
 }
 
-#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema)]
+#[derive(Clone, Copy, PartialEq, Debug, Default, Serialize, Deserialize, JsonSchema)]
 #[serde(rename_all = "snake_case")]
 pub enum GitHunkStyleSetting {
     /// Show unstaged hunks with a filled background and staged hunks hollow.
@@ -295,7 +290,7 @@ pub enum GitHunkStyleSetting {
     UnstagedHollow,
 }
 
-#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
+#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
 pub struct DiagnosticsSettingsContent {
     /// Whether to show the project diagnostics button in the status bar.
     pub button: Option<bool>,
@@ -349,7 +344,7 @@ pub struct InlineDiagnosticsSettingsContent {
     pub max_severity: Option<DiagnosticSeverityContent>,
 }
 
-#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
+#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
 pub struct NodeBinarySettings {
     /// The path to the Node binary.
     pub path: Option<String>,
@@ -382,3 +377,26 @@ pub enum DiagnosticSeverityContent {
     #[serde(alias = "all")]
     Hint,
 }
+
+/// A custom Git hosting provider.
+#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, JsonSchema)]
+pub struct GitHostingProviderConfig {
+    /// The type of the provider.
+    ///
+    /// Must be one of `github`, `gitlab`, or `bitbucket`.
+    pub provider: GitHostingProviderKind,
+
+    /// The base URL for the provider (e.g., "https://code.corp.big.com").
+    pub base_url: String,
+
+    /// The display name for the provider (e.g., "BigCorp GitHub").
+    pub name: String,
+}
+
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub enum GitHostingProviderKind {
+    Github,
+    Gitlab,
+    Bitbucket,
+}

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

@@ -7,7 +7,7 @@ use serde_repr::{Deserialize_repr, Serialize_repr};
 use std::sync::Arc;
 
 /// Settings for rendering text in UI and text buffers.
-#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
+#[derive(Clone, PartialEq, Debug, Default, Serialize, Deserialize, JsonSchema)]
 pub struct ThemeSettingsContent {
     /// The default font size for text in the UI.
     #[serde(default)]