wip - project

Ben Kunkle created

Change summary

Cargo.lock                                      |   1 
assets/settings/default.json                    |  13 
crates/agent_ui/src/slash_command_settings.rs   |  24 
crates/call/src/call_settings.rs                |  37 
crates/context_server/Cargo.toml                |   3 
crates/context_server/src/context_server.rs     |  27 
crates/project/src/project.rs                   |  36 
crates/project/src/project_settings.rs          | 648 +++++++++---------
crates/settings/src/settings_content.rs         |  98 +-
crates/settings/src/settings_content/project.rs | 384 +++++++++++
crates/settings/src/vscode_import.rs            |   5 
11 files changed, 829 insertions(+), 447 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -3596,6 +3596,7 @@ dependencies = [
  "schemars",
  "serde",
  "serde_json",
+ "settings",
  "smol",
  "tempfile",
  "url",

assets/settings/default.json 🔗

@@ -1807,6 +1807,15 @@
       "api_url": "https://api.mistral.ai/v1"
     }
   },
+  "session": {
+    /// 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
+    "restore_unsaved_buffers": true
+  },
   // Zed's Prettier integration settings.
   // Allows to enable/disable formatting with Prettier
   // and configure default Prettier, used when no project-level Prettier installation is found.
@@ -1845,6 +1854,10 @@
     //     }
     // }
   },
+  // DAP Specific settings.
+  "dap": {
+    // Specify the DAP name as a key here.
+  },
   // Common language server settings.
   "global_lsp_settings": {
     // Whether to show the LSP servers button in the status bar.

crates/agent_ui/src/slash_command_settings.rs 🔗

@@ -5,32 +5,30 @@ use serde::{Deserialize, Serialize};
 use settings::{Settings, SettingsKey, SettingsSources, SettingsUi};
 
 /// Settings for slash commands.
-#[derive(Deserialize, Serialize, Debug, Default, Clone, JsonSchema, SettingsUi, SettingsKey)]
-#[settings_key(key = "slash_commands")]
+#[derive(Debug, Default, Clone)]
 pub struct SlashCommandSettings {
     /// Settings for the `/cargo-workspace` slash command.
-    #[serde(default)]
     pub cargo_workspace: CargoWorkspaceCommandSettings,
 }
 
 /// Settings for the `/cargo-workspace` slash command.
-#[derive(Deserialize, Serialize, Debug, Default, Clone, JsonSchema)]
+#[derive(Debug, Default, Clone)]
 pub struct CargoWorkspaceCommandSettings {
     /// Whether `/cargo-workspace` is enabled.
-    #[serde(default)]
     pub enabled: bool,
 }
 
 impl Settings for SlashCommandSettings {
-    type FileContent = Self;
+    fn from_defaults(content: &settings::SettingsContent, cx: &mut App) -> Self {
+        Self {
+            cargo_workspace: CargoWorkspaceCommandSettings {
+                enabled: content.project.slash_commands.unwrap(),
+            },
+        }
+    }
 
-    fn load(sources: SettingsSources<Self::FileContent>, _cx: &mut App) -> Result<Self> {
-        SettingsSources::<Self::FileContent>::json_merge_with(
-            [sources.default]
-                .into_iter()
-                .chain(sources.user)
-                .chain(sources.server),
-        )
+    fn refine(&mut self, content: &settings::SettingsContent, cx: &mut App) {
+        todo!()
     }
 
     fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) {}

crates/call/src/call_settings.rs 🔗

@@ -3,6 +3,7 @@ use gpui::App;
 use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
 use settings::{Settings, SettingsKey, SettingsSources, SettingsUi};
+use util::MergeFrom;
 
 #[derive(Deserialize, Debug)]
 pub struct CallSettings {
@@ -10,27 +11,25 @@ pub struct CallSettings {
     pub share_on_join: bool,
 }
 
-/// Configuration of voice calls in Zed.
-#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, SettingsUi, SettingsKey)]
-#[settings_key(key = "calls")]
-pub struct CallSettingsContent {
-    /// Whether the microphone should be muted when joining a channel or a call.
-    ///
-    /// Default: false
-    pub mute_on_join: Option<bool>,
-
-    /// Whether your current project should be shared when joining an empty channel.
-    ///
-    /// Default: false
-    pub share_on_join: Option<bool>,
-}
-
 impl Settings for CallSettings {
-    type FileContent = CallSettingsContent;
+    fn from_defaults(content: &settings::SettingsContent, cx: &mut App) -> Self {
+        let call = content.call.unwrap();
+        CallSettings {
+            mute_on_join: call.mute_on_join.unwrap(),
+            share_on_join: call.share_on_join.unwrap(),
+        }
+    }
 
-    fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> Result<Self> {
-        sources.json_merge()
+    fn refine(&mut self, content: &settings::SettingsContent, cx: &mut App) {
+        if let Some(call) = content.call.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 Self::FileContent) {}
+    fn import_from_vscode(
+        _vscode: &settings::VsCodeSettings,
+        _current: &settings::SettingsContent,
+    ) {
+    }
 }

crates/context_server/Cargo.toml 🔗

@@ -25,8 +25,9 @@ net.workspace = true
 parking_lot.workspace = true
 postage.workspace = true
 schemars.workspace = true
-serde.workspace = true
 serde_json.workspace = true
+serde.workspace = true
+settings.workspace = true
 smol.workspace = true
 tempfile.workspace = true
 url = { workspace = true, features = ["serde"] }

crates/context_server/src/context_server.rs 🔗

@@ -17,6 +17,7 @@ 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)]
@@ -28,32 +29,6 @@ impl Display for ContextServerId {
     }
 }
 
-#[derive(Deserialize, Serialize, Clone, PartialEq, Eq, JsonSchema)]
-pub struct ContextServerCommand {
-    #[serde(rename = "command")]
-    pub path: PathBuf,
-    pub args: Vec<String>,
-    pub env: Option<HashMap<String, String>>,
-    /// Timeout for tool calls in milliseconds. Defaults to 60000 (60 seconds) if not specified.
-    pub timeout: Option<u64>,
-}
-
-impl std::fmt::Debug for ContextServerCommand {
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        let filtered_env = self.env.as_ref().map(|env| {
-            env.iter()
-                .map(|(k, v)| (k, if should_redact(k) { "[REDACTED]" } else { v }))
-                .collect::<Vec<_>>()
-        });
-
-        f.debug_struct("ContextServerCommand")
-            .field("path", &self.path)
-            .field("args", &self.args)
-            .field("env", &filtered_env)
-            .finish()
-    }
-}
-
 enum ContextServerTransport {
     Stdio(ContextServerCommand, Option<PathBuf>),
     Custom(Arc<dyn crate::transport::Transport>),

crates/project/src/project.rs 🔗

@@ -979,7 +979,7 @@ pub struct DisableAiSettings {
 impl settings::Settings for DisableAiSettings {
     fn from_defaults(content: &settings::SettingsContent, _cx: &mut App) -> Self {
         Self {
-            disable_ai: content.project.disable_ai.unwrap(),
+            disable_ai: content.disable_ai.unwrap(),
         }
     }
 
@@ -3246,21 +3246,7 @@ impl Project {
         let first_insertion = self.buffers_needing_diff.len() == 1;
 
         let settings = ProjectSettings::get_global(cx);
-        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;
-        };
+        let delay = settings.git.gutter_debounce;
 
         const MIN_DELAY: u64 = 50;
         let delay = delay.max(MIN_DELAY);
@@ -5648,11 +5634,11 @@ mod disable_ai_settings_tests {
         cx.update(|cx| {
             let mut store = SettingsStore::new(cx, &settings::test_settings());
             store.register_setting::<DisableAiSettings>(cx);
-            fn ai_disabled(cx: &App) -> bool {
-                DisableAiSettings::get_global(cx).disable_ai
-            }
             // Test 1: Default is false (AI enabled)
-            assert!(!ai_disabled(cx), "Default should allow AI");
+            assert!(
+                !DisableAiSettings::get_global(cx).disable_ai,
+                "Default should allow AI"
+            );
 
             let disable_true = serde_json::json!({
                 "disable_ai": true
@@ -5665,11 +5651,17 @@ mod disable_ai_settings_tests {
 
             store.set_user_settings(&disable_false, cx).unwrap();
             store.set_global_settings(&disable_true, cx).unwrap();
-            assert!(ai_disabled(cx), "Local false cannot override global true");
+            assert!(
+                DisableAiSettings::get_global(cx).disable_ai,
+                "Local false cannot override global true"
+            );
 
             store.set_global_settings(&disable_false, cx).unwrap();
             store.set_user_settings(&disable_true, cx).unwrap();
-            assert!(ai_disabled(cx), "Local false cannot override global true");
+            assert!(
+                DisableAiSettings::get_global(cx).disable_ai,
+                "Local false cannot override global true"
+            );
         });
     }
 }

crates/project/src/project_settings.rs 🔗

@@ -1,10 +1,14 @@
 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, Subscription, Task};
+use gpui::{
+    App, AsyncApp, BorrowAppContext, Context, Entity, EventEmitter, SharedString, Subscription,
+    Task,
+};
 use lsp::LanguageServerName;
 use paths::{
     EDITORCONFIG_NAME, local_debug_file_relative_path, local_settings_file_relative_path,
@@ -13,7 +17,7 @@ use paths::{
 };
 use rpc::{
     AnyProtoClient, TypedEnvelope,
-    proto::{self, FromProto, REMOTE_SERVER_PROJECT_ID, ToProto},
+    proto::{self, FromProto, Message, REMOTE_SERVER_PROJECT_ID, ToProto},
 };
 use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
@@ -28,7 +32,7 @@ use std::{
     time::Duration,
 };
 use task::{DebugTaskFile, TaskTemplates, VsCodeDebugTaskFile, VsCodeTaskFile};
-use util::{ResultExt, serde::default_true};
+use util::{MergeFrom as _, ResultExt, serde::default_true};
 use worktree::{PathChange, UpdatedEntriesSet, Worktree, WorktreeId};
 
 use crate::{
@@ -36,8 +40,7 @@ use crate::{
     worktree_store::{WorktreeStore, WorktreeStoreEvent},
 };
 
-#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema, SettingsUi, SettingsKey)]
-#[settings_key(None)]
+#[derive(Debug, Clone)]
 pub struct ProjectSettings {
     /// Configuration for language servers.
     ///
@@ -47,50 +50,49 @@ pub struct ProjectSettings {
     /// To override settings for a language, add an entry for that language server's
     /// name to the lsp value.
     /// Default: null
-    #[serde(default)]
-    pub lsp: HashMap<LanguageServerName, LspSettings>,
+    // todo! should these hash map types be Map<key, SettingsContent> or Map<Key, Settings>
+    pub lsp: HashMap<LanguageServerName, settings::LspSettingsContent>,
 
     /// Common language server settings.
-    #[serde(default)]
     pub global_lsp_settings: GlobalLspSettings,
 
     /// Configuration for Debugger-related features
-    #[serde(default)]
     pub dap: HashMap<DebugAdapterName, DapSettings>,
 
     /// Settings for context servers used for AI-related features.
-    #[serde(default)]
     pub context_servers: HashMap<Arc<str>, ContextServerSettings>,
 
     /// Configuration for Diagnostics-related features.
-    #[serde(default)]
     pub diagnostics: DiagnosticsSettings,
 
     /// Configuration for Git-related features
-    #[serde(default)]
     pub git: GitSettings,
 
     /// Configuration for Node-related features
-    #[serde(default)]
     pub node: NodeBinarySettings,
 
     /// Configuration for how direnv configuration should be loaded
-    #[serde(default)]
     pub load_direnv: DirenvSettings,
 
     /// Configuration for session-related features
-    #[serde(default)]
     pub session: SessionSettings,
 }
 
-#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
-#[serde(rename_all = "snake_case")]
+#[derive(Debug, Clone, Default, PartialEq)]
 pub struct DapSettings {
-    pub binary: Option<String>,
-    #[serde(default)]
+    pub binary: String,
     pub args: Vec<String>,
 }
 
+/// Common language server settings.
+#[derive(Debug, Clone, PartialEq)]
+pub struct GlobalLspSettings {
+    /// Whether to show the LSP servers button in the status bar.
+    ///
+    /// Default: `true`
+    pub button: bool,
+}
+
 #[derive(Deserialize, Serialize, Clone, PartialEq, Eq, JsonSchema, Debug)]
 #[serde(tag = "source", rename_all = "snake_case")]
 pub enum ContextServerSettings {
@@ -114,14 +116,17 @@ pub enum ContextServerSettings {
     },
 }
 
-/// Common language server settings.
-#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
-pub struct GlobalLspSettings {
-    /// Whether to show the LSP servers button in the status bar.
-    ///
-    /// Default: `true`
-    #[serde(default = "default_true")]
-    pub button: bool,
+impl From<settings::ContextServerSettingsContent> for ContextServerSettings {
+    fn from(value: settings::ContextServerSettingsContent) -> Self {
+        match value {
+            settings::ContextServerSettingsContent::Custom { enabled, command } => {
+                ContextServerSettings::Custom { enabled, command }
+            }
+            settings::ContextServerSettingsContent::Extension { enabled, settings } => {
+                ContextServerSettings::Extension { enabled, settings }
+            }
+        }
+    }
 }
 
 impl ContextServerSettings {
@@ -147,140 +152,6 @@ impl ContextServerSettings {
     }
 }
 
-#[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.
-    #[serde(default)]
-    pub ignore_system_version: bool,
-}
-
-#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
-#[serde(rename_all = "snake_case")]
-pub enum DirenvSettings {
-    /// Load direnv configuration through a shell hook
-    ShellHook,
-    /// Load direnv configuration directly using `direnv export json`
-    #[default]
-    Direct,
-}
-
-#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
-#[serde(default)]
-pub struct DiagnosticsSettings {
-    /// Whether to show the project diagnostics button in the status bar.
-    pub button: bool,
-
-    /// Whether or not to include warning diagnostics.
-    pub include_warnings: bool,
-
-    /// Settings for using LSP pull diagnostics mechanism in Zed.
-    pub lsp_pull_diagnostics: LspPullDiagnosticsSettings,
-
-    /// Settings for showing inline diagnostics.
-    pub inline: InlineDiagnosticsSettings,
-}
-
-#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema)]
-#[serde(default)]
-pub struct LspPullDiagnosticsSettings {
-    /// Whether to pull for diagnostics or not.
-    ///
-    /// Default: true
-    #[serde(default = "default_true")]
-    pub enabled: bool,
-    /// Minimum time to wait before pulling diagnostics from the language server(s).
-    /// 0 turns the debounce off.
-    ///
-    /// Default: 50
-    #[serde(default = "default_lsp_diagnostics_pull_debounce_ms")]
-    pub debounce_ms: u64,
-}
-
-fn default_lsp_diagnostics_pull_debounce_ms() -> u64 {
-    50
-}
-
-#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema)]
-#[serde(default)]
-pub struct InlineDiagnosticsSettings {
-    /// Whether or not to show inline diagnostics
-    ///
-    /// Default: false
-    pub enabled: bool,
-    /// Whether to only show the inline diagnostics after a delay after the
-    /// last editor event.
-    ///
-    /// Default: 150
-    #[serde(default = "default_inline_diagnostics_update_debounce_ms")]
-    pub update_debounce_ms: u64,
-    /// The amount of padding between the end of the source line and the start
-    /// of the inline diagnostic in units of columns.
-    ///
-    /// Default: 4
-    #[serde(default = "default_inline_diagnostics_padding")]
-    pub padding: u32,
-    /// The minimum column to display inline diagnostics. This setting can be
-    /// used to horizontally align inline diagnostics at some position. Lines
-    /// longer than this value will still push diagnostics further to the right.
-    ///
-    /// Default: 0
-    pub min_column: u32,
-
-    pub max_severity: Option<DiagnosticSeverity>,
-}
-
-fn default_inline_diagnostics_update_debounce_ms() -> u64 {
-    150
-}
-
-fn default_inline_diagnostics_padding() -> u32 {
-    4
-}
-
-impl Default for DiagnosticsSettings {
-    fn default() -> Self {
-        Self {
-            button: true,
-            include_warnings: true,
-            lsp_pull_diagnostics: LspPullDiagnosticsSettings::default(),
-            inline: InlineDiagnosticsSettings::default(),
-        }
-    }
-}
-
-impl Default for LspPullDiagnosticsSettings {
-    fn default() -> Self {
-        Self {
-            enabled: true,
-            debounce_ms: default_lsp_diagnostics_pull_debounce_ms(),
-        }
-    }
-}
-
-impl Default for InlineDiagnosticsSettings {
-    fn default() -> Self {
-        Self {
-            enabled: false,
-            update_debounce_ms: default_inline_diagnostics_update_debounce_ms(),
-            padding: default_inline_diagnostics_padding(),
-            min_column: 0,
-            max_severity: None,
-        }
-    }
-}
-
-impl Default for GlobalLspSettings {
-    fn default() -> Self {
-        Self {
-            button: default_true(),
-        }
-    }
-}
-
 #[derive(
     Clone,
     Copy,
@@ -301,7 +172,6 @@ pub enum DiagnosticSeverity {
     Error,
     Warning,
     Info,
-    #[serde(alias = "all")]
     Hint,
 }
 
@@ -317,6 +187,18 @@ impl DiagnosticSeverity {
     }
 }
 
+impl From<settings::DiagnosticSeverityContent> for DiagnosticSeverity {
+    fn from(severity: settings::DiagnosticSeverityContent) -> Self {
+        match severity {
+            settings::DiagnosticSeverityContent::Off => DiagnosticSeverity::Off,
+            settings::DiagnosticSeverityContent::Error => DiagnosticSeverity::Error,
+            settings::DiagnosticSeverityContent::Warning => DiagnosticSeverity::Warning,
+            settings::DiagnosticSeverityContent::Info => DiagnosticSeverity::Info,
+            settings::DiagnosticSeverityContent::Hint => DiagnosticSeverity::Hint,
+        }
+    }
+}
+
 /// Determines the severity of the diagnostic that should be moved to.
 #[derive(PartialEq, PartialOrd, Clone, Copy, Debug, Eq, Deserialize, JsonSchema)]
 #[serde(rename_all = "snake_case")]
@@ -390,126 +272,90 @@ impl GoToDiagnosticSeverityFilter {
     }
 }
 
-#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
+#[derive(Copy, Clone, Debug)]
 pub struct GitSettings {
     /// Whether or not to show the git gutter.
     ///
     /// Default: tracked_files
-    pub git_gutter: Option<GitGutterSetting>,
+    pub git_gutter: settings::GitGutterSetting,
     /// Sets the debounce threshold (in milliseconds) after which changes are reflected in the git gutter.
     ///
     /// Default: null
-    pub gutter_debounce: Option<u64>,
+    pub gutter_debounce: u64,
     /// Whether or not to show git blame data inline in
     /// the currently focused line.
     ///
     /// Default: on
-    pub inline_blame: Option<InlineBlameSettings>,
+    pub inline_blame: InlineBlameSettings,
     /// Which information to show in the branch picker.
     ///
     /// Default: on
-    pub branch_picker: Option<BranchPickerSettings>,
+    pub branch_picker: BranchPickerSettings,
     /// How hunks are displayed visually in the editor.
     ///
     /// Default: staged_hollow
-    pub hunk_style: Option<GitHunkStyleSetting>,
-}
-
-impl GitSettings {
-    pub fn inline_blame_enabled(&self) -> bool {
-        #[allow(unknown_lints, clippy::manual_unwrap_or_default)]
-        match self.inline_blame {
-            Some(InlineBlameSettings { enabled, .. }) => enabled,
-            _ => false,
-        }
-    }
-
-    pub fn inline_blame_delay(&self) -> Option<Duration> {
-        match self.inline_blame {
-            Some(InlineBlameSettings { delay_ms, .. }) if delay_ms > 0 => {
-                Some(Duration::from_millis(delay_ms))
-            }
-            _ => None,
-        }
-    }
-
-    pub fn show_inline_commit_summary(&self) -> bool {
-        match self.inline_blame {
-            Some(InlineBlameSettings {
-                show_commit_summary,
-                ..
-            }) => show_commit_summary,
-            _ => false,
-        }
-    }
-}
-
-#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema)]
-#[serde(rename_all = "snake_case")]
-pub enum GitHunkStyleSetting {
-    /// Show unstaged hunks with a filled background and staged hunks hollow.
-    #[default]
-    StagedHollow,
-    /// Show unstaged hunks hollow and staged hunks with a filled background.
-    UnstagedHollow,
-}
-
-#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema)]
-#[serde(rename_all = "snake_case")]
-pub enum GitGutterSetting {
-    /// Show git gutter in tracked files.
-    #[default]
-    TrackedFiles,
-    /// Hide git gutter
-    Hide,
+    pub hunk_style: settings::GitHunkStyleSetting,
 }
 
-#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema)]
-#[serde(rename_all = "snake_case")]
+#[derive(Clone, Copy, Debug)]
 pub struct InlineBlameSettings {
     /// Whether or not to show git blame data inline in
     /// the currently focused line.
     ///
     /// Default: true
-    #[serde(default = "default_true")]
     pub enabled: bool,
     /// Whether to only show the inline blame information
     /// after a delay once the cursor stops moving.
     ///
     /// Default: 0
-    #[serde(default)]
-    pub delay_ms: u64,
+    pub delay_ms: std::time::Duration,
     /// The amount of padding between the end of the source line and the start
     /// of the inline blame in units of columns.
     ///
     /// Default: 7
-    #[serde(default = "default_inline_blame_padding")]
     pub padding: u32,
     /// The minimum column number to show the inline blame information at
     ///
     /// Default: 0
-    #[serde(default)]
     pub min_column: u32,
     /// Whether to show commit summary as part of the inline blame.
     ///
     /// Default: false
-    #[serde(default)]
     pub show_commit_summary: bool,
 }
 
-fn default_inline_blame_padding() -> u32 {
-    7
-}
+impl GitSettings {
+    // todo! remove
+    pub fn inline_blame_enabled(&self) -> bool {
+        self.inline_blame.enabled
+        // #[allow(unknown_lints, clippy::manual_unwrap_or_default)]
+        // match self.inline_blame {
+        //     Some(InlineBlameSettings { enabled, .. }) => enabled,
+        //     _ => false,
+        // }
+    }
 
-impl Default for InlineBlameSettings {
-    fn default() -> Self {
-        Self {
-            enabled: true,
-            delay_ms: 0,
-            padding: default_inline_blame_padding(),
-            min_column: 0,
-            show_commit_summary: false,
-        }
+    // todo! remove
+    pub fn inline_blame_delay(&self) -> Option<Duration> {
+        Some(self.inline_blame.delay_ms)
+        // match self.inline_blame {
+        //     Some(InlineBlameSettings { delay_ms, .. }) if delay_ms > 0 => {
+        //         Some(Duration::from_millis(delay_ms))
+        //     }
+        //     _ => None,
+        // }
+    }
+
+    // todo! remove
+    pub fn show_inline_commit_summary(&self) -> bool {
+        self.inline_blame.show_commit_summary
+        // match self.inline_blame {
+        //     Some(InlineBlameSettings {
+        //         show_commit_summary,
+        //         ..
+        //     }) => show_commit_summary,
+        //     _ => false,
+        // }
     }
 }
 
@@ -531,93 +377,263 @@ impl Default for BranchPickerSettings {
     }
 }
 
-#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema, Hash)]
-pub struct BinarySettings {
-    pub path: Option<String>,
-    pub arguments: Option<Vec<String>>,
-    pub env: Option<BTreeMap<String, String>>,
-    pub ignore_system_version: Option<bool>,
-}
+#[derive(Clone, Debug)]
+pub struct DiagnosticsSettings {
+    /// Whether to show the project diagnostics button in the status bar.
+    pub button: bool,
 
-#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema, Hash)]
-pub struct FetchSettings {
-    // Whether to consider pre-releases for fetching
-    pub pre_release: Option<bool>,
-}
+    /// Whether or not to include warning diagnostics.
+    pub include_warnings: bool,
 
-#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, Hash)]
-#[serde(rename_all = "snake_case")]
-pub struct LspSettings {
-    pub binary: Option<BinarySettings>,
-    pub initialization_options: Option<serde_json::Value>,
-    pub settings: Option<serde_json::Value>,
-    /// If the server supports sending tasks over LSP extensions,
-    /// this setting can be used to enable or disable them in Zed.
-    /// Default: true
-    #[serde(default = "default_true")]
-    pub enable_lsp_tasks: bool,
-    pub fetch: Option<FetchSettings>,
-}
+    /// Settings for using LSP pull diagnostics mechanism in Zed.
+    pub lsp_pull_diagnostics: LspPullDiagnosticsSettings,
 
-impl Default for LspSettings {
-    fn default() -> Self {
-        Self {
-            binary: None,
-            initialization_options: None,
-            settings: None,
-            enable_lsp_tasks: true,
-            fetch: None,
-        }
-    }
+    /// Settings for showing inline diagnostics.
+    pub inline: InlineDiagnosticsSettings,
 }
 
-#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
-pub struct SessionSettings {
-    /// Whether or not to restore unsaved buffers on restart.
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub struct InlineDiagnosticsSettings {
+    /// Whether or not to show inline diagnostics
     ///
-    /// If this is true, user won't be prompted whether to save/discard
-    /// dirty files when closing the application.
+    /// Default: false
+    pub enabled: bool,
+    /// Whether to only show the inline diagnostics after a delay after the
+    /// last editor event.
+    ///
+    /// Default: 150
+    pub update_debounce_ms: u64,
+    /// The amount of padding between the end of the source line and the start
+    /// of the inline diagnostic in units of columns.
+    ///
+    /// Default: 4
+    pub padding: u32,
+    /// The minimum column to display inline diagnostics. This setting can be
+    /// used to horizontally align inline diagnostics at some position. Lines
+    /// longer than this value will still push diagnostics further to the right.
+    ///
+    /// Default: 0
+    pub min_column: u32,
+
+    pub max_severity: DiagnosticSeverity,
+}
+
+#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
+pub struct LspPullDiagnosticsSettings {
+    /// Whether to pull for diagnostics or not.
     ///
     /// Default: true
-    pub restore_unsaved_buffers: bool,
+    pub enabled: bool,
+    /// Minimum time to wait before pulling diagnostics from the language server(s).
+    /// 0 turns the debounce off.
+    ///
+    /// Default: 50
+    pub debounce_ms: u64,
 }
 
-impl Default for SessionSettings {
-    fn default() -> Self {
+impl Settings for ProjectSettings {
+    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();
+        let inline_diagnostics = diagnostics.inline.as_ref().unwrap();
+
+        let git = content.git.as_ref().unwrap();
+        let git_settings = GitSettings {
+            git_gutter: git.git_gutter.unwrap(),
+            gutter_debounce: git.gutter_debounce.unwrap(),
+            inline_blame: {
+                let inline = git.inline_blame.unwrap();
+                InlineBlameSettings {
+                    enabled: inline.enabled.unwrap(),
+                    delay_ms: std::time::Duration::from_millis(inline.delay_ms.unwrap()),
+                    padding: inline.padding.unwrap(),
+                    min_column: inline.min_column.unwrap(),
+                    show_commit_summary: inline.show_commit_summary.unwrap(),
+                }
+            },
+            branch_picker: {
+                let branch_picker = git.branch_picker.unwrap();
+                BranchPickerSettings {
+                    show_author_name: branch_picker.show_author_name.unwrap(),
+                }
+            },
+            hunk_style: git.hunk_style.unwrap(),
+        };
         Self {
-            restore_unsaved_buffers: true,
+            context_servers: project
+                .context_servers
+                .clone()
+                .into_iter()
+                .map(|(key, value)| (key, value.into()))
+                .collect(),
+            lsp: project
+                .lsp
+                .clone()
+                .into_iter()
+                .map(|(key, value)| (LanguageServerName(key.into()), value.into()))
+                .collect(),
+            global_lsp_settings: GlobalLspSettings {
+                button: content.global_lsp_settings.unwrap().button.unwrap(),
+            },
+            dap: project
+                .dap
+                .clone()
+                .into_iter()
+                .map(|(key, value)| {
+                    (
+                        DebugAdapterName(key.into()),
+                        DapSettings {
+                            binary: value.binary.unwrap(),
+                            args: value.args,
+                        },
+                    )
+                })
+                .collect(),
+            diagnostics: DiagnosticsSettings {
+                button: diagnostics.button.unwrap(),
+                include_warnings: diagnostics.include_warnings.unwrap(),
+                lsp_pull_diagnostics: LspPullDiagnosticsSettings {
+                    enabled: lsp_pull_diagnostics.enabled.unwrap(),
+                    debounce_ms: lsp_pull_diagnostics.debounce_ms.unwrap(),
+                },
+                inline: InlineDiagnosticsSettings {
+                    enabled: inline_diagnostics.enabled.unwrap(),
+                    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(),
+                },
+            },
+            git: git_settings,
+            node: content.node.clone(),
+            load_direnv: project.load_direnv.unwrap(),
+            session: content.session.clone(),
         }
     }
-}
 
-impl Settings for ProjectSettings {
-    type FileContent = Self;
+    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().filter_map(|(key, value)| {
+                Some((
+                    DebugAdapterName(key.into()),
+                    DapSettings {
+                        binary: value.binary?,
+                        args: value.args,
+                    },
+                ))
+            }));
+        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);
+                self.diagnostics
+                    .inline
+                    .max_severity
+                    .merge_from(&inline.max_severity.map(Into::into));
+            }
 
-    fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> anyhow::Result<Self> {
-        sources.json_merge()
+            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);
+            self.git.gutter_debounce.merge_from(&git.gutter_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)),
+        );
     }
 
-    fn import_from_vscode(vscode: &settings::VsCodeSettings, current: &mut Self::FileContent) {
+    fn import_from_vscode(
+        vscode: &settings::VsCodeSettings,
+        current: &mut settings::SettingsContent,
+    ) {
         // this just sets the binary name instead of a full path so it relies on path lookup
         // resolving to the one you want
-        vscode.enum_setting(
-            "npm.packageManager",
-            &mut current.node.npm_path,
-            |s| match s {
-                v @ ("npm" | "yarn" | "bun" | "pnpm") => Some(v.to_owned()),
-                _ => None,
-            },
-        );
+        let npm_path = vscode.read_enum_setting("npm.packageManager", |s| match s {
+            v @ ("npm" | "yarn" | "bun" | "pnpm") => Some(v.to_owned()),
+            _ => None,
+        });
+        if npm_path.is_some() {
+            current.node.get_or_insert_default().npm_path = npm_path;
+        }
 
         if let Some(b) = vscode.read_bool("git.blame.editorDecoration.enabled") {
-            if let Some(blame) = current.git.inline_blame.as_mut() {
-                blame.enabled = b
-            } else {
-                current.git.inline_blame = Some(InlineBlameSettings {
-                    enabled: b,
-                    ..Default::default()
-                })
-            }
+            // todo! get_or_insert_default is risky considering defaults probably don't correspond
+            // to default.json,
+            // probably need to pass in "defaults" for this type as additional arg, and
+            // use unwrap on those keys
+            current
+                .git
+                .get_or_insert_default()
+                .inline_blame
+                .get_or_insert_default()
+                .enabled = Some(b);
         }
 
         #[derive(Deserialize)]
@@ -627,29 +643,27 @@ impl Settings for ProjectSettings {
             env: Option<HashMap<String, String>>,
             // note: we don't support envFile and type
         }
-        impl From<VsCodeContextServerCommand> for ContextServerCommand {
-            fn from(cmd: VsCodeContextServerCommand) -> Self {
-                Self {
-                    path: cmd.command,
-                    args: cmd.args.unwrap_or_default(),
-                    env: cmd.env,
-                    timeout: None,
-                }
-            }
-        }
         if let Some(mcp) = vscode.read_value("mcp").and_then(|v| v.as_object()) {
             current
+                .project
                 .context_servers
                 .extend(mcp.iter().filter_map(|(k, v)| {
                     Some((
                         k.clone().into(),
-                        ContextServerSettings::Custom {
+                        settings::ContextServerSettingsContent::Custom {
                             enabled: true,
                             command: serde_json::from_value::<VsCodeContextServerCommand>(
                                 v.clone(),
                             )
-                            .ok()?
-                            .into(),
+                            .ok()
+                            .map(|cmd| {
+                                settings::ContextServerCommand {
+                                    path: cmd.command,
+                                    args: cmd.args.unwrap_or_default(),
+                                    env: cmd.env,
+                                    timeout: None,
+                                }
+                            })?,
                         },
                     ))
                 }));
@@ -740,7 +754,7 @@ impl SettingsObserver {
                     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());
+                            user_settings = new_settings.clone();
                             upstream_client
                                 .send(proto::UpdateUserSettings {
                                     project_id: REMOTE_SERVER_PROJECT_ID,

crates/settings/src/settings_content.rs 🔗

@@ -1,19 +1,21 @@
 mod agent;
 mod language;
+mod project;
 mod terminal;
 mod theme;
 pub use agent::*;
 pub use language::*;
+pub use project::*;
 pub use terminal::*;
 pub use theme::*;
 
-use std::env;
-
 use collections::HashMap;
 use gpui::{App, SharedString};
 use release_channel::ReleaseChannel;
 use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
+use std::env;
+pub use util::serde::default_true;
 
 use crate::ActiveSettingsProfileName;
 
@@ -37,9 +39,18 @@ pub struct SettingsContent {
 
     pub debugger: Option<DebuggerSettingsContent>,
 
+    /// Configuration for Diagnostics-related features.
+    pub diagnostics: Option<DiagnosticsSettingsContent>,
+
+    /// 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>,
+
     /// Whether or not to enable Helix mode.
     ///
     /// Default: false
@@ -50,11 +61,16 @@ pub struct SettingsContent {
     /// Example: {"log": {"client": "warn"}}
     pub log: Option<HashMap<String, String>>,
 
+    /// Configuration for Node-related features
+    pub node: Option<NodeBinarySettings>,
+
     pub proxy: Option<String>,
 
     /// The URL of the Zed server to connect to.
     pub server_url: Option<String>,
 
+    /// Configuration for session-related features
+    pub session: Option<SessionSettings>,
     /// Control what info is collected by Zed.
     pub telemetry: Option<TelemetrySettingsContent>,
 
@@ -67,6 +83,9 @@ pub struct SettingsContent {
     ///
     /// Default: false
     pub vim_mode: Option<bool>,
+
+    // Settings related to calls in Zed
+    pub calls: Option<CallSettingsContent>,
 }
 
 impl SettingsContent {
@@ -147,17 +166,6 @@ pub enum BaseKeymapContent {
     None,
 }
 
-#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
-pub struct ProjectSettingsContent {
-    #[serde(flatten)]
-    pub all_languages: AllLanguageSettingsContent,
-
-    #[serde(flatten)]
-    pub worktree: WorktreeSettingsContent,
-
-    pub disable_ai: Option<bool>,
-}
-
 #[derive(Clone, PartialEq, Default, Serialize, Deserialize, JsonSchema, Debug)]
 pub struct TitleBarSettingsContent {
     /// Controls when the title bar is visible: "always" | "never" | "hide_in_full_screen".
@@ -246,42 +254,6 @@ pub enum GitHostingProviderKind {
     Bitbucket,
 }
 
-#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
-pub struct WorktreeSettingsContent {
-    /// The displayed name of this project. If not set, the root directory name
-    /// will be displayed.
-    ///
-    /// Default: none
-    pub project_name: Option<String>,
-
-    /// Completely ignore files matching globs from `file_scan_exclusions`. Overrides
-    /// `file_scan_inclusions`.
-    ///
-    /// Default: [
-    ///   "**/.git",
-    ///   "**/.svn",
-    ///   "**/.hg",
-    ///   "**/.jj",
-    ///   "**/CVS",
-    ///   "**/.DS_Store",
-    ///   "**/Thumbs.db",
-    ///   "**/.classpath",
-    ///   "**/.settings"
-    /// ]
-    pub file_scan_exclusions: Option<Vec<String>>,
-
-    /// Always include files that match these globs when scanning for files, even if they're
-    /// ignored by git. This setting is overridden by `file_scan_exclusions`.
-    /// Default: [
-    ///  ".env*",
-    ///  "docker-compose.*.yml",
-    /// ]
-    pub file_scan_inclusions: Option<Vec<String>>,
-
-    /// Treat the files matching these globs as `.env` files.
-    /// Default: [ "**/.env*" ]
-    pub private_files: Option<Vec<String>>,
-}
 /// Control what info is collected by Zed.
 #[derive(Default, Clone, Serialize, Deserialize, JsonSchema, Debug)]
 pub struct TelemetrySettingsContent {
@@ -348,3 +320,31 @@ pub enum DockPosition {
     Bottom,
     Right,
 }
+
+/// Settings for slash commands.
+#[derive(Deserialize, Serialize, Debug, Default, Clone, JsonSchema)]
+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)]
+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)]
+pub struct CallSettingsContent {
+    /// Whether the microphone should be muted when joining a channel or a call.
+    ///
+    /// Default: false
+    pub mute_on_join: Option<bool>,
+
+    /// Whether your current project should be shared when joining an empty channel.
+    ///
+    /// Default: false
+    pub share_on_join: Option<bool>,
+}

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

@@ -0,0 +1,384 @@
+use std::{path::PathBuf, sync::Arc};
+
+use collections::{BTreeMap, HashMap};
+use schemars::JsonSchema;
+use serde::{Deserialize, Serialize};
+use util::serde::default_true;
+
+use crate::AllLanguageSettingsContent;
+
+#[derive(Debug, PartialEq, Clone, Default, Serialize, Deserialize, JsonSchema)]
+pub struct ProjectSettingsContent {
+    #[serde(flatten)]
+    pub all_languages: AllLanguageSettingsContent,
+
+    #[serde(flatten)]
+    pub worktree: WorktreeSettingsContent,
+
+    /// Configuration for language servers.
+    ///
+    /// The following settings can be overridden for specific language servers:
+    /// - initialization_options
+    ///
+    /// To override settings for a language, add an entry for that language server's
+    /// name to the lsp value.
+    /// Default: null
+    #[serde(default)]
+    pub lsp: HashMap<Arc<str>, LspSettingsContent>,
+
+    /// Configuration for Debugger-related features
+    #[serde(default)]
+    pub dap: HashMap<Arc<str>, DapSettingsContent>,
+
+    /// Settings for context servers used for AI-related features.
+    #[serde(default)]
+    pub context_servers: HashMap<Arc<str>, ContextServerSettingsContent>,
+
+    /// Configuration for how direnv configuration should be loaded
+    pub load_direnv: Option<DirenvSettings>,
+
+    /// Settings for slash commands.
+    pub slash_commands: Option<SlashCommandSettings>,
+}
+
+#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
+pub struct WorktreeSettingsContent {
+    /// The displayed name of this project. If not set, the root directory name
+    /// will be displayed.
+    ///
+    /// Default: none
+    pub project_name: Option<String>,
+
+    /// Completely ignore files matching globs from `file_scan_exclusions`. Overrides
+    /// `file_scan_inclusions`.
+    ///
+    /// Default: [
+    ///   "**/.git",
+    ///   "**/.svn",
+    ///   "**/.hg",
+    ///   "**/.jj",
+    ///   "**/CVS",
+    ///   "**/.DS_Store",
+    ///   "**/Thumbs.db",
+    ///   "**/.classpath",
+    ///   "**/.settings"
+    /// ]
+    pub file_scan_exclusions: Option<Vec<String>>,
+
+    /// Always include files that match these globs when scanning for files, even if they're
+    /// ignored by git. This setting is overridden by `file_scan_exclusions`.
+    /// Default: [
+    ///  ".env*",
+    ///  "docker-compose.*.yml",
+    /// ]
+    pub file_scan_inclusions: Option<Vec<String>>,
+
+    /// Treat the files matching these globs as `.env` files.
+    /// Default: [ "**/.env*" ]
+    pub private_files: Option<Vec<String>>,
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, Hash)]
+#[serde(rename_all = "snake_case")]
+pub struct LspSettingsContent {
+    pub binary: Option<BinarySettings>,
+    pub initialization_options: Option<serde_json::Value>,
+    pub settings: Option<serde_json::Value>,
+    /// If the server supports sending tasks over LSP extensions,
+    /// this setting can be used to enable or disable them in Zed.
+    /// Default: true
+    #[serde(default = "default_true")]
+    pub enable_lsp_tasks: bool,
+    pub fetch: Option<FetchSettings>,
+}
+
+impl Default for LspSettingsContent {
+    fn default() -> Self {
+        Self {
+            binary: None,
+            initialization_options: None,
+            settings: None,
+            enable_lsp_tasks: true,
+            fetch: None,
+        }
+    }
+}
+
+#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema, Hash)]
+pub struct BinarySettings {
+    pub path: Option<String>,
+    pub arguments: Option<Vec<String>>,
+    pub env: Option<BTreeMap<String, String>>,
+    pub ignore_system_version: Option<bool>,
+}
+
+#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema, Hash)]
+pub struct FetchSettings {
+    // Whether to consider pre-releases for fetching
+    pub pre_release: Option<bool>,
+}
+
+/// Common language server settings.
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
+pub struct GlobalLspSettingsContent {
+    /// Whether to show the LSP servers button in the status bar.
+    ///
+    /// Default: `true`
+    pub button: Option<bool>,
+}
+
+// todo! binary is actually just required, shouldn't be an option
+#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub struct DapSettingsContent {
+    pub binary: Option<String>,
+    #[serde(default)]
+    pub args: Vec<String>,
+}
+
+#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
+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,
+}
+
+impl Default for SessionSettings {
+    fn default() -> Self {
+        Self {
+            restore_unsaved_buffers: true,
+        }
+    }
+}
+
+#[derive(Deserialize, Serialize, Clone, PartialEq, Eq, JsonSchema, Debug)]
+#[serde(tag = "source", rename_all = "snake_case")]
+pub enum ContextServerSettingsContent {
+    Custom {
+        /// Whether the context server is enabled.
+        #[serde(default = "default_true")]
+        enabled: bool,
+
+        #[serde(flatten)]
+        command: ContextServerCommand,
+    },
+    Extension {
+        /// Whether the context server is enabled.
+        #[serde(default = "default_true")]
+        enabled: bool,
+        /// The settings for this context server specified by the extension.
+        ///
+        /// Consult the documentation for the context server to see what settings
+        /// are supported.
+        settings: serde_json::Value,
+    },
+}
+
+#[derive(Deserialize, Serialize, Clone, PartialEq, Eq, JsonSchema)]
+pub struct ContextServerCommand {
+    #[serde(rename = "command")]
+    pub path: PathBuf,
+    pub args: Vec<String>,
+    pub env: Option<HashMap<String, String>>,
+    /// Timeout for tool calls in milliseconds. Defaults to 60000 (60 seconds) if not specified.
+    pub timeout: Option<u64>,
+}
+
+impl std::fmt::Debug for ContextServerCommand {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        let filtered_env = self.env.as_ref().map(|env| {
+            env.iter()
+                .map(|(k, v)| {
+                    (
+                        k,
+                        if util::redact::should_redact(k) {
+                            "[REDACTED]"
+                        } else {
+                            v
+                        },
+                    )
+                })
+                .collect::<Vec<_>>()
+        });
+
+        f.debug_struct("ContextServerCommand")
+            .field("path", &self.path)
+            .field("args", &self.args)
+            .field("env", &filtered_env)
+            .finish()
+    }
+}
+
+#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
+pub struct GitSettings {
+    /// Whether or not to show the git gutter.
+    ///
+    /// Default: tracked_files
+    pub git_gutter: Option<GitGutterSetting>,
+    /// Sets the debounce threshold (in milliseconds) after which changes are reflected in the git gutter.
+    ///
+    /// Default: null
+    pub gutter_debounce: Option<u64>,
+    /// Whether or not to show git blame data inline in
+    /// the currently focused line.
+    ///
+    /// Default: on
+    pub inline_blame: Option<InlineBlameSettings>,
+    /// Which information to show in the branch picker.
+    ///
+    /// Default: on
+    pub branch_picker: Option<BranchPickerSettingsContent>,
+    /// How hunks are displayed visually in the editor.
+    ///
+    /// Default: staged_hollow
+    pub hunk_style: Option<GitHunkStyleSetting>,
+}
+
+#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub enum GitGutterSetting {
+    /// Show git gutter in tracked files.
+    #[default]
+    TrackedFiles,
+    /// Hide git gutter
+    Hide,
+}
+
+#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub struct InlineBlameSettings {
+    /// Whether or not to show git blame data inline in
+    /// the currently focused line.
+    ///
+    /// Default: true
+    pub enabled: Option<bool>,
+    /// Whether to only show the inline blame information
+    /// after a delay once the cursor stops moving.
+    ///
+    /// Default: 0
+    pub delay_ms: Option<u64>,
+    /// The amount of padding between the end of the source line and the start
+    /// of the inline blame in units of columns.
+    ///
+    /// Default: 7
+    pub padding: Option<u32>,
+    /// The minimum column number to show the inline blame information at
+    ///
+    /// Default: 0
+    pub min_column: Option<u32>,
+    /// Whether to show commit summary as part of the inline blame.
+    ///
+    /// Default: false
+    pub show_commit_summary: Option<bool>,
+}
+
+#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub struct BranchPickerSettingsContent {
+    /// Whether to show author name as part of the commit information.
+    ///
+    /// Default: false
+    pub show_author_name: Option<bool>,
+}
+
+#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub enum GitHunkStyleSetting {
+    /// Show unstaged hunks with a filled background and staged hunks hollow.
+    #[default]
+    StagedHollow,
+    /// Show unstaged hunks hollow and staged hunks with a filled background.
+    UnstagedHollow,
+}
+
+#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
+pub struct DiagnosticsSettingsContent {
+    /// Whether to show the project diagnostics button in the status bar.
+    pub button: Option<bool>,
+
+    /// Whether or not to include warning diagnostics.
+    pub include_warnings: Option<bool>,
+
+    /// Settings for using LSP pull diagnostics mechanism in Zed.
+    pub lsp_pull_diagnostics: Option<LspPullDiagnosticsSettingsContent>,
+
+    /// Settings for showing inline diagnostics.
+    pub inline: Option<InlineDiagnosticsSettingsContent>,
+}
+
+#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
+pub struct LspPullDiagnosticsSettingsContent {
+    /// Whether to pull for diagnostics or not.
+    ///
+    /// Default: true
+    pub enabled: Option<bool>,
+    /// Minimum time to wait before pulling diagnostics from the language server(s).
+    /// 0 turns the debounce off.
+    ///
+    /// Default: 50
+    pub debounce_ms: Option<u64>,
+}
+
+#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize, JsonSchema, Eq)]
+pub struct InlineDiagnosticsSettingsContent {
+    /// Whether or not to show inline diagnostics
+    ///
+    /// Default: false
+    pub enabled: Option<bool>,
+    /// Whether to only show the inline diagnostics after a delay after the
+    /// last editor event.
+    ///
+    /// Default: 150
+    pub update_debounce_ms: Option<u64>,
+    /// The amount of padding between the end of the source line and the start
+    /// of the inline diagnostic in units of columns.
+    ///
+    /// Default: 4
+    pub padding: Option<u32>,
+    /// The minimum column to display inline diagnostics. This setting can be
+    /// used to horizontally align inline diagnostics at some position. Lines
+    /// longer than this value will still push diagnostics further to the right.
+    ///
+    /// Default: 0
+    pub min_column: Option<u32>,
+
+    pub max_severity: Option<DiagnosticSeverityContent>,
+}
+
+#[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: Option<bool>,
+}
+
+#[derive(Clone, PartialEq, Debug, Default, Serialize, Deserialize, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub enum DirenvSettings {
+    /// Load direnv configuration through a shell hook
+    ShellHook,
+    /// Load direnv configuration directly using `direnv export json`
+    #[default]
+    Direct,
+}
+
+#[derive(
+    Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize, JsonSchema,
+)]
+#[serde(rename_all = "snake_case")]
+pub enum DiagnosticSeverityContent {
+    // No diagnostics are shown.
+    Off,
+    Error,
+    Warning,
+    Info,
+    #[serde(alias = "all")]
+    Hint,
+}

crates/settings/src/vscode_import.rs 🔗

@@ -135,4 +135,9 @@ impl VsCodeSettings {
             *setting = Some(s)
         }
     }
+
+    // todo! replace enum_setting
+    pub fn read_enum_setting<T>(&self, key: &str, f: impl FnOnce(&str) -> Option<T>) -> Option<T> {
+        self.content.get(key).and_then(Value::as_str).and_then(f)
+    }
 }