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