settings ui: Add missing setting elements (#39644)

Anthony Eid created

Added the following settings to the UI

Editor Page - Scrollbar Section (9 settings)
- Show
- Cursors
- Git Diff
- Search Results
- Selected Text
- Selected Symbol
- Diagnostics
- Horizontal Scrollbar
- Vertical Scrollbar

 Editor Page - Minimap Section (6 settings)
- Show
- Display In
- Thumb
- Thumb Border
- Current Line Highlight
- Max Width Columns

Editor Page - Editor Behavior Section (3 settings)
- Expand Excerpt Lines
- Excerpt Context Lines
- Minimum Contrast For Highlights

 Debugger Page (7 settings)
- Stepping Granularity
- Save Breakpoints
- Timeout
- Dock
- Log DAP Communications
- Format DAP Log Messages
- Button

 Panels Page - Git Panel Section (3 settings)
- Button
- Dock
- Default Width

Collaboration Page - Experimental Section (4 settings)
- Auto Microphone Volume
- Auto Speaker Volume
- Denoise
- Legacy Audio Compatible

Release Notes:

- N/A

Change summary

assets/settings/default.json                     |   4 
crates/editor/src/editor_settings.rs             |   2 
crates/project/src/project.rs                    |   8 
crates/project/src/project_settings.rs           |   6 
crates/settings/src/settings_content.rs          |  13 
crates/settings/src/settings_content/editor.rs   |  95 ++
crates/settings/src/settings_content/project.rs  |   2 
crates/settings/src/settings_content/terminal.rs |  14 
crates/settings/src/settings_store.rs            |  49 +
crates/settings_ui/src/page_data.rs              | 688 +++++++++++++++++
crates/settings_ui/src/settings_ui.rs            |  68 +
crates/ui_input/src/numeric_stepper.rs           |  29 
12 files changed, 919 insertions(+), 59 deletions(-)

Detailed changes

assets/settings/default.json 🔗

@@ -1233,8 +1233,8 @@
     "git_gutter": "tracked_files",
     /// Sets the debounce threshold (in milliseconds) after which changes are reflected in the git gutter.
     ///
-    /// Default: null
-    "gutter_debounce": null,
+    /// Default: 0
+    "gutter_debounce": 0,
     // Control whether the git blame information is shown inline,
     // in the currently focused line.
     "inline_blame": {

crates/editor/src/editor_settings.rs 🔗

@@ -267,7 +267,7 @@ impl Settings for EditorSettings {
                 delay: drag_and_drop_selection.delay.unwrap(),
             },
             lsp_document_colors: editor.lsp_document_colors.unwrap(),
-            minimum_contrast_for_highlights: editor.minimum_contrast_for_highlights.unwrap(),
+            minimum_contrast_for_highlights: editor.minimum_contrast_for_highlights.unwrap().0,
         }
     }
 

crates/project/src/project.rs 🔗

@@ -3252,9 +3252,9 @@ impl Project {
         self.buffers_needing_diff.insert(buffer.downgrade());
         let first_insertion = self.buffers_needing_diff.len() == 1;
         let settings = ProjectSettings::get_global(cx);
-        let delay = if let Some(delay) = settings.git.gutter_debounce {
-            delay
-        } else {
+        let delay = settings.git.gutter_debounce;
+
+        if delay == 0 {
             if first_insertion {
                 let this = cx.weak_entity();
                 cx.defer(move |cx| {
@@ -3266,7 +3266,7 @@ impl Project {
                 });
             }
             return;
-        };
+        }
 
         const MIN_DELAY: u64 = 50;
         let delay = delay.max(MIN_DELAY);

crates/project/src/project_settings.rs 🔗

@@ -300,8 +300,8 @@ pub struct GitSettings {
     pub git_gutter: settings::GitGutterSetting,
     /// Sets the debounce threshold (in milliseconds) after which changes are reflected in the git gutter.
     ///
-    /// Default: null
-    pub gutter_debounce: Option<u64>,
+    /// Default: 0
+    pub gutter_debounce: u64,
     /// Whether or not to show git blame data inline in
     /// the currently focused line.
     ///
@@ -446,7 +446,7 @@ impl Settings for ProjectSettings {
         let git = content.git.as_ref().unwrap();
         let git_settings = GitSettings {
             git_gutter: git.git_gutter.unwrap(),
-            gutter_debounce: git.gutter_debounce,
+            gutter_debounce: git.gutter_debounce.unwrap_or_default(),
             inline_blame: {
                 let inline = git.inline_blame.unwrap();
                 InlineBlameSettings {

crates/settings/src/settings_content.rs 🔗

@@ -383,7 +383,18 @@ pub struct DebuggerSettingsContent {
 
 /// The granularity of one 'step' in the stepping requests `next`, `stepIn`, `stepOut`, and `stepBack`.
 #[derive(
-    PartialEq, Eq, Debug, Hash, Clone, Copy, Deserialize, Serialize, JsonSchema, MergeFrom,
+    PartialEq,
+    Eq,
+    Debug,
+    Hash,
+    Clone,
+    Copy,
+    Deserialize,
+    Serialize,
+    JsonSchema,
+    MergeFrom,
+    strum::VariantArray,
+    strum::VariantNames,
 )]
 #[serde(rename_all = "snake_case")]
 pub enum SteppingGranularity {

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

@@ -1,3 +1,4 @@
+use std::fmt::Display;
 use std::num;
 
 use collections::HashMap;
@@ -153,7 +154,8 @@ pub struct EditorSettingsContent {
     ///
     /// Values range from 0 to 106. Set to 0 to disable adjustments.
     /// Default: 45
-    pub minimum_contrast_for_highlights: Option<f32>,
+    #[schemars(range(min = 0, max = 106))]
+    pub minimum_contrast_for_highlights: Option<MinimumContrast>,
 
     /// Whether to follow-up empty go to definition responses from the language server or not.
     /// `FindAllReferences` allows to look up references of the same symbol instead.
@@ -425,7 +427,18 @@ pub enum DoubleClickInMultibuffer {
 ///
 /// Default: always
 #[derive(
-    Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq, Eq,
+    Copy,
+    Clone,
+    Debug,
+    Default,
+    Serialize,
+    Deserialize,
+    JsonSchema,
+    MergeFrom,
+    PartialEq,
+    Eq,
+    strum::VariantArray,
+    strum::VariantNames,
 )]
 #[serde(rename_all = "snake_case")]
 pub enum MinimapThumb {
@@ -440,7 +453,18 @@ pub enum MinimapThumb {
 ///
 /// Default: left_open
 #[derive(
-    Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq, Eq,
+    Copy,
+    Clone,
+    Debug,
+    Default,
+    Serialize,
+    Deserialize,
+    JsonSchema,
+    MergeFrom,
+    PartialEq,
+    Eq,
+    strum::VariantArray,
+    strum::VariantNames,
 )]
 #[serde(rename_all = "snake_case")]
 pub enum MinimapThumbBorder {
@@ -460,7 +484,19 @@ pub enum MinimapThumbBorder {
 /// Which diagnostic indicators to show in the scrollbar.
 ///
 /// Default: all
-#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq, Eq)]
+#[derive(
+    Copy,
+    Clone,
+    Debug,
+    Serialize,
+    Deserialize,
+    JsonSchema,
+    MergeFrom,
+    PartialEq,
+    Eq,
+    strum::VariantArray,
+    strum::VariantNames,
+)]
 #[serde(rename_all = "lowercase")]
 pub enum ScrollbarDiagnostics {
     /// Show all diagnostic levels: hint, information, warnings, error.
@@ -682,7 +718,18 @@ pub struct DragAndDropSelectionContent {
 ///
 /// Default: never
 #[derive(
-    Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq, Eq,
+    Copy,
+    Clone,
+    Debug,
+    Default,
+    Serialize,
+    Deserialize,
+    JsonSchema,
+    MergeFrom,
+    PartialEq,
+    Eq,
+    strum::VariantArray,
+    strum::VariantNames,
 )]
 #[serde(rename_all = "snake_case")]
 pub enum ShowMinimap {
@@ -699,7 +746,18 @@ pub enum ShowMinimap {
 ///
 /// Default: all_editors
 #[derive(
-    Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq, Eq,
+    Copy,
+    Clone,
+    Debug,
+    Default,
+    Serialize,
+    Deserialize,
+    JsonSchema,
+    MergeFrom,
+    PartialEq,
+    Eq,
+    strum::VariantArray,
+    strum::VariantNames,
 )]
 #[serde(rename_all = "snake_case")]
 pub enum DisplayIn {
@@ -709,3 +767,28 @@ pub enum DisplayIn {
     #[default]
     ActiveEditor,
 }
+
+/// Minimum APCA perceptual contrast for text over highlight backgrounds.
+///
+/// Valid range: 0.0 to 106.0
+/// Default: 45.0
+#[derive(
+    Clone,
+    Copy,
+    Debug,
+    Serialize,
+    Deserialize,
+    JsonSchema,
+    MergeFrom,
+    PartialEq,
+    PartialOrd,
+    derive_more::FromStr,
+)]
+#[serde(transparent)]
+pub struct MinimumContrast(pub f32);
+
+impl Display for MinimumContrast {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        write!(f, "{:.1}", self.0)
+    }
+}

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

@@ -249,7 +249,7 @@ pub struct GitSettings {
     pub git_gutter: Option<GitGutterSetting>,
     /// Sets the debounce threshold (in milliseconds) after which changes are reflected in the git gutter.
     ///
-    /// Default: null
+    /// Default: 0
     pub gutter_debounce: Option<u64>,
     /// Whether or not to show git blame data inline in
     /// the currently focused line.

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

@@ -199,7 +199,19 @@ impl TerminalLineHeight {
 /// When to show the scrollbar.
 ///
 /// Default: auto
-#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq, Eq)]
+#[derive(
+    Copy,
+    Clone,
+    Debug,
+    Serialize,
+    Deserialize,
+    JsonSchema,
+    MergeFrom,
+    PartialEq,
+    Eq,
+    strum::VariantArray,
+    strum::VariantNames,
+)]
 #[serde(rename_all = "snake_case")]
 pub enum ShowScrollbar {
     /// Show the scrollbar if there's important information or

crates/settings/src/settings_store.rs 🔗

@@ -538,6 +538,7 @@ impl SettingsStore {
         &self,
         target_file: SettingsFile,
         pick: fn(&SettingsContent) -> &Option<T>,
+        type_name: &'static str,
     ) -> (SettingsFile, &T) {
         // TODO: 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
@@ -568,7 +569,7 @@ impl SettingsStore {
             }
         }
 
-        unreachable!("All values should have defaults");
+        unreachable!("{type_name}: doesn't have a default value");
     }
 }
 
@@ -1714,16 +1715,24 @@ mod tests {
         let default_value = get(&store.default_settings).unwrap();
 
         assert_eq!(
-            store.get_value_from_file(SettingsFile::Local(local.clone()), get),
+            store.get_value_from_file(
+                SettingsFile::Local(local.clone()),
+                get,
+                "preferred line length"
+            ),
             (SettingsFile::User, &0)
         );
         assert_eq!(
-            store.get_value_from_file(SettingsFile::User, get),
+            store.get_value_from_file(SettingsFile::User, get, "preferred line length"),
             (SettingsFile::User, &0)
         );
         store.set_user_settings(r#"{}"#, cx).unwrap();
         assert_eq!(
-            store.get_value_from_file(SettingsFile::Local(local.clone()), get),
+            store.get_value_from_file(
+                SettingsFile::Local(local.clone()),
+                get,
+                "preferred line length"
+            ),
             (SettingsFile::Default, &default_value)
         );
         store
@@ -1736,11 +1745,15 @@ mod tests {
             )
             .unwrap();
         assert_eq!(
-            store.get_value_from_file(SettingsFile::Local(local.clone()), get),
+            store.get_value_from_file(
+                SettingsFile::Local(local.clone()),
+                get,
+                "preferred line length"
+            ),
             (SettingsFile::Local(local), &80)
         );
         assert_eq!(
-            store.get_value_from_file(SettingsFile::User, get),
+            store.get_value_from_file(SettingsFile::User, get, "preferred line length"),
             (SettingsFile::Default, &default_value)
         );
     }
@@ -1817,11 +1830,19 @@ mod tests {
 
         // each local child should only inherit from it's parent
         assert_eq!(
-            store.get_value_from_file(SettingsFile::Local(local_2_child), get),
+            store.get_value_from_file(
+                SettingsFile::Local(local_2_child),
+                get,
+                "preferred_line_length"
+            ),
             (SettingsFile::Local(local_2), &2)
         );
         assert_eq!(
-            store.get_value_from_file(SettingsFile::Local(local_1_child.clone()), get),
+            store.get_value_from_file(
+                SettingsFile::Local(local_1_child.clone()),
+                get,
+                "preferred_line_length"
+            ),
             (SettingsFile::Local(local_1.clone()), &1)
         );
 
@@ -1847,7 +1868,11 @@ mod tests {
             .unwrap();
 
         assert_eq!(
-            store.get_value_from_file(SettingsFile::Local(local_1_adjacent_child.clone()), get),
+            store.get_value_from_file(
+                SettingsFile::Local(local_1_adjacent_child.clone()),
+                get,
+                "preferred_line_length"
+            ),
             (SettingsFile::Local(local_1.clone()), &1)
         );
         store
@@ -1869,7 +1894,11 @@ mod tests {
             )
             .unwrap();
         assert_eq!(
-            store.get_value_from_file(SettingsFile::Local(local_1_child), get),
+            store.get_value_from_file(
+                SettingsFile::Local(local_1_child),
+                get,
+                "preferred_line_length"
+            ),
             (SettingsFile::Local(local_1), &1)
         );
     }

crates/settings_ui/src/page_data.rs 🔗

@@ -712,6 +712,41 @@ pub(crate) fn user_settings_data() -> Vec<SettingsPage> {
                     }),
                     metadata: None,
                 }),
+                SettingsPageItem::SettingItem(SettingItem {
+                    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
+                        },
+                    }),
+                    metadata: None,
+                }),
+                SettingsPageItem::SettingItem(SettingItem {
+                    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
+                        },
+                    }),
+                    metadata: None,
+                }),
+                SettingsPageItem::SettingItem(SettingItem {
+                    title: "Minimum Contrast For Highlights",
+                    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
+                        },
+                        pick_mut: |settings_content| {
+                            &mut settings_content.editor.minimum_contrast_for_highlights
+                        },
+                    }),
+                    metadata: None,
+                }),
                 SettingsPageItem::SectionHeader("Scrolling"),
                 SettingsPageItem::SettingItem(SettingItem {
                     title: "Scroll Beyond Last Line",
@@ -1181,6 +1216,354 @@ pub(crate) fn user_settings_data() -> Vec<SettingsPage> {
                     }),
                     metadata: None,
                 }),
+                SettingsPageItem::SettingItem(SettingItem {
+                    title: "Min Line Number Digits",
+                    description: "Minimum number of characters to reserve space for in the gutter.",
+                    field: Box::new(SettingField {
+                        pick: |settings_content| {
+                            if let Some(gutter) = &settings_content.editor.gutter {
+                                &gutter.min_line_number_digits
+                            } else {
+                                &None
+                            }
+                        },
+                        pick_mut: |settings_content| {
+                            &mut settings_content
+                                .editor
+                                .gutter
+                                .get_or_insert_default()
+                                .min_line_number_digits
+                        },
+                    }),
+                    metadata: None,
+                }),
+                SettingsPageItem::SectionHeader("Scrollbar"),
+                SettingsPageItem::SettingItem(SettingItem {
+                    title: "Show",
+                    description: "When to show the scrollbar in the editor",
+                    field: Box::new(SettingField {
+                        pick: |settings_content| {
+                            if let Some(scrollbar) = &settings_content.editor.scrollbar {
+                                &scrollbar.show
+                            } else {
+                                &None
+                            }
+                        },
+                        pick_mut: |settings_content| {
+                            &mut settings_content
+                                .editor
+                                .scrollbar
+                                .get_or_insert_default()
+                                .show
+                        },
+                    }),
+                    metadata: None,
+                }),
+                SettingsPageItem::SettingItem(SettingItem {
+                    title: "Cursors",
+                    description: "Whether to show cursor positions in the scrollbar",
+                    field: Box::new(SettingField {
+                        pick: |settings_content| {
+                            if let Some(scrollbar) = &settings_content.editor.scrollbar {
+                                &scrollbar.cursors
+                            } else {
+                                &None
+                            }
+                        },
+                        pick_mut: |settings_content| {
+                            &mut settings_content
+                                .editor
+                                .scrollbar
+                                .get_or_insert_default()
+                                .cursors
+                        },
+                    }),
+                    metadata: None,
+                }),
+                SettingsPageItem::SettingItem(SettingItem {
+                    title: "Git Diff",
+                    description: "Whether to show git diff indicators in the scrollbar",
+                    field: Box::new(SettingField {
+                        pick: |settings_content| {
+                            if let Some(scrollbar) = &settings_content.editor.scrollbar {
+                                &scrollbar.git_diff
+                            } else {
+                                &None
+                            }
+                        },
+                        pick_mut: |settings_content| {
+                            &mut settings_content
+                                .editor
+                                .scrollbar
+                                .get_or_insert_default()
+                                .git_diff
+                        },
+                    }),
+                    metadata: None,
+                }),
+                SettingsPageItem::SettingItem(SettingItem {
+                    title: "Search Results",
+                    description: "Whether to show buffer search result indicators in the scrollbar",
+                    field: Box::new(SettingField {
+                        pick: |settings_content| {
+                            if let Some(scrollbar) = &settings_content.editor.scrollbar {
+                                &scrollbar.search_results
+                            } else {
+                                &None
+                            }
+                        },
+                        pick_mut: |settings_content| {
+                            &mut settings_content
+                                .editor
+                                .scrollbar
+                                .get_or_insert_default()
+                                .search_results
+                        },
+                    }),
+                    metadata: None,
+                }),
+                SettingsPageItem::SettingItem(SettingItem {
+                    title: "Selected Text",
+                    description: "Whether to show selected text occurrences in the scrollbar",
+                    field: Box::new(SettingField {
+                        pick: |settings_content| {
+                            if let Some(scrollbar) = &settings_content.editor.scrollbar {
+                                &scrollbar.selected_text
+                            } else {
+                                &None
+                            }
+                        },
+                        pick_mut: |settings_content| {
+                            &mut settings_content
+                                .editor
+                                .scrollbar
+                                .get_or_insert_default()
+                                .selected_text
+                        },
+                    }),
+                    metadata: None,
+                }),
+                SettingsPageItem::SettingItem(SettingItem {
+                    title: "Selected Symbol",
+                    description: "Whether to show selected symbol occurrences in the scrollbar",
+                    field: Box::new(SettingField {
+                        pick: |settings_content| {
+                            if let Some(scrollbar) = &settings_content.editor.scrollbar {
+                                &scrollbar.selected_symbol
+                            } else {
+                                &None
+                            }
+                        },
+                        pick_mut: |settings_content| {
+                            &mut settings_content
+                                .editor
+                                .scrollbar
+                                .get_or_insert_default()
+                                .selected_symbol
+                        },
+                    }),
+                    metadata: None,
+                }),
+                SettingsPageItem::SettingItem(SettingItem {
+                    title: "Diagnostics",
+                    description: "Which diagnostic indicators to show in the scrollbar",
+                    field: Box::new(SettingField {
+                        pick: |settings_content| {
+                            if let Some(scrollbar) = &settings_content.editor.scrollbar {
+                                &scrollbar.diagnostics
+                            } else {
+                                &None
+                            }
+                        },
+                        pick_mut: |settings_content| {
+                            &mut settings_content
+                                .editor
+                                .scrollbar
+                                .get_or_insert_default()
+                                .diagnostics
+                        },
+                    }),
+                    metadata: None,
+                }),
+                SettingsPageItem::SettingItem(SettingItem {
+                    title: "Horizontal Scrollbar",
+                    description: "When false, forcefully disables the horizontal scrollbar",
+                    field: Box::new(SettingField {
+                        pick: |settings_content| {
+                            if let Some(scrollbar) = &settings_content.editor.scrollbar {
+                                if let Some(axes) = &scrollbar.axes {
+                                    &axes.horizontal
+                                } else {
+                                    &None
+                                }
+                            } else {
+                                &None
+                            }
+                        },
+                        pick_mut: |settings_content| {
+                            &mut settings_content
+                                .editor
+                                .scrollbar
+                                .get_or_insert_default()
+                                .axes
+                                .get_or_insert_default()
+                                .horizontal
+                        },
+                    }),
+                    metadata: None,
+                }),
+                SettingsPageItem::SettingItem(SettingItem {
+                    title: "Vertical Scrollbar",
+                    description: "When false, forcefully disables the vertical scrollbar",
+                    field: Box::new(SettingField {
+                        pick: |settings_content| {
+                            if let Some(scrollbar) = &settings_content.editor.scrollbar {
+                                if let Some(axes) = &scrollbar.axes {
+                                    &axes.vertical
+                                } else {
+                                    &None
+                                }
+                            } else {
+                                &None
+                            }
+                        },
+                        pick_mut: |settings_content| {
+                            &mut settings_content
+                                .editor
+                                .scrollbar
+                                .get_or_insert_default()
+                                .axes
+                                .get_or_insert_default()
+                                .vertical
+                        },
+                    }),
+                    metadata: None,
+                }),
+                SettingsPageItem::SectionHeader("Minimap"),
+                SettingsPageItem::SettingItem(SettingItem {
+                    title: "Show",
+                    description: "When to show the minimap in the editor",
+                    field: Box::new(SettingField {
+                        pick: |settings_content| {
+                            if let Some(minimap) = &settings_content.editor.minimap {
+                                &minimap.show
+                            } else {
+                                &None
+                            }
+                        },
+                        pick_mut: |settings_content| {
+                            &mut settings_content.editor.minimap.get_or_insert_default().show
+                        },
+                    }),
+                    metadata: None,
+                }),
+                SettingsPageItem::SettingItem(SettingItem {
+                    title: "Display In",
+                    description: "Where to show the minimap in the editor",
+                    field: Box::new(SettingField {
+                        pick: |settings_content| {
+                            if let Some(minimap) = &settings_content.editor.minimap {
+                                &minimap.display_in
+                            } else {
+                                &None
+                            }
+                        },
+                        pick_mut: |settings_content| {
+                            &mut settings_content
+                                .editor
+                                .minimap
+                                .get_or_insert_default()
+                                .display_in
+                        },
+                    }),
+                    metadata: None,
+                }),
+                SettingsPageItem::SettingItem(SettingItem {
+                    title: "Thumb",
+                    description: "When to show the minimap thumb",
+                    field: Box::new(SettingField {
+                        pick: |settings_content| {
+                            if let Some(minimap) = &settings_content.editor.minimap {
+                                &minimap.thumb
+                            } else {
+                                &None
+                            }
+                        },
+                        pick_mut: |settings_content| {
+                            &mut settings_content
+                                .editor
+                                .minimap
+                                .get_or_insert_default()
+                                .thumb
+                        },
+                    }),
+                    metadata: None,
+                }),
+                SettingsPageItem::SettingItem(SettingItem {
+                    title: "Thumb Border",
+                    description: "Border style for the minimap's scrollbar thumb",
+                    field: Box::new(SettingField {
+                        pick: |settings_content| {
+                            if let Some(minimap) = &settings_content.editor.minimap {
+                                &minimap.thumb_border
+                            } else {
+                                &None
+                            }
+                        },
+                        pick_mut: |settings_content| {
+                            &mut settings_content
+                                .editor
+                                .minimap
+                                .get_or_insert_default()
+                                .thumb_border
+                        },
+                    }),
+                    metadata: None,
+                }),
+                SettingsPageItem::SettingItem(SettingItem {
+                    title: "Current Line Highlight",
+                    description: "How to highlight the current line in the minimap",
+                    field: Box::new(SettingField {
+                        pick: |settings_content| {
+                            if let Some(minimap) = &settings_content.editor.minimap
+                                && minimap.current_line_highlight.is_some()
+                            {
+                                &minimap.current_line_highlight
+                            } else {
+                                &settings_content.editor.current_line_highlight
+                            }
+                        },
+                        pick_mut: |settings_content| {
+                            &mut settings_content
+                                .editor
+                                .minimap
+                                .get_or_insert_default()
+                                .current_line_highlight
+                        },
+                    }),
+                    metadata: None,
+                }),
+                SettingsPageItem::SettingItem(SettingItem {
+                    title: "Max Width Columns",
+                    description: "Maximum number of columns to display in the minimap",
+                    field: Box::new(SettingField {
+                        pick: |settings_content| {
+                            if let Some(minimap) = &settings_content.editor.minimap {
+                                &minimap.max_width_columns
+                            } else {
+                                &None
+                            }
+                        },
+                        pick_mut: |settings_content| {
+                            &mut settings_content
+                                .editor
+                                .minimap
+                                .get_or_insert_default()
+                                .max_width_columns
+                        },
+                    }),
+                    metadata: None,
+                }),
                 SettingsPageItem::SectionHeader("Tabs"),
                 SettingsPageItem::SettingItem(SettingItem {
                     title: "Show Tab Bar",
@@ -1253,6 +1636,8 @@ pub(crate) fn user_settings_data() -> Vec<SettingsPage> {
                 // SettingsPageItem::SettingItem(SettingItem {
                 //     title: "Maximum Tabs",
                 //     description: "Maximum open tabs in a pane. Will not close an unsaved tab",
+                //     // todo(settings_ui): The default for this value is null and it's use in code
+                //     // is complex, so I'm going to come back to this later
                 //     field: Box::new(SettingField {
                 //         pick: |settings_content| &settings_content.workspace.max_tabs,
                 //         pick_mut: |settings_content| &mut settings_content.workspace.max_tabs,
@@ -2593,6 +2978,61 @@ pub(crate) fn user_settings_data() -> Vec<SettingsPage> {
                 //     }),
                 //     metadata: None,
                 // }),
+                SettingsPageItem::SectionHeader("Git Panel"),
+                SettingsPageItem::SettingItem(SettingItem {
+                    title: "Button",
+                    description: "Whether to show the git panel button in the status bar",
+                    field: Box::new(SettingField {
+                        pick: |settings_content| {
+                            if let Some(git_panel) = &settings_content.git_panel {
+                                &git_panel.button
+                            } else {
+                                &None
+                            }
+                        },
+                        pick_mut: |settings_content| {
+                            &mut settings_content.git_panel.get_or_insert_default().button
+                        },
+                    }),
+                    metadata: None,
+                }),
+                SettingsPageItem::SettingItem(SettingItem {
+                    title: "Dock",
+                    description: "Where to dock the git panel",
+                    field: Box::new(SettingField {
+                        pick: |settings_content| {
+                            if let Some(git_panel) = &settings_content.git_panel {
+                                &git_panel.dock
+                            } else {
+                                &None
+                            }
+                        },
+                        pick_mut: |settings_content| {
+                            &mut settings_content.git_panel.get_or_insert_default().dock
+                        },
+                    }),
+                    metadata: None,
+                }),
+                SettingsPageItem::SettingItem(SettingItem {
+                    title: "Default Width",
+                    description: "Default width of the git panel in pixels",
+                    field: Box::new(SettingField {
+                        pick: |settings_content| {
+                            if let Some(git_panel) = &settings_content.git_panel {
+                                &git_panel.default_width
+                            } else {
+                                &None
+                            }
+                        },
+                        pick_mut: |settings_content| {
+                            &mut settings_content
+                                .git_panel
+                                .get_or_insert_default()
+                                .default_width
+                        },
+                    }),
+                    metadata: None,
+                }),
                 SettingsPageItem::SectionHeader("Notification Panel"),
                 SettingsPageItem::SettingItem(SettingItem {
                     title: "Notification Panel Button",
@@ -2742,23 +3182,23 @@ pub(crate) fn user_settings_data() -> Vec<SettingsPage> {
                     metadata: None,
                 }),
                 // todo(settings_ui): Figure out the right default for this value in default.json
-                // SettingsPageItem::SettingItem(SettingItem {
-                //     title: "Gutter Debounce",
-                //     description: "Debounce threshold in milliseconds after which changes are reflected in the git gutter",
-                //     field: Box::new(SettingField {
-                //         pick: |settings_content| {
-                //             if let Some(git) = &settings_content.git {
-                //                 &git.gutter_debounce
-                //             } else {
-                //                 &None
-                //             }
-                //         },
-                //         pick_mut: |settings_content| {
-                //             &mut settings_content.git.get_or_insert_default().gutter_debounce
-                //         },
-                //     }),
-                //     metadata: None,
-                // }),
+                SettingsPageItem::SettingItem(SettingItem {
+                    title: "Gutter Debounce",
+                    description: "Debounce threshold in milliseconds after which changes are reflected in the git gutter",
+                    field: Box::new(SettingField {
+                        pick: |settings_content| {
+                            if let Some(git) = &settings_content.git {
+                                &git.gutter_debounce
+                            } else {
+                                &None
+                            }
+                        },
+                        pick_mut: |settings_content| {
+                            &mut settings_content.git.get_or_insert_default().gutter_debounce
+                        },
+                    }),
+                    metadata: None,
+                }),
                 SettingsPageItem::SettingItem(SettingItem {
                     title: "Inline Git Blame",
                     description: "Whether or not to show git blame data inline in the currently focused line",
@@ -3195,6 +3635,143 @@ pub(crate) fn user_settings_data() -> Vec<SettingsPage> {
                 }),
             ],
         },
+        SettingsPage {
+            title: "Debugger",
+            items: vec![
+                SettingsPageItem::SectionHeader("General"),
+                SettingsPageItem::SettingItem(SettingItem {
+                    title: "Stepping Granularity",
+                    description: "Determines the stepping granularity for debug operations",
+                    field: Box::new(SettingField {
+                        pick: |settings_content| {
+                            if let Some(debugger) = &settings_content.debugger {
+                                &debugger.stepping_granularity
+                            } else {
+                                &None
+                            }
+                        },
+                        pick_mut: |settings_content| {
+                            &mut settings_content
+                                .debugger
+                                .get_or_insert_default()
+                                .stepping_granularity
+                        },
+                    }),
+                    metadata: None,
+                }),
+                SettingsPageItem::SettingItem(SettingItem {
+                    title: "Save Breakpoints",
+                    description: "Whether breakpoints should be reused across Zed sessions",
+                    field: Box::new(SettingField {
+                        pick: |settings_content| {
+                            if let Some(debugger) = &settings_content.debugger {
+                                &debugger.save_breakpoints
+                            } else {
+                                &None
+                            }
+                        },
+                        pick_mut: |settings_content| {
+                            &mut settings_content
+                                .debugger
+                                .get_or_insert_default()
+                                .save_breakpoints
+                        },
+                    }),
+                    metadata: None,
+                }),
+                SettingsPageItem::SettingItem(SettingItem {
+                    title: "Timeout",
+                    description: "Time in milliseconds until timeout error when connecting to a TCP debug adapter",
+                    field: Box::new(SettingField {
+                        pick: |settings_content| {
+                            if let Some(debugger) = &settings_content.debugger {
+                                &debugger.timeout
+                            } else {
+                                &None
+                            }
+                        },
+                        pick_mut: |settings_content| {
+                            &mut settings_content.debugger.get_or_insert_default().timeout
+                        },
+                    }),
+                    metadata: None,
+                }),
+                SettingsPageItem::SettingItem(SettingItem {
+                    title: "Dock",
+                    description: "The dock position of the debug panel",
+                    field: Box::new(SettingField {
+                        pick: |settings_content| {
+                            if let Some(debugger) = &settings_content.debugger {
+                                &debugger.dock
+                            } else {
+                                &None
+                            }
+                        },
+                        pick_mut: |settings_content| {
+                            &mut settings_content.debugger.get_or_insert_default().dock
+                        },
+                    }),
+                    metadata: None,
+                }),
+                SettingsPageItem::SettingItem(SettingItem {
+                    title: "Log DAP Communications",
+                    description: "Whether to log messages between active debug adapters and Zed",
+                    field: Box::new(SettingField {
+                        pick: |settings_content| {
+                            if let Some(debugger) = &settings_content.debugger {
+                                &debugger.log_dap_communications
+                            } else {
+                                &None
+                            }
+                        },
+                        pick_mut: |settings_content| {
+                            &mut settings_content
+                                .debugger
+                                .get_or_insert_default()
+                                .log_dap_communications
+                        },
+                    }),
+                    metadata: None,
+                }),
+                SettingsPageItem::SettingItem(SettingItem {
+                    title: "Format DAP Log Messages",
+                    description: "Whether to format DAP messages when adding them to debug adapter logger",
+                    field: Box::new(SettingField {
+                        pick: |settings_content| {
+                            if let Some(debugger) = &settings_content.debugger {
+                                &debugger.format_dap_log_messages
+                            } else {
+                                &None
+                            }
+                        },
+                        pick_mut: |settings_content| {
+                            &mut settings_content
+                                .debugger
+                                .get_or_insert_default()
+                                .format_dap_log_messages
+                        },
+                    }),
+                    metadata: None,
+                }),
+                SettingsPageItem::SettingItem(SettingItem {
+                    title: "Button",
+                    description: "Whether to show the debug button in the status bar",
+                    field: Box::new(SettingField {
+                        pick: |settings_content| {
+                            if let Some(debugger) = &settings_content.debugger {
+                                &debugger.button
+                            } else {
+                                &None
+                            }
+                        },
+                        pick_mut: |settings_content| {
+                            &mut settings_content.debugger.get_or_insert_default().button
+                        },
+                    }),
+                    metadata: None,
+                }),
+            ],
+        },
         SettingsPage {
             title: "Collaboration",
             items: vec![
@@ -3251,6 +3828,83 @@ pub(crate) fn user_settings_data() -> Vec<SettingsPage> {
                     }),
                     metadata: None,
                 }),
+                SettingsPageItem::SettingItem(SettingItem {
+                    title: "Auto Microphone Volume",
+                    description: "Automatically adjust microphone volume (requires Rodio Audio)",
+                    field: Box::new(SettingField {
+                        pick: |settings_content| {
+                            if let Some(audio) = &settings_content.audio {
+                                &audio.auto_microphone_volume
+                            } else {
+                                &None
+                            }
+                        },
+                        pick_mut: |settings_content| {
+                            &mut settings_content
+                                .audio
+                                .get_or_insert_default()
+                                .auto_microphone_volume
+                        },
+                    }),
+                    metadata: None,
+                }),
+                SettingsPageItem::SettingItem(SettingItem {
+                    title: "Auto Speaker Volume",
+                    description: "Automatically adjust volume of other call members (requires Rodio Audio)",
+                    field: Box::new(SettingField {
+                        pick: |settings_content| {
+                            if let Some(audio) = &settings_content.audio {
+                                &audio.auto_speaker_volume
+                            } else {
+                                &None
+                            }
+                        },
+                        pick_mut: |settings_content| {
+                            &mut settings_content
+                                .audio
+                                .get_or_insert_default()
+                                .auto_speaker_volume
+                        },
+                    }),
+                    metadata: None,
+                }),
+                SettingsPageItem::SettingItem(SettingItem {
+                    title: "Denoise",
+                    description: "Remove background noises (requires Rodio Audio)",
+                    field: Box::new(SettingField {
+                        pick: |settings_content| {
+                            if let Some(audio) = &settings_content.audio {
+                                &audio.denoise
+                            } else {
+                                &None
+                            }
+                        },
+                        pick_mut: |settings_content| {
+                            &mut settings_content.audio.get_or_insert_default().denoise
+                        },
+                    }),
+                    metadata: None,
+                }),
+                SettingsPageItem::SettingItem(SettingItem {
+                    title: "Legacy Audio Compatible",
+                    description: "Use audio parameters compatible with previous versions (requires Rodio Audio)",
+                    field: Box::new(SettingField {
+                        pick: |settings_content| {
+                            if let Some(audio) = &settings_content.audio {
+                                &audio.legacy_audio_compatible
+                            } else {
+                                &None
+                            }
+                        },
+                        pick_mut: |settings_content| {
+                            &mut settings_content
+                                .audio
+                                .get_or_insert_default()
+                                .legacy_audio_compatible
+                        },
+                    }),
+                    metadata: None,
+                }),
             ],
         },
         SettingsPage {

crates/settings_ui/src/settings_ui.rs 🔗

@@ -22,7 +22,7 @@ use std::{
     any::{Any, TypeId, type_name},
     cell::RefCell,
     collections::HashMap,
-    num::NonZeroU32,
+    num::{NonZero, NonZeroU32},
     ops::Range,
     rc::Rc,
     sync::{Arc, LazyLock, RwLock, atomic::AtomicBool},
@@ -105,9 +105,11 @@ impl<T> AnySettingField for SettingField<T> {
             return file.to_settings();
         }
 
-        let (file, _) = cx
-            .global::<SettingsStore>()
-            .get_value_from_file(file.to_settings(), self.pick);
+        let (file, _) = cx.global::<SettingsStore>().get_value_from_file(
+            file.to_settings(),
+            self.pick,
+            self.type_name(),
+        );
         return file;
     }
 }
@@ -381,6 +383,12 @@ fn init_renderers(cx: &mut App) {
         .add_renderer::<u64>(|settings_field, file, _, window, cx| {
             render_numeric_stepper(*settings_field, file, window, cx)
         })
+        .add_renderer::<usize>(|settings_field, file, _, window, cx| {
+            render_numeric_stepper(*settings_field, file, window, cx)
+        })
+        .add_renderer::<NonZero<usize>>(|settings_field, file, _, window, cx| {
+            render_numeric_stepper(*settings_field, file, window, cx)
+        })
         .add_renderer::<NonZeroU32>(|settings_field, file, _, window, cx| {
             render_numeric_stepper(*settings_field, file, window, cx)
         })
@@ -389,6 +397,30 @@ fn init_renderers(cx: &mut App) {
         })
         .add_renderer::<FontWeight>(|settings_field, file, _, window, cx| {
             render_numeric_stepper(*settings_field, file, window, cx)
+        })
+        .add_renderer::<settings::MinimumContrast>(|settings_field, file, _, window, cx| {
+            render_numeric_stepper(*settings_field, file, window, cx)
+        })
+        .add_renderer::<settings::ShowScrollbar>(|settings_field, file, _, window, cx| {
+            render_dropdown(*settings_field, file, window, cx)
+        })
+        .add_renderer::<settings::ScrollbarDiagnostics>(|settings_field, file, _, window, cx| {
+            render_dropdown(*settings_field, file, window, cx)
+        })
+        .add_renderer::<settings::ShowMinimap>(|settings_field, file, _, window, cx| {
+            render_dropdown(*settings_field, file, window, cx)
+        })
+        .add_renderer::<settings::DisplayIn>(|settings_field, file, _, window, cx| {
+            render_dropdown(*settings_field, file, window, cx)
+        })
+        .add_renderer::<settings::MinimapThumb>(|settings_field, file, _, window, cx| {
+            render_dropdown(*settings_field, file, window, cx)
+        })
+        .add_renderer::<settings::MinimapThumbBorder>(|settings_field, file, _, window, cx| {
+            render_dropdown(*settings_field, file, window, cx)
+        })
+        .add_renderer::<settings::SteppingGranularity>(|settings_field, file, _, window, cx| {
+            render_dropdown(*settings_field, file, window, cx)
         });
 
     // todo(settings_ui): Figure out how we want to handle discriminant unions
@@ -1439,8 +1471,11 @@ fn render_text_field<T: From<String> + Into<String> + AsRef<str> + Clone>(
     metadata: Option<&SettingsFieldMetadata>,
     cx: &mut App,
 ) -> AnyElement {
-    let (_, initial_text) =
-        SettingsStore::global(cx).get_value_from_file(file.to_settings(), field.pick);
+    let (_, initial_text) = SettingsStore::global(cx).get_value_from_file(
+        file.to_settings(),
+        field.pick,
+        field.type_name(),
+    );
     let initial_text = Some(initial_text.clone()).filter(|s| !s.as_ref().is_empty());
 
     SettingsEditor::new()
@@ -1468,7 +1503,11 @@ fn render_toggle_button<B: Into<bool> + From<bool> + Copy>(
     file: SettingsUiFile,
     cx: &mut App,
 ) -> AnyElement {
-    let (_, &value) = SettingsStore::global(cx).get_value_from_file(file.to_settings(), field.pick);
+    let (_, &value) = SettingsStore::global(cx).get_value_from_file(
+        file.to_settings(),
+        field.pick,
+        field.type_name(),
+    );
 
     let toggle_state = if value.into() {
         ToggleState::Selected
@@ -1499,7 +1538,7 @@ fn render_font_picker(
     cx: &mut App,
 ) -> AnyElement {
     let current_value = SettingsStore::global(cx)
-        .get_value_from_file(file.to_settings(), field.pick)
+        .get_value_from_file(file.to_settings(), field.pick, field.type_name())
         .1
         .clone();
 
@@ -1556,7 +1595,11 @@ fn render_numeric_stepper<T: NumericStepperType + Send + Sync>(
     window: &mut Window,
     cx: &mut App,
 ) -> AnyElement {
-    let (_, &value) = SettingsStore::global(cx).get_value_from_file(file.to_settings(), field.pick);
+    let (_, &value) = SettingsStore::global(cx).get_value_from_file(
+        file.to_settings(),
+        field.pick,
+        field.type_name(),
+    );
 
     NumericStepper::new("numeric_stepper", value, window, cx)
         .on_change({
@@ -1585,8 +1628,11 @@ where
     let variants = || -> &'static [T] { <T as strum::VariantArray>::VARIANTS };
     let labels = || -> &'static [&'static str] { <T as strum::VariantNames>::VARIANTS };
 
-    let (_, &current_value) =
-        SettingsStore::global(cx).get_value_from_file(file.to_settings(), field.pick);
+    let (_, &current_value) = SettingsStore::global(cx).get_value_from_file(
+        file.to_settings(),
+        field.pick,
+        field.type_name(),
+    );
 
     let current_value_label =
         labels()[variants().iter().position(|v| *v == current_value).unwrap()];

crates/ui_input/src/numeric_stepper.rs 🔗

@@ -1,6 +1,6 @@
 use std::{
     fmt::Display,
-    num::{NonZeroU32, NonZeroU64},
+    num::{NonZero, NonZeroU32, NonZeroU64},
     rc::Rc,
     str::FromStr,
 };
@@ -8,7 +8,7 @@ use std::{
 use editor::{Editor, EditorStyle};
 use gpui::{ClickEvent, Entity, FocusHandle, Focusable, FontWeight, Modifiers};
 
-use settings::CodeFade;
+use settings::{CodeFade, MinimumContrast};
 use ui::{IconButtonShape, prelude::*};
 
 #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
@@ -88,6 +88,30 @@ impl NumericStepperType for settings::CodeFade {
     }
 }
 
+impl NumericStepperType for settings::MinimumContrast {
+    fn default_step() -> Self {
+        MinimumContrast(1.0)
+    }
+    fn large_step() -> Self {
+        MinimumContrast(10.0)
+    }
+    fn small_step() -> Self {
+        MinimumContrast(0.5)
+    }
+    fn min_value() -> Self {
+        MinimumContrast(0.0)
+    }
+    fn max_value() -> Self {
+        MinimumContrast(106.0)
+    }
+    fn saturating_add(self, rhs: Self) -> Self {
+        MinimumContrast((self.0 + rhs.0).min(Self::max_value().0))
+    }
+    fn saturating_sub(self, rhs: Self) -> Self {
+        MinimumContrast((self.0 - rhs.0).max(Self::min_value().0))
+    }
+}
+
 macro_rules! impl_numeric_stepper_int {
     ($type:ident) => {
         impl NumericStepperType for $type {
@@ -207,6 +231,7 @@ impl_numeric_stepper_int!(u64);
 
 impl_numeric_stepper_nonzero_int!(NonZeroU32, u32);
 impl_numeric_stepper_nonzero_int!(NonZeroU64, u64);
+impl_numeric_stepper_nonzero_int!(NonZero<usize>, usize);
 
 #[derive(RegisterComponent)]
 pub struct NumericStepper<T = usize> {