settings ui: Create settings key trait (#37489)

Anthony Eid and Ben Kunkle created

This PR separates out the associated constant `KEY` from the `Settings`
trait into a new trait `SettingsKey`. This allows for the key trait to
be derived using attributes to specify the path so that the new
`SettingsUi` derive macro can use the same attributes to determine top
level settings paths thereby removing the need to duplicate the path in
both `Settings::KEY` and `#[settings_ui(path = "...")]`

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

Release Notes:

- N/A

---------

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

Change summary

Cargo.lock                                          |   2 
crates/agent_servers/src/settings.rs                |   7 
crates/agent_settings/src/agent_settings.rs         |   9 
crates/agent_ui/src/slash_command_settings.rs       |   7 
crates/audio/src/audio_settings.rs                  |   9 
crates/auto_update/src/auto_update.rs               |   7 
crates/call/src/call_settings.rs                    |   7 
crates/client/src/client.rs                         |  17 -
crates/collab_ui/src/panel_settings.rs              |  22 +-
crates/dap/src/debugger_settings.rs                 |   9 
crates/editor/src/editor_settings.rs                |   7 
crates/extension_host/src/extension_settings.rs     |   7 
crates/extensions_ui/src/extensions_ui.rs           |   2 
crates/file_finder/src/file_finder_settings.rs      |   7 
crates/git_hosting_providers/src/settings.rs        |   7 
crates/git_ui/src/git_panel_settings.rs             |   7 
crates/go_to_line/src/cursor_position.rs            |   7 
crates/image_viewer/src/image_viewer_settings.rs    |   7 
crates/journal/src/journal.rs                       |   7 
crates/language/src/language_settings.rs            |  10 
crates/language_models/src/settings.rs              |   9 
crates/onboarding/src/ai_setup_page.rs              |  10 
crates/onboarding/src/basics_page.rs                |   2 
crates/outline_panel/src/outline_panel_settings.rs  |   7 
crates/project/src/project.rs                       |  79 +++++---
crates/project/src/project_settings.rs              |   9 
crates/project_panel/src/project_panel_settings.rs  |   7 
crates/recent_projects/src/remote_connections.rs    |   7 
crates/repl/src/jupyter_settings.rs                 |   7 
crates/settings/src/base_keymap_setting.rs          |  19 +
crates/settings/src/settings.rs                     |   6 
crates/settings/src/settings_store.rs               |  97 +++++++--
crates/settings_ui_macros/src/settings_ui_macros.rs | 134 ++++++++++++++
crates/terminal/src/terminal_settings.rs            |   7 
crates/theme/src/settings.rs                        |   7 
crates/title_bar/src/title_bar_settings.rs          |  11 
crates/vim/src/test/vim_test_context.rs             |   8 
crates/vim/src/vim.rs                               |  11 
crates/vim_mode_setting/Cargo.toml                  |   2 
crates/vim_mode_setting/src/vim_mode_setting.rs     |  78 ++++++--
crates/workspace/src/item.rs                        |  12 
crates/workspace/src/workspace_settings.rs          |  12 
crates/worktree/src/worktree_settings.rs            |   7 
crates/zlog_settings/src/zlog_settings.rs           |  18 +
44 files changed, 474 insertions(+), 256 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -17995,6 +17995,8 @@ version = "0.1.0"
 dependencies = [
  "anyhow",
  "gpui",
+ "schemars",
+ "serde",
  "settings",
  "workspace-hack",
 ]

crates/agent_servers/src/settings.rs 🔗

@@ -6,13 +6,14 @@ use collections::HashMap;
 use gpui::{App, SharedString};
 use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
-use settings::{Settings, SettingsSources, SettingsUi};
+use settings::{Settings, SettingsKey, SettingsSources, SettingsUi};
 
 pub fn init(cx: &mut App) {
     AllAgentServersSettings::register(cx);
 }
 
-#[derive(Default, Deserialize, Serialize, Clone, JsonSchema, Debug, SettingsUi)]
+#[derive(Default, Deserialize, Serialize, Clone, JsonSchema, Debug, SettingsUi, SettingsKey)]
+#[settings_key(key = "agent_servers")]
 pub struct AllAgentServersSettings {
     pub gemini: Option<BuiltinAgentServerSettings>,
     pub claude: Option<CustomAgentServerSettings>,
@@ -75,8 +76,6 @@ pub struct CustomAgentServerSettings {
 }
 
 impl settings::Settings for AllAgentServersSettings {
-    const KEY: Option<&'static str> = Some("agent_servers");
-
     type FileContent = Self;
 
     fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> Result<Self> {

crates/agent_settings/src/agent_settings.rs 🔗

@@ -8,7 +8,7 @@ use gpui::{App, Pixels, SharedString};
 use language_model::LanguageModel;
 use schemars::{JsonSchema, json_schema};
 use serde::{Deserialize, Serialize};
-use settings::{Settings, SettingsSources, SettingsUi};
+use settings::{Settings, SettingsKey, SettingsSources, SettingsUi};
 use std::borrow::Cow;
 
 pub use crate::agent_profile::*;
@@ -223,7 +223,8 @@ impl AgentSettingsContent {
     }
 }
 
-#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug, Default, SettingsUi)]
+#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug, Default, SettingsUi, SettingsKey)]
+#[settings_key(key = "agent", fallback_key = "assistant")]
 pub struct AgentSettingsContent {
     /// Whether the Agent is enabled.
     ///
@@ -399,10 +400,6 @@ pub struct ContextServerPresetContent {
 }
 
 impl Settings for AgentSettings {
-    const KEY: Option<&'static str> = Some("agent");
-
-    const FALLBACK_KEY: Option<&'static str> = Some("assistant");
-
     const PRESERVED_KEYS: Option<&'static [&'static str]> = Some(&["version"]);
 
     type FileContent = AgentSettingsContent;

crates/agent_ui/src/slash_command_settings.rs 🔗

@@ -2,10 +2,11 @@ use anyhow::Result;
 use gpui::App;
 use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
-use settings::{Settings, SettingsSources, SettingsUi};
+use settings::{Settings, SettingsKey, SettingsSources, SettingsUi};
 
 /// Settings for slash commands.
-#[derive(Deserialize, Serialize, Debug, Default, Clone, JsonSchema, SettingsUi)]
+#[derive(Deserialize, Serialize, Debug, Default, Clone, JsonSchema, SettingsUi, SettingsKey)]
+#[settings_key(key = "slash_commands")]
 pub struct SlashCommandSettings {
     /// Settings for the `/cargo-workspace` slash command.
     #[serde(default)]
@@ -21,8 +22,6 @@ pub struct CargoWorkspaceCommandSettings {
 }
 
 impl Settings for SlashCommandSettings {
-    const KEY: Option<&'static str> = Some("slash_commands");
-
     type FileContent = Self;
 
     fn load(sources: SettingsSources<Self::FileContent>, _cx: &mut App) -> Result<Self> {

crates/audio/src/audio_settings.rs 🔗

@@ -2,9 +2,9 @@ use anyhow::Result;
 use gpui::App;
 use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
-use settings::{Settings, SettingsSources, SettingsUi};
+use settings::{Settings, SettingsKey, SettingsSources, SettingsUi};
 
-#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
+#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, SettingsUi)]
 pub struct AudioSettings {
     /// Opt into the new audio system.
     #[serde(rename = "experimental.rodio_audio", default)]
@@ -12,8 +12,9 @@ pub struct AudioSettings {
 }
 
 /// Configuration of audio in Zed.
-#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, SettingsUi)]
+#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, SettingsUi, SettingsKey)]
 #[serde(default)]
+#[settings_key(key = "audio")]
 pub struct AudioSettingsContent {
     /// Whether to use the experimental audio system
     #[serde(rename = "experimental.rodio_audio", default)]
@@ -21,8 +22,6 @@ pub struct AudioSettingsContent {
 }
 
 impl Settings for AudioSettings {
-    const KEY: Option<&'static str> = Some("audio");
-
     type FileContent = AudioSettingsContent;
 
     fn load(sources: SettingsSources<Self::FileContent>, _cx: &mut App) -> Result<Self> {

crates/auto_update/src/auto_update.rs 🔗

@@ -10,7 +10,7 @@ use paths::remote_servers_dir;
 use release_channel::{AppCommitSha, ReleaseChannel};
 use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
-use settings::{Settings, SettingsSources, SettingsStore, SettingsUi};
+use settings::{Settings, SettingsKey, SettingsSources, SettingsStore, SettingsUi};
 use smol::{fs, io::AsyncReadExt};
 use smol::{fs::File, process::Command};
 use std::{
@@ -118,13 +118,12 @@ struct AutoUpdateSetting(bool);
 /// Whether or not to automatically check for updates.
 ///
 /// Default: true
-#[derive(Clone, Copy, Default, JsonSchema, Deserialize, Serialize, SettingsUi)]
+#[derive(Clone, Copy, Default, JsonSchema, Deserialize, Serialize, SettingsUi, SettingsKey)]
 #[serde(transparent)]
+#[settings_key(key = "auto_update")]
 struct AutoUpdateSettingContent(bool);
 
 impl Settings for AutoUpdateSetting {
-    const KEY: Option<&'static str> = Some("auto_update");
-
     type FileContent = AutoUpdateSettingContent;
 
     fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> Result<Self> {

crates/call/src/call_settings.rs 🔗

@@ -2,7 +2,7 @@ use anyhow::Result;
 use gpui::App;
 use schemars::JsonSchema;
 use serde_derive::{Deserialize, Serialize};
-use settings::{Settings, SettingsSources, SettingsUi};
+use settings::{Settings, SettingsKey, SettingsSources, SettingsUi};
 
 #[derive(Deserialize, Debug)]
 pub struct CallSettings {
@@ -11,7 +11,8 @@ pub struct CallSettings {
 }
 
 /// Configuration of voice calls in Zed.
-#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, SettingsUi)]
+#[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.
     ///
@@ -25,8 +26,6 @@ pub struct CallSettingsContent {
 }
 
 impl Settings for CallSettings {
-    const KEY: Option<&'static str> = Some("calls");
-
     type FileContent = CallSettingsContent;
 
     fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> Result<Self> {

crates/client/src/client.rs 🔗

@@ -31,7 +31,7 @@ use release_channel::{AppVersion, ReleaseChannel};
 use rpc::proto::{AnyTypedEnvelope, EnvelopedMessage, PeerId, RequestMessage};
 use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
-use settings::{Settings, SettingsSources, SettingsUi};
+use settings::{Settings, SettingsKey, SettingsSources, SettingsUi};
 use std::{
     any::TypeId,
     convert::TryFrom,
@@ -96,7 +96,8 @@ actions!(
     ]
 );
 
-#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, SettingsUi)]
+#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, SettingsUi, SettingsKey)]
+#[settings_key(None)]
 pub struct ClientSettingsContent {
     server_url: Option<String>,
 }
@@ -107,8 +108,6 @@ pub struct ClientSettings {
 }
 
 impl Settings for ClientSettings {
-    const KEY: Option<&'static str> = None;
-
     type FileContent = ClientSettingsContent;
 
     fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> Result<Self> {
@@ -122,7 +121,8 @@ impl Settings for ClientSettings {
     fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) {}
 }
 
-#[derive(Default, Clone, Serialize, Deserialize, JsonSchema, SettingsUi)]
+#[derive(Default, Clone, Serialize, Deserialize, JsonSchema, SettingsUi, SettingsKey)]
+#[settings_key(None)]
 pub struct ProxySettingsContent {
     proxy: Option<String>,
 }
@@ -133,8 +133,6 @@ pub struct ProxySettings {
 }
 
 impl Settings for ProxySettings {
-    const KEY: Option<&'static str> = None;
-
     type FileContent = ProxySettingsContent;
 
     fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> Result<Self> {
@@ -527,7 +525,8 @@ pub struct TelemetrySettings {
 }
 
 /// Control what info is collected by Zed.
-#[derive(Default, Clone, Serialize, Deserialize, JsonSchema, Debug, SettingsUi)]
+#[derive(Default, Clone, Serialize, Deserialize, JsonSchema, Debug, SettingsUi, SettingsKey)]
+#[settings_key(key = "telemetry")]
 pub struct TelemetrySettingsContent {
     /// Send debug info like crash reports.
     ///
@@ -540,8 +539,6 @@ pub struct TelemetrySettingsContent {
 }
 
 impl settings::Settings for TelemetrySettings {
-    const KEY: Option<&'static str> = Some("telemetry");
-
     type FileContent = TelemetrySettingsContent;
 
     fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> Result<Self> {

crates/collab_ui/src/panel_settings.rs 🔗

@@ -1,7 +1,7 @@
 use gpui::Pixels;
 use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
-use settings::{Settings, SettingsSources, SettingsUi};
+use settings::{Settings, SettingsKey, SettingsSources, SettingsUi};
 use workspace::dock::DockPosition;
 
 #[derive(Deserialize, Debug)]
@@ -27,7 +27,8 @@ pub struct ChatPanelSettings {
     pub default_width: Pixels,
 }
 
-#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, SettingsUi)]
+#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, SettingsUi, SettingsKey)]
+#[settings_key(key = "chat_panel")]
 pub struct ChatPanelSettingsContent {
     /// When to show the panel button in the status bar.
     ///
@@ -43,14 +44,16 @@ pub struct ChatPanelSettingsContent {
     pub default_width: Option<f32>,
 }
 
-#[derive(Deserialize, Debug)]
+#[derive(Deserialize, Debug, SettingsKey)]
+#[settings_key(key = "notification_panel")]
 pub struct NotificationPanelSettings {
     pub button: bool,
     pub dock: DockPosition,
     pub default_width: Pixels,
 }
 
-#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, SettingsUi)]
+#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, SettingsUi, SettingsKey)]
+#[settings_key(key = "collaboration_panel")]
 pub struct PanelSettingsContent {
     /// Whether to show the panel button in the status bar.
     ///
@@ -66,7 +69,8 @@ pub struct PanelSettingsContent {
     pub default_width: Option<f32>,
 }
 
-#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, SettingsUi)]
+#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, SettingsUi, SettingsKey)]
+#[settings_key(key = "message_editor")]
 pub struct MessageEditorSettings {
     /// Whether to automatically replace emoji shortcodes with emoji characters.
     /// For example: typing `:wave:` gets replaced with `👋`.
@@ -76,8 +80,6 @@ pub struct MessageEditorSettings {
 }
 
 impl Settings for CollaborationPanelSettings {
-    const KEY: Option<&'static str> = Some("collaboration_panel");
-
     type FileContent = PanelSettingsContent;
 
     fn load(
@@ -91,8 +93,6 @@ impl Settings for CollaborationPanelSettings {
 }
 
 impl Settings for ChatPanelSettings {
-    const KEY: Option<&'static str> = Some("chat_panel");
-
     type FileContent = ChatPanelSettingsContent;
 
     fn load(
@@ -106,8 +106,6 @@ impl Settings for ChatPanelSettings {
 }
 
 impl Settings for NotificationPanelSettings {
-    const KEY: Option<&'static str> = Some("notification_panel");
-
     type FileContent = PanelSettingsContent;
 
     fn load(
@@ -121,8 +119,6 @@ impl Settings for NotificationPanelSettings {
 }
 
 impl Settings for MessageEditorSettings {
-    const KEY: Option<&'static str> = Some("message_editor");
-
     type FileContent = MessageEditorSettings;
 
     fn load(

crates/dap/src/debugger_settings.rs 🔗

@@ -2,7 +2,7 @@ use dap_types::SteppingGranularity;
 use gpui::{App, Global};
 use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
-use settings::{Settings, SettingsSources, SettingsUi};
+use settings::{Settings, SettingsKey, SettingsSources, SettingsUi};
 
 #[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq, SettingsUi)]
 #[serde(rename_all = "snake_case")]
@@ -12,11 +12,12 @@ pub enum DebugPanelDockPosition {
     Right,
 }
 
-#[derive(Serialize, Deserialize, JsonSchema, Clone, Copy, SettingsUi)]
+#[derive(Serialize, Deserialize, JsonSchema, Clone, Copy, SettingsUi, SettingsKey)]
 #[serde(default)]
 // todo(settings_ui) @ben: I'm pretty sure not having the fields be optional here is a bug,
 // it means the defaults will override previously set values if a single key is missing
-#[settings_ui(group = "Debugger", path = "debugger")]
+#[settings_ui(group = "Debugger")]
+#[settings_key(key = "debugger")]
 pub struct DebuggerSettings {
     /// Determines the stepping granularity.
     ///
@@ -64,8 +65,6 @@ impl Default for DebuggerSettings {
 }
 
 impl Settings for DebuggerSettings {
-    const KEY: Option<&'static str> = Some("debugger");
-
     type FileContent = Self;
 
     fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> anyhow::Result<Self> {

crates/editor/src/editor_settings.rs 🔗

@@ -6,7 +6,7 @@ use language::CursorShape;
 use project::project_settings::DiagnosticSeverity;
 use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
-use settings::{Settings, SettingsSources, SettingsUi, VsCodeSettings};
+use settings::{Settings, SettingsKey, SettingsSources, SettingsUi, VsCodeSettings};
 use util::serde::default_true;
 
 /// Imports from the VSCode settings at
@@ -431,8 +431,9 @@ pub enum SnippetSortOrder {
     None,
 }
 
-#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, SettingsUi)]
+#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, SettingsUi, SettingsKey)]
 #[settings_ui(group = "Editor")]
+#[settings_key(None)]
 pub struct EditorSettingsContent {
     /// Whether the cursor blinks in the editor.
     ///
@@ -777,8 +778,6 @@ impl EditorSettings {
 }
 
 impl Settings for EditorSettings {
-    const KEY: Option<&'static str> = None;
-
     type FileContent = EditorSettingsContent;
 
     fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> anyhow::Result<Self> {

crates/extension_host/src/extension_settings.rs 🔗

@@ -3,10 +3,11 @@ use collections::HashMap;
 use gpui::App;
 use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
-use settings::{Settings, SettingsSources, SettingsUi};
+use settings::{Settings, SettingsKey, SettingsSources, SettingsUi};
 use std::sync::Arc;
 
-#[derive(Deserialize, Serialize, Debug, Default, Clone, JsonSchema, SettingsUi)]
+#[derive(Deserialize, Serialize, Debug, Default, Clone, JsonSchema, SettingsUi, SettingsKey)]
+#[settings_key(None)]
 pub struct ExtensionSettings {
     /// The extensions that should be automatically installed by Zed.
     ///
@@ -38,8 +39,6 @@ impl ExtensionSettings {
 }
 
 impl Settings for ExtensionSettings {
-    const KEY: Option<&'static str> = None;
-
     type FileContent = Self;
 
     fn load(sources: SettingsSources<Self::FileContent>, _cx: &mut App) -> Result<Self> {

crates/extensions_ui/src/extensions_ui.rs 🔗

@@ -1345,7 +1345,7 @@ impl ExtensionsPage {
                             this.update_settings::<VimModeSetting>(
                                 selection,
                                 cx,
-                                |setting, value| *setting = Some(value),
+                                |setting, value| setting.vim_mode = Some(value),
                             );
                         }),
                     )),

crates/file_finder/src/file_finder_settings.rs 🔗

@@ -1,7 +1,7 @@
 use anyhow::Result;
 use schemars::JsonSchema;
 use serde_derive::{Deserialize, Serialize};
-use settings::{Settings, SettingsSources, SettingsUi};
+use settings::{Settings, SettingsKey, SettingsSources, SettingsUi};
 
 #[derive(Deserialize, Debug, Clone, Copy, PartialEq)]
 pub struct FileFinderSettings {
@@ -11,7 +11,8 @@ pub struct FileFinderSettings {
     pub include_ignored: Option<bool>,
 }
 
-#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, SettingsUi)]
+#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, SettingsUi, SettingsKey)]
+#[settings_key(key = "file_finder")]
 pub struct FileFinderSettingsContent {
     /// Whether to show file icons in the file finder.
     ///
@@ -42,8 +43,6 @@ pub struct FileFinderSettingsContent {
 }
 
 impl Settings for FileFinderSettings {
-    const KEY: Option<&'static str> = Some("file_finder");
-
     type FileContent = FileFinderSettingsContent;
 
     fn load(sources: SettingsSources<Self::FileContent>, _: &mut gpui::App) -> Result<Self> {

crates/git_hosting_providers/src/settings.rs 🔗

@@ -5,7 +5,7 @@ use git::GitHostingProviderRegistry;
 use gpui::App;
 use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
-use settings::{Settings, SettingsStore, SettingsUi};
+use settings::{Settings, SettingsKey, SettingsStore, SettingsUi};
 use url::Url;
 use util::ResultExt as _;
 
@@ -78,7 +78,8 @@ pub struct GitHostingProviderConfig {
     pub name: String,
 }
 
-#[derive(Default, Debug, Clone, Serialize, Deserialize, JsonSchema, SettingsUi)]
+#[derive(Default, Debug, Clone, Serialize, Deserialize, JsonSchema, SettingsUi, SettingsKey)]
+#[settings_key(None)]
 pub struct GitHostingProviderSettings {
     /// The list of custom Git hosting providers.
     #[serde(default)]
@@ -86,8 +87,6 @@ pub struct GitHostingProviderSettings {
 }
 
 impl Settings for GitHostingProviderSettings {
-    const KEY: Option<&'static str> = None;
-
     type FileContent = Self;
 
     fn load(sources: settings::SettingsSources<Self::FileContent>, _: &mut App) -> Result<Self> {

crates/git_ui/src/git_panel_settings.rs 🔗

@@ -2,7 +2,7 @@ use editor::ShowScrollbar;
 use gpui::Pixels;
 use schemars::JsonSchema;
 use serde_derive::{Deserialize, Serialize};
-use settings::{Settings, SettingsSources, SettingsUi};
+use settings::{Settings, SettingsKey, SettingsSources, SettingsUi};
 use workspace::dock::DockPosition;
 
 #[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
@@ -36,7 +36,8 @@ pub enum StatusStyle {
     LabelColor,
 }
 
-#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, SettingsUi)]
+#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, SettingsUi, SettingsKey)]
+#[settings_key(key = "git_panel")]
 pub struct GitPanelSettingsContent {
     /// Whether to show the panel button in the status bar.
     ///
@@ -90,8 +91,6 @@ pub struct GitPanelSettings {
 }
 
 impl Settings for GitPanelSettings {
-    const KEY: Option<&'static str> = Some("git_panel");
-
     type FileContent = GitPanelSettingsContent;
 
     fn load(

crates/go_to_line/src/cursor_position.rs 🔗

@@ -2,7 +2,7 @@ use editor::{Editor, EditorSettings, MultiBufferSnapshot};
 use gpui::{App, Entity, FocusHandle, Focusable, Subscription, Task, WeakEntity};
 use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
-use settings::{Settings, SettingsSources, SettingsUi};
+use settings::{Settings, SettingsKey, SettingsSources, SettingsUi};
 use std::{fmt::Write, num::NonZeroU32, time::Duration};
 use text::{Point, Selection};
 use ui::{
@@ -301,13 +301,12 @@ pub(crate) enum LineIndicatorFormat {
     Long,
 }
 
-#[derive(Clone, Copy, Default, JsonSchema, Deserialize, Serialize, SettingsUi)]
+#[derive(Clone, Copy, Default, JsonSchema, Deserialize, Serialize, SettingsUi, SettingsKey)]
 #[serde(transparent)]
+#[settings_key(key = "line_indicator_format")]
 pub(crate) struct LineIndicatorFormatContent(LineIndicatorFormat);
 
 impl Settings for LineIndicatorFormat {
-    const KEY: Option<&'static str> = Some("line_indicator_format");
-
     type FileContent = LineIndicatorFormatContent;
 
     fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> anyhow::Result<Self> {

crates/image_viewer/src/image_viewer_settings.rs 🔗

@@ -1,10 +1,11 @@
 use gpui::App;
 use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
-use settings::{Settings, SettingsSources, SettingsUi};
+use settings::{Settings, SettingsKey, SettingsSources, SettingsUi};
 
 /// The settings for the image viewer.
-#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Default, SettingsUi)]
+#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Default, SettingsUi, SettingsKey)]
+#[settings_key(key = "image_viewer")]
 pub struct ImageViewerSettings {
     /// The unit to use for displaying image file sizes.
     ///
@@ -24,8 +25,6 @@ pub enum ImageFileSizeUnit {
 }
 
 impl Settings for ImageViewerSettings {
-    const KEY: Option<&'static str> = Some("image_viewer");
-
     type FileContent = Self;
 
     fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> anyhow::Result<Self> {

crates/journal/src/journal.rs 🔗

@@ -5,7 +5,7 @@ use editor::{Editor, SelectionEffects};
 use gpui::{App, AppContext as _, Context, Window, actions};
 use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
-use settings::{Settings, SettingsSources, SettingsUi};
+use settings::{Settings, SettingsKey, SettingsSources, SettingsUi};
 use std::{
     fs::OpenOptions,
     path::{Path, PathBuf},
@@ -22,7 +22,8 @@ actions!(
 );
 
 /// Settings specific to journaling
-#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, SettingsUi)]
+#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, SettingsUi, SettingsKey)]
+#[settings_key(key = "journal")]
 pub struct JournalSettings {
     /// The path of the directory where journal entries are stored.
     ///
@@ -52,8 +53,6 @@ pub enum HourFormat {
 }
 
 impl settings::Settings for JournalSettings {
-    const KEY: Option<&'static str> = Some("journal");
-
     type FileContent = Self;
 
     fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> Result<Self> {

crates/language/src/language_settings.rs 🔗

@@ -17,7 +17,8 @@ use serde::{
 };
 
 use settings::{
-    ParameterizedJsonSchema, Settings, SettingsLocation, SettingsSources, SettingsStore, SettingsUi,
+    ParameterizedJsonSchema, Settings, SettingsKey, SettingsLocation, SettingsSources,
+    SettingsStore, SettingsUi,
 };
 use shellexpand;
 use std::{borrow::Cow, num::NonZeroU32, path::Path, slice, sync::Arc};
@@ -292,7 +293,10 @@ pub struct CopilotSettings {
 }
 
 /// The settings for all languages.
-#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema, SettingsUi)]
+#[derive(
+    Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema, SettingsUi, SettingsKey,
+)]
+#[settings_key(None)]
 pub struct AllLanguageSettingsContent {
     /// The settings for enabling/disabling features.
     #[serde(default)]
@@ -1213,8 +1217,6 @@ impl InlayHintKind {
 }
 
 impl settings::Settings for AllLanguageSettings {
-    const KEY: Option<&'static str> = None;
-
     type FileContent = AllLanguageSettingsContent;
 
     fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> Result<Self> {

crates/language_models/src/settings.rs 🔗

@@ -5,7 +5,7 @@ use collections::HashMap;
 use gpui::App;
 use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
-use settings::{Settings, SettingsSources, SettingsUi};
+use settings::{Settings, SettingsKey, SettingsSources, SettingsUi};
 
 use crate::provider::{
     self,
@@ -46,7 +46,10 @@ pub struct AllLanguageModelSettings {
     pub zed_dot_dev: ZedDotDevSettings,
 }
 
-#[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema, SettingsUi)]
+#[derive(
+    Default, Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema, SettingsUi, SettingsKey,
+)]
+#[settings_key(key = "language_models")]
 pub struct AllLanguageModelSettingsContent {
     pub anthropic: Option<AnthropicSettingsContent>,
     pub bedrock: Option<AmazonBedrockSettingsContent>,
@@ -145,8 +148,6 @@ pub struct OpenRouterSettingsContent {
 }
 
 impl settings::Settings for AllLanguageModelSettings {
-    const KEY: Option<&'static str> = Some("language_models");
-
     const PRESERVED_KEYS: Option<&'static [&'static str]> = Some(&["version"]);
 
     type FileContent = AllLanguageModelSettingsContent;

crates/onboarding/src/ai_setup_page.rs 🔗

@@ -264,13 +264,9 @@ pub(crate) fn render_ai_setup_page(
                     );
 
                     let fs = <dyn Fs>::global(cx);
-                    update_settings_file::<DisableAiSettings>(
-                        fs,
-                        cx,
-                        move |ai_settings: &mut Option<bool>, _| {
-                            *ai_settings = Some(enabled);
-                        },
-                    );
+                    update_settings_file::<DisableAiSettings>(fs, cx, move |ai_settings, _| {
+                        ai_settings.disable_ai = Some(enabled);
+                    });
                 },
             )
             .tab_index({

crates/onboarding/src/basics_page.rs 🔗

@@ -388,7 +388,7 @@ fn render_vim_mode_switch(tab_index: &mut isize, cx: &mut App) -> impl IntoEleme
                     }
                 };
                 update_settings_file::<VimModeSetting>(fs.clone(), cx, move |setting, _| {
-                    *setting = Some(vim_mode);
+                    setting.vim_mode = Some(vim_mode);
                 });
 
                 telemetry::event!(

crates/outline_panel/src/outline_panel_settings.rs 🔗

@@ -2,7 +2,7 @@ use editor::ShowScrollbar;
 use gpui::Pixels;
 use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
-use settings::{Settings, SettingsSources, SettingsUi};
+use settings::{Settings, SettingsKey, SettingsSources, SettingsUi};
 
 #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Copy, PartialEq)]
 #[serde(rename_all = "snake_case")]
@@ -61,7 +61,8 @@ pub struct IndentGuidesSettingsContent {
     pub show: Option<ShowIndentGuides>,
 }
 
-#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, SettingsUi)]
+#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, SettingsUi, SettingsKey)]
+#[settings_key(key = "outline_panel")]
 pub struct OutlinePanelSettingsContent {
     /// Whether to show the outline panel button in the status bar.
     ///
@@ -116,8 +117,6 @@ pub struct OutlinePanelSettingsContent {
 }
 
 impl Settings for OutlinePanelSettings {
-    const KEY: Option<&'static str> = Some("outline_panel");
-
     type FileContent = OutlinePanelSettingsContent;
 
     fn load(

crates/project/src/project.rs 🔗

@@ -28,6 +28,7 @@ use context_server_store::ContextServerStore;
 pub use environment::{EnvironmentErrorMessage, ProjectEnvironmentEvent};
 use git::repository::get_git_committer;
 use git_store::{Repository, RepositoryId};
+use schemars::JsonSchema;
 pub mod search_history;
 mod yarn;
 
@@ -94,7 +95,10 @@ use rpc::{
 };
 use search::{SearchInputKind, SearchQuery, SearchResult};
 use search_history::SearchHistory;
-use settings::{InvalidSettingsError, Settings, SettingsLocation, SettingsSources, SettingsStore};
+use settings::{
+    InvalidSettingsError, Settings, SettingsKey, SettingsLocation, SettingsSources, SettingsStore,
+    SettingsUi,
+};
 use smol::channel::Receiver;
 use snippet::Snippet;
 use snippet_provider::SnippetProvider;
@@ -968,10 +972,26 @@ pub struct DisableAiSettings {
     pub disable_ai: bool,
 }
 
-impl settings::Settings for DisableAiSettings {
-    const KEY: Option<&'static str> = Some("disable_ai");
+#[derive(
+    Copy,
+    Clone,
+    PartialEq,
+    Eq,
+    Debug,
+    Default,
+    serde::Serialize,
+    serde::Deserialize,
+    SettingsUi,
+    SettingsKey,
+    JsonSchema,
+)]
+#[settings_key(None)]
+pub struct DisableAiSettingContent {
+    pub disable_ai: Option<bool>,
+}
 
-    type FileContent = Option<bool>;
+impl settings::Settings for DisableAiSettings {
+    type FileContent = DisableAiSettingContent;
 
     fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> Result<Self> {
         // For security reasons, settings can only make AI restrictions MORE strict, not less.
@@ -984,7 +1004,7 @@ impl settings::Settings for DisableAiSettings {
             .iter()
             .chain(sources.user.iter())
             .chain(sources.server.iter())
-            .any(|disabled| **disabled == Some(true));
+            .any(|disabled| disabled.disable_ai == Some(true));
 
         Ok(Self { disable_ai })
     }
@@ -5550,10 +5570,15 @@ mod disable_ai_settings_tests {
 
     #[gpui::test]
     async fn test_disable_ai_settings_security(cx: &mut TestAppContext) {
+        fn disable_setting(value: Option<bool>) -> DisableAiSettingContent {
+            DisableAiSettingContent { disable_ai: value }
+        }
         cx.update(|cx| {
             // Test 1: Default is false (AI enabled)
             let sources = SettingsSources {
-                default: &Some(false),
+                default: &DisableAiSettingContent {
+                    disable_ai: Some(false),
+                },
                 global: None,
                 extensions: None,
                 user: None,
@@ -5567,10 +5592,10 @@ mod disable_ai_settings_tests {
             assert!(!settings.disable_ai, "Default should allow AI");
 
             // Test 2: Global true, local false -> still disabled (local cannot re-enable)
-            let global_true = Some(true);
-            let local_false = Some(false);
+            let global_true = disable_setting(Some(true));
+            let local_false = disable_setting(Some(false));
             let sources = SettingsSources {
-                default: &Some(false),
+                default: &disable_setting(Some(false)),
                 global: None,
                 extensions: None,
                 user: Some(&global_true),
@@ -5587,10 +5612,10 @@ mod disable_ai_settings_tests {
             );
 
             // Test 3: Global false, local true -> disabled (local can make more restrictive)
-            let global_false = Some(false);
-            let local_true = Some(true);
+            let global_false = disable_setting(Some(false));
+            let local_true = disable_setting(Some(true));
             let sources = SettingsSources {
-                default: &Some(false),
+                default: &disable_setting(Some(false)),
                 global: None,
                 extensions: None,
                 user: Some(&global_false),
@@ -5604,10 +5629,10 @@ mod disable_ai_settings_tests {
             assert!(settings.disable_ai, "Local true can override global false");
 
             // Test 4: Server can only make more restrictive (set to true)
-            let user_false = Some(false);
-            let server_true = Some(true);
+            let user_false = disable_setting(Some(false));
+            let server_true = disable_setting(Some(true));
             let sources = SettingsSources {
-                default: &Some(false),
+                default: &disable_setting(Some(false)),
                 global: None,
                 extensions: None,
                 user: Some(&user_false),
@@ -5624,10 +5649,10 @@ mod disable_ai_settings_tests {
             );
 
             // Test 5: Server false cannot override user true
-            let user_true = Some(true);
-            let server_false = Some(false);
+            let user_true = disable_setting(Some(true));
+            let server_false = disable_setting(Some(false));
             let sources = SettingsSources {
-                default: &Some(false),
+                default: &disable_setting(Some(false)),
                 global: None,
                 extensions: None,
                 user: Some(&user_true),
@@ -5644,12 +5669,12 @@ mod disable_ai_settings_tests {
             );
 
             // Test 6: Multiple local settings, any true disables AI
-            let global_false = Some(false);
-            let local_false3 = Some(false);
-            let local_true2 = Some(true);
-            let local_false4 = Some(false);
+            let global_false = disable_setting(Some(false));
+            let local_false3 = disable_setting(Some(false));
+            let local_true2 = disable_setting(Some(true));
+            let local_false4 = disable_setting(Some(false));
             let sources = SettingsSources {
-                default: &Some(false),
+                default: &disable_setting(Some(false)),
                 global: None,
                 extensions: None,
                 user: Some(&global_false),
@@ -5663,11 +5688,11 @@ mod disable_ai_settings_tests {
             assert!(settings.disable_ai, "Any local true should disable AI");
 
             // Test 7: All three sources can independently disable AI
-            let user_false2 = Some(false);
-            let server_false2 = Some(false);
-            let local_true3 = Some(true);
+            let user_false2 = disable_setting(Some(false));
+            let server_false2 = disable_setting(Some(false));
+            let local_true3 = disable_setting(Some(true));
             let sources = SettingsSources {
-                default: &Some(false),
+                default: &disable_setting(Some(false)),
                 global: None,
                 extensions: None,
                 user: Some(&user_false2),

crates/project/src/project_settings.rs 🔗

@@ -18,8 +18,8 @@ use rpc::{
 use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
 use settings::{
-    InvalidSettingsError, LocalSettingsKind, Settings, SettingsLocation, SettingsSources,
-    SettingsStore, SettingsUi, parse_json_with_comments, watch_config_file,
+    InvalidSettingsError, LocalSettingsKind, Settings, SettingsKey, SettingsLocation,
+    SettingsSources, SettingsStore, SettingsUi, parse_json_with_comments, watch_config_file,
 };
 use std::{
     collections::BTreeMap,
@@ -36,7 +36,8 @@ use crate::{
     worktree_store::{WorktreeStore, WorktreeStoreEvent},
 };
 
-#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema, SettingsUi)]
+#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema, SettingsUi, SettingsKey)]
+#[settings_key(None)]
 pub struct ProjectSettings {
     /// Configuration for language servers.
     ///
@@ -568,8 +569,6 @@ impl Default for SessionSettings {
 }
 
 impl Settings for ProjectSettings {
-    const KEY: Option<&'static str> = None;
-
     type FileContent = Self;
 
     fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> anyhow::Result<Self> {

crates/project_panel/src/project_panel_settings.rs 🔗

@@ -2,7 +2,7 @@ use editor::ShowScrollbar;
 use gpui::Pixels;
 use schemars::JsonSchema;
 use serde_derive::{Deserialize, Serialize};
-use settings::{Settings, SettingsSources, SettingsUi};
+use settings::{Settings, SettingsKey, SettingsSources, SettingsUi};
 
 #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Copy, PartialEq)]
 #[serde(rename_all = "snake_case")]
@@ -92,7 +92,8 @@ pub enum ShowDiagnostics {
     All,
 }
 
-#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, SettingsUi)]
+#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, SettingsUi, SettingsKey)]
+#[settings_key(key = "project_panel")]
 pub struct ProjectPanelSettingsContent {
     /// Whether to show the project panel button in the status bar.
     ///
@@ -168,8 +169,6 @@ pub struct ProjectPanelSettingsContent {
 }
 
 impl Settings for ProjectPanelSettings {
-    const KEY: Option<&'static str> = Some("project_panel");
-
     type FileContent = ProjectPanelSettingsContent;
 
     fn load(

crates/recent_projects/src/remote_connections.rs 🔗

@@ -21,7 +21,7 @@ use remote::{
 };
 use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
-use settings::{Settings, SettingsSources, SettingsUi};
+use settings::{Settings, SettingsKey, SettingsSources, SettingsUi};
 use theme::ThemeSettings;
 use ui::{
     ActiveTheme, Color, CommonAnimationExt, Context, Icon, IconName, IconSize, InteractiveElement,
@@ -121,15 +121,14 @@ pub struct SshProject {
     pub paths: Vec<String>,
 }
 
-#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, SettingsUi)]
+#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, SettingsUi, SettingsKey)]
+#[settings_key(None)]
 pub struct RemoteSettingsContent {
     pub ssh_connections: Option<Vec<SshConnection>>,
     pub read_ssh_config: Option<bool>,
 }
 
 impl Settings for SshSettings {
-    const KEY: Option<&'static str> = None;
-
     type FileContent = RemoteSettingsContent;
 
     fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> Result<Self> {

crates/repl/src/jupyter_settings.rs 🔗

@@ -4,7 +4,7 @@ use editor::EditorSettings;
 use gpui::App;
 use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
-use settings::{Settings, SettingsSources, SettingsUi};
+use settings::{Settings, SettingsKey, SettingsSources, SettingsUi};
 
 #[derive(Debug, Default)]
 pub struct JupyterSettings {
@@ -20,7 +20,8 @@ impl JupyterSettings {
     }
 }
 
-#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug, SettingsUi)]
+#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug, SettingsUi, SettingsKey)]
+#[settings_key(key = "jupyter")]
 pub struct JupyterSettingsContent {
     /// Default kernels to select for each language.
     ///
@@ -37,8 +38,6 @@ impl Default for JupyterSettingsContent {
 }
 
 impl Settings for JupyterSettings {
-    const KEY: Option<&'static str> = Some("jupyter");
-
     type FileContent = JupyterSettingsContent;
 
     fn load(

crates/settings/src/base_keymap_setting.rs 🔗

@@ -1,10 +1,10 @@
 use std::fmt::{Display, Formatter};
 
-use crate as settings;
+use crate::{self as settings};
 use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
 use settings::{Settings, SettingsSources, VsCodeSettings};
-use settings_ui_macros::SettingsUi;
+use settings_ui_macros::{SettingsKey, SettingsUi};
 
 /// Base key bindings scheme. Base keymaps can be overridden with user keymaps.
 ///
@@ -101,16 +101,25 @@ impl BaseKeymap {
 }
 
 #[derive(
-    Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Default, SettingsUi,
+    Copy,
+    Clone,
+    Debug,
+    Serialize,
+    Deserialize,
+    JsonSchema,
+    PartialEq,
+    Eq,
+    Default,
+    SettingsUi,
+    SettingsKey,
 )]
 // extracted so that it can be an option, and still work with derive(SettingsUi)
+#[settings_key(None)]
 pub struct BaseKeymapSetting {
     pub base_keymap: Option<BaseKeymap>,
 }
 
 impl Settings for BaseKeymap {
-    const KEY: Option<&'static str> = None;
-
     type FileContent = BaseKeymapSetting;
 
     fn load(

crates/settings/src/settings.rs 🔗

@@ -21,12 +21,12 @@ pub use keymap_file::{
 pub use settings_file::*;
 pub use settings_json::*;
 pub use settings_store::{
-    InvalidSettingsError, LocalSettingsKind, Settings, SettingsLocation, SettingsSources,
-    SettingsStore,
+    InvalidSettingsError, LocalSettingsKind, Settings, SettingsKey, SettingsLocation,
+    SettingsSources, SettingsStore,
 };
 pub use settings_ui_core::*;
 // Re-export the derive macro
-pub use settings_ui_macros::SettingsUi;
+pub use settings_ui_macros::{SettingsKey, SettingsUi};
 pub use vscode_import::{VsCodeSettings, VsCodeSettingsSource};
 
 #[derive(Clone, Debug, PartialEq)]

crates/settings/src/settings_store.rs 🔗

@@ -36,17 +36,19 @@ use crate::{
     settings_ui_core::SettingsUi, update_value_in_json_text,
 };
 
-/// A value that can be defined as a user setting.
-///
-/// Settings can be loaded from a combination of multiple JSON files.
-pub trait Settings: 'static + Send + Sync {
+pub trait SettingsKey: 'static + Send + Sync {
     /// The name of a key within the JSON file from which this setting should
     /// be deserialized. If this is `None`, then the setting will be deserialized
     /// from the root object.
     const KEY: Option<&'static str>;
 
     const FALLBACK_KEY: Option<&'static str> = None;
+}
 
+/// A value that can be defined as a user setting.
+///
+/// Settings can be loaded from a combination of multiple JSON files.
+pub trait Settings: 'static + Send + Sync {
     /// The name of the keys in the [`FileContent`](Self::FileContent) that should
     /// always be written to a settings file, even if their value matches the default
     /// value.
@@ -57,8 +59,19 @@ pub trait Settings: 'static + Send + Sync {
     const PRESERVED_KEYS: Option<&'static [&'static str]> = None;
 
     /// The type that is stored in an individual JSON file.
-    type FileContent: Clone + Default + Serialize + DeserializeOwned + JsonSchema + SettingsUi;
-
+    type FileContent: Clone
+        + Default
+        + Serialize
+        + DeserializeOwned
+        + JsonSchema
+        + SettingsUi
+        + SettingsKey;
+
+    /*
+     *  let path = Settings
+     *
+     *
+     */
     /// The logic for combining together values from one or more JSON files into the
     /// final value for this setting.
     ///
@@ -71,7 +84,7 @@ pub trait Settings: 'static + Send + Sync {
         Self: Sized;
 
     fn missing_default() -> anyhow::Error {
-        anyhow::anyhow!("missing default")
+        anyhow::anyhow!("missing default for: {}", std::any::type_name::<Self>())
     }
 
     /// Use [the helpers in the vscode_import module](crate::vscode_import) to apply known
@@ -1393,7 +1406,7 @@ impl Debug for SettingsStore {
 
 impl<T: Settings> AnySettingValue for SettingValue<T> {
     fn key(&self) -> Option<&'static str> {
-        T::KEY
+        T::FileContent::KEY
     }
 
     fn setting_type_name(&self) -> &'static str {
@@ -1445,16 +1458,21 @@ impl<T: Settings> AnySettingValue for SettingValue<T> {
         mut json: &Value,
     ) -> (Option<&'static str>, Result<DeserializedSetting>) {
         let mut key = None;
-        if let Some(k) = T::KEY {
+        if let Some(k) = T::FileContent::KEY {
             if let Some(value) = json.get(k) {
                 json = value;
                 key = Some(k);
-            } else if let Some((k, value)) = T::FALLBACK_KEY.and_then(|k| Some((k, json.get(k)?))) {
+            } else if let Some((k, value)) =
+                T::FileContent::FALLBACK_KEY.and_then(|k| Some((k, json.get(k)?)))
+            {
                 json = value;
                 key = Some(k);
             } else {
                 let value = T::FileContent::default();
-                return (T::KEY, Ok(DeserializedSetting(Box::new(value))));
+                return (
+                    T::FileContent::KEY,
+                    Ok(DeserializedSetting(Box::new(value))),
+                );
             }
         }
         let value = serde_path_to_error::deserialize::<_, T::FileContent>(json)
@@ -1498,6 +1516,7 @@ impl<T: Settings> AnySettingValue for SettingValue<T> {
                 }
             }
         }
+
         self.global_value
             .as_ref()
             .unwrap_or_else(|| panic!("no default value for setting {}", self.setting_type_name()))
@@ -1570,7 +1589,7 @@ mod tests {
     // This is so the SettingsUi macro can still work properly
     use crate as settings;
     use serde_derive::Deserialize;
-    use settings_ui_macros::SettingsUi;
+    use settings_ui_macros::{SettingsKey, SettingsUi};
     use unindent::Unindent;
 
     #[gpui::test]
@@ -2120,7 +2139,8 @@ mod tests {
         staff: bool,
     }
 
-    #[derive(Default, Clone, Serialize, Deserialize, JsonSchema, SettingsUi)]
+    #[derive(Default, Clone, Serialize, Deserialize, JsonSchema, SettingsUi, SettingsKey)]
+    #[settings_key(key = "user")]
     struct UserSettingsContent {
         name: Option<String>,
         age: Option<u32>,
@@ -2128,7 +2148,6 @@ mod tests {
     }
 
     impl Settings for UserSettings {
-        const KEY: Option<&'static str> = Some("user");
         type FileContent = UserSettingsContent;
 
         fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> Result<Self> {
@@ -2143,12 +2162,37 @@ mod tests {
     #[derive(Debug, Deserialize, PartialEq)]
     struct TurboSetting(bool);
 
+    #[derive(
+        Copy,
+        Clone,
+        PartialEq,
+        Eq,
+        Debug,
+        Default,
+        serde::Serialize,
+        serde::Deserialize,
+        SettingsUi,
+        SettingsKey,
+        JsonSchema,
+    )]
+    #[serde(default)]
+    #[settings_key(None)]
+    pub struct TurboSettingContent {
+        turbo: Option<bool>,
+    }
+
     impl Settings for TurboSetting {
-        const KEY: Option<&'static str> = Some("turbo");
-        type FileContent = bool;
+        type FileContent = TurboSettingContent;
 
         fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> Result<Self> {
-            sources.json_merge()
+            Ok(Self(
+                sources
+                    .user
+                    .or(sources.server)
+                    .unwrap_or(sources.default)
+                    .turbo
+                    .unwrap_or_default(),
+            ))
         }
 
         fn import_from_vscode(_vscode: &VsCodeSettings, _current: &mut Self::FileContent) {}
@@ -2162,15 +2206,14 @@ mod tests {
         key2: String,
     }
 
-    #[derive(Clone, Default, Serialize, Deserialize, JsonSchema, SettingsUi)]
+    #[derive(Clone, Default, Serialize, Deserialize, JsonSchema, SettingsUi, SettingsKey)]
+    #[settings_key(None)]
     struct MultiKeySettingsJson {
         key1: Option<String>,
         key2: Option<String>,
     }
 
     impl Settings for MultiKeySettings {
-        const KEY: Option<&'static str> = None;
-
         type FileContent = MultiKeySettingsJson;
 
         fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> Result<Self> {
@@ -2200,15 +2243,16 @@ mod tests {
         Hour24,
     }
 
-    #[derive(Clone, Default, Debug, Serialize, Deserialize, JsonSchema, SettingsUi)]
+    #[derive(
+        Clone, Default, Debug, Serialize, Deserialize, JsonSchema, SettingsUi, SettingsKey,
+    )]
+    #[settings_key(key = "journal")]
     struct JournalSettingsJson {
         pub path: Option<String>,
         pub hour_format: Option<HourFormat>,
     }
 
     impl Settings for JournalSettings {
-        const KEY: Option<&'static str> = Some("journal");
-
         type FileContent = JournalSettingsJson;
 
         fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> Result<Self> {
@@ -2288,7 +2332,10 @@ mod tests {
         );
     }
 
-    #[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, SettingsUi)]
+    #[derive(
+        Clone, Debug, Default, Serialize, Deserialize, JsonSchema, SettingsUi, SettingsKey,
+    )]
+    #[settings_key(None)]
     struct LanguageSettings {
         #[serde(default)]
         languages: HashMap<String, LanguageSettingEntry>,
@@ -2301,8 +2348,6 @@ mod tests {
     }
 
     impl Settings for LanguageSettings {
-        const KEY: Option<&'static str> = None;
-
         type FileContent = Self;
 
         fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> Result<Self> {

crates/settings_ui_macros/src/settings_ui_macros.rs 🔗

@@ -43,10 +43,9 @@ pub fn derive_settings_ui(input: proc_macro::TokenStream) -> proc_macro::TokenSt
                     let lit: LitStr = meta.input.parse()?;
                     group_name = Some(lit.value());
                 } else if meta.path.is_ident("path") {
-                    // todo(settings_ui) try get KEY from Settings if possible, and once we do,
-                    // if can get key from settings, throw error if path also passed
+                    // todo(settings_ui) rely entirely on settings_key, remove path attribute
                     if path_name.is_some() {
-                        return Err(meta.error("Only one 'path' can be specified"));
+                        return Err(meta.error("Only one 'path' can be specified, either with `path` in `settings_ui` or with `settings_key`"));
                     }
                     meta.input.parse::<Token![=]>()?;
                     let lit: LitStr = meta.input.parse()?;
@@ -55,6 +54,12 @@ pub fn derive_settings_ui(input: proc_macro::TokenStream) -> proc_macro::TokenSt
                 Ok(())
             })
             .unwrap_or_else(|e| panic!("in #[settings_ui] attribute: {}", e));
+        } else if let Some(settings_key) = parse_setting_key_attr(attr) {
+            // todo(settings_ui) either remove fallback key or handle it here
+            if path_name.is_some() && settings_key.key.is_some() {
+                panic!("Both 'path' and 'settings_key' are specified. Must specify only one");
+            }
+            path_name = settings_key.key;
         }
     }
 
@@ -212,3 +217,126 @@ fn generate_ui_item_body(
         },
     }
 }
+
+struct SettingsKey {
+    key: Option<String>,
+    fallback_key: Option<String>,
+}
+
+fn parse_setting_key_attr(attr: &syn::Attribute) -> Option<SettingsKey> {
+    if !attr.path().is_ident("settings_key") {
+        return None;
+    }
+
+    let mut settings_key = SettingsKey {
+        key: None,
+        fallback_key: None,
+    };
+
+    let mut found_none = false;
+
+    attr.parse_nested_meta(|meta| {
+        if meta.path.is_ident("None") {
+            found_none = true;
+        } else if meta.path.is_ident("key") {
+            if settings_key.key.is_some() {
+                return Err(meta.error("Only one 'group' path can be specified"));
+            }
+            meta.input.parse::<Token![=]>()?;
+            let lit: LitStr = meta.input.parse()?;
+            settings_key.key = Some(lit.value());
+        } else if meta.path.is_ident("fallback_key") {
+            if found_none {
+                return Err(meta.error("Cannot specify 'fallback_key' and 'None'"));
+            }
+
+            if settings_key.fallback_key.is_some() {
+                return Err(meta.error("Only one 'fallback_key' can be specified"));
+            }
+
+            meta.input.parse::<Token![=]>()?;
+            let lit: LitStr = meta.input.parse()?;
+            settings_key.fallback_key = Some(lit.value());
+        }
+        Ok(())
+    })
+    .unwrap_or_else(|e| panic!("in #[settings_key] attribute: {}", e));
+
+    if found_none && settings_key.fallback_key.is_some() {
+        panic!("in #[settings_key] attribute: Cannot specify 'None' and 'fallback_key'");
+    }
+    if found_none && settings_key.key.is_some() {
+        panic!("in #[settings_key] attribute: Cannot specify 'None' and 'key'");
+    }
+    if !found_none && settings_key.key.is_none() {
+        panic!("in #[settings_key] attribute: 'key' must be specified");
+    }
+
+    return Some(settings_key);
+}
+
+#[proc_macro_derive(SettingsKey, attributes(settings_key))]
+pub fn derive_settings_key(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
+    let input = parse_macro_input!(input as DeriveInput);
+    let name = &input.ident;
+
+    // Handle generic parameters if present
+    let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
+
+    let mut settings_key = Option::<SettingsKey>::None;
+
+    for attr in &input.attrs {
+        let parsed_settings_key = parse_setting_key_attr(attr);
+        if parsed_settings_key.is_some() && settings_key.is_some() {
+            panic!("Duplicate #[settings_key] attribute");
+        }
+        settings_key = parsed_settings_key;
+    }
+
+    let Some(SettingsKey { key, fallback_key }) = settings_key else {
+        panic!("Missing #[settings_key] attribute");
+    };
+
+    let key = key.map_or_else(|| quote! {None}, |key| quote! {Some(#key)});
+    let fallback_key = fallback_key.map_or_else(
+        || quote! {None},
+        |fallback_key| quote! {Some(#fallback_key)},
+    );
+
+    let expanded = quote! {
+        impl #impl_generics settings::SettingsKey for #name #ty_generics #where_clause {
+            const KEY: Option<&'static str> = #key;
+
+            const FALLBACK_KEY: Option<&'static str> = #fallback_key;
+        };
+    };
+
+    proc_macro::TokenStream::from(expanded)
+}
+
+#[cfg(test)]
+mod tests {
+    use syn::{Attribute, parse_quote};
+
+    use super::*;
+
+    #[test]
+    fn test_extract_key() {
+        let input: Attribute = parse_quote!(
+            #[settings_key(key = "my_key")]
+        );
+        let settings_key = parse_setting_key_attr(&input).unwrap();
+        assert_eq!(settings_key.key, Some("my_key".to_string()));
+        assert_eq!(settings_key.fallback_key, None);
+    }
+
+    #[test]
+    fn test_empty_key() {
+        let input: Attribute = parse_quote!(
+            #[settings_key(None)]
+        );
+        let settings_key = parse_setting_key_attr(&input).unwrap();
+        assert_eq!(settings_key.key, None);
+        assert_eq!(settings_key.fallback_key, None);
+    }
+}

crates/terminal/src/terminal_settings.rs 🔗

@@ -6,7 +6,7 @@ use gpui::{AbsoluteLength, App, FontFallbacks, FontFeatures, FontWeight, Pixels,
 use schemars::JsonSchema;
 use serde_derive::{Deserialize, Serialize};
 
-use settings::{SettingsSources, SettingsUi};
+use settings::{SettingsKey, SettingsSources, SettingsUi};
 use std::path::PathBuf;
 use task::Shell;
 use theme::FontFamilyName;
@@ -135,7 +135,8 @@ pub enum ActivateScript {
     Pyenv,
 }
 
-#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, SettingsUi)]
+#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, SettingsUi, SettingsKey)]
+#[settings_key(key = "terminal")]
 pub struct TerminalSettingsContent {
     /// What shell to use when opening a terminal.
     ///
@@ -253,8 +254,6 @@ pub struct TerminalSettingsContent {
 }
 
 impl settings::Settings for TerminalSettings {
-    const KEY: Option<&'static str> = Some("terminal");
-
     type FileContent = TerminalSettingsContent;
 
     fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> anyhow::Result<Self> {

crates/theme/src/settings.rs 🔗

@@ -13,7 +13,7 @@ use gpui::{
 use refineable::Refineable;
 use schemars::{JsonSchema, json_schema};
 use serde::{Deserialize, Serialize};
-use settings::{ParameterizedJsonSchema, Settings, SettingsSources, SettingsUi};
+use settings::{ParameterizedJsonSchema, Settings, SettingsKey, SettingsSources, SettingsUi};
 use std::sync::Arc;
 use util::ResultExt as _;
 use util::schemars::replace_subschema;
@@ -366,7 +366,8 @@ impl IconThemeSelection {
 }
 
 /// Settings for rendering text in UI and text buffers.
-#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, SettingsUi)]
+#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, SettingsUi, SettingsKey)]
+#[settings_key(None)]
 pub struct ThemeSettingsContent {
     /// The default font size for text in the UI.
     #[serde(default)]
@@ -818,8 +819,6 @@ fn clamp_font_weight(weight: f32) -> FontWeight {
 }
 
 impl settings::Settings for ThemeSettings {
-    const KEY: Option<&'static str> = None;
-
     type FileContent = ThemeSettingsContent;
 
     fn load(sources: SettingsSources<Self::FileContent>, cx: &mut App) -> Result<Self> {

crates/title_bar/src/title_bar_settings.rs 🔗

@@ -1,7 +1,7 @@
 use db::anyhow;
 use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
-use settings::{Settings, SettingsSources, SettingsUi};
+use settings::{Settings, SettingsKey, SettingsSources, SettingsUi};
 
 #[derive(Copy, Clone, Deserialize, Debug)]
 pub struct TitleBarSettings {
@@ -14,8 +14,11 @@ pub struct TitleBarSettings {
     pub show_menus: bool,
 }
 
-#[derive(Copy, Clone, Default, Serialize, Deserialize, JsonSchema, Debug, SettingsUi)]
-#[settings_ui(group = "Title Bar", path = "title_bar")]
+#[derive(
+    Copy, Clone, Default, Serialize, Deserialize, JsonSchema, Debug, SettingsUi, SettingsKey,
+)]
+#[settings_ui(group = "Title Bar")]
+#[settings_key(key = "title_bar")]
 pub struct TitleBarSettingsContent {
     /// Whether to show the branch icon beside branch switcher in the title bar.
     ///
@@ -48,8 +51,6 @@ pub struct TitleBarSettingsContent {
 }
 
 impl Settings for TitleBarSettings {
-    const KEY: Option<&'static str> = Some("title_bar");
-
     type FileContent = TitleBarSettingsContent;
 
     fn load(sources: SettingsSources<Self::FileContent>, _: &mut gpui::App) -> anyhow::Result<Self>

crates/vim/src/test/vim_test_context.rs 🔗

@@ -68,7 +68,7 @@ impl VimTestContext {
 
     pub fn init_keybindings(enabled: bool, cx: &mut App) {
         SettingsStore::update_global(cx, |store, cx| {
-            store.update_user_settings::<VimModeSetting>(cx, |s| *s = Some(enabled));
+            store.update_user_settings::<VimModeSetting>(cx, |s| s.vim_mode = Some(enabled));
         });
         let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
             "keymaps/default-macos.json",
@@ -134,7 +134,7 @@ impl VimTestContext {
     pub fn enable_vim(&mut self) {
         self.cx.update(|_, cx| {
             SettingsStore::update_global(cx, |store, cx| {
-                store.update_user_settings::<VimModeSetting>(cx, |s| *s = Some(true));
+                store.update_user_settings::<VimModeSetting>(cx, |s| s.vim_mode = Some(true));
             });
         })
     }
@@ -142,7 +142,7 @@ impl VimTestContext {
     pub fn disable_vim(&mut self) {
         self.cx.update(|_, cx| {
             SettingsStore::update_global(cx, |store, cx| {
-                store.update_user_settings::<VimModeSetting>(cx, |s| *s = Some(false));
+                store.update_user_settings::<VimModeSetting>(cx, |s| s.vim_mode = Some(false));
             });
         })
     }
@@ -151,7 +151,7 @@ impl VimTestContext {
         self.cx.update(|_, cx| {
             SettingsStore::update_global(cx, |store, cx| {
                 store.update_user_settings::<vim_mode_setting::HelixModeSetting>(cx, |s| {
-                    *s = Some(true)
+                    s.helix_mode = Some(true)
                 });
             });
         })

crates/vim/src/vim.rs 🔗

@@ -39,7 +39,9 @@ use object::Object;
 use schemars::JsonSchema;
 use serde::Deserialize;
 use serde_derive::Serialize;
-use settings::{Settings, SettingsSources, SettingsStore, SettingsUi, update_settings_file};
+use settings::{
+    Settings, SettingsKey, SettingsSources, SettingsStore, SettingsUi, update_settings_file,
+};
 use state::{Mode, Operator, RecordedSelection, SearchState, VimGlobals};
 use std::{mem, ops::Range, sync::Arc};
 use surrounds::SurroundsType;
@@ -247,7 +249,7 @@ pub fn init(cx: &mut App) {
             let fs = workspace.app_state().fs.clone();
             let currently_enabled = Vim::enabled(cx);
             update_settings_file::<VimModeSetting>(fs, cx, move |setting, _| {
-                *setting = Some(!currently_enabled)
+                setting.vim_mode = Some(!currently_enabled)
             })
         });
 
@@ -1785,7 +1787,8 @@ struct VimSettings {
     pub cursor_shape: CursorShapeSettings,
 }
 
-#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, SettingsUi)]
+#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, SettingsUi, SettingsKey)]
+#[settings_key(key = "vim")]
 struct VimSettingsContent {
     pub default_mode: Option<ModeContent>,
     pub toggle_relative_line_numbers: Option<bool>,
@@ -1824,8 +1827,6 @@ impl From<ModeContent> for Mode {
 }
 
 impl Settings for VimSettings {
-    const KEY: Option<&'static str> = Some("vim");
-
     type FileContent = VimSettingsContent;
 
     fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> Result<Self> {

crates/vim_mode_setting/Cargo.toml 🔗

@@ -14,5 +14,7 @@ path = "src/vim_mode_setting.rs"
 [dependencies]
 anyhow.workspace = true
 gpui.workspace = true
+schemars.workspace = true
+serde.workspace = true
 settings.workspace = true
 workspace-hack.workspace = true

crates/vim_mode_setting/src/vim_mode_setting.rs 🔗

@@ -6,7 +6,8 @@
 
 use anyhow::Result;
 use gpui::App;
-use settings::{Settings, SettingsSources, SettingsUi};
+use schemars::JsonSchema;
+use settings::{Settings, SettingsKey, SettingsSources, SettingsUi};
 
 /// Initializes the `vim_mode_setting` crate.
 pub fn init(cx: &mut App) {
@@ -14,25 +15,40 @@ pub fn init(cx: &mut App) {
     HelixModeSetting::register(cx);
 }
 
-/// Whether or not to enable Vim mode.
-///
-/// Default: false
-#[derive(SettingsUi)]
 pub struct VimModeSetting(pub bool);
 
-impl Settings for VimModeSetting {
-    const KEY: Option<&'static str> = Some("vim_mode");
+#[derive(
+    Copy,
+    Clone,
+    PartialEq,
+    Eq,
+    Debug,
+    Default,
+    serde::Serialize,
+    serde::Deserialize,
+    SettingsUi,
+    SettingsKey,
+    JsonSchema,
+)]
+#[settings_key(None)]
+pub struct VimModeSettingContent {
+    /// Whether or not to enable Vim mode.
+    ///
+    /// Default: false
+    pub vim_mode: Option<bool>,
+}
 
-    type FileContent = Option<bool>;
+impl Settings for VimModeSetting {
+    type FileContent = VimModeSettingContent;
 
     fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> Result<Self> {
         Ok(Self(
             sources
                 .user
-                .or(sources.server)
-                .copied()
-                .flatten()
-                .unwrap_or(sources.default.ok_or_else(Self::missing_default)?),
+                .and_then(|mode| mode.vim_mode)
+                .or(sources.server.and_then(|mode| mode.vim_mode))
+                .or(sources.default.vim_mode)
+                .ok_or_else(Self::missing_default)?,
         ))
     }
 
@@ -41,25 +57,41 @@ impl Settings for VimModeSetting {
     }
 }
 
-/// Whether or not to enable Helix mode.
-///
-/// Default: false
-#[derive(SettingsUi)]
+#[derive(Debug)]
 pub struct HelixModeSetting(pub bool);
 
-impl Settings for HelixModeSetting {
-    const KEY: Option<&'static str> = Some("helix_mode");
+#[derive(
+    Copy,
+    Clone,
+    PartialEq,
+    Eq,
+    Debug,
+    Default,
+    serde::Serialize,
+    serde::Deserialize,
+    SettingsUi,
+    SettingsKey,
+    JsonSchema,
+)]
+#[settings_key(None)]
+pub struct HelixModeSettingContent {
+    /// Whether or not to enable Helix mode.
+    ///
+    /// Default: false
+    pub helix_mode: Option<bool>,
+}
 
-    type FileContent = Option<bool>;
+impl Settings for HelixModeSetting {
+    type FileContent = HelixModeSettingContent;
 
     fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> Result<Self> {
         Ok(Self(
             sources
                 .user
-                .or(sources.server)
-                .copied()
-                .flatten()
-                .unwrap_or(sources.default.ok_or_else(Self::missing_default)?),
+                .and_then(|mode| mode.helix_mode)
+                .or(sources.server.and_then(|mode| mode.helix_mode))
+                .or(sources.default.helix_mode)
+                .ok_or_else(Self::missing_default)?,
         ))
     }
 

crates/workspace/src/item.rs 🔗

@@ -17,7 +17,7 @@ use gpui::{
 use project::{Project, ProjectEntryId, ProjectPath};
 use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
-use settings::{Settings, SettingsLocation, SettingsSources, SettingsUi};
+use settings::{Settings, SettingsKey, SettingsLocation, SettingsSources, SettingsUi};
 use smallvec::SmallVec;
 use std::{
     any::{Any, TypeId},
@@ -101,7 +101,8 @@ pub enum ActivateOnClose {
     LeftNeighbour,
 }
 
-#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, SettingsUi)]
+#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, SettingsUi, SettingsKey)]
+#[settings_key(key = "tabs")]
 pub struct ItemSettingsContent {
     /// Whether to show the Git file status on a tab item.
     ///
@@ -130,7 +131,8 @@ pub struct ItemSettingsContent {
     show_close_button: Option<ShowCloseButton>,
 }
 
-#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, SettingsUi)]
+#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, SettingsUi, SettingsKey)]
+#[settings_key(key = "preview_tabs")]
 pub struct PreviewTabsSettingsContent {
     /// Whether to show opened editors as preview tabs.
     /// Preview tabs do not stay open, are reused until explicitly set to be kept open opened (via double-click or editing) and show file names in italic.
@@ -148,8 +150,6 @@ pub struct PreviewTabsSettingsContent {
 }
 
 impl Settings for ItemSettings {
-    const KEY: Option<&'static str> = Some("tabs");
-
     type FileContent = ItemSettingsContent;
 
     fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> Result<Self> {
@@ -187,8 +187,6 @@ impl Settings for ItemSettings {
 }
 
 impl Settings for PreviewTabsSettings {
-    const KEY: Option<&'static str> = Some("preview_tabs");
-
     type FileContent = PreviewTabsSettingsContent;
 
     fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> Result<Self> {

crates/workspace/src/workspace_settings.rs 🔗

@@ -6,7 +6,7 @@ use collections::HashMap;
 use gpui::App;
 use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
-use settings::{Settings, SettingsSources, SettingsUi};
+use settings::{Settings, SettingsKey, SettingsSources, SettingsUi};
 
 #[derive(Deserialize)]
 pub struct WorkspaceSettings {
@@ -118,7 +118,8 @@ pub enum RestoreOnStartupBehavior {
     LastSession,
 }
 
-#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, SettingsUi)]
+#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, SettingsUi, SettingsKey)]
+#[settings_key(None)]
 pub struct WorkspaceSettingsContent {
     /// Active pane styling settings.
     pub active_pane_modifiers: Option<ActivePanelModifiers>,
@@ -223,7 +224,8 @@ pub struct TabBarSettings {
     pub show_tab_bar_buttons: bool,
 }
 
-#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, SettingsUi)]
+#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, SettingsUi, SettingsKey)]
+#[settings_key(key = "tab_bar")]
 pub struct TabBarSettingsContent {
     /// Whether or not to show the tab bar in the editor.
     ///
@@ -282,8 +284,6 @@ pub struct CenteredLayoutSettings {
 }
 
 impl Settings for WorkspaceSettings {
-    const KEY: Option<&'static str> = None;
-
     type FileContent = WorkspaceSettingsContent;
 
     fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> Result<Self> {
@@ -373,8 +373,6 @@ impl Settings for WorkspaceSettings {
 }
 
 impl Settings for TabBarSettings {
-    const KEY: Option<&'static str> = Some("tab_bar");
-
     type FileContent = TabBarSettingsContent;
 
     fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> Result<Self> {

crates/worktree/src/worktree_settings.rs 🔗

@@ -4,7 +4,7 @@ use anyhow::Context as _;
 use gpui::App;
 use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
-use settings::{Settings, SettingsSources, SettingsUi};
+use settings::{Settings, SettingsKey, SettingsSources, SettingsUi};
 use util::paths::PathMatcher;
 
 #[derive(Clone, PartialEq, Eq)]
@@ -31,7 +31,8 @@ impl WorktreeSettings {
     }
 }
 
-#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, SettingsUi)]
+#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, SettingsUi, SettingsKey)]
+#[settings_key(None)]
 pub struct WorktreeSettingsContent {
     /// Completely ignore files matching globs from `file_scan_exclusions`. Overrides
     /// `file_scan_inclusions`.
@@ -65,8 +66,6 @@ pub struct WorktreeSettingsContent {
 }
 
 impl Settings for WorktreeSettings {
-    const KEY: Option<&'static str> = None;
-
     type FileContent = WorktreeSettingsContent;
 
     fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> anyhow::Result<Self> {

crates/zlog_settings/src/zlog_settings.rs 🔗

@@ -3,7 +3,7 @@ use anyhow::Result;
 use gpui::App;
 use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
-use settings::{Settings, SettingsStore, SettingsUi};
+use settings::{Settings, SettingsKey, SettingsStore, SettingsUi};
 
 pub fn init(cx: &mut App) {
     ZlogSettings::register(cx);
@@ -15,15 +15,25 @@ pub fn init(cx: &mut App) {
     .detach();
 }
 
-#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema, SettingsUi)]
+#[derive(
+    Clone,
+    Debug,
+    Default,
+    Serialize,
+    Deserialize,
+    PartialEq,
+    Eq,
+    JsonSchema,
+    SettingsUi,
+    SettingsKey,
+)]
+#[settings_key(key = "log")]
 pub struct ZlogSettings {
     #[serde(default, flatten)]
     pub scopes: std::collections::HashMap<String, String>,
 }
 
 impl Settings for ZlogSettings {
-    const KEY: Option<&'static str> = Some("log");
-
     type FileContent = Self;
 
     fn load(sources: settings::SettingsSources<Self::FileContent>, _: &mut App) -> Result<Self>