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