settings_ui.rs

   1mod components;
   2mod page_data;
   3mod pages;
   4
   5use anyhow::Result;
   6use editor::{Editor, EditorEvent};
   7use fuzzy::StringMatchCandidate;
   8use gpui::{
   9    Action, App, ClipboardItem, DEFAULT_ADDITIONAL_WINDOW_SIZE, Div, Entity, FocusHandle,
  10    Focusable, Global, KeyContext, ListState, ReadGlobal as _, ScrollHandle, Stateful,
  11    Subscription, Task, TitlebarOptions, UniformListScrollHandle, Window, WindowBounds,
  12    WindowHandle, WindowOptions, actions, div, list, point, prelude::*, px, uniform_list,
  13};
  14use project::{Project, WorktreeId};
  15use release_channel::ReleaseChannel;
  16use schemars::JsonSchema;
  17use serde::Deserialize;
  18use settings::{
  19    IntoGpui, Settings, SettingsContent, SettingsStore, initial_project_settings_content,
  20};
  21use std::{
  22    any::{Any, TypeId, type_name},
  23    cell::RefCell,
  24    collections::{HashMap, HashSet},
  25    num::{NonZero, NonZeroU32},
  26    ops::Range,
  27    rc::Rc,
  28    sync::{Arc, LazyLock, RwLock},
  29    time::Duration,
  30};
  31use theme::ThemeSettings;
  32use title_bar::platform_title_bar::PlatformTitleBar;
  33use ui::{
  34    Banner, ContextMenu, Divider, DropdownMenu, DropdownStyle, IconButtonShape, KeyBinding,
  35    KeybindingHint, PopoverMenu, Scrollbars, Switch, Tooltip, TreeViewItem, WithScrollbar,
  36    prelude::*,
  37};
  38use ui_input::{NumberField, NumberFieldMode, NumberFieldType};
  39use util::{ResultExt as _, paths::PathStyle, rel_path::RelPath};
  40use workspace::{AppState, OpenOptions, OpenVisible, Workspace, client_side_decorations};
  41use zed_actions::{OpenProjectSettings, OpenSettings, OpenSettingsAt};
  42
  43use crate::components::{
  44    EnumVariantDropdown, SettingsInputField, SettingsSectionHeader, font_picker, icon_theme_picker,
  45    theme_picker,
  46};
  47
  48const NAVBAR_CONTAINER_TAB_INDEX: isize = 0;
  49const NAVBAR_GROUP_TAB_INDEX: isize = 1;
  50
  51const HEADER_CONTAINER_TAB_INDEX: isize = 2;
  52const HEADER_GROUP_TAB_INDEX: isize = 3;
  53
  54const CONTENT_CONTAINER_TAB_INDEX: isize = 4;
  55const CONTENT_GROUP_TAB_INDEX: isize = 5;
  56
  57actions!(
  58    settings_editor,
  59    [
  60        /// Minimizes the settings UI window.
  61        Minimize,
  62        /// Toggles focus between the navbar and the main content.
  63        ToggleFocusNav,
  64        /// Expands the navigation entry.
  65        ExpandNavEntry,
  66        /// Collapses the navigation entry.
  67        CollapseNavEntry,
  68        /// Focuses the next file in the file list.
  69        FocusNextFile,
  70        /// Focuses the previous file in the file list.
  71        FocusPreviousFile,
  72        /// Opens an editor for the current file
  73        OpenCurrentFile,
  74        /// Focuses the previous root navigation entry.
  75        FocusPreviousRootNavEntry,
  76        /// Focuses the next root navigation entry.
  77        FocusNextRootNavEntry,
  78        /// Focuses the first navigation entry.
  79        FocusFirstNavEntry,
  80        /// Focuses the last navigation entry.
  81        FocusLastNavEntry,
  82        /// Focuses and opens the next navigation entry without moving focus to content.
  83        FocusNextNavEntry,
  84        /// Focuses and opens the previous navigation entry without moving focus to content.
  85        FocusPreviousNavEntry
  86    ]
  87);
  88
  89#[derive(Action, PartialEq, Eq, Clone, Copy, Debug, JsonSchema, Deserialize)]
  90#[action(namespace = settings_editor)]
  91struct FocusFile(pub u32);
  92
  93struct SettingField<T: 'static> {
  94    pick: fn(&SettingsContent) -> Option<&T>,
  95    write: fn(&mut SettingsContent, Option<T>),
  96
  97    /// A json-path-like string that gives a unique-ish string that identifies
  98    /// where in the JSON the setting is defined.
  99    ///
 100    /// The syntax is `jq`-like, but modified slightly to be URL-safe (and
 101    /// without the leading dot), e.g. `foo.bar`.
 102    ///
 103    /// They are URL-safe (this is important since links are the main use-case
 104    /// for these paths).
 105    ///
 106    /// There are a couple of special cases:
 107    /// - discrimminants are represented with a trailing `$`, for example
 108    /// `terminal.working_directory$`. This is to distinguish the discrimminant
 109    /// setting (i.e. the setting that changes whether the value is a string or
 110    /// an object) from the setting in the case that it is a string.
 111    /// - language-specific settings begin `languages.$(language)`. Links
 112    /// targeting these settings should take the form `languages/Rust/...`, for
 113    /// example, but are not currently supported.
 114    json_path: Option<&'static str>,
 115}
 116
 117impl<T: 'static> Clone for SettingField<T> {
 118    fn clone(&self) -> Self {
 119        *self
 120    }
 121}
 122
 123// manual impl because derive puts a Copy bound on T, which is inaccurate in our case
 124impl<T: 'static> Copy for SettingField<T> {}
 125
 126/// Helper for unimplemented settings, used in combination with `SettingField::unimplemented`
 127/// to keep the setting around in the UI with valid pick and write implementations, but don't actually try to render it.
 128/// TODO(settings_ui): In non-dev builds (`#[cfg(not(debug_assertions))]`) make this render as edit-in-json
 129#[derive(Clone, Copy)]
 130struct UnimplementedSettingField;
 131
 132impl PartialEq for UnimplementedSettingField {
 133    fn eq(&self, _other: &Self) -> bool {
 134        true
 135    }
 136}
 137
 138impl<T: 'static> SettingField<T> {
 139    /// Helper for settings with types that are not yet implemented.
 140    #[allow(unused)]
 141    fn unimplemented(self) -> SettingField<UnimplementedSettingField> {
 142        SettingField {
 143            pick: |_| Some(&UnimplementedSettingField),
 144            write: |_, _| unreachable!(),
 145            json_path: self.json_path,
 146        }
 147    }
 148}
 149
 150trait AnySettingField {
 151    fn as_any(&self) -> &dyn Any;
 152    fn type_name(&self) -> &'static str;
 153    fn type_id(&self) -> TypeId;
 154    // Returns the file this value was set in and true, or File::Default and false to indicate it was not found in any file (missing default)
 155    fn file_set_in(&self, file: SettingsUiFile, cx: &App) -> (settings::SettingsFile, bool);
 156    fn reset_to_default_fn(
 157        &self,
 158        current_file: &SettingsUiFile,
 159        file_set_in: &settings::SettingsFile,
 160        cx: &App,
 161    ) -> Option<Box<dyn Fn(&mut App)>>;
 162
 163    fn json_path(&self) -> Option<&'static str>;
 164}
 165
 166impl<T: PartialEq + Clone + Send + Sync + 'static> AnySettingField for SettingField<T> {
 167    fn as_any(&self) -> &dyn Any {
 168        self
 169    }
 170
 171    fn type_name(&self) -> &'static str {
 172        type_name::<T>()
 173    }
 174
 175    fn type_id(&self) -> TypeId {
 176        TypeId::of::<T>()
 177    }
 178
 179    fn file_set_in(&self, file: SettingsUiFile, cx: &App) -> (settings::SettingsFile, bool) {
 180        let (file, value) = cx
 181            .global::<SettingsStore>()
 182            .get_value_from_file(file.to_settings(), self.pick);
 183        return (file, value.is_some());
 184    }
 185
 186    fn reset_to_default_fn(
 187        &self,
 188        current_file: &SettingsUiFile,
 189        file_set_in: &settings::SettingsFile,
 190        cx: &App,
 191    ) -> Option<Box<dyn Fn(&mut App)>> {
 192        if file_set_in == &settings::SettingsFile::Default {
 193            return None;
 194        }
 195        if file_set_in != &current_file.to_settings() {
 196            return None;
 197        }
 198        let this = *self;
 199        let store = SettingsStore::global(cx);
 200        let default_value = (this.pick)(store.raw_default_settings());
 201        let is_default = store
 202            .get_content_for_file(file_set_in.clone())
 203            .map_or(None, this.pick)
 204            == default_value;
 205        if is_default {
 206            return None;
 207        }
 208        let current_file = current_file.clone();
 209
 210        return Some(Box::new(move |cx| {
 211            let store = SettingsStore::global(cx);
 212            let default_value = (this.pick)(store.raw_default_settings());
 213            let is_set_somewhere_other_than_default = store
 214                .get_value_up_to_file(current_file.to_settings(), this.pick)
 215                .0
 216                != settings::SettingsFile::Default;
 217            let value_to_set = if is_set_somewhere_other_than_default {
 218                default_value.cloned()
 219            } else {
 220                None
 221            };
 222            update_settings_file(current_file.clone(), None, cx, move |settings, _| {
 223                (this.write)(settings, value_to_set);
 224            })
 225            // todo(settings_ui): Don't log err
 226            .log_err();
 227        }));
 228    }
 229
 230    fn json_path(&self) -> Option<&'static str> {
 231        self.json_path
 232    }
 233}
 234
 235#[derive(Default, Clone)]
 236struct SettingFieldRenderer {
 237    renderers: Rc<
 238        RefCell<
 239            HashMap<
 240                TypeId,
 241                Box<
 242                    dyn Fn(
 243                        &SettingsWindow,
 244                        &SettingItem,
 245                        SettingsUiFile,
 246                        Option<&SettingsFieldMetadata>,
 247                        bool,
 248                        &mut Window,
 249                        &mut Context<SettingsWindow>,
 250                    ) -> Stateful<Div>,
 251                >,
 252            >,
 253        >,
 254    >,
 255}
 256
 257impl Global for SettingFieldRenderer {}
 258
 259impl SettingFieldRenderer {
 260    fn add_basic_renderer<T: 'static>(
 261        &mut self,
 262        render_control: impl Fn(
 263            SettingField<T>,
 264            SettingsUiFile,
 265            Option<&SettingsFieldMetadata>,
 266            &mut Window,
 267            &mut App,
 268        ) -> AnyElement
 269        + 'static,
 270    ) -> &mut Self {
 271        self.add_renderer(
 272            move |settings_window: &SettingsWindow,
 273                  item: &SettingItem,
 274                  field: SettingField<T>,
 275                  settings_file: SettingsUiFile,
 276                  metadata: Option<&SettingsFieldMetadata>,
 277                  sub_field: bool,
 278                  window: &mut Window,
 279                  cx: &mut Context<SettingsWindow>| {
 280                render_settings_item(
 281                    settings_window,
 282                    item,
 283                    settings_file.clone(),
 284                    render_control(field, settings_file, metadata, window, cx),
 285                    sub_field,
 286                    cx,
 287                )
 288            },
 289        )
 290    }
 291
 292    fn add_renderer<T: 'static>(
 293        &mut self,
 294        renderer: impl Fn(
 295            &SettingsWindow,
 296            &SettingItem,
 297            SettingField<T>,
 298            SettingsUiFile,
 299            Option<&SettingsFieldMetadata>,
 300            bool,
 301            &mut Window,
 302            &mut Context<SettingsWindow>,
 303        ) -> Stateful<Div>
 304        + 'static,
 305    ) -> &mut Self {
 306        let key = TypeId::of::<T>();
 307        let renderer = Box::new(
 308            move |settings_window: &SettingsWindow,
 309                  item: &SettingItem,
 310                  settings_file: SettingsUiFile,
 311                  metadata: Option<&SettingsFieldMetadata>,
 312                  sub_field: bool,
 313                  window: &mut Window,
 314                  cx: &mut Context<SettingsWindow>| {
 315                let field = *item
 316                    .field
 317                    .as_ref()
 318                    .as_any()
 319                    .downcast_ref::<SettingField<T>>()
 320                    .unwrap();
 321                renderer(
 322                    settings_window,
 323                    item,
 324                    field,
 325                    settings_file,
 326                    metadata,
 327                    sub_field,
 328                    window,
 329                    cx,
 330                )
 331            },
 332        );
 333        self.renderers.borrow_mut().insert(key, renderer);
 334        self
 335    }
 336}
 337
 338struct NonFocusableHandle {
 339    handle: FocusHandle,
 340    _subscription: Subscription,
 341}
 342
 343impl NonFocusableHandle {
 344    fn new(tab_index: isize, tab_stop: bool, window: &mut Window, cx: &mut App) -> Entity<Self> {
 345        let handle = cx.focus_handle().tab_index(tab_index).tab_stop(tab_stop);
 346        Self::from_handle(handle, window, cx)
 347    }
 348
 349    fn from_handle(handle: FocusHandle, window: &mut Window, cx: &mut App) -> Entity<Self> {
 350        cx.new(|cx| {
 351            let _subscription = cx.on_focus(&handle, window, {
 352                move |_, window, cx| {
 353                    window.focus_next(cx);
 354                }
 355            });
 356            Self {
 357                handle,
 358                _subscription,
 359            }
 360        })
 361    }
 362}
 363
 364impl Focusable for NonFocusableHandle {
 365    fn focus_handle(&self, _: &App) -> FocusHandle {
 366        self.handle.clone()
 367    }
 368}
 369
 370#[derive(Default)]
 371struct SettingsFieldMetadata {
 372    placeholder: Option<&'static str>,
 373    should_do_titlecase: Option<bool>,
 374}
 375
 376pub fn init(cx: &mut App) {
 377    init_renderers(cx);
 378
 379    cx.observe_new(|workspace: &mut workspace::Workspace, _, _| {
 380        workspace
 381            .register_action(
 382                |workspace, OpenSettingsAt { path }: &OpenSettingsAt, window, cx| {
 383                    let window_handle = window
 384                        .window_handle()
 385                        .downcast::<Workspace>()
 386                        .expect("Workspaces are root Windows");
 387                    open_settings_editor(workspace, Some(&path), false, window_handle, cx);
 388                },
 389            )
 390            .register_action(|workspace, _: &OpenSettings, window, cx| {
 391                let window_handle = window
 392                    .window_handle()
 393                    .downcast::<Workspace>()
 394                    .expect("Workspaces are root Windows");
 395                open_settings_editor(workspace, None, false, window_handle, cx);
 396            })
 397            .register_action(|workspace, _: &OpenProjectSettings, window, cx| {
 398                let window_handle = window
 399                    .window_handle()
 400                    .downcast::<Workspace>()
 401                    .expect("Workspaces are root Windows");
 402                open_settings_editor(workspace, None, true, window_handle, cx);
 403            });
 404    })
 405    .detach();
 406}
 407
 408fn init_renderers(cx: &mut App) {
 409    cx.default_global::<SettingFieldRenderer>()
 410        .add_renderer::<UnimplementedSettingField>(
 411            |settings_window, item, _, settings_file, _, sub_field, _, cx| {
 412                render_settings_item(
 413                    settings_window,
 414                    item,
 415                    settings_file,
 416                    Button::new("open-in-settings-file", "Edit in settings.json")
 417                        .style(ButtonStyle::Outlined)
 418                        .size(ButtonSize::Medium)
 419                        .tab_index(0_isize)
 420                        .tooltip(Tooltip::for_action_title_in(
 421                            "Edit in settings.json",
 422                            &OpenCurrentFile,
 423                            &settings_window.focus_handle,
 424                        ))
 425                        .on_click(cx.listener(|this, _, window, cx| {
 426                            this.open_current_settings_file(window, cx);
 427                        }))
 428                        .into_any_element(),
 429                    sub_field,
 430                    cx,
 431                )
 432            },
 433        )
 434        .add_basic_renderer::<bool>(render_toggle_button)
 435        .add_basic_renderer::<String>(render_text_field)
 436        .add_basic_renderer::<SharedString>(render_text_field)
 437        .add_basic_renderer::<settings::SaturatingBool>(render_toggle_button)
 438        .add_basic_renderer::<settings::CursorShape>(render_dropdown)
 439        .add_basic_renderer::<settings::RestoreOnStartupBehavior>(render_dropdown)
 440        .add_basic_renderer::<settings::BottomDockLayout>(render_dropdown)
 441        .add_basic_renderer::<settings::OnLastWindowClosed>(render_dropdown)
 442        .add_basic_renderer::<settings::CloseWindowWhenNoItems>(render_dropdown)
 443        .add_basic_renderer::<settings::TextRenderingMode>(render_dropdown)
 444        .add_basic_renderer::<settings::FontFamilyName>(render_font_picker)
 445        .add_basic_renderer::<settings::BaseKeymapContent>(render_dropdown)
 446        .add_basic_renderer::<settings::MultiCursorModifier>(render_dropdown)
 447        .add_basic_renderer::<settings::HideMouseMode>(render_dropdown)
 448        .add_basic_renderer::<settings::CurrentLineHighlight>(render_dropdown)
 449        .add_basic_renderer::<settings::ShowWhitespaceSetting>(render_dropdown)
 450        .add_basic_renderer::<settings::SoftWrap>(render_dropdown)
 451        .add_basic_renderer::<settings::ScrollBeyondLastLine>(render_dropdown)
 452        .add_basic_renderer::<settings::SnippetSortOrder>(render_dropdown)
 453        .add_basic_renderer::<settings::ClosePosition>(render_dropdown)
 454        .add_basic_renderer::<settings::DockSide>(render_dropdown)
 455        .add_basic_renderer::<settings::TerminalDockPosition>(render_dropdown)
 456        .add_basic_renderer::<settings::DockPosition>(render_dropdown)
 457        .add_basic_renderer::<settings::GitGutterSetting>(render_dropdown)
 458        .add_basic_renderer::<settings::GitHunkStyleSetting>(render_dropdown)
 459        .add_basic_renderer::<settings::GitPathStyle>(render_dropdown)
 460        .add_basic_renderer::<settings::DiagnosticSeverityContent>(render_dropdown)
 461        .add_basic_renderer::<settings::SeedQuerySetting>(render_dropdown)
 462        .add_basic_renderer::<settings::DoubleClickInMultibuffer>(render_dropdown)
 463        .add_basic_renderer::<settings::GoToDefinitionFallback>(render_dropdown)
 464        .add_basic_renderer::<settings::ActivateOnClose>(render_dropdown)
 465        .add_basic_renderer::<settings::ShowDiagnostics>(render_dropdown)
 466        .add_basic_renderer::<settings::ShowCloseButton>(render_dropdown)
 467        .add_basic_renderer::<settings::ProjectPanelEntrySpacing>(render_dropdown)
 468        .add_basic_renderer::<settings::ProjectPanelSortMode>(render_dropdown)
 469        .add_basic_renderer::<settings::RewrapBehavior>(render_dropdown)
 470        .add_basic_renderer::<settings::FormatOnSave>(render_dropdown)
 471        .add_basic_renderer::<settings::IndentGuideColoring>(render_dropdown)
 472        .add_basic_renderer::<settings::IndentGuideBackgroundColoring>(render_dropdown)
 473        .add_basic_renderer::<settings::FileFinderWidthContent>(render_dropdown)
 474        .add_basic_renderer::<settings::ShowDiagnostics>(render_dropdown)
 475        .add_basic_renderer::<settings::WordsCompletionMode>(render_dropdown)
 476        .add_basic_renderer::<settings::LspInsertMode>(render_dropdown)
 477        .add_basic_renderer::<settings::CompletionDetailAlignment>(render_dropdown)
 478        .add_basic_renderer::<settings::AlternateScroll>(render_dropdown)
 479        .add_basic_renderer::<settings::TerminalBlink>(render_dropdown)
 480        .add_basic_renderer::<settings::CursorShapeContent>(render_dropdown)
 481        .add_basic_renderer::<f32>(render_number_field)
 482        .add_basic_renderer::<u32>(render_number_field)
 483        .add_basic_renderer::<u64>(render_number_field)
 484        .add_basic_renderer::<usize>(render_number_field)
 485        .add_basic_renderer::<NonZero<usize>>(render_number_field)
 486        .add_basic_renderer::<NonZeroU32>(render_number_field)
 487        .add_basic_renderer::<settings::CodeFade>(render_number_field)
 488        .add_basic_renderer::<settings::DelayMs>(render_number_field)
 489        .add_basic_renderer::<gpui::FontWeight>(render_number_field)
 490        .add_basic_renderer::<settings::CenteredPaddingSettings>(render_number_field)
 491        .add_basic_renderer::<settings::InactiveOpacity>(render_number_field)
 492        .add_basic_renderer::<settings::MinimumContrast>(render_number_field)
 493        .add_basic_renderer::<settings::ShowScrollbar>(render_dropdown)
 494        .add_basic_renderer::<settings::ScrollbarDiagnostics>(render_dropdown)
 495        .add_basic_renderer::<settings::ShowMinimap>(render_dropdown)
 496        .add_basic_renderer::<settings::DisplayIn>(render_dropdown)
 497        .add_basic_renderer::<settings::MinimapThumb>(render_dropdown)
 498        .add_basic_renderer::<settings::MinimapThumbBorder>(render_dropdown)
 499        .add_basic_renderer::<settings::ModeContent>(render_dropdown)
 500        .add_basic_renderer::<settings::UseSystemClipboard>(render_dropdown)
 501        .add_basic_renderer::<settings::VimInsertModeCursorShape>(render_dropdown)
 502        .add_basic_renderer::<settings::SteppingGranularity>(render_dropdown)
 503        .add_basic_renderer::<settings::NotifyWhenAgentWaiting>(render_dropdown)
 504        .add_basic_renderer::<settings::NotifyWhenAgentWaiting>(render_dropdown)
 505        .add_basic_renderer::<settings::ImageFileSizeUnit>(render_dropdown)
 506        .add_basic_renderer::<settings::StatusStyle>(render_dropdown)
 507        .add_basic_renderer::<settings::EncodingDisplayOptions>(render_dropdown)
 508        .add_basic_renderer::<settings::PaneSplitDirectionHorizontal>(render_dropdown)
 509        .add_basic_renderer::<settings::PaneSplitDirectionVertical>(render_dropdown)
 510        .add_basic_renderer::<settings::PaneSplitDirectionVertical>(render_dropdown)
 511        .add_basic_renderer::<settings::DocumentColorsRenderMode>(render_dropdown)
 512        .add_basic_renderer::<settings::ThemeSelectionDiscriminants>(render_dropdown)
 513        .add_basic_renderer::<settings::ThemeAppearanceMode>(render_dropdown)
 514        .add_basic_renderer::<settings::ThemeName>(render_theme_picker)
 515        .add_basic_renderer::<settings::IconThemeSelectionDiscriminants>(render_dropdown)
 516        .add_basic_renderer::<settings::IconThemeName>(render_icon_theme_picker)
 517        .add_basic_renderer::<settings::BufferLineHeightDiscriminants>(render_dropdown)
 518        .add_basic_renderer::<settings::AutosaveSettingDiscriminants>(render_dropdown)
 519        .add_basic_renderer::<settings::WorkingDirectoryDiscriminants>(render_dropdown)
 520        .add_basic_renderer::<settings::IncludeIgnoredContent>(render_dropdown)
 521        .add_basic_renderer::<settings::ShowIndentGuides>(render_dropdown)
 522        .add_basic_renderer::<settings::ShellDiscriminants>(render_dropdown)
 523        .add_basic_renderer::<settings::EditPredictionsMode>(render_dropdown)
 524        .add_basic_renderer::<settings::RelativeLineNumbers>(render_dropdown)
 525        .add_basic_renderer::<settings::WindowDecorations>(render_dropdown)
 526        .add_basic_renderer::<settings::FontSize>(render_editable_number_field)
 527        // please semicolon stay on next line
 528        ;
 529}
 530
 531pub fn open_settings_editor(
 532    _workspace: &mut Workspace,
 533    path: Option<&str>,
 534    open_project_settings: bool,
 535    workspace_handle: WindowHandle<Workspace>,
 536    cx: &mut App,
 537) {
 538    telemetry::event!("Settings Viewed");
 539
 540    /// Assumes a settings GUI window is already open
 541    fn open_path(
 542        path: &str,
 543        // Note: This option is unsupported right now
 544        _open_project_settings: bool,
 545        settings_window: &mut SettingsWindow,
 546        window: &mut Window,
 547        cx: &mut Context<SettingsWindow>,
 548    ) {
 549        if path.starts_with("languages.$(language)") {
 550            log::error!("language-specific settings links are not currently supported");
 551            return;
 552        }
 553
 554        settings_window.search_bar.update(cx, |editor, cx| {
 555            editor.set_text(format!("#{path}"), window, cx);
 556        });
 557        settings_window.update_matches(cx);
 558    }
 559
 560    let existing_window = cx
 561        .windows()
 562        .into_iter()
 563        .find_map(|window| window.downcast::<SettingsWindow>());
 564
 565    if let Some(existing_window) = existing_window {
 566        existing_window
 567            .update(cx, |settings_window, window, cx| {
 568                settings_window.original_window = Some(workspace_handle);
 569                window.activate_window();
 570                if let Some(path) = path {
 571                    open_path(path, open_project_settings, settings_window, window, cx);
 572                } else if open_project_settings {
 573                    if let Some(file_index) = settings_window
 574                        .files
 575                        .iter()
 576                        .position(|(file, _)| file.worktree_id().is_some())
 577                    {
 578                        settings_window.change_file(file_index, window, cx);
 579                    }
 580
 581                    cx.notify();
 582                }
 583            })
 584            .ok();
 585        return;
 586    }
 587
 588    // We have to defer this to get the workspace off the stack.
 589
 590    let path = path.map(ToOwned::to_owned);
 591    cx.defer(move |cx| {
 592        let current_rem_size: f32 = theme::ThemeSettings::get_global(cx).ui_font_size(cx).into();
 593
 594        let default_bounds = DEFAULT_ADDITIONAL_WINDOW_SIZE;
 595        let default_rem_size = 16.0;
 596        let scale_factor = current_rem_size / default_rem_size;
 597        let scaled_bounds: gpui::Size<Pixels> = default_bounds.map(|axis| axis * scale_factor);
 598
 599        let app_id = ReleaseChannel::global(cx).app_id();
 600        let window_decorations = match std::env::var("ZED_WINDOW_DECORATIONS") {
 601            Ok(val) if val == "server" => gpui::WindowDecorations::Server,
 602            Ok(val) if val == "client" => gpui::WindowDecorations::Client,
 603            _ => gpui::WindowDecorations::Client,
 604        };
 605
 606        cx.open_window(
 607            WindowOptions {
 608                titlebar: Some(TitlebarOptions {
 609                    title: Some("Zed — Settings".into()),
 610                    appears_transparent: true,
 611                    traffic_light_position: Some(point(px(12.0), px(12.0))),
 612                }),
 613                focus: true,
 614                show: true,
 615                is_movable: true,
 616                kind: gpui::WindowKind::Normal,
 617                window_background: cx.theme().window_background_appearance(),
 618                app_id: Some(app_id.to_owned()),
 619                window_decorations: Some(window_decorations),
 620                window_min_size: Some(gpui::Size {
 621                    // Don't make the settings window thinner than this,
 622                    // otherwise, it gets unusable. Users with smaller res monitors
 623                    // can customize the height, but not the width.
 624                    width: px(900.0),
 625                    height: px(240.0),
 626                }),
 627                window_bounds: Some(WindowBounds::centered(scaled_bounds, cx)),
 628                ..Default::default()
 629            },
 630            |window, cx| {
 631                let settings_window =
 632                    cx.new(|cx| SettingsWindow::new(Some(workspace_handle), window, cx));
 633                settings_window.update(cx, |settings_window, cx| {
 634                    if let Some(path) = path {
 635                        open_path(&path, open_project_settings, settings_window, window, cx);
 636                    } else if open_project_settings {
 637                        if let Some(file_index) = settings_window
 638                            .files
 639                            .iter()
 640                            .position(|(file, _)| file.worktree_id().is_some())
 641                        {
 642                            settings_window.change_file(file_index, window, cx);
 643                        }
 644
 645                        settings_window.fetch_files(window, cx);
 646                    }
 647                });
 648
 649                settings_window
 650            },
 651        )
 652        .log_err();
 653    });
 654}
 655
 656/// The current sub page path that is selected.
 657/// If this is empty the selected page is rendered,
 658/// otherwise the last sub page gets rendered.
 659///
 660/// Global so that `pick` and `write` callbacks can access it
 661/// and use it to dynamically render sub pages (e.g. for language settings)
 662static ACTIVE_LANGUAGE: LazyLock<RwLock<Option<SharedString>>> =
 663    LazyLock::new(|| RwLock::new(Option::None));
 664
 665fn active_language() -> Option<SharedString> {
 666    ACTIVE_LANGUAGE
 667        .read()
 668        .ok()
 669        .and_then(|language| language.clone())
 670}
 671
 672fn active_language_mut() -> Option<std::sync::RwLockWriteGuard<'static, Option<SharedString>>> {
 673    ACTIVE_LANGUAGE.write().ok()
 674}
 675
 676pub struct SettingsWindow {
 677    title_bar: Option<Entity<PlatformTitleBar>>,
 678    original_window: Option<WindowHandle<Workspace>>,
 679    files: Vec<(SettingsUiFile, FocusHandle)>,
 680    worktree_root_dirs: HashMap<WorktreeId, String>,
 681    current_file: SettingsUiFile,
 682    pages: Vec<SettingsPage>,
 683    sub_page_stack: Vec<SubPage>,
 684    search_bar: Entity<Editor>,
 685    search_task: Option<Task<()>>,
 686    /// Index into navbar_entries
 687    navbar_entry: usize,
 688    navbar_entries: Vec<NavBarEntry>,
 689    navbar_scroll_handle: UniformListScrollHandle,
 690    /// [page_index][page_item_index] will be false
 691    /// when the item is filtered out either by searches
 692    /// or by the current file
 693    navbar_focus_subscriptions: Vec<gpui::Subscription>,
 694    filter_table: Vec<Vec<bool>>,
 695    has_query: bool,
 696    content_handles: Vec<Vec<Entity<NonFocusableHandle>>>,
 697    focus_handle: FocusHandle,
 698    navbar_focus_handle: Entity<NonFocusableHandle>,
 699    content_focus_handle: Entity<NonFocusableHandle>,
 700    files_focus_handle: FocusHandle,
 701    search_index: Option<Arc<SearchIndex>>,
 702    list_state: ListState,
 703    shown_errors: HashSet<String>,
 704}
 705
 706struct SearchIndex {
 707    bm25_engine: bm25::SearchEngine<usize>,
 708    fuzzy_match_candidates: Vec<StringMatchCandidate>,
 709    key_lut: Vec<SearchKeyLUTEntry>,
 710}
 711
 712struct SearchKeyLUTEntry {
 713    page_index: usize,
 714    header_index: usize,
 715    item_index: usize,
 716    json_path: Option<&'static str>,
 717}
 718
 719struct SubPage {
 720    link: SubPageLink,
 721    section_header: SharedString,
 722    scroll_handle: ScrollHandle,
 723}
 724
 725impl SubPage {
 726    fn new(link: SubPageLink, section_header: SharedString) -> Self {
 727        if link.r#type == SubPageType::Language
 728            && let Some(mut active_language_global) = active_language_mut()
 729        {
 730            active_language_global.replace(link.title.clone());
 731        }
 732
 733        SubPage {
 734            link,
 735            section_header,
 736            scroll_handle: ScrollHandle::new(),
 737        }
 738    }
 739}
 740
 741impl Drop for SubPage {
 742    fn drop(&mut self) {
 743        if self.link.r#type == SubPageType::Language
 744            && let Some(mut active_language_global) = active_language_mut()
 745            && active_language_global
 746                .as_ref()
 747                .is_some_and(|language_name| language_name == &self.link.title)
 748        {
 749            active_language_global.take();
 750        }
 751    }
 752}
 753
 754#[derive(Debug)]
 755struct NavBarEntry {
 756    title: &'static str,
 757    is_root: bool,
 758    expanded: bool,
 759    page_index: usize,
 760    item_index: Option<usize>,
 761    focus_handle: FocusHandle,
 762}
 763
 764struct SettingsPage {
 765    title: &'static str,
 766    items: Box<[SettingsPageItem]>,
 767}
 768
 769#[derive(PartialEq)]
 770enum SettingsPageItem {
 771    SectionHeader(&'static str),
 772    SettingItem(SettingItem),
 773    SubPageLink(SubPageLink),
 774    DynamicItem(DynamicItem),
 775    ActionLink(ActionLink),
 776}
 777
 778impl std::fmt::Debug for SettingsPageItem {
 779    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
 780        match self {
 781            SettingsPageItem::SectionHeader(header) => write!(f, "SectionHeader({})", header),
 782            SettingsPageItem::SettingItem(setting_item) => {
 783                write!(f, "SettingItem({})", setting_item.title)
 784            }
 785            SettingsPageItem::SubPageLink(sub_page_link) => {
 786                write!(f, "SubPageLink({})", sub_page_link.title)
 787            }
 788            SettingsPageItem::DynamicItem(dynamic_item) => {
 789                write!(f, "DynamicItem({})", dynamic_item.discriminant.title)
 790            }
 791            SettingsPageItem::ActionLink(action_link) => {
 792                write!(f, "ActionLink({})", action_link.title)
 793            }
 794        }
 795    }
 796}
 797
 798impl SettingsPageItem {
 799    fn header_text(&self) -> Option<&'static str> {
 800        match self {
 801            SettingsPageItem::SectionHeader(header) => Some(header),
 802            _ => None,
 803        }
 804    }
 805
 806    fn render(
 807        &self,
 808        settings_window: &SettingsWindow,
 809        item_index: usize,
 810        is_last: bool,
 811        window: &mut Window,
 812        cx: &mut Context<SettingsWindow>,
 813    ) -> AnyElement {
 814        let file = settings_window.current_file.clone();
 815
 816        let apply_padding = |element: Stateful<Div>| -> Stateful<Div> {
 817            let element = element.pt_4();
 818            if is_last {
 819                element.pb_10()
 820            } else {
 821                element.pb_4()
 822            }
 823        };
 824
 825        let mut render_setting_item_inner =
 826            |setting_item: &SettingItem,
 827             padding: bool,
 828             sub_field: bool,
 829             cx: &mut Context<SettingsWindow>| {
 830                let renderer = cx.default_global::<SettingFieldRenderer>().clone();
 831                let (_, found) = setting_item.field.file_set_in(file.clone(), cx);
 832
 833                let renderers = renderer.renderers.borrow();
 834
 835                let field_renderer =
 836                    renderers.get(&AnySettingField::type_id(setting_item.field.as_ref()));
 837                let field_renderer_or_warning =
 838                    field_renderer.ok_or("NO RENDERER").and_then(|renderer| {
 839                        if cfg!(debug_assertions) && !found {
 840                            Err("NO DEFAULT")
 841                        } else {
 842                            Ok(renderer)
 843                        }
 844                    });
 845
 846                let field = match field_renderer_or_warning {
 847                    Ok(field_renderer) => window.with_id(item_index, |window| {
 848                        field_renderer(
 849                            settings_window,
 850                            setting_item,
 851                            file.clone(),
 852                            setting_item.metadata.as_deref(),
 853                            sub_field,
 854                            window,
 855                            cx,
 856                        )
 857                    }),
 858                    Err(warning) => render_settings_item(
 859                        settings_window,
 860                        setting_item,
 861                        file.clone(),
 862                        Button::new("error-warning", warning)
 863                            .style(ButtonStyle::Outlined)
 864                            .size(ButtonSize::Medium)
 865                            .icon(Some(IconName::Debug))
 866                            .icon_position(IconPosition::Start)
 867                            .icon_color(Color::Error)
 868                            .tab_index(0_isize)
 869                            .tooltip(Tooltip::text(setting_item.field.type_name()))
 870                            .into_any_element(),
 871                        sub_field,
 872                        cx,
 873                    ),
 874                };
 875
 876                let field = if padding {
 877                    field.map(apply_padding)
 878                } else {
 879                    field
 880                };
 881
 882                (field, field_renderer_or_warning.is_ok())
 883            };
 884
 885        match self {
 886            SettingsPageItem::SectionHeader(header) => {
 887                SettingsSectionHeader::new(SharedString::new_static(header)).into_any_element()
 888            }
 889            SettingsPageItem::SettingItem(setting_item) => {
 890                let (field_with_padding, _) =
 891                    render_setting_item_inner(setting_item, true, false, cx);
 892
 893                v_flex()
 894                    .group("setting-item")
 895                    .px_8()
 896                    .child(field_with_padding)
 897                    .when(!is_last, |this| this.child(Divider::horizontal()))
 898                    .into_any_element()
 899            }
 900            SettingsPageItem::SubPageLink(sub_page_link) => v_flex()
 901                .group("setting-item")
 902                .px_8()
 903                .child(
 904                    h_flex()
 905                        .id(sub_page_link.title.clone())
 906                        .w_full()
 907                        .min_w_0()
 908                        .justify_between()
 909                        .map(apply_padding)
 910                        .child(
 911                            v_flex()
 912                                .relative()
 913                                .w_full()
 914                                .max_w_1_2()
 915                                .child(Label::new(sub_page_link.title.clone()))
 916                                .when_some(
 917                                    sub_page_link.description.as_ref(),
 918                                    |this, description| {
 919                                        this.child(
 920                                            Label::new(description.clone())
 921                                                .size(LabelSize::Small)
 922                                                .color(Color::Muted),
 923                                        )
 924                                    },
 925                                ),
 926                        )
 927                        .child(
 928                            Button::new(
 929                                ("sub-page".into(), sub_page_link.title.clone()),
 930                                "Configure",
 931                            )
 932                            .icon(IconName::ChevronRight)
 933                            .tab_index(0_isize)
 934                            .icon_position(IconPosition::End)
 935                            .icon_color(Color::Muted)
 936                            .icon_size(IconSize::Small)
 937                            .style(ButtonStyle::OutlinedGhost)
 938                            .size(ButtonSize::Medium)
 939                            .on_click({
 940                                let sub_page_link = sub_page_link.clone();
 941                                cx.listener(move |this, _, window, cx| {
 942                                    let header_text = this
 943                                        .sub_page_stack
 944                                        .last()
 945                                        .map(|sub_page| sub_page.link.title.clone())
 946                                        .or_else(|| {
 947                                            this.current_page()
 948                                                .items
 949                                                .iter()
 950                                                .take(item_index)
 951                                                .rev()
 952                                                .find_map(|item| {
 953                                                    item.header_text().map(SharedString::new_static)
 954                                                })
 955                                        });
 956
 957                                    let Some(header) = header_text else {
 958                                        unreachable!(
 959                                            "All items always have a section header above them"
 960                                        )
 961                                    };
 962
 963                                    this.push_sub_page(sub_page_link.clone(), header, window, cx)
 964                                })
 965                            }),
 966                        )
 967                        .child(render_settings_item_link(
 968                            sub_page_link.title.clone(),
 969                            sub_page_link.json_path,
 970                            false,
 971                            cx,
 972                        )),
 973                )
 974                .when(!is_last, |this| this.child(Divider::horizontal()))
 975                .into_any_element(),
 976            SettingsPageItem::DynamicItem(DynamicItem {
 977                discriminant: discriminant_setting_item,
 978                pick_discriminant,
 979                fields,
 980            }) => {
 981                let file = file.to_settings();
 982                let discriminant = SettingsStore::global(cx)
 983                    .get_value_from_file(file, *pick_discriminant)
 984                    .1;
 985
 986                let (discriminant_element, rendered_ok) =
 987                    render_setting_item_inner(discriminant_setting_item, true, false, cx);
 988
 989                let has_sub_fields =
 990                    rendered_ok && discriminant.is_some_and(|d| !fields[d].is_empty());
 991
 992                let mut content = v_flex()
 993                    .id("dynamic-item")
 994                    .child(
 995                        div()
 996                            .group("setting-item")
 997                            .px_8()
 998                            .child(discriminant_element.when(has_sub_fields, |this| this.pb_4())),
 999                    )
1000                    .when(!has_sub_fields && !is_last, |this| {
1001                        this.child(h_flex().px_8().child(Divider::horizontal()))
1002                    });
1003
1004                if rendered_ok {
1005                    let discriminant =
1006                        discriminant.expect("This should be Some if rendered_ok is true");
1007                    let sub_fields = &fields[discriminant];
1008                    let sub_field_count = sub_fields.len();
1009
1010                    for (index, field) in sub_fields.iter().enumerate() {
1011                        let is_last_sub_field = index == sub_field_count - 1;
1012                        let (raw_field, _) = render_setting_item_inner(field, false, true, cx);
1013
1014                        content = content.child(
1015                            raw_field
1016                                .group("setting-sub-item")
1017                                .mx_8()
1018                                .p_4()
1019                                .border_t_1()
1020                                .when(is_last_sub_field, |this| this.border_b_1())
1021                                .when(is_last_sub_field && is_last, |this| this.mb_8())
1022                                .border_dashed()
1023                                .border_color(cx.theme().colors().border_variant)
1024                                .bg(cx.theme().colors().element_background.opacity(0.2)),
1025                        );
1026                    }
1027                }
1028
1029                return content.into_any_element();
1030            }
1031            SettingsPageItem::ActionLink(action_link) => v_flex()
1032                .group("setting-item")
1033                .px_8()
1034                .child(
1035                    h_flex()
1036                        .id(action_link.title.clone())
1037                        .w_full()
1038                        .min_w_0()
1039                        .justify_between()
1040                        .map(apply_padding)
1041                        .child(
1042                            v_flex()
1043                                .relative()
1044                                .w_full()
1045                                .max_w_1_2()
1046                                .child(Label::new(action_link.title.clone()))
1047                                .when_some(
1048                                    action_link.description.as_ref(),
1049                                    |this, description| {
1050                                        this.child(
1051                                            Label::new(description.clone())
1052                                                .size(LabelSize::Small)
1053                                                .color(Color::Muted),
1054                                        )
1055                                    },
1056                                ),
1057                        )
1058                        .child(
1059                            Button::new(
1060                                ("action-link".into(), action_link.title.clone()),
1061                                action_link.button_text.clone(),
1062                            )
1063                            .icon(IconName::ArrowUpRight)
1064                            .tab_index(0_isize)
1065                            .icon_position(IconPosition::End)
1066                            .icon_color(Color::Muted)
1067                            .icon_size(IconSize::Small)
1068                            .style(ButtonStyle::OutlinedGhost)
1069                            .size(ButtonSize::Medium)
1070                            .on_click({
1071                                let on_click = action_link.on_click.clone();
1072                                cx.listener(move |this, _, window, cx| {
1073                                    on_click(this, window, cx);
1074                                })
1075                            }),
1076                        ),
1077                )
1078                .when(!is_last, |this| this.child(Divider::horizontal()))
1079                .into_any_element(),
1080        }
1081    }
1082}
1083
1084fn render_settings_item(
1085    settings_window: &SettingsWindow,
1086    setting_item: &SettingItem,
1087    file: SettingsUiFile,
1088    control: AnyElement,
1089    sub_field: bool,
1090    cx: &mut Context<'_, SettingsWindow>,
1091) -> Stateful<Div> {
1092    let (found_in_file, _) = setting_item.field.file_set_in(file.clone(), cx);
1093    let file_set_in = SettingsUiFile::from_settings(found_in_file.clone());
1094
1095    h_flex()
1096        .id(setting_item.title)
1097        .min_w_0()
1098        .justify_between()
1099        .child(
1100            v_flex()
1101                .relative()
1102                .w_1_2()
1103                .child(
1104                    h_flex()
1105                        .w_full()
1106                        .gap_1()
1107                        .child(Label::new(SharedString::new_static(setting_item.title)))
1108                        .when_some(
1109                            if sub_field {
1110                                None
1111                            } else {
1112                                setting_item
1113                                    .field
1114                                    .reset_to_default_fn(&file, &found_in_file, cx)
1115                            },
1116                            |this, reset_to_default| {
1117                                this.child(
1118                                    IconButton::new("reset-to-default-btn", IconName::Undo)
1119                                        .icon_color(Color::Muted)
1120                                        .icon_size(IconSize::Small)
1121                                        .tooltip(Tooltip::text("Reset to Default"))
1122                                        .on_click({
1123                                            move |_, _, cx| {
1124                                                reset_to_default(cx);
1125                                            }
1126                                        }),
1127                                )
1128                            },
1129                        )
1130                        .when_some(
1131                            file_set_in.filter(|file_set_in| file_set_in != &file),
1132                            |this, file_set_in| {
1133                                this.child(
1134                                    Label::new(format!(
1135                                        "—  Modified in {}",
1136                                        settings_window
1137                                            .display_name(&file_set_in)
1138                                            .expect("File name should exist")
1139                                    ))
1140                                    .color(Color::Muted)
1141                                    .size(LabelSize::Small),
1142                                )
1143                            },
1144                        ),
1145                )
1146                .child(
1147                    Label::new(SharedString::new_static(setting_item.description))
1148                        .size(LabelSize::Small)
1149                        .color(Color::Muted),
1150                ),
1151        )
1152        .child(control)
1153        .when(settings_window.sub_page_stack.is_empty(), |this| {
1154            this.child(render_settings_item_link(
1155                setting_item.description,
1156                setting_item.field.json_path(),
1157                sub_field,
1158                cx,
1159            ))
1160        })
1161}
1162
1163fn render_settings_item_link(
1164    id: impl Into<ElementId>,
1165    json_path: Option<&'static str>,
1166    sub_field: bool,
1167    cx: &mut Context<'_, SettingsWindow>,
1168) -> impl IntoElement {
1169    let clipboard_has_link = cx
1170        .read_from_clipboard()
1171        .and_then(|entry| entry.text())
1172        .map_or(false, |maybe_url| {
1173            json_path.is_some() && maybe_url.strip_prefix("zed://settings/") == json_path
1174        });
1175
1176    let (link_icon, link_icon_color) = if clipboard_has_link {
1177        (IconName::Check, Color::Success)
1178    } else {
1179        (IconName::Link, Color::Muted)
1180    };
1181
1182    div()
1183        .absolute()
1184        .top(rems_from_px(18.))
1185        .map(|this| {
1186            if sub_field {
1187                this.visible_on_hover("setting-sub-item")
1188                    .left(rems_from_px(-8.5))
1189            } else {
1190                this.visible_on_hover("setting-item")
1191                    .left(rems_from_px(-22.))
1192            }
1193        })
1194        .child(
1195            IconButton::new((id.into(), "copy-link-btn"), link_icon)
1196                .icon_color(link_icon_color)
1197                .icon_size(IconSize::Small)
1198                .shape(IconButtonShape::Square)
1199                .tooltip(Tooltip::text("Copy Link"))
1200                .when_some(json_path, |this, path| {
1201                    this.on_click(cx.listener(move |_, _, _, cx| {
1202                        let link = format!("zed://settings/{}", path);
1203                        cx.write_to_clipboard(ClipboardItem::new_string(link));
1204                        cx.notify();
1205                    }))
1206                }),
1207        )
1208}
1209
1210struct SettingItem {
1211    title: &'static str,
1212    description: &'static str,
1213    field: Box<dyn AnySettingField>,
1214    metadata: Option<Box<SettingsFieldMetadata>>,
1215    files: FileMask,
1216}
1217
1218struct DynamicItem {
1219    discriminant: SettingItem,
1220    pick_discriminant: fn(&SettingsContent) -> Option<usize>,
1221    fields: Vec<Vec<SettingItem>>,
1222}
1223
1224impl PartialEq for DynamicItem {
1225    fn eq(&self, other: &Self) -> bool {
1226        self.discriminant == other.discriminant && self.fields == other.fields
1227    }
1228}
1229
1230#[derive(PartialEq, Eq, Clone, Copy)]
1231struct FileMask(u8);
1232
1233impl std::fmt::Debug for FileMask {
1234    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1235        write!(f, "FileMask(")?;
1236        let mut items = vec![];
1237
1238        if self.contains(USER) {
1239            items.push("USER");
1240        }
1241        if self.contains(PROJECT) {
1242            items.push("LOCAL");
1243        }
1244        if self.contains(SERVER) {
1245            items.push("SERVER");
1246        }
1247
1248        write!(f, "{})", items.join(" | "))
1249    }
1250}
1251
1252const USER: FileMask = FileMask(1 << 0);
1253const PROJECT: FileMask = FileMask(1 << 2);
1254const SERVER: FileMask = FileMask(1 << 3);
1255
1256impl std::ops::BitAnd for FileMask {
1257    type Output = Self;
1258
1259    fn bitand(self, other: Self) -> Self {
1260        Self(self.0 & other.0)
1261    }
1262}
1263
1264impl std::ops::BitOr for FileMask {
1265    type Output = Self;
1266
1267    fn bitor(self, other: Self) -> Self {
1268        Self(self.0 | other.0)
1269    }
1270}
1271
1272impl FileMask {
1273    fn contains(&self, other: FileMask) -> bool {
1274        self.0 & other.0 != 0
1275    }
1276}
1277
1278impl PartialEq for SettingItem {
1279    fn eq(&self, other: &Self) -> bool {
1280        self.title == other.title
1281            && self.description == other.description
1282            && (match (&self.metadata, &other.metadata) {
1283                (None, None) => true,
1284                (Some(m1), Some(m2)) => m1.placeholder == m2.placeholder,
1285                _ => false,
1286            })
1287    }
1288}
1289
1290#[derive(Clone, PartialEq, Default)]
1291enum SubPageType {
1292    Language,
1293    #[default]
1294    Other,
1295}
1296
1297#[derive(Clone)]
1298struct SubPageLink {
1299    title: SharedString,
1300    r#type: SubPageType,
1301    description: Option<SharedString>,
1302    /// See [`SettingField.json_path`]
1303    json_path: Option<&'static str>,
1304    /// Whether or not the settings in this sub page are configurable in settings.json
1305    /// Removes the "Edit in settings.json" button from the page.
1306    in_json: bool,
1307    files: FileMask,
1308    render:
1309        fn(&SettingsWindow, &ScrollHandle, &mut Window, &mut Context<SettingsWindow>) -> AnyElement,
1310}
1311
1312impl PartialEq for SubPageLink {
1313    fn eq(&self, other: &Self) -> bool {
1314        self.title == other.title
1315    }
1316}
1317
1318#[derive(Clone)]
1319struct ActionLink {
1320    title: SharedString,
1321    description: Option<SharedString>,
1322    button_text: SharedString,
1323    on_click: Arc<dyn Fn(&mut SettingsWindow, &mut Window, &mut App) + Send + Sync>,
1324}
1325
1326impl PartialEq for ActionLink {
1327    fn eq(&self, other: &Self) -> bool {
1328        self.title == other.title
1329    }
1330}
1331
1332fn all_language_names(cx: &App) -> Vec<SharedString> {
1333    workspace::AppState::global(cx)
1334        .upgrade()
1335        .map_or(vec![], |state| {
1336            state
1337                .languages
1338                .language_names()
1339                .into_iter()
1340                .filter(|name| name.as_ref() != "Zed Keybind Context")
1341                .map(Into::into)
1342                .collect()
1343        })
1344}
1345
1346#[allow(unused)]
1347#[derive(Clone, PartialEq, Debug)]
1348enum SettingsUiFile {
1349    User,                                // Uses all settings.
1350    Project((WorktreeId, Arc<RelPath>)), // Has a special name, and special set of settings
1351    Server(&'static str),                // Uses a special name, and the user settings
1352}
1353
1354impl SettingsUiFile {
1355    fn setting_type(&self) -> &'static str {
1356        match self {
1357            SettingsUiFile::User => "User",
1358            SettingsUiFile::Project(_) => "Project",
1359            SettingsUiFile::Server(_) => "Server",
1360        }
1361    }
1362
1363    fn is_server(&self) -> bool {
1364        matches!(self, SettingsUiFile::Server(_))
1365    }
1366
1367    fn worktree_id(&self) -> Option<WorktreeId> {
1368        match self {
1369            SettingsUiFile::User => None,
1370            SettingsUiFile::Project((worktree_id, _)) => Some(*worktree_id),
1371            SettingsUiFile::Server(_) => None,
1372        }
1373    }
1374
1375    fn from_settings(file: settings::SettingsFile) -> Option<Self> {
1376        Some(match file {
1377            settings::SettingsFile::User => SettingsUiFile::User,
1378            settings::SettingsFile::Project(location) => SettingsUiFile::Project(location),
1379            settings::SettingsFile::Server => SettingsUiFile::Server("todo: server name"),
1380            settings::SettingsFile::Default => return None,
1381            settings::SettingsFile::Global => return None,
1382        })
1383    }
1384
1385    fn to_settings(&self) -> settings::SettingsFile {
1386        match self {
1387            SettingsUiFile::User => settings::SettingsFile::User,
1388            SettingsUiFile::Project(location) => settings::SettingsFile::Project(location.clone()),
1389            SettingsUiFile::Server(_) => settings::SettingsFile::Server,
1390        }
1391    }
1392
1393    fn mask(&self) -> FileMask {
1394        match self {
1395            SettingsUiFile::User => USER,
1396            SettingsUiFile::Project(_) => PROJECT,
1397            SettingsUiFile::Server(_) => SERVER,
1398        }
1399    }
1400}
1401
1402impl SettingsWindow {
1403    fn new(
1404        original_window: Option<WindowHandle<Workspace>>,
1405        window: &mut Window,
1406        cx: &mut Context<Self>,
1407    ) -> Self {
1408        let font_family_cache = theme::FontFamilyCache::global(cx);
1409
1410        cx.spawn(async move |this, cx| {
1411            font_family_cache.prefetch(cx).await;
1412            this.update(cx, |_, cx| {
1413                cx.notify();
1414            })
1415        })
1416        .detach();
1417
1418        let current_file = SettingsUiFile::User;
1419        let search_bar = cx.new(|cx| {
1420            let mut editor = Editor::single_line(window, cx);
1421            editor.set_placeholder_text("Search settings…", window, cx);
1422            editor
1423        });
1424
1425        cx.subscribe(&search_bar, |this, _, event: &EditorEvent, cx| {
1426            let EditorEvent::Edited { transaction_id: _ } = event else {
1427                return;
1428            };
1429
1430            this.update_matches(cx);
1431        })
1432        .detach();
1433
1434        let mut ui_font_size = ThemeSettings::get_global(cx).ui_font_size(cx);
1435        cx.observe_global_in::<SettingsStore>(window, move |this, window, cx| {
1436            this.fetch_files(window, cx);
1437
1438            // Whenever settings are changed, it's possible that the changed
1439            // settings affects the rendering of the `SettingsWindow`, like is
1440            // the case with `ui_font_size`. When that happens, we need to
1441            // instruct the `ListState` to re-measure the list items, as the
1442            // list item heights may have changed depending on the new font
1443            // size.
1444            let new_ui_font_size = ThemeSettings::get_global(cx).ui_font_size(cx);
1445            if new_ui_font_size != ui_font_size {
1446                this.list_state.remeasure();
1447                ui_font_size = new_ui_font_size;
1448            }
1449
1450            cx.notify();
1451        })
1452        .detach();
1453
1454        cx.on_window_closed(|cx| {
1455            if let Some(existing_window) = cx
1456                .windows()
1457                .into_iter()
1458                .find_map(|window| window.downcast::<SettingsWindow>())
1459                && cx.windows().len() == 1
1460            {
1461                cx.update_window(*existing_window, |_, window, _| {
1462                    window.remove_window();
1463                })
1464                .ok();
1465
1466                telemetry::event!("Settings Closed")
1467            }
1468        })
1469        .detach();
1470
1471        if let Some(app_state) = AppState::global(cx).upgrade() {
1472            for project in app_state
1473                .workspace_store
1474                .read(cx)
1475                .workspaces()
1476                .iter()
1477                .filter_map(|space| {
1478                    space
1479                        .read(cx)
1480                        .ok()
1481                        .map(|workspace| workspace.project().clone())
1482                })
1483                .collect::<Vec<_>>()
1484            {
1485                cx.observe_release_in(&project, window, |this, _, window, cx| {
1486                    this.fetch_files(window, cx)
1487                })
1488                .detach();
1489                cx.subscribe_in(&project, window, Self::handle_project_event)
1490                    .detach();
1491            }
1492
1493            for workspace in app_state
1494                .workspace_store
1495                .read(cx)
1496                .workspaces()
1497                .iter()
1498                .filter_map(|space| space.entity(cx).ok())
1499            {
1500                cx.observe_release_in(&workspace, window, |this, _, window, cx| {
1501                    this.fetch_files(window, cx)
1502                })
1503                .detach();
1504            }
1505        } else {
1506            log::error!("App state doesn't exist when creating a new settings window");
1507        }
1508
1509        let this_weak = cx.weak_entity();
1510        cx.observe_new::<Project>({
1511            let this_weak = this_weak.clone();
1512
1513            move |_, window, cx| {
1514                let project = cx.entity();
1515                let Some(window) = window else {
1516                    return;
1517                };
1518
1519                this_weak
1520                    .update(cx, |this, cx| {
1521                        this.fetch_files(window, cx);
1522                        cx.observe_release_in(&project, window, |_, _, window, cx| {
1523                            cx.defer_in(window, |this, window, cx| this.fetch_files(window, cx));
1524                        })
1525                        .detach();
1526
1527                        cx.subscribe_in(&project, window, Self::handle_project_event)
1528                            .detach();
1529                    })
1530                    .ok();
1531            }
1532        })
1533        .detach();
1534
1535        cx.observe_new::<Workspace>(move |_, window, cx| {
1536            let workspace = cx.entity();
1537            let Some(window) = window else {
1538                return;
1539            };
1540
1541            this_weak
1542                .update(cx, |this, cx| {
1543                    this.fetch_files(window, cx);
1544                    cx.observe_release_in(&workspace, window, |this, _, window, cx| {
1545                        this.fetch_files(window, cx)
1546                    })
1547                    .detach();
1548                })
1549                .ok();
1550        })
1551        .detach();
1552
1553        let title_bar = if !cfg!(target_os = "macos") {
1554            Some(cx.new(|cx| PlatformTitleBar::new("settings-title-bar", cx)))
1555        } else {
1556            None
1557        };
1558
1559        let list_state = gpui::ListState::new(0, gpui::ListAlignment::Top, px(0.0)).measure_all();
1560        list_state.set_scroll_handler(|_, _, _| {});
1561
1562        let mut this = Self {
1563            title_bar,
1564            original_window,
1565
1566            worktree_root_dirs: HashMap::default(),
1567            files: vec![],
1568
1569            current_file: current_file,
1570            pages: vec![],
1571            sub_page_stack: vec![],
1572            navbar_entries: vec![],
1573            navbar_entry: 0,
1574            navbar_scroll_handle: UniformListScrollHandle::default(),
1575            search_bar,
1576            search_task: None,
1577            filter_table: vec![],
1578            has_query: false,
1579            content_handles: vec![],
1580            focus_handle: cx.focus_handle(),
1581            navbar_focus_handle: NonFocusableHandle::new(
1582                NAVBAR_CONTAINER_TAB_INDEX,
1583                false,
1584                window,
1585                cx,
1586            ),
1587            navbar_focus_subscriptions: vec![],
1588            content_focus_handle: NonFocusableHandle::new(
1589                CONTENT_CONTAINER_TAB_INDEX,
1590                false,
1591                window,
1592                cx,
1593            ),
1594            files_focus_handle: cx
1595                .focus_handle()
1596                .tab_index(HEADER_CONTAINER_TAB_INDEX)
1597                .tab_stop(false),
1598            search_index: None,
1599            shown_errors: HashSet::default(),
1600            list_state,
1601        };
1602
1603        this.fetch_files(window, cx);
1604        this.build_ui(window, cx);
1605        this.build_search_index();
1606
1607        this.search_bar.update(cx, |editor, cx| {
1608            editor.focus_handle(cx).focus(window, cx);
1609        });
1610
1611        this
1612    }
1613
1614    fn handle_project_event(
1615        &mut self,
1616        _: &Entity<Project>,
1617        event: &project::Event,
1618        window: &mut Window,
1619        cx: &mut Context<SettingsWindow>,
1620    ) {
1621        match event {
1622            project::Event::WorktreeRemoved(_) | project::Event::WorktreeAdded(_) => {
1623                cx.defer_in(window, |this, window, cx| {
1624                    this.fetch_files(window, cx);
1625                });
1626            }
1627            _ => {}
1628        }
1629    }
1630
1631    fn toggle_navbar_entry(&mut self, nav_entry_index: usize) {
1632        // We can only toggle root entries
1633        if !self.navbar_entries[nav_entry_index].is_root {
1634            return;
1635        }
1636
1637        let expanded = &mut self.navbar_entries[nav_entry_index].expanded;
1638        *expanded = !*expanded;
1639        self.navbar_entry = nav_entry_index;
1640        self.reset_list_state();
1641    }
1642
1643    fn build_navbar(&mut self, cx: &App) {
1644        let mut navbar_entries = Vec::new();
1645
1646        for (page_index, page) in self.pages.iter().enumerate() {
1647            navbar_entries.push(NavBarEntry {
1648                title: page.title,
1649                is_root: true,
1650                expanded: false,
1651                page_index,
1652                item_index: None,
1653                focus_handle: cx.focus_handle().tab_index(0).tab_stop(true),
1654            });
1655
1656            for (item_index, item) in page.items.iter().enumerate() {
1657                let SettingsPageItem::SectionHeader(title) = item else {
1658                    continue;
1659                };
1660                navbar_entries.push(NavBarEntry {
1661                    title,
1662                    is_root: false,
1663                    expanded: false,
1664                    page_index,
1665                    item_index: Some(item_index),
1666                    focus_handle: cx.focus_handle().tab_index(0).tab_stop(true),
1667                });
1668            }
1669        }
1670
1671        self.navbar_entries = navbar_entries;
1672    }
1673
1674    fn setup_navbar_focus_subscriptions(
1675        &mut self,
1676        window: &mut Window,
1677        cx: &mut Context<SettingsWindow>,
1678    ) {
1679        let mut focus_subscriptions = Vec::new();
1680
1681        for entry_index in 0..self.navbar_entries.len() {
1682            let focus_handle = self.navbar_entries[entry_index].focus_handle.clone();
1683
1684            let subscription = cx.on_focus(
1685                &focus_handle,
1686                window,
1687                move |this: &mut SettingsWindow,
1688                      window: &mut Window,
1689                      cx: &mut Context<SettingsWindow>| {
1690                    this.open_and_scroll_to_navbar_entry(entry_index, None, false, window, cx);
1691                },
1692            );
1693            focus_subscriptions.push(subscription);
1694        }
1695        self.navbar_focus_subscriptions = focus_subscriptions;
1696    }
1697
1698    fn visible_navbar_entries(&self) -> impl Iterator<Item = (usize, &NavBarEntry)> {
1699        let mut index = 0;
1700        let entries = &self.navbar_entries;
1701        let search_matches = &self.filter_table;
1702        let has_query = self.has_query;
1703        std::iter::from_fn(move || {
1704            while index < entries.len() {
1705                let entry = &entries[index];
1706                let included_in_search = if let Some(item_index) = entry.item_index {
1707                    search_matches[entry.page_index][item_index]
1708                } else {
1709                    search_matches[entry.page_index].iter().any(|b| *b)
1710                        || search_matches[entry.page_index].is_empty()
1711                };
1712                if included_in_search {
1713                    break;
1714                }
1715                index += 1;
1716            }
1717            if index >= self.navbar_entries.len() {
1718                return None;
1719            }
1720            let entry = &entries[index];
1721            let entry_index = index;
1722
1723            index += 1;
1724            if entry.is_root && !entry.expanded && !has_query {
1725                while index < entries.len() {
1726                    if entries[index].is_root {
1727                        break;
1728                    }
1729                    index += 1;
1730                }
1731            }
1732
1733            return Some((entry_index, entry));
1734        })
1735    }
1736
1737    fn filter_matches_to_file(&mut self) {
1738        let current_file = self.current_file.mask();
1739        for (page, page_filter) in std::iter::zip(&self.pages, &mut self.filter_table) {
1740            let mut header_index = 0;
1741            let mut any_found_since_last_header = true;
1742
1743            for (index, item) in page.items.iter().enumerate() {
1744                match item {
1745                    SettingsPageItem::SectionHeader(_) => {
1746                        if !any_found_since_last_header {
1747                            page_filter[header_index] = false;
1748                        }
1749                        header_index = index;
1750                        any_found_since_last_header = false;
1751                    }
1752                    SettingsPageItem::SettingItem(SettingItem { files, .. })
1753                    | SettingsPageItem::SubPageLink(SubPageLink { files, .. })
1754                    | SettingsPageItem::DynamicItem(DynamicItem {
1755                        discriminant: SettingItem { files, .. },
1756                        ..
1757                    }) => {
1758                        if !files.contains(current_file) {
1759                            page_filter[index] = false;
1760                        } else {
1761                            any_found_since_last_header = true;
1762                        }
1763                    }
1764                    SettingsPageItem::ActionLink(_) => {
1765                        any_found_since_last_header = true;
1766                    }
1767                }
1768            }
1769            if let Some(last_header) = page_filter.get_mut(header_index)
1770                && !any_found_since_last_header
1771            {
1772                *last_header = false;
1773            }
1774        }
1775    }
1776
1777    fn update_matches(&mut self, cx: &mut Context<SettingsWindow>) {
1778        self.search_task.take();
1779        let mut query = self.search_bar.read(cx).text(cx);
1780        if query.is_empty() || self.search_index.is_none() {
1781            for page in &mut self.filter_table {
1782                page.fill(true);
1783            }
1784            self.has_query = false;
1785            self.filter_matches_to_file();
1786            self.reset_list_state();
1787            cx.notify();
1788            return;
1789        }
1790
1791        let is_json_link_query;
1792        if query.starts_with("#") {
1793            query.remove(0);
1794            is_json_link_query = true;
1795        } else {
1796            is_json_link_query = false;
1797        }
1798
1799        let search_index = self.search_index.as_ref().unwrap().clone();
1800
1801        fn update_matches_inner(
1802            this: &mut SettingsWindow,
1803            search_index: &SearchIndex,
1804            match_indices: impl Iterator<Item = usize>,
1805            cx: &mut Context<SettingsWindow>,
1806        ) {
1807            for page in &mut this.filter_table {
1808                page.fill(false);
1809            }
1810
1811            for match_index in match_indices {
1812                let SearchKeyLUTEntry {
1813                    page_index,
1814                    header_index,
1815                    item_index,
1816                    ..
1817                } = search_index.key_lut[match_index];
1818                let page = &mut this.filter_table[page_index];
1819                page[header_index] = true;
1820                page[item_index] = true;
1821            }
1822            this.has_query = true;
1823            this.filter_matches_to_file();
1824            this.open_first_nav_page();
1825            this.reset_list_state();
1826            cx.notify();
1827        }
1828
1829        self.search_task = Some(cx.spawn(async move |this, cx| {
1830            if is_json_link_query {
1831                let mut indices = vec![];
1832                for (index, SearchKeyLUTEntry { json_path, .. }) in
1833                    search_index.key_lut.iter().enumerate()
1834                {
1835                    let Some(json_path) = json_path else {
1836                        continue;
1837                    };
1838
1839                    if let Some(post) = query.strip_prefix(json_path)
1840                        && (post.is_empty() || post.starts_with('.'))
1841                    {
1842                        indices.push(index);
1843                    }
1844                }
1845                if !indices.is_empty() {
1846                    this.update(cx, |this, cx| {
1847                        update_matches_inner(this, search_index.as_ref(), indices.into_iter(), cx);
1848                    })
1849                    .ok();
1850                    return;
1851                }
1852            }
1853            let bm25_task = cx.background_spawn({
1854                let search_index = search_index.clone();
1855                let max_results = search_index.key_lut.len();
1856                let query = query.clone();
1857                async move { search_index.bm25_engine.search(&query, max_results) }
1858            });
1859            let cancel_flag = std::sync::atomic::AtomicBool::new(false);
1860            let fuzzy_search_task = fuzzy::match_strings(
1861                search_index.fuzzy_match_candidates.as_slice(),
1862                &query,
1863                false,
1864                true,
1865                search_index.fuzzy_match_candidates.len(),
1866                &cancel_flag,
1867                cx.background_executor().clone(),
1868            );
1869
1870            let fuzzy_matches = fuzzy_search_task.await;
1871            let bm25_matches = bm25_task.await;
1872
1873            _ = this
1874                .update(cx, |this, cx| {
1875                    // For tuning the score threshold
1876                    // for fuzzy_match in &fuzzy_matches {
1877                    //     let SearchItemKey {
1878                    //         page_index,
1879                    //         header_index,
1880                    //         item_index,
1881                    //     } = search_index.key_lut[fuzzy_match.candidate_id];
1882                    //     let SettingsPageItem::SectionHeader(header) =
1883                    //         this.pages[page_index].items[header_index]
1884                    //     else {
1885                    //         continue;
1886                    //     };
1887                    //     let SettingsPageItem::SettingItem(SettingItem {
1888                    //         title, description, ..
1889                    //     }) = this.pages[page_index].items[item_index]
1890                    //     else {
1891                    //         continue;
1892                    //     };
1893                    //     let score = fuzzy_match.score;
1894                    //     eprint!("# {header} :: QUERY = {query} :: SCORE = {score}\n{title}\n{description}\n\n");
1895                    // }
1896                    let fuzzy_indices = fuzzy_matches
1897                        .into_iter()
1898                        // MAGIC NUMBER: Was found to have right balance between not too many weird matches, but also
1899                        // flexible enough to catch misspellings and <4 letter queries
1900                        .take_while(|fuzzy_match| fuzzy_match.score >= 0.5)
1901                        .map(|fuzzy_match| fuzzy_match.candidate_id);
1902                    let bm25_indices = bm25_matches
1903                        .into_iter()
1904                        .map(|bm25_match| bm25_match.document.id);
1905                    let merged_indices = bm25_indices.chain(fuzzy_indices);
1906
1907                    update_matches_inner(this, search_index.as_ref(), merged_indices, cx);
1908                })
1909                .ok();
1910
1911            cx.background_executor().timer(Duration::from_secs(1)).await;
1912            telemetry::event!("Settings Searched", query = query)
1913        }));
1914    }
1915
1916    fn build_filter_table(&mut self) {
1917        self.filter_table = self
1918            .pages
1919            .iter()
1920            .map(|page| vec![true; page.items.len()])
1921            .collect::<Vec<_>>();
1922    }
1923
1924    fn build_search_index(&mut self) {
1925        let mut key_lut: Vec<SearchKeyLUTEntry> = vec![];
1926        let mut documents = Vec::default();
1927        let mut fuzzy_match_candidates = Vec::default();
1928
1929        fn push_candidates(
1930            fuzzy_match_candidates: &mut Vec<StringMatchCandidate>,
1931            key_index: usize,
1932            input: &str,
1933        ) {
1934            for word in input.split_ascii_whitespace() {
1935                fuzzy_match_candidates.push(StringMatchCandidate::new(key_index, word));
1936            }
1937        }
1938
1939        // PERF: We are currently searching all items even in project files
1940        // where many settings are filtered out, using the logic in filter_matches_to_file
1941        // we could only search relevant items based on the current file
1942        for (page_index, page) in self.pages.iter().enumerate() {
1943            let mut header_index = 0;
1944            let mut header_str = "";
1945            for (item_index, item) in page.items.iter().enumerate() {
1946                let key_index = key_lut.len();
1947                let mut json_path = None;
1948                match item {
1949                    SettingsPageItem::DynamicItem(DynamicItem {
1950                        discriminant: item, ..
1951                    })
1952                    | SettingsPageItem::SettingItem(item) => {
1953                        json_path = item
1954                            .field
1955                            .json_path()
1956                            .map(|path| path.trim_end_matches('$'));
1957                        documents.push(bm25::Document {
1958                            id: key_index,
1959                            contents: [page.title, header_str, item.title, item.description]
1960                                .join("\n"),
1961                        });
1962                        push_candidates(&mut fuzzy_match_candidates, key_index, item.title);
1963                        push_candidates(&mut fuzzy_match_candidates, key_index, item.description);
1964                    }
1965                    SettingsPageItem::SectionHeader(header) => {
1966                        documents.push(bm25::Document {
1967                            id: key_index,
1968                            contents: header.to_string(),
1969                        });
1970                        push_candidates(&mut fuzzy_match_candidates, key_index, header);
1971                        header_index = item_index;
1972                        header_str = *header;
1973                    }
1974                    SettingsPageItem::SubPageLink(sub_page_link) => {
1975                        json_path = sub_page_link.json_path;
1976                        documents.push(bm25::Document {
1977                            id: key_index,
1978                            contents: [page.title, header_str, sub_page_link.title.as_ref()]
1979                                .join("\n"),
1980                        });
1981                        push_candidates(
1982                            &mut fuzzy_match_candidates,
1983                            key_index,
1984                            sub_page_link.title.as_ref(),
1985                        );
1986                    }
1987                    SettingsPageItem::ActionLink(action_link) => {
1988                        documents.push(bm25::Document {
1989                            id: key_index,
1990                            contents: [page.title, header_str, action_link.title.as_ref()]
1991                                .join("\n"),
1992                        });
1993                        push_candidates(
1994                            &mut fuzzy_match_candidates,
1995                            key_index,
1996                            action_link.title.as_ref(),
1997                        );
1998                    }
1999                }
2000                push_candidates(&mut fuzzy_match_candidates, key_index, page.title);
2001                push_candidates(&mut fuzzy_match_candidates, key_index, header_str);
2002
2003                key_lut.push(SearchKeyLUTEntry {
2004                    page_index,
2005                    header_index,
2006                    item_index,
2007                    json_path,
2008                });
2009            }
2010        }
2011        let engine =
2012            bm25::SearchEngineBuilder::with_documents(bm25::Language::English, documents).build();
2013        self.search_index = Some(Arc::new(SearchIndex {
2014            bm25_engine: engine,
2015            key_lut,
2016            fuzzy_match_candidates,
2017        }));
2018    }
2019
2020    fn build_content_handles(&mut self, window: &mut Window, cx: &mut Context<SettingsWindow>) {
2021        self.content_handles = self
2022            .pages
2023            .iter()
2024            .map(|page| {
2025                std::iter::repeat_with(|| NonFocusableHandle::new(0, false, window, cx))
2026                    .take(page.items.len())
2027                    .collect()
2028            })
2029            .collect::<Vec<_>>();
2030    }
2031
2032    fn reset_list_state(&mut self) {
2033        let mut visible_items_count = self.visible_page_items().count();
2034
2035        if visible_items_count > 0 {
2036            // show page title if page is non empty
2037            visible_items_count += 1;
2038        }
2039
2040        self.list_state.reset(visible_items_count);
2041    }
2042
2043    fn build_ui(&mut self, window: &mut Window, cx: &mut Context<SettingsWindow>) {
2044        if self.pages.is_empty() {
2045            self.pages = page_data::settings_data(cx);
2046            self.build_navbar(cx);
2047            self.setup_navbar_focus_subscriptions(window, cx);
2048            self.build_content_handles(window, cx);
2049        }
2050        self.sub_page_stack.clear();
2051        // PERF: doesn't have to be rebuilt, can just be filled with true. pages is constant once it is built
2052        self.build_filter_table();
2053        self.reset_list_state();
2054        self.update_matches(cx);
2055
2056        cx.notify();
2057    }
2058
2059    #[track_caller]
2060    fn fetch_files(&mut self, window: &mut Window, cx: &mut Context<SettingsWindow>) {
2061        self.worktree_root_dirs.clear();
2062        let prev_files = self.files.clone();
2063        let settings_store = cx.global::<SettingsStore>();
2064        let mut ui_files = vec![];
2065        let mut all_files = settings_store.get_all_files();
2066        if !all_files.contains(&settings::SettingsFile::User) {
2067            all_files.push(settings::SettingsFile::User);
2068        }
2069        for file in all_files {
2070            let Some(settings_ui_file) = SettingsUiFile::from_settings(file) else {
2071                continue;
2072            };
2073            if settings_ui_file.is_server() {
2074                continue;
2075            }
2076
2077            if let Some(worktree_id) = settings_ui_file.worktree_id() {
2078                let directory_name = all_projects(cx)
2079                    .find_map(|project| project.read(cx).worktree_for_id(worktree_id, cx))
2080                    .and_then(|worktree| worktree.read(cx).root_dir())
2081                    .and_then(|root_dir| {
2082                        root_dir
2083                            .file_name()
2084                            .map(|os_string| os_string.to_string_lossy().to_string())
2085                    });
2086
2087                let Some(directory_name) = directory_name else {
2088                    log::error!(
2089                        "No directory name found for settings file at worktree ID: {}",
2090                        worktree_id
2091                    );
2092                    continue;
2093                };
2094
2095                self.worktree_root_dirs.insert(worktree_id, directory_name);
2096            }
2097
2098            let focus_handle = prev_files
2099                .iter()
2100                .find_map(|(prev_file, handle)| {
2101                    (prev_file == &settings_ui_file).then(|| handle.clone())
2102                })
2103                .unwrap_or_else(|| cx.focus_handle().tab_index(0).tab_stop(true));
2104            ui_files.push((settings_ui_file, focus_handle));
2105        }
2106
2107        ui_files.reverse();
2108
2109        let mut missing_worktrees = Vec::new();
2110
2111        for worktree in all_projects(cx)
2112            .flat_map(|project| project.read(cx).visible_worktrees(cx))
2113            .filter(|tree| !self.worktree_root_dirs.contains_key(&tree.read(cx).id()))
2114        {
2115            let worktree = worktree.read(cx);
2116            let worktree_id = worktree.id();
2117            let Some(directory_name) = worktree.root_dir().and_then(|file| {
2118                file.file_name()
2119                    .map(|os_string| os_string.to_string_lossy().to_string())
2120            }) else {
2121                continue;
2122            };
2123
2124            missing_worktrees.push((worktree_id, directory_name.clone()));
2125            let path = RelPath::empty().to_owned().into_arc();
2126
2127            let settings_ui_file = SettingsUiFile::Project((worktree_id, path));
2128
2129            let focus_handle = prev_files
2130                .iter()
2131                .find_map(|(prev_file, handle)| {
2132                    (prev_file == &settings_ui_file).then(|| handle.clone())
2133                })
2134                .unwrap_or_else(|| cx.focus_handle().tab_index(0).tab_stop(true));
2135
2136            ui_files.push((settings_ui_file, focus_handle));
2137        }
2138
2139        self.worktree_root_dirs.extend(missing_worktrees);
2140
2141        self.files = ui_files;
2142        let current_file_still_exists = self
2143            .files
2144            .iter()
2145            .any(|(file, _)| file == &self.current_file);
2146        if !current_file_still_exists {
2147            self.change_file(0, window, cx);
2148        }
2149    }
2150
2151    fn open_navbar_entry_page(&mut self, navbar_entry: usize) {
2152        if !self.is_nav_entry_visible(navbar_entry) {
2153            self.open_first_nav_page();
2154        }
2155
2156        let is_new_page = self.navbar_entries[self.navbar_entry].page_index
2157            != self.navbar_entries[navbar_entry].page_index;
2158        self.navbar_entry = navbar_entry;
2159
2160        // We only need to reset visible items when updating matches
2161        // and selecting a new page
2162        if is_new_page {
2163            self.reset_list_state();
2164        }
2165
2166        self.sub_page_stack.clear();
2167    }
2168
2169    fn open_first_nav_page(&mut self) {
2170        let Some(first_navbar_entry_index) = self.visible_navbar_entries().next().map(|e| e.0)
2171        else {
2172            return;
2173        };
2174        self.open_navbar_entry_page(first_navbar_entry_index);
2175    }
2176
2177    fn change_file(&mut self, ix: usize, window: &mut Window, cx: &mut Context<SettingsWindow>) {
2178        if ix >= self.files.len() {
2179            self.current_file = SettingsUiFile::User;
2180            self.build_ui(window, cx);
2181            return;
2182        }
2183
2184        if self.files[ix].0 == self.current_file {
2185            return;
2186        }
2187        self.current_file = self.files[ix].0.clone();
2188
2189        if let SettingsUiFile::Project((_, _)) = &self.current_file {
2190            telemetry::event!("Setting Project Clicked");
2191        }
2192
2193        self.build_ui(window, cx);
2194
2195        if self
2196            .visible_navbar_entries()
2197            .any(|(index, _)| index == self.navbar_entry)
2198        {
2199            self.open_and_scroll_to_navbar_entry(self.navbar_entry, None, true, window, cx);
2200        } else {
2201            self.open_first_nav_page();
2202        };
2203    }
2204
2205    fn render_files_header(
2206        &self,
2207        window: &mut Window,
2208        cx: &mut Context<SettingsWindow>,
2209    ) -> impl IntoElement {
2210        static OVERFLOW_LIMIT: usize = 1;
2211
2212        let file_button =
2213            |ix, file: &SettingsUiFile, focus_handle, cx: &mut Context<SettingsWindow>| {
2214                Button::new(
2215                    ix,
2216                    self.display_name(&file)
2217                        .expect("Files should always have a name"),
2218                )
2219                .toggle_state(file == &self.current_file)
2220                .selected_style(ButtonStyle::Tinted(ui::TintColor::Accent))
2221                .track_focus(focus_handle)
2222                .on_click(cx.listener({
2223                    let focus_handle = focus_handle.clone();
2224                    move |this, _: &gpui::ClickEvent, window, cx| {
2225                        this.change_file(ix, window, cx);
2226                        focus_handle.focus(window, cx);
2227                    }
2228                }))
2229            };
2230
2231        let this = cx.entity();
2232
2233        let selected_file_ix = self
2234            .files
2235            .iter()
2236            .enumerate()
2237            .skip(OVERFLOW_LIMIT)
2238            .find_map(|(ix, (file, _))| {
2239                if file == &self.current_file {
2240                    Some(ix)
2241                } else {
2242                    None
2243                }
2244            })
2245            .unwrap_or(OVERFLOW_LIMIT);
2246        let edit_in_json_id = SharedString::new(format!("edit-in-json-{}", selected_file_ix));
2247
2248        h_flex()
2249            .w_full()
2250            .gap_1()
2251            .justify_between()
2252            .track_focus(&self.files_focus_handle)
2253            .tab_group()
2254            .tab_index(HEADER_GROUP_TAB_INDEX)
2255            .child(
2256                h_flex()
2257                    .gap_1()
2258                    .children(
2259                        self.files.iter().enumerate().take(OVERFLOW_LIMIT).map(
2260                            |(ix, (file, focus_handle))| file_button(ix, file, focus_handle, cx),
2261                        ),
2262                    )
2263                    .when(self.files.len() > OVERFLOW_LIMIT, |div| {
2264                        let (file, focus_handle) = &self.files[selected_file_ix];
2265
2266                        div.child(file_button(selected_file_ix, file, focus_handle, cx))
2267                            .when(self.files.len() > OVERFLOW_LIMIT + 1, |div| {
2268                                div.child(
2269                                    DropdownMenu::new(
2270                                        "more-files",
2271                                        format!("+{}", self.files.len() - (OVERFLOW_LIMIT + 1)),
2272                                        ContextMenu::build(window, cx, move |mut menu, _, _| {
2273                                            for (mut ix, (file, focus_handle)) in self
2274                                                .files
2275                                                .iter()
2276                                                .enumerate()
2277                                                .skip(OVERFLOW_LIMIT + 1)
2278                                            {
2279                                                let (display_name, focus_handle) =
2280                                                    if selected_file_ix == ix {
2281                                                        ix = OVERFLOW_LIMIT;
2282                                                        (
2283                                                            self.display_name(&self.files[ix].0),
2284                                                            self.files[ix].1.clone(),
2285                                                        )
2286                                                    } else {
2287                                                        (
2288                                                            self.display_name(&file),
2289                                                            focus_handle.clone(),
2290                                                        )
2291                                                    };
2292
2293                                                menu = menu.entry(
2294                                                    display_name
2295                                                        .expect("Files should always have a name"),
2296                                                    None,
2297                                                    {
2298                                                        let this = this.clone();
2299                                                        move |window, cx| {
2300                                                            this.update(cx, |this, cx| {
2301                                                                this.change_file(ix, window, cx);
2302                                                            });
2303                                                            focus_handle.focus(window, cx);
2304                                                        }
2305                                                    },
2306                                                );
2307                                            }
2308
2309                                            menu
2310                                        }),
2311                                    )
2312                                    .style(DropdownStyle::Subtle)
2313                                    .trigger_tooltip(Tooltip::text("View Other Projects"))
2314                                    .trigger_icon(IconName::ChevronDown)
2315                                    .attach(gpui::Corner::BottomLeft)
2316                                    .offset(gpui::Point {
2317                                        x: px(0.0),
2318                                        y: px(2.0),
2319                                    })
2320                                    .tab_index(0),
2321                                )
2322                            })
2323                    }),
2324            )
2325            .child(
2326                Button::new(edit_in_json_id, "Edit in settings.json")
2327                    .tab_index(0_isize)
2328                    .style(ButtonStyle::OutlinedGhost)
2329                    .tooltip(Tooltip::for_action_title_in(
2330                        "Edit in settings.json",
2331                        &OpenCurrentFile,
2332                        &self.focus_handle,
2333                    ))
2334                    .on_click(cx.listener(|this, _, window, cx| {
2335                        this.open_current_settings_file(window, cx);
2336                    })),
2337            )
2338    }
2339
2340    pub(crate) fn display_name(&self, file: &SettingsUiFile) -> Option<String> {
2341        match file {
2342            SettingsUiFile::User => Some("User".to_string()),
2343            SettingsUiFile::Project((worktree_id, path)) => self
2344                .worktree_root_dirs
2345                .get(&worktree_id)
2346                .map(|directory_name| {
2347                    let path_style = PathStyle::local();
2348                    if path.is_empty() {
2349                        directory_name.clone()
2350                    } else {
2351                        format!(
2352                            "{}{}{}",
2353                            directory_name,
2354                            path_style.primary_separator(),
2355                            path.display(path_style)
2356                        )
2357                    }
2358                }),
2359            SettingsUiFile::Server(file) => Some(file.to_string()),
2360        }
2361    }
2362
2363    // TODO:
2364    //  Reconsider this after preview launch
2365    // fn file_location_str(&self) -> String {
2366    //     match &self.current_file {
2367    //         SettingsUiFile::User => "settings.json".to_string(),
2368    //         SettingsUiFile::Project((worktree_id, path)) => self
2369    //             .worktree_root_dirs
2370    //             .get(&worktree_id)
2371    //             .map(|directory_name| {
2372    //                 let path_style = PathStyle::local();
2373    //                 let file_path = path.join(paths::local_settings_file_relative_path());
2374    //                 format!(
2375    //                     "{}{}{}",
2376    //                     directory_name,
2377    //                     path_style.separator(),
2378    //                     file_path.display(path_style)
2379    //                 )
2380    //             })
2381    //             .expect("Current file should always be present in root dir map"),
2382    //         SettingsUiFile::Server(file) => file.to_string(),
2383    //     }
2384    // }
2385
2386    fn render_search(&self, _window: &mut Window, cx: &mut App) -> Div {
2387        h_flex()
2388            .py_1()
2389            .px_1p5()
2390            .mb_3()
2391            .gap_1p5()
2392            .rounded_sm()
2393            .bg(cx.theme().colors().editor_background)
2394            .border_1()
2395            .border_color(cx.theme().colors().border)
2396            .child(Icon::new(IconName::MagnifyingGlass).color(Color::Muted))
2397            .child(self.search_bar.clone())
2398    }
2399
2400    fn render_nav(
2401        &self,
2402        window: &mut Window,
2403        cx: &mut Context<SettingsWindow>,
2404    ) -> impl IntoElement {
2405        let visible_count = self.visible_navbar_entries().count();
2406
2407        let focus_keybind_label = if self
2408            .navbar_focus_handle
2409            .read(cx)
2410            .handle
2411            .contains_focused(window, cx)
2412            || self
2413                .visible_navbar_entries()
2414                .any(|(_, entry)| entry.focus_handle.is_focused(window))
2415        {
2416            "Focus Content"
2417        } else {
2418            "Focus Navbar"
2419        };
2420
2421        let mut key_context = KeyContext::new_with_defaults();
2422        key_context.add("NavigationMenu");
2423        key_context.add("menu");
2424        if self.search_bar.focus_handle(cx).is_focused(window) {
2425            key_context.add("search");
2426        }
2427
2428        v_flex()
2429            .key_context(key_context)
2430            .on_action(cx.listener(|this, _: &CollapseNavEntry, window, cx| {
2431                let Some(focused_entry) = this.focused_nav_entry(window, cx) else {
2432                    return;
2433                };
2434                let focused_entry_parent = this.root_entry_containing(focused_entry);
2435                if this.navbar_entries[focused_entry_parent].expanded {
2436                    this.toggle_navbar_entry(focused_entry_parent);
2437                    window.focus(&this.navbar_entries[focused_entry_parent].focus_handle, cx);
2438                }
2439                cx.notify();
2440            }))
2441            .on_action(cx.listener(|this, _: &ExpandNavEntry, window, cx| {
2442                let Some(focused_entry) = this.focused_nav_entry(window, cx) else {
2443                    return;
2444                };
2445                if !this.navbar_entries[focused_entry].is_root {
2446                    return;
2447                }
2448                if !this.navbar_entries[focused_entry].expanded {
2449                    this.toggle_navbar_entry(focused_entry);
2450                }
2451                cx.notify();
2452            }))
2453            .on_action(
2454                cx.listener(|this, _: &FocusPreviousRootNavEntry, window, cx| {
2455                    let entry_index = this
2456                        .focused_nav_entry(window, cx)
2457                        .unwrap_or(this.navbar_entry);
2458                    let mut root_index = None;
2459                    for (index, entry) in this.visible_navbar_entries() {
2460                        if index >= entry_index {
2461                            break;
2462                        }
2463                        if entry.is_root {
2464                            root_index = Some(index);
2465                        }
2466                    }
2467                    let Some(previous_root_index) = root_index else {
2468                        return;
2469                    };
2470                    this.focus_and_scroll_to_nav_entry(previous_root_index, window, cx);
2471                }),
2472            )
2473            .on_action(cx.listener(|this, _: &FocusNextRootNavEntry, window, cx| {
2474                let entry_index = this
2475                    .focused_nav_entry(window, cx)
2476                    .unwrap_or(this.navbar_entry);
2477                let mut root_index = None;
2478                for (index, entry) in this.visible_navbar_entries() {
2479                    if index <= entry_index {
2480                        continue;
2481                    }
2482                    if entry.is_root {
2483                        root_index = Some(index);
2484                        break;
2485                    }
2486                }
2487                let Some(next_root_index) = root_index else {
2488                    return;
2489                };
2490                this.focus_and_scroll_to_nav_entry(next_root_index, window, cx);
2491            }))
2492            .on_action(cx.listener(|this, _: &FocusFirstNavEntry, window, cx| {
2493                if let Some((first_entry_index, _)) = this.visible_navbar_entries().next() {
2494                    this.focus_and_scroll_to_nav_entry(first_entry_index, window, cx);
2495                }
2496            }))
2497            .on_action(cx.listener(|this, _: &FocusLastNavEntry, window, cx| {
2498                if let Some((last_entry_index, _)) = this.visible_navbar_entries().last() {
2499                    this.focus_and_scroll_to_nav_entry(last_entry_index, window, cx);
2500                }
2501            }))
2502            .on_action(cx.listener(|this, _: &FocusNextNavEntry, window, cx| {
2503                let entry_index = this
2504                    .focused_nav_entry(window, cx)
2505                    .unwrap_or(this.navbar_entry);
2506                let mut next_index = None;
2507                for (index, _) in this.visible_navbar_entries() {
2508                    if index > entry_index {
2509                        next_index = Some(index);
2510                        break;
2511                    }
2512                }
2513                let Some(next_entry_index) = next_index else {
2514                    return;
2515                };
2516                this.open_and_scroll_to_navbar_entry(
2517                    next_entry_index,
2518                    Some(gpui::ScrollStrategy::Bottom),
2519                    false,
2520                    window,
2521                    cx,
2522                );
2523            }))
2524            .on_action(cx.listener(|this, _: &FocusPreviousNavEntry, window, cx| {
2525                let entry_index = this
2526                    .focused_nav_entry(window, cx)
2527                    .unwrap_or(this.navbar_entry);
2528                let mut prev_index = None;
2529                for (index, _) in this.visible_navbar_entries() {
2530                    if index >= entry_index {
2531                        break;
2532                    }
2533                    prev_index = Some(index);
2534                }
2535                let Some(prev_entry_index) = prev_index else {
2536                    return;
2537                };
2538                this.open_and_scroll_to_navbar_entry(
2539                    prev_entry_index,
2540                    Some(gpui::ScrollStrategy::Top),
2541                    false,
2542                    window,
2543                    cx,
2544                );
2545            }))
2546            .w_56()
2547            .h_full()
2548            .p_2p5()
2549            .when(cfg!(target_os = "macos"), |this| this.pt_10())
2550            .flex_none()
2551            .border_r_1()
2552            .border_color(cx.theme().colors().border)
2553            .bg(cx.theme().colors().panel_background)
2554            .child(self.render_search(window, cx))
2555            .child(
2556                v_flex()
2557                    .flex_1()
2558                    .overflow_hidden()
2559                    .track_focus(&self.navbar_focus_handle.focus_handle(cx))
2560                    .tab_group()
2561                    .tab_index(NAVBAR_GROUP_TAB_INDEX)
2562                    .child(
2563                        uniform_list(
2564                            "settings-ui-nav-bar",
2565                            visible_count + 1,
2566                            cx.processor(move |this, range: Range<usize>, _, cx| {
2567                                this.visible_navbar_entries()
2568                                    .skip(range.start.saturating_sub(1))
2569                                    .take(range.len())
2570                                    .map(|(entry_index, entry)| {
2571                                        TreeViewItem::new(
2572                                            ("settings-ui-navbar-entry", entry_index),
2573                                            entry.title,
2574                                        )
2575                                        .track_focus(&entry.focus_handle)
2576                                        .root_item(entry.is_root)
2577                                        .toggle_state(this.is_navbar_entry_selected(entry_index))
2578                                        .when(entry.is_root, |item| {
2579                                            item.expanded(entry.expanded || this.has_query)
2580                                                .on_toggle(cx.listener(
2581                                                    move |this, _, window, cx| {
2582                                                        this.toggle_navbar_entry(entry_index);
2583                                                        window.focus(
2584                                                            &this.navbar_entries[entry_index]
2585                                                                .focus_handle,
2586                                                            cx,
2587                                                        );
2588                                                        cx.notify();
2589                                                    },
2590                                                ))
2591                                        })
2592                                        .on_click({
2593                                            let category = this.pages[entry.page_index].title;
2594                                            let subcategory =
2595                                                (!entry.is_root).then_some(entry.title);
2596
2597                                            cx.listener(move |this, _, window, cx| {
2598                                                telemetry::event!(
2599                                                    "Settings Navigation Clicked",
2600                                                    category = category,
2601                                                    subcategory = subcategory
2602                                                );
2603
2604                                                this.open_and_scroll_to_navbar_entry(
2605                                                    entry_index,
2606                                                    None,
2607                                                    true,
2608                                                    window,
2609                                                    cx,
2610                                                );
2611                                            })
2612                                        })
2613                                    })
2614                                    .collect()
2615                            }),
2616                        )
2617                        .size_full()
2618                        .track_scroll(&self.navbar_scroll_handle),
2619                    )
2620                    .vertical_scrollbar_for(&self.navbar_scroll_handle, window, cx),
2621            )
2622            .child(
2623                h_flex()
2624                    .w_full()
2625                    .h_8()
2626                    .p_2()
2627                    .pb_0p5()
2628                    .flex_shrink_0()
2629                    .border_t_1()
2630                    .border_color(cx.theme().colors().border_variant)
2631                    .child(
2632                        KeybindingHint::new(
2633                            KeyBinding::for_action_in(
2634                                &ToggleFocusNav,
2635                                &self.navbar_focus_handle.focus_handle(cx),
2636                                cx,
2637                            ),
2638                            cx.theme().colors().surface_background.opacity(0.5),
2639                        )
2640                        .suffix(focus_keybind_label),
2641                    ),
2642            )
2643    }
2644
2645    fn open_and_scroll_to_navbar_entry(
2646        &mut self,
2647        navbar_entry_index: usize,
2648        scroll_strategy: Option<gpui::ScrollStrategy>,
2649        focus_content: bool,
2650        window: &mut Window,
2651        cx: &mut Context<Self>,
2652    ) {
2653        self.open_navbar_entry_page(navbar_entry_index);
2654        cx.notify();
2655
2656        let mut handle_to_focus = None;
2657
2658        if self.navbar_entries[navbar_entry_index].is_root
2659            || !self.is_nav_entry_visible(navbar_entry_index)
2660        {
2661            if let Some(scroll_handle) = self.current_sub_page_scroll_handle() {
2662                scroll_handle.set_offset(point(px(0.), px(0.)));
2663            }
2664
2665            if focus_content {
2666                let Some(first_item_index) =
2667                    self.visible_page_items().next().map(|(index, _)| index)
2668                else {
2669                    return;
2670                };
2671                handle_to_focus = Some(self.focus_handle_for_content_element(first_item_index, cx));
2672            } else if !self.is_nav_entry_visible(navbar_entry_index) {
2673                let Some(first_visible_nav_entry_index) =
2674                    self.visible_navbar_entries().next().map(|(index, _)| index)
2675                else {
2676                    return;
2677                };
2678                self.focus_and_scroll_to_nav_entry(first_visible_nav_entry_index, window, cx);
2679            } else {
2680                handle_to_focus =
2681                    Some(self.navbar_entries[navbar_entry_index].focus_handle.clone());
2682            }
2683        } else {
2684            let entry_item_index = self.navbar_entries[navbar_entry_index]
2685                .item_index
2686                .expect("Non-root items should have an item index");
2687            self.scroll_to_content_item(entry_item_index, window, cx);
2688            if focus_content {
2689                handle_to_focus = Some(self.focus_handle_for_content_element(entry_item_index, cx));
2690            } else {
2691                handle_to_focus =
2692                    Some(self.navbar_entries[navbar_entry_index].focus_handle.clone());
2693            }
2694        }
2695
2696        if let Some(scroll_strategy) = scroll_strategy
2697            && let Some(logical_entry_index) = self
2698                .visible_navbar_entries()
2699                .into_iter()
2700                .position(|(index, _)| index == navbar_entry_index)
2701        {
2702            self.navbar_scroll_handle
2703                .scroll_to_item(logical_entry_index + 1, scroll_strategy);
2704        }
2705
2706        // Page scroll handle updates the active item index
2707        // in it's next paint call after using scroll_handle.scroll_to_top_of_item
2708        // The call after that updates the offset of the scroll handle. So to
2709        // ensure the scroll handle doesn't lag behind we need to render three frames
2710        // back to back.
2711        cx.on_next_frame(window, move |_, window, cx| {
2712            if let Some(handle) = handle_to_focus.as_ref() {
2713                window.focus(handle, cx);
2714            }
2715
2716            cx.on_next_frame(window, |_, _, cx| {
2717                cx.notify();
2718            });
2719            cx.notify();
2720        });
2721        cx.notify();
2722    }
2723
2724    fn scroll_to_content_item(
2725        &self,
2726        content_item_index: usize,
2727        _window: &mut Window,
2728        cx: &mut Context<Self>,
2729    ) {
2730        let index = self
2731            .visible_page_items()
2732            .position(|(index, _)| index == content_item_index)
2733            .unwrap_or(0);
2734        if index == 0 {
2735            if let Some(scroll_handle) = self.current_sub_page_scroll_handle() {
2736                scroll_handle.set_offset(point(px(0.), px(0.)));
2737            }
2738
2739            self.list_state.scroll_to(gpui::ListOffset {
2740                item_ix: 0,
2741                offset_in_item: px(0.),
2742            });
2743            return;
2744        }
2745        self.list_state.scroll_to(gpui::ListOffset {
2746            item_ix: index + 1,
2747            offset_in_item: px(0.),
2748        });
2749        cx.notify();
2750    }
2751
2752    fn is_nav_entry_visible(&self, nav_entry_index: usize) -> bool {
2753        self.visible_navbar_entries()
2754            .any(|(index, _)| index == nav_entry_index)
2755    }
2756
2757    fn focus_and_scroll_to_first_visible_nav_entry(
2758        &self,
2759        window: &mut Window,
2760        cx: &mut Context<Self>,
2761    ) {
2762        if let Some(nav_entry_index) = self.visible_navbar_entries().next().map(|(index, _)| index)
2763        {
2764            self.focus_and_scroll_to_nav_entry(nav_entry_index, window, cx);
2765        }
2766    }
2767
2768    fn focus_and_scroll_to_nav_entry(
2769        &self,
2770        nav_entry_index: usize,
2771        window: &mut Window,
2772        cx: &mut Context<Self>,
2773    ) {
2774        let Some(position) = self
2775            .visible_navbar_entries()
2776            .position(|(index, _)| index == nav_entry_index)
2777        else {
2778            return;
2779        };
2780        self.navbar_scroll_handle
2781            .scroll_to_item(position, gpui::ScrollStrategy::Top);
2782        window.focus(&self.navbar_entries[nav_entry_index].focus_handle, cx);
2783        cx.notify();
2784    }
2785
2786    fn current_sub_page_scroll_handle(&self) -> Option<&ScrollHandle> {
2787        self.sub_page_stack.last().map(|page| &page.scroll_handle)
2788    }
2789
2790    fn visible_page_items(&self) -> impl Iterator<Item = (usize, &SettingsPageItem)> {
2791        let page_idx = self.current_page_index();
2792
2793        self.current_page()
2794            .items
2795            .iter()
2796            .enumerate()
2797            .filter(move |&(item_index, _)| self.filter_table[page_idx][item_index])
2798    }
2799
2800    fn render_sub_page_breadcrumbs(&self) -> impl IntoElement {
2801        h_flex().gap_1().children(
2802            itertools::intersperse(
2803                std::iter::once(self.current_page().title.into()).chain(
2804                    self.sub_page_stack
2805                        .iter()
2806                        .enumerate()
2807                        .flat_map(|(index, page)| {
2808                            (index == 0)
2809                                .then(|| page.section_header.clone())
2810                                .into_iter()
2811                                .chain(std::iter::once(page.link.title.clone()))
2812                        }),
2813                ),
2814                "/".into(),
2815            )
2816            .map(|item| Label::new(item).color(Color::Muted)),
2817        )
2818    }
2819
2820    fn render_no_results(&self, cx: &App) -> impl IntoElement {
2821        let search_query = self.search_bar.read(cx).text(cx);
2822
2823        v_flex()
2824            .size_full()
2825            .items_center()
2826            .justify_center()
2827            .gap_1()
2828            .child(Label::new("No Results"))
2829            .child(
2830                Label::new(format!("No settings match \"{}\"", search_query))
2831                    .size(LabelSize::Small)
2832                    .color(Color::Muted),
2833            )
2834    }
2835
2836    fn render_current_page_items(
2837        &mut self,
2838        _window: &mut Window,
2839        cx: &mut Context<SettingsWindow>,
2840    ) -> impl IntoElement {
2841        let current_page_index = self.current_page_index();
2842        let mut page_content = v_flex().id("settings-ui-page").size_full();
2843
2844        let has_active_search = !self.search_bar.read(cx).is_empty(cx);
2845        let has_no_results = self.visible_page_items().next().is_none() && has_active_search;
2846
2847        if has_no_results {
2848            page_content = page_content.child(self.render_no_results(cx))
2849        } else {
2850            let last_non_header_index = self
2851                .visible_page_items()
2852                .filter_map(|(index, item)| {
2853                    (!matches!(item, SettingsPageItem::SectionHeader(_))).then_some(index)
2854                })
2855                .last();
2856
2857            let root_nav_label = self
2858                .navbar_entries
2859                .iter()
2860                .find(|entry| entry.is_root && entry.page_index == self.current_page_index())
2861                .map(|entry| entry.title);
2862
2863            let list_content = list(
2864                self.list_state.clone(),
2865                cx.processor(move |this, index, window, cx| {
2866                    if index == 0 {
2867                        return div()
2868                            .px_8()
2869                            .when(this.sub_page_stack.is_empty(), |this| {
2870                                this.when_some(root_nav_label, |this, title| {
2871                                    this.child(
2872                                        Label::new(title).size(LabelSize::Large).mt_2().mb_3(),
2873                                    )
2874                                })
2875                            })
2876                            .into_any_element();
2877                    }
2878
2879                    let mut visible_items = this.visible_page_items();
2880                    let Some((actual_item_index, item)) = visible_items.nth(index - 1) else {
2881                        return gpui::Empty.into_any_element();
2882                    };
2883
2884                    let no_bottom_border = visible_items
2885                        .next()
2886                        .map(|(_, item)| matches!(item, SettingsPageItem::SectionHeader(_)))
2887                        .unwrap_or(false);
2888
2889                    let is_last = Some(actual_item_index) == last_non_header_index;
2890
2891                    let item_focus_handle = this.content_handles[current_page_index]
2892                        [actual_item_index]
2893                        .focus_handle(cx);
2894
2895                    v_flex()
2896                        .id(("settings-page-item", actual_item_index))
2897                        .track_focus(&item_focus_handle)
2898                        .w_full()
2899                        .min_w_0()
2900                        .child(item.render(
2901                            this,
2902                            actual_item_index,
2903                            no_bottom_border || is_last,
2904                            window,
2905                            cx,
2906                        ))
2907                        .into_any_element()
2908                }),
2909            );
2910
2911            page_content = page_content.child(list_content.size_full())
2912        }
2913        page_content
2914    }
2915
2916    fn render_sub_page_items<'a, Items>(
2917        &self,
2918        items: Items,
2919        scroll_handle: &ScrollHandle,
2920        window: &mut Window,
2921        cx: &mut Context<SettingsWindow>,
2922    ) -> impl IntoElement
2923    where
2924        Items: Iterator<Item = (usize, &'a SettingsPageItem)>,
2925    {
2926        let page_content = v_flex()
2927            .id("settings-ui-page")
2928            .size_full()
2929            .overflow_y_scroll()
2930            .track_scroll(scroll_handle);
2931        self.render_sub_page_items_in(page_content, items, window, cx)
2932    }
2933
2934    fn render_sub_page_items_section<'a, Items>(
2935        &self,
2936        items: Items,
2937        window: &mut Window,
2938        cx: &mut Context<SettingsWindow>,
2939    ) -> impl IntoElement
2940    where
2941        Items: Iterator<Item = (usize, &'a SettingsPageItem)>,
2942    {
2943        let page_content = v_flex().id("settings-ui-sub-page-section").size_full();
2944        self.render_sub_page_items_in(page_content, items, window, cx)
2945    }
2946
2947    fn render_sub_page_items_in<'a, Items>(
2948        &self,
2949        page_content: Stateful<Div>,
2950        items: Items,
2951        window: &mut Window,
2952        cx: &mut Context<SettingsWindow>,
2953    ) -> impl IntoElement
2954    where
2955        Items: Iterator<Item = (usize, &'a SettingsPageItem)>,
2956    {
2957        let items: Vec<_> = items.collect();
2958        let items_len = items.len();
2959
2960        let has_active_search = !self.search_bar.read(cx).is_empty(cx);
2961        let has_no_results = items_len == 0 && has_active_search;
2962
2963        if has_no_results {
2964            page_content.child(self.render_no_results(cx))
2965        } else {
2966            let last_non_header_index = items
2967                .iter()
2968                .enumerate()
2969                .rev()
2970                .find(|(_, (_, item))| !matches!(item, SettingsPageItem::SectionHeader(_)))
2971                .map(|(index, _)| index);
2972
2973            let root_nav_label = self
2974                .navbar_entries
2975                .iter()
2976                .find(|entry| entry.is_root && entry.page_index == self.current_page_index())
2977                .map(|entry| entry.title);
2978
2979            page_content
2980                .when(self.sub_page_stack.is_empty(), |this| {
2981                    this.when_some(root_nav_label, |this, title| {
2982                        this.child(Label::new(title).size(LabelSize::Large).mt_2().mb_3())
2983                    })
2984                })
2985                .children(items.clone().into_iter().enumerate().map(
2986                    |(index, (actual_item_index, item))| {
2987                        let no_bottom_border =
2988                            items.get(index + 1).is_some_and(|(_, next_item)| {
2989                                matches!(next_item, SettingsPageItem::SectionHeader(_))
2990                            });
2991
2992                        let is_last = Some(index) == last_non_header_index;
2993
2994                        v_flex()
2995                            .w_full()
2996                            .min_w_0()
2997                            .id(("settings-page-item", actual_item_index))
2998                            .child(item.render(
2999                                self,
3000                                actual_item_index,
3001                                no_bottom_border || is_last,
3002                                window,
3003                                cx,
3004                            ))
3005                    },
3006                ))
3007        }
3008    }
3009
3010    fn render_page(
3011        &mut self,
3012        window: &mut Window,
3013        cx: &mut Context<SettingsWindow>,
3014    ) -> impl IntoElement {
3015        let page_header;
3016        let page_content;
3017
3018        if let Some(current_sub_page) = self.sub_page_stack.last() {
3019            page_header = h_flex()
3020                .w_full()
3021                .justify_between()
3022                .child(
3023                    h_flex()
3024                        .ml_neg_1p5()
3025                        .gap_1()
3026                        .child(
3027                            IconButton::new("back-btn", IconName::ArrowLeft)
3028                                .icon_size(IconSize::Small)
3029                                .shape(IconButtonShape::Square)
3030                                .on_click(cx.listener(|this, _, window, cx| {
3031                                    this.pop_sub_page(window, cx);
3032                                })),
3033                        )
3034                        .child(self.render_sub_page_breadcrumbs()),
3035                )
3036                .when(current_sub_page.link.in_json, |this| {
3037                    this.child(
3038                        Button::new("open-in-settings-file", "Edit in settings.json")
3039                            .tab_index(0_isize)
3040                            .style(ButtonStyle::OutlinedGhost)
3041                            .tooltip(Tooltip::for_action_title_in(
3042                                "Edit in settings.json",
3043                                &OpenCurrentFile,
3044                                &self.focus_handle,
3045                            ))
3046                            .on_click(cx.listener(|this, _, window, cx| {
3047                                this.open_current_settings_file(window, cx);
3048                            })),
3049                    )
3050                })
3051                .into_any_element();
3052
3053            let active_page_render_fn = &current_sub_page.link.render;
3054            page_content =
3055                (active_page_render_fn)(self, &current_sub_page.scroll_handle, window, cx);
3056        } else {
3057            page_header = self.render_files_header(window, cx).into_any_element();
3058
3059            page_content = self
3060                .render_current_page_items(window, cx)
3061                .into_any_element();
3062        }
3063
3064        let current_sub_page = self.sub_page_stack.last();
3065
3066        let mut warning_banner = gpui::Empty.into_any_element();
3067        if let Some(error) =
3068            SettingsStore::global(cx).error_for_file(self.current_file.to_settings())
3069        {
3070            fn banner(
3071                label: &'static str,
3072                error: String,
3073                shown_errors: &mut HashSet<String>,
3074                cx: &mut Context<SettingsWindow>,
3075            ) -> impl IntoElement {
3076                if shown_errors.insert(error.clone()) {
3077                    telemetry::event!("Settings Error Shown", label = label, error = &error);
3078                }
3079                Banner::new()
3080                    .severity(Severity::Warning)
3081                    .child(
3082                        v_flex()
3083                            .my_0p5()
3084                            .gap_0p5()
3085                            .child(Label::new(label))
3086                            .child(Label::new(error).size(LabelSize::Small).color(Color::Muted)),
3087                    )
3088                    .action_slot(
3089                        div().pr_1().pb_1().child(
3090                            Button::new("fix-in-json", "Fix in settings.json")
3091                                .tab_index(0_isize)
3092                                .style(ButtonStyle::Tinted(ui::TintColor::Warning))
3093                                .on_click(cx.listener(|this, _, window, cx| {
3094                                    this.open_current_settings_file(window, cx);
3095                                })),
3096                        ),
3097                    )
3098            }
3099
3100            let parse_error = error.parse_error();
3101            let parse_failed = parse_error.is_some();
3102
3103            warning_banner = v_flex()
3104                .gap_2()
3105                .when_some(parse_error, |this, err| {
3106                    this.child(banner(
3107                        "Failed to load your settings. Some values may be incorrect and changes may be lost.",
3108                        err,
3109                        &mut self.shown_errors,
3110                        cx,
3111                    ))
3112                })
3113                .map(|this| match &error.migration_status {
3114                    settings::MigrationStatus::Succeeded => this.child(banner(
3115                        "Your settings are out of date, and need to be updated.",
3116                        match &self.current_file {
3117                            SettingsUiFile::User => "They can be automatically migrated to the latest version.",
3118                            SettingsUiFile::Server(_) | SettingsUiFile::Project(_)  => "They must be manually migrated to the latest version."
3119                        }.to_string(),
3120                        &mut self.shown_errors,
3121                        cx,
3122                    )),
3123                    settings::MigrationStatus::Failed { error: err } if !parse_failed => this
3124                        .child(banner(
3125                            "Your settings file is out of date, automatic migration failed",
3126                            err.clone(),
3127                            &mut self.shown_errors,
3128                            cx,
3129                        )),
3130                    _ => this,
3131                })
3132                .into_any_element()
3133        }
3134
3135        v_flex()
3136            .id("settings-ui-page")
3137            .on_action(cx.listener(|this, _: &menu::SelectNext, window, cx| {
3138                if !this.sub_page_stack.is_empty() {
3139                    window.focus_next(cx);
3140                    return;
3141                }
3142                for (logical_index, (actual_index, _)) in this.visible_page_items().enumerate() {
3143                    let handle = this.content_handles[this.current_page_index()][actual_index]
3144                        .focus_handle(cx);
3145                    let mut offset = 1; // for page header
3146
3147                    if let Some((_, next_item)) = this.visible_page_items().nth(logical_index + 1)
3148                        && matches!(next_item, SettingsPageItem::SectionHeader(_))
3149                    {
3150                        offset += 1;
3151                    }
3152                    if handle.contains_focused(window, cx) {
3153                        let next_logical_index = logical_index + offset + 1;
3154                        this.list_state.scroll_to_reveal_item(next_logical_index);
3155                        // We need to render the next item to ensure it's focus handle is in the element tree
3156                        cx.on_next_frame(window, |_, window, cx| {
3157                            cx.notify();
3158                            cx.on_next_frame(window, |_, window, cx| {
3159                                window.focus_next(cx);
3160                                cx.notify();
3161                            });
3162                        });
3163                        cx.notify();
3164                        return;
3165                    }
3166                }
3167                window.focus_next(cx);
3168            }))
3169            .on_action(cx.listener(|this, _: &menu::SelectPrevious, window, cx| {
3170                if !this.sub_page_stack.is_empty() {
3171                    window.focus_prev(cx);
3172                    return;
3173                }
3174                let mut prev_was_header = false;
3175                for (logical_index, (actual_index, item)) in this.visible_page_items().enumerate() {
3176                    let is_header = matches!(item, SettingsPageItem::SectionHeader(_));
3177                    let handle = this.content_handles[this.current_page_index()][actual_index]
3178                        .focus_handle(cx);
3179                    let mut offset = 1; // for page header
3180
3181                    if prev_was_header {
3182                        offset -= 1;
3183                    }
3184                    if handle.contains_focused(window, cx) {
3185                        let next_logical_index = logical_index + offset - 1;
3186                        this.list_state.scroll_to_reveal_item(next_logical_index);
3187                        // We need to render the next item to ensure it's focus handle is in the element tree
3188                        cx.on_next_frame(window, |_, window, cx| {
3189                            cx.notify();
3190                            cx.on_next_frame(window, |_, window, cx| {
3191                                window.focus_prev(cx);
3192                                cx.notify();
3193                            });
3194                        });
3195                        cx.notify();
3196                        return;
3197                    }
3198                    prev_was_header = is_header;
3199                }
3200                window.focus_prev(cx);
3201            }))
3202            .when(current_sub_page.is_none(), |this| {
3203                this.vertical_scrollbar_for(&self.list_state, window, cx)
3204            })
3205            .when_some(current_sub_page, |this, current_sub_page| {
3206                this.custom_scrollbars(
3207                    Scrollbars::new(ui::ScrollAxes::Vertical)
3208                        .tracked_scroll_handle(&current_sub_page.scroll_handle)
3209                        .id((current_sub_page.link.title.clone(), 42)),
3210                    window,
3211                    cx,
3212                )
3213            })
3214            .track_focus(&self.content_focus_handle.focus_handle(cx))
3215            .pt_6()
3216            .gap_4()
3217            .flex_1()
3218            .bg(cx.theme().colors().editor_background)
3219            .child(
3220                v_flex()
3221                    .px_8()
3222                    .gap_2()
3223                    .child(page_header)
3224                    .child(warning_banner),
3225            )
3226            .child(
3227                div()
3228                    .flex_1()
3229                    .size_full()
3230                    .tab_group()
3231                    .tab_index(CONTENT_GROUP_TAB_INDEX)
3232                    .child(page_content),
3233            )
3234    }
3235
3236    /// This function will create a new settings file if one doesn't exist
3237    /// if the current file is a project settings with a valid worktree id
3238    /// We do this because the settings ui allows initializing project settings
3239    fn open_current_settings_file(&mut self, window: &mut Window, cx: &mut Context<Self>) {
3240        match &self.current_file {
3241            SettingsUiFile::User => {
3242                let Some(original_window) = self.original_window else {
3243                    return;
3244                };
3245                original_window
3246                    .update(cx, |workspace, window, cx| {
3247                        workspace
3248                            .with_local_or_wsl_workspace(window, cx, |workspace, window, cx| {
3249                                let project = workspace.project().clone();
3250
3251                                cx.spawn_in(window, async move |workspace, cx| {
3252                                    let (config_dir, settings_file) =
3253                                        project.update(cx, |project, cx| {
3254                                            (
3255                                                project.try_windows_path_to_wsl(
3256                                                    paths::config_dir().as_path(),
3257                                                    cx,
3258                                                ),
3259                                                project.try_windows_path_to_wsl(
3260                                                    paths::settings_file().as_path(),
3261                                                    cx,
3262                                                ),
3263                                            )
3264                                        });
3265                                    let config_dir = config_dir.await?;
3266                                    let settings_file = settings_file.await?;
3267                                    project
3268                                        .update(cx, |project, cx| {
3269                                            project.find_or_create_worktree(&config_dir, false, cx)
3270                                        })
3271                                        .await
3272                                        .ok();
3273                                    workspace
3274                                        .update_in(cx, |workspace, window, cx| {
3275                                            workspace.open_paths(
3276                                                vec![settings_file],
3277                                                OpenOptions {
3278                                                    visible: Some(OpenVisible::None),
3279                                                    ..Default::default()
3280                                                },
3281                                                None,
3282                                                window,
3283                                                cx,
3284                                            )
3285                                        })?
3286                                        .await;
3287
3288                                    workspace.update_in(cx, |_, window, cx| {
3289                                        window.activate_window();
3290                                        cx.notify();
3291                                    })
3292                                })
3293                                .detach();
3294                            })
3295                            .detach();
3296                    })
3297                    .ok();
3298
3299                window.remove_window();
3300            }
3301            SettingsUiFile::Project((worktree_id, path)) => {
3302                let settings_path = path.join(paths::local_settings_file_relative_path());
3303                let Some(app_state) = workspace::AppState::global(cx).upgrade() else {
3304                    return;
3305                };
3306
3307                let Some((worktree, corresponding_workspace)) = app_state
3308                    .workspace_store
3309                    .read(cx)
3310                    .workspaces()
3311                    .iter()
3312                    .find_map(|workspace| {
3313                        workspace
3314                            .read_with(cx, |workspace, cx| {
3315                                workspace
3316                                    .project()
3317                                    .read(cx)
3318                                    .worktree_for_id(*worktree_id, cx)
3319                            })
3320                            .ok()
3321                            .flatten()
3322                            .zip(Some(*workspace))
3323                    })
3324                else {
3325                    log::error!(
3326                        "No corresponding workspace contains worktree id: {}",
3327                        worktree_id
3328                    );
3329
3330                    return;
3331                };
3332
3333                let create_task = if worktree.read(cx).entry_for_path(&settings_path).is_some() {
3334                    None
3335                } else {
3336                    Some(worktree.update(cx, |tree, cx| {
3337                        tree.create_entry(
3338                            settings_path.clone(),
3339                            false,
3340                            Some(initial_project_settings_content().as_bytes().to_vec()),
3341                            cx,
3342                        )
3343                    }))
3344                };
3345
3346                let worktree_id = *worktree_id;
3347
3348                // TODO: move zed::open_local_file() APIs to this crate, and
3349                // re-implement the "initial_contents" behavior
3350                corresponding_workspace
3351                    .update(cx, |_, window, cx| {
3352                        cx.spawn_in(window, async move |workspace, cx| {
3353                            if let Some(create_task) = create_task {
3354                                create_task.await.ok()?;
3355                            };
3356
3357                            workspace
3358                                .update_in(cx, |workspace, window, cx| {
3359                                    workspace.open_path(
3360                                        (worktree_id, settings_path.clone()),
3361                                        None,
3362                                        true,
3363                                        window,
3364                                        cx,
3365                                    )
3366                                })
3367                                .ok()?
3368                                .await
3369                                .log_err()?;
3370
3371                            workspace
3372                                .update_in(cx, |_, window, cx| {
3373                                    window.activate_window();
3374                                    cx.notify();
3375                                })
3376                                .ok();
3377
3378                            Some(())
3379                        })
3380                        .detach();
3381                    })
3382                    .ok();
3383
3384                window.remove_window();
3385            }
3386            SettingsUiFile::Server(_) => {
3387                // Server files are not editable
3388                return;
3389            }
3390        };
3391    }
3392
3393    fn current_page_index(&self) -> usize {
3394        if self.navbar_entries.is_empty() {
3395            return 0;
3396        }
3397
3398        self.navbar_entries[self.navbar_entry].page_index
3399    }
3400
3401    fn current_page(&self) -> &SettingsPage {
3402        &self.pages[self.current_page_index()]
3403    }
3404
3405    fn is_navbar_entry_selected(&self, ix: usize) -> bool {
3406        ix == self.navbar_entry
3407    }
3408
3409    fn push_sub_page(
3410        &mut self,
3411        sub_page_link: SubPageLink,
3412        section_header: SharedString,
3413        window: &mut Window,
3414        cx: &mut Context<SettingsWindow>,
3415    ) {
3416        self.sub_page_stack
3417            .push(SubPage::new(sub_page_link, section_header));
3418        self.content_focus_handle.focus_handle(cx).focus(window, cx);
3419        cx.notify();
3420    }
3421
3422    fn pop_sub_page(&mut self, window: &mut Window, cx: &mut Context<SettingsWindow>) {
3423        self.sub_page_stack.pop();
3424        self.content_focus_handle.focus_handle(cx).focus(window, cx);
3425        cx.notify();
3426    }
3427
3428    fn focus_file_at_index(&mut self, index: usize, window: &mut Window, cx: &mut App) {
3429        if let Some((_, handle)) = self.files.get(index) {
3430            handle.focus(window, cx);
3431        }
3432    }
3433
3434    fn focused_file_index(&self, window: &Window, cx: &Context<Self>) -> usize {
3435        if self.files_focus_handle.contains_focused(window, cx)
3436            && let Some(index) = self
3437                .files
3438                .iter()
3439                .position(|(_, handle)| handle.is_focused(window))
3440        {
3441            return index;
3442        }
3443        if let Some(current_file_index) = self
3444            .files
3445            .iter()
3446            .position(|(file, _)| file == &self.current_file)
3447        {
3448            return current_file_index;
3449        }
3450        0
3451    }
3452
3453    fn focus_handle_for_content_element(
3454        &self,
3455        actual_item_index: usize,
3456        cx: &Context<Self>,
3457    ) -> FocusHandle {
3458        let page_index = self.current_page_index();
3459        self.content_handles[page_index][actual_item_index].focus_handle(cx)
3460    }
3461
3462    fn focused_nav_entry(&self, window: &Window, cx: &App) -> Option<usize> {
3463        if !self
3464            .navbar_focus_handle
3465            .focus_handle(cx)
3466            .contains_focused(window, cx)
3467        {
3468            return None;
3469        }
3470        for (index, entry) in self.navbar_entries.iter().enumerate() {
3471            if entry.focus_handle.is_focused(window) {
3472                return Some(index);
3473            }
3474        }
3475        None
3476    }
3477
3478    fn root_entry_containing(&self, nav_entry_index: usize) -> usize {
3479        let mut index = Some(nav_entry_index);
3480        while let Some(prev_index) = index
3481            && !self.navbar_entries[prev_index].is_root
3482        {
3483            index = prev_index.checked_sub(1);
3484        }
3485        return index.expect("No root entry found");
3486    }
3487}
3488
3489impl Render for SettingsWindow {
3490    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
3491        let ui_font = theme::setup_ui_font(window, cx);
3492
3493        client_side_decorations(
3494            v_flex()
3495                .text_color(cx.theme().colors().text)
3496                .size_full()
3497                .children(self.title_bar.clone())
3498                .child(
3499                    div()
3500                        .id("settings-window")
3501                        .key_context("SettingsWindow")
3502                        .track_focus(&self.focus_handle)
3503                        .on_action(cx.listener(|this, _: &OpenCurrentFile, window, cx| {
3504                            this.open_current_settings_file(window, cx);
3505                        }))
3506                        .on_action(|_: &Minimize, window, _cx| {
3507                            window.minimize_window();
3508                        })
3509                        .on_action(cx.listener(|this, _: &search::FocusSearch, window, cx| {
3510                            this.search_bar.focus_handle(cx).focus(window, cx);
3511                        }))
3512                        .on_action(cx.listener(|this, _: &ToggleFocusNav, window, cx| {
3513                            if this
3514                                .navbar_focus_handle
3515                                .focus_handle(cx)
3516                                .contains_focused(window, cx)
3517                            {
3518                                this.open_and_scroll_to_navbar_entry(
3519                                    this.navbar_entry,
3520                                    None,
3521                                    true,
3522                                    window,
3523                                    cx,
3524                                );
3525                            } else {
3526                                this.focus_and_scroll_to_nav_entry(this.navbar_entry, window, cx);
3527                            }
3528                        }))
3529                        .on_action(cx.listener(
3530                            |this, FocusFile(file_index): &FocusFile, window, cx| {
3531                                this.focus_file_at_index(*file_index as usize, window, cx);
3532                            },
3533                        ))
3534                        .on_action(cx.listener(|this, _: &FocusNextFile, window, cx| {
3535                            let next_index = usize::min(
3536                                this.focused_file_index(window, cx) + 1,
3537                                this.files.len().saturating_sub(1),
3538                            );
3539                            this.focus_file_at_index(next_index, window, cx);
3540                        }))
3541                        .on_action(cx.listener(|this, _: &FocusPreviousFile, window, cx| {
3542                            let prev_index = this.focused_file_index(window, cx).saturating_sub(1);
3543                            this.focus_file_at_index(prev_index, window, cx);
3544                        }))
3545                        .on_action(cx.listener(|this, _: &menu::SelectNext, window, cx| {
3546                            if this
3547                                .search_bar
3548                                .focus_handle(cx)
3549                                .contains_focused(window, cx)
3550                            {
3551                                this.focus_and_scroll_to_first_visible_nav_entry(window, cx);
3552                            } else {
3553                                window.focus_next(cx);
3554                            }
3555                        }))
3556                        .on_action(|_: &menu::SelectPrevious, window, cx| {
3557                            window.focus_prev(cx);
3558                        })
3559                        .flex()
3560                        .flex_row()
3561                        .flex_1()
3562                        .min_h_0()
3563                        .font(ui_font)
3564                        .bg(cx.theme().colors().background)
3565                        .text_color(cx.theme().colors().text)
3566                        .when(!cfg!(target_os = "macos"), |this| {
3567                            this.border_t_1().border_color(cx.theme().colors().border)
3568                        })
3569                        .child(self.render_nav(window, cx))
3570                        .child(self.render_page(window, cx)),
3571                ),
3572            window,
3573            cx,
3574        )
3575    }
3576}
3577
3578fn all_projects(cx: &App) -> impl Iterator<Item = Entity<project::Project>> {
3579    workspace::AppState::global(cx)
3580        .upgrade()
3581        .map(|app_state| {
3582            app_state
3583                .workspace_store
3584                .read(cx)
3585                .workspaces()
3586                .iter()
3587                .filter_map(|workspace| Some(workspace.read(cx).ok()?.project().clone()))
3588        })
3589        .into_iter()
3590        .flatten()
3591}
3592
3593fn update_settings_file(
3594    file: SettingsUiFile,
3595    file_name: Option<&'static str>,
3596    cx: &mut App,
3597    update: impl 'static + Send + FnOnce(&mut SettingsContent, &App),
3598) -> Result<()> {
3599    telemetry::event!("Settings Change", setting = file_name, type = file.setting_type());
3600
3601    match file {
3602        SettingsUiFile::Project((worktree_id, rel_path)) => {
3603            let rel_path = rel_path.join(paths::local_settings_file_relative_path());
3604            let Some((worktree, project)) = all_projects(cx).find_map(|project| {
3605                project
3606                    .read(cx)
3607                    .worktree_for_id(worktree_id, cx)
3608                    .zip(Some(project))
3609            }) else {
3610                anyhow::bail!("Could not find project with worktree id: {}", worktree_id);
3611            };
3612
3613            project.update(cx, |project, cx| {
3614                let task = if project.contains_local_settings_file(worktree_id, &rel_path, cx) {
3615                    None
3616                } else {
3617                    Some(worktree.update(cx, |worktree, cx| {
3618                        worktree.create_entry(rel_path.clone(), false, None, cx)
3619                    }))
3620                };
3621
3622                cx.spawn(async move |project, cx| {
3623                    if let Some(task) = task
3624                        && task.await.is_err()
3625                    {
3626                        return;
3627                    };
3628
3629                    project
3630                        .update(cx, |project, cx| {
3631                            project.update_local_settings_file(worktree_id, rel_path, cx, update);
3632                        })
3633                        .ok();
3634                })
3635                .detach();
3636            });
3637
3638            return Ok(());
3639        }
3640        SettingsUiFile::User => {
3641            // todo(settings_ui) error?
3642            SettingsStore::global(cx).update_settings_file(<dyn fs::Fs>::global(cx), update);
3643            Ok(())
3644        }
3645        SettingsUiFile::Server(_) => unimplemented!(),
3646    }
3647}
3648
3649fn render_text_field<T: From<String> + Into<String> + AsRef<str> + Clone>(
3650    field: SettingField<T>,
3651    file: SettingsUiFile,
3652    metadata: Option<&SettingsFieldMetadata>,
3653    _window: &mut Window,
3654    cx: &mut App,
3655) -> AnyElement {
3656    let (_, initial_text) =
3657        SettingsStore::global(cx).get_value_from_file(file.to_settings(), field.pick);
3658    let initial_text = initial_text.filter(|s| !s.as_ref().is_empty());
3659
3660    SettingsInputField::new()
3661        .tab_index(0)
3662        .when_some(initial_text, |editor, text| {
3663            editor.with_initial_text(text.as_ref().to_string())
3664        })
3665        .when_some(
3666            metadata.and_then(|metadata| metadata.placeholder),
3667            |editor, placeholder| editor.with_placeholder(placeholder),
3668        )
3669        .on_confirm({
3670            move |new_text, cx| {
3671                update_settings_file(file.clone(), field.json_path, cx, move |settings, _cx| {
3672                    (field.write)(settings, new_text.map(Into::into));
3673                })
3674                .log_err(); // todo(settings_ui) don't log err
3675            }
3676        })
3677        .into_any_element()
3678}
3679
3680fn render_toggle_button<B: Into<bool> + From<bool> + Copy>(
3681    field: SettingField<B>,
3682    file: SettingsUiFile,
3683    _metadata: Option<&SettingsFieldMetadata>,
3684    _window: &mut Window,
3685    cx: &mut App,
3686) -> AnyElement {
3687    let (_, value) = SettingsStore::global(cx).get_value_from_file(file.to_settings(), field.pick);
3688
3689    let toggle_state = if value.copied().map_or(false, Into::into) {
3690        ToggleState::Selected
3691    } else {
3692        ToggleState::Unselected
3693    };
3694
3695    Switch::new("toggle_button", toggle_state)
3696        .tab_index(0_isize)
3697        .on_click({
3698            move |state, _window, cx| {
3699                telemetry::event!("Settings Change", setting = field.json_path, type = file.setting_type());
3700
3701                let state = *state == ui::ToggleState::Selected;
3702                update_settings_file(file.clone(), field.json_path, cx, move |settings, _cx| {
3703                    (field.write)(settings, Some(state.into()));
3704                })
3705                .log_err(); // todo(settings_ui) don't log err
3706            }
3707        })
3708        .into_any_element()
3709}
3710
3711fn render_number_field<T: NumberFieldType + Send + Sync>(
3712    field: SettingField<T>,
3713    file: SettingsUiFile,
3714    _metadata: Option<&SettingsFieldMetadata>,
3715    window: &mut Window,
3716    cx: &mut App,
3717) -> AnyElement {
3718    let (_, value) = SettingsStore::global(cx).get_value_from_file(file.to_settings(), field.pick);
3719    let value = value.copied().unwrap_or_else(T::min_value);
3720
3721    let id = field
3722        .json_path
3723        .map(|p| format!("numeric_stepper_{}", p))
3724        .unwrap_or_else(|| "numeric_stepper".to_string());
3725
3726    NumberField::new(id, value, window, cx)
3727        .tab_index(0_isize)
3728        .on_change({
3729            move |value, _window, cx| {
3730                let value = *value;
3731                update_settings_file(file.clone(), field.json_path, cx, move |settings, _cx| {
3732                    (field.write)(settings, Some(value));
3733                })
3734                .log_err(); // todo(settings_ui) don't log err
3735            }
3736        })
3737        .into_any_element()
3738}
3739
3740fn render_editable_number_field<T: NumberFieldType + Send + Sync>(
3741    field: SettingField<T>,
3742    file: SettingsUiFile,
3743    _metadata: Option<&SettingsFieldMetadata>,
3744    window: &mut Window,
3745    cx: &mut App,
3746) -> AnyElement {
3747    let (_, value) = SettingsStore::global(cx).get_value_from_file(file.to_settings(), field.pick);
3748    let value = value.copied().unwrap_or_else(T::min_value);
3749
3750    let id = field
3751        .json_path
3752        .map(|p| format!("numeric_stepper_{}", p))
3753        .unwrap_or_else(|| "numeric_stepper".to_string());
3754
3755    NumberField::new(id, value, window, cx)
3756        .mode(NumberFieldMode::Edit, cx)
3757        .tab_index(0_isize)
3758        .on_change({
3759            move |value, _window, cx| {
3760                let value = *value;
3761                update_settings_file(file.clone(), field.json_path, cx, move |settings, _cx| {
3762                    (field.write)(settings, Some(value));
3763                })
3764                .log_err(); // todo(settings_ui) don't log err
3765            }
3766        })
3767        .into_any_element()
3768}
3769
3770fn render_dropdown<T>(
3771    field: SettingField<T>,
3772    file: SettingsUiFile,
3773    metadata: Option<&SettingsFieldMetadata>,
3774    _window: &mut Window,
3775    cx: &mut App,
3776) -> AnyElement
3777where
3778    T: strum::VariantArray + strum::VariantNames + Copy + PartialEq + Send + Sync + 'static,
3779{
3780    let variants = || -> &'static [T] { <T as strum::VariantArray>::VARIANTS };
3781    let labels = || -> &'static [&'static str] { <T as strum::VariantNames>::VARIANTS };
3782    let should_do_titlecase = metadata
3783        .and_then(|metadata| metadata.should_do_titlecase)
3784        .unwrap_or(true);
3785
3786    let (_, current_value) =
3787        SettingsStore::global(cx).get_value_from_file(file.to_settings(), field.pick);
3788    let current_value = current_value.copied().unwrap_or(variants()[0]);
3789
3790    EnumVariantDropdown::new("dropdown", current_value, variants(), labels(), {
3791        move |value, cx| {
3792            if value == current_value {
3793                return;
3794            }
3795            update_settings_file(file.clone(), field.json_path, cx, move |settings, _cx| {
3796                (field.write)(settings, Some(value));
3797            })
3798            .log_err(); // todo(settings_ui) don't log err
3799        }
3800    })
3801    .tab_index(0)
3802    .title_case(should_do_titlecase)
3803    .into_any_element()
3804}
3805
3806fn render_picker_trigger_button(id: SharedString, label: SharedString) -> Button {
3807    Button::new(id, label)
3808        .tab_index(0_isize)
3809        .style(ButtonStyle::Outlined)
3810        .size(ButtonSize::Medium)
3811        .icon(IconName::ChevronUpDown)
3812        .icon_color(Color::Muted)
3813        .icon_size(IconSize::Small)
3814        .icon_position(IconPosition::End)
3815}
3816
3817fn render_font_picker(
3818    field: SettingField<settings::FontFamilyName>,
3819    file: SettingsUiFile,
3820    _metadata: Option<&SettingsFieldMetadata>,
3821    _window: &mut Window,
3822    cx: &mut App,
3823) -> AnyElement {
3824    let current_value = SettingsStore::global(cx)
3825        .get_value_from_file(file.to_settings(), field.pick)
3826        .1
3827        .cloned()
3828        .map_or_else(|| SharedString::default(), |value| value.into_gpui());
3829
3830    PopoverMenu::new("font-picker")
3831        .trigger(render_picker_trigger_button(
3832            "font_family_picker_trigger".into(),
3833            current_value.clone(),
3834        ))
3835        .menu(move |window, cx| {
3836            let file = file.clone();
3837            let current_value = current_value.clone();
3838
3839            Some(cx.new(move |cx| {
3840                font_picker(
3841                    current_value,
3842                    move |font_name, cx| {
3843                        update_settings_file(
3844                            file.clone(),
3845                            field.json_path,
3846                            cx,
3847                            move |settings, _cx| {
3848                                (field.write)(settings, Some(font_name.to_string().into()));
3849                            },
3850                        )
3851                        .log_err(); // todo(settings_ui) don't log err
3852                    },
3853                    window,
3854                    cx,
3855                )
3856            }))
3857        })
3858        .anchor(gpui::Corner::TopLeft)
3859        .offset(gpui::Point {
3860            x: px(0.0),
3861            y: px(2.0),
3862        })
3863        .with_handle(ui::PopoverMenuHandle::default())
3864        .into_any_element()
3865}
3866
3867fn render_theme_picker(
3868    field: SettingField<settings::ThemeName>,
3869    file: SettingsUiFile,
3870    _metadata: Option<&SettingsFieldMetadata>,
3871    _window: &mut Window,
3872    cx: &mut App,
3873) -> AnyElement {
3874    let (_, value) = SettingsStore::global(cx).get_value_from_file(file.to_settings(), field.pick);
3875    let current_value = value
3876        .cloned()
3877        .map(|theme_name| theme_name.0.into())
3878        .unwrap_or_else(|| cx.theme().name.clone());
3879
3880    PopoverMenu::new("theme-picker")
3881        .trigger(render_picker_trigger_button(
3882            "theme_picker_trigger".into(),
3883            current_value.clone(),
3884        ))
3885        .menu(move |window, cx| {
3886            Some(cx.new(|cx| {
3887                let file = file.clone();
3888                let current_value = current_value.clone();
3889                theme_picker(
3890                    current_value,
3891                    move |theme_name, cx| {
3892                        update_settings_file(
3893                            file.clone(),
3894                            field.json_path,
3895                            cx,
3896                            move |settings, _cx| {
3897                                (field.write)(
3898                                    settings,
3899                                    Some(settings::ThemeName(theme_name.into())),
3900                                );
3901                            },
3902                        )
3903                        .log_err(); // todo(settings_ui) don't log err
3904                    },
3905                    window,
3906                    cx,
3907                )
3908            }))
3909        })
3910        .anchor(gpui::Corner::TopLeft)
3911        .offset(gpui::Point {
3912            x: px(0.0),
3913            y: px(2.0),
3914        })
3915        .with_handle(ui::PopoverMenuHandle::default())
3916        .into_any_element()
3917}
3918
3919fn render_icon_theme_picker(
3920    field: SettingField<settings::IconThemeName>,
3921    file: SettingsUiFile,
3922    _metadata: Option<&SettingsFieldMetadata>,
3923    _window: &mut Window,
3924    cx: &mut App,
3925) -> AnyElement {
3926    let (_, value) = SettingsStore::global(cx).get_value_from_file(file.to_settings(), field.pick);
3927    let current_value = value
3928        .cloned()
3929        .map(|theme_name| theme_name.0.into())
3930        .unwrap_or_else(|| cx.theme().name.clone());
3931
3932    PopoverMenu::new("icon-theme-picker")
3933        .trigger(render_picker_trigger_button(
3934            "icon_theme_picker_trigger".into(),
3935            current_value.clone(),
3936        ))
3937        .menu(move |window, cx| {
3938            Some(cx.new(|cx| {
3939                let file = file.clone();
3940                let current_value = current_value.clone();
3941                icon_theme_picker(
3942                    current_value,
3943                    move |theme_name, cx| {
3944                        update_settings_file(
3945                            file.clone(),
3946                            field.json_path,
3947                            cx,
3948                            move |settings, _cx| {
3949                                (field.write)(
3950                                    settings,
3951                                    Some(settings::IconThemeName(theme_name.into())),
3952                                );
3953                            },
3954                        )
3955                        .log_err(); // todo(settings_ui) don't log err
3956                    },
3957                    window,
3958                    cx,
3959                )
3960            }))
3961        })
3962        .anchor(gpui::Corner::TopLeft)
3963        .offset(gpui::Point {
3964            x: px(0.0),
3965            y: px(2.0),
3966        })
3967        .with_handle(ui::PopoverMenuHandle::default())
3968        .into_any_element()
3969}
3970
3971#[cfg(test)]
3972pub mod test {
3973
3974    use super::*;
3975
3976    impl SettingsWindow {
3977        fn navbar_entry(&self) -> usize {
3978            self.navbar_entry
3979        }
3980    }
3981
3982    impl PartialEq for NavBarEntry {
3983        fn eq(&self, other: &Self) -> bool {
3984            self.title == other.title
3985                && self.is_root == other.is_root
3986                && self.expanded == other.expanded
3987                && self.page_index == other.page_index
3988                && self.item_index == other.item_index
3989            // ignoring focus_handle
3990        }
3991    }
3992
3993    pub fn register_settings(cx: &mut App) {
3994        settings::init(cx);
3995        theme::init(theme::LoadThemes::JustBase, cx);
3996        editor::init(cx);
3997        menu::init();
3998    }
3999
4000    fn parse(input: &'static str, window: &mut Window, cx: &mut App) -> SettingsWindow {
4001        struct PageBuilder {
4002            title: &'static str,
4003            items: Vec<SettingsPageItem>,
4004        }
4005        let mut page_builders: Vec<PageBuilder> = Vec::new();
4006        let mut expanded_pages = Vec::new();
4007        let mut selected_idx = None;
4008        let mut index = 0;
4009        let mut in_expanded_section = false;
4010
4011        for mut line in input
4012            .lines()
4013            .map(|line| line.trim())
4014            .filter(|line| !line.is_empty())
4015        {
4016            if let Some(pre) = line.strip_suffix('*') {
4017                assert!(selected_idx.is_none(), "Only one selected entry allowed");
4018                selected_idx = Some(index);
4019                line = pre;
4020            }
4021            let (kind, title) = line.split_once(" ").unwrap();
4022            assert_eq!(kind.len(), 1);
4023            let kind = kind.chars().next().unwrap();
4024            if kind == 'v' {
4025                let page_idx = page_builders.len();
4026                expanded_pages.push(page_idx);
4027                page_builders.push(PageBuilder {
4028                    title,
4029                    items: vec![],
4030                });
4031                index += 1;
4032                in_expanded_section = true;
4033            } else if kind == '>' {
4034                page_builders.push(PageBuilder {
4035                    title,
4036                    items: vec![],
4037                });
4038                index += 1;
4039                in_expanded_section = false;
4040            } else if kind == '-' {
4041                page_builders
4042                    .last_mut()
4043                    .unwrap()
4044                    .items
4045                    .push(SettingsPageItem::SectionHeader(title));
4046                if selected_idx == Some(index) && !in_expanded_section {
4047                    panic!("Items in unexpanded sections cannot be selected");
4048                }
4049                index += 1;
4050            } else {
4051                panic!(
4052                    "Entries must start with one of 'v', '>', or '-'\n line: {}",
4053                    line
4054                );
4055            }
4056        }
4057
4058        let pages: Vec<SettingsPage> = page_builders
4059            .into_iter()
4060            .map(|builder| SettingsPage {
4061                title: builder.title,
4062                items: builder.items.into_boxed_slice(),
4063            })
4064            .collect();
4065
4066        let mut settings_window = SettingsWindow {
4067            title_bar: None,
4068            original_window: None,
4069            worktree_root_dirs: HashMap::default(),
4070            files: Vec::default(),
4071            current_file: crate::SettingsUiFile::User,
4072            pages,
4073            search_bar: cx.new(|cx| Editor::single_line(window, cx)),
4074            navbar_entry: selected_idx.expect("Must have a selected navbar entry"),
4075            navbar_entries: Vec::default(),
4076            navbar_scroll_handle: UniformListScrollHandle::default(),
4077            navbar_focus_subscriptions: vec![],
4078            filter_table: vec![],
4079            sub_page_stack: vec![],
4080            has_query: false,
4081            content_handles: vec![],
4082            search_task: None,
4083            focus_handle: cx.focus_handle(),
4084            navbar_focus_handle: NonFocusableHandle::new(
4085                NAVBAR_CONTAINER_TAB_INDEX,
4086                false,
4087                window,
4088                cx,
4089            ),
4090            content_focus_handle: NonFocusableHandle::new(
4091                CONTENT_CONTAINER_TAB_INDEX,
4092                false,
4093                window,
4094                cx,
4095            ),
4096            files_focus_handle: cx.focus_handle(),
4097            search_index: None,
4098            list_state: ListState::new(0, gpui::ListAlignment::Top, px(0.0)),
4099            shown_errors: HashSet::default(),
4100        };
4101
4102        settings_window.build_filter_table();
4103        settings_window.build_navbar(cx);
4104        for expanded_page_index in expanded_pages {
4105            for entry in &mut settings_window.navbar_entries {
4106                if entry.page_index == expanded_page_index && entry.is_root {
4107                    entry.expanded = true;
4108                }
4109            }
4110        }
4111        settings_window
4112    }
4113
4114    #[track_caller]
4115    fn check_navbar_toggle(
4116        before: &'static str,
4117        toggle_page: &'static str,
4118        after: &'static str,
4119        window: &mut Window,
4120        cx: &mut App,
4121    ) {
4122        let mut settings_window = parse(before, window, cx);
4123        let toggle_page_idx = settings_window
4124            .pages
4125            .iter()
4126            .position(|page| page.title == toggle_page)
4127            .expect("page not found");
4128        let toggle_idx = settings_window
4129            .navbar_entries
4130            .iter()
4131            .position(|entry| entry.page_index == toggle_page_idx)
4132            .expect("page not found");
4133        settings_window.toggle_navbar_entry(toggle_idx);
4134
4135        let expected_settings_window = parse(after, window, cx);
4136
4137        pretty_assertions::assert_eq!(
4138            settings_window
4139                .visible_navbar_entries()
4140                .map(|(_, entry)| entry)
4141                .collect::<Vec<_>>(),
4142            expected_settings_window
4143                .visible_navbar_entries()
4144                .map(|(_, entry)| entry)
4145                .collect::<Vec<_>>(),
4146        );
4147        pretty_assertions::assert_eq!(
4148            settings_window.navbar_entries[settings_window.navbar_entry()],
4149            expected_settings_window.navbar_entries[expected_settings_window.navbar_entry()],
4150        );
4151    }
4152
4153    macro_rules! check_navbar_toggle {
4154        ($name:ident, before: $before:expr, toggle_page: $toggle_page:expr, after: $after:expr) => {
4155            #[gpui::test]
4156            fn $name(cx: &mut gpui::TestAppContext) {
4157                let window = cx.add_empty_window();
4158                window.update(|window, cx| {
4159                    register_settings(cx);
4160                    check_navbar_toggle($before, $toggle_page, $after, window, cx);
4161                });
4162            }
4163        };
4164    }
4165
4166    check_navbar_toggle!(
4167        navbar_basic_open,
4168        before: r"
4169        v General
4170        - General
4171        - Privacy*
4172        v Project
4173        - Project Settings
4174        ",
4175        toggle_page: "General",
4176        after: r"
4177        > General*
4178        v Project
4179        - Project Settings
4180        "
4181    );
4182
4183    check_navbar_toggle!(
4184        navbar_basic_close,
4185        before: r"
4186        > General*
4187        - General
4188        - Privacy
4189        v Project
4190        - Project Settings
4191        ",
4192        toggle_page: "General",
4193        after: r"
4194        v General*
4195        - General
4196        - Privacy
4197        v Project
4198        - Project Settings
4199        "
4200    );
4201
4202    check_navbar_toggle!(
4203        navbar_basic_second_root_entry_close,
4204        before: r"
4205        > General
4206        - General
4207        - Privacy
4208        v Project
4209        - Project Settings*
4210        ",
4211        toggle_page: "Project",
4212        after: r"
4213        > General
4214        > Project*
4215        "
4216    );
4217
4218    check_navbar_toggle!(
4219        navbar_toggle_subroot,
4220        before: r"
4221        v General Page
4222        - General
4223        - Privacy
4224        v Project
4225        - Worktree Settings Content*
4226        v AI
4227        - General
4228        > Appearance & Behavior
4229        ",
4230        toggle_page: "Project",
4231        after: r"
4232        v General Page
4233        - General
4234        - Privacy
4235        > Project*
4236        v AI
4237        - General
4238        > Appearance & Behavior
4239        "
4240    );
4241
4242    check_navbar_toggle!(
4243        navbar_toggle_close_propagates_selected_index,
4244        before: r"
4245        v General Page
4246        - General
4247        - Privacy
4248        v Project
4249        - Worktree Settings Content
4250        v AI
4251        - General*
4252        > Appearance & Behavior
4253        ",
4254        toggle_page: "General Page",
4255        after: r"
4256        > General Page*
4257        v Project
4258        - Worktree Settings Content
4259        v AI
4260        - General
4261        > Appearance & Behavior
4262        "
4263    );
4264
4265    check_navbar_toggle!(
4266        navbar_toggle_expand_propagates_selected_index,
4267        before: r"
4268        > General Page
4269        - General
4270        - Privacy
4271        v Project
4272        - Worktree Settings Content
4273        v AI
4274        - General*
4275        > Appearance & Behavior
4276        ",
4277        toggle_page: "General Page",
4278        after: r"
4279        v General Page*
4280        - General
4281        - Privacy
4282        v Project
4283        - Worktree Settings Content
4284        v AI
4285        - General
4286        > Appearance & Behavior
4287        "
4288    );
4289}