settings_ui.rs

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