settings_ui: Move settings UI trait to file content (#37337)

Ben Kunkle created

Closes #ISSUE

Initially, the `SettingsUi` trait was tied to `Settings`, however, given
that the `Settings::FileContent` type (which may be the same as the type
that implements `Settings`) will be the type that more directly maps to
the JSON structure (and therefore have the documentation, correct field
names (or `serde` rename attributes), etc) it makes more sense to have
the deriving of `SettingsUi` occur on the `FileContent` type rather than
the `Settings` type.

In order for this to work a relatively important change had to be made
to the derive macro, that being that it now "unwraps" options into their
inner type, so a field with type `Option<Foo>` where `Foo: SettingsUi`
will treat the field as if it were just `Foo`, expecting there to be a
default set in `default.json`. This imposes some restrictions on what
`Settings::FileContent` can be as seen in 1e19398 where `FileContent`
itself can't be optional without manually implementing `SettingsUi`, as
well as introducing some risk that if the `FileContent` type has
`serde(default)`, the default value will override the default value from
`default.json` in the UI even though it may differ (but it should!).

A future PR should probably replace the other settings with `FileContent
= Option<T>` (all of which currently have `T == bool`) with wrapper
structs and have `KEY = None` so the further niceties
`derive(SettingsUi)` will provide such as path renaming, custom UI, auto
naming and doc comment extraction can be used.

Release Notes:

- N/A *or* Added/Fixed/Improved ...

Change summary

crates/agent_settings/src/agent_settings.rs         |  4 +-
crates/audio/src/audio_settings.rs                  |  4 +-
crates/auto_update/src/auto_update.rs               | 13 +++---
crates/call/src/call_settings.rs                    |  4 +-
crates/client/src/client.rs                         | 12 +++---
crates/collab_ui/src/panel_settings.rs              | 10 ++--
crates/dap/src/debugger_settings.rs                 |  2 +
crates/editor/src/editor_settings.rs                |  4 +-
crates/file_finder/src/file_finder_settings.rs      |  4 +-
crates/git_ui/src/git_panel_settings.rs             |  4 +-
crates/go_to_line/src/cursor_position.rs            | 10 ++--
crates/language/src/language_settings.rs            |  4 +-
crates/language_models/src/settings.rs              |  4 +-
crates/onboarding/src/base_keymap_picker.rs         |  2 
crates/onboarding/src/basics_page.rs                |  2 
crates/outline_panel/src/outline_panel_settings.rs  |  4 +-
crates/project_panel/src/project_panel_settings.rs  |  4 +-
crates/recent_projects/src/remote_connections.rs    |  4 +-
crates/repl/src/jupyter_settings.rs                 |  4 +-
crates/settings/src/base_keymap_setting.rs          | 23 +++++++++---
crates/settings/src/settings_store.rs               | 18 +++++-----
crates/settings/src/settings_ui.rs                  | 23 ++++++------
crates/settings_ui_macros/src/settings_ui_macros.rs | 27 +++++++++++++++
crates/terminal/src/terminal_settings.rs            |  4 +-
crates/theme/src/settings.rs                        |  4 +-
crates/title_bar/src/title_bar_settings.rs          |  6 +-
crates/vim/src/vim.rs                               |  4 +-
crates/workspace/src/item.rs                        |  8 ++--
crates/workspace/src/workspace_settings.rs          | 10 ++--
crates/worktree/src/worktree_settings.rs            |  4 +-
30 files changed, 136 insertions(+), 94 deletions(-)

Detailed changes

crates/agent_settings/src/agent_settings.rs 🔗

@@ -48,7 +48,7 @@ pub enum NotifyWhenAgentWaiting {
     Never,
 }
 
-#[derive(Default, Clone, Debug, SettingsUi)]
+#[derive(Default, Clone, Debug)]
 pub struct AgentSettings {
     pub enabled: bool,
     pub button: bool,
@@ -223,7 +223,7 @@ impl AgentSettingsContent {
     }
 }
 
-#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug, Default)]
+#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug, Default, SettingsUi)]
 pub struct AgentSettingsContent {
     /// Whether the Agent is enabled.
     ///

crates/audio/src/audio_settings.rs 🔗

@@ -4,7 +4,7 @@ use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
 use settings::{Settings, SettingsSources, SettingsUi};
 
-#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, SettingsUi)]
+#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
 pub struct AudioSettings {
     /// Opt into the new audio system.
     #[serde(rename = "experimental.rodio_audio", default)]
@@ -12,7 +12,7 @@ pub struct AudioSettings {
 }
 
 /// Configuration of audio in Zed.
-#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
+#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, SettingsUi)]
 #[serde(default)]
 pub struct AudioSettingsContent {
     /// Whether to use the experimental audio system

crates/auto_update/src/auto_update.rs 🔗

@@ -113,20 +113,19 @@ impl Drop for MacOsUnmounter {
     }
 }
 
-#[derive(SettingsUi)]
 struct AutoUpdateSetting(bool);
 
 /// Whether or not to automatically check for updates.
 ///
 /// Default: true
-#[derive(Clone, Copy, Default, JsonSchema, Deserialize, Serialize)]
+#[derive(Clone, Copy, Default, JsonSchema, Deserialize, Serialize, SettingsUi)]
 #[serde(transparent)]
 struct AutoUpdateSettingContent(bool);
 
 impl Settings for AutoUpdateSetting {
     const KEY: Option<&'static str> = Some("auto_update");
 
-    type FileContent = Option<AutoUpdateSettingContent>;
+    type FileContent = AutoUpdateSettingContent;
 
     fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> Result<Self> {
         let auto_update = [
@@ -136,17 +135,19 @@ impl Settings for AutoUpdateSetting {
             sources.user,
         ]
         .into_iter()
-        .find_map(|value| value.copied().flatten())
-        .unwrap_or(sources.default.ok_or_else(Self::missing_default)?);
+        .find_map(|value| value.copied())
+        .unwrap_or(*sources.default);
 
         Ok(Self(auto_update.0))
     }
 
     fn import_from_vscode(vscode: &settings::VsCodeSettings, current: &mut Self::FileContent) {
-        vscode.enum_setting("update.mode", current, |s| match s {
+        let mut cur = &mut Some(*current);
+        vscode.enum_setting("update.mode", &mut cur, |s| match s {
             "none" | "manual" => Some(AutoUpdateSettingContent(false)),
             _ => Some(AutoUpdateSettingContent(true)),
         });
+        *current = cur.unwrap();
     }
 }
 

crates/call/src/call_settings.rs 🔗

@@ -4,14 +4,14 @@ use schemars::JsonSchema;
 use serde_derive::{Deserialize, Serialize};
 use settings::{Settings, SettingsSources, SettingsUi};
 
-#[derive(Deserialize, Debug, SettingsUi)]
+#[derive(Deserialize, Debug)]
 pub struct CallSettings {
     pub mute_on_join: bool,
     pub share_on_join: bool,
 }
 
 /// Configuration of voice calls in Zed.
-#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
+#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, SettingsUi)]
 pub struct CallSettingsContent {
     /// Whether the microphone should be muted when joining a channel or a call.
     ///

crates/client/src/client.rs 🔗

@@ -96,12 +96,12 @@ actions!(
     ]
 );
 
-#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)]
+#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, SettingsUi)]
 pub struct ClientSettingsContent {
     server_url: Option<String>,
 }
 
-#[derive(Deserialize, SettingsUi)]
+#[derive(Deserialize)]
 pub struct ClientSettings {
     pub server_url: String,
 }
@@ -122,12 +122,12 @@ impl Settings for ClientSettings {
     fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) {}
 }
 
-#[derive(Default, Clone, Serialize, Deserialize, JsonSchema)]
+#[derive(Default, Clone, Serialize, Deserialize, JsonSchema, SettingsUi)]
 pub struct ProxySettingsContent {
     proxy: Option<String>,
 }
 
-#[derive(Deserialize, Default, SettingsUi)]
+#[derive(Deserialize, Default)]
 pub struct ProxySettings {
     pub proxy: Option<String>,
 }
@@ -520,14 +520,14 @@ impl<T: 'static> Drop for PendingEntitySubscription<T> {
     }
 }
 
-#[derive(Copy, Clone, Deserialize, Debug, SettingsUi)]
+#[derive(Copy, Clone, Deserialize, Debug)]
 pub struct TelemetrySettings {
     pub diagnostics: bool,
     pub metrics: bool,
 }
 
 /// Control what info is collected by Zed.
-#[derive(Default, Clone, Serialize, Deserialize, JsonSchema, Debug)]
+#[derive(Default, Clone, Serialize, Deserialize, JsonSchema, Debug, SettingsUi)]
 pub struct TelemetrySettingsContent {
     /// Send debug info like crash reports.
     ///

crates/collab_ui/src/panel_settings.rs 🔗

@@ -4,7 +4,7 @@ use serde::{Deserialize, Serialize};
 use settings::{Settings, SettingsSources, SettingsUi};
 use workspace::dock::DockPosition;
 
-#[derive(Deserialize, Debug, SettingsUi)]
+#[derive(Deserialize, Debug)]
 pub struct CollaborationPanelSettings {
     pub button: bool,
     pub dock: DockPosition,
@@ -20,14 +20,14 @@ pub enum ChatPanelButton {
     WhenInCall,
 }
 
-#[derive(Deserialize, Debug, SettingsUi)]
+#[derive(Deserialize, Debug)]
 pub struct ChatPanelSettings {
     pub button: ChatPanelButton,
     pub dock: DockPosition,
     pub default_width: Pixels,
 }
 
-#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
+#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, SettingsUi)]
 pub struct ChatPanelSettingsContent {
     /// When to show the panel button in the status bar.
     ///
@@ -43,14 +43,14 @@ pub struct ChatPanelSettingsContent {
     pub default_width: Option<f32>,
 }
 
-#[derive(Deserialize, Debug, SettingsUi)]
+#[derive(Deserialize, Debug)]
 pub struct NotificationPanelSettings {
     pub button: bool,
     pub dock: DockPosition,
     pub default_width: Pixels,
 }
 
-#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
+#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, SettingsUi)]
 pub struct PanelSettingsContent {
     /// Whether to show the panel button in the status bar.
     ///

crates/dap/src/debugger_settings.rs 🔗

@@ -14,6 +14,8 @@ pub enum DebugPanelDockPosition {
 
 #[derive(Serialize, Deserialize, JsonSchema, Clone, Copy, SettingsUi)]
 #[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")]
 pub struct DebuggerSettings {
     /// Determines the stepping granularity.

crates/editor/src/editor_settings.rs 🔗

@@ -11,7 +11,7 @@ use util::serde::default_true;
 
 /// Imports from the VSCode settings at
 /// https://code.visualstudio.com/docs/reference/default-settings
-#[derive(Deserialize, Clone, SettingsUi)]
+#[derive(Deserialize, Clone)]
 pub struct EditorSettings {
     pub cursor_blink: bool,
     pub cursor_shape: Option<CursorShape>,
@@ -415,7 +415,7 @@ pub enum SnippetSortOrder {
     None,
 }
 
-#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)]
+#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, SettingsUi)]
 pub struct EditorSettingsContent {
     /// Whether the cursor blinks in the editor.
     ///

crates/file_finder/src/file_finder_settings.rs 🔗

@@ -3,7 +3,7 @@ use schemars::JsonSchema;
 use serde_derive::{Deserialize, Serialize};
 use settings::{Settings, SettingsSources, SettingsUi};
 
-#[derive(Deserialize, Debug, Clone, Copy, PartialEq, SettingsUi)]
+#[derive(Deserialize, Debug, Clone, Copy, PartialEq)]
 pub struct FileFinderSettings {
     pub file_icons: bool,
     pub modal_max_width: Option<FileFinderWidth>,
@@ -11,7 +11,7 @@ pub struct FileFinderSettings {
     pub include_ignored: Option<bool>,
 }
 
-#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
+#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, SettingsUi)]
 pub struct FileFinderSettingsContent {
     /// Whether to show file icons in the file finder.
     ///

crates/git_ui/src/git_panel_settings.rs 🔗

@@ -36,7 +36,7 @@ pub enum StatusStyle {
     LabelColor,
 }
 
-#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
+#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, SettingsUi)]
 pub struct GitPanelSettingsContent {
     /// Whether to show the panel button in the status bar.
     ///
@@ -77,7 +77,7 @@ pub struct GitPanelSettingsContent {
     pub collapse_untracked_diff: Option<bool>,
 }
 
-#[derive(Deserialize, Debug, Clone, PartialEq, SettingsUi)]
+#[derive(Deserialize, Debug, Clone, PartialEq)]
 pub struct GitPanelSettings {
     pub button: bool,
     pub dock: DockPosition,

crates/go_to_line/src/cursor_position.rs 🔗

@@ -293,7 +293,7 @@ impl StatusItemView for CursorPosition {
     }
 }
 
-#[derive(Clone, Copy, Default, PartialEq, JsonSchema, Deserialize, Serialize, SettingsUi)]
+#[derive(Clone, Copy, Default, PartialEq, JsonSchema, Deserialize, Serialize)]
 #[serde(rename_all = "snake_case")]
 pub(crate) enum LineIndicatorFormat {
     Short,
@@ -301,14 +301,14 @@ pub(crate) enum LineIndicatorFormat {
     Long,
 }
 
-#[derive(Clone, Copy, Default, JsonSchema, Deserialize, Serialize)]
+#[derive(Clone, Copy, Default, JsonSchema, Deserialize, Serialize, SettingsUi)]
 #[serde(transparent)]
 pub(crate) struct LineIndicatorFormatContent(LineIndicatorFormat);
 
 impl Settings for LineIndicatorFormat {
     const KEY: Option<&'static str> = Some("line_indicator_format");
 
-    type FileContent = Option<LineIndicatorFormatContent>;
+    type FileContent = LineIndicatorFormatContent;
 
     fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> anyhow::Result<Self> {
         let format = [
@@ -317,8 +317,8 @@ impl Settings for LineIndicatorFormat {
             sources.user,
         ]
         .into_iter()
-        .find_map(|value| value.copied().flatten())
-        .unwrap_or(sources.default.ok_or_else(Self::missing_default)?);
+        .find_map(|value| value.copied())
+        .unwrap_or(*sources.default);
 
         Ok(format.0)
     }

crates/language/src/language_settings.rs 🔗

@@ -55,7 +55,7 @@ pub fn all_language_settings<'a>(
 }
 
 /// The settings for all languages.
-#[derive(Debug, Clone, SettingsUi)]
+#[derive(Debug, Clone)]
 pub struct AllLanguageSettings {
     /// The edit prediction settings.
     pub edit_predictions: EditPredictionSettings,
@@ -292,7 +292,7 @@ pub struct CopilotSettings {
 }
 
 /// The settings for all languages.
-#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
+#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema, SettingsUi)]
 pub struct AllLanguageSettingsContent {
     /// The settings for enabling/disabling features.
     #[serde(default)]

crates/language_models/src/settings.rs 🔗

@@ -29,7 +29,7 @@ pub fn init_settings(cx: &mut App) {
     AllLanguageModelSettings::register(cx);
 }
 
-#[derive(Default, SettingsUi)]
+#[derive(Default)]
 pub struct AllLanguageModelSettings {
     pub anthropic: AnthropicSettings,
     pub bedrock: AmazonBedrockSettings,
@@ -46,7 +46,7 @@ pub struct AllLanguageModelSettings {
     pub zed_dot_dev: ZedDotDevSettings,
 }
 
-#[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)]
+#[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema, SettingsUi)]
 pub struct AllLanguageModelSettingsContent {
     pub anthropic: Option<AnthropicSettingsContent>,
     pub bedrock: Option<AmazonBedrockSettingsContent>,

crates/onboarding/src/base_keymap_picker.rs 🔗

@@ -187,7 +187,7 @@ impl PickerDelegate for BaseKeymapSelectorDelegate {
             );
 
             update_settings_file::<BaseKeymap>(self.fs.clone(), cx, move |setting, _| {
-                *setting = Some(base_keymap)
+                setting.base_keymap = Some(base_keymap)
             });
         }
 

crates/onboarding/src/basics_page.rs 🔗

@@ -325,7 +325,7 @@ fn render_base_keymap_section(tab_index: &mut isize, cx: &mut App) -> impl IntoE
         let fs = <dyn Fs>::global(cx);
 
         update_settings_file::<BaseKeymap>(fs, cx, move |setting, _| {
-            *setting = Some(keymap_base);
+            setting.base_keymap = Some(keymap_base);
         });
     }
 }

crates/outline_panel/src/outline_panel_settings.rs 🔗

@@ -18,7 +18,7 @@ pub enum ShowIndentGuides {
     Never,
 }
 
-#[derive(Deserialize, Debug, Clone, Copy, PartialEq, SettingsUi)]
+#[derive(Deserialize, Debug, Clone, Copy, PartialEq)]
 pub struct OutlinePanelSettings {
     pub button: bool,
     pub default_width: Pixels,
@@ -61,7 +61,7 @@ pub struct IndentGuidesSettingsContent {
     pub show: Option<ShowIndentGuides>,
 }
 
-#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
+#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, SettingsUi)]
 pub struct OutlinePanelSettingsContent {
     /// Whether to show the outline panel button in the status bar.
     ///

crates/project_panel/src/project_panel_settings.rs 🔗

@@ -28,7 +28,7 @@ pub enum EntrySpacing {
     Standard,
 }
 
-#[derive(Deserialize, Debug, Clone, Copy, PartialEq, SettingsUi)]
+#[derive(Deserialize, Debug, Clone, Copy, PartialEq)]
 pub struct ProjectPanelSettings {
     pub button: bool,
     pub hide_gitignore: bool,
@@ -92,7 +92,7 @@ pub enum ShowDiagnostics {
     All,
 }
 
-#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
+#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, SettingsUi)]
 pub struct ProjectPanelSettingsContent {
     /// Whether to show the project panel button in the status bar.
     ///

crates/recent_projects/src/remote_connections.rs 🔗

@@ -30,7 +30,7 @@ use ui::{
 use util::serde::default_true;
 use workspace::{AppState, ModalView, Workspace};
 
-#[derive(Deserialize, SettingsUi)]
+#[derive(Deserialize)]
 pub struct SshSettings {
     pub ssh_connections: Option<Vec<SshConnection>>,
     /// Whether to read ~/.ssh/config for ssh connection sources.
@@ -121,7 +121,7 @@ pub struct SshProject {
     pub paths: Vec<String>,
 }
 
-#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)]
+#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, SettingsUi)]
 pub struct RemoteSettingsContent {
     pub ssh_connections: Option<Vec<SshConnection>>,
     pub read_ssh_config: Option<bool>,

crates/repl/src/jupyter_settings.rs 🔗

@@ -6,7 +6,7 @@ use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
 use settings::{Settings, SettingsSources, SettingsUi};
 
-#[derive(Debug, Default, SettingsUi)]
+#[derive(Debug, Default)]
 pub struct JupyterSettings {
     pub kernel_selections: HashMap<String, String>,
 }
@@ -20,7 +20,7 @@ impl JupyterSettings {
     }
 }
 
-#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug)]
+#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug, SettingsUi)]
 pub struct JupyterSettingsContent {
     /// Default kernels to select for each language.
     ///

crates/settings/src/base_keymap_setting.rs 🔗

@@ -100,25 +100,36 @@ impl BaseKeymap {
     }
 }
 
+#[derive(
+    Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Default, SettingsUi,
+)]
+// extracted so that it can be an option, and still work with derive(SettingsUi)
+pub struct BaseKeymapSetting {
+    pub base_keymap: Option<BaseKeymap>,
+}
+
 impl Settings for BaseKeymap {
-    const KEY: Option<&'static str> = Some("base_keymap");
+    const KEY: Option<&'static str> = None;
 
-    type FileContent = Option<Self>;
+    type FileContent = BaseKeymapSetting;
 
     fn load(
         sources: SettingsSources<Self::FileContent>,
         _: &mut gpui::App,
     ) -> anyhow::Result<Self> {
-        if let Some(Some(user_value)) = sources.user.copied() {
+        if let Some(Some(user_value)) = sources.user.map(|setting| setting.base_keymap) {
             return Ok(user_value);
         }
-        if let Some(Some(server_value)) = sources.server.copied() {
+        if let Some(Some(server_value)) = sources.server.map(|setting| setting.base_keymap) {
             return Ok(server_value);
         }
-        sources.default.ok_or_else(Self::missing_default)
+        sources
+            .default
+            .base_keymap
+            .ok_or_else(Self::missing_default)
     }
 
     fn import_from_vscode(_vscode: &VsCodeSettings, current: &mut Self::FileContent) {
-        *current = Some(BaseKeymap::VSCode);
+        current.base_keymap = Some(BaseKeymap::VSCode);
     }
 }

crates/settings/src/settings_store.rs 🔗

@@ -39,7 +39,7 @@ use crate::{
 /// A value that can be defined as a user setting.
 ///
 /// Settings can be loaded from a combination of multiple JSON files.
-pub trait Settings: SettingsUi + 'static + Send + Sync {
+pub trait Settings: '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.
@@ -57,7 +57,7 @@ pub trait Settings: SettingsUi + '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;
+    type FileContent: Clone + Default + Serialize + DeserializeOwned + JsonSchema + SettingsUi;
 
     /// The logic for combining together values from one or more JSON files into the
     /// final value for this setting.
@@ -1565,7 +1565,7 @@ impl<T: Settings> AnySettingValue for SettingValue<T> {
     }
 
     fn settings_ui_item(&self) -> SettingsUiEntry {
-        <T as SettingsUi>::settings_ui_entry()
+        <<T as Settings>::FileContent as SettingsUi>::settings_ui_entry()
     }
 }
 
@@ -2147,12 +2147,12 @@ mod tests {
         }
     }
 
-    #[derive(Debug, Deserialize, PartialEq, SettingsUi)]
+    #[derive(Debug, Deserialize, PartialEq)]
     struct TurboSetting(bool);
 
     impl Settings for TurboSetting {
         const KEY: Option<&'static str> = Some("turbo");
-        type FileContent = Option<bool>;
+        type FileContent = bool;
 
         fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> Result<Self> {
             sources.json_merge()
@@ -2161,7 +2161,7 @@ mod tests {
         fn import_from_vscode(_vscode: &VsCodeSettings, _current: &mut Self::FileContent) {}
     }
 
-    #[derive(Clone, Debug, PartialEq, Deserialize, SettingsUi)]
+    #[derive(Clone, Debug, PartialEq, Deserialize)]
     struct MultiKeySettings {
         #[serde(default)]
         key1: String,
@@ -2169,7 +2169,7 @@ mod tests {
         key2: String,
     }
 
-    #[derive(Clone, Default, Serialize, Deserialize, JsonSchema)]
+    #[derive(Clone, Default, Serialize, Deserialize, JsonSchema, SettingsUi)]
     struct MultiKeySettingsJson {
         key1: Option<String>,
         key2: Option<String>,
@@ -2194,7 +2194,7 @@ mod tests {
         }
     }
 
-    #[derive(Debug, Deserialize, SettingsUi)]
+    #[derive(Debug, Deserialize)]
     struct JournalSettings {
         pub path: String,
         pub hour_format: HourFormat,
@@ -2207,7 +2207,7 @@ mod tests {
         Hour24,
     }
 
-    #[derive(Clone, Default, Debug, Serialize, Deserialize, JsonSchema)]
+    #[derive(Clone, Default, Debug, Serialize, Deserialize, JsonSchema, SettingsUi)]
     struct JournalSettingsJson {
         pub path: Option<String>,
         pub hour_format: Option<HourFormat>,

crates/settings/src/settings_ui.rs 🔗

@@ -7,9 +7,16 @@ use crate::SettingsStore;
 
 pub trait SettingsUi {
     fn settings_ui_item() -> SettingsUiItem {
+        // todo(settings_ui): remove this default impl, only entry should have a default impl
+        // because it's expected that the macro or custom impl use the item and the known paths to create the entry
         SettingsUiItem::None
     }
-    fn settings_ui_entry() -> SettingsUiEntry;
+
+    fn settings_ui_entry() -> SettingsUiEntry {
+        SettingsUiEntry {
+            item: SettingsUiEntryVariant::None,
+        }
+    }
 }
 
 pub struct SettingsUiEntry {
@@ -106,11 +113,11 @@ impl SettingsUi for bool {
     fn settings_ui_item() -> SettingsUiItem {
         SettingsUiItem::Single(SettingsUiItemSingle::SwitchField)
     }
+}
 
-    fn settings_ui_entry() -> SettingsUiEntry {
-        SettingsUiEntry {
-            item: SettingsUiEntryVariant::None,
-        }
+impl SettingsUi for Option<bool> {
+    fn settings_ui_item() -> SettingsUiItem {
+        SettingsUiItem::Single(SettingsUiItemSingle::SwitchField)
     }
 }
 
@@ -118,10 +125,4 @@ impl SettingsUi for u64 {
     fn settings_ui_item() -> SettingsUiItem {
         SettingsUiItem::Single(SettingsUiItemSingle::NumericStepper)
     }
-
-    fn settings_ui_entry() -> SettingsUiEntry {
-        SettingsUiEntry {
-            item: SettingsUiEntryVariant::None,
-        }
-    }
 }

crates/settings_ui_macros/src/settings_ui_macros.rs 🔗

@@ -88,7 +88,34 @@ pub fn derive_settings_ui(input: proc_macro::TokenStream) -> proc_macro::TokenSt
     proc_macro::TokenStream::from(expanded)
 }
 
+fn extract_type_from_option(ty: TokenStream) -> TokenStream {
+    match option_inner_type(ty.clone()) {
+        Some(inner_type) => inner_type,
+        None => ty,
+    }
+}
+
+fn option_inner_type(ty: TokenStream) -> Option<TokenStream> {
+    let ty = syn::parse2::<syn::Type>(ty).ok()?;
+    let syn::Type::Path(path) = ty else {
+        return None;
+    };
+    let segment = path.path.segments.last()?;
+    if segment.ident != "Option" {
+        return None;
+    }
+    let syn::PathArguments::AngleBracketed(args) = &segment.arguments else {
+        return None;
+    };
+    let arg = args.args.first()?;
+    let syn::GenericArgument::Type(ty) = arg else {
+        return None;
+    };
+    return Some(ty.to_token_stream());
+}
+
 fn map_ui_item_to_render(path: &str, ty: TokenStream) -> TokenStream {
+    let ty = extract_type_from_option(ty);
     quote! {
         settings::SettingsUiEntry {
             item: match #ty::settings_ui_item() {

crates/terminal/src/terminal_settings.rs 🔗

@@ -24,7 +24,7 @@ pub struct Toolbar {
     pub breadcrumbs: bool,
 }
 
-#[derive(Clone, Debug, Deserialize, SettingsUi)]
+#[derive(Clone, Debug, Deserialize)]
 pub struct TerminalSettings {
     pub shell: Shell,
     pub working_directory: WorkingDirectory,
@@ -135,7 +135,7 @@ pub enum ActivateScript {
     Pyenv,
 }
 
-#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
+#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, SettingsUi)]
 pub struct TerminalSettingsContent {
     /// What shell to use when opening a terminal.
     ///

crates/theme/src/settings.rs 🔗

@@ -87,7 +87,7 @@ impl From<UiDensity> for String {
 }
 
 /// Customizable settings for the UI and theme system.
-#[derive(Clone, PartialEq, SettingsUi)]
+#[derive(Clone, PartialEq)]
 pub struct ThemeSettings {
     /// The UI font size. Determines the size of text in the UI,
     /// as well as the size of a [gpui::Rems] unit.
@@ -365,7 +365,7 @@ impl IconThemeSelection {
 }
 
 /// Settings for rendering text in UI and text buffers.
-#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
+#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, SettingsUi)]
 pub struct ThemeSettingsContent {
     /// The default font size for text in the UI.
     #[serde(default)]

crates/title_bar/src/title_bar_settings.rs 🔗

@@ -3,8 +3,7 @@ use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
 use settings::{Settings, SettingsSources, SettingsUi};
 
-#[derive(Copy, Clone, Deserialize, Debug, SettingsUi)]
-#[settings_ui(group = "Title Bar", path = "title_bar")]
+#[derive(Copy, Clone, Deserialize, Debug)]
 pub struct TitleBarSettings {
     pub show_branch_icon: bool,
     pub show_onboarding_banner: bool,
@@ -15,7 +14,8 @@ pub struct TitleBarSettings {
     pub show_menus: bool,
 }
 
-#[derive(Copy, Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
+#[derive(Copy, Clone, Default, Serialize, Deserialize, JsonSchema, Debug, SettingsUi)]
+#[settings_ui(group = "Title Bar", path = "title_bar")]
 pub struct TitleBarSettingsContent {
     /// Whether to show the branch icon beside branch switcher in the title bar.
     ///

crates/vim/src/vim.rs 🔗

@@ -1774,7 +1774,7 @@ struct CursorShapeSettings {
     pub insert: Option<CursorShape>,
 }
 
-#[derive(Deserialize, SettingsUi)]
+#[derive(Deserialize)]
 struct VimSettings {
     pub default_mode: Mode,
     pub toggle_relative_line_numbers: bool,
@@ -1785,7 +1785,7 @@ struct VimSettings {
     pub cursor_shape: CursorShapeSettings,
 }
 
-#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)]
+#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, SettingsUi)]
 struct VimSettingsContent {
     pub default_mode: Option<ModeContent>,
     pub toggle_relative_line_numbers: Option<bool>,

crates/workspace/src/item.rs 🔗

@@ -49,7 +49,7 @@ impl Default for SaveOptions {
     }
 }
 
-#[derive(Deserialize, SettingsUi)]
+#[derive(Deserialize)]
 pub struct ItemSettings {
     pub git_status: bool,
     pub close_position: ClosePosition,
@@ -59,7 +59,7 @@ pub struct ItemSettings {
     pub show_close_button: ShowCloseButton,
 }
 
-#[derive(Deserialize, SettingsUi)]
+#[derive(Deserialize)]
 pub struct PreviewTabsSettings {
     pub enabled: bool,
     pub enable_preview_from_file_finder: bool,
@@ -101,7 +101,7 @@ pub enum ActivateOnClose {
     LeftNeighbour,
 }
 
-#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)]
+#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, SettingsUi)]
 pub struct ItemSettingsContent {
     /// Whether to show the Git file status on a tab item.
     ///
@@ -130,7 +130,7 @@ pub struct ItemSettingsContent {
     show_close_button: Option<ShowCloseButton>,
 }
 
-#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)]
+#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, SettingsUi)]
 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.

crates/workspace/src/workspace_settings.rs 🔗

@@ -8,7 +8,7 @@ use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
 use settings::{Settings, SettingsSources, SettingsUi};
 
-#[derive(Deserialize, SettingsUi)]
+#[derive(Deserialize)]
 pub struct WorkspaceSettings {
     pub active_pane_modifiers: ActivePanelModifiers,
     pub bottom_dock_layout: BottomDockLayout,
@@ -118,7 +118,7 @@ pub enum RestoreOnStartupBehavior {
     LastSession,
 }
 
-#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)]
+#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, SettingsUi)]
 pub struct WorkspaceSettingsContent {
     /// Active pane styling settings.
     pub active_pane_modifiers: Option<ActivePanelModifiers>,
@@ -216,14 +216,14 @@ pub struct WorkspaceSettingsContent {
     pub zoomed_padding: Option<bool>,
 }
 
-#[derive(Deserialize, SettingsUi)]
+#[derive(Deserialize)]
 pub struct TabBarSettings {
     pub show: bool,
     pub show_nav_history_buttons: bool,
     pub show_tab_bar_buttons: bool,
 }
 
-#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)]
+#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, SettingsUi)]
 pub struct TabBarSettingsContent {
     /// Whether or not to show the tab bar in the editor.
     ///
@@ -266,7 +266,7 @@ pub enum PaneSplitDirectionVertical {
     Right,
 }
 
-#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
+#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, SettingsUi)]
 #[serde(rename_all = "snake_case")]
 pub struct CenteredLayoutSettings {
     /// The relative width of the left padding of the central pane from the

crates/worktree/src/worktree_settings.rs 🔗

@@ -7,7 +7,7 @@ use serde::{Deserialize, Serialize};
 use settings::{Settings, SettingsSources, SettingsUi};
 use util::paths::PathMatcher;
 
-#[derive(Clone, PartialEq, Eq, SettingsUi)]
+#[derive(Clone, PartialEq, Eq)]
 pub struct WorktreeSettings {
     pub file_scan_inclusions: PathMatcher,
     pub file_scan_exclusions: PathMatcher,
@@ -31,7 +31,7 @@ impl WorktreeSettings {
     }
 }
 
-#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)]
+#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, SettingsUi)]
 pub struct WorktreeSettingsContent {
     /// Completely ignore files matching globs from `file_scan_exclusions`. Overrides
     /// `file_scan_inclusions`.