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