settings_ui.rs

   1//! # settings_ui
   2mod components;
   3use anyhow::Result;
   4use editor::{Editor, EditorEvent};
   5use feature_flags::{FeatureFlag, FeatureFlagAppExt as _};
   6use fuzzy::StringMatchCandidate;
   7use gpui::{
   8    App, Div, Entity, Focusable, FontWeight, Global, ReadGlobal as _, ScrollHandle, Task,
   9    TitlebarOptions, UniformListScrollHandle, Window, WindowHandle, WindowOptions, div, point,
  10    prelude::*, px, size, uniform_list,
  11};
  12use project::WorktreeId;
  13use settings::{
  14    BottomDockLayout, CloseWindowWhenNoItems, CodeFade, CursorShape, OnLastWindowClosed,
  15    RestoreOnStartupBehavior, SaturatingBool, SettingsContent, SettingsStore,
  16};
  17use std::{
  18    any::{Any, TypeId, type_name},
  19    cell::RefCell,
  20    collections::HashMap,
  21    num::NonZeroU32,
  22    ops::Range,
  23    rc::Rc,
  24    sync::{Arc, atomic::AtomicBool},
  25};
  26use ui::{
  27    ButtonLike, ContextMenu, Divider, DropdownMenu, DropdownStyle, IconButtonShape, PopoverMenu,
  28    Switch, SwitchColor, TreeViewItem, WithScrollbar, prelude::*,
  29};
  30use ui_input::{NumericStepper, NumericStepperStyle, NumericStepperType};
  31use util::{ResultExt as _, paths::PathStyle, rel_path::RelPath};
  32use zed_actions::OpenSettingsEditor;
  33
  34use crate::components::SettingsEditor;
  35
  36#[derive(Clone, Copy)]
  37struct SettingField<T: 'static> {
  38    pick: fn(&SettingsContent) -> &Option<T>,
  39    pick_mut: fn(&mut SettingsContent) -> &mut Option<T>,
  40}
  41
  42trait AnySettingField {
  43    fn as_any(&self) -> &dyn Any;
  44    fn type_name(&self) -> &'static str;
  45    fn type_id(&self) -> TypeId;
  46    fn file_set_in(&self, file: SettingsUiFile, cx: &App) -> settings::SettingsFile;
  47}
  48
  49impl<T> AnySettingField for SettingField<T> {
  50    fn as_any(&self) -> &dyn Any {
  51        self
  52    }
  53
  54    fn type_name(&self) -> &'static str {
  55        type_name::<T>()
  56    }
  57
  58    fn type_id(&self) -> TypeId {
  59        TypeId::of::<T>()
  60    }
  61
  62    fn file_set_in(&self, file: SettingsUiFile, cx: &App) -> settings::SettingsFile {
  63        let (file, _) = cx
  64            .global::<SettingsStore>()
  65            .get_value_from_file(file.to_settings(), self.pick);
  66        return file;
  67    }
  68}
  69
  70#[derive(Default, Clone)]
  71struct SettingFieldRenderer {
  72    renderers: Rc<
  73        RefCell<
  74            HashMap<
  75                TypeId,
  76                Box<
  77                    dyn Fn(
  78                        &dyn AnySettingField,
  79                        SettingsUiFile,
  80                        Option<&SettingsFieldMetadata>,
  81                        &mut Window,
  82                        &mut App,
  83                    ) -> AnyElement,
  84                >,
  85            >,
  86        >,
  87    >,
  88}
  89
  90impl Global for SettingFieldRenderer {}
  91
  92impl SettingFieldRenderer {
  93    fn add_renderer<T: 'static>(
  94        &mut self,
  95        renderer: impl Fn(
  96            &SettingField<T>,
  97            SettingsUiFile,
  98            Option<&SettingsFieldMetadata>,
  99            &mut Window,
 100            &mut App,
 101        ) -> AnyElement
 102        + 'static,
 103    ) -> &mut Self {
 104        let key = TypeId::of::<T>();
 105        let renderer = Box::new(
 106            move |any_setting_field: &dyn AnySettingField,
 107                  settings_file: SettingsUiFile,
 108                  metadata: Option<&SettingsFieldMetadata>,
 109                  window: &mut Window,
 110                  cx: &mut App| {
 111                let field = any_setting_field
 112                    .as_any()
 113                    .downcast_ref::<SettingField<T>>()
 114                    .unwrap();
 115                renderer(field, settings_file, metadata, window, cx)
 116            },
 117        );
 118        self.renderers.borrow_mut().insert(key, renderer);
 119        self
 120    }
 121
 122    fn render(
 123        &self,
 124        any_setting_field: &dyn AnySettingField,
 125        settings_file: SettingsUiFile,
 126        metadata: Option<&SettingsFieldMetadata>,
 127        window: &mut Window,
 128        cx: &mut App,
 129    ) -> AnyElement {
 130        let key = any_setting_field.type_id();
 131        if let Some(renderer) = self.renderers.borrow().get(&key) {
 132            renderer(any_setting_field, settings_file, metadata, window, cx)
 133        } else {
 134            panic!(
 135                "No renderer found for type: {}",
 136                any_setting_field.type_name()
 137            )
 138        }
 139    }
 140}
 141
 142struct SettingsFieldMetadata {
 143    placeholder: Option<&'static str>,
 144}
 145
 146fn user_settings_data() -> Vec<SettingsPage> {
 147    vec![
 148        SettingsPage {
 149            title: "General Page",
 150            items: vec![
 151                SettingsPageItem::SectionHeader("General"),
 152                SettingsPageItem::SettingItem(SettingItem {
 153                    title: "Confirm Quit",
 154                    description: "Whether to confirm before quitting Zed",
 155                    field: Box::new(SettingField {
 156                        pick: |settings_content| &settings_content.workspace.confirm_quit,
 157                        pick_mut: |settings_content| &mut settings_content.workspace.confirm_quit,
 158                    }),
 159                    metadata: None,
 160                }),
 161                SettingsPageItem::SettingItem(SettingItem {
 162                    title: "Restore On Startup",
 163                    description: "Whether to restore previous session when opening Zed",
 164                    field: Box::new(SettingField {
 165                        pick: |settings_content| &settings_content.workspace.restore_on_startup,
 166                        pick_mut: |settings_content| {
 167                            &mut settings_content.workspace.restore_on_startup
 168                        },
 169                    }),
 170                    metadata: None,
 171                }),
 172                SettingsPageItem::SettingItem(SettingItem {
 173                    title: "Restore File State",
 174                    description: "Whether to restore previous file state when reopening",
 175                    field: Box::new(SettingField {
 176                        pick: |settings_content| &settings_content.workspace.restore_on_file_reopen,
 177                        pick_mut: |settings_content| {
 178                            &mut settings_content.workspace.restore_on_file_reopen
 179                        },
 180                    }),
 181                    metadata: None,
 182                }),
 183                SettingsPageItem::SettingItem(SettingItem {
 184                    title: "Close on File Delete",
 185                    description: "Whether to automatically close files that have been deleted",
 186                    field: Box::new(SettingField {
 187                        pick: |settings_content| &settings_content.workspace.close_on_file_delete,
 188                        pick_mut: |settings_content| {
 189                            &mut settings_content.workspace.close_on_file_delete
 190                        },
 191                    }),
 192                    metadata: None,
 193                }),
 194                SettingsPageItem::SettingItem(SettingItem {
 195                    title: "When Closing With No Tabs",
 196                    description: "What to do when using 'close active item' with no tabs",
 197                    field: Box::new(SettingField {
 198                        pick: |settings_content| {
 199                            &settings_content.workspace.when_closing_with_no_tabs
 200                        },
 201                        pick_mut: |settings_content| {
 202                            &mut settings_content.workspace.when_closing_with_no_tabs
 203                        },
 204                    }),
 205                    metadata: None,
 206                }),
 207                SettingsPageItem::SettingItem(SettingItem {
 208                    title: "On Last Window Closed",
 209                    description: "What to do when the last window is closed",
 210                    field: Box::new(SettingField {
 211                        pick: |settings_content| &settings_content.workspace.on_last_window_closed,
 212                        pick_mut: |settings_content| {
 213                            &mut settings_content.workspace.on_last_window_closed
 214                        },
 215                    }),
 216                    metadata: None,
 217                }),
 218                SettingsPageItem::SettingItem(SettingItem {
 219                    title: "Use System Path Prompts",
 220                    description: "Whether to use system dialogs for Open and Save As",
 221                    field: Box::new(SettingField {
 222                        pick: |settings_content| {
 223                            &settings_content.workspace.use_system_path_prompts
 224                        },
 225                        pick_mut: |settings_content| {
 226                            &mut settings_content.workspace.use_system_path_prompts
 227                        },
 228                    }),
 229                    metadata: None,
 230                }),
 231                SettingsPageItem::SettingItem(SettingItem {
 232                    title: "Use System Prompts",
 233                    description: "Whether to use system prompts for confirmations",
 234                    field: Box::new(SettingField {
 235                        pick: |settings_content| &settings_content.workspace.use_system_prompts,
 236                        pick_mut: |settings_content| {
 237                            &mut settings_content.workspace.use_system_prompts
 238                        },
 239                    }),
 240                    metadata: None,
 241                }),
 242                SettingsPageItem::SectionHeader("Scoped Settings"),
 243                // todo(settings_ui): Implement another setting item type that just shows an edit in settings.json
 244                // SettingsPageItem::SettingItem(SettingItem {
 245                //     title: "Preview Channel",
 246                //     description: "Which settings should be activated only in Preview build of Zed",
 247                //     field: Box::new(SettingField {
 248                //         pick: |settings_content| &settings_content.workspace.use_system_prompts,
 249                //         pick_mut: |settings_content| {
 250                //             &mut settings_content.workspace.use_system_prompts
 251                //         },
 252                //     }),
 253                //     metadata: None,
 254                // }),
 255                // SettingsPageItem::SettingItem(SettingItem {
 256                //     title: "Settings Profiles",
 257                //     description: "Any number of settings profiles that are temporarily applied on top of your existing user settings.",
 258                //     field: Box::new(SettingField {
 259                //         pick: |settings_content| &settings_content.workspace.use_system_prompts,
 260                //         pick_mut: |settings_content| {
 261                //             &mut settings_content.workspace.use_system_prompts
 262                //         },
 263                //     }),
 264                //     metadata: None,
 265                // }),
 266                SettingsPageItem::SectionHeader("Privacy"),
 267                SettingsPageItem::SettingItem(SettingItem {
 268                    title: "Telemetry Diagnostics",
 269                    description: "Send debug info like crash reports.",
 270                    field: Box::new(SettingField {
 271                        pick: |settings_content| {
 272                            if let Some(telemetry) = &settings_content.telemetry {
 273                                &telemetry.diagnostics
 274                            } else {
 275                                &None
 276                            }
 277                        },
 278                        pick_mut: |settings_content| {
 279                            &mut settings_content
 280                                .telemetry
 281                                .get_or_insert_default()
 282                                .diagnostics
 283                        },
 284                    }),
 285                    metadata: None,
 286                }),
 287                SettingsPageItem::SettingItem(SettingItem {
 288                    title: "Telemetry Metrics",
 289                    description: "Send anonymized usage data like what languages you're using Zed with.",
 290                    field: Box::new(SettingField {
 291                        pick: |settings_content| {
 292                            if let Some(telemetry) = &settings_content.telemetry {
 293                                &telemetry.metrics
 294                            } else {
 295                                &None
 296                            }
 297                        },
 298                        pick_mut: |settings_content| {
 299                            &mut settings_content.telemetry.get_or_insert_default().metrics
 300                        },
 301                    }),
 302                    metadata: None,
 303                }),
 304            ],
 305        },
 306        SettingsPage {
 307            title: "Appearance & Behavior",
 308            items: vec![
 309                SettingsPageItem::SectionHeader("Theme"),
 310                // todo(settings_ui): Figure out how we want to add these
 311                // SettingsPageItem::SettingItem(SettingItem {
 312                //     title: "Theme Mode",
 313                //     description: "How to select the theme",
 314                //     field: Box::new(SettingField {
 315                //         pick: |settings_content| &settings_content.theme.theme,
 316                //         pick_mut: |settings_content| &mut settings_content.theme.theme,
 317                //     }),
 318                //     metadata: None,
 319                // }),
 320                // SettingsPageItem::SettingItem(SettingItem {
 321                //     title: "Icon Theme",
 322                //     // todo(settings_ui)
 323                //     // This description is misleading because the icon theme is used in more places than the file explorer)
 324                //     description: "Choose the icon theme for file explorer",
 325                //     field: Box::new(SettingField {
 326                //         pick: |settings_content| &settings_content.theme.icon_theme,
 327                //         pick_mut: |settings_content| &mut settings_content.theme.icon_theme,
 328                //     }),
 329                //     metadata: None,
 330                // }),
 331                SettingsPageItem::SectionHeader("Layout"),
 332                SettingsPageItem::SettingItem(SettingItem {
 333                    title: "Bottom Dock Layout",
 334                    description: "Layout mode for the bottom dock",
 335                    field: Box::new(SettingField {
 336                        pick: |settings_content| &settings_content.workspace.bottom_dock_layout,
 337                        pick_mut: |settings_content| {
 338                            &mut settings_content.workspace.bottom_dock_layout
 339                        },
 340                    }),
 341                    metadata: None,
 342                }),
 343                SettingsPageItem::SettingItem(SettingItem {
 344                    title: "Zoomed Padding",
 345                    description: "Whether to show padding for zoomed panels",
 346                    field: Box::new(SettingField {
 347                        pick: |settings_content| &settings_content.workspace.zoomed_padding,
 348                        pick_mut: |settings_content| &mut settings_content.workspace.zoomed_padding,
 349                    }),
 350                    metadata: None,
 351                }),
 352                SettingsPageItem::SettingItem(SettingItem {
 353                    title: "Use System Window Tabs",
 354                    description: "Whether to allow windows to tab together based on the user's tabbing preference (macOS only)",
 355                    field: Box::new(SettingField {
 356                        pick: |settings_content| &settings_content.workspace.use_system_window_tabs,
 357                        pick_mut: |settings_content| {
 358                            &mut settings_content.workspace.use_system_window_tabs
 359                        },
 360                    }),
 361                    metadata: None,
 362                }),
 363                SettingsPageItem::SectionHeader("Fonts"),
 364                SettingsPageItem::SettingItem(SettingItem {
 365                    title: "Buffer Font Family",
 366                    description: "Font family for editor text",
 367                    field: Box::new(SettingField {
 368                        pick: |settings_content| &settings_content.theme.buffer_font_family,
 369                        pick_mut: |settings_content| &mut settings_content.theme.buffer_font_family,
 370                    }),
 371                    metadata: None,
 372                }),
 373                SettingsPageItem::SettingItem(SettingItem {
 374                    title: "Buffer Font Size",
 375                    description: "Font size for editor text",
 376                    field: Box::new(SettingField {
 377                        pick: |settings_content| &settings_content.theme.buffer_font_size,
 378                        pick_mut: |settings_content| &mut settings_content.theme.buffer_font_size,
 379                    }),
 380                    metadata: None,
 381                }),
 382                SettingsPageItem::SettingItem(SettingItem {
 383                    title: "Buffer Font Weight",
 384                    description: "Font weight for editor text (100-900)",
 385                    field: Box::new(SettingField {
 386                        pick: |settings_content| &settings_content.theme.buffer_font_weight,
 387                        pick_mut: |settings_content| &mut settings_content.theme.buffer_font_weight,
 388                    }),
 389                    metadata: None,
 390                }),
 391                SettingsPageItem::SettingItem(SettingItem {
 392                    title: "Buffer Line Height",
 393                    description: "Line height for editor text",
 394                    field: Box::new(SettingField {
 395                        pick: |settings_content| &settings_content.theme.buffer_line_height,
 396                        pick_mut: |settings_content| &mut settings_content.theme.buffer_line_height,
 397                    }),
 398                    metadata: None,
 399                }),
 400                SettingsPageItem::SettingItem(SettingItem {
 401                    title: "UI Font Family",
 402                    description: "Font family for UI elements",
 403                    field: Box::new(SettingField {
 404                        pick: |settings_content| &settings_content.theme.ui_font_family,
 405                        pick_mut: |settings_content| &mut settings_content.theme.ui_font_family,
 406                    }),
 407                    metadata: None,
 408                }),
 409                SettingsPageItem::SettingItem(SettingItem {
 410                    title: "UI Font Size",
 411                    description: "Font size for UI elements",
 412                    field: Box::new(SettingField {
 413                        pick: |settings_content| &settings_content.theme.ui_font_size,
 414                        pick_mut: |settings_content| &mut settings_content.theme.ui_font_size,
 415                    }),
 416                    metadata: None,
 417                }),
 418                SettingsPageItem::SettingItem(SettingItem {
 419                    title: "UI Font Weight",
 420                    description: "Font weight for UI elements (100-900)",
 421                    field: Box::new(SettingField {
 422                        pick: |settings_content| &settings_content.theme.ui_font_weight,
 423                        pick_mut: |settings_content| &mut settings_content.theme.ui_font_weight,
 424                    }),
 425                    metadata: None,
 426                }),
 427                SettingsPageItem::SectionHeader("Keymap"),
 428                SettingsPageItem::SettingItem(SettingItem {
 429                    title: "Base Keymap",
 430                    description: "The name of a base set of key bindings to use",
 431                    field: Box::new(SettingField {
 432                        pick: |settings_content| &settings_content.base_keymap,
 433                        pick_mut: |settings_content| &mut settings_content.base_keymap,
 434                    }),
 435                    metadata: None,
 436                }),
 437                // todo(settings_ui): Vim/Helix Mode should be apart of one type because it's undefined
 438                // behavior to have them both enabled at the same time
 439                SettingsPageItem::SettingItem(SettingItem {
 440                    title: "Vim Mode",
 441                    description: "Whether to enable vim modes and key bindings",
 442                    field: Box::new(SettingField {
 443                        pick: |settings_content| &settings_content.vim_mode,
 444                        pick_mut: |settings_content| &mut settings_content.vim_mode,
 445                    }),
 446                    metadata: None,
 447                }),
 448                SettingsPageItem::SettingItem(SettingItem {
 449                    title: "Helix Mode",
 450                    description: "Whether to enable helix modes and key bindings",
 451                    field: Box::new(SettingField {
 452                        pick: |settings_content| &settings_content.helix_mode,
 453                        pick_mut: |settings_content| &mut settings_content.helix_mode,
 454                    }),
 455                    metadata: None,
 456                }),
 457                SettingsPageItem::SettingItem(SettingItem {
 458                    title: "Multi Cursor Modifier",
 459                    description: "Modifier key for adding multiple cursors",
 460                    field: Box::new(SettingField {
 461                        pick: |settings_content| &settings_content.editor.multi_cursor_modifier,
 462                        pick_mut: |settings_content| {
 463                            &mut settings_content.editor.multi_cursor_modifier
 464                        },
 465                    }),
 466                    metadata: None,
 467                }),
 468                SettingsPageItem::SectionHeader("Cursor"),
 469                SettingsPageItem::SettingItem(SettingItem {
 470                    title: "Cursor Blink",
 471                    description: "Whether the cursor blinks in the editor",
 472                    field: Box::new(SettingField {
 473                        pick: |settings_content| &settings_content.editor.cursor_blink,
 474                        pick_mut: |settings_content| &mut settings_content.editor.cursor_blink,
 475                    }),
 476                    metadata: None,
 477                }),
 478                SettingsPageItem::SettingItem(SettingItem {
 479                    title: "Cursor Shape",
 480                    description: "Cursor shape for the editor",
 481                    field: Box::new(SettingField {
 482                        pick: |settings_content| &settings_content.editor.cursor_shape,
 483                        pick_mut: |settings_content| &mut settings_content.editor.cursor_shape,
 484                    }),
 485                    metadata: None,
 486                }),
 487                SettingsPageItem::SettingItem(SettingItem {
 488                    title: "Hide Mouse",
 489                    description: "When to hide the mouse cursor",
 490                    field: Box::new(SettingField {
 491                        pick: |settings_content| &settings_content.editor.hide_mouse,
 492                        pick_mut: |settings_content| &mut settings_content.editor.hide_mouse,
 493                    }),
 494                    metadata: None,
 495                }),
 496                SettingsPageItem::SectionHeader("Highlighting"),
 497                SettingsPageItem::SettingItem(SettingItem {
 498                    title: "Unnecessary Code Fade",
 499                    description: "How much to fade out unused code (0.0 - 0.9)",
 500                    field: Box::new(SettingField {
 501                        pick: |settings_content| &settings_content.theme.unnecessary_code_fade,
 502                        pick_mut: |settings_content| {
 503                            &mut settings_content.theme.unnecessary_code_fade
 504                        },
 505                    }),
 506                    metadata: None,
 507                }),
 508                SettingsPageItem::SettingItem(SettingItem {
 509                    title: "Current Line Highlight",
 510                    description: "How to highlight the current line",
 511                    field: Box::new(SettingField {
 512                        pick: |settings_content| &settings_content.editor.current_line_highlight,
 513                        pick_mut: |settings_content| {
 514                            &mut settings_content.editor.current_line_highlight
 515                        },
 516                    }),
 517                    metadata: None,
 518                }),
 519                SettingsPageItem::SettingItem(SettingItem {
 520                    title: "Selection Highlight",
 521                    description: "Whether to highlight all occurrences of selected text",
 522                    field: Box::new(SettingField {
 523                        pick: |settings_content| &settings_content.editor.selection_highlight,
 524                        pick_mut: |settings_content| {
 525                            &mut settings_content.editor.selection_highlight
 526                        },
 527                    }),
 528                    metadata: None,
 529                }),
 530                SettingsPageItem::SettingItem(SettingItem {
 531                    title: "Rounded Selection",
 532                    description: "Whether the text selection should have rounded corners",
 533                    field: Box::new(SettingField {
 534                        pick: |settings_content| &settings_content.editor.rounded_selection,
 535                        pick_mut: |settings_content| &mut settings_content.editor.rounded_selection,
 536                    }),
 537                    metadata: None,
 538                }),
 539                SettingsPageItem::SectionHeader("Guides"),
 540                SettingsPageItem::SettingItem(SettingItem {
 541                    title: "Show Wrap Guides",
 542                    description: "Whether to show wrap guides (vertical rulers)",
 543                    field: Box::new(SettingField {
 544                        pick: |settings_content| {
 545                            &settings_content
 546                                .project
 547                                .all_languages
 548                                .defaults
 549                                .show_wrap_guides
 550                        },
 551                        pick_mut: |settings_content| {
 552                            &mut settings_content
 553                                .project
 554                                .all_languages
 555                                .defaults
 556                                .show_wrap_guides
 557                        },
 558                    }),
 559                    metadata: None,
 560                }),
 561                // todo(settings_ui): This needs a custom component
 562                // SettingsPageItem::SettingItem(SettingItem {
 563                //     title: "Wrap Guides",
 564                //     description: "Character counts at which to show wrap guides",
 565                //     field: Box::new(SettingField {
 566                //         pick: |settings_content| {
 567                //             &settings_content
 568                //                 .project
 569                //                 .all_languages
 570                //                 .defaults
 571                //                 .wrap_guides
 572                //         },
 573                //         pick_mut: |settings_content| {
 574                //             &mut settings_content
 575                //                 .project
 576                //                 .all_languages
 577                //                 .defaults
 578                //                 .wrap_guides
 579                //         },
 580                //     }),
 581                //     metadata: None,
 582                // }),
 583                SettingsPageItem::SectionHeader("Whitespace"),
 584                SettingsPageItem::SettingItem(SettingItem {
 585                    title: "Show Whitespace",
 586                    description: "Whether to show tabs and spaces",
 587                    field: Box::new(SettingField {
 588                        pick: |settings_content| {
 589                            &settings_content
 590                                .project
 591                                .all_languages
 592                                .defaults
 593                                .show_whitespaces
 594                        },
 595                        pick_mut: |settings_content| {
 596                            &mut settings_content
 597                                .project
 598                                .all_languages
 599                                .defaults
 600                                .show_whitespaces
 601                        },
 602                    }),
 603                    metadata: None,
 604                }),
 605                SettingsPageItem::SectionHeader("Window"),
 606                // todo(settings_ui): Should we filter by platform?
 607                SettingsPageItem::SettingItem(SettingItem {
 608                    title: "Use System Window Tabs",
 609                    description: "Whether to allow windows to tab together (macOS only)",
 610                    field: Box::new(SettingField {
 611                        pick: |settings_content| &settings_content.workspace.use_system_window_tabs,
 612                        pick_mut: |settings_content| {
 613                            &mut settings_content.workspace.use_system_window_tabs
 614                        },
 615                    }),
 616                    metadata: None,
 617                }),
 618                SettingsPageItem::SectionHeader("Layout"),
 619                SettingsPageItem::SettingItem(SettingItem {
 620                    title: "Zoomed Padding",
 621                    description: "Whether to show padding for zoomed panels",
 622                    field: Box::new(SettingField {
 623                        pick: |settings_content| &settings_content.workspace.zoomed_padding,
 624                        pick_mut: |settings_content| &mut settings_content.workspace.zoomed_padding,
 625                    }),
 626                    metadata: None,
 627                }),
 628                // todo(settings_ui): Needs numeric stepper + option within an option
 629                // SettingsPageItem::SettingItem(SettingItem {
 630                //     title: "Centered Layout Left Padding",
 631                //     description: "Left padding for centered layout",
 632                //     field: Box::new(SettingField {
 633                //         pick: |settings_content| {
 634                //             &settings_content.workspace.centered_layout.left_padding
 635                //         },
 636                //         pick_mut: |settings_content| {
 637                //             &mut settings_content.workspace.centered_layout.left_padding
 638                //         },
 639                //     }),
 640                //     metadata: None,
 641                // }),
 642                // SettingsPageItem::SettingItem(SettingItem {
 643                //     title: "Centered Layout Right Padding",
 644                //     description: "Right padding for centered layout",
 645                //     field: Box::new(SettingField {
 646                //         pick: |settings_content| {
 647                //             if let Some(centered_layout) =
 648                //                 &settings_content.workspace.centered_layout
 649                //             {
 650                //                 &centered_layout.right_padding
 651                //             } else {
 652                //                 &None
 653                //             }
 654                //         },
 655                //         pick_mut: |settings_content| {
 656                //             if let Some(mut centered_layout) =
 657                //                 settings_content.workspace.centered_layout
 658                //             {
 659                //                 &mut centered_layout.right_padding
 660                //             } else {
 661                //                 &mut None
 662                //             }
 663                //         },
 664                //     }),
 665                //     metadata: None,
 666                // }),
 667                SettingsPageItem::SettingItem(SettingItem {
 668                    title: "Bottom Dock Layout",
 669                    description: "Layout mode of the bottom dock",
 670                    field: Box::new(SettingField {
 671                        pick: |settings_content| &settings_content.workspace.bottom_dock_layout,
 672                        pick_mut: |settings_content| {
 673                            &mut settings_content.workspace.bottom_dock_layout
 674                        },
 675                    }),
 676                    metadata: None,
 677                }),
 678            ],
 679        },
 680        SettingsPage {
 681            title: "Editor",
 682            items: vec![
 683                SettingsPageItem::SectionHeader("Indentation"),
 684                // todo(settings_ui): Needs numeric stepper
 685                SettingsPageItem::SettingItem(SettingItem {
 686                    title: "Tab Size",
 687                    description: "How many columns a tab should occupy",
 688                    field: Box::new(SettingField {
 689                        pick: |settings_content| {
 690                            &settings_content.project.all_languages.defaults.tab_size
 691                        },
 692                        pick_mut: |settings_content| {
 693                            &mut settings_content.project.all_languages.defaults.tab_size
 694                        },
 695                    }),
 696                    metadata: None,
 697                }),
 698                SettingsPageItem::SettingItem(SettingItem {
 699                    title: "Hard Tabs",
 700                    description: "Whether to indent lines using tab characters, as opposed to multiple spaces",
 701                    field: Box::new(SettingField {
 702                        pick: |settings_content| {
 703                            &settings_content.project.all_languages.defaults.hard_tabs
 704                        },
 705                        pick_mut: |settings_content| {
 706                            &mut settings_content.project.all_languages.defaults.hard_tabs
 707                        },
 708                    }),
 709                    metadata: None,
 710                }),
 711                SettingsPageItem::SettingItem(SettingItem {
 712                    title: "Auto Indent",
 713                    description: "Whether indentation should be adjusted based on the context whilst typing",
 714                    field: Box::new(SettingField {
 715                        pick: |settings_content| {
 716                            &settings_content.project.all_languages.defaults.auto_indent
 717                        },
 718                        pick_mut: |settings_content| {
 719                            &mut settings_content.project.all_languages.defaults.auto_indent
 720                        },
 721                    }),
 722                    metadata: None,
 723                }),
 724                SettingsPageItem::SettingItem(SettingItem {
 725                    title: "Auto Indent On Paste",
 726                    description: "Whether indentation of pasted content should be adjusted based on the context",
 727                    field: Box::new(SettingField {
 728                        pick: |settings_content| {
 729                            &settings_content
 730                                .project
 731                                .all_languages
 732                                .defaults
 733                                .auto_indent_on_paste
 734                        },
 735                        pick_mut: |settings_content| {
 736                            &mut settings_content
 737                                .project
 738                                .all_languages
 739                                .defaults
 740                                .auto_indent_on_paste
 741                        },
 742                    }),
 743                    metadata: None,
 744                }),
 745                SettingsPageItem::SectionHeader("Wrapping"),
 746                // todo(settings_ui): Needs numeric stepper
 747                // SettingsPageItem::SettingItem(SettingItem {
 748                //     title: "Preferred Line Length",
 749                //     description: "The column at which to soft-wrap lines, for buffers where soft-wrap is enabled",
 750                //     field: Box::new(SettingField {
 751                //         pick: |settings_content| &settings_content.project.all_languages.defaults.preferred_line_length,
 752                //         pick_mut: |settings_content| &mut settings_content.project.all_languages.defaults.preferred_line_length,
 753                //     }),
 754                //     metadata: None,
 755                // }),
 756                SettingsPageItem::SettingItem(SettingItem {
 757                    title: "Soft Wrap",
 758                    description: "How to soft-wrap long lines of text",
 759                    field: Box::new(SettingField {
 760                        pick: |settings_content| {
 761                            &settings_content.project.all_languages.defaults.soft_wrap
 762                        },
 763                        pick_mut: |settings_content| {
 764                            &mut settings_content.project.all_languages.defaults.soft_wrap
 765                        },
 766                    }),
 767                    metadata: None,
 768                }),
 769                SettingsPageItem::SectionHeader("Search"),
 770                SettingsPageItem::SettingItem(SettingItem {
 771                    title: "Search Wrap",
 772                    description: "Whether the editor search results will loop",
 773                    field: Box::new(SettingField {
 774                        pick: |settings_content| &settings_content.editor.search_wrap,
 775                        pick_mut: |settings_content| &mut settings_content.editor.search_wrap,
 776                    }),
 777                    metadata: None,
 778                }),
 779                SettingsPageItem::SettingItem(SettingItem {
 780                    title: "Seed Search Query From Cursor",
 781                    description: "When to populate a new search's query based on the text under the cursor",
 782                    field: Box::new(SettingField {
 783                        pick: |settings_content| {
 784                            &settings_content.editor.seed_search_query_from_cursor
 785                        },
 786                        pick_mut: |settings_content| {
 787                            &mut settings_content.editor.seed_search_query_from_cursor
 788                        },
 789                    }),
 790                    metadata: None,
 791                }),
 792                SettingsPageItem::SettingItem(SettingItem {
 793                    title: "Use Smartcase Search",
 794                    description: "Whether to use smartcase search",
 795                    field: Box::new(SettingField {
 796                        pick: |settings_content| &settings_content.editor.use_smartcase_search,
 797                        pick_mut: |settings_content| {
 798                            &mut settings_content.editor.use_smartcase_search
 799                        },
 800                    }),
 801                    metadata: None,
 802                }),
 803                SettingsPageItem::SectionHeader("Editor Behavior"),
 804                SettingsPageItem::SettingItem(SettingItem {
 805                    title: "Redact Private Values",
 806                    description: "Hide the values of variables in private files",
 807                    field: Box::new(SettingField {
 808                        pick: |settings_content| &settings_content.editor.redact_private_values,
 809                        pick_mut: |settings_content| {
 810                            &mut settings_content.editor.redact_private_values
 811                        },
 812                    }),
 813                    metadata: None,
 814                }),
 815                SettingsPageItem::SettingItem(SettingItem {
 816                    title: "Middle Click Paste",
 817                    description: "Whether to enable middle-click paste on Linux",
 818                    field: Box::new(SettingField {
 819                        pick: |settings_content| &settings_content.editor.middle_click_paste,
 820                        pick_mut: |settings_content| {
 821                            &mut settings_content.editor.middle_click_paste
 822                        },
 823                    }),
 824                    metadata: None,
 825                }),
 826                SettingsPageItem::SettingItem(SettingItem {
 827                    title: "Double Click In Multibuffer",
 828                    description: "What to do when multibuffer is double clicked in some of its excerpts",
 829                    field: Box::new(SettingField {
 830                        pick: |settings_content| {
 831                            &settings_content.editor.double_click_in_multibuffer
 832                        },
 833                        pick_mut: |settings_content| {
 834                            &mut settings_content.editor.double_click_in_multibuffer
 835                        },
 836                    }),
 837                    metadata: None,
 838                }),
 839                SettingsPageItem::SettingItem(SettingItem {
 840                    title: "Go To Definition Fallback",
 841                    description: "Whether to follow-up empty go to definition responses from the language server",
 842                    field: Box::new(SettingField {
 843                        pick: |settings_content| &settings_content.editor.go_to_definition_fallback,
 844                        pick_mut: |settings_content| {
 845                            &mut settings_content.editor.go_to_definition_fallback
 846                        },
 847                    }),
 848                    metadata: None,
 849                }),
 850                SettingsPageItem::SectionHeader("Scrolling"),
 851                SettingsPageItem::SettingItem(SettingItem {
 852                    title: "Scroll Beyond Last Line",
 853                    description: "Whether the editor will scroll beyond the last line",
 854                    field: Box::new(SettingField {
 855                        pick: |settings_content| &settings_content.editor.scroll_beyond_last_line,
 856                        pick_mut: |settings_content| {
 857                            &mut settings_content.editor.scroll_beyond_last_line
 858                        },
 859                    }),
 860                    metadata: None,
 861                }),
 862                SettingsPageItem::SettingItem(SettingItem {
 863                    title: "Vertical Scroll Margin",
 864                    description: "The number of lines to keep above/below the cursor when auto-scrolling",
 865                    field: Box::new(SettingField {
 866                        pick: |settings_content| &settings_content.editor.vertical_scroll_margin,
 867                        pick_mut: |settings_content| {
 868                            &mut settings_content.editor.vertical_scroll_margin
 869                        },
 870                    }),
 871                    metadata: None,
 872                }),
 873                SettingsPageItem::SettingItem(SettingItem {
 874                    title: "Horizontal Scroll Margin",
 875                    description: "The number of characters to keep on either side when scrolling with the mouse",
 876                    field: Box::new(SettingField {
 877                        pick: |settings_content| &settings_content.editor.horizontal_scroll_margin,
 878                        pick_mut: |settings_content| {
 879                            &mut settings_content.editor.horizontal_scroll_margin
 880                        },
 881                    }),
 882                    metadata: None,
 883                }),
 884                SettingsPageItem::SettingItem(SettingItem {
 885                    title: "Scroll Sensitivity",
 886                    description: "Scroll sensitivity multiplier for both horizontal and vertical scrolling",
 887                    field: Box::new(SettingField {
 888                        pick: |settings_content| &settings_content.editor.scroll_sensitivity,
 889                        pick_mut: |settings_content| {
 890                            &mut settings_content.editor.scroll_sensitivity
 891                        },
 892                    }),
 893                    metadata: None,
 894                }),
 895                SettingsPageItem::SettingItem(SettingItem {
 896                    title: "Fast Scroll Sensitivity",
 897                    description: "Fast Scroll sensitivity multiplier for both horizontal and vertical scrolling",
 898                    field: Box::new(SettingField {
 899                        pick: |settings_content| &settings_content.editor.fast_scroll_sensitivity,
 900                        pick_mut: |settings_content| {
 901                            &mut settings_content.editor.fast_scroll_sensitivity
 902                        },
 903                    }),
 904                    metadata: None,
 905                }),
 906                SettingsPageItem::SettingItem(SettingItem {
 907                    title: "Autoscroll On Clicks",
 908                    description: "Whether to scroll when clicking near the edge of the visible text area",
 909                    field: Box::new(SettingField {
 910                        pick: |settings_content| &settings_content.editor.autoscroll_on_clicks,
 911                        pick_mut: |settings_content| {
 912                            &mut settings_content.editor.autoscroll_on_clicks
 913                        },
 914                    }),
 915                    metadata: None,
 916                }),
 917                SettingsPageItem::SectionHeader("Auto Actions"),
 918                SettingsPageItem::SettingItem(SettingItem {
 919                    title: "Use Autoclose",
 920                    description: "Whether to automatically type closing characters for you",
 921                    field: Box::new(SettingField {
 922                        pick: |settings_content| {
 923                            &settings_content
 924                                .project
 925                                .all_languages
 926                                .defaults
 927                                .use_autoclose
 928                        },
 929                        pick_mut: |settings_content| {
 930                            &mut settings_content
 931                                .project
 932                                .all_languages
 933                                .defaults
 934                                .use_autoclose
 935                        },
 936                    }),
 937                    metadata: None,
 938                }),
 939                SettingsPageItem::SettingItem(SettingItem {
 940                    title: "Use Auto Surround",
 941                    description: "Whether to automatically surround text with characters for you",
 942                    field: Box::new(SettingField {
 943                        pick: |settings_content| {
 944                            &settings_content
 945                                .project
 946                                .all_languages
 947                                .defaults
 948                                .use_auto_surround
 949                        },
 950                        pick_mut: |settings_content| {
 951                            &mut settings_content
 952                                .project
 953                                .all_languages
 954                                .defaults
 955                                .use_auto_surround
 956                        },
 957                    }),
 958                    metadata: None,
 959                }),
 960                SettingsPageItem::SettingItem(SettingItem {
 961                    title: "Use On Type Format",
 962                    description: "Whether to use additional LSP queries to format the code after every trigger symbol input",
 963                    field: Box::new(SettingField {
 964                        pick: |settings_content| {
 965                            &settings_content
 966                                .project
 967                                .all_languages
 968                                .defaults
 969                                .use_on_type_format
 970                        },
 971                        pick_mut: |settings_content| {
 972                            &mut settings_content
 973                                .project
 974                                .all_languages
 975                                .defaults
 976                                .use_on_type_format
 977                        },
 978                    }),
 979                    metadata: None,
 980                }),
 981                SettingsPageItem::SettingItem(SettingItem {
 982                    title: "Always Treat Brackets As Autoclosed",
 983                    description: "Controls how the editor handles the autoclosed characters",
 984                    field: Box::new(SettingField {
 985                        pick: |settings_content| {
 986                            &settings_content
 987                                .project
 988                                .all_languages
 989                                .defaults
 990                                .always_treat_brackets_as_autoclosed
 991                        },
 992                        pick_mut: |settings_content| {
 993                            &mut settings_content
 994                                .project
 995                                .all_languages
 996                                .defaults
 997                                .always_treat_brackets_as_autoclosed
 998                        },
 999                    }),
1000                    metadata: None,
1001                }),
1002                SettingsPageItem::SectionHeader("Formatting"),
1003                SettingsPageItem::SettingItem(SettingItem {
1004                    title: "Remove Trailing Whitespace On Save",
1005                    description: "Whether or not to remove any trailing whitespace from lines of a buffer before saving it",
1006                    field: Box::new(SettingField {
1007                        pick: |settings_content| {
1008                            &settings_content
1009                                .project
1010                                .all_languages
1011                                .defaults
1012                                .remove_trailing_whitespace_on_save
1013                        },
1014                        pick_mut: |settings_content| {
1015                            &mut settings_content
1016                                .project
1017                                .all_languages
1018                                .defaults
1019                                .remove_trailing_whitespace_on_save
1020                        },
1021                    }),
1022                    metadata: None,
1023                }),
1024                SettingsPageItem::SettingItem(SettingItem {
1025                    title: "Ensure Final Newline On Save",
1026                    description: "Whether or not to ensure there's a single newline at the end of a buffer when saving it",
1027                    field: Box::new(SettingField {
1028                        pick: |settings_content| {
1029                            &settings_content
1030                                .project
1031                                .all_languages
1032                                .defaults
1033                                .ensure_final_newline_on_save
1034                        },
1035                        pick_mut: |settings_content| {
1036                            &mut settings_content
1037                                .project
1038                                .all_languages
1039                                .defaults
1040                                .ensure_final_newline_on_save
1041                        },
1042                    }),
1043                    metadata: None,
1044                }),
1045                SettingsPageItem::SettingItem(SettingItem {
1046                    title: "Extend Comment On Newline",
1047                    description: "Whether to start a new line with a comment when a previous line is a comment as well",
1048                    field: Box::new(SettingField {
1049                        pick: |settings_content| {
1050                            &settings_content
1051                                .project
1052                                .all_languages
1053                                .defaults
1054                                .extend_comment_on_newline
1055                        },
1056                        pick_mut: |settings_content| {
1057                            &mut settings_content
1058                                .project
1059                                .all_languages
1060                                .defaults
1061                                .extend_comment_on_newline
1062                        },
1063                    }),
1064                    metadata: None,
1065                }),
1066                SettingsPageItem::SectionHeader("Completions"),
1067                SettingsPageItem::SettingItem(SettingItem {
1068                    title: "Show Completions On Input",
1069                    description: "Whether to pop the completions menu while typing in an editor without explicitly requesting it",
1070                    field: Box::new(SettingField {
1071                        pick: |settings_content| {
1072                            &settings_content
1073                                .project
1074                                .all_languages
1075                                .defaults
1076                                .show_completions_on_input
1077                        },
1078                        pick_mut: |settings_content| {
1079                            &mut settings_content
1080                                .project
1081                                .all_languages
1082                                .defaults
1083                                .show_completions_on_input
1084                        },
1085                    }),
1086                    metadata: None,
1087                }),
1088                SettingsPageItem::SettingItem(SettingItem {
1089                    title: "Show Completion Documentation",
1090                    description: "Whether to display inline and alongside documentation for items in the completions menu",
1091                    field: Box::new(SettingField {
1092                        pick: |settings_content| {
1093                            &settings_content
1094                                .project
1095                                .all_languages
1096                                .defaults
1097                                .show_completion_documentation
1098                        },
1099                        pick_mut: |settings_content| {
1100                            &mut settings_content
1101                                .project
1102                                .all_languages
1103                                .defaults
1104                                .show_completion_documentation
1105                        },
1106                    }),
1107                    metadata: None,
1108                }),
1109                SettingsPageItem::SettingItem(SettingItem {
1110                    title: "Auto Signature Help",
1111                    description: "Whether to automatically show a signature help pop-up or not",
1112                    field: Box::new(SettingField {
1113                        pick: |settings_content| &settings_content.editor.auto_signature_help,
1114                        pick_mut: |settings_content| {
1115                            &mut settings_content.editor.auto_signature_help
1116                        },
1117                    }),
1118                    metadata: None,
1119                }),
1120                SettingsPageItem::SettingItem(SettingItem {
1121                    title: "Show Signature Help After Edits",
1122                    description: "Whether to show the signature help pop-up after completions or bracket pairs inserted",
1123                    field: Box::new(SettingField {
1124                        pick: |settings_content| {
1125                            &settings_content.editor.show_signature_help_after_edits
1126                        },
1127                        pick_mut: |settings_content| {
1128                            &mut settings_content.editor.show_signature_help_after_edits
1129                        },
1130                    }),
1131                    metadata: None,
1132                }),
1133                SettingsPageItem::SettingItem(SettingItem {
1134                    title: "Snippet Sort Order",
1135                    description: "Determines how snippets are sorted relative to other completion items",
1136                    field: Box::new(SettingField {
1137                        pick: |settings_content| &settings_content.editor.snippet_sort_order,
1138                        pick_mut: |settings_content| {
1139                            &mut settings_content.editor.snippet_sort_order
1140                        },
1141                    }),
1142                    metadata: None,
1143                }),
1144                SettingsPageItem::SectionHeader("Hover"),
1145                SettingsPageItem::SettingItem(SettingItem {
1146                    title: "Hover Popover Enabled",
1147                    description: "Whether to show the informational hover box when moving the mouse over symbols in the editor",
1148                    field: Box::new(SettingField {
1149                        pick: |settings_content| &settings_content.editor.hover_popover_enabled,
1150                        pick_mut: |settings_content| {
1151                            &mut settings_content.editor.hover_popover_enabled
1152                        },
1153                    }),
1154                    metadata: None,
1155                }),
1156                // todo(settings ui): add units to this numeric stepper
1157                SettingsPageItem::SettingItem(SettingItem {
1158                    title: "Hover Popover Delay",
1159                    description: "Time to wait in milliseconds before showing the informational hover box",
1160                    field: Box::new(SettingField {
1161                        pick: |settings_content| &settings_content.editor.hover_popover_delay,
1162                        pick_mut: |settings_content| {
1163                            &mut settings_content.editor.hover_popover_delay
1164                        },
1165                    }),
1166                    metadata: None,
1167                }),
1168                SettingsPageItem::SectionHeader("Code Actions"),
1169                SettingsPageItem::SettingItem(SettingItem {
1170                    title: "Inline Code Actions",
1171                    description: "Whether to show code action button at start of buffer line",
1172                    field: Box::new(SettingField {
1173                        pick: |settings_content| &settings_content.editor.inline_code_actions,
1174                        pick_mut: |settings_content| {
1175                            &mut settings_content.editor.inline_code_actions
1176                        },
1177                    }),
1178                    metadata: None,
1179                }),
1180                SettingsPageItem::SectionHeader("Selection"),
1181                SettingsPageItem::SettingItem(SettingItem {
1182                    title: "Drag And Drop Selection",
1183                    description: "Whether to enable drag and drop selection",
1184                    field: Box::new(SettingField {
1185                        pick: |settings_content| {
1186                            if let Some(drag_and_drop) =
1187                                &settings_content.editor.drag_and_drop_selection
1188                            {
1189                                &drag_and_drop.enabled
1190                            } else {
1191                                &None
1192                            }
1193                        },
1194                        pick_mut: |settings_content| {
1195                            &mut settings_content
1196                                .editor
1197                                .drag_and_drop_selection
1198                                .get_or_insert_default()
1199                                .enabled
1200                        },
1201                    }),
1202                    metadata: None,
1203                }),
1204                // todo(settings_ui): Needs numeric stepper
1205                // SettingsPageItem::SettingItem(SettingItem {
1206                //     title: "Drag And Drop Selection Delay",
1207                //     description: "Delay in milliseconds before drag and drop selection starts",
1208                //     field: Box::new(SettingField {
1209                //         pick: |settings_content| {
1210                //             if let Some(drag_and_drop) = &settings_content.editor.drag_and_drop_selection {
1211                //                 &drag_and_drop.delay
1212                //             } else {
1213                //                 &None
1214                //             }
1215                //         },
1216                //         pick_mut: |settings_content| {
1217                //             &mut settings_content.editor.drag_and_drop_selection.get_or_insert_default().delay
1218                //         },
1219                //     }),
1220                //     metadata: None,
1221                // }),
1222                SettingsPageItem::SectionHeader("Line Numbers"),
1223                SettingsPageItem::SettingItem(SettingItem {
1224                    title: "Relative Line Numbers",
1225                    description: "Whether the line numbers on editors gutter are relative or not",
1226                    field: Box::new(SettingField {
1227                        pick: |settings_content| &settings_content.editor.relative_line_numbers,
1228                        pick_mut: |settings_content| {
1229                            &mut settings_content.editor.relative_line_numbers
1230                        },
1231                    }),
1232                    metadata: None,
1233                }),
1234                SettingsPageItem::SectionHeader("Gutter"),
1235                SettingsPageItem::SettingItem(SettingItem {
1236                    title: "Show Line Numbers",
1237                    description: "Whether to show line numbers in the gutter",
1238                    field: Box::new(SettingField {
1239                        pick: |settings_content| {
1240                            if let Some(gutter) = &settings_content.editor.gutter {
1241                                &gutter.line_numbers
1242                            } else {
1243                                &None
1244                            }
1245                        },
1246                        pick_mut: |settings_content| {
1247                            &mut settings_content
1248                                .editor
1249                                .gutter
1250                                .get_or_insert_default()
1251                                .line_numbers
1252                        },
1253                    }),
1254                    metadata: None,
1255                }),
1256                SettingsPageItem::SettingItem(SettingItem {
1257                    title: "Show Runnables",
1258                    description: "Whether to show runnable buttons in the gutter",
1259                    field: Box::new(SettingField {
1260                        pick: |settings_content| {
1261                            if let Some(gutter) = &settings_content.editor.gutter {
1262                                &gutter.runnables
1263                            } else {
1264                                &None
1265                            }
1266                        },
1267                        pick_mut: |settings_content| {
1268                            &mut settings_content
1269                                .editor
1270                                .gutter
1271                                .get_or_insert_default()
1272                                .runnables
1273                        },
1274                    }),
1275                    metadata: None,
1276                }),
1277                SettingsPageItem::SettingItem(SettingItem {
1278                    title: "Show Breakpoints",
1279                    description: "Whether to show breakpoints in the gutter",
1280                    field: Box::new(SettingField {
1281                        pick: |settings_content| {
1282                            if let Some(gutter) = &settings_content.editor.gutter {
1283                                &gutter.breakpoints
1284                            } else {
1285                                &None
1286                            }
1287                        },
1288                        pick_mut: |settings_content| {
1289                            &mut settings_content
1290                                .editor
1291                                .gutter
1292                                .get_or_insert_default()
1293                                .breakpoints
1294                        },
1295                    }),
1296                    metadata: None,
1297                }),
1298                SettingsPageItem::SettingItem(SettingItem {
1299                    title: "Show Folds",
1300                    description: "Whether to show code folding controls in the gutter",
1301                    field: Box::new(SettingField {
1302                        pick: |settings_content| {
1303                            if let Some(gutter) = &settings_content.editor.gutter {
1304                                &gutter.folds
1305                            } else {
1306                                &None
1307                            }
1308                        },
1309                        pick_mut: |settings_content| {
1310                            &mut settings_content.editor.gutter.get_or_insert_default().folds
1311                        },
1312                    }),
1313                    metadata: None,
1314                }),
1315                SettingsPageItem::SectionHeader("Tabs"),
1316                SettingsPageItem::SettingItem(SettingItem {
1317                    title: "Show Tab Bar",
1318                    description: "Whether or not to show the tab bar in the editor",
1319                    field: Box::new(SettingField {
1320                        pick: |settings_content| {
1321                            if let Some(tab_bar) = &settings_content.tab_bar {
1322                                &tab_bar.show
1323                            } else {
1324                                &None
1325                            }
1326                        },
1327                        pick_mut: |settings_content| {
1328                            &mut settings_content.tab_bar.get_or_insert_default().show
1329                        },
1330                    }),
1331                    metadata: None,
1332                }),
1333                SettingsPageItem::SettingItem(SettingItem {
1334                    title: "Show Git Status In Tabs",
1335                    description: "Whether to show the Git file status on a tab item",
1336                    field: Box::new(SettingField {
1337                        pick: |settings_content| {
1338                            if let Some(tabs) = &settings_content.tabs {
1339                                &tabs.git_status
1340                            } else {
1341                                &None
1342                            }
1343                        },
1344                        pick_mut: |settings_content| {
1345                            &mut settings_content.tabs.get_or_insert_default().git_status
1346                        },
1347                    }),
1348                    metadata: None,
1349                }),
1350                SettingsPageItem::SettingItem(SettingItem {
1351                    title: "Show File Icons In Tabs",
1352                    description: "Whether to show the file icon for a tab",
1353                    field: Box::new(SettingField {
1354                        pick: |settings_content| {
1355                            if let Some(tabs) = &settings_content.tabs {
1356                                &tabs.file_icons
1357                            } else {
1358                                &None
1359                            }
1360                        },
1361                        pick_mut: |settings_content| {
1362                            &mut settings_content.tabs.get_or_insert_default().file_icons
1363                        },
1364                    }),
1365                    metadata: None,
1366                }),
1367                SettingsPageItem::SettingItem(SettingItem {
1368                    title: "Tab Close Position",
1369                    description: "Position of the close button in a tab",
1370                    field: Box::new(SettingField {
1371                        pick: |settings_content| {
1372                            if let Some(tabs) = &settings_content.tabs {
1373                                &tabs.close_position
1374                            } else {
1375                                &None
1376                            }
1377                        },
1378                        pick_mut: |settings_content| {
1379                            &mut settings_content.tabs.get_or_insert_default().close_position
1380                        },
1381                    }),
1382                    metadata: None,
1383                }),
1384                // todo(settings_ui): Needs numeric stepper
1385                // SettingsPageItem::SettingItem(SettingItem {
1386                //     title: "Maximum Tabs",
1387                //     description: "Maximum open tabs in a pane. Will not close an unsaved tab",
1388                //     field: Box::new(SettingField {
1389                //         pick: |settings_content| &settings_content.workspace.max_tabs,
1390                //         pick_mut: |settings_content| &mut settings_content.workspace.max_tabs,
1391                //     }),
1392                //     metadata: None,
1393                // }),
1394            ],
1395        },
1396        SettingsPage {
1397            title: "Languages & Frameworks",
1398            items: vec![
1399                SettingsPageItem::SectionHeader("General"),
1400                SettingsPageItem::SettingItem(SettingItem {
1401                    title: "Enable Language Server",
1402                    description: "Whether to use language servers to provide code intelligence",
1403                    field: Box::new(SettingField {
1404                        pick: |settings_content| {
1405                            &settings_content
1406                                .project
1407                                .all_languages
1408                                .defaults
1409                                .enable_language_server
1410                        },
1411                        pick_mut: |settings_content| {
1412                            &mut settings_content
1413                                .project
1414                                .all_languages
1415                                .defaults
1416                                .enable_language_server
1417                        },
1418                    }),
1419                    metadata: None,
1420                }),
1421                SettingsPageItem::SectionHeader("Languages"),
1422                SettingsPageItem::SubPageLink(SubPageLink {
1423                    title: "JSON",
1424                    render: Rc::new(|_, _, _| "A settings page!".into_any_element()),
1425                }),
1426            ],
1427        },
1428        SettingsPage {
1429            title: "Workbench & Window",
1430            items: vec![
1431                SettingsPageItem::SectionHeader("Workbench"),
1432                SettingsPageItem::SettingItem(SettingItem {
1433                    title: "Editor Tabs",
1434                    description: "Whether or not to show the tab bar in the editor",
1435                    field: Box::new(SettingField {
1436                        pick: |settings_content| {
1437                            if let Some(tab_bar) = &settings_content.tab_bar {
1438                                &tab_bar.show
1439                            } else {
1440                                &None
1441                            }
1442                        },
1443                        pick_mut: |settings_content| {
1444                            &mut settings_content.tab_bar.get_or_insert_default().show
1445                        },
1446                    }),
1447                    metadata: None,
1448                }),
1449                SettingsPageItem::SettingItem(SettingItem {
1450                    title: "Active language Button",
1451                    description: "Whether to show the active language button in the status bar",
1452                    field: Box::new(SettingField {
1453                        pick: |settings_content| {
1454                            if let Some(status_bar) = &settings_content.status_bar {
1455                                &status_bar.active_language_button
1456                            } else {
1457                                &None
1458                            }
1459                        },
1460                        pick_mut: |settings_content| {
1461                            &mut settings_content
1462                                .status_bar
1463                                .get_or_insert_default()
1464                                .active_language_button
1465                        },
1466                    }),
1467                    metadata: None,
1468                }),
1469                SettingsPageItem::SettingItem(SettingItem {
1470                    title: "Cursor Position Button",
1471                    description: "Whether to show the cursor position button in the status bar",
1472                    field: Box::new(SettingField {
1473                        pick: |settings_content| {
1474                            if let Some(status_bar) = &settings_content.status_bar {
1475                                &status_bar.cursor_position_button
1476                            } else {
1477                                &None
1478                            }
1479                        },
1480                        pick_mut: |settings_content| {
1481                            &mut settings_content
1482                                .status_bar
1483                                .get_or_insert_default()
1484                                .cursor_position_button
1485                        },
1486                    }),
1487                    metadata: None,
1488                }),
1489                SettingsPageItem::SectionHeader("Terminal"),
1490                SettingsPageItem::SettingItem(SettingItem {
1491                    title: "Terminal Button",
1492                    description: "Whether to show the terminal button in the status bar",
1493                    field: Box::new(SettingField {
1494                        pick: |settings_content| {
1495                            if let Some(terminal) = &settings_content.terminal {
1496                                &terminal.button
1497                            } else {
1498                                &None
1499                            }
1500                        },
1501                        pick_mut: |settings_content| {
1502                            &mut settings_content.terminal.get_or_insert_default().button
1503                        },
1504                    }),
1505                    metadata: None,
1506                }),
1507                SettingsPageItem::SettingItem(SettingItem {
1508                    title: "Show Navigation History Buttons",
1509                    description: "Whether or not to show the navigation history buttons in the tab bar",
1510                    field: Box::new(SettingField {
1511                        pick: |settings_content| {
1512                            if let Some(tab_bar) = &settings_content.tab_bar {
1513                                &tab_bar.show_nav_history_buttons
1514                            } else {
1515                                &None
1516                            }
1517                        },
1518                        pick_mut: |settings_content| {
1519                            &mut settings_content
1520                                .tab_bar
1521                                .get_or_insert_default()
1522                                .show_nav_history_buttons
1523                        },
1524                    }),
1525                    metadata: None,
1526                }),
1527            ],
1528        },
1529        SettingsPage {
1530            title: "Panels & Tools",
1531            items: vec![
1532                SettingsPageItem::SectionHeader("Project Panel"),
1533                SettingsPageItem::SettingItem(SettingItem {
1534                    title: "Project Panel Button",
1535                    description: "Whether to show the project panel button in the status bar",
1536                    field: Box::new(SettingField {
1537                        pick: |settings_content| {
1538                            if let Some(project_panel) = &settings_content.project_panel {
1539                                &project_panel.button
1540                            } else {
1541                                &None
1542                            }
1543                        },
1544                        pick_mut: |settings_content| {
1545                            &mut settings_content
1546                                .project_panel
1547                                .get_or_insert_default()
1548                                .button
1549                        },
1550                    }),
1551                    metadata: None,
1552                }),
1553                SettingsPageItem::SettingItem(SettingItem {
1554                    title: "Project Panel Dock",
1555                    description: "Where to dock the project panel",
1556                    field: Box::new(SettingField {
1557                        pick: |settings_content| {
1558                            if let Some(project_panel) = &settings_content.project_panel {
1559                                &project_panel.dock
1560                            } else {
1561                                &None
1562                            }
1563                        },
1564                        pick_mut: |settings_content| {
1565                            &mut settings_content.project_panel.get_or_insert_default().dock
1566                        },
1567                    }),
1568                    metadata: None,
1569                }),
1570                // todo(settings_ui): Needs numeric stepper
1571                // SettingsPageItem::SettingItem(SettingItem {
1572                //     title: "Project Panel Default Width",
1573                //     description: "Default width of the project panel in pixels",
1574                //     field: Box::new(SettingField {
1575                //         pick: |settings_content| {
1576                //             if let Some(project_panel) = &settings_content.project_panel {
1577                //                 &project_panel.default_width
1578                //             } else {
1579                //                 &None
1580                //             }
1581                //         },
1582                //         pick_mut: |settings_content| {
1583                //             &mut settings_content
1584                //                 .project_panel
1585                //                 .get_or_insert_default()
1586                //                 .default_width
1587                //         },
1588                //     }),
1589                //     metadata: None,
1590                // }),
1591                SettingsPageItem::SectionHeader("Terminal"),
1592                SettingsPageItem::SettingItem(SettingItem {
1593                    title: "Terminal Dock",
1594                    description: "Where to dock the terminal panel",
1595                    field: Box::new(SettingField {
1596                        pick: |settings_content| {
1597                            if let Some(terminal) = &settings_content.terminal {
1598                                &terminal.dock
1599                            } else {
1600                                &None
1601                            }
1602                        },
1603                        pick_mut: |settings_content| {
1604                            &mut settings_content.terminal.get_or_insert_default().dock
1605                        },
1606                    }),
1607                    metadata: None,
1608                }),
1609                SettingsPageItem::SectionHeader("Tab Settings"),
1610                SettingsPageItem::SettingItem(SettingItem {
1611                    title: "Activate On Close",
1612                    description: "What to do after closing the current tab",
1613                    field: Box::new(SettingField {
1614                        pick: |settings_content| {
1615                            if let Some(tabs) = &settings_content.tabs {
1616                                &tabs.activate_on_close
1617                            } else {
1618                                &None
1619                            }
1620                        },
1621                        pick_mut: |settings_content| {
1622                            &mut settings_content
1623                                .tabs
1624                                .get_or_insert_default()
1625                                .activate_on_close
1626                        },
1627                    }),
1628                    metadata: None,
1629                }),
1630                SettingsPageItem::SettingItem(SettingItem {
1631                    title: "Tab Show Diagnostics",
1632                    description: "Which files containing diagnostic errors/warnings to mark in the tabs",
1633                    field: Box::new(SettingField {
1634                        pick: |settings_content| {
1635                            if let Some(tabs) = &settings_content.tabs {
1636                                &tabs.show_diagnostics
1637                            } else {
1638                                &None
1639                            }
1640                        },
1641                        pick_mut: |settings_content| {
1642                            &mut settings_content
1643                                .tabs
1644                                .get_or_insert_default()
1645                                .show_diagnostics
1646                        },
1647                    }),
1648                    metadata: None,
1649                }),
1650                SettingsPageItem::SettingItem(SettingItem {
1651                    title: "Show Close Button",
1652                    description: "Controls the appearance behavior of the tab's close button",
1653                    field: Box::new(SettingField {
1654                        pick: |settings_content| {
1655                            if let Some(tabs) = &settings_content.tabs {
1656                                &tabs.show_close_button
1657                            } else {
1658                                &None
1659                            }
1660                        },
1661                        pick_mut: |settings_content| {
1662                            &mut settings_content
1663                                .tabs
1664                                .get_or_insert_default()
1665                                .show_close_button
1666                        },
1667                    }),
1668                    metadata: None,
1669                }),
1670                SettingsPageItem::SectionHeader("Preview Tabs"),
1671                SettingsPageItem::SettingItem(SettingItem {
1672                    title: "Preview Tabs Enabled",
1673                    description: "Whether to show opened editors as preview tabs",
1674                    field: Box::new(SettingField {
1675                        pick: |settings_content| {
1676                            if let Some(preview_tabs) = &settings_content.preview_tabs {
1677                                &preview_tabs.enabled
1678                            } else {
1679                                &None
1680                            }
1681                        },
1682                        pick_mut: |settings_content| {
1683                            &mut settings_content
1684                                .preview_tabs
1685                                .get_or_insert_default()
1686                                .enabled
1687                        },
1688                    }),
1689                    metadata: None,
1690                }),
1691                SettingsPageItem::SettingItem(SettingItem {
1692                    title: "Enable Preview From File Finder",
1693                    description: "Whether to open tabs in preview mode when selected from the file finder",
1694                    field: Box::new(SettingField {
1695                        pick: |settings_content| {
1696                            if let Some(preview_tabs) = &settings_content.preview_tabs {
1697                                &preview_tabs.enable_preview_from_file_finder
1698                            } else {
1699                                &None
1700                            }
1701                        },
1702                        pick_mut: |settings_content| {
1703                            &mut settings_content
1704                                .preview_tabs
1705                                .get_or_insert_default()
1706                                .enable_preview_from_file_finder
1707                        },
1708                    }),
1709                    metadata: None,
1710                }),
1711                SettingsPageItem::SettingItem(SettingItem {
1712                    title: "Enable Preview From Code Navigation",
1713                    description: "Whether a preview tab gets replaced when code navigation is used to navigate away from the tab",
1714                    field: Box::new(SettingField {
1715                        pick: |settings_content| {
1716                            if let Some(preview_tabs) = &settings_content.preview_tabs {
1717                                &preview_tabs.enable_preview_from_code_navigation
1718                            } else {
1719                                &None
1720                            }
1721                        },
1722                        pick_mut: |settings_content| {
1723                            &mut settings_content
1724                                .preview_tabs
1725                                .get_or_insert_default()
1726                                .enable_preview_from_code_navigation
1727                        },
1728                    }),
1729                    metadata: None,
1730                }),
1731            ],
1732        },
1733        SettingsPage {
1734            title: "Version Control",
1735            items: vec![
1736                SettingsPageItem::SectionHeader("Git"),
1737                SettingsPageItem::SettingItem(SettingItem {
1738                    title: "Git Gutter",
1739                    description: "Control whether the git gutter is shown",
1740                    field: Box::new(SettingField {
1741                        pick: |settings_content| {
1742                            if let Some(git) = &settings_content.git {
1743                                &git.git_gutter
1744                            } else {
1745                                &None
1746                            }
1747                        },
1748                        pick_mut: |settings_content| {
1749                            &mut settings_content.git.get_or_insert_default().git_gutter
1750                        },
1751                    }),
1752                    metadata: None,
1753                }),
1754                // todo(settings_ui): Figure out the right default for this value in default.json
1755                // SettingsPageItem::SettingItem(SettingItem {
1756                //     title: "Gutter Debounce",
1757                //     description: "Debounce threshold in milliseconds after which changes are reflected in the git gutter",
1758                //     field: Box::new(SettingField {
1759                //         pick: |settings_content| {
1760                //             if let Some(git) = &settings_content.git {
1761                //                 &git.gutter_debounce
1762                //             } else {
1763                //                 &None
1764                //             }
1765                //         },
1766                //         pick_mut: |settings_content| {
1767                //             &mut settings_content.git.get_or_insert_default().gutter_debounce
1768                //         },
1769                //     }),
1770                //     metadata: None,
1771                // }),
1772                SettingsPageItem::SettingItem(SettingItem {
1773                    title: "Inline Blame Enabled",
1774                    description: "Whether or not to show git blame data inline in the currently focused line",
1775                    field: Box::new(SettingField {
1776                        pick: |settings_content| {
1777                            if let Some(git) = &settings_content.git {
1778                                if let Some(inline_blame) = &git.inline_blame {
1779                                    &inline_blame.enabled
1780                                } else {
1781                                    &None
1782                                }
1783                            } else {
1784                                &None
1785                            }
1786                        },
1787                        pick_mut: |settings_content| {
1788                            &mut settings_content
1789                                .git
1790                                .get_or_insert_default()
1791                                .inline_blame
1792                                .get_or_insert_default()
1793                                .enabled
1794                        },
1795                    }),
1796                    metadata: None,
1797                }),
1798                SettingsPageItem::SettingItem(SettingItem {
1799                    title: "Inline Blame Delay",
1800                    description: "The delay after which the inline blame information is shown",
1801                    field: Box::new(SettingField {
1802                        pick: |settings_content| {
1803                            if let Some(git) = &settings_content.git {
1804                                if let Some(inline_blame) = &git.inline_blame {
1805                                    &inline_blame.delay_ms
1806                                } else {
1807                                    &None
1808                                }
1809                            } else {
1810                                &None
1811                            }
1812                        },
1813                        pick_mut: |settings_content| {
1814                            &mut settings_content
1815                                .git
1816                                .get_or_insert_default()
1817                                .inline_blame
1818                                .get_or_insert_default()
1819                                .delay_ms
1820                        },
1821                    }),
1822                    metadata: None,
1823                }),
1824                SettingsPageItem::SettingItem(SettingItem {
1825                    title: "Inline Blame Padding",
1826                    description: "Padding between the end of the source line and the start of the inline blame in columns",
1827                    field: Box::new(SettingField {
1828                        pick: |settings_content| {
1829                            if let Some(git) = &settings_content.git {
1830                                if let Some(inline_blame) = &git.inline_blame {
1831                                    &inline_blame.padding
1832                                } else {
1833                                    &None
1834                                }
1835                            } else {
1836                                &None
1837                            }
1838                        },
1839                        pick_mut: |settings_content| {
1840                            &mut settings_content
1841                                .git
1842                                .get_or_insert_default()
1843                                .inline_blame
1844                                .get_or_insert_default()
1845                                .padding
1846                        },
1847                    }),
1848                    metadata: None,
1849                }),
1850                SettingsPageItem::SettingItem(SettingItem {
1851                    title: "Inline Blame Min Column",
1852                    description: "The minimum column number to show the inline blame information at",
1853                    field: Box::new(SettingField {
1854                        pick: |settings_content| {
1855                            if let Some(git) = &settings_content.git {
1856                                if let Some(inline_blame) = &git.inline_blame {
1857                                    &inline_blame.min_column
1858                                } else {
1859                                    &None
1860                                }
1861                            } else {
1862                                &None
1863                            }
1864                        },
1865                        pick_mut: |settings_content| {
1866                            &mut settings_content
1867                                .git
1868                                .get_or_insert_default()
1869                                .inline_blame
1870                                .get_or_insert_default()
1871                                .min_column
1872                        },
1873                    }),
1874                    metadata: None,
1875                }),
1876                SettingsPageItem::SettingItem(SettingItem {
1877                    title: "Show Commit Summary",
1878                    description: "Whether to show commit summary as part of the inline blame",
1879                    field: Box::new(SettingField {
1880                        pick: |settings_content| {
1881                            if let Some(git) = &settings_content.git {
1882                                if let Some(inline_blame) = &git.inline_blame {
1883                                    &inline_blame.show_commit_summary
1884                                } else {
1885                                    &None
1886                                }
1887                            } else {
1888                                &None
1889                            }
1890                        },
1891                        pick_mut: |settings_content| {
1892                            &mut settings_content
1893                                .git
1894                                .get_or_insert_default()
1895                                .inline_blame
1896                                .get_or_insert_default()
1897                                .show_commit_summary
1898                        },
1899                    }),
1900                    metadata: None,
1901                }),
1902                SettingsPageItem::SettingItem(SettingItem {
1903                    title: "Show Avatar",
1904                    description: "Whether to show the avatar of the author of the commit",
1905                    field: Box::new(SettingField {
1906                        pick: |settings_content| {
1907                            if let Some(git) = &settings_content.git {
1908                                if let Some(blame) = &git.blame {
1909                                    &blame.show_avatar
1910                                } else {
1911                                    &None
1912                                }
1913                            } else {
1914                                &None
1915                            }
1916                        },
1917                        pick_mut: |settings_content| {
1918                            &mut settings_content
1919                                .git
1920                                .get_or_insert_default()
1921                                .blame
1922                                .get_or_insert_default()
1923                                .show_avatar
1924                        },
1925                    }),
1926                    metadata: None,
1927                }),
1928                SettingsPageItem::SettingItem(SettingItem {
1929                    title: "Show Author Name In Branch Picker",
1930                    description: "Whether to show author name as part of the commit information in branch picker",
1931                    field: Box::new(SettingField {
1932                        pick: |settings_content| {
1933                            if let Some(git) = &settings_content.git {
1934                                if let Some(branch_picker) = &git.branch_picker {
1935                                    &branch_picker.show_author_name
1936                                } else {
1937                                    &None
1938                                }
1939                            } else {
1940                                &None
1941                            }
1942                        },
1943                        pick_mut: |settings_content| {
1944                            &mut settings_content
1945                                .git
1946                                .get_or_insert_default()
1947                                .branch_picker
1948                                .get_or_insert_default()
1949                                .show_author_name
1950                        },
1951                    }),
1952                    metadata: None,
1953                }),
1954                SettingsPageItem::SettingItem(SettingItem {
1955                    title: "Hunk Style",
1956                    description: "How git hunks are displayed visually in the editor",
1957                    field: Box::new(SettingField {
1958                        pick: |settings_content| {
1959                            if let Some(git) = &settings_content.git {
1960                                &git.hunk_style
1961                            } else {
1962                                &None
1963                            }
1964                        },
1965                        pick_mut: |settings_content| {
1966                            &mut settings_content.git.get_or_insert_default().hunk_style
1967                        },
1968                    }),
1969                    metadata: None,
1970                }),
1971            ],
1972        },
1973        SettingsPage {
1974            title: "System & Network",
1975            items: vec![
1976                SettingsPageItem::SectionHeader("Network"),
1977                // todo(settings_ui): Proxy needs a default
1978                // SettingsPageItem::SettingItem(SettingItem {
1979                //     title: "Proxy",
1980                //     description: "The proxy to use for network requests",
1981                //     field: Box::new(SettingField {
1982                //         pick: |settings_content| &settings_content.proxy,
1983                //         pick_mut: |settings_content| &mut settings_content.proxy,
1984                //     }),
1985                //     metadata: Some(Box::new(SettingsFieldMetadata {
1986                //         placeholder: Some("socks5h://localhost:10808"),
1987                //     })),
1988                // }),
1989                SettingsPageItem::SettingItem(SettingItem {
1990                    title: "Server URL",
1991                    description: "The URL of the Zed server to connect to",
1992                    field: Box::new(SettingField {
1993                        pick: |settings_content| &settings_content.server_url,
1994                        pick_mut: |settings_content| &mut settings_content.server_url,
1995                    }),
1996                    metadata: Some(Box::new(SettingsFieldMetadata {
1997                        placeholder: Some("https://zed.dev"),
1998                    })),
1999                }),
2000                SettingsPageItem::SectionHeader("System"),
2001                SettingsPageItem::SettingItem(SettingItem {
2002                    title: "Auto Update",
2003                    description: "Whether or not to automatically check for updates",
2004                    field: Box::new(SettingField {
2005                        pick: |settings_content| &settings_content.auto_update,
2006                        pick_mut: |settings_content| &mut settings_content.auto_update,
2007                    }),
2008                    metadata: None,
2009                }),
2010            ],
2011        },
2012        SettingsPage {
2013            title: "Diagnostics & Errors",
2014            items: vec![
2015                SettingsPageItem::SectionHeader("Display"),
2016                SettingsPageItem::SettingItem(SettingItem {
2017                    title: "Diagnostics Button",
2018                    description: "Whether to show the project diagnostics button in the status bar",
2019                    field: Box::new(SettingField {
2020                        pick: |settings_content| {
2021                            if let Some(diagnostics) = &settings_content.diagnostics {
2022                                &diagnostics.button
2023                            } else {
2024                                &None
2025                            }
2026                        },
2027                        pick_mut: |settings_content| {
2028                            &mut settings_content.diagnostics.get_or_insert_default().button
2029                        },
2030                    }),
2031                    metadata: None,
2032                }),
2033                SettingsPageItem::SectionHeader("Filtering"),
2034                SettingsPageItem::SettingItem(SettingItem {
2035                    title: "Max Severity",
2036                    description: "Which level to use to filter out diagnostics displayed in the editor",
2037                    field: Box::new(SettingField {
2038                        pick: |settings_content| &settings_content.editor.diagnostics_max_severity,
2039                        pick_mut: |settings_content| {
2040                            &mut settings_content.editor.diagnostics_max_severity
2041                        },
2042                    }),
2043                    metadata: None,
2044                }),
2045                SettingsPageItem::SettingItem(SettingItem {
2046                    title: "Include Warnings",
2047                    description: "Whether to show warnings or not by default",
2048                    field: Box::new(SettingField {
2049                        pick: |settings_content| {
2050                            if let Some(diagnostics) = &settings_content.diagnostics {
2051                                &diagnostics.include_warnings
2052                            } else {
2053                                &None
2054                            }
2055                        },
2056                        pick_mut: |settings_content| {
2057                            &mut settings_content
2058                                .diagnostics
2059                                .get_or_insert_default()
2060                                .include_warnings
2061                        },
2062                    }),
2063                    metadata: None,
2064                }),
2065                SettingsPageItem::SectionHeader("Inline"),
2066                SettingsPageItem::SettingItem(SettingItem {
2067                    title: "Inline Diagnostics Enabled",
2068                    description: "Whether to show diagnostics inline or not",
2069                    field: Box::new(SettingField {
2070                        pick: |settings_content| {
2071                            if let Some(diagnostics) = &settings_content.diagnostics {
2072                                if let Some(inline) = &diagnostics.inline {
2073                                    &inline.enabled
2074                                } else {
2075                                    &None
2076                                }
2077                            } else {
2078                                &None
2079                            }
2080                        },
2081                        pick_mut: |settings_content| {
2082                            &mut settings_content
2083                                .diagnostics
2084                                .get_or_insert_default()
2085                                .inline
2086                                .get_or_insert_default()
2087                                .enabled
2088                        },
2089                    }),
2090                    metadata: None,
2091                }),
2092                // todo(settings_ui): Needs numeric stepper
2093                // SettingsPageItem::SettingItem(SettingItem {
2094                //     title: "Inline Update Debounce",
2095                //     description: "The delay in milliseconds to show inline diagnostics after the last diagnostic update",
2096                //     field: Box::new(SettingField {
2097                //         pick: |settings_content| {
2098                //             if let Some(diagnostics) = &settings_content.diagnostics {
2099                //                 if let Some(inline) = &diagnostics.inline {
2100                //                     &inline.update_debounce_ms
2101                //                 } else {
2102                //                     &None
2103                //                 }
2104                //             } else {
2105                //                 &None
2106                //             }
2107                //         },
2108                //         pick_mut: |settings_content| {
2109                //             &mut settings_content
2110                //                 .diagnostics
2111                //                 .get_or_insert_default()
2112                //                 .inline
2113                //                 .get_or_insert_default()
2114                //                 .update_debounce_ms
2115                //         },
2116                //     }),
2117                //     metadata: None,
2118                // }),
2119                // todo(settings_ui): Needs numeric stepper
2120                // SettingsPageItem::SettingItem(SettingItem {
2121                //     title: "Inline Padding",
2122                //     description: "The amount of padding between the end of the source line and the start of the inline diagnostic",
2123                //     field: Box::new(SettingField {
2124                //         pick: |settings_content| {
2125                //             if let Some(diagnostics) = &settings_content.diagnostics {
2126                //                 if let Some(inline) = &diagnostics.inline {
2127                //                     &inline.padding
2128                //                 } else {
2129                //                     &None
2130                //                 }
2131                //             } else {
2132                //                 &None
2133                //             }
2134                //         },
2135                //         pick_mut: |settings_content| {
2136                //             &mut settings_content
2137                //                 .diagnostics
2138                //                 .get_or_insert_default()
2139                //                 .inline
2140                //                 .get_or_insert_default()
2141                //                 .padding
2142                //         },
2143                //     }),
2144                //     metadata: None,
2145                // }),
2146                // todo(settings_ui): Needs numeric stepper
2147                // SettingsPageItem::SettingItem(SettingItem {
2148                //     title: "Inline Min Column",
2149                //     description: "The minimum column to display inline diagnostics",
2150                //     field: Box::new(SettingField {
2151                //         pick: |settings_content| {
2152                //             if let Some(diagnostics) = &settings_content.diagnostics {
2153                //                 if let Some(inline) = &diagnostics.inline {
2154                //                     &inline.min_column
2155                //                 } else {
2156                //                     &None
2157                //                 }
2158                //             } else {
2159                //                 &None
2160                //             }
2161                //         },
2162                //         pick_mut: |settings_content| {
2163                //             &mut settings_content
2164                //                 .diagnostics
2165                //                 .get_or_insert_default()
2166                //                 .inline
2167                //                 .get_or_insert_default()
2168                //                 .min_column
2169                //         },
2170                //     }),
2171                //     metadata: None,
2172                // }),
2173                SettingsPageItem::SectionHeader("Performance"),
2174                SettingsPageItem::SettingItem(SettingItem {
2175                    title: "LSP Pull Diagnostics Enabled",
2176                    description: "Whether to pull for diagnostics or not",
2177                    field: Box::new(SettingField {
2178                        pick: |settings_content| {
2179                            if let Some(diagnostics) = &settings_content.diagnostics {
2180                                if let Some(lsp_pull) = &diagnostics.lsp_pull_diagnostics {
2181                                    &lsp_pull.enabled
2182                                } else {
2183                                    &None
2184                                }
2185                            } else {
2186                                &None
2187                            }
2188                        },
2189                        pick_mut: |settings_content| {
2190                            &mut settings_content
2191                                .diagnostics
2192                                .get_or_insert_default()
2193                                .lsp_pull_diagnostics
2194                                .get_or_insert_default()
2195                                .enabled
2196                        },
2197                    }),
2198                    metadata: None,
2199                }),
2200                // todo(settings_ui): Needs numeric stepper
2201                // SettingsPageItem::SettingItem(SettingItem {
2202                //     title: "LSP Pull Debounce",
2203                //     description: "Minimum time to wait before pulling diagnostics from the language server(s)",
2204                //     field: Box::new(SettingField {
2205                //         pick: |settings_content| {
2206                //             if let Some(diagnostics) = &settings_content.diagnostics {
2207                //                 if let Some(lsp_pull) = &diagnostics.lsp_pull_diagnostics {
2208                //                     &lsp_pull.debounce_ms
2209                //                 } else {
2210                //                     &None
2211                //                 }
2212                //             } else {
2213                //                 &None
2214                //             }
2215                //         },
2216                //         pick_mut: |settings_content| {
2217                //             &mut settings_content
2218                //                 .diagnostics
2219                //                 .get_or_insert_default()
2220                //                 .lsp_pull_diagnostics
2221                //                 .get_or_insert_default()
2222                //                 .debounce_ms
2223                //         },
2224                //     }),
2225                //     metadata: None,
2226                // }),
2227            ],
2228        },
2229        SettingsPage {
2230            title: "Collaboration",
2231            items: vec![
2232                SettingsPageItem::SectionHeader("Calls"),
2233                SettingsPageItem::SettingItem(SettingItem {
2234                    title: "Mute On Join",
2235                    description: "Whether the microphone should be muted when joining a channel or a call",
2236                    field: Box::new(SettingField {
2237                        pick: |settings_content| {
2238                            if let Some(calls) = &settings_content.calls {
2239                                &calls.mute_on_join
2240                            } else {
2241                                &None
2242                            }
2243                        },
2244                        pick_mut: |settings_content| {
2245                            &mut settings_content.calls.get_or_insert_default().mute_on_join
2246                        },
2247                    }),
2248                    metadata: None,
2249                }),
2250                SettingsPageItem::SettingItem(SettingItem {
2251                    title: "Share On Join",
2252                    description: "Whether your current project should be shared when joining an empty channel",
2253                    field: Box::new(SettingField {
2254                        pick: |settings_content| {
2255                            if let Some(calls) = &settings_content.calls {
2256                                &calls.share_on_join
2257                            } else {
2258                                &None
2259                            }
2260                        },
2261                        pick_mut: |settings_content| {
2262                            &mut settings_content.calls.get_or_insert_default().share_on_join
2263                        },
2264                    }),
2265                    metadata: None,
2266                }),
2267                SettingsPageItem::SectionHeader("Panel"),
2268                SettingsPageItem::SettingItem(SettingItem {
2269                    title: "Collaboration Panel Button",
2270                    description: "Whether to show the collaboration panel button in the status bar",
2271                    field: Box::new(SettingField {
2272                        pick: |settings_content| {
2273                            if let Some(collab) = &settings_content.collaboration_panel {
2274                                &collab.button
2275                            } else {
2276                                &None
2277                            }
2278                        },
2279                        pick_mut: |settings_content| {
2280                            &mut settings_content
2281                                .collaboration_panel
2282                                .get_or_insert_default()
2283                                .button
2284                        },
2285                    }),
2286                    metadata: None,
2287                }),
2288                SettingsPageItem::SectionHeader("Experimental"),
2289                SettingsPageItem::SettingItem(SettingItem {
2290                    title: "Rodio Audio",
2291                    description: "Opt into the new audio system",
2292                    field: Box::new(SettingField {
2293                        pick: |settings_content| {
2294                            if let Some(audio) = &settings_content.audio {
2295                                &audio.rodio_audio
2296                            } else {
2297                                &None
2298                            }
2299                        },
2300                        pick_mut: |settings_content| {
2301                            &mut settings_content.audio.get_or_insert_default().rodio_audio
2302                        },
2303                    }),
2304                    metadata: None,
2305                }),
2306            ],
2307        },
2308        SettingsPage {
2309            title: "AI",
2310            items: vec![
2311                SettingsPageItem::SectionHeader("General"),
2312                SettingsPageItem::SettingItem(SettingItem {
2313                    title: "Disable AI",
2314                    description: "Whether to disable all AI features in Zed",
2315                    field: Box::new(SettingField {
2316                        pick: |settings_content| &settings_content.disable_ai,
2317                        pick_mut: |settings_content| &mut settings_content.disable_ai,
2318                    }),
2319                    metadata: None,
2320                }),
2321            ],
2322        },
2323    ]
2324}
2325
2326// Derive Macro, on the new ProjectSettings struct
2327
2328fn project_settings_data() -> Vec<SettingsPage> {
2329    vec![
2330        SettingsPage {
2331            title: "Project",
2332            items: vec![
2333                SettingsPageItem::SectionHeader("Worktree Settings Content"),
2334                SettingsPageItem::SettingItem(SettingItem {
2335                    title: "Project Name",
2336                    description: "The displayed name of this project. If not set, the root directory name",
2337                    field: Box::new(SettingField {
2338                        pick: |settings_content| &settings_content.project.worktree.project_name,
2339                        pick_mut: |settings_content| {
2340                            &mut settings_content.project.worktree.project_name
2341                        },
2342                    }),
2343                    metadata: Some(Box::new(SettingsFieldMetadata {
2344                        placeholder: Some("A new name"),
2345                    })),
2346                }),
2347            ],
2348        },
2349        SettingsPage {
2350            title: "Appearance & Behavior",
2351            items: vec![
2352                SettingsPageItem::SectionHeader("Guides"),
2353                SettingsPageItem::SettingItem(SettingItem {
2354                    title: "Show Wrap Guides",
2355                    description: "Whether to show wrap guides (vertical rulers)",
2356                    field: Box::new(SettingField {
2357                        pick: |settings_content| {
2358                            &settings_content
2359                                .project
2360                                .all_languages
2361                                .defaults
2362                                .show_wrap_guides
2363                        },
2364                        pick_mut: |settings_content| {
2365                            &mut settings_content
2366                                .project
2367                                .all_languages
2368                                .defaults
2369                                .show_wrap_guides
2370                        },
2371                    }),
2372                    metadata: None,
2373                }),
2374                // todo(settings_ui): This needs a custom component
2375                // SettingsPageItem::SettingItem(SettingItem {
2376                //     title: "Wrap Guides",
2377                //     description: "Character counts at which to show wrap guides",
2378                //     field: Box::new(SettingField {
2379                //         pick: |settings_content| {
2380                //             &settings_content
2381                //                 .project
2382                //                 .all_languages
2383                //                 .defaults
2384                //                 .wrap_guides
2385                //         },
2386                //         pick_mut: |settings_content| {
2387                //             &mut settings_content
2388                //                 .project
2389                //                 .all_languages
2390                //                 .defaults
2391                //                 .wrap_guides
2392                //         },
2393                //     }),
2394                //     metadata: None,
2395                // }),
2396                SettingsPageItem::SectionHeader("Whitespace"),
2397                SettingsPageItem::SettingItem(SettingItem {
2398                    title: "Show Whitespace",
2399                    description: "Whether to show tabs and spaces",
2400                    field: Box::new(SettingField {
2401                        pick: |settings_content| {
2402                            &settings_content
2403                                .project
2404                                .all_languages
2405                                .defaults
2406                                .show_whitespaces
2407                        },
2408                        pick_mut: |settings_content| {
2409                            &mut settings_content
2410                                .project
2411                                .all_languages
2412                                .defaults
2413                                .show_whitespaces
2414                        },
2415                    }),
2416                    metadata: None,
2417                }),
2418            ],
2419        },
2420        SettingsPage {
2421            title: "Editing",
2422            items: vec![
2423                SettingsPageItem::SectionHeader("Indentation"),
2424                // todo(settings_ui): Needs numeric stepper
2425                // SettingsPageItem::SettingItem(SettingItem {
2426                //     title: "Tab Size",
2427                //     description: "How many columns a tab should occupy",
2428                //     field: Box::new(SettingField {
2429                //         pick: |settings_content| &settings_content.project.all_languages.defaults.tab_size,
2430                //         pick_mut: |settings_content| &mut settings_content.project.all_languages.defaults.tab_size,
2431                //     }),
2432                //     metadata: None,
2433                // }),
2434                SettingsPageItem::SettingItem(SettingItem {
2435                    title: "Hard Tabs",
2436                    description: "Whether to indent lines using tab characters, as opposed to multiple spaces",
2437                    field: Box::new(SettingField {
2438                        pick: |settings_content| {
2439                            &settings_content.project.all_languages.defaults.hard_tabs
2440                        },
2441                        pick_mut: |settings_content| {
2442                            &mut settings_content.project.all_languages.defaults.hard_tabs
2443                        },
2444                    }),
2445                    metadata: None,
2446                }),
2447                SettingsPageItem::SettingItem(SettingItem {
2448                    title: "Auto Indent",
2449                    description: "Whether indentation should be adjusted based on the context whilst typing",
2450                    field: Box::new(SettingField {
2451                        pick: |settings_content| {
2452                            &settings_content.project.all_languages.defaults.auto_indent
2453                        },
2454                        pick_mut: |settings_content| {
2455                            &mut settings_content.project.all_languages.defaults.auto_indent
2456                        },
2457                    }),
2458                    metadata: None,
2459                }),
2460                SettingsPageItem::SettingItem(SettingItem {
2461                    title: "Auto Indent On Paste",
2462                    description: "Whether indentation of pasted content should be adjusted based on the context",
2463                    field: Box::new(SettingField {
2464                        pick: |settings_content| {
2465                            &settings_content
2466                                .project
2467                                .all_languages
2468                                .defaults
2469                                .auto_indent_on_paste
2470                        },
2471                        pick_mut: |settings_content| {
2472                            &mut settings_content
2473                                .project
2474                                .all_languages
2475                                .defaults
2476                                .auto_indent_on_paste
2477                        },
2478                    }),
2479                    metadata: None,
2480                }),
2481                SettingsPageItem::SectionHeader("Wrapping"),
2482                // todo(settings_ui): Needs numeric stepper
2483                // SettingsPageItem::SettingItem(SettingItem {
2484                //     title: "Preferred Line Length",
2485                //     description: "The column at which to soft-wrap lines, for buffers where soft-wrap is enabled",
2486                //     field: Box::new(SettingField {
2487                //         pick: |settings_content| &settings_content.project.all_languages.defaults.preferred_line_length,
2488                //         pick_mut: |settings_content| &mut settings_content.project.all_languages.defaults.preferred_line_length,
2489                //     }),
2490                //     metadata: None,
2491                // }),
2492                SettingsPageItem::SettingItem(SettingItem {
2493                    title: "Soft Wrap",
2494                    description: "How to soft-wrap long lines of text",
2495                    field: Box::new(SettingField {
2496                        pick: |settings_content| {
2497                            &settings_content.project.all_languages.defaults.soft_wrap
2498                        },
2499                        pick_mut: |settings_content| {
2500                            &mut settings_content.project.all_languages.defaults.soft_wrap
2501                        },
2502                    }),
2503                    metadata: None,
2504                }),
2505                SettingsPageItem::SectionHeader("Auto Actions"),
2506                SettingsPageItem::SettingItem(SettingItem {
2507                    title: "Use Autoclose",
2508                    description: "Whether to automatically type closing characters for you",
2509                    field: Box::new(SettingField {
2510                        pick: |settings_content| {
2511                            &settings_content
2512                                .project
2513                                .all_languages
2514                                .defaults
2515                                .use_autoclose
2516                        },
2517                        pick_mut: |settings_content| {
2518                            &mut settings_content
2519                                .project
2520                                .all_languages
2521                                .defaults
2522                                .use_autoclose
2523                        },
2524                    }),
2525                    metadata: None,
2526                }),
2527                SettingsPageItem::SettingItem(SettingItem {
2528                    title: "Use Auto Surround",
2529                    description: "Whether to automatically surround text with characters for you",
2530                    field: Box::new(SettingField {
2531                        pick: |settings_content| {
2532                            &settings_content
2533                                .project
2534                                .all_languages
2535                                .defaults
2536                                .use_auto_surround
2537                        },
2538                        pick_mut: |settings_content| {
2539                            &mut settings_content
2540                                .project
2541                                .all_languages
2542                                .defaults
2543                                .use_auto_surround
2544                        },
2545                    }),
2546                    metadata: None,
2547                }),
2548                SettingsPageItem::SettingItem(SettingItem {
2549                    title: "Use On Type Format",
2550                    description: "Whether to use additional LSP queries to format the code after every trigger symbol input",
2551                    field: Box::new(SettingField {
2552                        pick: |settings_content| {
2553                            &settings_content
2554                                .project
2555                                .all_languages
2556                                .defaults
2557                                .use_on_type_format
2558                        },
2559                        pick_mut: |settings_content| {
2560                            &mut settings_content
2561                                .project
2562                                .all_languages
2563                                .defaults
2564                                .use_on_type_format
2565                        },
2566                    }),
2567                    metadata: None,
2568                }),
2569                SettingsPageItem::SettingItem(SettingItem {
2570                    title: "Always Treat Brackets As Autoclosed",
2571                    description: "Controls how the editor handles the autoclosed characters",
2572                    field: Box::new(SettingField {
2573                        pick: |settings_content| {
2574                            &settings_content
2575                                .project
2576                                .all_languages
2577                                .defaults
2578                                .always_treat_brackets_as_autoclosed
2579                        },
2580                        pick_mut: |settings_content| {
2581                            &mut settings_content
2582                                .project
2583                                .all_languages
2584                                .defaults
2585                                .always_treat_brackets_as_autoclosed
2586                        },
2587                    }),
2588                    metadata: None,
2589                }),
2590                SettingsPageItem::SectionHeader("Formatting"),
2591                SettingsPageItem::SettingItem(SettingItem {
2592                    title: "Remove Trailing Whitespace On Save",
2593                    description: "Whether or not to remove any trailing whitespace from lines of a buffer before saving it",
2594                    field: Box::new(SettingField {
2595                        pick: |settings_content| {
2596                            &settings_content
2597                                .project
2598                                .all_languages
2599                                .defaults
2600                                .remove_trailing_whitespace_on_save
2601                        },
2602                        pick_mut: |settings_content| {
2603                            &mut settings_content
2604                                .project
2605                                .all_languages
2606                                .defaults
2607                                .remove_trailing_whitespace_on_save
2608                        },
2609                    }),
2610                    metadata: None,
2611                }),
2612                SettingsPageItem::SettingItem(SettingItem {
2613                    title: "Ensure Final Newline On Save",
2614                    description: "Whether or not to ensure there's a single newline at the end of a buffer when saving it",
2615                    field: Box::new(SettingField {
2616                        pick: |settings_content| {
2617                            &settings_content
2618                                .project
2619                                .all_languages
2620                                .defaults
2621                                .ensure_final_newline_on_save
2622                        },
2623                        pick_mut: |settings_content| {
2624                            &mut settings_content
2625                                .project
2626                                .all_languages
2627                                .defaults
2628                                .ensure_final_newline_on_save
2629                        },
2630                    }),
2631                    metadata: None,
2632                }),
2633                SettingsPageItem::SettingItem(SettingItem {
2634                    title: "Extend Comment On Newline",
2635                    description: "Whether to start a new line with a comment when a previous line is a comment as well",
2636                    field: Box::new(SettingField {
2637                        pick: |settings_content| {
2638                            &settings_content
2639                                .project
2640                                .all_languages
2641                                .defaults
2642                                .extend_comment_on_newline
2643                        },
2644                        pick_mut: |settings_content| {
2645                            &mut settings_content
2646                                .project
2647                                .all_languages
2648                                .defaults
2649                                .extend_comment_on_newline
2650                        },
2651                    }),
2652                    metadata: None,
2653                }),
2654                SettingsPageItem::SectionHeader("Completions"),
2655                SettingsPageItem::SettingItem(SettingItem {
2656                    title: "Show Completions On Input",
2657                    description: "Whether to pop the completions menu while typing in an editor without explicitly requesting it",
2658                    field: Box::new(SettingField {
2659                        pick: |settings_content| {
2660                            &settings_content
2661                                .project
2662                                .all_languages
2663                                .defaults
2664                                .show_completions_on_input
2665                        },
2666                        pick_mut: |settings_content| {
2667                            &mut settings_content
2668                                .project
2669                                .all_languages
2670                                .defaults
2671                                .show_completions_on_input
2672                        },
2673                    }),
2674                    metadata: None,
2675                }),
2676                SettingsPageItem::SettingItem(SettingItem {
2677                    title: "Show Completion Documentation",
2678                    description: "Whether to display inline and alongside documentation for items in the completions menu",
2679                    field: Box::new(SettingField {
2680                        pick: |settings_content| {
2681                            &settings_content
2682                                .project
2683                                .all_languages
2684                                .defaults
2685                                .show_completion_documentation
2686                        },
2687                        pick_mut: |settings_content| {
2688                            &mut settings_content
2689                                .project
2690                                .all_languages
2691                                .defaults
2692                                .show_completion_documentation
2693                        },
2694                    }),
2695                    metadata: None,
2696                }),
2697            ],
2698        },
2699    ]
2700}
2701
2702pub struct SettingsUiFeatureFlag;
2703
2704impl FeatureFlag for SettingsUiFeatureFlag {
2705    const NAME: &'static str = "settings-ui";
2706}
2707
2708pub fn init(cx: &mut App) {
2709    init_renderers(cx);
2710
2711    cx.observe_new(|workspace: &mut workspace::Workspace, _, _| {
2712        workspace.register_action_renderer(|div, _, _, cx| {
2713            let settings_ui_actions = [std::any::TypeId::of::<OpenSettingsEditor>()];
2714            let has_flag = cx.has_flag::<SettingsUiFeatureFlag>();
2715            command_palette_hooks::CommandPaletteFilter::update_global(cx, |filter, _| {
2716                if has_flag {
2717                    filter.show_action_types(&settings_ui_actions);
2718                } else {
2719                    filter.hide_action_types(&settings_ui_actions);
2720                }
2721            });
2722            if has_flag {
2723                div.on_action(cx.listener(|_, _: &OpenSettingsEditor, _, cx| {
2724                    open_settings_editor(cx).ok();
2725                }))
2726            } else {
2727                div
2728            }
2729        });
2730    })
2731    .detach();
2732}
2733
2734fn init_renderers(cx: &mut App) {
2735    // fn (field: SettingsField, current_file: SettingsFile, cx) -> (currently_set_in: SettingsFile, overridden_in: Vec<SettingsFile>)
2736    cx.default_global::<SettingFieldRenderer>()
2737        .add_renderer::<bool>(|settings_field, file, _, _, cx| {
2738            render_toggle_button(*settings_field, file, cx).into_any_element()
2739        })
2740        .add_renderer::<String>(|settings_field, file, metadata, _, cx| {
2741            render_text_field(settings_field.clone(), file, metadata, cx)
2742        })
2743        .add_renderer::<SaturatingBool>(|settings_field, file, _, _, cx| {
2744            render_toggle_button(*settings_field, file, cx)
2745        })
2746        .add_renderer::<CursorShape>(|settings_field, file, _, window, cx| {
2747            render_dropdown(*settings_field, file, window, cx)
2748        })
2749        .add_renderer::<RestoreOnStartupBehavior>(|settings_field, file, _, window, cx| {
2750            render_dropdown(*settings_field, file, window, cx)
2751        })
2752        .add_renderer::<BottomDockLayout>(|settings_field, file, _, window, cx| {
2753            render_dropdown(*settings_field, file, window, cx)
2754        })
2755        .add_renderer::<OnLastWindowClosed>(|settings_field, file, _, window, cx| {
2756            render_dropdown(*settings_field, file, window, cx)
2757        })
2758        .add_renderer::<CloseWindowWhenNoItems>(|settings_field, file, _, window, cx| {
2759            render_dropdown(*settings_field, file, window, cx)
2760        })
2761        .add_renderer::<settings::FontFamilyName>(|settings_field, file, _, window, cx| {
2762            // todo(settings_ui): We need to pass in a validator for this to ensure that users that type in invalid font names
2763            render_font_picker(settings_field.clone(), file, window, cx)
2764        })
2765        .add_renderer::<settings::BufferLineHeight>(|settings_field, file, _, window, cx| {
2766            // todo(settings_ui): Do we want to expose the custom variant of buffer line height?
2767            // right now there's a manual impl of strum::VariantArray
2768            render_dropdown(*settings_field, file, window, cx)
2769        })
2770        .add_renderer::<settings::BaseKeymapContent>(|settings_field, file, _, window, cx| {
2771            render_dropdown(*settings_field, file, window, cx)
2772        })
2773        .add_renderer::<settings::MultiCursorModifier>(|settings_field, file, _, window, cx| {
2774            render_dropdown(*settings_field, file, window, cx)
2775        })
2776        .add_renderer::<settings::HideMouseMode>(|settings_field, file, _, window, cx| {
2777            render_dropdown(*settings_field, file, window, cx)
2778        })
2779        .add_renderer::<settings::CurrentLineHighlight>(|settings_field, file, _, window, cx| {
2780            render_dropdown(*settings_field, file, window, cx)
2781        })
2782        .add_renderer::<settings::ShowWhitespaceSetting>(|settings_field, file, _, window, cx| {
2783            render_dropdown(*settings_field, file, window, cx)
2784        })
2785        .add_renderer::<settings::SoftWrap>(|settings_field, file, _, window, cx| {
2786            render_dropdown(*settings_field, file, window, cx)
2787        })
2788        .add_renderer::<settings::ScrollBeyondLastLine>(|settings_field, file, _, window, cx| {
2789            render_dropdown(*settings_field, file, window, cx)
2790        })
2791        .add_renderer::<settings::SnippetSortOrder>(|settings_field, file, _, window, cx| {
2792            render_dropdown(*settings_field, file, window, cx)
2793        })
2794        .add_renderer::<settings::ClosePosition>(|settings_field, file, _, window, cx| {
2795            render_dropdown(*settings_field, file, window, cx)
2796        })
2797        .add_renderer::<settings::DockSide>(|settings_field, file, _, window, cx| {
2798            render_dropdown(*settings_field, file, window, cx)
2799        })
2800        .add_renderer::<settings::TerminalDockPosition>(|settings_field, file, _, window, cx| {
2801            render_dropdown(*settings_field, file, window, cx)
2802        })
2803        .add_renderer::<settings::GitGutterSetting>(|settings_field, file, _, window, cx| {
2804            render_dropdown(*settings_field, file, window, cx)
2805        })
2806        .add_renderer::<settings::GitHunkStyleSetting>(|settings_field, file, _, window, cx| {
2807            render_dropdown(*settings_field, file, window, cx)
2808        })
2809        .add_renderer::<settings::DiagnosticSeverityContent>(
2810            |settings_field, file, _, window, cx| {
2811                render_dropdown(*settings_field, file, window, cx)
2812            },
2813        )
2814        .add_renderer::<settings::SeedQuerySetting>(|settings_field, file, _, window, cx| {
2815            render_dropdown(*settings_field, file, window, cx)
2816        })
2817        .add_renderer::<settings::DoubleClickInMultibuffer>(
2818            |settings_field, file, _, window, cx| {
2819                render_dropdown(*settings_field, file, window, cx)
2820            },
2821        )
2822        .add_renderer::<settings::GoToDefinitionFallback>(|settings_field, file, _, window, cx| {
2823            render_dropdown(*settings_field, file, window, cx)
2824        })
2825        .add_renderer::<settings::ActivateOnClose>(|settings_field, file, _, window, cx| {
2826            render_dropdown(*settings_field, file, window, cx)
2827        })
2828        .add_renderer::<settings::ShowDiagnostics>(|settings_field, file, _, window, cx| {
2829            render_dropdown(*settings_field, file, window, cx)
2830        })
2831        .add_renderer::<settings::ShowCloseButton>(|settings_field, file, _, window, cx| {
2832            render_dropdown(*settings_field, file, window, cx)
2833        })
2834        .add_renderer::<f32>(|settings_field, file, _, window, cx| {
2835            render_numeric_stepper(*settings_field, file, window, cx)
2836        })
2837        .add_renderer::<u32>(|settings_field, file, _, window, cx| {
2838            render_numeric_stepper(*settings_field, file, window, cx)
2839        })
2840        .add_renderer::<u64>(|settings_field, file, _, window, cx| {
2841            render_numeric_stepper(*settings_field, file, window, cx)
2842        })
2843        .add_renderer::<NonZeroU32>(|settings_field, file, _, window, cx| {
2844            render_numeric_stepper(*settings_field, file, window, cx)
2845        })
2846        .add_renderer::<CodeFade>(|settings_field, file, _, window, cx| {
2847            render_numeric_stepper(*settings_field, file, window, cx)
2848        })
2849        .add_renderer::<FontWeight>(|settings_field, file, _, window, cx| {
2850            render_numeric_stepper(*settings_field, file, window, cx)
2851        });
2852
2853    // todo(settings_ui): Figure out how we want to handle discriminant unions
2854    // .add_renderer::<ThemeSelection>(|settings_field, file, _, window, cx| {
2855    //     render_dropdown(*settings_field, file, window, cx)
2856    // });
2857}
2858
2859pub fn open_settings_editor(cx: &mut App) -> anyhow::Result<WindowHandle<SettingsWindow>> {
2860    cx.open_window(
2861        WindowOptions {
2862            titlebar: Some(TitlebarOptions {
2863                title: Some("Settings Window".into()),
2864                appears_transparent: true,
2865                traffic_light_position: Some(point(px(12.0), px(12.0))),
2866            }),
2867            focus: true,
2868            show: true,
2869            kind: gpui::WindowKind::Normal,
2870            window_background: cx.theme().window_background_appearance(),
2871            window_min_size: Some(size(px(800.), px(600.))), // 4:3 Aspect Ratio
2872            ..Default::default()
2873        },
2874        |window, cx| cx.new(|cx| SettingsWindow::new(window, cx)),
2875    )
2876}
2877
2878pub struct SettingsWindow {
2879    files: Vec<SettingsUiFile>,
2880    current_file: SettingsUiFile,
2881    pages: Vec<SettingsPage>,
2882    search_bar: Entity<Editor>,
2883    search_task: Option<Task<()>>,
2884    navbar_entry: usize, // Index into pages - should probably be (usize, Option<usize>) for section + page
2885    navbar_entries: Vec<NavBarEntry>,
2886    list_handle: UniformListScrollHandle,
2887    search_matches: Vec<Vec<bool>>,
2888    /// The current sub page path that is selected.
2889    /// If this is empty the selected page is rendered,
2890    /// otherwise the last sub page gets rendered.
2891    sub_page_stack: Vec<SubPage>,
2892    scroll_handle: ScrollHandle,
2893}
2894
2895struct SubPage {
2896    link: SubPageLink,
2897    section_header: &'static str,
2898}
2899
2900#[derive(PartialEq, Debug)]
2901struct NavBarEntry {
2902    title: &'static str,
2903    is_root: bool,
2904    expanded: bool,
2905    page_index: usize,
2906    item_index: Option<usize>,
2907}
2908
2909struct SettingsPage {
2910    title: &'static str,
2911    items: Vec<SettingsPageItem>,
2912}
2913
2914#[derive(PartialEq)]
2915enum SettingsPageItem {
2916    SectionHeader(&'static str),
2917    SettingItem(SettingItem),
2918    SubPageLink(SubPageLink),
2919}
2920
2921impl std::fmt::Debug for SettingsPageItem {
2922    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2923        match self {
2924            SettingsPageItem::SectionHeader(header) => write!(f, "SectionHeader({})", header),
2925            SettingsPageItem::SettingItem(setting_item) => {
2926                write!(f, "SettingItem({})", setting_item.title)
2927            }
2928            SettingsPageItem::SubPageLink(sub_page_link) => {
2929                write!(f, "SubPageLink({})", sub_page_link.title)
2930            }
2931        }
2932    }
2933}
2934
2935impl SettingsPageItem {
2936    fn render(
2937        &self,
2938        file: SettingsUiFile,
2939        section_header: &'static str,
2940        is_last: bool,
2941        window: &mut Window,
2942        cx: &mut Context<SettingsWindow>,
2943    ) -> AnyElement {
2944        match self {
2945            SettingsPageItem::SectionHeader(header) => v_flex()
2946                .w_full()
2947                .gap_1()
2948                .child(
2949                    Label::new(SharedString::new_static(header))
2950                        .size(LabelSize::XSmall)
2951                        .color(Color::Muted)
2952                        .buffer_font(cx),
2953                )
2954                .child(Divider::horizontal().color(ui::DividerColor::BorderVariant))
2955                .into_any_element(),
2956            SettingsPageItem::SettingItem(setting_item) => {
2957                let renderer = cx.default_global::<SettingFieldRenderer>().clone();
2958                let file_set_in =
2959                    SettingsUiFile::from_settings(setting_item.field.file_set_in(file.clone(), cx));
2960
2961                h_flex()
2962                    .id(setting_item.title)
2963                    .w_full()
2964                    .gap_2()
2965                    .flex_wrap()
2966                    .justify_between()
2967                    .map(|this| {
2968                        if is_last {
2969                            this.pb_6()
2970                        } else {
2971                            this.pb_4()
2972                                .border_b_1()
2973                                .border_color(cx.theme().colors().border_variant)
2974                        }
2975                    })
2976                    .child(
2977                        v_flex()
2978                            .max_w_1_2()
2979                            .flex_shrink()
2980                            .child(
2981                                h_flex()
2982                                    .w_full()
2983                                    .gap_1()
2984                                    .child(Label::new(SharedString::new_static(setting_item.title)))
2985                                    .when_some(
2986                                        file_set_in.filter(|file_set_in| file_set_in != &file),
2987                                        |this, file_set_in| {
2988                                            this.child(
2989                                                Label::new(format!(
2990                                                    "— set in {}",
2991                                                    file_set_in.name()
2992                                                ))
2993                                                .color(Color::Muted)
2994                                                .size(LabelSize::Small),
2995                                            )
2996                                        },
2997                                    ),
2998                            )
2999                            .child(
3000                                Label::new(SharedString::new_static(setting_item.description))
3001                                    .size(LabelSize::Small)
3002                                    .color(Color::Muted),
3003                            ),
3004                    )
3005                    .child(renderer.render(
3006                        setting_item.field.as_ref(),
3007                        file,
3008                        setting_item.metadata.as_deref(),
3009                        window,
3010                        cx,
3011                    ))
3012                    .into_any_element()
3013            }
3014            SettingsPageItem::SubPageLink(sub_page_link) => h_flex()
3015                .id(sub_page_link.title)
3016                .w_full()
3017                .gap_2()
3018                .flex_wrap()
3019                .justify_between()
3020                .when(!is_last, |this| {
3021                    this.pb_4()
3022                        .border_b_1()
3023                        .border_color(cx.theme().colors().border_variant)
3024                })
3025                .child(
3026                    v_flex()
3027                        .max_w_1_2()
3028                        .flex_shrink()
3029                        .child(Label::new(SharedString::new_static(sub_page_link.title))),
3030                )
3031                .child(
3032                    Button::new(("sub-page".into(), sub_page_link.title), "Configure")
3033                        .size(ButtonSize::Medium)
3034                        .icon(IconName::ChevronRight)
3035                        .icon_position(IconPosition::End)
3036                        .icon_color(Color::Muted)
3037                        .icon_size(IconSize::Small)
3038                        .style(ButtonStyle::Outlined),
3039                )
3040                .on_click({
3041                    let sub_page_link = sub_page_link.clone();
3042                    cx.listener(move |this, _, _, cx| {
3043                        this.push_sub_page(sub_page_link.clone(), section_header, cx)
3044                    })
3045                })
3046                .into_any_element(),
3047        }
3048    }
3049}
3050
3051struct SettingItem {
3052    title: &'static str,
3053    description: &'static str,
3054    field: Box<dyn AnySettingField>,
3055    metadata: Option<Box<SettingsFieldMetadata>>,
3056}
3057
3058impl PartialEq for SettingItem {
3059    fn eq(&self, other: &Self) -> bool {
3060        self.title == other.title
3061            && self.description == other.description
3062            && (match (&self.metadata, &other.metadata) {
3063                (None, None) => true,
3064                (Some(m1), Some(m2)) => m1.placeholder == m2.placeholder,
3065                _ => false,
3066            })
3067    }
3068}
3069
3070#[derive(Clone)]
3071struct SubPageLink {
3072    title: &'static str,
3073    render: Rc<dyn Fn(&mut SettingsWindow, &mut Window, &mut App) -> AnyElement>,
3074}
3075
3076impl PartialEq for SubPageLink {
3077    fn eq(&self, other: &Self) -> bool {
3078        self.title == other.title
3079    }
3080}
3081
3082#[allow(unused)]
3083#[derive(Clone, PartialEq)]
3084enum SettingsUiFile {
3085    User,                              // Uses all settings.
3086    Local((WorktreeId, Arc<RelPath>)), // Has a special name, and special set of settings
3087    Server(&'static str),              // Uses a special name, and the user settings
3088}
3089
3090impl SettingsUiFile {
3091    fn pages(&self) -> Vec<SettingsPage> {
3092        match self {
3093            SettingsUiFile::User => user_settings_data(),
3094            SettingsUiFile::Local(_) => project_settings_data(),
3095            SettingsUiFile::Server(_) => user_settings_data(),
3096        }
3097    }
3098
3099    fn name(&self) -> SharedString {
3100        match self {
3101            SettingsUiFile::User => SharedString::new_static("User"),
3102            // TODO is PathStyle::local() ever not appropriate?
3103            SettingsUiFile::Local((_, path)) => {
3104                format!("Local ({})", path.display(PathStyle::local())).into()
3105            }
3106            SettingsUiFile::Server(file) => format!("Server ({})", file).into(),
3107        }
3108    }
3109
3110    fn from_settings(file: settings::SettingsFile) -> Option<Self> {
3111        Some(match file {
3112            settings::SettingsFile::User => SettingsUiFile::User,
3113            settings::SettingsFile::Local(location) => SettingsUiFile::Local(location),
3114            settings::SettingsFile::Server => SettingsUiFile::Server("todo: server name"),
3115            settings::SettingsFile::Default => return None,
3116        })
3117    }
3118
3119    fn to_settings(&self) -> settings::SettingsFile {
3120        match self {
3121            SettingsUiFile::User => settings::SettingsFile::User,
3122            SettingsUiFile::Local(location) => settings::SettingsFile::Local(location.clone()),
3123            SettingsUiFile::Server(_) => settings::SettingsFile::Server,
3124        }
3125    }
3126}
3127
3128impl SettingsWindow {
3129    pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
3130        let font_family_cache = theme::FontFamilyCache::global(cx);
3131
3132        cx.spawn(async move |this, cx| {
3133            font_family_cache.prefetch(cx).await;
3134            this.update(cx, |_, cx| {
3135                cx.notify();
3136            })
3137        })
3138        .detach();
3139
3140        let current_file = SettingsUiFile::User;
3141        let search_bar = cx.new(|cx| {
3142            let mut editor = Editor::single_line(window, cx);
3143            editor.set_placeholder_text("Search settings…", window, cx);
3144            editor
3145        });
3146
3147        cx.subscribe(&search_bar, |this, _, event: &EditorEvent, cx| {
3148            let EditorEvent::Edited { transaction_id: _ } = event else {
3149                return;
3150            };
3151
3152            this.update_matches(cx);
3153        })
3154        .detach();
3155
3156        cx.observe_global_in::<SettingsStore>(window, move |this, _, cx| {
3157            this.fetch_files(cx);
3158            cx.notify();
3159        })
3160        .detach();
3161
3162        let mut this = Self {
3163            files: vec![],
3164            current_file: current_file,
3165            pages: vec![],
3166            navbar_entries: vec![],
3167            navbar_entry: 0,
3168            list_handle: UniformListScrollHandle::default(),
3169            search_bar,
3170            search_task: None,
3171            search_matches: vec![],
3172            sub_page_stack: vec![],
3173            scroll_handle: ScrollHandle::new(),
3174        };
3175
3176        this.fetch_files(cx);
3177        this.build_ui(cx);
3178
3179        this.search_bar.update(cx, |editor, cx| {
3180            editor.focus_handle(cx).focus(window);
3181        });
3182
3183        this
3184    }
3185
3186    fn toggle_navbar_entry(&mut self, ix: usize) {
3187        // We can only toggle root entries
3188        if !self.navbar_entries[ix].is_root {
3189            return;
3190        }
3191
3192        let toggle_page_index = self.page_index_from_navbar_index(ix);
3193        let selected_page_index = self.page_index_from_navbar_index(self.navbar_entry);
3194
3195        let expanded = &mut self.navbar_entries[ix].expanded;
3196        *expanded = !*expanded;
3197        // if currently selected page is a child of the parent page we are folding,
3198        // set the current page to the parent page
3199        if !*expanded && selected_page_index == toggle_page_index {
3200            self.navbar_entry = ix;
3201        }
3202    }
3203
3204    fn build_navbar(&mut self) {
3205        let mut navbar_entries = Vec::with_capacity(self.navbar_entries.len());
3206        for (page_index, page) in self.pages.iter().enumerate() {
3207            navbar_entries.push(NavBarEntry {
3208                title: page.title,
3209                is_root: true,
3210                expanded: false,
3211                page_index,
3212                item_index: None,
3213            });
3214
3215            for (item_index, item) in page.items.iter().enumerate() {
3216                let SettingsPageItem::SectionHeader(title) = item else {
3217                    continue;
3218                };
3219                navbar_entries.push(NavBarEntry {
3220                    title,
3221                    is_root: false,
3222                    expanded: false,
3223                    page_index,
3224                    item_index: Some(item_index),
3225                });
3226            }
3227        }
3228        self.navbar_entries = navbar_entries;
3229    }
3230
3231    fn visible_navbar_entries(&self) -> impl Iterator<Item = (usize, &NavBarEntry)> {
3232        let mut index = 0;
3233        let entries = &self.navbar_entries;
3234        let search_matches = &self.search_matches;
3235        std::iter::from_fn(move || {
3236            while index < entries.len() {
3237                let entry = &entries[index];
3238                let included_in_search = if let Some(item_index) = entry.item_index {
3239                    search_matches[entry.page_index][item_index]
3240                } else {
3241                    search_matches[entry.page_index].iter().any(|b| *b)
3242                        || search_matches[entry.page_index].is_empty()
3243                };
3244                if included_in_search {
3245                    break;
3246                }
3247                index += 1;
3248            }
3249            if index >= self.navbar_entries.len() {
3250                return None;
3251            }
3252            let entry = &entries[index];
3253            let entry_index = index;
3254
3255            index += 1;
3256            if entry.is_root && !entry.expanded {
3257                while index < entries.len() {
3258                    if entries[index].is_root {
3259                        break;
3260                    }
3261                    index += 1;
3262                }
3263            }
3264
3265            return Some((entry_index, entry));
3266        })
3267    }
3268
3269    fn update_matches(&mut self, cx: &mut Context<SettingsWindow>) {
3270        self.search_task.take();
3271        let query = self.search_bar.read(cx).text(cx);
3272        if query.is_empty() {
3273            for page in &mut self.search_matches {
3274                page.fill(true);
3275            }
3276            cx.notify();
3277            return;
3278        }
3279
3280        struct ItemKey {
3281            page_index: usize,
3282            header_index: usize,
3283            item_index: usize,
3284        }
3285        let mut key_lut: Vec<ItemKey> = vec![];
3286        let mut candidates = Vec::default();
3287
3288        for (page_index, page) in self.pages.iter().enumerate() {
3289            let mut header_index = 0;
3290            for (item_index, item) in page.items.iter().enumerate() {
3291                let key_index = key_lut.len();
3292                match item {
3293                    SettingsPageItem::SettingItem(item) => {
3294                        candidates.push(StringMatchCandidate::new(key_index, item.title));
3295                        candidates.push(StringMatchCandidate::new(key_index, item.description));
3296                    }
3297                    SettingsPageItem::SectionHeader(header) => {
3298                        candidates.push(StringMatchCandidate::new(key_index, header));
3299                        header_index = item_index;
3300                    }
3301                    SettingsPageItem::SubPageLink(sub_page_link) => {
3302                        candidates.push(StringMatchCandidate::new(key_index, sub_page_link.title));
3303                    }
3304                }
3305                key_lut.push(ItemKey {
3306                    page_index,
3307                    header_index,
3308                    item_index,
3309                });
3310            }
3311        }
3312        let atomic_bool = AtomicBool::new(false);
3313
3314        self.search_task = Some(cx.spawn(async move |this, cx| {
3315            let string_matches = fuzzy::match_strings(
3316                candidates.as_slice(),
3317                &query,
3318                false,
3319                true,
3320                candidates.len(),
3321                &atomic_bool,
3322                cx.background_executor().clone(),
3323            );
3324            let string_matches = string_matches.await;
3325
3326            this.update(cx, |this, cx| {
3327                for page in &mut this.search_matches {
3328                    page.fill(false);
3329                }
3330
3331                for string_match in string_matches {
3332                    let ItemKey {
3333                        page_index,
3334                        header_index,
3335                        item_index,
3336                    } = key_lut[string_match.candidate_id];
3337                    let page = &mut this.search_matches[page_index];
3338                    page[header_index] = true;
3339                    page[item_index] = true;
3340                }
3341                let first_navbar_entry_index = this
3342                    .visible_navbar_entries()
3343                    .next()
3344                    .map(|e| e.0)
3345                    .unwrap_or(0);
3346                this.navbar_entry = first_navbar_entry_index;
3347                cx.notify();
3348            })
3349            .ok();
3350        }));
3351    }
3352
3353    fn build_search_matches(&mut self) {
3354        self.search_matches = self
3355            .pages
3356            .iter()
3357            .map(|page| vec![true; page.items.len()])
3358            .collect::<Vec<_>>();
3359    }
3360
3361    fn build_ui(&mut self, cx: &mut Context<SettingsWindow>) {
3362        self.pages = self.current_file.pages();
3363        self.build_search_matches();
3364        self.build_navbar();
3365
3366        if !self.search_bar.read(cx).is_empty(cx) {
3367            self.update_matches(cx);
3368        }
3369
3370        cx.notify();
3371    }
3372
3373    fn fetch_files(&mut self, cx: &mut Context<SettingsWindow>) {
3374        let settings_store = cx.global::<SettingsStore>();
3375        let mut ui_files = vec![];
3376        let all_files = settings_store.get_all_files();
3377        for file in all_files {
3378            let Some(settings_ui_file) = SettingsUiFile::from_settings(file) else {
3379                continue;
3380            };
3381            ui_files.push(settings_ui_file);
3382        }
3383        ui_files.reverse();
3384        self.files = ui_files;
3385        if !self.files.contains(&self.current_file) {
3386            self.change_file(0, cx);
3387        }
3388    }
3389
3390    fn change_file(&mut self, ix: usize, cx: &mut Context<SettingsWindow>) {
3391        if ix >= self.files.len() {
3392            self.current_file = SettingsUiFile::User;
3393            return;
3394        }
3395        if self.files[ix] == self.current_file {
3396            return;
3397        }
3398        self.current_file = self.files[ix].clone();
3399        self.navbar_entry = 0;
3400        self.build_ui(cx);
3401    }
3402
3403    fn render_files(&self, _window: &mut Window, cx: &mut Context<SettingsWindow>) -> Div {
3404        h_flex()
3405            .gap_1()
3406            .children(self.files.iter().enumerate().map(|(ix, file)| {
3407                Button::new(ix, file.name())
3408                    .toggle_state(file == &self.current_file)
3409                    .selected_style(ButtonStyle::Tinted(ui::TintColor::Accent))
3410                    .on_click(cx.listener(move |this, _, _window, cx| this.change_file(ix, cx)))
3411            }))
3412    }
3413
3414    fn render_search(&self, _window: &mut Window, cx: &mut App) -> Div {
3415        h_flex()
3416            .py_1()
3417            .px_1p5()
3418            .gap_1p5()
3419            .rounded_sm()
3420            .bg(cx.theme().colors().editor_background)
3421            .border_1()
3422            .border_color(cx.theme().colors().border)
3423            .child(Icon::new(IconName::MagnifyingGlass).color(Color::Muted))
3424            .child(self.search_bar.clone())
3425    }
3426
3427    fn render_nav(
3428        &self,
3429        window: &mut Window,
3430        cx: &mut Context<SettingsWindow>,
3431    ) -> impl IntoElement {
3432        let visible_entries: Vec<_> = self.visible_navbar_entries().collect();
3433        let visible_count = visible_entries.len();
3434
3435        v_flex()
3436            .w_64()
3437            .p_2p5()
3438            .pt_10()
3439            .gap_3()
3440            .flex_none()
3441            .border_r_1()
3442            .border_color(cx.theme().colors().border)
3443            .bg(cx.theme().colors().panel_background)
3444            .child(self.render_search(window, cx))
3445            .child(
3446                v_flex()
3447                    .size_full()
3448                    .child(
3449                        uniform_list(
3450                            "settings-ui-nav-bar",
3451                            visible_count,
3452                            cx.processor(move |this, range: Range<usize>, _, cx| {
3453                                let entries: Vec<_> = this.visible_navbar_entries().collect();
3454                                range
3455                                    .filter_map(|ix| entries.get(ix).copied())
3456                                    .map(|(ix, entry)| {
3457                                        TreeViewItem::new(
3458                                            ("settings-ui-navbar-entry", ix),
3459                                            entry.title,
3460                                        )
3461                                        .root_item(entry.is_root)
3462                                        .toggle_state(this.is_navbar_entry_selected(ix))
3463                                        .when(entry.is_root, |item| {
3464                                            item.expanded(entry.expanded).on_toggle(cx.listener(
3465                                                move |this, _, _, cx| {
3466                                                    this.toggle_navbar_entry(ix);
3467                                                    cx.notify();
3468                                                },
3469                                            ))
3470                                        })
3471                                        .on_click(cx.listener(move |this, _, _, cx| {
3472                                            this.navbar_entry = ix;
3473                                            cx.notify();
3474                                        }))
3475                                        .into_any_element()
3476                                    })
3477                                    .collect()
3478                            }),
3479                        )
3480                        .track_scroll(self.list_handle.clone())
3481                        .flex_grow(),
3482                    )
3483                    .vertical_scrollbar_for(self.list_handle.clone(), window, cx),
3484            )
3485    }
3486
3487    fn page_items(&self) -> impl Iterator<Item = &SettingsPageItem> {
3488        let page_idx = self.current_page_index();
3489
3490        self.current_page()
3491            .items
3492            .iter()
3493            .enumerate()
3494            .filter_map(move |(item_index, item)| {
3495                self.search_matches[page_idx][item_index].then_some(item)
3496            })
3497    }
3498
3499    fn render_sub_page_breadcrumbs(&self) -> impl IntoElement {
3500        let mut items = vec![];
3501        items.push(self.current_page().title);
3502        items.extend(
3503            self.sub_page_stack
3504                .iter()
3505                .flat_map(|page| [page.section_header, page.link.title]),
3506        );
3507
3508        let last = items.pop().unwrap();
3509        h_flex()
3510            .gap_1()
3511            .children(
3512                items
3513                    .into_iter()
3514                    .flat_map(|item| [item, "/"])
3515                    .map(|item| Label::new(item).color(Color::Muted)),
3516            )
3517            .child(Label::new(last))
3518    }
3519
3520    fn render_page(
3521        &mut self,
3522        window: &mut Window,
3523        cx: &mut Context<SettingsWindow>,
3524    ) -> impl IntoElement {
3525        let mut page = v_flex()
3526            .w_full()
3527            .pt_4()
3528            .pb_6()
3529            .px_6()
3530            .gap_4()
3531            .bg(cx.theme().colors().editor_background)
3532            .vertical_scrollbar_for(self.scroll_handle.clone(), window, cx);
3533
3534        let mut page_content = v_flex()
3535            .id("settings-ui-page")
3536            .size_full()
3537            .gap_4()
3538            .overflow_y_scroll()
3539            .track_scroll(&self.scroll_handle);
3540
3541        if self.sub_page_stack.len() == 0 {
3542            page = page.child(self.render_files(window, cx));
3543
3544            let items: Vec<_> = self.page_items().collect();
3545            let items_len = items.len();
3546            let mut section_header = None;
3547
3548            let search_query = self.search_bar.read(cx).text(cx);
3549            let has_active_search = !search_query.is_empty();
3550            let has_no_results = items_len == 0 && has_active_search;
3551
3552            if has_no_results {
3553                page_content = page_content.child(
3554                    v_flex()
3555                        .size_full()
3556                        .items_center()
3557                        .justify_center()
3558                        .gap_1()
3559                        .child(div().child("No Results"))
3560                        .child(
3561                            div()
3562                                .text_sm()
3563                                .text_color(cx.theme().colors().text_muted)
3564                                .child(format!("No settings match \"{}\"", search_query)),
3565                        ),
3566                )
3567            } else {
3568                let last_non_header_index = items
3569                    .iter()
3570                    .enumerate()
3571                    .rev()
3572                    .find(|(_, item)| !matches!(item, SettingsPageItem::SectionHeader(_)))
3573                    .map(|(index, _)| index);
3574
3575                page_content = page_content.children(items.clone().into_iter().enumerate().map(
3576                    |(index, item)| {
3577                        let no_bottom_border = items
3578                            .get(index + 1)
3579                            .map(|next_item| {
3580                                matches!(next_item, SettingsPageItem::SectionHeader(_))
3581                            })
3582                            .unwrap_or(false);
3583                        let is_last = Some(index) == last_non_header_index;
3584
3585                        if let SettingsPageItem::SectionHeader(header) = item {
3586                            section_header = Some(*header);
3587                        }
3588                        item.render(
3589                            self.current_file.clone(),
3590                            section_header.expect("All items rendered after a section header"),
3591                            no_bottom_border || is_last,
3592                            window,
3593                            cx,
3594                        )
3595                    },
3596                ))
3597            }
3598        } else {
3599            page = page.child(
3600                h_flex()
3601                    .ml_neg_1p5()
3602                    .gap_1()
3603                    .child(
3604                        IconButton::new("back-btn", IconName::ArrowLeft)
3605                            .icon_size(IconSize::Small)
3606                            .shape(IconButtonShape::Square)
3607                            .on_click(cx.listener(|this, _, _, cx| {
3608                                this.pop_sub_page(cx);
3609                            })),
3610                    )
3611                    .child(self.render_sub_page_breadcrumbs()),
3612            );
3613
3614            let active_page_render_fn = self.sub_page_stack.last().unwrap().link.render.clone();
3615            page_content = page_content.child((active_page_render_fn)(self, window, cx));
3616        }
3617
3618        return page.child(page_content);
3619    }
3620
3621    fn current_page_index(&self) -> usize {
3622        self.page_index_from_navbar_index(self.navbar_entry)
3623    }
3624
3625    fn current_page(&self) -> &SettingsPage {
3626        &self.pages[self.current_page_index()]
3627    }
3628
3629    fn page_index_from_navbar_index(&self, index: usize) -> usize {
3630        if self.navbar_entries.is_empty() {
3631            return 0;
3632        }
3633
3634        self.navbar_entries[index].page_index
3635    }
3636
3637    fn is_navbar_entry_selected(&self, ix: usize) -> bool {
3638        ix == self.navbar_entry
3639    }
3640
3641    fn push_sub_page(
3642        &mut self,
3643        sub_page_link: SubPageLink,
3644        section_header: &'static str,
3645        cx: &mut Context<SettingsWindow>,
3646    ) {
3647        self.sub_page_stack.push(SubPage {
3648            link: sub_page_link,
3649            section_header,
3650        });
3651        cx.notify();
3652    }
3653
3654    fn pop_sub_page(&mut self, cx: &mut Context<SettingsWindow>) {
3655        self.sub_page_stack.pop();
3656        cx.notify();
3657    }
3658}
3659
3660impl Render for SettingsWindow {
3661    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
3662        let ui_font = theme::setup_ui_font(window, cx);
3663
3664        div()
3665            .key_context("SettingsWindow")
3666            .flex()
3667            .flex_row()
3668            .size_full()
3669            .font(ui_font)
3670            .bg(cx.theme().colors().background)
3671            .text_color(cx.theme().colors().text)
3672            .child(self.render_nav(window, cx))
3673            .child(self.render_page(window, cx))
3674    }
3675}
3676
3677fn update_settings_file(
3678    file: SettingsUiFile,
3679    cx: &mut App,
3680    update: impl 'static + Send + FnOnce(&mut SettingsContent, &App),
3681) -> Result<()> {
3682    match file {
3683        SettingsUiFile::Local((worktree_id, rel_path)) => {
3684            fn all_projects(cx: &App) -> impl Iterator<Item = Entity<project::Project>> {
3685                workspace::AppState::global(cx)
3686                    .upgrade()
3687                    .map(|app_state| {
3688                        app_state
3689                            .workspace_store
3690                            .read(cx)
3691                            .workspaces()
3692                            .iter()
3693                            .filter_map(|workspace| {
3694                                Some(workspace.read(cx).ok()?.project().clone())
3695                            })
3696                    })
3697                    .into_iter()
3698                    .flatten()
3699            }
3700            let rel_path = rel_path.join(paths::local_settings_file_relative_path());
3701            let project = all_projects(cx).find(|project| {
3702                project.read_with(cx, |project, cx| {
3703                    project.contains_local_settings_file(worktree_id, &rel_path, cx)
3704                })
3705            });
3706            let Some(project) = project else {
3707                anyhow::bail!(
3708                    "Could not find worktree containing settings file: {}",
3709                    &rel_path.display(PathStyle::local())
3710                );
3711            };
3712            project.update(cx, |project, cx| {
3713                project.update_local_settings_file(worktree_id, rel_path, cx, update);
3714            });
3715            return Ok(());
3716        }
3717        SettingsUiFile::User => {
3718            // todo(settings_ui) error?
3719            SettingsStore::global(cx).update_settings_file(<dyn fs::Fs>::global(cx), update);
3720            Ok(())
3721        }
3722        SettingsUiFile::Server(_) => unimplemented!(),
3723    }
3724}
3725
3726fn render_text_field<T: From<String> + Into<String> + AsRef<str> + Clone>(
3727    field: SettingField<T>,
3728    file: SettingsUiFile,
3729    metadata: Option<&SettingsFieldMetadata>,
3730    cx: &mut App,
3731) -> AnyElement {
3732    let (_, initial_text) =
3733        SettingsStore::global(cx).get_value_from_file(file.to_settings(), field.pick);
3734    let initial_text = Some(initial_text.clone()).filter(|s| !s.as_ref().is_empty());
3735
3736    SettingsEditor::new()
3737        .when_some(initial_text, |editor, text| {
3738            editor.with_initial_text(text.into())
3739        })
3740        .when_some(
3741            metadata.and_then(|metadata| metadata.placeholder),
3742            |editor, placeholder| editor.with_placeholder(placeholder),
3743        )
3744        .on_confirm({
3745            move |new_text, cx| {
3746                update_settings_file(file.clone(), cx, move |settings, _cx| {
3747                    *(field.pick_mut)(settings) = new_text.map(Into::into);
3748                })
3749                .log_err(); // todo(settings_ui) don't log err
3750            }
3751        })
3752        .into_any_element()
3753}
3754
3755fn render_toggle_button<B: Into<bool> + From<bool> + Copy>(
3756    field: SettingField<B>,
3757    file: SettingsUiFile,
3758    cx: &mut App,
3759) -> AnyElement {
3760    let (_, &value) = SettingsStore::global(cx).get_value_from_file(file.to_settings(), field.pick);
3761
3762    let toggle_state = if value.into() {
3763        ToggleState::Selected
3764    } else {
3765        ToggleState::Unselected
3766    };
3767
3768    Switch::new("toggle_button", toggle_state)
3769        .color(ui::SwitchColor::Accent)
3770        .on_click({
3771            move |state, _window, cx| {
3772                let state = *state == ui::ToggleState::Selected;
3773                update_settings_file(file.clone(), cx, move |settings, _cx| {
3774                    *(field.pick_mut)(settings) = Some(state.into());
3775                })
3776                .log_err(); // todo(settings_ui) don't log err
3777            }
3778        })
3779        .color(SwitchColor::Accent)
3780        .into_any_element()
3781}
3782
3783fn render_font_picker(
3784    field: SettingField<settings::FontFamilyName>,
3785    file: SettingsUiFile,
3786    window: &mut Window,
3787    cx: &mut App,
3788) -> AnyElement {
3789    let current_value = SettingsStore::global(cx)
3790        .get_value_from_file(file.to_settings(), field.pick)
3791        .1
3792        .clone();
3793
3794    let font_picker = cx.new(|cx| {
3795        ui_input::font_picker(
3796            current_value.clone().into(),
3797            move |font_name, cx| {
3798                update_settings_file(file.clone(), cx, move |settings, _cx| {
3799                    *(field.pick_mut)(settings) = Some(font_name.into());
3800                })
3801                .log_err(); // todo(settings_ui) don't log err
3802            },
3803            window,
3804            cx,
3805        )
3806    });
3807
3808    div()
3809        .child(
3810            PopoverMenu::new("font-picker")
3811                .menu(move |_window, _cx| Some(font_picker.clone()))
3812                .trigger(
3813                    ButtonLike::new("font-family-button")
3814                        .style(ButtonStyle::Outlined)
3815                        .size(ButtonSize::Medium)
3816                        .full_width()
3817                        .child(
3818                            h_flex()
3819                                .w_full()
3820                                .justify_between()
3821                                .child(Label::new(current_value))
3822                                .child(
3823                                    Icon::new(IconName::ChevronUpDown)
3824                                        .color(Color::Muted)
3825                                        .size(IconSize::XSmall),
3826                                ),
3827                        ),
3828                )
3829                .full_width(true)
3830                .anchor(gpui::Corner::TopLeft)
3831                .offset(gpui::Point {
3832                    x: px(0.0),
3833                    y: px(4.0),
3834                })
3835                .with_handle(ui::PopoverMenuHandle::default()),
3836        )
3837        .into_any_element()
3838}
3839
3840fn render_numeric_stepper<T: NumericStepperType + Send + Sync>(
3841    field: SettingField<T>,
3842    file: SettingsUiFile,
3843    window: &mut Window,
3844    cx: &mut App,
3845) -> AnyElement {
3846    let (_, &value) = SettingsStore::global(cx).get_value_from_file(file.to_settings(), field.pick);
3847
3848    NumericStepper::new("numeric_stepper", value, window, cx)
3849        .on_change({
3850            move |value, _window, cx| {
3851                let value = *value;
3852                update_settings_file(file.clone(), cx, move |settings, _cx| {
3853                    *(field.pick_mut)(settings) = Some(value);
3854                })
3855                .log_err(); // todo(settings_ui) don't log err
3856            }
3857        })
3858        .style(NumericStepperStyle::Outlined)
3859        .into_any_element()
3860}
3861
3862fn render_dropdown<T>(
3863    field: SettingField<T>,
3864    file: SettingsUiFile,
3865    window: &mut Window,
3866    cx: &mut App,
3867) -> AnyElement
3868where
3869    T: strum::VariantArray + strum::VariantNames + Copy + PartialEq + Send + Sync + 'static,
3870{
3871    let variants = || -> &'static [T] { <T as strum::VariantArray>::VARIANTS };
3872    let labels = || -> &'static [&'static str] { <T as strum::VariantNames>::VARIANTS };
3873
3874    let (_, &current_value) =
3875        SettingsStore::global(cx).get_value_from_file(file.to_settings(), field.pick);
3876
3877    let current_value_label =
3878        labels()[variants().iter().position(|v| *v == current_value).unwrap()];
3879
3880    DropdownMenu::new(
3881        "dropdown",
3882        current_value_label,
3883        ContextMenu::build(window, cx, move |mut menu, _, _| {
3884            for (&value, &label) in std::iter::zip(variants(), labels()) {
3885                let file = file.clone();
3886                menu = menu.toggleable_entry(
3887                    label,
3888                    value == current_value,
3889                    IconPosition::Start,
3890                    None,
3891                    move |_, cx| {
3892                        if value == current_value {
3893                            return;
3894                        }
3895                        update_settings_file(file.clone(), cx, move |settings, _cx| {
3896                            *(field.pick_mut)(settings) = Some(value);
3897                        })
3898                        .log_err(); // todo(settings_ui) don't log err
3899                    },
3900                );
3901            }
3902            menu
3903        }),
3904    )
3905    .trigger_size(ButtonSize::Medium)
3906    .style(DropdownStyle::Outlined)
3907    .offset(gpui::Point {
3908        x: px(0.0),
3909        y: px(2.0),
3910    })
3911    .into_any_element()
3912}
3913
3914#[cfg(test)]
3915mod test {
3916
3917    use super::*;
3918
3919    impl SettingsWindow {
3920        fn navbar_entry(&self) -> usize {
3921            self.navbar_entry
3922        }
3923
3924        fn new_builder(window: &mut Window, cx: &mut Context<Self>) -> Self {
3925            let mut this = Self::new(window, cx);
3926            this.navbar_entries.clear();
3927            this.pages.clear();
3928            this
3929        }
3930
3931        fn build(mut self) -> Self {
3932            self.build_search_matches();
3933            self.build_navbar();
3934            self
3935        }
3936
3937        fn add_page(
3938            mut self,
3939            title: &'static str,
3940            build_page: impl Fn(SettingsPage) -> SettingsPage,
3941        ) -> Self {
3942            let page = SettingsPage {
3943                title,
3944                items: Vec::default(),
3945            };
3946
3947            self.pages.push(build_page(page));
3948            self
3949        }
3950
3951        fn search(&mut self, search_query: &str, window: &mut Window, cx: &mut Context<Self>) {
3952            self.search_task.take();
3953            self.search_bar.update(cx, |editor, cx| {
3954                editor.set_text(search_query, window, cx);
3955            });
3956            self.update_matches(cx);
3957        }
3958
3959        fn assert_search_results(&self, other: &Self) {
3960            // page index could be different because of filtered out pages
3961            #[derive(Debug, PartialEq)]
3962            struct EntryMinimal {
3963                is_root: bool,
3964                title: &'static str,
3965            }
3966            pretty_assertions::assert_eq!(
3967                other
3968                    .visible_navbar_entries()
3969                    .map(|(_, entry)| EntryMinimal {
3970                        is_root: entry.is_root,
3971                        title: entry.title,
3972                    })
3973                    .collect::<Vec<_>>(),
3974                self.visible_navbar_entries()
3975                    .map(|(_, entry)| EntryMinimal {
3976                        is_root: entry.is_root,
3977                        title: entry.title,
3978                    })
3979                    .collect::<Vec<_>>(),
3980            );
3981            assert_eq!(
3982                self.current_page().items.iter().collect::<Vec<_>>(),
3983                other.page_items().collect::<Vec<_>>()
3984            );
3985        }
3986    }
3987
3988    impl SettingsPage {
3989        fn item(mut self, item: SettingsPageItem) -> Self {
3990            self.items.push(item);
3991            self
3992        }
3993    }
3994
3995    impl SettingsPageItem {
3996        fn basic_item(title: &'static str, description: &'static str) -> Self {
3997            SettingsPageItem::SettingItem(SettingItem {
3998                title,
3999                description,
4000                field: Box::new(SettingField {
4001                    pick: |settings_content| &settings_content.auto_update,
4002                    pick_mut: |settings_content| &mut settings_content.auto_update,
4003                }),
4004                metadata: None,
4005            })
4006        }
4007    }
4008
4009    fn register_settings(cx: &mut App) {
4010        settings::init(cx);
4011        theme::init(theme::LoadThemes::JustBase, cx);
4012        workspace::init_settings(cx);
4013        project::Project::init_settings(cx);
4014        language::init(cx);
4015        editor::init(cx);
4016        menu::init();
4017    }
4018
4019    fn parse(input: &'static str, window: &mut Window, cx: &mut App) -> SettingsWindow {
4020        let mut pages: Vec<SettingsPage> = Vec::new();
4021        let mut expanded_pages = Vec::new();
4022        let mut selected_idx = None;
4023        let mut index = 0;
4024        let mut in_expanded_section = false;
4025
4026        for mut line in input
4027            .lines()
4028            .map(|line| line.trim())
4029            .filter(|line| !line.is_empty())
4030        {
4031            if let Some(pre) = line.strip_suffix('*') {
4032                assert!(selected_idx.is_none(), "Only one selected entry allowed");
4033                selected_idx = Some(index);
4034                line = pre;
4035            }
4036            let (kind, title) = line.split_once(" ").unwrap();
4037            assert_eq!(kind.len(), 1);
4038            let kind = kind.chars().next().unwrap();
4039            if kind == 'v' {
4040                let page_idx = pages.len();
4041                expanded_pages.push(page_idx);
4042                pages.push(SettingsPage {
4043                    title,
4044                    items: vec![],
4045                });
4046                index += 1;
4047                in_expanded_section = true;
4048            } else if kind == '>' {
4049                pages.push(SettingsPage {
4050                    title,
4051                    items: vec![],
4052                });
4053                index += 1;
4054                in_expanded_section = false;
4055            } else if kind == '-' {
4056                pages
4057                    .last_mut()
4058                    .unwrap()
4059                    .items
4060                    .push(SettingsPageItem::SectionHeader(title));
4061                if selected_idx == Some(index) && !in_expanded_section {
4062                    panic!("Items in unexpanded sections cannot be selected");
4063                }
4064                index += 1;
4065            } else {
4066                panic!(
4067                    "Entries must start with one of 'v', '>', or '-'\n line: {}",
4068                    line
4069                );
4070            }
4071        }
4072
4073        let mut settings_window = SettingsWindow {
4074            files: Vec::default(),
4075            current_file: crate::SettingsUiFile::User,
4076            pages,
4077            search_bar: cx.new(|cx| Editor::single_line(window, cx)),
4078            navbar_entry: selected_idx.expect("Must have a selected navbar entry"),
4079            navbar_entries: Vec::default(),
4080            list_handle: UniformListScrollHandle::default(),
4081            search_matches: vec![],
4082            search_task: None,
4083            sub_page_stack: vec![],
4084            scroll_handle: ScrollHandle::new(),
4085        };
4086
4087        settings_window.build_search_matches();
4088        settings_window.build_navbar();
4089        for expanded_page_index in expanded_pages {
4090            for entry in &mut settings_window.navbar_entries {
4091                if entry.page_index == expanded_page_index && entry.is_root {
4092                    entry.expanded = true;
4093                }
4094            }
4095        }
4096        settings_window
4097    }
4098
4099    #[track_caller]
4100    fn check_navbar_toggle(
4101        before: &'static str,
4102        toggle_page: &'static str,
4103        after: &'static str,
4104        window: &mut Window,
4105        cx: &mut App,
4106    ) {
4107        let mut settings_window = parse(before, window, cx);
4108        let toggle_page_idx = settings_window
4109            .pages
4110            .iter()
4111            .position(|page| page.title == toggle_page)
4112            .expect("page not found");
4113        let toggle_idx = settings_window
4114            .navbar_entries
4115            .iter()
4116            .position(|entry| entry.page_index == toggle_page_idx)
4117            .expect("page not found");
4118        settings_window.toggle_navbar_entry(toggle_idx);
4119
4120        let expected_settings_window = parse(after, window, cx);
4121
4122        pretty_assertions::assert_eq!(
4123            settings_window
4124                .visible_navbar_entries()
4125                .map(|(_, entry)| entry)
4126                .collect::<Vec<_>>(),
4127            expected_settings_window
4128                .visible_navbar_entries()
4129                .map(|(_, entry)| entry)
4130                .collect::<Vec<_>>(),
4131        );
4132        pretty_assertions::assert_eq!(
4133            settings_window.navbar_entries[settings_window.navbar_entry()],
4134            expected_settings_window.navbar_entries[expected_settings_window.navbar_entry()],
4135        );
4136    }
4137
4138    macro_rules! check_navbar_toggle {
4139        ($name:ident, before: $before:expr, toggle_page: $toggle_page:expr, after: $after:expr) => {
4140            #[gpui::test]
4141            fn $name(cx: &mut gpui::TestAppContext) {
4142                let window = cx.add_empty_window();
4143                window.update(|window, cx| {
4144                    register_settings(cx);
4145                    check_navbar_toggle($before, $toggle_page, $after, window, cx);
4146                });
4147            }
4148        };
4149    }
4150
4151    check_navbar_toggle!(
4152        navbar_basic_open,
4153        before: r"
4154        v General
4155        - General
4156        - Privacy*
4157        v Project
4158        - Project Settings
4159        ",
4160        toggle_page: "General",
4161        after: r"
4162        > General*
4163        v Project
4164        - Project Settings
4165        "
4166    );
4167
4168    check_navbar_toggle!(
4169        navbar_basic_close,
4170        before: r"
4171        > General*
4172        - General
4173        - Privacy
4174        v Project
4175        - Project Settings
4176        ",
4177        toggle_page: "General",
4178        after: r"
4179        v General*
4180        - General
4181        - Privacy
4182        v Project
4183        - Project Settings
4184        "
4185    );
4186
4187    check_navbar_toggle!(
4188        navbar_basic_second_root_entry_close,
4189        before: r"
4190        > General
4191        - General
4192        - Privacy
4193        v Project
4194        - Project Settings*
4195        ",
4196        toggle_page: "Project",
4197        after: r"
4198        > General
4199        > Project*
4200        "
4201    );
4202
4203    check_navbar_toggle!(
4204        navbar_toggle_subroot,
4205        before: r"
4206        v General Page
4207        - General
4208        - Privacy
4209        v Project
4210        - Worktree Settings Content*
4211        v AI
4212        - General
4213        > Appearance & Behavior
4214        ",
4215        toggle_page: "Project",
4216        after: r"
4217        v General Page
4218        - General
4219        - Privacy
4220        > Project*
4221        v AI
4222        - General
4223        > Appearance & Behavior
4224        "
4225    );
4226
4227    check_navbar_toggle!(
4228        navbar_toggle_close_propagates_selected_index,
4229        before: r"
4230        v General Page
4231        - General
4232        - Privacy
4233        v Project
4234        - Worktree Settings Content
4235        v AI
4236        - General*
4237        > Appearance & Behavior
4238        ",
4239        toggle_page: "General Page",
4240        after: r"
4241        > General Page
4242        v Project
4243        - Worktree Settings Content
4244        v AI
4245        - General*
4246        > Appearance & Behavior
4247        "
4248    );
4249
4250    check_navbar_toggle!(
4251        navbar_toggle_expand_propagates_selected_index,
4252        before: r"
4253        > General Page
4254        - General
4255        - Privacy
4256        v Project
4257        - Worktree Settings Content
4258        v AI
4259        - General*
4260        > Appearance & Behavior
4261        ",
4262        toggle_page: "General Page",
4263        after: r"
4264        v General Page
4265        - General
4266        - Privacy
4267        v Project
4268        - Worktree Settings Content
4269        v AI
4270        - General*
4271        > Appearance & Behavior
4272        "
4273    );
4274
4275    #[gpui::test]
4276    fn test_basic_search(cx: &mut gpui::TestAppContext) {
4277        let cx = cx.add_empty_window();
4278        let (actual, expected) = cx.update(|window, cx| {
4279            register_settings(cx);
4280
4281            let expected = cx.new(|cx| {
4282                SettingsWindow::new_builder(window, cx)
4283                    .add_page("General", |page| {
4284                        page.item(SettingsPageItem::SectionHeader("General settings"))
4285                            .item(SettingsPageItem::basic_item("test title", "General test"))
4286                    })
4287                    .build()
4288            });
4289
4290            let actual = cx.new(|cx| {
4291                SettingsWindow::new_builder(window, cx)
4292                    .add_page("General", |page| {
4293                        page.item(SettingsPageItem::SectionHeader("General settings"))
4294                            .item(SettingsPageItem::basic_item("test title", "General test"))
4295                    })
4296                    .add_page("Theme", |page| {
4297                        page.item(SettingsPageItem::SectionHeader("Theme settings"))
4298                    })
4299                    .build()
4300            });
4301
4302            actual.update(cx, |settings, cx| settings.search("gen", window, cx));
4303
4304            (actual, expected)
4305        });
4306
4307        cx.cx.run_until_parked();
4308
4309        cx.update(|_window, cx| {
4310            let expected = expected.read(cx);
4311            let actual = actual.read(cx);
4312            expected.assert_search_results(&actual);
4313        })
4314    }
4315
4316    #[gpui::test]
4317    fn test_search_render_page_with_filtered_out_navbar_entries(cx: &mut gpui::TestAppContext) {
4318        let cx = cx.add_empty_window();
4319        let (actual, expected) = cx.update(|window, cx| {
4320            register_settings(cx);
4321
4322            let actual = cx.new(|cx| {
4323                SettingsWindow::new_builder(window, cx)
4324                    .add_page("General", |page| {
4325                        page.item(SettingsPageItem::SectionHeader("General settings"))
4326                            .item(SettingsPageItem::basic_item(
4327                                "Confirm Quit",
4328                                "Whether to confirm before quitting Zed",
4329                            ))
4330                            .item(SettingsPageItem::basic_item(
4331                                "Auto Update",
4332                                "Automatically update Zed",
4333                            ))
4334                    })
4335                    .add_page("AI", |page| {
4336                        page.item(SettingsPageItem::basic_item(
4337                            "Disable AI",
4338                            "Whether to disable all AI features in Zed",
4339                        ))
4340                    })
4341                    .add_page("Appearance & Behavior", |page| {
4342                        page.item(SettingsPageItem::SectionHeader("Cursor")).item(
4343                            SettingsPageItem::basic_item(
4344                                "Cursor Shape",
4345                                "Cursor shape for the editor",
4346                            ),
4347                        )
4348                    })
4349                    .build()
4350            });
4351
4352            let expected = cx.new(|cx| {
4353                SettingsWindow::new_builder(window, cx)
4354                    .add_page("Appearance & Behavior", |page| {
4355                        page.item(SettingsPageItem::SectionHeader("Cursor")).item(
4356                            SettingsPageItem::basic_item(
4357                                "Cursor Shape",
4358                                "Cursor shape for the editor",
4359                            ),
4360                        )
4361                    })
4362                    .build()
4363            });
4364
4365            actual.update(cx, |settings, cx| settings.search("cursor", window, cx));
4366
4367            (actual, expected)
4368        });
4369
4370        cx.cx.run_until_parked();
4371
4372        cx.update(|_window, cx| {
4373            let expected = expected.read(cx);
4374            let actual = actual.read(cx);
4375            expected.assert_search_results(&actual);
4376        })
4377    }
4378}