settings_ui: Add dynamic setting fields (#40443)

Ben Kunkle created

Closes #ISSUE

Includes the start of how we can get rid of most of the `.unimplemented`
"Edit in JSON" buttons in the settings UI. For now only Theme selection
is implemented, follow ups will add more settings

Release Notes:

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

Change summary

Cargo.lock                                    |  58 +-
Cargo.toml                                    |   2 
crates/settings/src/settings_content/theme.rs |  26 
crates/settings/src/settings_store.rs         |  46 +
crates/settings_ui/src/page_data.rs           | 587 ++++++++++++++------
crates/settings_ui/src/settings_ui.rs         | 232 +++++--
6 files changed, 653 insertions(+), 298 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -678,7 +678,7 @@ dependencies = [
  "serde",
  "serde_json",
  "settings",
- "strum 0.27.1",
+ "strum 0.27.2",
  "thiserror 2.0.12",
  "workspace-hack",
 ]
@@ -2198,7 +2198,7 @@ dependencies = [
  "schemars 1.0.1",
  "serde",
  "serde_json",
- "strum 0.27.1",
+ "strum 0.27.2",
  "thiserror 2.0.12",
  "workspace-hack",
 ]
@@ -3388,7 +3388,7 @@ dependencies = [
  "pretty_assertions",
  "serde",
  "serde_json",
- "strum 0.27.1",
+ "strum 0.27.2",
  "uuid",
  "workspace-hack",
 ]
@@ -3403,7 +3403,7 @@ dependencies = [
  "ordered-float 2.10.1",
  "rustc-hash 2.1.1",
  "serde",
- "strum 0.27.1",
+ "strum 0.27.2",
  "workspace-hack",
 ]
 
@@ -3598,7 +3598,7 @@ dependencies = [
  "sha2",
  "smol",
  "sqlx",
- "strum 0.27.1",
+ "strum 0.27.2",
  "subtle",
  "supermaven_api",
  "task",
@@ -3783,7 +3783,7 @@ dependencies = [
  "gpui",
  "inventory",
  "parking_lot",
- "strum 0.27.1",
+ "strum 0.27.2",
  "theme",
  "workspace-hack",
 ]
@@ -5462,7 +5462,7 @@ dependencies = [
  "serde_json",
  "settings",
  "slotmap",
- "strum 0.27.1",
+ "strum 0.27.2",
  "text",
  "tree-sitter",
  "tree-sitter-c",
@@ -6074,7 +6074,7 @@ dependencies = [
  "serde",
  "settings",
  "smallvec",
- "strum 0.27.1",
+ "strum 0.27.2",
  "telemetry",
  "theme",
  "ui",
@@ -7195,7 +7195,7 @@ dependencies = [
  "serde",
  "serde_json",
  "settings",
- "strum 0.27.1",
+ "strum 0.27.2",
  "telemetry",
  "theme",
  "time",
@@ -7301,7 +7301,7 @@ dependencies = [
  "serde",
  "serde_json",
  "settings",
- "strum 0.27.1",
+ "strum 0.27.2",
  "workspace-hack",
 ]
 
@@ -7408,7 +7408,7 @@ dependencies = [
  "smallvec",
  "smol",
  "stacksafe",
- "strum 0.27.1",
+ "strum 0.27.2",
  "sum_tree",
  "taffy",
  "thiserror 2.0.12",
@@ -8059,7 +8059,7 @@ name = "icons"
 version = "0.1.0"
 dependencies = [
  "serde",
- "strum 0.27.1",
+ "strum 0.27.2",
  "workspace-hack",
 ]
 
@@ -9041,7 +9041,7 @@ dependencies = [
  "serde_json",
  "settings",
  "smol",
- "strum 0.27.1",
+ "strum 0.27.2",
  "thiserror 2.0.12",
  "tiktoken-rs",
  "tokio",
@@ -10192,7 +10192,7 @@ dependencies = [
  "schemars 1.0.1",
  "serde",
  "serde_json",
- "strum 0.27.1",
+ "strum 0.27.2",
  "workspace-hack",
 ]
 
@@ -11109,7 +11109,7 @@ dependencies = [
  "serde",
  "serde_json",
  "settings",
- "strum 0.27.1",
+ "strum 0.27.2",
  "workspace-hack",
 ]
 
@@ -11124,7 +11124,7 @@ dependencies = [
  "serde",
  "serde_json",
  "settings",
- "strum 0.27.1",
+ "strum 0.27.2",
  "thiserror 2.0.12",
  "workspace-hack",
 ]
@@ -14418,7 +14418,7 @@ dependencies = [
  "serde",
  "serde_json",
  "sha2",
- "strum 0.27.1",
+ "strum 0.27.2",
  "tracing",
  "util",
  "workspace-hack",
@@ -15427,7 +15427,7 @@ dependencies = [
  "serde_with",
  "settings_macros",
  "smallvec",
- "strum 0.27.1",
+ "strum 0.27.2",
  "tree-sitter",
  "tree-sitter-json",
  "unindent",
@@ -15494,7 +15494,7 @@ dependencies = [
  "serde",
  "session",
  "settings",
- "strum 0.27.1",
+ "strum 0.27.2",
  "theme",
  "title_bar",
  "ui",
@@ -16253,7 +16253,7 @@ dependencies = [
  "settings",
  "simplelog",
  "story",
- "strum 0.27.1",
+ "strum 0.27.2",
  "theme",
  "title_bar",
  "ui",
@@ -16355,9 +16355,9 @@ dependencies = [
 
 [[package]]
 name = "strum"
-version = "0.27.1"
+version = "0.27.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f64def088c51c9510a8579e3c5d67c65349dcf755e5479ad3d010aa6454e2c32"
+checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf"
 dependencies = [
  "strum_macros 0.27.1",
 ]
@@ -17232,7 +17232,7 @@ dependencies = [
  "serde_json",
  "serde_json_lenient",
  "settings",
- "strum 0.27.1",
+ "strum 0.27.2",
  "thiserror 2.0.12",
  "util",
  "uuid",
@@ -17266,7 +17266,7 @@ dependencies = [
  "serde_json",
  "serde_json_lenient",
  "simplelog",
- "strum 0.27.1",
+ "strum 0.27.2",
  "theme",
  "vscode_theme",
  "workspace-hack",
@@ -18330,7 +18330,7 @@ dependencies = [
  "settings",
  "smallvec",
  "story",
- "strum 0.27.1",
+ "strum 0.27.2",
  "theme",
  "ui_macros",
  "util",
@@ -18714,7 +18714,7 @@ dependencies = [
  "anyhow",
  "schemars 1.0.1",
  "serde",
- "strum 0.27.1",
+ "strum 0.27.2",
  "workspace-hack",
 ]
 
@@ -20655,7 +20655,7 @@ dependencies = [
  "settings",
  "smallvec",
  "sqlez",
- "strum 0.27.1",
+ "strum 0.27.2",
  "task",
  "telemetry",
  "tempfile",
@@ -20980,7 +20980,7 @@ dependencies = [
  "anyhow",
  "schemars 1.0.1",
  "serde",
- "strum 0.27.1",
+ "strum 0.27.2",
  "workspace-hack",
 ]
 
@@ -21751,7 +21751,7 @@ dependencies = [
  "serde",
  "serde_json",
  "settings",
- "strum 0.27.1",
+ "strum 0.27.2",
  "telemetry",
  "telemetry_events",
  "theme",

Cargo.toml 🔗

@@ -651,7 +651,7 @@ sqlformat = "0.2"
 stacksafe = "0.1"
 streaming-iterator = "0.1"
 strsim = "0.11"
-strum = { version = "0.27.0", features = ["derive"] }
+strum = { version = "0.27.2", features = ["derive"] }
 subtle = "2.5.0"
 syn = { version = "2.0.101", features = ["full", "extra-traits", "visit-mut"] }
 sys-locale = "0.3.1"

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

@@ -125,7 +125,18 @@ fn default_buffer_font_weight() -> Option<FontWeight> {
 }
 
 /// Represents the selection of a theme, which can be either static or dynamic.
-#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq, Eq)]
+#[derive(
+    Clone,
+    Debug,
+    Serialize,
+    Deserialize,
+    JsonSchema,
+    MergeFrom,
+    PartialEq,
+    Eq,
+    strum::EnumDiscriminants,
+)]
+#[strum_discriminants(derive(strum::VariantArray, strum::VariantNames, strum::FromRepr))]
 #[serde(untagged)]
 pub enum ThemeSelection {
     /// A static theme selection, represented by a single theme name.
@@ -167,7 +178,18 @@ pub enum IconThemeSelection {
 ///
 /// `System` will select the theme based on the system's appearance.
 #[derive(
-    Debug, PartialEq, Eq, Clone, Copy, Default, Serialize, Deserialize, JsonSchema, MergeFrom,
+    Debug,
+    PartialEq,
+    Eq,
+    Clone,
+    Copy,
+    Default,
+    Serialize,
+    Deserialize,
+    JsonSchema,
+    MergeFrom,
+    strum::VariantArray,
+    strum::VariantNames,
 )]
 #[serde(rename_all = "snake_case")]
 pub enum ThemeMode {

crates/settings/src/settings_store.rs 🔗

@@ -535,30 +535,30 @@ impl SettingsStore {
     /// Returns the first file found that contains the value.
     /// The value will only be None if no file contains the value.
     /// I.e. if no file contains the value, returns `(File::Default, None)`
-    pub fn get_value_from_file<T>(
-        &self,
+    pub fn get_value_from_file<'a, T: 'a>(
+        &'a self,
         target_file: SettingsFile,
-        pick: fn(&SettingsContent) -> &Option<T>,
-    ) -> (SettingsFile, Option<&T>) {
+        pick: fn(&'a SettingsContent) -> Option<T>,
+    ) -> (SettingsFile, Option<T>) {
         self.get_value_from_file_inner(target_file, pick, true)
     }
 
     /// Same as `Self::get_value_from_file` except that it does not include the current file.
     /// Therefore it returns the value that was potentially overloaded by the target file.
-    pub fn get_value_up_to_file<T>(
-        &self,
+    pub fn get_value_up_to_file<'a, T: 'a>(
+        &'a self,
         target_file: SettingsFile,
-        pick: fn(&SettingsContent) -> &Option<T>,
-    ) -> (SettingsFile, Option<&T>) {
+        pick: fn(&'a SettingsContent) -> Option<T>,
+    ) -> (SettingsFile, Option<T>) {
         self.get_value_from_file_inner(target_file, pick, false)
     }
 
-    fn get_value_from_file_inner<T>(
-        &self,
+    fn get_value_from_file_inner<'a, T: 'a>(
+        &'a self,
         target_file: SettingsFile,
-        pick: fn(&SettingsContent) -> &Option<T>,
+        pick: fn(&'a SettingsContent) -> Option<T>,
         include_target_file: bool,
-    ) -> (SettingsFile, Option<&T>) {
+    ) -> (SettingsFile, Option<T>) {
         // todo(settings_ui): Add a metadata field for overriding the "overrides" tag, for contextually different settings
         //  e.g. disable AI isn't overridden, or a vec that gets extended instead or some such
 
@@ -588,7 +588,7 @@ impl SettingsStore {
             let Some(content) = self.get_content_for_file(file.clone()) else {
                 continue;
             };
-            if let Some(value) = pick(content).as_ref() {
+            if let Some(value) = pick(content) {
                 return (file, Some(value));
             }
         }
@@ -1774,11 +1774,16 @@ mod tests {
             )
             .unwrap();
 
-        fn get(content: &SettingsContent) -> &Option<u32> {
-            &content.project.all_languages.defaults.preferred_line_length
+        fn get(content: &SettingsContent) -> Option<&u32> {
+            content
+                .project
+                .all_languages
+                .defaults
+                .preferred_line_length
+                .as_ref()
         }
 
-        let default_value = get(&store.default_settings).unwrap();
+        let default_value = *get(&store.default_settings).unwrap();
 
         assert_eq!(
             store.get_value_from_file(SettingsFile::Project(local.clone()), get),
@@ -1841,8 +1846,13 @@ mod tests {
             .into_arc(),
         );
 
-        fn get(content: &SettingsContent) -> &Option<u32> {
-            &content.project.all_languages.defaults.preferred_line_length
+        fn get(content: &SettingsContent) -> Option<&u32> {
+            content
+                .project
+                .all_languages
+                .defaults
+                .preferred_line_length
+                .as_ref()
         }
 
         store

crates/settings_ui/src/page_data.rs 🔗

@@ -1,11 +1,12 @@
 use gpui::App;
 use settings::{LanguageSettingsContent, SettingsContent};
 use std::sync::Arc;
+use strum::IntoDiscriminant as _;
 use ui::{IntoElement, SharedString};
 
 use crate::{
-    LOCAL, SettingField, SettingItem, SettingsFieldMetadata, SettingsPage, SettingsPageItem,
-    SubPageLink, USER, all_language_names, sub_page_stack,
+    DynamicItem, LOCAL, SettingField, SettingItem, SettingsFieldMetadata, SettingsPage,
+    SettingsPageItem, SubPageLink, USER, all_language_names, sub_page_stack,
 };
 
 pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
@@ -18,8 +19,10 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
                     title: "Confirm Quit",
                     description: "Confirm before quitting Zed",
                     field: Box::new(SettingField {
-                        pick: |settings_content| &settings_content.workspace.confirm_quit,
-                        pick_mut: |settings_content| &mut settings_content.workspace.confirm_quit,
+                        pick: |settings_content| settings_content.workspace.confirm_quit.as_ref(),
+                        write: |settings_content, value| {
+                            settings_content.workspace.confirm_quit = value;
+                        },
                     }),
                     metadata: None,
                     files: USER,
@@ -29,10 +32,13 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
                     description: "What to do when using the 'close active item' action with no tabs",
                     field: Box::new(SettingField {
                         pick: |settings_content| {
-                            &settings_content.workspace.when_closing_with_no_tabs
+                            settings_content
+                                .workspace
+                                .when_closing_with_no_tabs
+                                .as_ref()
                         },
-                        pick_mut: |settings_content| {
-                            &mut settings_content.workspace.when_closing_with_no_tabs
+                        write: |settings_content, value| {
+                            settings_content.workspace.when_closing_with_no_tabs = value;
                         },
                     }),
                     metadata: None,
@@ -42,9 +48,11 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
                     title: "On Last Window Closed",
                     description: "What to do when the last window is closed",
                     field: Box::new(SettingField {
-                        pick: |settings_content| &settings_content.workspace.on_last_window_closed,
-                        pick_mut: |settings_content| {
-                            &mut settings_content.workspace.on_last_window_closed
+                        pick: |settings_content| {
+                            settings_content.workspace.on_last_window_closed.as_ref()
+                        },
+                        write: |settings_content, value| {
+                            settings_content.workspace.on_last_window_closed = value;
                         },
                     }),
                     metadata: None,
@@ -55,10 +63,10 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
                     description: "Use native OS dialogs for 'Open' and 'Save As'",
                     field: Box::new(SettingField {
                         pick: |settings_content| {
-                            &settings_content.workspace.use_system_path_prompts
+                            settings_content.workspace.use_system_path_prompts.as_ref()
                         },
-                        pick_mut: |settings_content| {
-                            &mut settings_content.workspace.use_system_path_prompts
+                        write: |settings_content, value| {
+                            settings_content.workspace.use_system_path_prompts = value;
                         },
                     }),
                     metadata: None,
@@ -68,9 +76,11 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
                     title: "Use System Prompts",
                     description: "Use native OS dialogs for confirmations",
                     field: Box::new(SettingField {
-                        pick: |settings_content| &settings_content.workspace.use_system_prompts,
-                        pick_mut: |settings_content| {
-                            &mut settings_content.workspace.use_system_prompts
+                        pick: |settings_content| {
+                            settings_content.workspace.use_system_prompts.as_ref()
+                        },
+                        write: |settings_content, value| {
+                            settings_content.workspace.use_system_prompts = value;
                         },
                     }),
                     metadata: None,
@@ -80,9 +90,11 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
                     title: "Redact Private Values",
                     description: "Hide the values of variables in private files",
                     field: Box::new(SettingField {
-                        pick: |settings_content| &settings_content.editor.redact_private_values,
-                        pick_mut: |settings_content| {
-                            &mut settings_content.editor.redact_private_values
+                        pick: |settings_content| {
+                            settings_content.editor.redact_private_values.as_ref()
+                        },
+                        write: |settings_content, value| {
+                            settings_content.editor.redact_private_values = value;
                         },
                     }),
                     metadata: None,
@@ -94,10 +106,10 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
                     field: Box::new(
                         SettingField {
                             pick: |settings_content| {
-                                &settings_content.project.worktree.private_files
+                                settings_content.project.worktree.private_files.as_ref()
                             },
-                            pick_mut: |settings_content| {
-                                &mut settings_content.project.worktree.private_files
+                            write: |settings_content, value| {
+                                settings_content.project.worktree.private_files = value;
                             },
                         }
                         .unimplemented(),
@@ -110,15 +122,17 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
                     title: "Restore Unsaved Buffers",
                     description: "Whether or not to restore unsaved buffers on restart",
                     field: Box::new(SettingField {
-                        pick: |settings_content| match settings_content.session.as_ref() {
-                            Some(session) => &session.restore_unsaved_buffers,
-                            None => &None,
+                        pick: |settings_content| {
+                            settings_content
+                                .session
+                                .as_ref()
+                                .and_then(|session| session.restore_unsaved_buffers.as_ref())
                         },
-                        pick_mut: |settings_content| {
-                            &mut settings_content
+                        write: |settings_content, value| {
+                            settings_content
                                 .session
                                 .get_or_insert_default()
-                                .restore_unsaved_buffers
+                                .restore_unsaved_buffers = value;
                         },
                     }),
                     metadata: None,
@@ -128,9 +142,11 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
                     title: "Restore On Startup",
                     description: "What to restore from the previous session when opening Zed",
                     field: Box::new(SettingField {
-                        pick: |settings_content| &settings_content.workspace.restore_on_startup,
-                        pick_mut: |settings_content| {
-                            &mut settings_content.workspace.restore_on_startup
+                        pick: |settings_content| {
+                            settings_content.workspace.restore_on_startup.as_ref()
+                        },
+                        write: |settings_content, value| {
+                            settings_content.workspace.restore_on_startup = value;
                         },
                     }),
                     metadata: None,
@@ -144,9 +160,11 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
                     description: "Which settings should be activated only in Preview build of Zed",
                     field: Box::new(
                         SettingField {
-                            pick: |settings_content| &settings_content.workspace.use_system_prompts,
-                            pick_mut: |settings_content| {
-                                &mut settings_content.workspace.use_system_prompts
+                            pick: |settings_content| {
+                                settings_content.workspace.use_system_prompts.as_ref()
+                            },
+                            write: |settings_content, value| {
+                                settings_content.workspace.use_system_prompts = value;
                             },
                         }
                         .unimplemented(),
@@ -159,9 +177,11 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
                     description: "Any number of settings profiles that are temporarily applied on top of your existing user settings",
                     field: Box::new(
                         SettingField {
-                            pick: |settings_content| &settings_content.workspace.use_system_prompts,
-                            pick_mut: |settings_content| {
-                                &mut settings_content.workspace.use_system_prompts
+                            pick: |settings_content| {
+                                settings_content.workspace.use_system_prompts.as_ref()
+                            },
+                            write: |settings_content, value| {
+                                settings_content.workspace.use_system_prompts = value;
                             },
                         }
                         .unimplemented(),
@@ -174,17 +194,16 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
                     description: "Send debug information like crash reports",
                     field: Box::new(SettingField {
                         pick: |settings_content| {
-                            if let Some(telemetry) = &settings_content.telemetry {
-                                &telemetry.diagnostics
-                            } else {
-                                &None
-                            }
+                            settings_content
+                                .telemetry
+                                .as_ref()
+                                .and_then(|telemetry| telemetry.diagnostics.as_ref())
                         },
-                        pick_mut: |settings_content| {
-                            &mut settings_content
+                        write: |settings_content, value| {
+                            settings_content
                                 .telemetry
                                 .get_or_insert_default()
-                                .diagnostics
+                                .diagnostics = value;
                         },
                     }),
                     metadata: None,
@@ -195,14 +214,13 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
                     description: "Send anonymized usage data like what languages you're using Zed with",
                     field: Box::new(SettingField {
                         pick: |settings_content| {
-                            if let Some(telemetry) = &settings_content.telemetry {
-                                &telemetry.metrics
-                            } else {
-                                &None
-                            }
+                            settings_content
+                                .telemetry
+                                .as_ref()
+                                .and_then(|telemetry| telemetry.metrics.as_ref())
                         },
-                        pick_mut: |settings_content| {
-                            &mut settings_content.telemetry.get_or_insert_default().metrics
+                        write: |settings_content, value| {
+                            settings_content.telemetry.get_or_insert_default().metrics = value;
                         },
                     }),
                     metadata: None,
@@ -213,8 +231,10 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
                     title: "Auto Update",
                     description: "Whether or not to automatically check for updates",
                     field: Box::new(SettingField {
-                        pick: |settings_content| &settings_content.auto_update,
-                        pick_mut: |settings_content| &mut settings_content.auto_update,
+                        pick: |settings_content| settings_content.auto_update.as_ref(),
+                        write: |settings_content, value| {
+                            settings_content.auto_update = value;
+                        },
                     }),
                     metadata: None,
                     files: USER,
@@ -225,19 +245,167 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
             title: "Appearance",
             items: vec![
                 SettingsPageItem::SectionHeader("Theme"),
-                // todo(settings_ui): Figure out how we want to add these
-                SettingsPageItem::SettingItem(SettingItem {
-                    files: USER,
-                    title: "Theme Mode",
-                    description: "How to select the theme",
-                    field: Box::new(
-                        SettingField {
-                            pick: |settings_content| &settings_content.theme.theme,
-                            pick_mut: |settings_content| &mut settings_content.theme.theme,
+                SettingsPageItem::DynamicItem(DynamicItem {
+                    discriminant: SettingItem {
+                        files: USER,
+                        title: "Theme Mode",
+                        description: "How to select the theme",
+                        field: Box::new(SettingField {
+                            pick: |settings_content| {
+                                Some(&<<settings::ThemeSelection as strum::IntoDiscriminant>::Discriminant as strum::VariantArray>::VARIANTS[
+                                    settings_content
+                                        .theme
+                                        .theme
+                                        .as_ref()?
+                                        .discriminant() as usize])
+                            },
+                            write: |settings_content, value| {
+                                let Some(value) = value else {
+                                    return;
+                                };
+                                let settings_value = settings_content.theme.theme.as_mut().expect("Has Default");
+                                *settings_value = match value {
+                                    settings::ThemeSelectionDiscriminants::Static => {
+                                        let name = match settings_value {
+                                            settings::ThemeSelection::Static(_) => return,
+                                            settings::ThemeSelection::Dynamic { mode, light, dark } => {
+                                                match mode {
+                                                    theme::ThemeMode::Light => light.clone(),
+                                                    theme::ThemeMode::Dark => dark.clone(),
+                                                    theme::ThemeMode::System => dark.clone(), // no cx, can't determine correct choice
+                                                }
+                                            },
+                                        };
+                                        settings::ThemeSelection::Static(name)
+                                    },
+                                    settings::ThemeSelectionDiscriminants::Dynamic => {
+                                        let static_name = match settings_value {
+                                            settings::ThemeSelection::Static(theme_name) => theme_name.clone(),
+                                            settings::ThemeSelection::Dynamic {..} => return,
+                                        };
+
+                                        settings::ThemeSelection::Dynamic {
+                                            mode: settings::ThemeMode::System,
+                                            light: static_name.clone(),
+                                            dark: static_name,
+                                        }
+                                    },
+                                };
+                            },
+                        }),
+                        metadata: None,
+                    },
+                    pick_discriminant: |settings_content| {
+                        Some(settings_content.theme.theme.as_ref()?.discriminant() as usize)
+                    },
+                    fields: <<settings::ThemeSelection as strum::IntoDiscriminant>::Discriminant as strum::VariantArray>::VARIANTS.into_iter().map(|variant| {
+                        match variant {
+                            settings::ThemeSelectionDiscriminants::Static => vec![
+                                SettingItem {
+                                    files: USER,
+                                    title: "Theme Name",
+                                    description: "The Name Of The Theme To Use",
+                                    field: Box::new(SettingField {
+                                        pick: |settings_content| {
+                                            match settings_content.theme.theme.as_ref() {
+                                                Some(settings::ThemeSelection::Static(name)) => Some(name),
+                                                _ => None
+                                            }
+                                        },
+                                        write: |settings_content, value| {
+                                            let Some(value) = value else {
+                                                return;
+                                            };
+                                            match settings_content
+                                                .theme
+                                                .theme.as_mut() {
+                                                    Some(settings::ThemeSelection::Static(theme_name)) => *theme_name = value,
+                                                    _ => return
+                                                }
+                                        },
+                                    }),
+                                    metadata: None,
+                                }
+                            ],
+                            settings::ThemeSelectionDiscriminants::Dynamic => vec![
+                                SettingItem {
+                                    files: USER,
+                                    title: "Mode",
+                                    description: "How To Determine Whether to Use a Light or Dark Theme",
+                                    field: Box::new(SettingField {
+                                        pick: |settings_content| {
+                                            match settings_content.theme.theme.as_ref() {
+                                                Some(settings::ThemeSelection::Dynamic { mode, ..}) => Some(mode),
+                                                _ => None
+                                            }
+                                        },
+                                        write: |settings_content, value| {
+                                            let Some(value) = value else {
+                                                return;
+                                            };
+                                            match settings_content
+                                                .theme
+                                                .theme.as_mut() {
+                                                    Some(settings::ThemeSelection::Dynamic{ mode, ..}) => *mode = value,
+                                                    _ => return
+                                                }
+                                        },
+                                    }),
+                                    metadata: None,
+                                },
+                                SettingItem {
+                                    files: USER,
+                                    title: "Light Theme",
+                                    description: "The Theme To Use When Mode Is Set To Light, Or When Mode Is Set To System And The System Is In Light Mode",
+                                    field: Box::new(SettingField {
+                                        pick: |settings_content| {
+                                            match settings_content.theme.theme.as_ref() {
+                                                Some(settings::ThemeSelection::Dynamic { light, ..}) => Some(light),
+                                                _ => None
+                                            }
+                                        },
+                                        write: |settings_content, value| {
+                                            let Some(value) = value else {
+                                                return;
+                                            };
+                                            match settings_content
+                                                .theme
+                                                .theme.as_mut() {
+                                                    Some(settings::ThemeSelection::Dynamic{ light, ..}) => *light = value,
+                                                    _ => return
+                                                }
+                                        },
+                                    }),
+                                    metadata: None,
+                                },
+                                SettingItem {
+                                    files: USER,
+                                    title: "Dark Theme",
+                                    description: "The Theme To Use When Mode Is Set To Dark, Or When Mode Is Set To System And The System Is In Dark Mode",
+                                    field: Box::new(SettingField {
+                                        pick: |settings_content| {
+                                            match settings_content.theme.theme.as_ref() {
+                                                Some(settings::ThemeSelection::Dynamic { dark, ..}) => Some(dark),
+                                                _ => None
+                                            }
+                                        },
+                                        write: |settings_content, value| {
+                                            let Some(value) = value else {
+                                                return;
+                                            };
+                                            match settings_content
+                                                .theme
+                                                .theme.as_mut() {
+                                                    Some(settings::ThemeSelection::Dynamic{ dark, ..}) => *dark = value,
+                                                    _ => return
+                                                }
+                                        },
+                                    }),
+                                    metadata: None,
+                                }
+                            ],
                         }
-                        .unimplemented(),
-                    ),
-                    metadata: None,
+                    }).collect(),
                 }),
                 SettingsPageItem::SettingItem(SettingItem {
                     files: USER,
@@ -247,8 +415,8 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
                     description: "Choose the icon theme for file explorer",
                     field: Box::new(
                         SettingField {
-                            pick: |settings_content| &settings_content.theme.icon_theme,
-                            pick_mut: |settings_content| &mut settings_content.theme.icon_theme,
+                            pick: |settings_content| settings_content.theme.icon_theme.as_ref(),
+                            write: |settings_content, value|{  settings_content.theme.icon_theme = value;},
                         }
                         .unimplemented(),
                     ),
@@ -259,8 +427,8 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
                     title: "Font Family",
                     description: "Font family for editor text",
                     field: Box::new(SettingField {
-                        pick: |settings_content| &settings_content.theme.buffer_font_family,
-                        pick_mut: |settings_content| &mut settings_content.theme.buffer_font_family,
+                        pick: |settings_content| settings_content.theme.buffer_font_family.as_ref(),
+                        write: |settings_content, value|{  settings_content.theme.buffer_font_family = value;},
                     }),
                     metadata: None,
                     files: USER,
@@ -269,8 +437,8 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
                     title: "Font Size",
                     description: "Font size for editor text",
                     field: Box::new(SettingField {
-                        pick: |settings_content| &settings_content.theme.buffer_font_size,
-                        pick_mut: |settings_content| &mut settings_content.theme.buffer_font_size,
+                        pick: |settings_content| settings_content.theme.buffer_font_size.as_ref(),
+                        write: |settings_content, value|{  settings_content.theme.buffer_font_size = value;},
                     }),
                     metadata: None,
                     files: USER,
@@ -279,8 +447,8 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
                     title: "Font Weight",
                     description: "Font weight for editor text (100-900)",
                     field: Box::new(SettingField {
-                        pick: |settings_content| &settings_content.theme.buffer_font_weight,
-                        pick_mut: |settings_content| &mut settings_content.theme.buffer_font_weight,
+                        pick: |settings_content| settings_content.theme.buffer_font_weight.as_ref(),
+                        write: |settings_content, value|{  settings_content.theme.buffer_font_weight = value;},
                     }),
                     metadata: None,
                     files: USER,
@@ -292,9 +460,12 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
                     description: "Line height for editor text",
                     field: Box::new(
                         SettingField {
-                            pick: |settings_content| &settings_content.theme.buffer_line_height,
-                            pick_mut: |settings_content| {
-                                &mut settings_content.theme.buffer_line_height
+                            pick: |settings_content| {
+                                settings_content.theme.buffer_line_height.as_ref()
+                            },
+                            write: |settings_content, value| {
+                                settings_content.theme.buffer_line_height = value;
+
                             },
                         }
                         .unimplemented(),
@@ -307,9 +478,12 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
                     description: "The OpenType features to enable for rendering in text buffers.",
                     field: Box::new(
                         SettingField {
-                            pick: |settings_content| &settings_content.theme.buffer_font_features,
-                            pick_mut: |settings_content| {
-                                &mut settings_content.theme.buffer_font_features
+                            pick: |settings_content| {
+                                settings_content.theme.buffer_font_features.as_ref()
+                            },
+                            write: |settings_content, value| {
+                                settings_content.theme.buffer_font_features = value;
+
                             },
                         }
                         .unimplemented(),
@@ -322,9 +496,12 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
                     description: "The font fallbacks to use for rendering in text buffers.",
                     field: Box::new(
                         SettingField {
-                            pick: |settings_content| &settings_content.theme.buffer_font_fallbacks,
-                            pick_mut: |settings_content| {
-                                &mut settings_content.theme.buffer_font_fallbacks
+                            pick: |settings_content| {
+                                settings_content.theme.buffer_font_fallbacks.as_ref()
+                            },
+                            write: |settings_content, value| {
+                                settings_content.theme.buffer_font_fallbacks = value;
+
                             },
                         }
                         .unimplemented(),
@@ -336,8 +513,8 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
                     title: "Font Family",
                     description: "Font family for UI elements",
                     field: Box::new(SettingField {
-                        pick: |settings_content| &settings_content.theme.ui_font_family,
-                        pick_mut: |settings_content| &mut settings_content.theme.ui_font_family,
+                        pick: |settings_content| settings_content.theme.ui_font_family.as_ref(),
+                        write: |settings_content, value|{  settings_content.theme.ui_font_family = value;},
                     }),
                     metadata: None,
                     files: USER,
@@ -346,8 +523,8 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
                     title: "Font Size",
                     description: "Font size for UI elements",
                     field: Box::new(SettingField {
-                        pick: |settings_content| &settings_content.theme.ui_font_size,
-                        pick_mut: |settings_content| &mut settings_content.theme.ui_font_size,
+                        pick: |settings_content| settings_content.theme.ui_font_size.as_ref(),
+                        write: |settings_content, value|{  settings_content.theme.ui_font_size = value;},
                     }),
                     metadata: None,
                     files: USER,
@@ -356,8 +533,8 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
                     title: "Font Weight",
                     description: "Font weight for UI elements (100-900)",
                     field: Box::new(SettingField {
-                        pick: |settings_content| &settings_content.theme.ui_font_weight,
-                        pick_mut: |settings_content| &mut settings_content.theme.ui_font_weight,
+                        pick: |settings_content| settings_content.theme.ui_font_weight.as_ref(),
+                        write: |settings_content, value|{  settings_content.theme.ui_font_weight = value;},
                     }),
                     metadata: None,
                     files: USER,
@@ -368,9 +545,12 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
                     description: "The OpenType features to enable for rendering in UI elements.",
                     field: Box::new(
                         SettingField {
-                            pick: |settings_content| &settings_content.theme.ui_font_features,
-                            pick_mut: |settings_content| {
-                                &mut settings_content.theme.ui_font_features
+                            pick: |settings_content| {
+                                settings_content.theme.ui_font_features.as_ref()
+                            },
+                            write: |settings_content, value| {
+                                settings_content.theme.ui_font_features = value;
+
                             },
                         }
                         .unimplemented(),
@@ -383,9 +563,12 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
                     description: "The font fallbacks to use for rendering in the UI.",
                     field: Box::new(
                         SettingField {
-                            pick: |settings_content| &settings_content.theme.ui_font_fallbacks,
-                            pick_mut: |settings_content| {
-                                &mut settings_content.theme.ui_font_fallbacks
+                            pick: |settings_content| {
+                                settings_content.theme.ui_font_fallbacks.as_ref()
+                            },
+                            write: |settings_content, value| {
+                                settings_content.theme.ui_font_fallbacks = value;
+
                             },
                         }
                         .unimplemented(),
@@ -398,13 +581,13 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
                     description: "Font size for agent response text in the agent panel. Falls back to the regular UI font size.",
                     field: Box::new(SettingField {
                         pick: |settings_content| {
-                            if settings_content.theme.agent_ui_font_size.is_some() {
-                                &settings_content.theme.agent_ui_font_size
-                            } else {
-                                &settings_content.theme.ui_font_size
-                            }
+                            settings_content
+                                .theme
+                                .agent_ui_font_size
+                                .as_ref()
+                                .or(settings_content.theme.ui_font_size.as_ref())
                         },
-                        pick_mut: |settings_content| &mut settings_content.theme.agent_ui_font_size,
+                        write: |settings_content, value|{  settings_content.theme.agent_ui_font_size = value;},
                     }),
                     metadata: None,
                     files: USER,
@@ -413,9 +596,16 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
                     title: "Buffer Font Size",
                     description: "Font size for user messages text in the agent panel",
                     field: Box::new(SettingField {
-                        pick: |settings_content| &settings_content.theme.agent_buffer_font_size,
-                        pick_mut: |settings_content| {
-                            &mut settings_content.theme.agent_buffer_font_size
+                        pick: |settings_content| {
+                            settings_content
+                                .theme
+                                .agent_buffer_font_size
+                                .as_ref()
+                                .or(settings_content.theme.buffer_font_size.as_ref())
+                        },
+                        write: |settings_content, value| {
+                            settings_content.theme.agent_buffer_font_size = value;
+
                         },
                     }),
                     metadata: None,
@@ -426,9 +616,12 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
                     title: "Multi Cursor Modifier",
                     description: "Modifier key for adding multiple cursors",
                     field: Box::new(SettingField {
-                        pick: |settings_content| &settings_content.editor.multi_cursor_modifier,
-                        pick_mut: |settings_content| {
-                            &mut settings_content.editor.multi_cursor_modifier
+                        pick: |settings_content| {
+                            settings_content.editor.multi_cursor_modifier.as_ref()
+                        },
+                        write: |settings_content, value| {
+                            settings_content.editor.multi_cursor_modifier = value;
+
                         },
                     }),
                     metadata: None,
@@ -438,8 +631,8 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
                     title: "Cursor Blink",
                     description: "Whether the cursor blinks in the editor",
                     field: Box::new(SettingField {
-                        pick: |settings_content| &settings_content.editor.cursor_blink,
-                        pick_mut: |settings_content| &mut settings_content.editor.cursor_blink,
+                        pick: |settings_content| settings_content.editor.cursor_blink.as_ref(),
+                        write: |settings_content, value|{  settings_content.editor.cursor_blink = value;},
                     }),
                     metadata: None,
                     files: USER,
@@ -448,8 +641,8 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
                     title: "Cursor Shape",
                     description: "Cursor shape for the editor",
                     field: Box::new(SettingField {
-                        pick: |settings_content| &settings_content.editor.cursor_shape,
-                        pick_mut: |settings_content| &mut settings_content.editor.cursor_shape,
+                        pick: |settings_content| settings_content.editor.cursor_shape.as_ref(),
+                        write: |settings_content, value|{  settings_content.editor.cursor_shape = value;},
                     }),
                     metadata: None,
                     files: USER,
@@ -458,8 +651,8 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
                     title: "Hide Mouse",
                     description: "When to hide the mouse cursor",
                     field: Box::new(SettingField {
-                        pick: |settings_content| &settings_content.editor.hide_mouse,
-                        pick_mut: |settings_content| &mut settings_content.editor.hide_mouse,
+                        pick: |settings_content| settings_content.editor.hide_mouse.as_ref(),
+                        write: |settings_content, value|{  settings_content.editor.hide_mouse = value;},
                     }),
                     metadata: None,
                     files: USER,
@@ -469,9 +662,12 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
                     title: "Unnecessary Code Fade",
                     description: "How much to fade out unused code (0.0 - 0.9)",
                     field: Box::new(SettingField {
-                        pick: |settings_content| &settings_content.theme.unnecessary_code_fade,
-                        pick_mut: |settings_content| {
-                            &mut settings_content.theme.unnecessary_code_fade
+                        pick: |settings_content| {
+                            settings_content.theme.unnecessary_code_fade.as_ref()
+                        },
+                        write: |settings_content, value| {
+                            settings_content.theme.unnecessary_code_fade = value;
+
                         },
                     }),
                     metadata: None,
@@ -481,9 +677,12 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
                     title: "Current Line Highlight",
                     description: "How to highlight the current line",
                     field: Box::new(SettingField {
-                        pick: |settings_content| &settings_content.editor.current_line_highlight,
-                        pick_mut: |settings_content| {
-                            &mut settings_content.editor.current_line_highlight
+                        pick: |settings_content| {
+                            settings_content.editor.current_line_highlight.as_ref()
+                        },
+                        write: |settings_content, value| {
+                            settings_content.editor.current_line_highlight = value;
+
                         },
                     }),
                     metadata: None,
@@ -493,9 +692,12 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
                     title: "Selection Highlight",
                     description: "Highlight all occurrences of selected text",
                     field: Box::new(SettingField {
-                        pick: |settings_content| &settings_content.editor.selection_highlight,
-                        pick_mut: |settings_content| {
-                            &mut settings_content.editor.selection_highlight
+                        pick: |settings_content| {
+                            settings_content.editor.selection_highlight.as_ref()
+                        },
+                        write: |settings_content, value| {
+                            settings_content.editor.selection_highlight = value;
+
                         },
                     }),
                     metadata: None,
@@ -505,8 +707,8 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
                     title: "Rounded Selection",
                     description: "Whether the text selection should have rounded corners",
                     field: Box::new(SettingField {
-                        pick: |settings_content| &settings_content.editor.rounded_selection,
-                        pick_mut: |settings_content| &mut settings_content.editor.rounded_selection,
+                        pick: |settings_content| settings_content.editor.rounded_selection.as_ref(),
+                        write: |settings_content, value|{  settings_content.editor.rounded_selection = value;},
                     }),
                     metadata: None,
                     files: USER,
@@ -516,10 +718,14 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
                     description: "The minimum APCA perceptual contrast to maintain when rendering text over highlight backgrounds",
                     field: Box::new(SettingField {
                         pick: |settings_content| {
-                            &settings_content.editor.minimum_contrast_for_highlights
+                            settings_content
+                                .editor
+                                .minimum_contrast_for_highlights
+                                .as_ref()
                         },
-                        pick_mut: |settings_content| {
-                            &mut settings_content.editor.minimum_contrast_for_highlights
+                        write: |settings_content, value| {
+                            settings_content.editor.minimum_contrast_for_highlights = value;
+
                         },
                     }),
                     metadata: None,
@@ -531,18 +737,20 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
                     description: "Show wrap guides (vertical rulers)",
                     field: Box::new(SettingField {
                         pick: |settings_content| {
-                            &settings_content
+                            settings_content
                                 .project
                                 .all_languages
                                 .defaults
                                 .show_wrap_guides
+                                .as_ref()
                         },
-                        pick_mut: |settings_content| {
-                            &mut settings_content
+                        write: |settings_content, value| {
+                            settings_content
+
                                 .project
                                 .all_languages
                                 .defaults
-                                .show_wrap_guides
+                                .show_wrap_guides = value;
                         },
                     }),
                     metadata: None,
@@ -555,10 +763,16 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
                     field: Box::new(
                         SettingField {
                             pick: |settings_content| {
-                                &settings_content.project.all_languages.defaults.wrap_guides
+                                settings_content
+                                    .project
+                                    .all_languages
+                                    .defaults
+                                    .wrap_guides
+                                    .as_ref()
                             },
-                            pick_mut: |settings_content| {
-                                &mut settings_content.project.all_languages.defaults.wrap_guides
+                            write: |settings_content, value| {
+                                settings_content.project.all_languages.defaults.wrap_guides = value;
+
                             },
                         }
                         .unimplemented(),
@@ -576,8 +790,10 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
                     title: "Base Keymap",
                     description: "The name of a base set of key bindings to use",
                     field: Box::new(SettingField {
-                        pick: |settings_content| &settings_content.base_keymap,
-                        pick_mut: |settings_content| &mut settings_content.base_keymap,
+                        pick: |settings_content| settings_content.base_keymap.as_ref(),
+                        write: |settings_content, value| {
+                            settings_content.base_keymap = value;
+                        },
                     }),
                     metadata: Some(Box::new(SettingsFieldMetadata {
                         should_do_titlecase: Some(false),
@@ -592,8 +808,10 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
                     title: "Vim Mode",
                     description: "Enable vim modes and key bindings",
                     field: Box::new(SettingField {
-                        pick: |settings_content| &settings_content.vim_mode,
-                        pick_mut: |settings_content| &mut settings_content.vim_mode,
+                        pick: |settings_content| settings_content.vim_mode.as_ref(),
+                        write: |settings_content, value| {
+                            settings_content.vim_mode = value;
+                        },
                     }),
                     metadata: None,
                     files: USER,
@@ -602,8 +820,10 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
                     title: "Helix Mode",
                     description: "Enable helix modes and key bindings",
                     field: Box::new(SettingField {
-                        pick: |settings_content| &settings_content.helix_mode,
-                        pick_mut: |settings_content| &mut settings_content.helix_mode,
+                        pick: |settings_content| settings_content.helix_mode.as_ref(),
+                        write: |settings_content, value| {
+                            settings_content.helix_mode = value;
+                        },
                     }),
                     metadata: None,
                     files: USER,
@@ -620,9 +840,11 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
                         description: "When to Auto Save Buffer Changes",
                         field: Box::new(
                             SettingField {
-                                pick: |settings_content| &settings_content.workspace.autosave,
-                                pick_mut: |settings_content| {
-                                    &mut settings_content.workspace.autosave
+                                pick: |settings_content| {
+                                    settings_content.workspace.autosave.as_ref()
+                                },
+                                write: |settings_content, value| {
+                                    settings_content.workspace.autosave = value;
                                 },
                             }
                             .unimplemented(),
@@ -636,10 +858,10 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
                         description: "What to do when multibuffer is double-clicked in some of its excerpts",
                         field: Box::new(SettingField {
                             pick: |settings_content| {
-                                &settings_content.editor.double_click_in_multibuffer
+                                settings_content.editor.double_click_in_multibuffer.as_ref()
                             },
-                            pick_mut: |settings_content| {
-                                &mut settings_content.editor.double_click_in_multibuffer
+                            write: |settings_content, value| {
+                                settings_content.editor.double_click_in_multibuffer = value;
                             },
                         }),
                         metadata: None,
@@ -649,9 +871,11 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
                         title: "Expand Excerpt Lines",
                         description: "How many lines to expand the multibuffer excerpts by default",
                         field: Box::new(SettingField {
-                            pick: |settings_content| &settings_content.editor.expand_excerpt_lines,
-                            pick_mut: |settings_content| {
-                                &mut settings_content.editor.expand_excerpt_lines
+                            pick: |settings_content| {
+                                settings_content.editor.expand_excerpt_lines.as_ref()
+                            },
+                            write: |settings_content, value| {
+                                settings_content.editor.expand_excerpt_lines = value;
                             },
                         }),
                         metadata: None,
@@ -661,9 +885,11 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
                         title: "Excerpt Context Lines",
                         description: "How many lines of context to provide in multibuffer excerpts by default",
                         field: Box::new(SettingField {
-                            pick: |settings_content| &settings_content.editor.excerpt_context_lines,
-                            pick_mut: |settings_content| {
-                                &mut settings_content.editor.excerpt_context_lines
+                            pick: |settings_content| {
+                                settings_content.editor.excerpt_context_lines.as_ref()
+                            },
+                            write: |settings_content, value| {
+                                settings_content.editor.excerpt_context_lines = value;
                             },
                         }),
                         metadata: None,
@@ -674,17 +900,18 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
                         description: "Default depth to expand outline items in the current file",
                         field: Box::new(SettingField {
                             pick: |settings_content| {
-                                if let Some(outline_panel) = &settings_content.outline_panel {
-                                    &outline_panel.expand_outlines_with_depth
-                                } else {
-                                    &None
-                                }
+                                settings_content
+                                    .outline_panel
+                                    .as_ref()
+                                    .and_then(|outline_panel| {
+                                        outline_panel.expand_outlines_with_depth.as_ref()
+                                    })
                             },
-                            pick_mut: |settings_content| {
-                                &mut settings_content
+                            write: |settings_content, value| {
+                                settings_content
                                     .outline_panel
                                     .get_or_insert_default()
-                                    .expand_outlines_with_depth
+                                    .expand_outlines_with_depth = value;
                             },
                         }),
                         metadata: None,
@@ -696,10 +923,10 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
                         description: "Whether the editor will scroll beyond the last line",
                         field: Box::new(SettingField {
                             pick: |settings_content| {
-                                &settings_content.editor.scroll_beyond_last_line
+                                settings_content.editor.scroll_beyond_last_line.as_ref()
                             },
-                            pick_mut: |settings_content| {
-                                &mut settings_content.editor.scroll_beyond_last_line
+                            write: |settings_content, value| {
+                                settings_content.editor.scroll_beyond_last_line = value;
                             },
                         }),
                         metadata: None,
@@ -710,10 +937,10 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
                         description: "The number of lines to keep above/below the cursor when auto-scrolling",
                         field: Box::new(SettingField {
                             pick: |settings_content| {
-                                &settings_content.editor.vertical_scroll_margin
+                                settings_content.editor.vertical_scroll_margin.as_ref()
                             },
-                            pick_mut: |settings_content| {
-                                &mut settings_content.editor.vertical_scroll_margin
+                            write: |settings_content, value| {
+                                settings_content.editor.vertical_scroll_margin = value;
                             },
                         }),
                         metadata: None,
@@ -724,10 +951,10 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
                         description: "The number of characters to keep on either side when scrolling with the mouse",
                         field: Box::new(SettingField {
                             pick: |settings_content| {
-                                &settings_content.editor.horizontal_scroll_margin
+                                settings_content.editor.horizontal_scroll_margin.as_ref()
                             },
-                            pick_mut: |settings_content| {
-                                &mut settings_content.editor.horizontal_scroll_margin
+                            write: |settings_content, value| {
+                                settings_content.editor.horizontal_scroll_margin = value;
                             },
                         }),
                         metadata: None,
@@ -737,9 +964,11 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
                         title: "Scroll Sensitivity",
                         description: "Scroll sensitivity multiplier for both horizontal and vertical scrolling",
                         field: Box::new(SettingField {
-                            pick: |settings_content| &settings_content.editor.scroll_sensitivity,
-                            pick_mut: |settings_content| {
-                                &mut settings_content.editor.scroll_sensitivity
+                            pick: |settings_content| {
+                                settings_content.editor.scroll_sensitivity.as_ref()
+                            },
+                            write: |settings_content, value| {
+                                settings_content.editor.scroll_sensitivity = value;
                             },
                         }),
                         metadata: None,
@@ -750,10 +979,10 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
                         description: "Fast Scroll sensitivity multiplier for both horizontal and vertical scrolling",
                         field: Box::new(SettingField {
                             pick: |settings_content| {
-                                &settings_content.editor.fast_scroll_sensitivity
+                                settings_content.editor.fast_scroll_sensitivity.as_ref()
                             },
-                            pick_mut: |settings_content| {
-                                &mut settings_content.editor.fast_scroll_sensitivity
+                            write: |settings_content, value| {
+                                settings_content.editor.fast_scroll_sensitivity = value;
                             },
                         }),
                         metadata: None,
@@ -763,9 +992,11 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
                         title: "Autoscroll On Clicks",
                         description: "Whether to scroll when clicking near the edge of the visible text area",
                         field: Box::new(SettingField {
-                            pick: |settings_content| &settings_content.editor.autoscroll_on_clicks,
-                            pick_mut: |settings_content| {
-                                &mut settings_content.editor.autoscroll_on_clicks
+                            pick: |settings_content| {
+                                settings_content.editor.autoscroll_on_clicks.as_ref()
+                            },
+                            write: |settings_content, value| {
+                                settings_content.editor.autoscroll_on_clicks = value;
                             },
                         }),
                         metadata: None,

crates/settings_ui/src/settings_ui.rs 🔗

@@ -6,10 +6,10 @@ use editor::{Editor, EditorEvent};
 use feature_flags::FeatureFlag;
 use fuzzy::StringMatchCandidate;
 use gpui::{
-    Action, App, Div, Entity, FocusHandle, Focusable, FontWeight, Global, ListState,
-    ReadGlobal as _, ScrollHandle, Stateful, Subscription, Task, TitlebarOptions,
-    UniformListScrollHandle, Window, WindowBounds, WindowHandle, WindowOptions, actions, div, list,
-    point, prelude::*, px, size, uniform_list,
+    Action, App, Div, Entity, FocusHandle, Focusable, Global, ListState, ReadGlobal as _,
+    ScrollHandle, Stateful, Subscription, Task, TitlebarOptions, UniformListScrollHandle, Window,
+    WindowBounds, WindowHandle, WindowOptions, actions, div, list, point, prelude::*, px, size,
+    uniform_list,
 };
 use heck::ToTitleCase as _;
 use project::WorktreeId;
@@ -84,8 +84,8 @@ actions!(
 struct FocusFile(pub u32);
 
 struct SettingField<T: 'static> {
-    pick: fn(&SettingsContent) -> &Option<T>,
-    pick_mut: fn(&mut SettingsContent) -> &mut Option<T>,
+    pick: fn(&SettingsContent) -> Option<&T>,
+    write: fn(&mut SettingsContent, Option<T>),
 }
 
 impl<T: 'static> Clone for SettingField<T> {
@@ -98,7 +98,7 @@ impl<T: 'static> Clone for SettingField<T> {
 impl<T: 'static> Copy for SettingField<T> {}
 
 /// Helper for unimplemented settings, used in combination with `SettingField::unimplemented`
-/// to keep the setting around in the UI with valid pick and pick_mut implementations, but don't actually try to render it.
+/// to keep the setting around in the UI with valid pick and write implementations, but don't actually try to render it.
 /// TODO(settings_ui): In non-dev builds (`#[cfg(not(debug_assertions))]`) make this render as edit-in-json
 #[derive(Clone, Copy)]
 struct UnimplementedSettingField;
@@ -114,8 +114,8 @@ impl<T: 'static> SettingField<T> {
     #[allow(unused)]
     fn unimplemented(self) -> SettingField<UnimplementedSettingField> {
         SettingField {
-            pick: |_| &Some(UnimplementedSettingField),
-            pick_mut: |_| unreachable!(),
+            pick: |_| Some(&UnimplementedSettingField),
+            write: |_, _| unreachable!(),
         }
     }
 }
@@ -163,12 +163,15 @@ impl<T: PartialEq + Clone + Send + Sync + 'static> AnySettingField for SettingFi
         if file_set_in == &settings::SettingsFile::Default {
             return None;
         }
+        if file_set_in != &current_file.to_settings() {
+            return None;
+        }
         let this = *self;
         let store = SettingsStore::global(cx);
         let default_value = (this.pick)(store.raw_default_settings());
         let is_default = store
             .get_content_for_file(file_set_in.clone())
-            .map_or(&None, this.pick)
+            .map_or(None, this.pick)
             == default_value;
         if is_default {
             return None;
@@ -183,12 +186,12 @@ impl<T: PartialEq + Clone + Send + Sync + 'static> AnySettingField for SettingFi
                 .0
                 != settings::SettingsFile::Default;
             let value_to_set = if is_set_somewhere_other_than_default {
-                default_value.clone()
+                default_value.cloned()
             } else {
                 None
             };
             update_settings_file(current_file.clone(), cx, move |settings, _| {
-                *(this.pick_mut)(settings) = value_to_set;
+                (this.write)(settings, value_to_set);
             })
             // todo(settings_ui): Don't log err
             .log_err();
@@ -374,12 +377,6 @@ fn init_renderers(cx: &mut App) {
         .add_basic_renderer::<settings::OnLastWindowClosed>(render_dropdown)
         .add_basic_renderer::<settings::CloseWindowWhenNoItems>(render_dropdown)
         .add_basic_renderer::<settings::FontFamilyName>(render_font_picker)
-        // todo(settings_ui): This needs custom ui
-        // .add_renderer::<settings::BufferLineHeight>(|settings_field, file, _, window, cx| {
-        //     // todo(settings_ui): Do we want to expose the custom variant of buffer line height?
-        //     // right now there's a manual impl of strum::VariantArray
-        //     render_dropdown(*settings_field, file, window, cx)
-        // })
         .add_basic_renderer::<settings::BaseKeymapContent>(render_dropdown)
         .add_basic_renderer::<settings::MultiCursorModifier>(render_dropdown)
         .add_basic_renderer::<settings::HideMouseMode>(render_dropdown)
@@ -420,7 +417,7 @@ fn init_renderers(cx: &mut App) {
         .add_basic_renderer::<NonZero<usize>>(render_number_field)
         .add_basic_renderer::<NonZeroU32>(render_number_field)
         .add_basic_renderer::<settings::CodeFade>(render_number_field)
-        .add_basic_renderer::<FontWeight>(render_number_field)
+        .add_basic_renderer::<gpui::FontWeight>(render_number_field)
         .add_basic_renderer::<settings::MinimumContrast>(render_number_field)
         .add_basic_renderer::<settings::ShowScrollbar>(render_dropdown)
         .add_basic_renderer::<settings::ScrollbarDiagnostics>(render_dropdown)
@@ -430,17 +427,18 @@ fn init_renderers(cx: &mut App) {
         .add_basic_renderer::<settings::MinimapThumbBorder>(render_dropdown)
         .add_basic_renderer::<settings::SteppingGranularity>(render_dropdown)
         .add_basic_renderer::<settings::NotifyWhenAgentWaiting>(render_dropdown)
+        .add_basic_renderer::<settings::NotifyWhenAgentWaiting>(render_dropdown)
         .add_basic_renderer::<settings::ImageFileSizeUnit>(render_dropdown)
         .add_basic_renderer::<settings::StatusStyle>(render_dropdown)
         .add_basic_renderer::<settings::PaneSplitDirectionHorizontal>(render_dropdown)
         .add_basic_renderer::<settings::PaneSplitDirectionVertical>(render_dropdown)
         .add_basic_renderer::<settings::PaneSplitDirectionVertical>(render_dropdown)
         .add_basic_renderer::<settings::DocumentColorsRenderMode>(render_dropdown)
-    // please semicolon stay on next line
-    ;
-    // .add_renderer::<ThemeSelection>(|settings_field, file, _, window, cx| {
-    //     render_dropdown(*settings_field, file, window, cx)
-    // });
+        .add_basic_renderer::<settings::ThemeSelectionDiscriminants>(render_dropdown)
+        .add_basic_renderer::<settings::ThemeMode>(render_dropdown)
+        .add_basic_renderer::<settings::ThemeName>(render_theme_picker)
+        // please semicolon stay on next line
+        ;
 }
 
 pub fn open_settings_editor(
@@ -500,7 +498,7 @@ pub fn open_settings_editor(
 /// If this is empty the selected page is rendered,
 /// otherwise the last sub page gets rendered.
 ///
-/// Global so that `pick` and `pick_mut` callbacks can access it
+/// Global so that `pick` and `write` callbacks can access it
 /// and use it to dynamically render sub pages (e.g. for language settings)
 static SUB_PAGE_STACK: LazyLock<RwLock<Vec<SubPage>>> = LazyLock::new(|| RwLock::new(Vec::new()));
 
@@ -584,6 +582,7 @@ enum SettingsPageItem {
     SectionHeader(&'static str),
     SettingItem(SettingItem),
     SubPageLink(SubPageLink),
+    DynamicItem(DynamicItem),
 }
 
 impl std::fmt::Debug for SettingsPageItem {
@@ -596,6 +595,9 @@ impl std::fmt::Debug for SettingsPageItem {
             SettingsPageItem::SubPageLink(sub_page_link) => {
                 write!(f, "SubPageLink({})", sub_page_link.title)
             }
+            SettingsPageItem::DynamicItem(dynamic_item) => {
+                write!(f, "DynamicItem({})", dynamic_item.discriminant.title)
+            }
         }
     }
 }
@@ -610,19 +612,17 @@ impl SettingsPageItem {
         cx: &mut Context<SettingsWindow>,
     ) -> AnyElement {
         let file = settings_window.current_file.clone();
-        match self {
-            SettingsPageItem::SectionHeader(header) => v_flex()
-                .w_full()
-                .gap_1p5()
-                .child(
-                    Label::new(SharedString::new_static(header))
-                        .size(LabelSize::Small)
-                        .color(Color::Muted)
-                        .buffer_font(cx),
-                )
-                .child(Divider::horizontal().color(DividerColor::BorderFaded))
-                .into_any_element(),
-            SettingsPageItem::SettingItem(setting_item) => {
+        let border_variant = cx.theme().colors().border_variant;
+        let apply_padding = |element: Stateful<Div>| -> Stateful<Div> {
+            let element = element.pt_4();
+            if is_last {
+                element.pb_10()
+            } else {
+                element.pb_4().border_b_1().border_color(border_variant)
+            }
+        };
+        let mut render_setting_item_inner =
+            |setting_item: &SettingItem, cx: &mut Context<SettingsWindow>| {
                 let renderer = cx.default_global::<SettingFieldRenderer>().clone();
                 let (_, found) = setting_item.field.file_set_in(file.clone(), cx);
 
@@ -642,7 +642,7 @@ impl SettingsPageItem {
                     Ok(field_renderer) => field_renderer(
                         settings_window,
                         setting_item,
-                        file,
+                        file.clone(),
                         setting_item.metadata.as_deref(),
                         window,
                         cx,
@@ -650,7 +650,7 @@ impl SettingsPageItem {
                     Err(warning) => render_settings_item(
                         settings_window,
                         setting_item,
-                        file,
+                        file.clone(),
                         Button::new("error-warning", warning)
                             .style(ButtonStyle::Outlined)
                             .size(ButtonSize::Medium)
@@ -665,17 +665,23 @@ impl SettingsPageItem {
                     ),
                 };
 
-                field
-                    .pt_4()
-                    .map(|this| {
-                        if is_last {
-                            this.pb_10()
-                        } else {
-                            this.pb_4()
-                                .border_b_1()
-                                .border_color(cx.theme().colors().border_variant)
-                        }
-                    })
+                (field.map(apply_padding), field_renderer_or_warning.is_ok())
+            };
+        match self {
+            SettingsPageItem::SectionHeader(header) => v_flex()
+                .w_full()
+                .gap_1p5()
+                .child(
+                    Label::new(SharedString::new_static(header))
+                        .size(LabelSize::Small)
+                        .color(Color::Muted)
+                        .buffer_font(cx),
+                )
+                .child(Divider::horizontal().color(DividerColor::BorderFaded))
+                .into_any_element(),
+            SettingsPageItem::SettingItem(setting_item) => {
+                render_setting_item_inner(setting_item, cx)
+                    .0
                     .into_any_element()
             }
             SettingsPageItem::SubPageLink(sub_page_link) => h_flex()
@@ -684,16 +690,7 @@ impl SettingsPageItem {
                 .min_w_0()
                 .gap_2()
                 .justify_between()
-                .pt_4()
-                .map(|this| {
-                    if is_last {
-                        this.pb_10()
-                    } else {
-                        this.pb_4()
-                            .border_b_1()
-                            .border_color(cx.theme().colors().border_variant)
-                    }
-                })
+                .map(apply_padding)
                 .child(
                     v_flex()
                         .w_full()
@@ -736,6 +733,32 @@ impl SettingsPageItem {
                     }),
                 )
                 .into_any_element(),
+            SettingsPageItem::DynamicItem(DynamicItem {
+                discriminant: discriminant_setting_item,
+                pick_discriminant,
+                fields,
+            }) => {
+                let file = file.to_settings();
+                let discriminant = SettingsStore::global(cx)
+                    .get_value_from_file(file, *pick_discriminant)
+                    .1;
+                let (discriminant_element, rendered_ok) =
+                    render_setting_item_inner(discriminant_setting_item, cx);
+                let mut content = v_flex()
+                    .gap_2()
+                    .id("dynamic-item")
+                    .child(discriminant_element);
+                if rendered_ok {
+                    let discriminant =
+                        discriminant.expect("This should be Some if rendered_ok is true");
+                    let sub_fields = &fields[discriminant];
+                    for field in sub_fields {
+                        content = content.child(render_setting_item_inner(field, cx).0.pl_6());
+                    }
+                }
+
+                return content.into_any_element();
+            }
         }
     }
 }
@@ -767,8 +790,7 @@ fn render_settings_item(
                         .when_some(
                             setting_item
                                 .field
-                                .reset_to_default_fn(&file, &found_in_file, cx)
-                                .filter(|_| file_set_in.as_ref() == Some(&file)),
+                                .reset_to_default_fn(&file, &found_in_file, cx),
                             |this, reset_to_default| {
                                 this.child(
                                     IconButton::new("reset-to-default-btn", IconName::Undo)
@@ -816,6 +838,18 @@ struct SettingItem {
     files: FileMask,
 }
 
+struct DynamicItem {
+    discriminant: SettingItem,
+    pick_discriminant: fn(&SettingsContent) -> Option<usize>,
+    fields: Vec<Vec<SettingItem>>,
+}
+
+impl PartialEq for DynamicItem {
+    fn eq(&self, other: &Self) -> bool {
+        self.discriminant == other.discriminant && self.fields == other.fields
+    }
+}
+
 #[derive(PartialEq, Eq, Clone, Copy)]
 struct FileMask(u8);
 
@@ -1204,7 +1238,11 @@ impl SettingsWindow {
                         any_found_since_last_header = false;
                     }
                     SettingsPageItem::SettingItem(SettingItem { files, .. })
-                    | SettingsPageItem::SubPageLink(SubPageLink { files, .. }) => {
+                    | SettingsPageItem::SubPageLink(SubPageLink { files, .. })
+                    | SettingsPageItem::DynamicItem(DynamicItem {
+                        discriminant: SettingItem { files, .. },
+                        ..
+                    }) => {
                         if !files.contains(current_file) {
                             page_filter[index] = false;
                         } else {
@@ -1375,7 +1413,10 @@ impl SettingsWindow {
             for (item_index, item) in page.items.iter().enumerate() {
                 let key_index = key_lut.len();
                 match item {
-                    SettingsPageItem::SettingItem(item) => {
+                    SettingsPageItem::DynamicItem(DynamicItem {
+                        discriminant: item, ..
+                    })
+                    | SettingsPageItem::SettingItem(item) => {
                         documents.push(bm25::Document {
                             id: key_index,
                             contents: [page.title, header_str, item.title, item.description]
@@ -2687,7 +2728,7 @@ fn render_text_field<T: From<String> + Into<String> + AsRef<str> + Clone>(
         .on_confirm({
             move |new_text, cx| {
                 update_settings_file(file.clone(), cx, move |settings, _cx| {
-                    *(field.pick_mut)(settings) = new_text.map(Into::into);
+                    (field.write)(settings, new_text.map(Into::into));
                 })
                 .log_err(); // todo(settings_ui) don't log err
             }
@@ -2716,7 +2757,7 @@ fn render_toggle_button<B: Into<bool> + From<bool> + Copy>(
             move |state, _window, cx| {
                 let state = *state == ui::ToggleState::Selected;
                 update_settings_file(file.clone(), cx, move |settings, _cx| {
-                    *(field.pick_mut)(settings) = Some(state.into());
+                    (field.write)(settings, Some(state.into()));
                 })
                 .log_err(); // todo(settings_ui) don't log err
             }
@@ -2744,7 +2785,7 @@ fn render_font_picker(
             current_value.clone().into(),
             move |font_name, cx| {
                 update_settings_file(file.clone(), cx, move |settings, _cx| {
-                    *(field.pick_mut)(settings) = Some(font_name.into());
+                    (field.write)(settings, Some(font_name.into()));
                 })
                 .log_err(); // todo(settings_ui) don't log err
             },
@@ -2788,7 +2829,7 @@ fn render_number_field<T: NumberFieldType + Send + Sync>(
             move |value, _window, cx| {
                 let value = *value;
                 update_settings_file(file.clone(), cx, move |settings, _cx| {
-                    *(field.pick_mut)(settings) = Some(value);
+                    (field.write)(settings, Some(value));
                 })
                 .log_err(); // todo(settings_ui) don't log err
             }
@@ -2843,7 +2884,58 @@ where
                             return;
                         }
                         update_settings_file(file.clone(), cx, move |settings, _cx| {
-                            *(field.pick_mut)(settings) = Some(value);
+                            (field.write)(settings, Some(value));
+                        })
+                        .log_err(); // todo(settings_ui) don't log err
+                    },
+                );
+            }
+            menu
+        }),
+    )
+    .trigger_size(ButtonSize::Medium)
+    .style(DropdownStyle::Outlined)
+    .offset(gpui::Point {
+        x: px(0.0),
+        y: px(2.0),
+    })
+    .tab_index(0)
+    .into_any_element()
+}
+
+fn render_theme_picker(
+    field: SettingField<settings::ThemeName>,
+    file: SettingsUiFile,
+    _metadata: Option<&SettingsFieldMetadata>,
+    window: &mut Window,
+    cx: &mut App,
+) -> AnyElement {
+    let (_, value) = SettingsStore::global(cx).get_value_from_file(file.to_settings(), field.pick);
+    let current_value = value
+        .cloned()
+        .map(|theme_name| theme_name.0.into())
+        .unwrap_or_else(|| cx.theme().name.clone());
+
+    DropdownMenu::new(
+        "font-picker",
+        current_value.clone(),
+        ContextMenu::build(window, cx, move |mut menu, _, cx| {
+            let all_theme_names = theme::ThemeRegistry::global(cx).list_names();
+            for theme_name in all_theme_names {
+                let file = file.clone();
+                let selected = theme_name.as_ref() == current_value.as_ref();
+                menu = menu.toggleable_entry(
+                    theme_name.clone(),
+                    selected,
+                    IconPosition::End,
+                    None,
+                    move |_, cx| {
+                        if selected {
+                            return;
+                        }
+                        let theme_name = theme_name.clone();
+                        update_settings_file(file.clone(), cx, move |settings, _cx| {
+                            (field.write)(settings, Some(settings::ThemeName(theme_name.into())));
                         })
                         .log_err(); // todo(settings_ui) don't log err
                     },