Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T04:44:05.519Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

settings_ui.rs

6938 lines · 261.6 KB · rust
1mod components;
2mod page_data;
3pub mod pages;
4
5use agent_skills::SkillIndex;
6use anyhow::{Context as _, Result};
7use cloud_api_types::OrganizationConfiguration;
8use editor::{Editor, EditorEvent};
9use futures::{StreamExt, channel::mpsc};
10use fuzzy::StringMatchCandidate;
11use gpui::{
12    Action, App, AsyncApp, ClipboardItem, DEFAULT_ADDITIONAL_WINDOW_SIZE, Div, Entity, FocusHandle,
13    Focusable, Global, KeyContext, ListState, ReadGlobal as _, Role, ScrollHandle, Stateful,
14    Subscription, Task, TitlebarOptions, UniformListScrollHandle, WeakEntity, Window, WindowBounds,
15    WindowHandle, WindowOptions, actions, div, list, point, prelude::*, px, uniform_list,
16};
17
18use language::Buffer;
19use platform_title_bar::PlatformTitleBar;
20use project::{Project, ProjectPath, Worktree, WorktreeId};
21use release_channel::ReleaseChannel;
22use schemars::JsonSchema;
23use serde::Deserialize;
24use settings::{
25    IntoGpui, Settings, SettingsContent, SettingsStore, initial_project_settings_content,
26};
27use std::{
28    any::{Any, TypeId, type_name},
29    cell::RefCell,
30    collections::{HashMap, HashSet},
31    num::{NonZero, NonZeroU32},
32    ops::Range,
33    path::PathBuf,
34    rc::Rc,
35    sync::{Arc, LazyLock, RwLock},
36    time::Duration,
37};
38use theme_settings::ThemeSettings;
39use ui::{
40    Banner, ContextMenu, Divider, DropdownMenu, DropdownStyle, IconButtonShape, KeyBinding,
41    KeybindingHint, PopoverMenu, Scrollbars, Switch, Tooltip, TreeViewItem, WithScrollbar,
42    prelude::*,
43};
44
45use util::{ResultExt as _, paths::PathStyle, rel_path::RelPath};
46use workspace::{
47    AppState, MultiWorkspace, OpenOptions, OpenVisible, Workspace, WorkspaceSettings,
48    client_side_decorations,
49};
50use zed_actions::{
51    AGENT_SKILLS_SETTINGS_PATH, OpenProjectSettings, OpenSettings, OpenSettingsAt,
52    OpenSettingsAtTarget, OpenSettingsPage,
53};
54
55use crate::components::{
56    EnumVariantDropdown, NumberField, NumberFieldMode, NumberFieldType, SettingsInputField,
57    SettingsSectionHeader, font_picker, icon_theme_picker, render_ollama_model_picker,
58    text_field_a11y_state, theme_picker,
59};
60use crate::pages::{
61    CustomAgentForm, LlmProviderForm, McpServerForm, render_input_audio_device_dropdown,
62    render_output_audio_device_dropdown,
63};
64
65const NAVBAR_CONTAINER_TAB_INDEX: isize = 0;
66const NAVBAR_GROUP_TAB_INDEX: isize = 1;
67
68const HEADER_CONTAINER_TAB_INDEX: isize = 2;
69const HEADER_GROUP_TAB_INDEX: isize = 3;
70
71const CONTENT_CONTAINER_TAB_INDEX: isize = 4;
72const CONTENT_GROUP_TAB_INDEX: isize = 5;
73
74const SIDEBAR_WIDTH: Pixels = px(226.);
75const CONTENT_MIN_WIDTH: Pixels = px(400.);
76
77actions!(
78    settings_editor,
79    [
80        /// Minimizes the settings UI window.
81        Minimize,
82        /// Toggles focus between the navbar and the main content.
83        ToggleFocusNav,
84        /// Expands the navigation entry.
85        ExpandNavEntry,
86        /// Collapses the navigation entry.
87        CollapseNavEntry,
88        /// Focuses the next file in the file list.
89        FocusNextFile,
90        /// Focuses the previous file in the file list.
91        FocusPreviousFile,
92        /// Opens an editor for the current file
93        OpenCurrentFile,
94        /// Focuses the previous root navigation entry.
95        FocusPreviousRootNavEntry,
96        /// Focuses the next root navigation entry.
97        FocusNextRootNavEntry,
98        /// Focuses the first navigation entry.
99        FocusFirstNavEntry,
100        /// Focuses the last navigation entry.
101        FocusLastNavEntry,
102        /// Focuses and opens the next navigation entry without moving focus to content.
103        FocusNextNavEntry,
104        /// Focuses and opens the previous navigation entry without moving focus to content.
105        FocusPreviousNavEntry
106    ]
107);
108
109#[derive(Action, PartialEq, Eq, Clone, Copy, Debug, JsonSchema, Deserialize)]
110#[action(namespace = settings_editor)]
111struct FocusFile(pub u32);
112
113struct SettingField<T: 'static> {
114    pick: fn(&SettingsContent) -> Option<&T>,
115    write: fn(&mut SettingsContent, Option<T>, &App),
116    /// Tells us whether the setting is overridden by the currently selected
117    /// organization's settings. Takes the organization configuration and the
118    /// resolved settings value, and returns `Some(...)` if the organization
119    /// overrides the setting, otherwise `None`.
120    organization_override: Option<fn(&OrganizationConfiguration) -> Option<&T>>,
121
122    /// A json-path-like string that gives a unique-ish string that identifies
123    /// where in the JSON the setting is defined.
124    ///
125    /// The syntax is `jq`-like, but modified slightly to be URL-safe (and
126    /// without the leading dot), e.g. `foo.bar`.
127    ///
128    /// They are URL-safe (this is important since links are the main use-case
129    /// for these paths).
130    ///
131    /// There are a couple of special cases:
132    /// - discrimminants are represented with a trailing `$`, for example
133    /// `terminal.working_directory$`. This is to distinguish the discrimminant
134    /// setting (i.e. the setting that changes whether the value is a string or
135    /// an object) from the setting in the case that it is a string.
136    /// - language-specific settings begin `languages.$(language)`. Links
137    /// targeting these settings should take the form `languages/Rust/...`, for
138    /// example, but are not currently supported.
139    json_path: Option<&'static str>,
140}
141
142impl<T: 'static> Clone for SettingField<T> {
143    fn clone(&self) -> Self {
144        *self
145    }
146}
147
148// manual impl because derive puts a Copy bound on T, which is inaccurate in our case
149impl<T: 'static> Copy for SettingField<T> {}
150
151/// Helper for unimplemented settings, used in combination with `SettingField::unimplemented`
152/// to keep the setting around in the UI with valid pick and write implementations, but don't actually try to render it.
153/// TODO(settings_ui): In non-dev builds (`#[cfg(not(debug_assertions))]`) make this render as edit-in-json
154#[derive(Clone, Copy)]
155struct UnimplementedSettingField;
156
157impl PartialEq for UnimplementedSettingField {
158    fn eq(&self, _other: &Self) -> bool {
159        true
160    }
161}
162
163impl<T: 'static> SettingField<T> {
164    /// Helper for settings with types that are not yet implemented.
165    #[allow(unused)]
166    fn unimplemented(self) -> SettingField<UnimplementedSettingField> {
167        SettingField {
168            pick: |_| Some(&UnimplementedSettingField),
169            write: |_, _, _| unreachable!(),
170            organization_override: None,
171            json_path: self.json_path,
172        }
173    }
174}
175
176trait AnySettingField {
177    fn as_any(&self) -> &dyn Any;
178    fn type_name(&self) -> &'static str;
179    fn type_id(&self) -> TypeId;
180    // 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)
181    fn file_set_in(&self, file: SettingsUiFile, cx: &App) -> (settings::SettingsFile, bool);
182    fn reset_to_default_fn(
183        &self,
184        current_file: &SettingsUiFile,
185        file_set_in: &settings::SettingsFile,
186        cx: &App,
187    ) -> Option<Box<dyn Fn(&mut Window, &mut App)>>;
188
189    fn json_path(&self) -> Option<&'static str>;
190
191    fn is_overridden_by_organization(&self, cx: &App) -> bool;
192}
193
194impl<T: PartialEq + Clone + Send + Sync + 'static> AnySettingField for SettingField<T> {
195    fn as_any(&self) -> &dyn Any {
196        self
197    }
198
199    fn type_name(&self) -> &'static str {
200        type_name::<T>()
201    }
202
203    fn type_id(&self) -> TypeId {
204        TypeId::of::<T>()
205    }
206
207    fn file_set_in(&self, file: SettingsUiFile, cx: &App) -> (settings::SettingsFile, bool) {
208        let (file, value) = cx
209            .global::<SettingsStore>()
210            .get_value_from_file(file.to_settings(), self.pick);
211        return (file, value.is_some());
212    }
213
214    fn reset_to_default_fn(
215        &self,
216        current_file: &SettingsUiFile,
217        file_set_in: &settings::SettingsFile,
218        cx: &App,
219    ) -> Option<Box<dyn Fn(&mut Window, &mut App)>> {
220        if file_set_in == &settings::SettingsFile::Default {
221            return None;
222        }
223        if file_set_in != &current_file.to_settings() {
224            return None;
225        }
226        let this = *self;
227        let store = SettingsStore::global(cx);
228        let default_value = (this.pick)(store.raw_default_settings());
229        let is_default = store
230            .get_content_for_file(file_set_in.clone())
231            .map_or(None, this.pick)
232            == default_value;
233        if is_default {
234            return None;
235        }
236        let current_file = current_file.clone();
237
238        return Some(Box::new(move |window, cx| {
239            let store = SettingsStore::global(cx);
240            let default_value = (this.pick)(store.raw_default_settings());
241            let is_set_somewhere_other_than_default = store
242                .get_value_up_to_file(current_file.to_settings(), this.pick)
243                .0
244                != settings::SettingsFile::Default;
245            let value_to_set = if is_set_somewhere_other_than_default {
246                default_value.cloned()
247            } else {
248                None
249            };
250            update_settings_file(
251                current_file.clone(),
252                None,
253                window,
254                cx,
255                move |settings, app| {
256                    (this.write)(settings, value_to_set, app);
257                },
258            )
259            // todo(settings_ui): Don't log err
260            .log_err();
261        }));
262    }
263
264    fn json_path(&self) -> Option<&'static str> {
265        self.json_path
266    }
267
268    fn is_overridden_by_organization(&self, cx: &App) -> bool {
269        let Some(org_override) = self.organization_override else {
270            return false;
271        };
272
273        let user_store = AppState::global(cx).user_store.read(cx);
274        let Some(org_config) = user_store.current_organization_configuration() else {
275            return false;
276        };
277
278        (org_override)(&org_config).is_some()
279    }
280}
281
282#[derive(Default, Clone)]
283struct SettingFieldRenderer {
284    renderers: Rc<
285        RefCell<
286            HashMap<
287                TypeId,
288                Box<
289                    dyn Fn(
290                        &SettingsWindow,
291                        &SettingItem,
292                        SettingsUiFile,
293                        Option<&SettingsFieldMetadata>,
294                        bool,
295                        &mut Window,
296                        &mut Context<SettingsWindow>,
297                    ) -> Stateful<Div>,
298                >,
299            >,
300        >,
301    >,
302}
303
304impl Global for SettingFieldRenderer {}
305
306impl SettingFieldRenderer {
307    fn add_basic_renderer<T: 'static>(
308        &mut self,
309        render_control: impl Fn(
310            SettingField<T>,
311            SettingsUiFile,
312            Option<&SettingsFieldMetadata>,
313            &'static str,
314            &'static str,
315            &mut Window,
316            &mut App,
317        ) -> AnyElement
318        + 'static,
319    ) -> &mut Self {
320        self.add_renderer(
321            move |settings_window: &SettingsWindow,
322                  item: &SettingItem,
323                  field: SettingField<T>,
324                  settings_file: SettingsUiFile,
325                  metadata: Option<&SettingsFieldMetadata>,
326                  sub_field: bool,
327                  window: &mut Window,
328                  cx: &mut Context<SettingsWindow>| {
329                let control = render_control(
330                    field,
331                    settings_file.clone(),
332                    metadata,
333                    item.title,
334                    item.description,
335                    window,
336                    cx,
337                );
338                render_settings_item(settings_window, item, settings_file, control, sub_field, cx)
339            },
340        )
341    }
342
343    fn add_renderer<T: 'static>(
344        &mut self,
345        renderer: impl Fn(
346            &SettingsWindow,
347            &SettingItem,
348            SettingField<T>,
349            SettingsUiFile,
350            Option<&SettingsFieldMetadata>,
351            bool,
352            &mut Window,
353            &mut Context<SettingsWindow>,
354        ) -> Stateful<Div>
355        + 'static,
356    ) -> &mut Self {
357        let key = TypeId::of::<T>();
358        let renderer = Box::new(
359            move |settings_window: &SettingsWindow,
360                  item: &SettingItem,
361                  settings_file: SettingsUiFile,
362                  metadata: Option<&SettingsFieldMetadata>,
363                  sub_field: bool,
364                  window: &mut Window,
365                  cx: &mut Context<SettingsWindow>| {
366                let field = *item
367                    .field
368                    .as_ref()
369                    .as_any()
370                    .downcast_ref::<SettingField<T>>()
371                    .unwrap();
372                renderer(
373                    settings_window,
374                    item,
375                    field,
376                    settings_file,
377                    metadata,
378                    sub_field,
379                    window,
380                    cx,
381                )
382            },
383        );
384        self.renderers.borrow_mut().insert(key, renderer);
385        self
386    }
387}
388
389struct NonFocusableHandle {
390    handle: FocusHandle,
391    _subscription: Subscription,
392}
393
394impl NonFocusableHandle {
395    fn new(tab_index: isize, tab_stop: bool, window: &mut Window, cx: &mut App) -> Entity<Self> {
396        let handle = cx.focus_handle().tab_index(tab_index).tab_stop(tab_stop);
397        Self::from_handle(handle, window, cx)
398    }
399
400    fn from_handle(handle: FocusHandle, window: &mut Window, cx: &mut App) -> Entity<Self> {
401        cx.new(|cx| {
402            let _subscription = cx.on_focus(&handle, window, {
403                move |_, window, cx| {
404                    window.focus_next(cx);
405                }
406            });
407            Self {
408                handle,
409                _subscription,
410            }
411        })
412    }
413}
414
415impl Focusable for NonFocusableHandle {
416    fn focus_handle(&self, _: &App) -> FocusHandle {
417        self.handle.clone()
418    }
419}
420
421#[derive(Default)]
422struct SettingsFieldMetadata {
423    placeholder: Option<&'static str>,
424    should_do_titlecase: Option<bool>,
425    display_confirm_button: bool,
426    display_clear_button: bool,
427    confirm_on_focus_out: bool,
428    treat_missing_text_as_empty: bool,
429}
430
431pub fn init(cx: &mut App) {
432    init_renderers(cx);
433    let queue = ProjectSettingsUpdateQueue::new(cx);
434    cx.set_global(queue);
435
436    cx.on_action(|_: &OpenSettings, cx| {
437        open_settings_editor(None, None, None, cx);
438    });
439    cx.on_action(|_: &zed_actions::assistant::OpenSkillCreator, cx| {
440        open_skill_creator(pages::SkillCreatorOpenMode::Form, None, cx);
441    });
442    cx.on_action(|_: &zed_actions::assistant::CreateSkillFromUrl, cx| {
443        let initial_url = pages::skill_url_from_clipboard(cx);
444        open_skill_creator(pages::SkillCreatorOpenMode::Url { initial_url }, None, cx);
445    });
446
447    cx.observe_new(|workspace: &mut workspace::Workspace, _, _| {
448        workspace
449            .register_action(|_, action: &OpenSettingsAt, window, cx| {
450                let window_handle = window.window_handle().downcast::<MultiWorkspace>();
451                open_settings_editor_at_target(
452                    Some(&action.path),
453                    action.target.as_ref().map(SettingsFileTarget::from),
454                    window_handle,
455                    cx,
456                );
457            })
458            .register_action(|_, action: &OpenSettingsPage, window, cx| {
459                let window_handle = window.window_handle().downcast::<MultiWorkspace>();
460                open_settings_editor_to_page(
461                    &action.page,
462                    action.target.as_ref().map(SettingsFileTarget::from),
463                    window_handle,
464                    cx,
465                );
466            })
467            .register_action(|_, _: &OpenSettings, window, cx| {
468                let window_handle = window.window_handle().downcast::<MultiWorkspace>();
469                open_settings_editor(None, None, window_handle, cx);
470            })
471            .register_action(|workspace, _: &OpenProjectSettings, window, cx| {
472                let window_handle = window.window_handle().downcast::<MultiWorkspace>();
473                let target_worktree_id = workspace
474                    .project()
475                    .read(cx)
476                    .visible_worktrees(cx)
477                    .find_map(|tree| {
478                        tree.read(cx)
479                            .root_entry()?
480                            .is_dir()
481                            .then_some(tree.read(cx).id())
482                    });
483                open_settings_editor(None, target_worktree_id, window_handle, cx);
484            })
485            .register_action(
486                |_, _: &zed_actions::assistant::OpenSkillCreator, window, cx| {
487                    let window_handle = window.window_handle().downcast::<MultiWorkspace>();
488                    open_skill_creator(pages::SkillCreatorOpenMode::Form, window_handle, cx);
489                },
490            )
491            .register_action(
492                |_, _: &zed_actions::assistant::CreateSkillFromUrl, window, cx| {
493                    let window_handle = window.window_handle().downcast::<MultiWorkspace>();
494                    let initial_url = pages::skill_url_from_clipboard(cx);
495                    open_skill_creator(
496                        pages::SkillCreatorOpenMode::Url { initial_url },
497                        window_handle,
498                        cx,
499                    );
500                },
501            );
502    })
503    .detach();
504}
505
506fn init_renderers(cx: &mut App) {
507    cx.default_global::<SettingFieldRenderer>()
508        .add_renderer::<UnimplementedSettingField>(
509            |settings_window, item, _, settings_file, _, sub_field, _, cx| {
510                render_settings_item(
511                    settings_window,
512                    item,
513                    settings_file,
514                    Button::new("open-in-settings-file", "Edit in settings.json")
515                        .style(ButtonStyle::Outlined)
516                        .size(ButtonSize::Medium)
517                        .tab_index(0_isize)
518                        .tooltip(Tooltip::for_action_title_in(
519                            "Edit in settings.json",
520                            &OpenCurrentFile,
521                            &settings_window.focus_handle,
522                        ))
523                        .on_click(cx.listener(|this, _, window, cx| {
524                            this.open_current_settings_file(window, cx);
525                        }))
526                        .into_any_element(),
527                    sub_field,
528                    cx,
529                )
530            },
531        )
532        .add_basic_renderer::<bool>(render_toggle_button)
533        .add_basic_renderer::<String>(render_text_field)
534        .add_basic_renderer::<SharedString>(render_text_field)
535        .add_basic_renderer::<settings::SaturatingBool>(render_toggle_button)
536        .add_basic_renderer::<settings::CursorShape>(render_dropdown)
537        .add_basic_renderer::<settings::RestoreOnStartupBehavior>(render_dropdown)
538        .add_basic_renderer::<settings::BottomDockLayout>(render_dropdown)
539        .add_basic_renderer::<settings::OnLastWindowClosed>(render_dropdown)
540        .add_basic_renderer::<settings::CliDefaultOpenBehavior>(render_dropdown)
541        .add_basic_renderer::<settings::DefaultOpenBehavior>(render_dropdown)
542        .add_basic_renderer::<settings::CloseWindowWhenNoItems>(render_dropdown)
543        .add_basic_renderer::<settings::TextRenderingMode>(render_dropdown)
544        .add_basic_renderer::<settings::FontFamilyName>(render_font_picker)
545        .add_basic_renderer::<settings::BaseKeymapContent>(render_dropdown)
546        .add_basic_renderer::<settings::MultiCursorModifier>(render_dropdown)
547        .add_basic_renderer::<settings::HideMouseMode>(render_dropdown)
548        .add_basic_renderer::<settings::ReduceMotionMode>(render_dropdown)
549        .add_basic_renderer::<settings::CurrentLineHighlight>(render_dropdown)
550        .add_basic_renderer::<settings::ShowWhitespaceSetting>(render_dropdown)
551        .add_basic_renderer::<settings::SoftWrap>(render_dropdown)
552        .add_basic_renderer::<settings::AutoIndentMode>(render_dropdown)
553        .add_basic_renderer::<settings::ScrollBeyondLastLine>(render_dropdown)
554        .add_basic_renderer::<settings::SnippetSortOrder>(render_dropdown)
555        .add_basic_renderer::<settings::ClosePosition>(render_dropdown)
556        .add_basic_renderer::<settings::DockSide>(render_dropdown)
557        .add_basic_renderer::<settings::TerminalDockPosition>(render_dropdown)
558        .add_basic_renderer::<settings::DockPosition>(render_dropdown)
559        .add_basic_renderer::<settings::SidebarDockPosition>(render_dropdown)
560        .add_basic_renderer::<settings::GitGutterSetting>(render_dropdown)
561        .add_basic_renderer::<settings::GitHunkStyleSetting>(render_dropdown)
562        .add_basic_renderer::<settings::GitPathStyle>(render_dropdown)
563        .add_basic_renderer::<settings::InlineBlameLocation>(render_dropdown)
564        .add_basic_renderer::<settings::DiagnosticSeverityContent>(render_dropdown)
565        .add_basic_renderer::<settings::SeedQuerySetting>(render_dropdown)
566        .add_basic_renderer::<settings::DoubleClickInMultibuffer>(render_dropdown)
567        .add_basic_renderer::<settings::GoToDefinitionFallback>(render_dropdown)
568        .add_basic_renderer::<settings::GoToDefinitionScrollStrategy>(render_dropdown)
569        .add_basic_renderer::<settings::OpenResultsIn>(render_dropdown)
570        .add_basic_renderer::<settings::ActivateOnClose>(render_dropdown)
571        .add_basic_renderer::<settings::ShowDiagnostics>(render_dropdown)
572        .add_basic_renderer::<settings::ShowCloseButton>(render_dropdown)
573        .add_basic_renderer::<settings::ProjectPanelEntrySpacing>(render_dropdown)
574        .add_basic_renderer::<settings::ProjectPanelSortMode>(render_dropdown)
575        .add_basic_renderer::<settings::ProjectPanelSortOrder>(render_dropdown)
576        .add_basic_renderer::<settings::RewrapBehavior>(render_dropdown)
577        .add_basic_renderer::<settings::FormatOnSave>(render_dropdown)
578        .add_basic_renderer::<settings::LineEndingSetting>(render_dropdown)
579        .add_basic_renderer::<settings::IndentGuideColoring>(render_dropdown)
580        .add_basic_renderer::<settings::IndentGuideBackgroundColoring>(render_dropdown)
581        .add_basic_renderer::<settings::ShowDiagnostics>(render_dropdown)
582        .add_basic_renderer::<settings::WordsCompletionMode>(render_dropdown)
583        .add_basic_renderer::<settings::LspInsertMode>(render_dropdown)
584        .add_basic_renderer::<settings::CompletionDetailAlignment>(render_dropdown)
585        .add_basic_renderer::<settings::CompletionMenuItemKind>(render_dropdown)
586        .add_basic_renderer::<settings::DiffViewStyle>(render_dropdown)
587        .add_basic_renderer::<settings::AlternateScroll>(render_dropdown)
588        .add_basic_renderer::<settings::TerminalBlink>(render_dropdown)
589        .add_basic_renderer::<settings::CursorShapeContent>(render_dropdown)
590        .add_basic_renderer::<settings::EditPredictionPromptFormatContent>(render_dropdown)
591        .add_basic_renderer::<settings::EditPredictionDataCollectionChoice>(render_dropdown)
592        .add_basic_renderer::<f32>(render_editable_number_field)
593        .add_basic_renderer::<settings::AutoCompactThreshold>(render_text_field)
594        .add_basic_renderer::<u32>(render_editable_number_field)
595        .add_basic_renderer::<u64>(render_editable_number_field)
596        .add_basic_renderer::<usize>(render_editable_number_field)
597        .add_basic_renderer::<NonZero<usize>>(render_editable_number_field)
598        .add_basic_renderer::<NonZeroU32>(render_editable_number_field)
599        .add_basic_renderer::<settings::CodeFade>(render_editable_number_field)
600        .add_basic_renderer::<settings::DelayMs>(render_editable_number_field)
601        .add_basic_renderer::<settings::FontWeightContent>(render_editable_number_field)
602        .add_basic_renderer::<settings::CenteredPaddingSettings>(render_editable_number_field)
603        .add_basic_renderer::<settings::InactiveOpacity>(render_editable_number_field)
604        .add_basic_renderer::<settings::MinimumContrast>(render_editable_number_field)
605        .add_basic_renderer::<settings::ShowScrollbar>(render_dropdown)
606        .add_basic_renderer::<settings::ScrollbarDiagnostics>(render_dropdown)
607        .add_basic_renderer::<settings::ShowMinimap>(render_dropdown)
608        .add_basic_renderer::<settings::DisplayIn>(render_dropdown)
609        .add_basic_renderer::<settings::MinimapThumb>(render_dropdown)
610        .add_basic_renderer::<settings::MinimapThumbBorder>(render_dropdown)
611        .add_basic_renderer::<settings::ModeContent>(render_dropdown)
612        .add_basic_renderer::<settings::UseSystemClipboard>(render_dropdown)
613        .add_basic_renderer::<settings::VimInsertModeCursorShape>(render_dropdown)
614        .add_basic_renderer::<settings::SteppingGranularity>(render_dropdown)
615        .add_basic_renderer::<settings::NotifyWhenAgentWaiting>(render_dropdown)
616        .add_basic_renderer::<settings::PlaySoundWhenAgentDone>(render_dropdown)
617        .add_basic_renderer::<settings::ThinkingBlockDisplay>(render_dropdown)
618        .add_basic_renderer::<settings::ImageFileSizeUnit>(render_dropdown)
619        .add_basic_renderer::<settings::StatusStyle>(render_dropdown)
620        .add_basic_renderer::<settings::GitPanelClickBehavior>(render_dropdown)
621        .add_basic_renderer::<settings::GitPanelSortBy>(render_dropdown)
622        .add_basic_renderer::<settings::GitPanelGroupBy>(render_dropdown)
623        .add_basic_renderer::<settings::EncodingDisplayOptions>(render_dropdown)
624        .add_basic_renderer::<settings::PaneSplitDirectionHorizontal>(render_dropdown)
625        .add_basic_renderer::<settings::PaneSplitDirectionVertical>(render_dropdown)
626        .add_basic_renderer::<settings::PaneSplitDirectionVertical>(render_dropdown)
627        .add_basic_renderer::<settings::CodeLens>(render_dropdown)
628        .add_basic_renderer::<settings::DocumentColorsRenderMode>(render_dropdown)
629        .add_basic_renderer::<settings::ThemeSelectionDiscriminants>(render_dropdown)
630        .add_basic_renderer::<settings::ThemeAppearanceMode>(render_dropdown)
631        .add_basic_renderer::<settings::ThemeName>(render_theme_picker)
632        .add_basic_renderer::<settings::IconThemeSelectionDiscriminants>(render_dropdown)
633        .add_basic_renderer::<settings::IconThemeName>(render_icon_theme_picker)
634        .add_basic_renderer::<settings::BufferLineHeightDiscriminants>(render_dropdown)
635        .add_basic_renderer::<settings::AutosaveSettingDiscriminants>(render_dropdown)
636        .add_basic_renderer::<settings::WorkingDirectoryDiscriminants>(render_dropdown)
637        .add_basic_renderer::<settings::IncludeIgnoredContent>(render_dropdown)
638        .add_basic_renderer::<settings::ShowIndentGuides>(render_dropdown)
639        .add_basic_renderer::<settings::ShellDiscriminants>(render_dropdown)
640        .add_basic_renderer::<settings::EditPredictionsMode>(render_dropdown)
641        .add_basic_renderer::<settings::RelativeLineNumbers>(render_dropdown)
642        .add_basic_renderer::<settings::WindowDecorations>(render_dropdown)
643        .add_basic_renderer::<settings::WindowButtonLayoutContentDiscriminants>(render_dropdown)
644        .add_basic_renderer::<settings::ScanSymlinksSetting>(render_dropdown)
645        .add_basic_renderer::<settings::FontSize>(render_editable_number_field)
646        .add_basic_renderer::<settings::OllamaModelName>(render_ollama_model_picker)
647        .add_basic_renderer::<settings::SemanticTokens>(render_dropdown)
648        .add_basic_renderer::<settings::DocumentFoldingRanges>(render_dropdown)
649        .add_basic_renderer::<settings::DocumentSymbols>(render_dropdown)
650        .add_basic_renderer::<settings::AudioInputDeviceName>(render_input_audio_device_dropdown)
651        .add_basic_renderer::<settings::AudioOutputDeviceName>(render_output_audio_device_dropdown)
652        .add_basic_renderer::<settings::TerminalBell>(render_dropdown)
653        // please semicolon stay on next line
654        ;
655}
656
657#[derive(Clone, Copy)]
658enum SettingsFileTarget {
659    User,
660    Project(WorktreeId),
661}
662
663impl From<&OpenSettingsAtTarget> for SettingsFileTarget {
664    fn from(target: &OpenSettingsAtTarget) -> Self {
665        match target {
666            OpenSettingsAtTarget::User => Self::User,
667            OpenSettingsAtTarget::Project { worktree_id } => {
668                Self::Project(WorktreeId::from_usize(*worktree_id))
669            }
670        }
671    }
672}
673
674pub fn open_settings_editor(
675    path: Option<&str>,
676    target_worktree_id: Option<WorktreeId>,
677    workspace_handle: Option<WindowHandle<MultiWorkspace>>,
678    cx: &mut App,
679) {
680    open_settings_editor_at_target(
681        path,
682        target_worktree_id.map(SettingsFileTarget::Project),
683        workspace_handle,
684        cx,
685    );
686}
687
688fn select_settings_file_target(
689    target_file: SettingsFileTarget,
690    settings_window: &mut SettingsWindow,
691    window: &mut Window,
692    cx: &mut Context<SettingsWindow>,
693) {
694    let file_index = settings_window
695        .files
696        .iter()
697        .position(|(file, _)| match target_file {
698            SettingsFileTarget::User => matches!(file, SettingsUiFile::User),
699            SettingsFileTarget::Project(worktree_id) => file.worktree_id() == Some(worktree_id),
700        });
701    if let Some(file_index) = file_index {
702        settings_window.change_file(file_index, window, cx);
703    }
704}
705
706fn open_settings_editor_to_page(
707    page: &str,
708    target_file: Option<SettingsFileTarget>,
709    workspace_handle: Option<WindowHandle<MultiWorkspace>>,
710    cx: &mut App,
711) {
712    let page = page.to_string();
713    open_settings_editor_with(workspace_handle, cx, move |settings_window, window, cx| {
714        if let Some(target_file) = target_file {
715            select_settings_file_target(target_file, settings_window, window, cx);
716        }
717
718        settings_window.opening_link = false;
719        settings_window.search_bar.update(cx, |editor, cx| {
720            editor.set_text(String::new(), window, cx);
721        });
722        for page_filter in &mut settings_window.filter_table {
723            page_filter.fill(true);
724        }
725        settings_window.has_query = false;
726        settings_window.filter_matches_to_file();
727
728        let Some(navbar_entry_index) = settings_window
729            .navbar_entries
730            .iter()
731            .position(|entry| entry.is_root && entry.title.eq_ignore_ascii_case(&page))
732        else {
733            log::error!("settings page not found: {page}");
734            return;
735        };
736
737        settings_window.open_and_scroll_to_navbar_entry(
738            navbar_entry_index,
739            None,
740            false,
741            window,
742            cx,
743        );
744    });
745}
746
747fn open_settings_editor_at_target(
748    path: Option<&str>,
749    target_file: Option<SettingsFileTarget>,
750    workspace_handle: Option<WindowHandle<MultiWorkspace>>,
751    cx: &mut App,
752) {
753    /// Assumes a settings GUI window is already open
754    fn open_path(
755        path: &str,
756        settings_window: &mut SettingsWindow,
757        window: &mut Window,
758        cx: &mut Context<SettingsWindow>,
759    ) {
760        if path.starts_with("languages.$(language)") {
761            log::error!("language-specific settings links are not currently supported");
762            return;
763        }
764
765        let query = format!("#{path}");
766        let indices = settings_window.filter_by_json_path(&query);
767
768        settings_window.opening_link = true;
769        settings_window.search_bar.update(cx, |editor, cx| {
770            editor.set_text(query.clone(), window, cx);
771        });
772        settings_window.apply_match_indices(indices.iter().copied(), &query);
773
774        if indices.len() == 1
775            && let Some(search_index) = settings_window.search_index.as_ref()
776        {
777            let SearchKeyLUTEntry {
778                page_index,
779                item_index,
780                header_index,
781                ..
782            } = search_index.key_lut[indices[0]];
783            let page = &settings_window.pages[page_index];
784            let item = &page.items[item_index];
785
786            if settings_window.filter_table[page_index][item_index]
787                && let SettingsPageItem::SubPageLink(link) = item
788                && let SettingsPageItem::SectionHeader(header) = page.items[header_index]
789            {
790                settings_window.push_sub_page(link.clone(), SharedString::from(header), window, cx);
791            }
792        }
793
794        cx.notify();
795    }
796
797    let path = path.map(ToOwned::to_owned);
798    open_settings_editor_with(workspace_handle, cx, move |settings_window, window, cx| {
799        if let Some(target_file) = target_file {
800            select_settings_file_target(target_file, settings_window, window, cx);
801        }
802        if let Some(path) = path {
803            open_path(&path, settings_window, window, cx);
804        } else if target_file.is_some() {
805            cx.notify();
806        }
807    });
808}
809
810pub fn open_skill_creator(
811    open_mode: pages::SkillCreatorOpenMode,
812    workspace_handle: Option<WindowHandle<MultiWorkspace>>,
813    cx: &mut App,
814) {
815    open_settings_editor_with(workspace_handle, cx, |settings_window, window, cx| {
816        settings_window.navigate_to_skill_creator(open_mode, window, cx);
817    });
818}
819
820fn open_settings_editor_with(
821    workspace_handle: Option<WindowHandle<MultiWorkspace>>,
822    cx: &mut App,
823    callback: impl FnOnce(&mut SettingsWindow, &mut Window, &mut Context<SettingsWindow>) + 'static,
824) {
825    telemetry::event!("Settings Viewed");
826
827    let existing_window = cx
828        .windows()
829        .into_iter()
830        .find_map(|window| window.downcast::<SettingsWindow>());
831
832    if let Some(existing_window) = existing_window {
833        existing_window
834            .update(cx, |settings_window, window, cx| {
835                settings_window.original_window = workspace_handle;
836
837                window.activate_window();
838                callback(settings_window, window, cx);
839            })
840            .ok();
841        return;
842    }
843
844    // We have to defer this to get the workspace off the stack.
845    cx.defer(move |cx| {
846        let current_rem_size: f32 = theme_settings::ThemeSettings::get_global(cx)
847            .ui_font_size(cx)
848            .into();
849
850        let default_bounds = DEFAULT_ADDITIONAL_WINDOW_SIZE;
851        let default_rem_size = 16.0;
852        let scale_factor = current_rem_size / default_rem_size;
853        let scaled_bounds: gpui::Size<Pixels> = default_bounds.map(|axis| axis * scale_factor);
854
855        let app_id = ReleaseChannel::global(cx).app_id();
856        let window_decorations = match std::env::var("ZED_WINDOW_DECORATIONS") {
857            Ok(val) if val == "server" => gpui::WindowDecorations::Server,
858            Ok(val) if val == "client" => gpui::WindowDecorations::Client,
859            _ => match WorkspaceSettings::get_global(cx).window_decorations {
860                settings::WindowDecorations::Server => gpui::WindowDecorations::Server,
861                settings::WindowDecorations::Client => gpui::WindowDecorations::Client,
862            },
863        };
864
865        cx.open_window(
866            WindowOptions {
867                titlebar: Some(TitlebarOptions {
868                    title: Some("Omega — Settings".into()),
869                    appears_transparent: true,
870                    traffic_light_position: Some(point(px(12.0), px(12.0))),
871                }),
872                focus: true,
873                show: true,
874                is_movable: true,
875                kind: gpui::WindowKind::Normal,
876                window_background: cx.theme().window_background_appearance(),
877                app_id: Some(app_id.to_owned()),
878                window_decorations: Some(window_decorations),
879                window_min_size: Some(gpui::Size {
880                    // Do not make the settings window thinner than this,
881                    // otherwise, the space used to display the actual content
882                    // gets so small that certain sections grow too tall due
883                    // to intense text wrapping.
884                    width: SIDEBAR_WIDTH + CONTENT_MIN_WIDTH,
885                    height: px(240.0),
886                }),
887                window_bounds: Some(WindowBounds::centered(scaled_bounds, cx)),
888                ..Default::default()
889            },
890            |window, cx| {
891                let settings_window =
892                    cx.new(|cx| SettingsWindow::new(workspace_handle, window, cx));
893                settings_window.update(cx, |settings_window, cx| {
894                    callback(settings_window, window, cx);
895                });
896
897                settings_window
898            },
899        )
900        .log_err();
901    });
902}
903
904/// The current sub page path that is selected.
905/// If this is empty the selected page is rendered,
906/// otherwise the last sub page gets rendered.
907///
908/// Global so that `pick` and `write` callbacks can access it
909/// and use it to dynamically render sub pages (e.g. for language settings)
910static ACTIVE_LANGUAGE: LazyLock<RwLock<Option<SharedString>>> =
911    LazyLock::new(|| RwLock::new(Option::None));
912
913fn active_language() -> Option<SharedString> {
914    ACTIVE_LANGUAGE
915        .read()
916        .ok()
917        .and_then(|language| language.clone())
918}
919
920fn active_language_mut() -> Option<std::sync::RwLockWriteGuard<'static, Option<SharedString>>> {
921    ACTIVE_LANGUAGE.write().ok()
922}
923
924pub struct SettingsWindow {
925    title_bar: Option<Entity<PlatformTitleBar>>,
926    original_window: Option<WindowHandle<MultiWorkspace>>,
927    files: Vec<(SettingsUiFile, FocusHandle)>,
928    worktree_root_dirs: HashMap<WorktreeId, String>,
929    current_file: SettingsUiFile,
930    pages: Vec<SettingsPage>,
931    sub_page_stack: Vec<SubPage>,
932    opening_link: bool,
933    search_bar: Entity<Editor>,
934    search_task: Option<Task<()>>,
935    /// Cached settings file buffers to avoid repeated disk I/O on each settings change
936    project_setting_file_buffers: HashMap<ProjectPath, Entity<Buffer>>,
937    /// Index into navbar_entries
938    navbar_entry: usize,
939    navbar_entries: Vec<NavBarEntry>,
940    navbar_scroll_handle: UniformListScrollHandle,
941    /// [page_index][page_item_index] will be false
942    /// when the item is filtered out either by searches
943    /// or by the current file
944    navbar_focus_subscriptions: Vec<gpui::Subscription>,
945    filter_table: Vec<Vec<bool>>,
946    has_query: bool,
947    content_handles: Vec<Vec<Entity<NonFocusableHandle>>>,
948    focus_handle: FocusHandle,
949    navbar_focus_handle: Entity<NonFocusableHandle>,
950    content_focus_handle: Entity<NonFocusableHandle>,
951    files_focus_handle: FocusHandle,
952    search_index: Option<Arc<SearchIndex>>,
953    list_state: ListState,
954    shown_errors: HashSet<String>,
955    pub(crate) hidden_deleted_skill_directory_paths: HashSet<PathBuf>,
956    pub(crate) regex_validation_error: Option<String>,
957    pub(crate) sandbox_host_validation_error: Option<String>,
958    last_copied_link_path: Option<&'static str>,
959    /// Cached configuration views per provider, created lazily.
960    pub(crate) provider_configuration_views:
961        HashMap<language_model::LanguageModelProviderId, gpui::AnyView>,
962    /// The provider whose configuration sub-page is currently open, if any.
963    pub(crate) configuring_provider: Option<language_model::LanguageModelProviderId>,
964    /// Directory path of the skill whose share link was most recently copied,
965    /// used to show a transient "copied" checkmark on its share button.
966    pub(crate) last_copied_skill_directory_path: Option<PathBuf>,
967    /// State for the active "add OpenAI/Anthropic-compatible provider" form sub-page, if open.
968    pub(crate) llm_provider_form: Option<LlmProviderForm>,
969    /// Stable focus handle for the LLM "Add Provider" button, so it can show a
970    /// focus ring when the page auto-focuses it on open (which happens via mouse,
971    /// where `focus_visible` styling would otherwise be suppressed).
972    pub(crate) llm_provider_add_focus_handle: FocusHandle,
973    /// State for the active "add/edit custom MCP server" form sub-page, if open.
974    pub(crate) mcp_server_form: Option<McpServerForm>,
975    /// Stable focus handle for the MCP "Add Server" button, so it can show a
976    /// focus ring when the page auto-focuses it on open (which happens via mouse,
977    /// where `focus_visible` styling would otherwise be suppressed).
978    pub(crate) mcp_add_server_focus_handle: FocusHandle,
979    /// State for the active "add/edit custom external agent" form sub-page, if open.
980    pub(crate) custom_agent_form: Option<CustomAgentForm>,
981    /// Stable focus handle for the external agents "Add Agent" button, so it can
982    /// show a focus ring when the page auto-focuses it on open (which happens via
983    /// mouse, where `focus_visible` styling would otherwise be suppressed).
984    pub(crate) external_agent_add_focus_handle: FocusHandle,
985    /// Harness maintenance state per external agent, for the External Agents
986    /// page. omega#81, `OMEGA-DELTA-0033`.
987    ///
988    /// Cached rather than computed during render because deciding it means
989    /// hashing an installed tree, and a settings row must not do that on every
990    /// frame. Refreshed when the window opens, when settings change, and after
991    /// any pin the owner takes or removes — the three moments it can differ.
992    pub(crate) harness_maintenance:
993        ::collections::HashMap<project::AgentId, omega_harness::HarnessFrontDoorState>,
994    pub(crate) harness_maintenance_task: Option<Task<()>>,
995    /// What went wrong the last time the owner pressed a maintenance control.
996    ///
997    /// Rendered beside the control. A pin write that fails silently is the same
998    /// defect as a refusal nobody can see.
999    pub(crate) harness_maintenance_error: Option<(project::AgentId, SharedString)>,
1000    skill_creator_page: Option<(Entity<pages::SkillCreatorPage>, Subscription)>,
1001}
1002
1003struct SearchDocument {
1004    id: usize,
1005    words: Vec<String>,
1006}
1007
1008struct SearchIndex {
1009    documents: Vec<SearchDocument>,
1010    fuzzy_match_candidates: Vec<StringMatchCandidate>,
1011    key_lut: Vec<SearchKeyLUTEntry>,
1012}
1013
1014struct SearchKeyLUTEntry {
1015    page_index: usize,
1016    header_index: usize,
1017    item_index: usize,
1018    json_path: Option<&'static str>,
1019}
1020
1021struct SubPage {
1022    link: SubPageLink,
1023    section_header: SharedString,
1024    scroll_handle: ScrollHandle,
1025}
1026
1027impl SubPage {
1028    fn new(link: SubPageLink, section_header: SharedString) -> Self {
1029        if link.r#type == SubPageType::Language
1030            && let Some(mut active_language_global) = active_language_mut()
1031        {
1032            active_language_global.replace(link.title.clone());
1033        }
1034
1035        SubPage {
1036            link,
1037            section_header,
1038            scroll_handle: ScrollHandle::new(),
1039        }
1040    }
1041}
1042
1043impl Drop for SubPage {
1044    fn drop(&mut self) {
1045        if self.link.r#type == SubPageType::Language
1046            && let Some(mut active_language_global) = active_language_mut()
1047            && active_language_global
1048                .as_ref()
1049                .is_some_and(|language_name| language_name == &self.link.title)
1050        {
1051            active_language_global.take();
1052        }
1053    }
1054}
1055
1056#[derive(Debug)]
1057struct NavBarEntry {
1058    title: &'static str,
1059    is_root: bool,
1060    expanded: bool,
1061    page_index: usize,
1062    item_index: Option<usize>,
1063    focus_handle: FocusHandle,
1064}
1065
1066struct SettingsPage {
1067    title: &'static str,
1068    items: Box<[SettingsPageItem]>,
1069}
1070
1071#[derive(PartialEq)]
1072enum SettingsPageItem {
1073    SectionHeader(&'static str),
1074    SettingItem(SettingItem),
1075    SubPageLink(SubPageLink),
1076    DynamicItem(DynamicItem),
1077    ActionLink(ActionLink),
1078}
1079
1080impl std::fmt::Debug for SettingsPageItem {
1081    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1082        match self {
1083            SettingsPageItem::SectionHeader(header) => write!(f, "SectionHeader({})", header),
1084            SettingsPageItem::SettingItem(setting_item) => {
1085                write!(f, "SettingItem({})", setting_item.title)
1086            }
1087            SettingsPageItem::SubPageLink(sub_page_link) => {
1088                write!(f, "SubPageLink({})", sub_page_link.title)
1089            }
1090            SettingsPageItem::DynamicItem(dynamic_item) => {
1091                write!(f, "DynamicItem({})", dynamic_item.discriminant.title)
1092            }
1093            SettingsPageItem::ActionLink(action_link) => {
1094                write!(f, "ActionLink({})", action_link.title)
1095            }
1096        }
1097    }
1098}
1099
1100impl SettingsPageItem {
1101    fn header_text(&self) -> Option<&'static str> {
1102        match self {
1103            SettingsPageItem::SectionHeader(header) => Some(header),
1104            _ => None,
1105        }
1106    }
1107
1108    fn render(
1109        &self,
1110        settings_window: &SettingsWindow,
1111        item_index: usize,
1112        bottom_border: bool,
1113        extra_bottom_padding: bool,
1114        window: &mut Window,
1115        cx: &mut Context<SettingsWindow>,
1116    ) -> AnyElement {
1117        let file = settings_window.current_file.clone();
1118
1119        let apply_padding = |element: Stateful<Div>| -> Stateful<Div> {
1120            let element = element.pt_4();
1121            if extra_bottom_padding {
1122                element.pb_10()
1123            } else {
1124                element.pb_4()
1125            }
1126        };
1127
1128        let mut render_setting_item_inner =
1129            |setting_item: &SettingItem,
1130             padding: bool,
1131             sub_field: bool,
1132             cx: &mut Context<SettingsWindow>| {
1133                let renderer = cx.default_global::<SettingFieldRenderer>().clone();
1134                let (_, found) = setting_item.field.file_set_in(file.clone(), cx);
1135
1136                let renderers = renderer.renderers.borrow();
1137
1138                let field_renderer =
1139                    renderers.get(&AnySettingField::type_id(setting_item.field.as_ref()));
1140                let field_renderer_or_warning =
1141                    field_renderer.ok_or("NO RENDERER").and_then(|renderer| {
1142                        if cfg!(debug_assertions) && !found {
1143                            Err("NO DEFAULT")
1144                        } else {
1145                            Ok(renderer)
1146                        }
1147                    });
1148
1149                let field = match field_renderer_or_warning {
1150                    Ok(field_renderer) => window.with_id(item_index, |window| {
1151                        field_renderer(
1152                            settings_window,
1153                            setting_item,
1154                            file.clone(),
1155                            setting_item.metadata.as_deref(),
1156                            sub_field,
1157                            window,
1158                            cx,
1159                        )
1160                    }),
1161                    Err(warning) => render_settings_item(
1162                        settings_window,
1163                        setting_item,
1164                        file.clone(),
1165                        Button::new("error-warning", warning)
1166                            .style(ButtonStyle::Outlined)
1167                            .size(ButtonSize::Medium)
1168                            .start_icon(Icon::new(IconName::Debug).color(Color::Error))
1169                            .tab_index(0_isize)
1170                            .tooltip(Tooltip::text(setting_item.field.type_name()))
1171                            .into_any_element(),
1172                        sub_field,
1173                        cx,
1174                    ),
1175                };
1176
1177                let field = if padding {
1178                    field.map(apply_padding)
1179                } else {
1180                    field
1181                };
1182
1183                (field, field_renderer_or_warning.is_ok())
1184            };
1185
1186        match self {
1187            SettingsPageItem::SectionHeader(header) => {
1188                SettingsSectionHeader::new(SharedString::new_static(header)).into_any_element()
1189            }
1190            SettingsPageItem::SettingItem(setting_item) => {
1191                let (field_with_padding, _) =
1192                    render_setting_item_inner(setting_item, true, false, cx);
1193
1194                v_flex()
1195                    .group("setting-item")
1196                    .px_8()
1197                    .child(field_with_padding)
1198                    .when(bottom_border, |this| this.child(Divider::horizontal()))
1199                    .into_any_element()
1200            }
1201            SettingsPageItem::SubPageLink(sub_page_link) => v_flex()
1202                .group("setting-item")
1203                .px_8()
1204                .child(
1205                    h_flex()
1206                        .id(sub_page_link.title.clone())
1207                        .w_full()
1208                        .min_w_0()
1209                        .justify_between()
1210                        .map(apply_padding)
1211                        .child(
1212                            v_flex()
1213                                .relative()
1214                                .w_full()
1215                                .max_w_1_2()
1216                                .child(Label::new(sub_page_link.title.clone()))
1217                                .when_some(
1218                                    sub_page_link.description.as_ref(),
1219                                    |this, description| {
1220                                        this.child(
1221                                            Label::new(description.clone())
1222                                                .size(LabelSize::Small)
1223                                                .color(Color::Muted),
1224                                        )
1225                                    },
1226                                ),
1227                        )
1228                        .child(
1229                            Button::new(
1230                                ("sub-page".into(), sub_page_link.title.clone()),
1231                                "Configure",
1232                            )
1233                            .aria_label(format!("Configure {}", sub_page_link.title))
1234                            .tab_index(0_isize)
1235                            .end_icon(
1236                                Icon::new(IconName::ChevronRight)
1237                                    .size(IconSize::Small)
1238                                    .color(Color::Muted),
1239                            )
1240                            .style(ButtonStyle::OutlinedGhost)
1241                            .size(ButtonSize::Medium)
1242                            .on_click({
1243                                let sub_page_link = sub_page_link.clone();
1244                                cx.listener(move |this, _, window, cx| {
1245                                    let header_text = this
1246                                        .sub_page_stack
1247                                        .last()
1248                                        .map(|sub_page| sub_page.link.title.clone())
1249                                        .or_else(|| {
1250                                            this.current_page()
1251                                                .items
1252                                                .iter()
1253                                                .take(item_index)
1254                                                .rev()
1255                                                .find_map(|item| {
1256                                                    item.header_text().map(SharedString::new_static)
1257                                                })
1258                                        });
1259
1260                                    let Some(header) = header_text else {
1261                                        unreachable!(
1262                                            "All items always have a section header above them"
1263                                        )
1264                                    };
1265
1266                                    this.push_sub_page(sub_page_link.clone(), header, window, cx)
1267                                })
1268                            }),
1269                        )
1270                        .child(render_settings_item_link(
1271                            sub_page_link.title.clone(),
1272                            sub_page_link.json_path,
1273                            false,
1274                            settings_window,
1275                            cx,
1276                        )),
1277                )
1278                .when(bottom_border, |this| this.child(Divider::horizontal()))
1279                .into_any_element(),
1280            SettingsPageItem::DynamicItem(DynamicItem {
1281                discriminant: discriminant_setting_item,
1282                pick_discriminant,
1283                fields,
1284            }) => {
1285                let file = file.to_settings();
1286                let discriminant = SettingsStore::global(cx)
1287                    .get_value_from_file(file, *pick_discriminant)
1288                    .1;
1289
1290                let (discriminant_element, rendered_ok) =
1291                    render_setting_item_inner(discriminant_setting_item, true, false, cx);
1292
1293                let has_sub_fields =
1294                    rendered_ok && discriminant.is_some_and(|d| !fields[d].is_empty());
1295
1296                let mut content = v_flex()
1297                    .id("dynamic-item")
1298                    .child(
1299                        div()
1300                            .group("setting-item")
1301                            .px_8()
1302                            .child(discriminant_element.when(has_sub_fields, |this| this.pb_4())),
1303                    )
1304                    .when(!has_sub_fields && bottom_border, |this| {
1305                        this.child(h_flex().px_8().child(Divider::horizontal()))
1306                    });
1307
1308                if rendered_ok {
1309                    let discriminant =
1310                        discriminant.expect("This should be Some if rendered_ok is true");
1311                    let sub_fields = &fields[discriminant];
1312                    let sub_field_count = sub_fields.len();
1313
1314                    for (index, field) in sub_fields.iter().enumerate() {
1315                        let is_last_sub_field = index == sub_field_count - 1;
1316                        let (raw_field, _) = render_setting_item_inner(field, false, true, cx);
1317
1318                        content = content.child(
1319                            raw_field
1320                                .group("setting-sub-item")
1321                                .mx_8()
1322                                .p_4()
1323                                .border_t_1()
1324                                .when(is_last_sub_field, |this| this.border_b_1())
1325                                .when(is_last_sub_field && extra_bottom_padding, |this| {
1326                                    this.mb_8()
1327                                })
1328                                .border_dashed()
1329                                .border_color(cx.theme().colors().border_variant)
1330                                .bg(cx.theme().colors().element_background.opacity(0.2)),
1331                        );
1332                    }
1333                }
1334
1335                return content.into_any_element();
1336            }
1337            SettingsPageItem::ActionLink(action_link) => v_flex()
1338                .group("setting-item")
1339                .px_8()
1340                .child(
1341                    h_flex()
1342                        .id(action_link.title.clone())
1343                        .w_full()
1344                        .min_w_0()
1345                        .justify_between()
1346                        .map(apply_padding)
1347                        .child(
1348                            v_flex()
1349                                .relative()
1350                                .w_full()
1351                                .max_w_1_2()
1352                                .child(Label::new(action_link.title.clone()))
1353                                .when_some(
1354                                    action_link.description.as_ref(),
1355                                    |this, description| {
1356                                        this.child(
1357                                            Label::new(description.clone())
1358                                                .size(LabelSize::Small)
1359                                                .color(Color::Muted),
1360                                        )
1361                                    },
1362                                ),
1363                        )
1364                        .child(
1365                            Button::new(
1366                                ("action-link".into(), action_link.title.clone()),
1367                                action_link.button_text.clone(),
1368                            )
1369                            .tab_index(0_isize)
1370                            .end_icon(
1371                                Icon::new(IconName::ArrowUpRight)
1372                                    .size(IconSize::Small)
1373                                    .color(Color::Muted),
1374                            )
1375                            .style(ButtonStyle::OutlinedGhost)
1376                            .size(ButtonSize::Medium)
1377                            .on_click({
1378                                let on_click = action_link.on_click.clone();
1379                                cx.listener(move |this, _, window, cx| {
1380                                    on_click(this, window, cx);
1381                                })
1382                            }),
1383                        ),
1384                )
1385                .when(bottom_border, |this| this.child(Divider::horizontal()))
1386                .into_any_element(),
1387        }
1388    }
1389}
1390
1391/// Shared layout for both JSON-backed and non-JSON-backed setting items.
1392///
1393/// Renders title + description on the left, control on the right, with
1394/// optional reset button and copy-link icon.
1395fn render_settings_item_layout(
1396    settings_window: &SettingsWindow,
1397    title: &'static str,
1398    description: &'static str,
1399    control: AnyElement,
1400    reset_fn: Option<Box<dyn Fn(&mut Window, &mut App)>>,
1401    modified_in: Option<String>,
1402    json_path: Option<&'static str>,
1403    sub_field: bool,
1404    cx: &mut Context<'_, SettingsWindow>,
1405) -> Stateful<Div> {
1406    // Note: the row itself is intentionally not exposed as a labeled group.
1407    // Each control names and describes itself (via the setting title and
1408    // description), so adding a group with the same label here would make
1409    // screen readers announce the setting name twice.
1410    h_flex()
1411        .id(title)
1412        .min_w_0()
1413        .justify_between()
1414        .child(
1415            v_flex()
1416                .relative()
1417                .w_full()
1418                .max_w_2_3()
1419                .min_w_0()
1420                .child(
1421                    h_flex()
1422                        .w_full()
1423                        .gap_1()
1424                        .child(Label::new(SharedString::new_static(title)))
1425                        .when_some(reset_fn, |this, reset_to_default| {
1426                            this.child(
1427                                IconButton::new("reset-to-default-btn", IconName::Undo)
1428                                    .icon_color(Color::Muted)
1429                                    .icon_size(IconSize::Small)
1430                                    .aria_label("Reset to Default")
1431                                    .tooltip(Tooltip::text("Reset to Default"))
1432                                    .on_click(move |_, window, cx| {
1433                                        reset_to_default(window, cx);
1434                                    }),
1435                            )
1436                        })
1437                        .when_some(modified_in, |this, modified_in| {
1438                            this.child(
1439                                Label::new(format!("\u{2014}  Modified in {modified_in}"))
1440                                    .color(Color::Muted)
1441                                    .size(LabelSize::Small),
1442                            )
1443                        }),
1444                )
1445                .child(
1446                    Label::new(SharedString::new_static(description))
1447                        .size(LabelSize::Small)
1448                        .color(Color::Muted)
1449                        .render_code_spans(),
1450                ),
1451        )
1452        .child(control)
1453        .when(settings_window.sub_page_stack.is_empty(), |this| {
1454            this.child(render_settings_item_link(
1455                description,
1456                json_path,
1457                sub_field,
1458                settings_window,
1459                cx,
1460            ))
1461        })
1462}
1463
1464fn render_settings_item(
1465    settings_window: &SettingsWindow,
1466    setting_item: &SettingItem,
1467    file: SettingsUiFile,
1468    control: AnyElement,
1469    sub_field: bool,
1470    cx: &mut Context<'_, SettingsWindow>,
1471) -> Stateful<Div> {
1472    let (found_in_file, _) = setting_item.field.file_set_in(file.clone(), cx);
1473    let file_set_in = SettingsUiFile::from_settings(found_in_file.clone());
1474
1475    let reset_fn = if sub_field {
1476        None
1477    } else {
1478        setting_item
1479            .field
1480            .reset_to_default_fn(&file, &found_in_file, cx)
1481    };
1482
1483    let modified_in = file_set_in
1484        .filter(|f| f != &file)
1485        .and_then(|f| settings_window.display_name(&f));
1486
1487    let control = if setting_item.field.is_overridden_by_organization(cx) {
1488        h_flex()
1489            .gap_2()
1490            .child(
1491                div()
1492                    .id(format!(
1493                        "{}-organization-configuration-warning",
1494                        setting_item.title
1495                    ))
1496                    .child(
1497                        Icon::new(IconName::Warning)
1498                            .size(IconSize::Small)
1499                            .color(Color::Warning),
1500                    )
1501                    .tooltip(|_, cx| {
1502                        Tooltip::with_meta(
1503                            "Overridden by Organization",
1504                            None,
1505                            "Contact your organization admins to adjust this setting.",
1506                            cx,
1507                        )
1508                    }),
1509            )
1510            .child(control)
1511            .into_any_element()
1512    } else {
1513        control
1514    };
1515
1516    render_settings_item_layout(
1517        settings_window,
1518        setting_item.title,
1519        setting_item.description,
1520        control,
1521        reset_fn,
1522        modified_in,
1523        setting_item.field.json_path(),
1524        sub_field,
1525        cx,
1526    )
1527}
1528
1529fn render_settings_item_link(
1530    id: impl Into<ElementId>,
1531    json_path: Option<&'static str>,
1532    sub_field: bool,
1533    settings_window: &SettingsWindow,
1534    cx: &mut Context<'_, SettingsWindow>,
1535) -> impl IntoElement {
1536    let copied_link_matches =
1537        json_path.is_some() && json_path == settings_window.last_copied_link_path;
1538
1539    let (link_icon, link_icon_color) = if copied_link_matches {
1540        (IconName::Check, Color::Success)
1541    } else {
1542        (IconName::Link, Color::Muted)
1543    };
1544
1545    div()
1546        .absolute()
1547        .top(rems_from_px(18.))
1548        .map(|this| {
1549            if sub_field {
1550                this.visible_on_hover("setting-sub-item")
1551                    .left(rems_from_px(-8.5))
1552            } else {
1553                this.visible_on_hover("setting-item")
1554                    .left(rems_from_px(-22.))
1555            }
1556        })
1557        .child(
1558            IconButton::new((id.into(), "copy-link-btn"), link_icon)
1559                .icon_color(link_icon_color)
1560                .icon_size(IconSize::Small)
1561                .shape(IconButtonShape::Square)
1562                .aria_label("Copy Link")
1563                .tooltip(Tooltip::text("Copy Link"))
1564                .when_some(json_path, |this, path| {
1565                    this.on_click(cx.listener(move |this, _, _, cx| {
1566                        let link = format!("zed://settings/{}", path);
1567                        cx.write_to_clipboard(ClipboardItem::new_string(link));
1568                        this.last_copied_link_path = Some(path);
1569                        cx.notify();
1570                    }))
1571                }),
1572        )
1573}
1574
1575struct SettingItem {
1576    title: &'static str,
1577    description: &'static str,
1578    field: Box<dyn AnySettingField>,
1579    metadata: Option<Box<SettingsFieldMetadata>>,
1580    files: FileMask,
1581}
1582
1583struct DynamicItem {
1584    discriminant: SettingItem,
1585    pick_discriminant: fn(&SettingsContent) -> Option<usize>,
1586    fields: Vec<Vec<SettingItem>>,
1587}
1588
1589impl PartialEq for DynamicItem {
1590    fn eq(&self, other: &Self) -> bool {
1591        self.discriminant == other.discriminant && self.fields == other.fields
1592    }
1593}
1594
1595#[derive(PartialEq, Eq, Clone, Copy)]
1596struct FileMask(u8);
1597
1598impl std::fmt::Debug for FileMask {
1599    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1600        write!(f, "FileMask(")?;
1601        let mut items = vec![];
1602
1603        if self.contains(USER) {
1604            items.push("USER");
1605        }
1606        if self.contains(PROJECT) {
1607            items.push("LOCAL");
1608        }
1609        if self.contains(SERVER) {
1610            items.push("SERVER");
1611        }
1612
1613        write!(f, "{})", items.join(" | "))
1614    }
1615}
1616
1617const USER: FileMask = FileMask(1 << 0);
1618const PROJECT: FileMask = FileMask(1 << 2);
1619const SERVER: FileMask = FileMask(1 << 3);
1620
1621impl std::ops::BitAnd for FileMask {
1622    type Output = Self;
1623
1624    fn bitand(self, other: Self) -> Self {
1625        Self(self.0 & other.0)
1626    }
1627}
1628
1629impl std::ops::BitOr for FileMask {
1630    type Output = Self;
1631
1632    fn bitor(self, other: Self) -> Self {
1633        Self(self.0 | other.0)
1634    }
1635}
1636
1637impl FileMask {
1638    fn contains(&self, other: FileMask) -> bool {
1639        self.0 & other.0 != 0
1640    }
1641}
1642
1643impl PartialEq for SettingItem {
1644    fn eq(&self, other: &Self) -> bool {
1645        self.title == other.title
1646            && self.description == other.description
1647            && (match (&self.metadata, &other.metadata) {
1648                (None, None) => true,
1649                (Some(m1), Some(m2)) => m1.placeholder == m2.placeholder,
1650                _ => false,
1651            })
1652    }
1653}
1654
1655#[derive(Clone, PartialEq, Default)]
1656enum SubPageType {
1657    Language,
1658    SkillCreator,
1659    #[default]
1660    Other,
1661}
1662
1663#[derive(Clone)]
1664struct SubPageLink {
1665    title: SharedString,
1666    r#type: SubPageType,
1667    description: Option<SharedString>,
1668    search_aliases: &'static [&'static str],
1669    /// See [`SettingField.json_path`]
1670    json_path: Option<&'static str>,
1671    /// Whether or not the settings in this sub page are configurable in settings.json
1672    /// Removes the "Edit in settings.json" button from the page.
1673    in_json: bool,
1674    files: FileMask,
1675    render:
1676        fn(&SettingsWindow, &ScrollHandle, &mut Window, &mut Context<SettingsWindow>) -> AnyElement,
1677}
1678
1679impl PartialEq for SubPageLink {
1680    fn eq(&self, other: &Self) -> bool {
1681        self.title == other.title
1682    }
1683}
1684
1685#[derive(Clone)]
1686struct ActionLink {
1687    title: SharedString,
1688    description: Option<SharedString>,
1689    button_text: SharedString,
1690    on_click: Arc<dyn Fn(&mut SettingsWindow, &mut Window, &mut App) + Send + Sync>,
1691    files: FileMask,
1692}
1693
1694impl PartialEq for ActionLink {
1695    fn eq(&self, other: &Self) -> bool {
1696        self.title == other.title
1697    }
1698}
1699
1700fn all_language_names(cx: &App) -> Vec<SharedString> {
1701    let state = workspace::AppState::global(cx);
1702    state
1703        .languages
1704        .language_names()
1705        .into_iter()
1706        .filter(|name| name.as_ref() != "Keybind Context")
1707        .map(Into::into)
1708        .collect()
1709}
1710
1711#[allow(unused)]
1712#[derive(Clone, PartialEq, Debug)]
1713enum SettingsUiFile {
1714    User,                                // Uses all settings.
1715    Project((WorktreeId, Arc<RelPath>)), // Has a special name, and special set of settings
1716    Server(&'static str),                // Uses a special name, and the user settings
1717}
1718
1719impl SettingsUiFile {
1720    fn setting_type(&self) -> &'static str {
1721        match self {
1722            SettingsUiFile::User => "User",
1723            SettingsUiFile::Project(_) => "Project",
1724            SettingsUiFile::Server(_) => "Server",
1725        }
1726    }
1727
1728    fn is_server(&self) -> bool {
1729        matches!(self, SettingsUiFile::Server(_))
1730    }
1731
1732    fn worktree_id(&self) -> Option<WorktreeId> {
1733        match self {
1734            SettingsUiFile::User => None,
1735            SettingsUiFile::Project((worktree_id, _)) => Some(*worktree_id),
1736            SettingsUiFile::Server(_) => None,
1737        }
1738    }
1739
1740    fn from_settings(file: settings::SettingsFile) -> Option<Self> {
1741        Some(match file {
1742            settings::SettingsFile::User => SettingsUiFile::User,
1743            settings::SettingsFile::Project(location) => SettingsUiFile::Project(location),
1744            settings::SettingsFile::Server => SettingsUiFile::Server("todo: server name"),
1745            settings::SettingsFile::Default => return None,
1746            settings::SettingsFile::Global => return None,
1747        })
1748    }
1749
1750    fn to_settings(&self) -> settings::SettingsFile {
1751        match self {
1752            SettingsUiFile::User => settings::SettingsFile::User,
1753            SettingsUiFile::Project(location) => settings::SettingsFile::Project(location.clone()),
1754            SettingsUiFile::Server(_) => settings::SettingsFile::Server,
1755        }
1756    }
1757
1758    fn mask(&self) -> FileMask {
1759        match self {
1760            SettingsUiFile::User => USER,
1761            SettingsUiFile::Project(_) => PROJECT,
1762            SettingsUiFile::Server(_) => SERVER,
1763        }
1764    }
1765}
1766
1767impl SettingsWindow {
1768    fn new(
1769        original_window: Option<WindowHandle<MultiWorkspace>>,
1770        window: &mut Window,
1771        cx: &mut Context<Self>,
1772    ) -> Self {
1773        let font_family_cache = theme::FontFamilyCache::global(cx);
1774
1775        cx.spawn(async move |this, cx| {
1776            font_family_cache.prefetch(cx).await;
1777            this.update(cx, |_, cx| {
1778                cx.notify();
1779            })
1780        })
1781        .detach();
1782
1783        let current_file = SettingsUiFile::User;
1784        let search_bar = cx.new(|cx| {
1785            let mut editor = Editor::single_line(window, cx);
1786            editor.set_placeholder_text("Search settings…", window, cx);
1787            editor
1788        });
1789        cx.subscribe(&search_bar, |this, _, event: &EditorEvent, cx| {
1790            let EditorEvent::Edited { transaction_id: _ } = event else {
1791                return;
1792            };
1793
1794            if this.opening_link {
1795                this.opening_link = false;
1796                return;
1797            }
1798            this.update_matches(cx);
1799        })
1800        .detach();
1801
1802        let mut ui_font_size = ThemeSettings::get_global(cx).ui_font_size(cx);
1803        cx.observe_global_in::<SettingsStore>(window, move |this, window, cx| {
1804            this.fetch_files(window, cx);
1805            // omega#81. Adding, removing, or re-versioning an external agent is
1806            // a settings change, and each of those changes what the External
1807            // Agents page must say about pins and provenance.
1808            this.refresh_harness_maintenance(cx);
1809
1810            // Whenever settings are changed, it's possible that the changed
1811            // settings affects the rendering of the `SettingsWindow`, like is
1812            // the case with `ui_font_size`. When that happens, we need to
1813            // instruct the `ListState` to re-measure the list items, as the
1814            // list item heights may have changed depending on the new font
1815            // size.
1816            let new_ui_font_size = ThemeSettings::get_global(cx).ui_font_size(cx);
1817            if new_ui_font_size != ui_font_size {
1818                this.list_state.remeasure();
1819                ui_font_size = new_ui_font_size;
1820            }
1821
1822            cx.notify();
1823        })
1824        .detach();
1825
1826        use feature_flags::FeatureFlagAppExt as _;
1827        let mut last_is_staff = cx.is_staff();
1828        cx.observe_global_in::<feature_flags::FeatureFlagStore>(window, move |this, window, cx| {
1829            let is_staff = cx.is_staff();
1830            if is_staff != last_is_staff {
1831                last_is_staff = is_staff;
1832                this.rebuild_pages(window, cx);
1833            }
1834        })
1835        .detach();
1836
1837        cx.observe_global_in::<SkillIndex>(window, |this, _window, cx| {
1838            if let Some(skill_index) = cx.try_global::<SkillIndex>() {
1839                this.hidden_deleted_skill_directory_paths
1840                    .retain(|directory_path| {
1841                        skill_index
1842                            .global_skills
1843                            .iter()
1844                            .chain(
1845                                skill_index
1846                                    .project_skills
1847                                    .iter()
1848                                    .flat_map(|group| group.skills.iter()),
1849                            )
1850                            .any(|skill| skill.directory_path.as_path() == directory_path.as_path())
1851                    });
1852            } else {
1853                this.hidden_deleted_skill_directory_paths.clear();
1854            }
1855            cx.notify();
1856        })
1857        .detach();
1858
1859        let language_model_registry = language_model::LanguageModelRegistry::global(cx);
1860        cx.subscribe(&language_model_registry, |_, _, _event, cx| {
1861            cx.notify();
1862        })
1863        .detach();
1864
1865        cx.on_window_closed(|cx, _window_id| {
1866            if let Some(existing_window) = cx
1867                .windows()
1868                .into_iter()
1869                .find_map(|window| window.downcast::<SettingsWindow>())
1870                && cx.windows().len() == 1
1871            {
1872                cx.update_window(*existing_window, |_, window, _| {
1873                    window.remove_window();
1874                })
1875                .ok();
1876
1877                telemetry::event!("Settings Closed")
1878            }
1879        })
1880        .detach();
1881
1882        let app_state = AppState::global(cx);
1883        let workspaces: Vec<Entity<Workspace>> = app_state
1884            .workspace_store
1885            .read(cx)
1886            .workspaces()
1887            .filter_map(|weak| weak.upgrade())
1888            .collect();
1889
1890        for workspace in workspaces {
1891            let project = workspace.read(cx).project().clone();
1892            cx.observe_release_in(&project, window, |this, _, window, cx| {
1893                this.fetch_files(window, cx)
1894            })
1895            .detach();
1896            cx.subscribe_in(&project, window, Self::handle_project_event)
1897                .detach();
1898            cx.observe_release_in(&workspace, window, |this, _, window, cx| {
1899                this.fetch_files(window, cx)
1900            })
1901            .detach();
1902        }
1903
1904        let this_weak = cx.weak_entity();
1905        cx.observe_new::<Project>({
1906            let this_weak = this_weak.clone();
1907
1908            move |_, window, cx| {
1909                let project = cx.entity();
1910                let Some(window) = window else {
1911                    return;
1912                };
1913
1914                this_weak
1915                    .update(cx, |_, cx| {
1916                        cx.defer_in(window, |settings_window, window, cx| {
1917                            settings_window.fetch_files(window, cx)
1918                        });
1919                        cx.observe_release_in(&project, window, |_, _, window, cx| {
1920                            cx.defer_in(window, |this, window, cx| this.fetch_files(window, cx));
1921                        })
1922                        .detach();
1923
1924                        cx.subscribe_in(&project, window, Self::handle_project_event)
1925                            .detach();
1926                    })
1927                    .ok();
1928            }
1929        })
1930        .detach();
1931
1932        let handle = window.window_handle();
1933        cx.observe_new::<Workspace>(move |workspace, _, cx| {
1934            let project = workspace.project().clone();
1935            let this_weak = this_weak.clone();
1936
1937            // We defer on the settings window (via `handle`) rather than using
1938            // the workspace's window from observe_new. When window.defer() runs
1939            // its callback, it calls handle.update() which temporarily removes
1940            // that window from cx.windows. If we deferred on the workspace's
1941            // window, then when fetch_files() tries to read ALL workspaces from
1942            // the store (including the newly created one), it would fail with
1943            // "window not found" because that workspace's window would be
1944            // temporarily removed from cx.windows for the duration of our callback.
1945            handle
1946                .update(cx, move |_, window, cx| {
1947                    window.defer(cx, move |window, cx| {
1948                        this_weak
1949                            .update(cx, |this, cx| {
1950                                this.fetch_files(window, cx);
1951                                cx.observe_release_in(&project, window, |this, _, window, cx| {
1952                                    this.fetch_files(window, cx)
1953                                })
1954                                .detach();
1955                            })
1956                            .ok();
1957                    });
1958                })
1959                .ok();
1960        })
1961        .detach();
1962
1963        let title_bar = if !cfg!(target_os = "macos") {
1964            Some(cx.new(|cx| PlatformTitleBar::new("settings-title-bar", cx)))
1965        } else {
1966            None
1967        };
1968
1969        let list_state = gpui::ListState::new(0, gpui::ListAlignment::Top, px(0.0)).measure_all();
1970        list_state.set_scroll_handler(|_, _, _| {});
1971
1972        let mut this = Self {
1973            title_bar,
1974            original_window,
1975
1976            worktree_root_dirs: HashMap::default(),
1977            files: vec![],
1978
1979            current_file: current_file,
1980            project_setting_file_buffers: HashMap::default(),
1981            pages: vec![],
1982            sub_page_stack: vec![],
1983            opening_link: false,
1984            navbar_entries: vec![],
1985            navbar_entry: 0,
1986            navbar_scroll_handle: UniformListScrollHandle::default(),
1987            search_bar,
1988            search_task: None,
1989            filter_table: vec![],
1990            has_query: false,
1991            content_handles: vec![],
1992            focus_handle: cx.focus_handle(),
1993            navbar_focus_handle: NonFocusableHandle::new(
1994                NAVBAR_CONTAINER_TAB_INDEX,
1995                false,
1996                window,
1997                cx,
1998            ),
1999            navbar_focus_subscriptions: vec![],
2000            content_focus_handle: NonFocusableHandle::new(
2001                CONTENT_CONTAINER_TAB_INDEX,
2002                false,
2003                window,
2004                cx,
2005            ),
2006            files_focus_handle: cx
2007                .focus_handle()
2008                .tab_index(HEADER_CONTAINER_TAB_INDEX)
2009                .tab_stop(false),
2010            search_index: None,
2011            shown_errors: HashSet::default(),
2012            hidden_deleted_skill_directory_paths: HashSet::default(),
2013            regex_validation_error: None,
2014            sandbox_host_validation_error: None,
2015            list_state,
2016            last_copied_link_path: None,
2017            provider_configuration_views: HashMap::default(),
2018            configuring_provider: None,
2019            last_copied_skill_directory_path: None,
2020            llm_provider_form: None,
2021            llm_provider_add_focus_handle: cx.focus_handle(),
2022            mcp_server_form: None,
2023            mcp_add_server_focus_handle: cx.focus_handle(),
2024            custom_agent_form: None,
2025            external_agent_add_focus_handle: cx.focus_handle(),
2026            harness_maintenance: Default::default(),
2027            harness_maintenance_task: None,
2028            harness_maintenance_error: None,
2029            skill_creator_page: None,
2030        };
2031
2032        this.fetch_files(window, cx);
2033        this.build_ui(window, cx);
2034        this.build_search_index();
2035        this.refresh_harness_maintenance(cx);
2036
2037        this.search_bar.update(cx, |editor, cx| {
2038            editor.focus_handle(cx).focus(window, cx);
2039        });
2040
2041        this
2042    }
2043
2044    /// Recompute what the External Agents page shows about pins and
2045    /// provenance. omega#81, `OMEGA-DELTA-0033`.
2046    ///
2047    /// Reads the pin ledger, measures each installed tree, and reads the
2048    /// receipt log — the same three inputs the launch gate reads, through the
2049    /// same function. The page renders the result and decides nothing.
2050    ///
2051    /// Off the render path deliberately: hashing an installed tree per frame
2052    /// would make opening a settings page cost megabytes of disk reads a
2053    /// second.
2054    pub(crate) fn refresh_harness_maintenance(&mut self, cx: &mut Context<Self>) {
2055        let Some(store) = pages::external_agents_page::agent_server_store(self, cx) else {
2056            self.harness_maintenance.clear();
2057            self.harness_maintenance_task = None;
2058            return;
2059        };
2060        let targets = {
2061            let store = store.read(cx);
2062            store
2063                .external_agents()
2064                .cloned()
2065                .collect::<Vec<_>>()
2066                .into_iter()
2067                .filter_map(|id| store.maintenance_target(&id).map(|target| (id, target)))
2068                .collect::<Vec<_>>()
2069        };
2070        let fs = <dyn fs::Fs>::global(cx);
2071
2072        self.harness_maintenance_task = Some(cx.spawn(async move |this, cx| {
2073            let mut states = ::collections::HashMap::default();
2074            for (id, target) in targets {
2075                let state = project::harness_maintenance::read_front_door_state(
2076                    fs.as_ref(),
2077                    target.harness_id.as_ref(),
2078                    target.version.as_ref(),
2079                    target.distribution,
2080                    target.installed_dir.as_deref(),
2081                )
2082                .await;
2083                states.insert(id, state);
2084            }
2085            this.update(cx, |this, cx| {
2086                this.harness_maintenance = states;
2087                cx.notify();
2088            })
2089            .ok();
2090        }));
2091    }
2092
2093    /// Recompute the maintenance rows and wait for nothing.
2094    ///
2095    /// The visual test needs the row to be populated at the moment it takes the
2096    /// picture; production repopulates it on window open, on a settings change,
2097    /// and after an owner action, none of which a screenshot can wait for.
2098    #[cfg(any(test, feature = "test-support"))]
2099    pub fn refresh_harness_maintenance_for_test(&mut self, cx: &mut Context<Self>) {
2100        self.refresh_harness_maintenance(cx);
2101    }
2102
2103    fn harness_maintenance_target(
2104        &self,
2105        id: &project::AgentId,
2106        cx: &App,
2107    ) -> Option<project::agent_server_store::HarnessMaintenanceTarget> {
2108        let store = pages::external_agents_page::agent_server_store(self, cx)?;
2109        store.read(cx).maintenance_target(id)
2110    }
2111
2112    /// Freeze one harness at the bytes Omega measures now. omega#81.
2113    ///
2114    /// The whole write happens in `project::harness_maintenance`, through the
2115    /// same gate the launch path runs, so a tree the gate would refuse cannot be
2116    /// pinned from here. This method routes a click and shows what came back.
2117    pub(crate) fn pin_harness(&mut self, id: &project::AgentId, cx: &mut Context<Self>) {
2118        let Some(target) = self.harness_maintenance_target(id, cx) else {
2119            return;
2120        };
2121        let fs = <dyn fs::Fs>::global(cx);
2122        let id = id.clone();
2123        self.harness_maintenance_error = None;
2124        cx.spawn(async move |this, cx| {
2125            let result = match target.installed_dir.as_deref() {
2126                Some(dir) => {
2127                    project::harness_maintenance::pin_installed_harness(
2128                        fs,
2129                        target.harness_id.as_ref(),
2130                        target.version.as_ref(),
2131                        dir,
2132                        project::harness_maintenance::now_ms(),
2133                    )
2134                    .await
2135                }
2136                None => Err(anyhow::anyhow!(
2137                    "Omega owns no installed directory for this agent, so there is nothing to pin."
2138                )),
2139            };
2140            this.update(cx, |this, cx| {
2141                this.set_harness_maintenance_result(&id, result, cx);
2142            })
2143            .ok();
2144        })
2145        .detach();
2146    }
2147
2148    /// Unfreeze one harness. omega#81.
2149    pub(crate) fn unpin_harness(&mut self, id: &project::AgentId, cx: &mut Context<Self>) {
2150        let Some(target) = self.harness_maintenance_target(id, cx) else {
2151            return;
2152        };
2153        let fs = <dyn fs::Fs>::global(cx);
2154        let id = id.clone();
2155        self.harness_maintenance_error = None;
2156        cx.spawn(async move |this, cx| {
2157            let result =
2158                project::harness_maintenance::unpin_harness(fs, target.harness_id.as_ref()).await;
2159            this.update(cx, |this, cx| {
2160                this.set_harness_maintenance_result(&id, result, cx);
2161            })
2162            .ok();
2163        })
2164        .detach();
2165    }
2166
2167    /// Re-measure one harness's installed tree on request, writing a receipt
2168    /// under [`omega_harness::MaintenanceAction::ReprobeCapability`]. omega#81.
2169    pub(crate) fn reprobe_harness(&mut self, id: &project::AgentId, cx: &mut Context<Self>) {
2170        let Some(target) = self.harness_maintenance_target(id, cx) else {
2171            return;
2172        };
2173        let fs = <dyn fs::Fs>::global(cx);
2174        let id = id.clone();
2175        self.harness_maintenance_error = None;
2176        cx.spawn(async move |this, cx| {
2177            let result = match target.installed_dir.as_deref() {
2178                Some(dir) => project::harness_maintenance::reprobe_installed_harness(
2179                    fs,
2180                    target.harness_id.as_ref(),
2181                    target.version.as_ref(),
2182                    dir,
2183                    project::harness_maintenance::now_ms(),
2184                )
2185                .await
2186                .map(|_| ()),
2187                None => Err(anyhow::anyhow!(
2188                    "Omega owns no installed directory for this agent, so there is nothing to \
2189                     re-check."
2190                )),
2191            };
2192            this.update(cx, |this, cx| {
2193                this.set_harness_maintenance_result(&id, result, cx);
2194            })
2195            .ok();
2196        })
2197        .detach();
2198    }
2199
2200    /// Show what a maintenance control did, and re-read the state either way.
2201    ///
2202    /// A failed pin write that left the row unchanged and said nothing would be
2203    /// the same defect the receipt contract exists to prevent, one layer up.
2204    fn set_harness_maintenance_result(
2205        &mut self,
2206        id: &project::AgentId,
2207        result: anyhow::Result<()>,
2208        cx: &mut Context<Self>,
2209    ) {
2210        self.harness_maintenance_error = result
2211            .err()
2212            .map(|error| (id.clone(), SharedString::from(error.to_string())));
2213        self.refresh_harness_maintenance(cx);
2214        cx.notify();
2215    }
2216
2217    fn handle_project_event(
2218        &mut self,
2219        _: &Entity<Project>,
2220        event: &project::Event,
2221        window: &mut Window,
2222        cx: &mut Context<SettingsWindow>,
2223    ) {
2224        match event {
2225            project::Event::WorktreeRemoved(_) | project::Event::WorktreeAdded(_) => {
2226                cx.defer_in(window, |this, window, cx| {
2227                    this.fetch_files(window, cx);
2228                });
2229            }
2230            _ => {}
2231        }
2232    }
2233
2234    fn toggle_navbar_entry(&mut self, nav_entry_index: usize) {
2235        // We can only toggle root entries
2236        if !self.navbar_entries[nav_entry_index].is_root {
2237            return;
2238        }
2239
2240        let expanded = &mut self.navbar_entries[nav_entry_index].expanded;
2241        *expanded = !*expanded;
2242        self.navbar_entry = nav_entry_index;
2243        self.reset_list_state();
2244    }
2245
2246    fn toggle_and_focus_navbar_entry(
2247        &mut self,
2248        nav_entry_index: usize,
2249        window: &mut Window,
2250        cx: &mut Context<Self>,
2251    ) {
2252        self.toggle_navbar_entry(nav_entry_index);
2253        window.focus(&self.navbar_entries[nav_entry_index].focus_handle, cx);
2254        cx.notify();
2255    }
2256
2257    fn toggle_navbar_entry_on_double_click(
2258        &mut self,
2259        nav_entry_index: usize,
2260        event: &gpui::ClickEvent,
2261        window: &mut Window,
2262        cx: &mut Context<Self>,
2263    ) -> bool {
2264        let Some(entry) = self.navbar_entries.get(nav_entry_index) else {
2265            return false;
2266        };
2267        if !entry.is_root || event.click_count() != 2 {
2268            return false;
2269        }
2270
2271        self.toggle_and_focus_navbar_entry(nav_entry_index, window, cx);
2272        true
2273    }
2274
2275    fn build_navbar(&mut self, cx: &App) {
2276        let mut navbar_entries = Vec::new();
2277
2278        for (page_index, page) in self.pages.iter().enumerate() {
2279            navbar_entries.push(NavBarEntry {
2280                title: page.title,
2281                is_root: true,
2282                expanded: false,
2283                page_index,
2284                item_index: None,
2285                focus_handle: cx.focus_handle().tab_index(0).tab_stop(true),
2286            });
2287
2288            for (item_index, item) in page.items.iter().enumerate() {
2289                let SettingsPageItem::SectionHeader(title) = item else {
2290                    continue;
2291                };
2292                navbar_entries.push(NavBarEntry {
2293                    title,
2294                    is_root: false,
2295                    expanded: false,
2296                    page_index,
2297                    item_index: Some(item_index),
2298                    focus_handle: cx.focus_handle().tab_index(0).tab_stop(true),
2299                });
2300            }
2301        }
2302
2303        self.navbar_entries = navbar_entries;
2304    }
2305
2306    fn setup_navbar_focus_subscriptions(
2307        &mut self,
2308        window: &mut Window,
2309        cx: &mut Context<SettingsWindow>,
2310    ) {
2311        let mut focus_subscriptions = Vec::new();
2312
2313        for entry_index in 0..self.navbar_entries.len() {
2314            let focus_handle = self.navbar_entries[entry_index].focus_handle.clone();
2315
2316            let subscription = cx.on_focus(
2317                &focus_handle,
2318                window,
2319                move |this: &mut SettingsWindow,
2320                      window: &mut Window,
2321                      cx: &mut Context<SettingsWindow>| {
2322                    if this.sub_page_stack.is_empty() {
2323                        this.open_and_scroll_to_navbar_entry(entry_index, None, false, window, cx);
2324                    }
2325                },
2326            );
2327            focus_subscriptions.push(subscription);
2328        }
2329        self.navbar_focus_subscriptions = focus_subscriptions;
2330    }
2331
2332    fn visible_navbar_entries(&self) -> impl Iterator<Item = (usize, &NavBarEntry)> {
2333        let mut index = 0;
2334        let entries = &self.navbar_entries;
2335        let search_matches = &self.filter_table;
2336        let has_query = self.has_query;
2337        std::iter::from_fn(move || {
2338            while index < entries.len() {
2339                let entry = &entries[index];
2340                let included_in_search = if let Some(item_index) = entry.item_index {
2341                    search_matches[entry.page_index][item_index]
2342                } else {
2343                    search_matches[entry.page_index].iter().any(|b| *b)
2344                        || search_matches[entry.page_index].is_empty()
2345                };
2346                if included_in_search {
2347                    break;
2348                }
2349                index += 1;
2350            }
2351            if index >= self.navbar_entries.len() {
2352                return None;
2353            }
2354            let entry = &entries[index];
2355            let entry_index = index;
2356
2357            index += 1;
2358            if entry.is_root && !entry.expanded && !has_query {
2359                while index < entries.len() {
2360                    if entries[index].is_root {
2361                        break;
2362                    }
2363                    index += 1;
2364                }
2365            }
2366
2367            return Some((entry_index, entry));
2368        })
2369    }
2370
2371    fn filter_matches_to_file(&mut self) {
2372        let current_file = self.current_file.mask();
2373        for (page, page_filter) in std::iter::zip(&self.pages, &mut self.filter_table) {
2374            let mut header_index = 0;
2375            let mut any_found_since_last_header = true;
2376
2377            for (index, item) in page.items.iter().enumerate() {
2378                match item {
2379                    SettingsPageItem::SectionHeader(_) => {
2380                        if !any_found_since_last_header {
2381                            page_filter[header_index] = false;
2382                        }
2383                        header_index = index;
2384                        any_found_since_last_header = false;
2385                    }
2386                    SettingsPageItem::SettingItem(SettingItem { files, .. })
2387                    | SettingsPageItem::SubPageLink(SubPageLink { files, .. })
2388                    | SettingsPageItem::DynamicItem(DynamicItem {
2389                        discriminant: SettingItem { files, .. },
2390                        ..
2391                    }) => {
2392                        if !files.contains(current_file) {
2393                            page_filter[index] = false;
2394                        } else {
2395                            any_found_since_last_header = true;
2396                        }
2397                    }
2398                    SettingsPageItem::ActionLink(ActionLink { files, .. }) => {
2399                        if !files.contains(current_file) {
2400                            page_filter[index] = false;
2401                        } else {
2402                            any_found_since_last_header = true;
2403                        }
2404                    }
2405                }
2406            }
2407            if let Some(last_header) = page_filter.get_mut(header_index)
2408                && !any_found_since_last_header
2409            {
2410                *last_header = false;
2411            }
2412        }
2413    }
2414
2415    fn filter_by_json_path(&self, query: &str) -> Vec<usize> {
2416        let Some(path) = query.strip_prefix('#') else {
2417            return vec![];
2418        };
2419        let Some(search_index) = self.search_index.as_ref() else {
2420            return vec![];
2421        };
2422        let mut indices = vec![];
2423        for (index, SearchKeyLUTEntry { json_path, .. }) in search_index.key_lut.iter().enumerate()
2424        {
2425            let Some(json_path) = json_path else {
2426                continue;
2427            };
2428
2429            if let Some(post) = json_path.strip_prefix(path)
2430                && (post.is_empty() || post.starts_with('.'))
2431            {
2432                indices.push(index);
2433            }
2434        }
2435        indices
2436    }
2437
2438    fn apply_match_indices(&mut self, match_indices: impl Iterator<Item = usize>, query: &str) {
2439        let Some(search_index) = self.search_index.as_ref() else {
2440            return;
2441        };
2442
2443        for page in &mut self.filter_table {
2444            page.fill(false);
2445        }
2446
2447        for match_index in match_indices {
2448            let SearchKeyLUTEntry {
2449                page_index,
2450                header_index,
2451                item_index,
2452                ..
2453            } = search_index.key_lut[match_index];
2454            let page = &mut self.filter_table[page_index];
2455            page[header_index] = true;
2456            page[item_index] = true;
2457        }
2458        self.has_query = true;
2459        self.filter_matches_to_file();
2460        let query_lower = query.to_lowercase();
2461        let query_words: Vec<&str> = query_lower.split_whitespace().collect();
2462        self.open_best_matching_nav_page(&query_words);
2463        self.reset_list_state();
2464        self.scroll_content_to_best_match(&query_words);
2465    }
2466
2467    fn update_matches(&mut self, cx: &mut Context<SettingsWindow>) {
2468        self.search_task.take();
2469        let query = self.search_bar.read(cx).text(cx);
2470        if query.is_empty() || self.search_index.is_none() {
2471            for page in &mut self.filter_table {
2472                page.fill(true);
2473            }
2474            self.has_query = false;
2475            self.filter_matches_to_file();
2476            self.reset_list_state();
2477            cx.notify();
2478            return;
2479        }
2480
2481        let is_json_link_query = query.starts_with("#");
2482        if is_json_link_query {
2483            let indices = self.filter_by_json_path(&query);
2484            if !indices.is_empty() {
2485                self.apply_match_indices(indices.into_iter(), &query);
2486                cx.notify();
2487                return;
2488            }
2489        }
2490
2491        let search_index = self.search_index.as_ref().unwrap().clone();
2492
2493        self.search_task = Some(cx.spawn(async move |this, cx| {
2494            let exact_match_task = cx.background_spawn({
2495                let search_index = search_index.clone();
2496                let query = query.clone();
2497                async move {
2498                    let query_lower = query.to_lowercase();
2499                    let query_words: Vec<&str> = query_lower.split_whitespace().collect();
2500                    if query_words.is_empty() {
2501                        return Vec::new();
2502                    }
2503                    search_index
2504                        .documents
2505                        .iter()
2506                        .filter(|doc| {
2507                            query_words.iter().all(|query_word| {
2508                                doc.words
2509                                    .iter()
2510                                    .any(|doc_word| doc_word.starts_with(query_word))
2511                            })
2512                        })
2513                        .map(|doc| doc.id)
2514                        .collect::<Vec<usize>>()
2515                }
2516            });
2517            let cancel_flag = std::sync::atomic::AtomicBool::new(false);
2518            let fuzzy_search_task = fuzzy::match_strings(
2519                search_index.fuzzy_match_candidates.as_slice(),
2520                &query,
2521                false,
2522                true,
2523                search_index.fuzzy_match_candidates.len(),
2524                &cancel_flag,
2525                cx.background_executor().clone(),
2526            );
2527
2528            let fuzzy_matches = fuzzy_search_task.await;
2529            let exact_matches = exact_match_task.await;
2530
2531            this.update(cx, |this, cx| {
2532                let exact_indices = exact_matches.into_iter();
2533                let fuzzy_indices = fuzzy_matches
2534                    .into_iter()
2535                    .take_while(|fuzzy_match| fuzzy_match.score >= 0.5)
2536                    .map(|fuzzy_match| fuzzy_match.candidate_id);
2537                let merged_indices = exact_indices.chain(fuzzy_indices);
2538
2539                this.apply_match_indices(merged_indices, &query);
2540                cx.notify();
2541            })
2542            .ok();
2543
2544            cx.background_executor().timer(Duration::from_secs(1)).await;
2545            telemetry::event!("Settings Searched", query = query)
2546        }));
2547    }
2548
2549    fn build_filter_table(&mut self) {
2550        self.filter_table = self
2551            .pages
2552            .iter()
2553            .map(|page| vec![true; page.items.len()])
2554            .collect::<Vec<_>>();
2555    }
2556
2557    fn build_search_index(&mut self) {
2558        fn split_into_words(parts: &[&str]) -> Vec<String> {
2559            parts
2560                .iter()
2561                .flat_map(|s| {
2562                    s.split(|c: char| !c.is_alphanumeric())
2563                        .filter(|w| !w.is_empty())
2564                        .map(|w| w.to_lowercase())
2565                })
2566                .collect()
2567        }
2568
2569        let mut key_lut: Vec<SearchKeyLUTEntry> = vec![];
2570        let mut documents: Vec<SearchDocument> = Vec::default();
2571        let mut fuzzy_match_candidates = Vec::default();
2572
2573        fn push_candidates(
2574            fuzzy_match_candidates: &mut Vec<StringMatchCandidate>,
2575            key_index: usize,
2576            input: &str,
2577        ) {
2578            for word in input.split_ascii_whitespace() {
2579                fuzzy_match_candidates.push(StringMatchCandidate::new(key_index, word));
2580            }
2581        }
2582
2583        // PERF: We are currently searching all items even in project files
2584        // where many settings are filtered out, using the logic in filter_matches_to_file
2585        // we could only search relevant items based on the current file
2586        for (page_index, page) in self.pages.iter().enumerate() {
2587            let mut header_index = 0;
2588            let mut header_str = "";
2589            for (item_index, item) in page.items.iter().enumerate() {
2590                let key_index = key_lut.len();
2591                let mut json_path = None;
2592                match item {
2593                    SettingsPageItem::DynamicItem(DynamicItem {
2594                        discriminant: item, ..
2595                    })
2596                    | SettingsPageItem::SettingItem(item) => {
2597                        json_path = item
2598                            .field
2599                            .json_path()
2600                            .map(|path| path.trim_end_matches('$'));
2601                        documents.push(SearchDocument {
2602                            id: key_index,
2603                            words: split_into_words(&[
2604                                page.title,
2605                                header_str,
2606                                item.title,
2607                                item.description,
2608                            ]),
2609                        });
2610                        push_candidates(&mut fuzzy_match_candidates, key_index, item.title);
2611                        push_candidates(&mut fuzzy_match_candidates, key_index, item.description);
2612                    }
2613                    SettingsPageItem::SectionHeader(header) => {
2614                        documents.push(SearchDocument {
2615                            id: key_index,
2616                            words: split_into_words(&[header]),
2617                        });
2618                        push_candidates(&mut fuzzy_match_candidates, key_index, header);
2619                        header_index = item_index;
2620                        header_str = *header;
2621                    }
2622                    SettingsPageItem::SubPageLink(sub_page_link) => {
2623                        json_path = sub_page_link.json_path;
2624                        let mut parts = vec![page.title, header_str, sub_page_link.title.as_ref()];
2625                        parts.extend(sub_page_link.search_aliases);
2626                        documents.push(SearchDocument {
2627                            id: key_index,
2628                            words: split_into_words(&parts),
2629                        });
2630                        push_candidates(
2631                            &mut fuzzy_match_candidates,
2632                            key_index,
2633                            sub_page_link.title.as_ref(),
2634                        );
2635                        for alias in sub_page_link.search_aliases {
2636                            push_candidates(&mut fuzzy_match_candidates, key_index, alias);
2637                        }
2638                    }
2639                    SettingsPageItem::ActionLink(action_link) => {
2640                        documents.push(SearchDocument {
2641                            id: key_index,
2642                            words: split_into_words(&[
2643                                page.title,
2644                                header_str,
2645                                action_link.title.as_ref(),
2646                            ]),
2647                        });
2648                        push_candidates(
2649                            &mut fuzzy_match_candidates,
2650                            key_index,
2651                            action_link.title.as_ref(),
2652                        );
2653                    }
2654                }
2655                push_candidates(&mut fuzzy_match_candidates, key_index, page.title);
2656                push_candidates(&mut fuzzy_match_candidates, key_index, header_str);
2657
2658                key_lut.push(SearchKeyLUTEntry {
2659                    page_index,
2660                    header_index,
2661                    item_index,
2662                    json_path,
2663                });
2664            }
2665        }
2666        self.search_index = Some(Arc::new(SearchIndex {
2667            documents,
2668            key_lut,
2669            fuzzy_match_candidates,
2670        }));
2671    }
2672
2673    fn build_content_handles(&mut self, window: &mut Window, cx: &mut Context<SettingsWindow>) {
2674        self.content_handles = self
2675            .pages
2676            .iter()
2677            .map(|page| {
2678                std::iter::repeat_with(|| NonFocusableHandle::new(0, false, window, cx))
2679                    .take(page.items.len())
2680                    .collect()
2681            })
2682            .collect::<Vec<_>>();
2683    }
2684
2685    fn reset_list_state(&mut self) {
2686        let mut visible_items_count = self.visible_page_items().count();
2687
2688        if visible_items_count > 0 {
2689            // show page title if page is non empty
2690            visible_items_count += 1;
2691        }
2692
2693        self.list_state.reset(visible_items_count);
2694    }
2695
2696    fn build_ui(&mut self, window: &mut Window, cx: &mut Context<SettingsWindow>) {
2697        if self.pages.is_empty() {
2698            self.pages = page_data::settings_data(cx);
2699            self.build_navbar(cx);
2700            self.setup_navbar_focus_subscriptions(window, cx);
2701            self.build_content_handles(window, cx);
2702        }
2703        self.sub_page_stack.clear();
2704        // PERF: doesn't have to be rebuilt, can just be filled with true. pages is constant once it is built
2705        self.build_filter_table();
2706        self.reset_list_state();
2707        self.update_matches(cx);
2708
2709        cx.notify();
2710    }
2711
2712    fn rebuild_pages(&mut self, window: &mut Window, cx: &mut Context<SettingsWindow>) {
2713        self.pages.clear();
2714        self.navbar_entries.clear();
2715        self.navbar_focus_subscriptions.clear();
2716        self.content_handles.clear();
2717        self.build_ui(window, cx);
2718        self.build_search_index();
2719    }
2720
2721    #[track_caller]
2722    fn fetch_files(&mut self, window: &mut Window, cx: &mut Context<SettingsWindow>) {
2723        self.worktree_root_dirs.clear();
2724        let prev_files = self.files.clone();
2725        let settings_store = cx.global::<SettingsStore>();
2726        let mut ui_files = vec![];
2727        let mut all_files = settings_store.get_all_files();
2728        if !all_files.contains(&settings::SettingsFile::User) {
2729            all_files.push(settings::SettingsFile::User);
2730        }
2731        for file in all_files {
2732            let Some(settings_ui_file) = SettingsUiFile::from_settings(file) else {
2733                continue;
2734            };
2735            if settings_ui_file.is_server() {
2736                continue;
2737            }
2738
2739            if let Some(worktree_id) = settings_ui_file.worktree_id() {
2740                let directory_name = all_projects(self.original_window.as_ref(), cx)
2741                    .find_map(|project| project.read(cx).worktree_for_id(worktree_id, cx))
2742                    .map(|worktree| worktree.read(cx).root_name());
2743
2744                let Some(directory_name) = directory_name else {
2745                    log::error!(
2746                        "No directory name found for settings file at worktree ID: {}",
2747                        worktree_id
2748                    );
2749                    continue;
2750                };
2751
2752                self.worktree_root_dirs
2753                    .insert(worktree_id, directory_name.as_unix_str().to_string());
2754            }
2755
2756            let focus_handle = prev_files
2757                .iter()
2758                .find_map(|(prev_file, handle)| {
2759                    (prev_file == &settings_ui_file).then(|| handle.clone())
2760                })
2761                .unwrap_or_else(|| cx.focus_handle().tab_index(0).tab_stop(true));
2762            ui_files.push((settings_ui_file, focus_handle));
2763        }
2764
2765        ui_files.reverse();
2766
2767        if self.original_window.is_some() {
2768            let mut missing_worktrees = Vec::new();
2769
2770            for worktree in all_projects(self.original_window.as_ref(), cx)
2771                .flat_map(|project| project.read(cx).visible_worktrees(cx))
2772                .filter(|tree| !self.worktree_root_dirs.contains_key(&tree.read(cx).id()))
2773            {
2774                let worktree = worktree.read(cx);
2775                let worktree_id = worktree.id();
2776                let Some(directory_name) = worktree.root_dir().and_then(|file| {
2777                    file.file_name()
2778                        .map(|os_string| os_string.to_string_lossy().to_string())
2779                }) else {
2780                    continue;
2781                };
2782
2783                missing_worktrees.push((worktree_id, directory_name.clone()));
2784                let path = RelPath::empty().to_owned().into_arc();
2785
2786                let settings_ui_file = SettingsUiFile::Project((worktree_id, path));
2787
2788                let focus_handle = prev_files
2789                    .iter()
2790                    .find_map(|(prev_file, handle)| {
2791                        (prev_file == &settings_ui_file).then(|| handle.clone())
2792                    })
2793                    .unwrap_or_else(|| cx.focus_handle().tab_index(0).tab_stop(true));
2794
2795                ui_files.push((settings_ui_file, focus_handle));
2796            }
2797
2798            self.worktree_root_dirs.extend(missing_worktrees);
2799        }
2800
2801        self.files = ui_files;
2802        let current_file_still_exists = self
2803            .files
2804            .iter()
2805            .any(|(file, _)| file == &self.current_file);
2806        if !current_file_still_exists {
2807            self.change_file(0, window, cx);
2808        }
2809    }
2810
2811    fn open_navbar_entry_page(&mut self, navbar_entry: usize) {
2812        // Navigating to another page dismisses the transient "copied share
2813        // link" checkmark shown on a Skills page row.
2814        self.last_copied_skill_directory_path = None;
2815
2816        if !self.is_nav_entry_visible(navbar_entry) {
2817            self.open_first_nav_page();
2818        }
2819
2820        let is_new_page = self.navbar_entries[self.navbar_entry].page_index
2821            != self.navbar_entries[navbar_entry].page_index;
2822
2823        self.navbar_entry = navbar_entry;
2824
2825        // We only need to reset visible items when updating matches
2826        // and selecting a new page
2827        if is_new_page {
2828            self.reset_list_state();
2829        }
2830
2831        self.sub_page_stack.clear();
2832    }
2833
2834    fn open_best_matching_nav_page(&mut self, query_words: &[&str]) {
2835        let mut entries = self.visible_navbar_entries().peekable();
2836        let first_entry = entries.peek().map(|(index, _)| (0, *index));
2837        let best_match = entries
2838            .enumerate()
2839            .filter(|(_, (_, entry))| !entry.is_root)
2840            .map(|(logical_index, (index, entry))| {
2841                let title_lower = entry.title.to_lowercase();
2842                let matching_words = query_words
2843                    .iter()
2844                    .filter(|query_word| {
2845                        title_lower
2846                            .split_whitespace()
2847                            .any(|title_word| title_word.starts_with(*query_word))
2848                    })
2849                    .count();
2850                (logical_index, index, matching_words)
2851            })
2852            .filter(|(_, _, count)| *count > 0)
2853            .max_by_key(|(_, _, count)| *count)
2854            .map(|(logical_index, index, _)| (logical_index, index));
2855        if let Some((logical_index, navbar_entry_index)) = best_match.or(first_entry) {
2856            self.open_navbar_entry_page(navbar_entry_index);
2857            self.navbar_scroll_handle
2858                .scroll_to_item(logical_index + 1, gpui::ScrollStrategy::Top);
2859        }
2860    }
2861
2862    fn scroll_content_to_best_match(&self, query_words: &[&str]) {
2863        let position = self
2864            .visible_page_items()
2865            .enumerate()
2866            .find(|(_, (_, item))| match item {
2867                SettingsPageItem::SectionHeader(title) => {
2868                    let title_lower = title.to_lowercase();
2869                    query_words.iter().all(|query_word| {
2870                        title_lower
2871                            .split_whitespace()
2872                            .any(|title_word| title_word.starts_with(query_word))
2873                    })
2874                }
2875                _ => false,
2876            })
2877            .map(|(position, _)| position);
2878        if let Some(position) = position {
2879            self.list_state.scroll_to(gpui::ListOffset {
2880                item_ix: position + 1,
2881                offset_in_item: px(0.),
2882            });
2883        }
2884    }
2885
2886    fn open_first_nav_page(&mut self) {
2887        let Some(first_navbar_entry_index) = self.visible_navbar_entries().next().map(|e| e.0)
2888        else {
2889            return;
2890        };
2891        self.open_navbar_entry_page(first_navbar_entry_index);
2892    }
2893
2894    fn change_file(&mut self, ix: usize, window: &mut Window, cx: &mut Context<SettingsWindow>) {
2895        if ix >= self.files.len() {
2896            self.current_file = SettingsUiFile::User;
2897            self.build_ui(window, cx);
2898            return;
2899        }
2900
2901        if self.files[ix].0 == self.current_file {
2902            return;
2903        }
2904        self.current_file = self.files[ix].0.clone();
2905
2906        if let SettingsUiFile::Project((_, _)) = &self.current_file {
2907            telemetry::event!("Setting Project Clicked");
2908        }
2909
2910        self.build_ui(window, cx);
2911
2912        if self
2913            .visible_navbar_entries()
2914            .any(|(index, _)| index == self.navbar_entry)
2915        {
2916            self.open_and_scroll_to_navbar_entry(self.navbar_entry, None, true, window, cx);
2917        } else {
2918            self.open_first_nav_page();
2919        };
2920    }
2921
2922    /// Changes the current settings file like [`Self::change_file`], but keeps
2923    /// the currently open sub-page stack when every sub-page in it is
2924    /// available in the new file's scope (e.g. switching a Skills sub-page
2925    /// between the user scope and a project scope).
2926    fn change_file_in_sub_page(
2927        &mut self,
2928        ix: usize,
2929        window: &mut Window,
2930        cx: &mut Context<SettingsWindow>,
2931    ) {
2932        if ix >= self.files.len() || self.files[ix].0 == self.current_file {
2933            return;
2934        }
2935        self.current_file = self.files[ix].0.clone();
2936
2937        if let SettingsUiFile::Project((_, _)) = &self.current_file {
2938            telemetry::event!("Setting Project Clicked");
2939        }
2940
2941        self.last_copied_skill_directory_path = None;
2942
2943        let sub_page_stack = std::mem::take(&mut self.sub_page_stack);
2944        self.build_ui(window, cx);
2945
2946        let file_mask = self.current_file.mask();
2947        if let Some(first_sub_page) = sub_page_stack.first()
2948            && sub_page_stack
2949                .iter()
2950                .all(|sub_page| sub_page.link.files.contains(file_mask))
2951        {
2952            if !self.is_nav_entry_visible(self.navbar_entry) {
2953                // The previously selected page may be filtered out in the new
2954                // scope (e.g. after deep-linking into a sub-page). Re-anchor
2955                // the navbar to the page containing the open sub-page, which
2956                // is visible because its sub-page link supports this scope.
2957                let anchor_entry = self
2958                    .pages
2959                    .iter()
2960                    .position(|page| {
2961                        page.items.iter().any(|item| {
2962                            matches!(item, SettingsPageItem::SubPageLink(link) if link == &first_sub_page.link)
2963                        })
2964                    })
2965                    .and_then(|page_index| {
2966                        self.navbar_entries
2967                            .iter()
2968                            .position(|entry| entry.is_root && entry.page_index == page_index)
2969                    });
2970                if let Some(anchor_entry) = anchor_entry
2971                    && self.is_nav_entry_visible(anchor_entry)
2972                {
2973                    self.open_navbar_entry_page(anchor_entry);
2974                }
2975            }
2976            if self.is_nav_entry_visible(self.navbar_entry) {
2977                self.sub_page_stack = sub_page_stack;
2978                cx.notify();
2979                return;
2980            }
2981        }
2982
2983        if self.is_nav_entry_visible(self.navbar_entry) {
2984            self.open_and_scroll_to_navbar_entry(self.navbar_entry, None, true, window, cx);
2985        } else {
2986            self.open_first_nav_page();
2987        }
2988    }
2989
2990    fn render_files_header(
2991        &self,
2992        window: &mut Window,
2993        cx: &mut Context<SettingsWindow>,
2994    ) -> impl IntoElement {
2995        static OVERFLOW_LIMIT: usize = 1;
2996
2997        let file_button =
2998            |ix, file: &SettingsUiFile, focus_handle, cx: &mut Context<SettingsWindow>| {
2999                Button::new(
3000                    ix,
3001                    self.display_name(&file)
3002                        .expect("Files should always have a name"),
3003                )
3004                .toggle_state(file == &self.current_file)
3005                .selected_style(ButtonStyle::Tinted(ui::TintColor::Accent))
3006                .track_focus(focus_handle)
3007                .on_click(cx.listener({
3008                    let focus_handle = focus_handle.clone();
3009                    move |this, _: &gpui::ClickEvent, window, cx| {
3010                        this.change_file(ix, window, cx);
3011                        focus_handle.focus(window, cx);
3012                    }
3013                }))
3014            };
3015
3016        let this = cx.entity();
3017
3018        let selected_file_ix = self
3019            .files
3020            .iter()
3021            .enumerate()
3022            .skip(OVERFLOW_LIMIT)
3023            .find_map(|(ix, (file, _))| {
3024                if file == &self.current_file {
3025                    Some(ix)
3026                } else {
3027                    None
3028                }
3029            })
3030            .unwrap_or(OVERFLOW_LIMIT);
3031        let edit_in_json_id = SharedString::new(format!("edit-in-json-{}", selected_file_ix));
3032
3033        h_flex()
3034            .id("settings-ui-files-header")
3035            .role(Role::Group)
3036            .aria_label("Settings File")
3037            .w_full()
3038            .gap_1()
3039            .justify_between()
3040            .track_focus(&self.files_focus_handle)
3041            .tab_group()
3042            .tab_index(HEADER_GROUP_TAB_INDEX)
3043            .child(
3044                h_flex()
3045                    .gap_1()
3046                    .children(
3047                        self.files.iter().enumerate().take(OVERFLOW_LIMIT).map(
3048                            |(ix, (file, focus_handle))| file_button(ix, file, focus_handle, cx),
3049                        ),
3050                    )
3051                    .when(self.files.len() > OVERFLOW_LIMIT, |div| {
3052                        let (file, focus_handle) = &self.files[selected_file_ix];
3053
3054                        div.child(file_button(selected_file_ix, file, focus_handle, cx))
3055                            .when(self.files.len() > OVERFLOW_LIMIT + 1, |div| {
3056                                div.child(
3057                                    DropdownMenu::new(
3058                                        "more-files",
3059                                        format!("+{}", self.files.len() - (OVERFLOW_LIMIT + 1)),
3060                                        ContextMenu::build(window, cx, move |mut menu, _, _| {
3061                                            for (mut ix, (file, focus_handle)) in self
3062                                                .files
3063                                                .iter()
3064                                                .enumerate()
3065                                                .skip(OVERFLOW_LIMIT + 1)
3066                                            {
3067                                                let (display_name, focus_handle) =
3068                                                    if selected_file_ix == ix {
3069                                                        ix = OVERFLOW_LIMIT;
3070                                                        (
3071                                                            self.display_name(&self.files[ix].0),
3072                                                            self.files[ix].1.clone(),
3073                                                        )
3074                                                    } else {
3075                                                        (
3076                                                            self.display_name(&file),
3077                                                            focus_handle.clone(),
3078                                                        )
3079                                                    };
3080
3081                                                menu = menu.entry(
3082                                                    display_name
3083                                                        .expect("Files should always have a name"),
3084                                                    None,
3085                                                    {
3086                                                        let this = this.clone();
3087                                                        move |window, cx| {
3088                                                            this.update(cx, |this, cx| {
3089                                                                this.change_file(ix, window, cx);
3090                                                            });
3091                                                            focus_handle.focus(window, cx);
3092                                                        }
3093                                                    },
3094                                                );
3095                                            }
3096
3097                                            menu
3098                                        }),
3099                                    )
3100                                    .style(DropdownStyle::Subtle)
3101                                    .trigger_tooltip(Tooltip::text("View Other Projects"))
3102                                    .trigger_icon(IconName::ChevronDown)
3103                                    .attach(gpui::Anchor::BottomLeft)
3104                                    .offset(gpui::Point {
3105                                        x: px(0.0),
3106                                        y: px(2.0),
3107                                    })
3108                                    .tab_index(0),
3109                                )
3110                            })
3111                    }),
3112            )
3113            .child(
3114                Button::new(edit_in_json_id, "Edit in settings.json")
3115                    .tab_index(0_isize)
3116                    .style(ButtonStyle::OutlinedGhost)
3117                    .tooltip(Tooltip::for_action_title_in(
3118                        "Edit in settings.json",
3119                        &OpenCurrentFile,
3120                        &self.focus_handle,
3121                    ))
3122                    .on_click(cx.listener(|this, _, window, cx| {
3123                        this.open_current_settings_file(window, cx);
3124                    })),
3125            )
3126    }
3127
3128    pub(crate) fn display_name(&self, file: &SettingsUiFile) -> Option<String> {
3129        match file {
3130            SettingsUiFile::User => Some("User".to_string()),
3131            SettingsUiFile::Project((worktree_id, path)) => self
3132                .worktree_root_dirs
3133                .get(&worktree_id)
3134                .map(|directory_name| {
3135                    let path_style = PathStyle::local();
3136                    if path.is_empty() {
3137                        directory_name.clone()
3138                    } else {
3139                        format!(
3140                            "{}{}{}",
3141                            directory_name,
3142                            path_style.primary_separator(),
3143                            path.display(path_style)
3144                        )
3145                    }
3146                }),
3147            SettingsUiFile::Server(file) => Some(file.to_string()),
3148        }
3149    }
3150
3151    // TODO:
3152    //  Reconsider this after preview launch
3153    // fn file_location_str(&self) -> String {
3154    //     match &self.current_file {
3155    //         SettingsUiFile::User => "settings.json".to_string(),
3156    //         SettingsUiFile::Project((worktree_id, path)) => self
3157    //             .worktree_root_dirs
3158    //             .get(&worktree_id)
3159    //             .map(|directory_name| {
3160    //                 let path_style = PathStyle::local();
3161    //                 let file_path = path.join(paths::local_settings_file_relative_path());
3162    //                 format!(
3163    //                     "{}{}{}",
3164    //                     directory_name,
3165    //                     path_style.separator(),
3166    //                     file_path.display(path_style)
3167    //                 )
3168    //             })
3169    //             .expect("Current file should always be present in root dir map"),
3170    //         SettingsUiFile::Server(file) => file.to_string(),
3171    //     }
3172    // }
3173
3174    fn render_search(&self, window: &mut Window, cx: &mut App) -> impl IntoElement {
3175        let (a11y_value, a11y_text_runs) =
3176            text_field_a11y_state("settings-ui-search", &self.search_bar, window, cx);
3177
3178        h_flex()
3179            .id("settings-ui-search")
3180            .role(Role::SearchInput)
3181            .aria_label("Search Settings")
3182            .aria_value(a11y_value)
3183            .track_focus(&self.search_bar.focus_handle(cx))
3184            .a11y_synthetic_children(a11y_text_runs)
3185            .py_1()
3186            .px_1p5()
3187            .mb_3()
3188            .gap_1p5()
3189            .rounded_sm()
3190            .bg(cx.theme().colors().editor_background)
3191            .border_1()
3192            .border_color(cx.theme().colors().border)
3193            .child(Icon::new(IconName::MagnifyingGlass).color(Color::Muted))
3194            .child(self.search_bar.clone())
3195    }
3196
3197    fn render_nav(
3198        &self,
3199        window: &mut Window,
3200        cx: &mut Context<SettingsWindow>,
3201    ) -> impl IntoElement {
3202        let visible_count = self.visible_navbar_entries().count();
3203
3204        let focus_keybind_label = if self
3205            .navbar_focus_handle
3206            .read(cx)
3207            .handle
3208            .contains_focused(window, cx)
3209            || self
3210                .visible_navbar_entries()
3211                .any(|(_, entry)| entry.focus_handle.is_focused(window))
3212        {
3213            "Focus Content"
3214        } else {
3215            "Focus Navbar"
3216        };
3217
3218        let mut key_context = KeyContext::new_with_defaults();
3219        key_context.add("NavigationMenu");
3220        key_context.add("menu");
3221        if self.search_bar.focus_handle(cx).is_focused(window) {
3222            key_context.add("search");
3223        }
3224
3225        v_flex()
3226            .key_context(key_context)
3227            .on_action(cx.listener(|this, _: &CollapseNavEntry, window, cx| {
3228                let Some(focused_entry) = this.focused_nav_entry(window, cx) else {
3229                    return;
3230                };
3231                let focused_entry_parent = this.root_entry_containing(focused_entry);
3232                if this.navbar_entries[focused_entry_parent].expanded {
3233                    this.toggle_navbar_entry(focused_entry_parent);
3234                    window.focus(&this.navbar_entries[focused_entry_parent].focus_handle, cx);
3235                }
3236                cx.notify();
3237            }))
3238            .on_action(cx.listener(|this, _: &ExpandNavEntry, window, cx| {
3239                let Some(focused_entry) = this.focused_nav_entry(window, cx) else {
3240                    return;
3241                };
3242                if !this.navbar_entries[focused_entry].is_root {
3243                    return;
3244                }
3245                if !this.navbar_entries[focused_entry].expanded {
3246                    this.toggle_navbar_entry(focused_entry);
3247                }
3248                cx.notify();
3249            }))
3250            .on_action(
3251                cx.listener(|this, _: &FocusPreviousRootNavEntry, window, cx| {
3252                    let entry_index = this
3253                        .focused_nav_entry(window, cx)
3254                        .unwrap_or(this.navbar_entry);
3255                    let mut root_index = None;
3256                    for (index, entry) in this.visible_navbar_entries() {
3257                        if index >= entry_index {
3258                            break;
3259                        }
3260                        if entry.is_root {
3261                            root_index = Some(index);
3262                        }
3263                    }
3264                    let Some(previous_root_index) = root_index else {
3265                        return;
3266                    };
3267                    this.focus_and_scroll_to_nav_entry(previous_root_index, window, cx);
3268                }),
3269            )
3270            .on_action(cx.listener(|this, _: &FocusNextRootNavEntry, window, cx| {
3271                let entry_index = this
3272                    .focused_nav_entry(window, cx)
3273                    .unwrap_or(this.navbar_entry);
3274                let mut root_index = None;
3275                for (index, entry) in this.visible_navbar_entries() {
3276                    if index <= entry_index {
3277                        continue;
3278                    }
3279                    if entry.is_root {
3280                        root_index = Some(index);
3281                        break;
3282                    }
3283                }
3284                let Some(next_root_index) = root_index else {
3285                    return;
3286                };
3287                this.focus_and_scroll_to_nav_entry(next_root_index, window, cx);
3288            }))
3289            .on_action(cx.listener(|this, _: &FocusFirstNavEntry, window, cx| {
3290                if let Some((first_entry_index, _)) = this.visible_navbar_entries().next() {
3291                    this.focus_and_scroll_to_nav_entry(first_entry_index, window, cx);
3292                }
3293            }))
3294            .on_action(cx.listener(|this, _: &FocusLastNavEntry, window, cx| {
3295                if let Some((last_entry_index, _)) = this.visible_navbar_entries().last() {
3296                    this.focus_and_scroll_to_nav_entry(last_entry_index, window, cx);
3297                }
3298            }))
3299            .on_action(cx.listener(|this, _: &FocusNextNavEntry, window, cx| {
3300                let entry_index = this
3301                    .focused_nav_entry(window, cx)
3302                    .unwrap_or(this.navbar_entry);
3303                let mut next_index = None;
3304                for (index, _) in this.visible_navbar_entries() {
3305                    if index > entry_index {
3306                        next_index = Some(index);
3307                        break;
3308                    }
3309                }
3310                let Some(next_entry_index) = next_index else {
3311                    return;
3312                };
3313                this.open_and_scroll_to_navbar_entry(
3314                    next_entry_index,
3315                    Some(gpui::ScrollStrategy::Bottom),
3316                    false,
3317                    window,
3318                    cx,
3319                );
3320            }))
3321            .on_action(cx.listener(|this, _: &FocusPreviousNavEntry, window, cx| {
3322                let entry_index = this
3323                    .focused_nav_entry(window, cx)
3324                    .unwrap_or(this.navbar_entry);
3325                let mut prev_index = None;
3326                for (index, _) in this.visible_navbar_entries() {
3327                    if index >= entry_index {
3328                        break;
3329                    }
3330                    prev_index = Some(index);
3331                }
3332                let Some(prev_entry_index) = prev_index else {
3333                    return;
3334                };
3335                this.open_and_scroll_to_navbar_entry(
3336                    prev_entry_index,
3337                    Some(gpui::ScrollStrategy::Top),
3338                    false,
3339                    window,
3340                    cx,
3341                );
3342            }))
3343            .w(SIDEBAR_WIDTH)
3344            .h_full()
3345            .p_2p5()
3346            .when(cfg!(target_os = "macos"), |this| this.pt_10())
3347            .flex_none()
3348            .border_r_1()
3349            .border_color(cx.theme().colors().border)
3350            .bg(cx.theme().colors().panel_background)
3351            .child(self.render_search(window, cx))
3352            .child(
3353                v_flex()
3354                    .id("settings-ui-nav")
3355                    .role(Role::Tree)
3356                    .aria_label("Settings Navigation")
3357                    .flex_1()
3358                    .overflow_hidden()
3359                    .track_focus(&self.navbar_focus_handle.focus_handle(cx))
3360                    .tab_group()
3361                    .tab_index(NAVBAR_GROUP_TAB_INDEX)
3362                    .child(
3363                        uniform_list(
3364                            "settings-ui-nav-bar",
3365                            visible_count + 1,
3366                            cx.processor(move |this, range: Range<usize>, _, cx| {
3367                                this.visible_navbar_entries()
3368                                    .skip(range.start.saturating_sub(1))
3369                                    .take(range.len())
3370                                    .map(|(entry_index, entry)| {
3371                                        TreeViewItem::new(
3372                                            ("settings-ui-navbar-entry", entry_index),
3373                                            entry.title,
3374                                        )
3375                                        .track_focus(&entry.focus_handle)
3376                                        .root_item(entry.is_root)
3377                                        .toggle_state(this.is_navbar_entry_selected(entry_index))
3378                                        .when(entry.is_root, |item| {
3379                                            item.expanded(entry.expanded || this.has_query)
3380                                                .on_toggle(cx.listener(
3381                                                    move |this, _, window, cx| {
3382                                                        this.toggle_and_focus_navbar_entry(
3383                                                            entry_index,
3384                                                            window,
3385                                                            cx,
3386                                                        );
3387                                                    },
3388                                                ))
3389                                        })
3390                                        .on_click({
3391                                            let category = this.pages[entry.page_index].title;
3392                                            let subcategory =
3393                                                (!entry.is_root).then_some(entry.title);
3394
3395                                            cx.listener(move |this, event: &gpui::ClickEvent, window, cx| {
3396                                                if this.toggle_navbar_entry_on_double_click(
3397                                                        entry_index,
3398                                                        event,
3399                                                        window,
3400                                                        cx,
3401                                                    )
3402                                                {
3403                                                    return;
3404                                                }
3405
3406                                                telemetry::event!(
3407                                                    "Settings Navigation Clicked",
3408                                                    category = category,
3409                                                    subcategory = subcategory
3410                                                );
3411
3412                                                this.open_and_scroll_to_navbar_entry(
3413                                                    entry_index,
3414                                                    None,
3415                                                    true,
3416                                                    window,
3417                                                    cx,
3418                                                );
3419                                            })
3420                                        })
3421                                    })
3422                                    .collect()
3423                            }),
3424                        )
3425                        .size_full()
3426                        .track_scroll(&self.navbar_scroll_handle),
3427                    )
3428                    .vertical_scrollbar_for(&self.navbar_scroll_handle, window, cx),
3429            )
3430            .child(
3431                h_flex()
3432                    .w_full()
3433                    .h_8()
3434                    .p_2()
3435                    .pb_0p5()
3436                    .flex_shrink_0()
3437                    .border_t_1()
3438                    .border_color(cx.theme().colors().border_variant)
3439                    .child(
3440                        KeybindingHint::new(
3441                            KeyBinding::for_action_in(
3442                                &ToggleFocusNav,
3443                                &self.navbar_focus_handle.focus_handle(cx),
3444                                cx,
3445                            ),
3446                            cx.theme().colors().surface_background.opacity(0.5),
3447                        )
3448                        .suffix(focus_keybind_label),
3449                    ),
3450            )
3451    }
3452
3453    fn open_and_scroll_to_navbar_entry(
3454        &mut self,
3455        navbar_entry_index: usize,
3456        scroll_strategy: Option<gpui::ScrollStrategy>,
3457        focus_content: bool,
3458        window: &mut Window,
3459        cx: &mut Context<Self>,
3460    ) {
3461        self.open_navbar_entry_page(navbar_entry_index);
3462        cx.notify();
3463
3464        let mut handle_to_focus = None;
3465
3466        if self.navbar_entries[navbar_entry_index].is_root
3467            || !self.is_nav_entry_visible(navbar_entry_index)
3468        {
3469            if let Some(scroll_handle) = self.current_sub_page_scroll_handle() {
3470                scroll_handle.set_offset(point(px(0.), px(0.)));
3471            }
3472
3473            if focus_content {
3474                let Some(first_item_index) =
3475                    self.visible_page_items().next().map(|(index, _)| index)
3476                else {
3477                    return;
3478                };
3479                handle_to_focus = Some(self.focus_handle_for_content_element(first_item_index, cx));
3480            } else if !self.is_nav_entry_visible(navbar_entry_index) {
3481                let Some(first_visible_nav_entry_index) =
3482                    self.visible_navbar_entries().next().map(|(index, _)| index)
3483                else {
3484                    return;
3485                };
3486                self.focus_and_scroll_to_nav_entry(first_visible_nav_entry_index, window, cx);
3487            } else {
3488                handle_to_focus =
3489                    Some(self.navbar_entries[navbar_entry_index].focus_handle.clone());
3490            }
3491        } else {
3492            let entry_item_index = self.navbar_entries[navbar_entry_index]
3493                .item_index
3494                .expect("Non-root items should have an item index");
3495            self.scroll_to_content_item(entry_item_index, window, cx);
3496            if focus_content {
3497                handle_to_focus = Some(self.focus_handle_for_content_element(entry_item_index, cx));
3498            } else {
3499                handle_to_focus =
3500                    Some(self.navbar_entries[navbar_entry_index].focus_handle.clone());
3501            }
3502        }
3503
3504        if let Some(scroll_strategy) = scroll_strategy
3505            && let Some(logical_entry_index) = self
3506                .visible_navbar_entries()
3507                .into_iter()
3508                .position(|(index, _)| index == navbar_entry_index)
3509        {
3510            self.navbar_scroll_handle
3511                .scroll_to_item(logical_entry_index + 1, scroll_strategy);
3512        }
3513
3514        // Page scroll handle updates the active item index
3515        // in it's next paint call after using scroll_handle.scroll_to_top_of_item
3516        // The call after that updates the offset of the scroll handle. So to
3517        // ensure the scroll handle doesn't lag behind we need to render three frames
3518        // back to back.
3519        cx.on_next_frame(window, move |_, window, cx| {
3520            if let Some(handle) = handle_to_focus.as_ref() {
3521                window.focus(handle, cx);
3522            }
3523
3524            cx.on_next_frame(window, |_, _, cx| {
3525                cx.notify();
3526            });
3527            cx.notify();
3528        });
3529        cx.notify();
3530    }
3531
3532    fn scroll_to_content_item(
3533        &self,
3534        content_item_index: usize,
3535        _window: &mut Window,
3536        cx: &mut Context<Self>,
3537    ) {
3538        let index = self
3539            .visible_page_items()
3540            .position(|(index, _)| index == content_item_index)
3541            .unwrap_or(0);
3542        if index == 0 {
3543            if let Some(scroll_handle) = self.current_sub_page_scroll_handle() {
3544                scroll_handle.set_offset(point(px(0.), px(0.)));
3545            }
3546
3547            self.list_state.scroll_to(gpui::ListOffset {
3548                item_ix: 0,
3549                offset_in_item: px(0.),
3550            });
3551            return;
3552        }
3553        self.list_state.scroll_to(gpui::ListOffset {
3554            item_ix: index + 1,
3555            offset_in_item: px(0.),
3556        });
3557        cx.notify();
3558    }
3559
3560    fn is_nav_entry_visible(&self, nav_entry_index: usize) -> bool {
3561        self.visible_navbar_entries()
3562            .any(|(index, _)| index == nav_entry_index)
3563    }
3564
3565    fn focus_and_scroll_to_first_visible_nav_entry(
3566        &self,
3567        window: &mut Window,
3568        cx: &mut Context<Self>,
3569    ) {
3570        if let Some(nav_entry_index) = self.visible_navbar_entries().next().map(|(index, _)| index)
3571        {
3572            self.focus_and_scroll_to_nav_entry(nav_entry_index, window, cx);
3573        }
3574    }
3575
3576    fn focus_and_scroll_to_nav_entry(
3577        &self,
3578        nav_entry_index: usize,
3579        window: &mut Window,
3580        cx: &mut Context<Self>,
3581    ) {
3582        let Some(position) = self
3583            .visible_navbar_entries()
3584            .position(|(index, _)| index == nav_entry_index)
3585        else {
3586            return;
3587        };
3588        self.navbar_scroll_handle
3589            .scroll_to_item(position, gpui::ScrollStrategy::Top);
3590        window.focus(&self.navbar_entries[nav_entry_index].focus_handle, cx);
3591        cx.notify();
3592    }
3593
3594    fn current_sub_page_scroll_handle(&self) -> Option<&ScrollHandle> {
3595        self.sub_page_stack.last().map(|page| &page.scroll_handle)
3596    }
3597
3598    fn visible_page_items(&self) -> impl Iterator<Item = (usize, &SettingsPageItem)> {
3599        let page_idx = self.current_page_index();
3600
3601        self.current_page()
3602            .items
3603            .iter()
3604            .enumerate()
3605            .filter(move |&(item_index, _)| self.filter_table[page_idx][item_index])
3606    }
3607
3608    fn render_sub_page_breadcrumbs(
3609        &self,
3610        window: &mut Window,
3611        cx: &mut Context<Self>,
3612    ) -> impl IntoElement {
3613        let scope_name: SharedString = self
3614            .display_name(&self.current_file)
3615            .unwrap_or_else(|| self.current_file.setting_type().to_string())
3616            .into();
3617
3618        // Only offer scopes in which every sub-page in the stack is available.
3619        let allowed_mask = self
3620            .sub_page_stack
3621            .iter()
3622            .fold(USER | PROJECT | SERVER, |mask, sub_page| {
3623                mask & sub_page.link.files
3624            });
3625        let allowed_file_indices: Vec<usize> = self
3626            .files
3627            .iter()
3628            .enumerate()
3629            .filter(|(_, (file, _))| allowed_mask.contains(file.mask()))
3630            .map(|(ix, _)| ix)
3631            .collect();
3632
3633        let scope_element = if allowed_file_indices.len() > 1 {
3634            let this = cx.entity();
3635            DropdownMenu::new(
3636                "sub-page-scope-picker",
3637                scope_name,
3638                ContextMenu::build(window, cx, move |mut menu, _, _| {
3639                    menu = menu.header("Scope");
3640
3641                    for ix in allowed_file_indices {
3642                        let (file, focus_handle) = &self.files[ix];
3643                        let display_name = self
3644                            .display_name(file)
3645                            .expect("Files should always have a name");
3646
3647                        menu = menu.toggleable_entry(
3648                            display_name,
3649                            file == &self.current_file,
3650                            IconPosition::End,
3651                            None,
3652                            {
3653                                let this = this.clone();
3654                                let focus_handle = focus_handle.clone();
3655                                move |window, cx| {
3656                                    this.update(cx, |this, cx| {
3657                                        this.change_file_in_sub_page(ix, window, cx);
3658                                    });
3659                                    focus_handle.focus(window, cx);
3660                                }
3661                            },
3662                        );
3663                    }
3664
3665                    menu
3666                }),
3667            )
3668            .style(DropdownStyle::Subtle)
3669            .trigger_tooltip(Tooltip::text("Change Scope"))
3670            .attach(gpui::Anchor::BottomLeft)
3671            .offset(gpui::Point {
3672                x: px(0.0),
3673                y: px(2.0),
3674            })
3675            .tab_index(0)
3676            .into_any_element()
3677        } else {
3678            Label::new(scope_name)
3679                .color(Color::Muted)
3680                .into_any_element()
3681        };
3682
3683        h_flex()
3684            .min_w_0()
3685            .gap_1()
3686            .overflow_x_hidden()
3687            .child(scope_element)
3688            .child(Label::new("/").color(Color::Muted))
3689            .children(
3690                itertools::intersperse(
3691                    std::iter::once(self.current_page().title.into()).chain(
3692                        self.sub_page_stack
3693                            .iter()
3694                            .enumerate()
3695                            .flat_map(|(index, page)| {
3696                                (index == 0)
3697                                    .then(|| page.section_header.clone())
3698                                    .into_iter()
3699                                    .chain(std::iter::once(page.link.title.clone()))
3700                            }),
3701                    ),
3702                    "/".into(),
3703                )
3704                .map(|item| Label::new(item).color(Color::Muted)),
3705            )
3706    }
3707
3708    fn render_no_results(&self, cx: &App) -> impl IntoElement {
3709        let search_query = self.search_bar.read(cx).text(cx);
3710
3711        v_flex()
3712            .size_full()
3713            .items_center()
3714            .justify_center()
3715            .gap_1()
3716            .child(Label::new("No Results"))
3717            .child(
3718                Label::new(format!("No settings match \"{}\"", search_query))
3719                    .size(LabelSize::Small)
3720                    .color(Color::Muted),
3721            )
3722    }
3723
3724    fn render_current_page_items(
3725        &mut self,
3726        _window: &mut Window,
3727        cx: &mut Context<SettingsWindow>,
3728    ) -> impl IntoElement {
3729        let current_page_index = self.current_page_index();
3730        let mut page_content = v_flex()
3731            .id("settings-ui-page")
3732            .role(Role::Group)
3733            .aria_label("Settings Content")
3734            .size_full();
3735
3736        let has_active_search = !self.search_bar.read(cx).is_empty(cx);
3737        let has_no_results = self.visible_page_items().next().is_none() && has_active_search;
3738
3739        if has_no_results {
3740            page_content = page_content.child(self.render_no_results(cx))
3741        } else {
3742            let last_non_header_index = self
3743                .visible_page_items()
3744                .filter_map(|(index, item)| {
3745                    (!matches!(item, SettingsPageItem::SectionHeader(_))).then_some(index)
3746                })
3747                .last();
3748
3749            let root_nav_label = self
3750                .navbar_entries
3751                .iter()
3752                .find(|entry| entry.is_root && entry.page_index == self.current_page_index())
3753                .map(|entry| entry.title);
3754
3755            let list_content = list(
3756                self.list_state.clone(),
3757                cx.processor(move |this, index, window, cx| {
3758                    if index == 0 {
3759                        return div()
3760                            .px_8()
3761                            .when(this.sub_page_stack.is_empty(), |this| {
3762                                this.when_some(root_nav_label, |this, title| {
3763                                    this.child(
3764                                        Label::new(title).size(LabelSize::Large).mt_2().mb_3(),
3765                                    )
3766                                })
3767                            })
3768                            .into_any_element();
3769                    }
3770
3771                    let mut visible_items = this.visible_page_items();
3772                    let Some((actual_item_index, item)) = visible_items.nth(index - 1) else {
3773                        return gpui::Empty.into_any_element();
3774                    };
3775
3776                    let next_is_header = visible_items
3777                        .next()
3778                        .map(|(_, item)| matches!(item, SettingsPageItem::SectionHeader(_)))
3779                        .unwrap_or(false);
3780
3781                    let is_last = Some(actual_item_index) == last_non_header_index;
3782                    let is_last_in_section = next_is_header || is_last;
3783
3784                    let bottom_border = !is_last_in_section;
3785                    let extra_bottom_padding = is_last_in_section;
3786
3787                    let item_focus_handle = this.content_handles[current_page_index]
3788                        [actual_item_index]
3789                        .focus_handle(cx);
3790
3791                    v_flex()
3792                        .id(("settings-page-item", actual_item_index))
3793                        .track_focus(&item_focus_handle)
3794                        .w_full()
3795                        .min_w_0()
3796                        .child(item.render(
3797                            this,
3798                            actual_item_index,
3799                            bottom_border,
3800                            extra_bottom_padding,
3801                            window,
3802                            cx,
3803                        ))
3804                        .into_any_element()
3805                }),
3806            );
3807
3808            page_content = page_content.child(list_content.size_full())
3809        }
3810        page_content
3811    }
3812
3813    fn render_sub_page_items<'a, Items>(
3814        &self,
3815        items: Items,
3816        scroll_handle: &ScrollHandle,
3817        window: &mut Window,
3818        cx: &mut Context<SettingsWindow>,
3819    ) -> impl IntoElement
3820    where
3821        Items: Iterator<Item = (usize, &'a SettingsPageItem)>,
3822    {
3823        let page_content = v_flex()
3824            .id("settings-ui-page")
3825            .size_full()
3826            .overflow_y_scroll()
3827            .track_scroll(scroll_handle);
3828        self.render_sub_page_items_in(page_content, items, false, window, cx)
3829    }
3830
3831    fn render_sub_page_items_section<'a, Items>(
3832        &self,
3833        items: Items,
3834        is_inline_section: bool,
3835        window: &mut Window,
3836        cx: &mut Context<SettingsWindow>,
3837    ) -> impl IntoElement
3838    where
3839        Items: Iterator<Item = (usize, &'a SettingsPageItem)>,
3840    {
3841        let page_content = v_flex().id("settings-ui-sub-page-section").size_full();
3842        self.render_sub_page_items_in(page_content, items, is_inline_section, window, cx)
3843    }
3844
3845    fn render_sub_page_items_in<'a, Items>(
3846        &self,
3847        page_content: Stateful<Div>,
3848        items: Items,
3849        is_inline_section: bool,
3850        window: &mut Window,
3851        cx: &mut Context<SettingsWindow>,
3852    ) -> impl IntoElement
3853    where
3854        Items: Iterator<Item = (usize, &'a SettingsPageItem)>,
3855    {
3856        let items: Vec<_> = items.collect();
3857        let items_len = items.len();
3858
3859        let has_active_search = !self.search_bar.read(cx).is_empty(cx);
3860        let has_no_results = items_len == 0 && has_active_search;
3861
3862        if has_no_results {
3863            page_content.child(self.render_no_results(cx))
3864        } else {
3865            let last_non_header_index = items
3866                .iter()
3867                .enumerate()
3868                .rev()
3869                .find(|(_, (_, item))| !matches!(item, SettingsPageItem::SectionHeader(_)))
3870                .map(|(index, _)| index);
3871
3872            let root_nav_label = self
3873                .navbar_entries
3874                .iter()
3875                .find(|entry| entry.is_root && entry.page_index == self.current_page_index())
3876                .map(|entry| entry.title);
3877
3878            page_content
3879                .when(self.sub_page_stack.is_empty(), |this| {
3880                    this.when_some(root_nav_label, |this, title| {
3881                        this.child(Label::new(title).size(LabelSize::Large).mt_2().mb_3())
3882                    })
3883                })
3884                .children(items.clone().into_iter().enumerate().map(
3885                    |(index, (actual_item_index, item))| {
3886                        let is_last_item = Some(index) == last_non_header_index;
3887                        let next_is_header = items.get(index + 1).is_some_and(|(_, next_item)| {
3888                            matches!(next_item, SettingsPageItem::SectionHeader(_))
3889                        });
3890                        let bottom_border = !is_inline_section && !next_is_header && !is_last_item;
3891
3892                        let extra_bottom_padding =
3893                            !is_inline_section && (next_is_header || is_last_item);
3894
3895                        v_flex()
3896                            .w_full()
3897                            .min_w_0()
3898                            .id(("settings-page-item", actual_item_index))
3899                            .child(item.render(
3900                                self,
3901                                actual_item_index,
3902                                bottom_border,
3903                                extra_bottom_padding,
3904                                window,
3905                                cx,
3906                            ))
3907                    },
3908                ))
3909        }
3910    }
3911
3912    fn render_page(
3913        &mut self,
3914        window: &mut Window,
3915        cx: &mut Context<SettingsWindow>,
3916    ) -> impl IntoElement {
3917        let page_header;
3918        let page_content;
3919
3920        if let Some(current_sub_page) = self.sub_page_stack.last() {
3921            let is_skills_page =
3922                current_sub_page.link.json_path == Some(AGENT_SKILLS_SETTINGS_PATH);
3923            let is_llm_providers_page = current_sub_page.link.json_path == Some("llm_providers")
3924                && current_sub_page.link.title.as_ref() == "LLM Providers";
3925            let is_external_agents_page = current_sub_page.link.json_path == Some("agent_servers");
3926            let is_mcp_servers_page = current_sub_page.link.json_path == Some("context_servers");
3927
3928            page_header = h_flex()
3929                .w_full()
3930                .min_w_0()
3931                .justify_between()
3932                .child(
3933                    h_flex()
3934                        .min_w_0()
3935                        .ml_neg_1p5()
3936                        .gap_1()
3937                        .child(
3938                            IconButton::new("back-btn", IconName::ArrowLeft)
3939                                .icon_size(IconSize::Small)
3940                                .shape(IconButtonShape::Square)
3941                                .on_click(cx.listener(|this, _, window, cx| {
3942                                    this.pop_sub_page(window, cx);
3943                                })),
3944                        )
3945                        .child(self.render_sub_page_breadcrumbs(window, cx)),
3946                )
3947                .child(
3948                    div()
3949                        .flex_shrink_0()
3950                        .when(current_sub_page.link.in_json, |this| {
3951                            this.child(
3952                                Button::new("open-in-settings-file", "Edit in settings.json")
3953                                    .tab_index(0_isize)
3954                                    .style(ButtonStyle::OutlinedGhost)
3955                                    .tooltip(Tooltip::for_action_title_in(
3956                                        "Edit in settings.json",
3957                                        &OpenCurrentFile,
3958                                        &self.focus_handle,
3959                                    ))
3960                                    .on_click(cx.listener(|this, _, window, cx| {
3961                                        this.open_current_settings_file(window, cx);
3962                                    })),
3963                            )
3964                        })
3965                        .when(is_llm_providers_page, |this| {
3966                            this.child(pages::render_add_llm_provider_popover(self, window, cx))
3967                        })
3968                        .when(is_skills_page, |this| {
3969                            this.child(
3970                                Button::new("open-skill-creator", "Create Skill")
3971                                    .tab_index(0_isize)
3972                                    .style(ButtonStyle::OutlinedGhost)
3973                                    .on_click(cx.listener(|this, _, window, cx| {
3974                                        this.open_skill_creator_sub_page(
3975                                            pages::SkillCreatorOpenMode::Form,
3976                                            window,
3977                                            cx,
3978                                        );
3979                                    })),
3980                            )
3981                        })
3982                        .when(is_external_agents_page, |this| {
3983                            this.child(pages::render_add_agent_popover(self, window, cx))
3984                        })
3985                        .when(is_mcp_servers_page, |this| {
3986                            this.child(pages::render_add_server_popover(self, window, cx))
3987                        }),
3988                )
3989                .into_any_element();
3990
3991            let active_page_render_fn = &current_sub_page.link.render;
3992            page_content =
3993                (active_page_render_fn)(self, &current_sub_page.scroll_handle, window, cx);
3994        } else {
3995            page_header = self.render_files_header(window, cx).into_any_element();
3996
3997            page_content = self
3998                .render_current_page_items(window, cx)
3999                .into_any_element();
4000        }
4001
4002        let current_sub_page = self.sub_page_stack.last();
4003
4004        let mut warning_banner = gpui::Empty.into_any_element();
4005        if let Some(error) =
4006            SettingsStore::global(cx).error_for_file(self.current_file.to_settings())
4007        {
4008            fn banner(
4009                label: &'static str,
4010                error: String,
4011                shown_errors: &mut HashSet<String>,
4012                cx: &mut Context<SettingsWindow>,
4013            ) -> impl IntoElement {
4014                if shown_errors.insert(error.clone()) {
4015                    telemetry::event!("Settings Error Shown", label = label, error = &error);
4016                }
4017                Banner::new()
4018                    .severity(Severity::Warning)
4019                    .child(
4020                        v_flex()
4021                            .my_0p5()
4022                            .gap_0p5()
4023                            .child(Label::new(label))
4024                            .child(Label::new(error).size(LabelSize::Small).color(Color::Muted)),
4025                    )
4026                    .action_slot(
4027                        div().pr_1().pb_1().child(
4028                            Button::new("fix-in-json", "Fix in settings.json")
4029                                .tab_index(0_isize)
4030                                .style(ButtonStyle::Tinted(ui::TintColor::Warning))
4031                                .on_click(cx.listener(|this, _, window, cx| {
4032                                    this.open_current_settings_file(window, cx);
4033                                })),
4034                        ),
4035                    )
4036            }
4037
4038            let parse_error = error.parse_error();
4039            let parse_failed = parse_error.is_some();
4040
4041            warning_banner = v_flex()
4042                .gap_2()
4043                .when_some(parse_error, |this, err| {
4044                    this.child(banner(
4045                        "Failed to load your settings. Some values may be incorrect and changes may be lost.",
4046                        err,
4047                        &mut self.shown_errors,
4048                        cx,
4049                    ))
4050                })
4051                .map(|this| match &error.migration_status {
4052                    settings::MigrationStatus::Succeeded => this.child(banner(
4053                        "Your settings are out of date, and need to be updated.",
4054                        match &self.current_file {
4055                            SettingsUiFile::User => "They can be automatically migrated to the latest version.",
4056                            SettingsUiFile::Server(_) | SettingsUiFile::Project(_)  => "They must be manually migrated to the latest version."
4057                        }.to_string(),
4058                        &mut self.shown_errors,
4059                        cx,
4060                    )),
4061                    settings::MigrationStatus::Failed { error: err } if !parse_failed => this
4062                        .child(banner(
4063                            "Your settings file is out of date, automatic migration failed",
4064                            err.clone(),
4065                            &mut self.shown_errors,
4066                            cx,
4067                        )),
4068                    _ => this,
4069                })
4070                .into_any_element()
4071        }
4072
4073        v_flex()
4074            .id("settings-ui-page")
4075            .on_action(cx.listener(|this, _: &menu::SelectNext, window, cx| {
4076                if !this.sub_page_stack.is_empty() {
4077                    // Keep Tab navigation within the sub-page content. Global
4078                    // `focus_next` would otherwise wrap past the last control to
4079                    // the navbar; instead, when focus leaves the content region we
4080                    // wrap back to the first content tab stop.
4081                    let content_handle = this.content_focus_handle.focus_handle(cx);
4082                    window.focus_next(cx);
4083                    if !content_handle.contains_focused(window, cx) {
4084                        content_handle.focus(window, cx);
4085                        window.focus_next(cx);
4086                    }
4087                    return;
4088                }
4089                for (logical_index, (actual_index, _)) in this.visible_page_items().enumerate() {
4090                    let handle = this.content_handles[this.current_page_index()][actual_index]
4091                        .focus_handle(cx);
4092                    let mut offset = 1; // for page header
4093
4094                    if let Some((_, next_item)) = this.visible_page_items().nth(logical_index + 1)
4095                        && matches!(next_item, SettingsPageItem::SectionHeader(_))
4096                    {
4097                        offset += 1;
4098                    }
4099                    if handle.contains_focused(window, cx) {
4100                        let next_logical_index = logical_index + offset + 1;
4101                        this.list_state.scroll_to_reveal_item(next_logical_index);
4102                        // We need to render the next item to ensure it's focus handle is in the element tree
4103                        cx.on_next_frame(window, |_, window, cx| {
4104                            cx.notify();
4105                            cx.on_next_frame(window, |_, window, cx| {
4106                                window.focus_next(cx);
4107                                cx.notify();
4108                            });
4109                        });
4110                        cx.notify();
4111                        return;
4112                    }
4113                }
4114                window.focus_next(cx);
4115            }))
4116            .on_action(cx.listener(|this, _: &menu::SelectPrevious, window, cx| {
4117                if !this.sub_page_stack.is_empty() {
4118                    window.focus_prev(cx);
4119                    return;
4120                }
4121                let mut prev_was_header = false;
4122                for (logical_index, (actual_index, item)) in this.visible_page_items().enumerate() {
4123                    let is_header = matches!(item, SettingsPageItem::SectionHeader(_));
4124                    let handle = this.content_handles[this.current_page_index()][actual_index]
4125                        .focus_handle(cx);
4126                    let mut offset = 1; // for page header
4127
4128                    if prev_was_header {
4129                        offset -= 1;
4130                    }
4131                    if handle.contains_focused(window, cx) {
4132                        let next_logical_index = logical_index + offset - 1;
4133                        this.list_state.scroll_to_reveal_item(next_logical_index);
4134                        // We need to render the next item to ensure it's focus handle is in the element tree
4135                        cx.on_next_frame(window, |_, window, cx| {
4136                            cx.notify();
4137                            cx.on_next_frame(window, |_, window, cx| {
4138                                window.focus_prev(cx);
4139                                cx.notify();
4140                            });
4141                        });
4142                        cx.notify();
4143                        return;
4144                    }
4145                    prev_was_header = is_header;
4146                }
4147                window.focus_prev(cx);
4148            }))
4149            .when(current_sub_page.is_none(), |this| {
4150                this.vertical_scrollbar_for(&self.list_state, window, cx)
4151            })
4152            .when_some(current_sub_page, |this, current_sub_page| {
4153                this.custom_scrollbars(
4154                    Scrollbars::new(ui::ScrollAxes::Vertical)
4155                        .tracked_scroll_handle(&current_sub_page.scroll_handle)
4156                        .id((current_sub_page.link.title.clone(), 42)),
4157                    window,
4158                    cx,
4159                )
4160            })
4161            .track_focus(&self.content_focus_handle.focus_handle(cx))
4162            .pt_6()
4163            .gap_4()
4164            .flex_1()
4165            .min_w_0()
4166            .bg(cx.theme().colors().editor_background)
4167            .child(
4168                v_flex()
4169                    .px_8()
4170                    .gap_2()
4171                    .child(page_header)
4172                    .child(warning_banner)
4173            )
4174            .child(
4175                div()
4176                    .flex_1()
4177                    .min_h_0()
4178                    .size_full()
4179                    .tab_group()
4180                    .tab_index(CONTENT_GROUP_TAB_INDEX)
4181                    .child(page_content),
4182            )
4183    }
4184
4185    /// This function will create a new settings file if one doesn't exist
4186    /// if the current file is a project settings with a valid worktree id
4187    /// We do this because the settings ui allows initializing project settings
4188    pub(crate) fn open_current_settings_file(
4189        &mut self,
4190        window: &mut Window,
4191        cx: &mut Context<Self>,
4192    ) {
4193        match &self.current_file {
4194            SettingsUiFile::User => {
4195                let Some(original_window) = self.original_window else {
4196                    return;
4197                };
4198                original_window
4199                    .update(cx, |multi_workspace, window, cx| {
4200                        multi_workspace
4201                            .workspace()
4202                            .clone()
4203                            .update(cx, |workspace, cx| {
4204                                workspace
4205                                    .with_local_or_wsl_workspace(
4206                                        window,
4207                                        cx,
4208                                        open_user_settings_in_workspace,
4209                                    )
4210                                    .detach();
4211                            });
4212                    })
4213                    .ok();
4214
4215                window.remove_window();
4216            }
4217            SettingsUiFile::Project((worktree_id, path)) => {
4218                let settings_path = path.join(paths::local_settings_file_relative_path());
4219                let app_state = workspace::AppState::global(cx);
4220
4221                let Some((workspace_window, worktree, corresponding_workspace)) = app_state
4222                    .workspace_store
4223                    .read(cx)
4224                    .workspaces_with_windows()
4225                    .filter_map(|(window_handle, weak)| {
4226                        let workspace = weak.upgrade()?;
4227                        let window = window_handle.downcast::<MultiWorkspace>()?;
4228                        Some((window, workspace))
4229                    })
4230                    .find_map(|(window, workspace): (_, Entity<Workspace>)| {
4231                        workspace
4232                            .read(cx)
4233                            .project()
4234                            .read(cx)
4235                            .worktree_for_id(*worktree_id, cx)
4236                            .map(|worktree| (window, worktree, workspace))
4237                    })
4238                else {
4239                    log::error!(
4240                        "No corresponding workspace contains worktree id: {}",
4241                        worktree_id
4242                    );
4243
4244                    return;
4245                };
4246
4247                let create_task = if worktree.read(cx).entry_for_path(&settings_path).is_some() {
4248                    None
4249                } else {
4250                    Some(worktree.update(cx, |tree, cx| {
4251                        tree.create_entry(
4252                            settings_path.clone().into(),
4253                            false,
4254                            Some(initial_project_settings_content().as_bytes().to_vec()),
4255                            cx,
4256                        )
4257                    }))
4258                };
4259
4260                let worktree_id = *worktree_id;
4261
4262                // TODO: move zed::open_local_file() APIs to this crate, and
4263                // re-implement the "initial_contents" behavior
4264                let workspace_weak = corresponding_workspace.downgrade();
4265                workspace_window
4266                    .update(cx, |_, window, cx| {
4267                        cx.spawn_in(window, async move |_, cx| {
4268                            if let Some(create_task) = create_task {
4269                                create_task.await.ok()?;
4270                            };
4271
4272                            workspace_weak
4273                                .update_in(cx, |workspace, window, cx| {
4274                                    workspace.open_path(
4275                                        (worktree_id, settings_path.clone()),
4276                                        None,
4277                                        true,
4278                                        window,
4279                                        cx,
4280                                    )
4281                                })
4282                                .ok()?
4283                                .await
4284                                .log_err()?;
4285
4286                            workspace_weak
4287                                .update_in(cx, |_, window, cx| {
4288                                    window.activate_window();
4289                                    cx.notify();
4290                                })
4291                                .ok();
4292
4293                            Some(())
4294                        })
4295                        .detach();
4296                    })
4297                    .ok();
4298
4299                window.remove_window();
4300            }
4301            SettingsUiFile::Server(_) => {
4302                // Server files are not editable
4303                return;
4304            }
4305        };
4306    }
4307
4308    fn current_page_index(&self) -> usize {
4309        if self.navbar_entries.is_empty() {
4310            return 0;
4311        }
4312
4313        self.navbar_entries[self.navbar_entry].page_index
4314    }
4315
4316    fn current_page(&self) -> &SettingsPage {
4317        &self.pages[self.current_page_index()]
4318    }
4319
4320    fn is_navbar_entry_selected(&self, ix: usize) -> bool {
4321        ix == self.navbar_entry
4322    }
4323
4324    fn push_sub_page(
4325        &mut self,
4326        sub_page_link: SubPageLink,
4327        section_header: SharedString,
4328        window: &mut Window,
4329        cx: &mut Context<SettingsWindow>,
4330    ) {
4331        self.sandbox_host_validation_error = None;
4332        self.sub_page_stack
4333            .push(SubPage::new(sub_page_link, section_header));
4334        self.content_focus_handle.focus_handle(cx).focus(window, cx);
4335        cx.notify();
4336    }
4337
4338    /// Push a dynamically-created sub-page with a custom render function.
4339    /// This is useful for nested sub-pages that aren't defined in the main pages list.
4340    pub fn push_dynamic_sub_page(
4341        &mut self,
4342        title: impl Into<SharedString>,
4343        section_header: impl Into<SharedString>,
4344        json_path: Option<&'static str>,
4345        in_json: bool,
4346        render: fn(
4347            &SettingsWindow,
4348            &ScrollHandle,
4349            &mut Window,
4350            &mut Context<SettingsWindow>,
4351        ) -> AnyElement,
4352        window: &mut Window,
4353        cx: &mut Context<SettingsWindow>,
4354    ) {
4355        self.regex_validation_error = None;
4356        let sub_page_link = SubPageLink {
4357            title: title.into(),
4358            r#type: SubPageType::default(),
4359            description: None,
4360            search_aliases: &[],
4361            json_path,
4362            in_json,
4363            files: USER,
4364            render,
4365        };
4366        self.push_sub_page(sub_page_link, section_header.into(), window, cx);
4367    }
4368
4369    pub(crate) fn skill_creator_page(&self) -> Option<Entity<pages::SkillCreatorPage>> {
4370        self.skill_creator_page
4371            .as_ref()
4372            .map(|(page, _)| page.clone())
4373    }
4374
4375    /// If the creator is already the active sub-page, the open mode is applied
4376    /// to the existing form instead
4377    pub fn open_skill_creator_sub_page(
4378        &mut self,
4379        open_mode: pages::SkillCreatorOpenMode,
4380        window: &mut Window,
4381        cx: &mut Context<SettingsWindow>,
4382    ) {
4383        let creator_is_active_sub_page = self
4384            .sub_page_stack
4385            .last()
4386            .is_some_and(|sub_page| sub_page.link.r#type == SubPageType::SkillCreator);
4387
4388        if creator_is_active_sub_page && let Some((page, _)) = &self.skill_creator_page {
4389            let page = page.clone();
4390            page.update(cx, |page, cx| page.apply_open_mode(open_mode, window, cx));
4391            return;
4392        }
4393
4394        let settings_window = cx.weak_entity();
4395        let page = cx.new(|cx| pages::SkillCreatorPage::new(settings_window, window, cx));
4396
4397        let subscription =
4398            cx.subscribe_in(
4399                &page,
4400                window,
4401                |this, _page, event: &pages::SkillCreatorEvent, window, cx| match event {
4402                    pages::SkillCreatorEvent::Dismissed | pages::SkillCreatorEvent::Saved => {
4403                        if this.sub_page_stack.last().is_some_and(|sub_page| {
4404                            sub_page.link.r#type == SubPageType::SkillCreator
4405                        }) {
4406                            this.pop_sub_page(window, cx);
4407                        }
4408                    }
4409                },
4410            );
4411
4412        self.skill_creator_page = Some((page.clone(), subscription));
4413
4414        let sub_page_link = SubPageLink {
4415            title: "Create Skill".into(),
4416            r#type: SubPageType::SkillCreator,
4417            description: None,
4418            search_aliases: &[],
4419            json_path: None,
4420            in_json: false,
4421            files: USER | PROJECT,
4422            render: pages::render_skill_creator_page,
4423        };
4424
4425        self.push_sub_page(sub_page_link, "Agent".into(), window, cx);
4426
4427        let creating_from_url = !matches!(open_mode, pages::SkillCreatorOpenMode::Url { .. });
4428        page.update(cx, |page, cx| {
4429            page.apply_open_mode(open_mode, window, cx);
4430        });
4431        if creating_from_url {
4432            let name_editor_focus_handle = page.read(cx).name_editor_focus_handle(cx);
4433            window.focus(&name_editor_focus_handle, cx);
4434        }
4435    }
4436
4437    pub fn navigate_to_skill_creator(
4438        &mut self,
4439        open_mode: pages::SkillCreatorOpenMode,
4440        window: &mut Window,
4441        cx: &mut Context<SettingsWindow>,
4442    ) {
4443        self.sub_page_stack.clear();
4444        let skills_page_index = self.pages.iter().position(|page| {
4445            page.items.iter().any(|item| {
4446                matches!(
4447                    item,
4448                    SettingsPageItem::SubPageLink(link)
4449                        if link.json_path == Some(AGENT_SKILLS_SETTINGS_PATH)
4450                )
4451            })
4452        });
4453        if let Some(page_index) = skills_page_index
4454            && let Some(navbar_entry_index) = self
4455                .navbar_entries
4456                .iter()
4457                .position(|entry| entry.page_index == page_index && entry.is_root)
4458        {
4459            self.open_navbar_entry_page(navbar_entry_index);
4460        }
4461        self.navigate_to_sub_page(AGENT_SKILLS_SETTINGS_PATH, window, cx);
4462        self.open_skill_creator_sub_page(open_mode, window, cx);
4463    }
4464
4465    /// Navigate to a sub-page by its json_path.
4466    /// Returns true if the sub-page was found and pushed, false otherwise.
4467    pub fn navigate_to_sub_page(
4468        &mut self,
4469        json_path: &str,
4470        window: &mut Window,
4471        cx: &mut Context<SettingsWindow>,
4472    ) -> bool {
4473        for page in &self.pages {
4474            for (item_index, item) in page.items.iter().enumerate() {
4475                if let SettingsPageItem::SubPageLink(sub_page_link) = item {
4476                    if sub_page_link.json_path == Some(json_path) {
4477                        let section_header = page
4478                            .items
4479                            .iter()
4480                            .take(item_index)
4481                            .rev()
4482                            .find_map(|item| item.header_text().map(SharedString::new_static))
4483                            .unwrap_or_else(|| "Settings".into());
4484
4485                        self.push_sub_page(sub_page_link.clone(), section_header, window, cx);
4486                        return true;
4487                    }
4488                }
4489            }
4490        }
4491        false
4492    }
4493
4494    /// Navigate to a setting by its json_path.
4495    /// Clears the sub-page stack and scrolls to the setting item.
4496    /// Returns true if the setting was found, false otherwise.
4497    pub fn navigate_to_setting(
4498        &mut self,
4499        json_path: &str,
4500        window: &mut Window,
4501        cx: &mut Context<SettingsWindow>,
4502    ) -> bool {
4503        self.sub_page_stack.clear();
4504
4505        for (page_index, page) in self.pages.iter().enumerate() {
4506            for (item_index, item) in page.items.iter().enumerate() {
4507                let item_json_path = match item {
4508                    SettingsPageItem::SettingItem(setting_item) => setting_item.field.json_path(),
4509                    SettingsPageItem::DynamicItem(dynamic_item) => {
4510                        dynamic_item.discriminant.field.json_path()
4511                    }
4512                    _ => None,
4513                };
4514                if item_json_path == Some(json_path) {
4515                    if let Some(navbar_entry_index) = self
4516                        .navbar_entries
4517                        .iter()
4518                        .position(|e| e.page_index == page_index && e.is_root)
4519                    {
4520                        self.open_and_scroll_to_navbar_entry(
4521                            navbar_entry_index,
4522                            None,
4523                            false,
4524                            window,
4525                            cx,
4526                        );
4527                        self.scroll_to_content_item(item_index, window, cx);
4528                        return true;
4529                    }
4530                }
4531            }
4532        }
4533        false
4534    }
4535
4536    pub(crate) fn pop_sub_page(&mut self, window: &mut Window, cx: &mut Context<SettingsWindow>) {
4537        self.regex_validation_error = None;
4538        self.sandbox_host_validation_error = None;
4539        if let Some(popped) = self.sub_page_stack.pop()
4540            && popped.link.r#type == SubPageType::SkillCreator
4541        {
4542            self.skill_creator_page = None;
4543        }
4544        self.content_focus_handle.focus_handle(cx).focus(window, cx);
4545        cx.notify();
4546    }
4547
4548    fn focus_file_at_index(&mut self, index: usize, window: &mut Window, cx: &mut App) {
4549        if let Some((_, handle)) = self.files.get(index) {
4550            handle.focus(window, cx);
4551        }
4552    }
4553
4554    fn focused_file_index(&self, window: &Window, cx: &Context<Self>) -> usize {
4555        if self.files_focus_handle.contains_focused(window, cx)
4556            && let Some(index) = self
4557                .files
4558                .iter()
4559                .position(|(_, handle)| handle.is_focused(window))
4560        {
4561            return index;
4562        }
4563        if let Some(current_file_index) = self
4564            .files
4565            .iter()
4566            .position(|(file, _)| file == &self.current_file)
4567        {
4568            return current_file_index;
4569        }
4570        0
4571    }
4572
4573    fn focus_handle_for_content_element(
4574        &self,
4575        actual_item_index: usize,
4576        cx: &Context<Self>,
4577    ) -> FocusHandle {
4578        let page_index = self.current_page_index();
4579        self.content_handles[page_index][actual_item_index].focus_handle(cx)
4580    }
4581
4582    fn focused_nav_entry(&self, window: &Window, cx: &App) -> Option<usize> {
4583        if !self
4584            .navbar_focus_handle
4585            .focus_handle(cx)
4586            .contains_focused(window, cx)
4587        {
4588            return None;
4589        }
4590        for (index, entry) in self.navbar_entries.iter().enumerate() {
4591            if entry.focus_handle.is_focused(window) {
4592                return Some(index);
4593            }
4594        }
4595        None
4596    }
4597
4598    fn root_entry_containing(&self, nav_entry_index: usize) -> usize {
4599        let mut index = Some(nav_entry_index);
4600        while let Some(prev_index) = index
4601            && !self.navbar_entries[prev_index].is_root
4602        {
4603            index = prev_index.checked_sub(1);
4604        }
4605        return index.expect("No root entry found");
4606    }
4607}
4608
4609impl Render for SettingsWindow {
4610    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
4611        let ui_font = theme_settings::setup_ui_font(window, cx);
4612
4613        client_side_decorations(
4614            v_flex()
4615                .text_color(cx.theme().colors().text)
4616                .size_full()
4617                .children(self.title_bar.clone())
4618                .child(
4619                    div()
4620                        .id("settings-window")
4621                        .key_context("SettingsWindow")
4622                        .track_focus(&self.focus_handle)
4623                        .on_action(cx.listener(|this, _: &OpenCurrentFile, window, cx| {
4624                            this.open_current_settings_file(window, cx);
4625                        }))
4626                        .on_action(|_: &Minimize, window, _cx| {
4627                            window.minimize_window();
4628                        })
4629                        .on_action(cx.listener(|this, _: &search::FocusSearch, window, cx| {
4630                            this.search_bar.focus_handle(cx).focus(window, cx);
4631                        }))
4632                        .on_action(cx.listener(|this, _: &ToggleFocusNav, window, cx| {
4633                            if this
4634                                .navbar_focus_handle
4635                                .focus_handle(cx)
4636                                .contains_focused(window, cx)
4637                            {
4638                                this.open_and_scroll_to_navbar_entry(
4639                                    this.navbar_entry,
4640                                    None,
4641                                    true,
4642                                    window,
4643                                    cx,
4644                                );
4645                            } else {
4646                                this.focus_and_scroll_to_nav_entry(this.navbar_entry, window, cx);
4647                            }
4648                        }))
4649                        .on_action(cx.listener(
4650                            |this, FocusFile(file_index): &FocusFile, window, cx| {
4651                                this.focus_file_at_index(*file_index as usize, window, cx);
4652                            },
4653                        ))
4654                        .on_action(cx.listener(|this, _: &FocusNextFile, window, cx| {
4655                            let next_index = usize::min(
4656                                this.focused_file_index(window, cx) + 1,
4657                                this.files.len().saturating_sub(1),
4658                            );
4659                            this.focus_file_at_index(next_index, window, cx);
4660                        }))
4661                        .on_action(cx.listener(|this, _: &FocusPreviousFile, window, cx| {
4662                            let prev_index = this.focused_file_index(window, cx).saturating_sub(1);
4663                            this.focus_file_at_index(prev_index, window, cx);
4664                        }))
4665                        .on_action(cx.listener(|this, _: &menu::SelectNext, window, cx| {
4666                            if this
4667                                .search_bar
4668                                .focus_handle(cx)
4669                                .contains_focused(window, cx)
4670                            {
4671                                this.focus_and_scroll_to_first_visible_nav_entry(window, cx);
4672                            } else {
4673                                window.focus_next(cx);
4674                            }
4675                        }))
4676                        .on_action(|_: &menu::SelectPrevious, window, cx| {
4677                            window.focus_prev(cx);
4678                        })
4679                        .flex()
4680                        .flex_row()
4681                        .flex_1()
4682                        .min_h_0()
4683                        .font(ui_font)
4684                        .bg(cx.theme().colors().background)
4685                        .text_color(cx.theme().colors().text)
4686                        .when(!cfg!(target_os = "macos"), |this| {
4687                            this.border_t_1().border_color(cx.theme().colors().border)
4688                        })
4689                        .child(self.render_nav(window, cx))
4690                        .child(self.render_page(window, cx)),
4691                ),
4692            window,
4693            cx,
4694        )
4695    }
4696}
4697
4698pub(crate) fn all_projects(
4699    window: Option<&WindowHandle<MultiWorkspace>>,
4700    cx: &App,
4701) -> impl Iterator<Item = Entity<Project>> {
4702    let mut seen_project_ids = std::collections::HashSet::new();
4703    let app_state = workspace::AppState::global(cx);
4704    app_state
4705        .workspace_store
4706        .read(cx)
4707        .workspaces()
4708        .filter_map(|weak| weak.upgrade())
4709        .map(|workspace: Entity<Workspace>| workspace.read(cx).project().clone())
4710        .chain(
4711            window
4712                .and_then(|handle| handle.read(cx).ok())
4713                .into_iter()
4714                .flat_map(|multi_workspace| {
4715                    multi_workspace
4716                        .workspaces()
4717                        .map(|workspace| workspace.read(cx).project().clone())
4718                        .collect::<Vec<_>>()
4719                }),
4720        )
4721        .filter(move |project| seen_project_ids.insert(project.entity_id()))
4722}
4723
4724fn open_user_settings_in_workspace(
4725    workspace: &mut Workspace,
4726    window: &mut Window,
4727    cx: &mut Context<Workspace>,
4728) {
4729    let project = workspace.project().clone();
4730
4731    cx.spawn_in(window, async move |workspace, cx| {
4732        let (config_dir, settings_file) = project.update(cx, |project, cx| {
4733            (
4734                project.try_windows_path_to_wsl(paths::config_dir().as_path(), cx),
4735                project.try_windows_path_to_wsl(paths::settings_file().as_path(), cx),
4736            )
4737        });
4738        let config_dir = config_dir.await?;
4739        let settings_file = settings_file.await?;
4740        project
4741            .update(cx, |project, cx| {
4742                project.find_or_create_worktree(&config_dir, false, cx)
4743            })
4744            .await
4745            .ok();
4746        workspace
4747            .update_in(cx, |workspace, window, cx| {
4748                workspace.open_paths(
4749                    vec![settings_file],
4750                    OpenOptions {
4751                        visible: Some(OpenVisible::None),
4752                        ..Default::default()
4753                    },
4754                    None,
4755                    window,
4756                    cx,
4757                )
4758            })?
4759            .await;
4760
4761        workspace.update_in(cx, |_, window, cx| {
4762            window.activate_window();
4763            cx.notify();
4764        })
4765    })
4766    .detach();
4767}
4768
4769fn update_settings_file(
4770    file: SettingsUiFile,
4771    file_name: Option<&'static str>,
4772    window: &mut Window,
4773    cx: &mut App,
4774    update: impl 'static + Send + FnOnce(&mut SettingsContent, &App),
4775) -> Result<()> {
4776    telemetry::event!("Settings Change", setting = file_name, type = file.setting_type());
4777
4778    match file {
4779        SettingsUiFile::Project((worktree_id, rel_path)) => {
4780            let rel_path = rel_path.join(paths::local_settings_file_relative_path());
4781            let Some(settings_window) = window.root::<SettingsWindow>().flatten() else {
4782                anyhow::bail!("No settings window found");
4783            };
4784
4785            update_project_setting_file(worktree_id, rel_path.into(), update, settings_window, cx)
4786        }
4787        SettingsUiFile::User => {
4788            // todo(settings_ui) error?
4789            SettingsStore::global(cx).update_settings_file(<dyn fs::Fs>::global(cx), update);
4790            Ok(())
4791        }
4792        SettingsUiFile::Server(_) => unimplemented!(),
4793    }
4794}
4795
4796struct ProjectSettingsUpdateEntry {
4797    worktree_id: WorktreeId,
4798    rel_path: Arc<RelPath>,
4799    settings_window: WeakEntity<SettingsWindow>,
4800    project: WeakEntity<Project>,
4801    worktree: WeakEntity<Worktree>,
4802    update: Box<dyn FnOnce(&mut SettingsContent, &App)>,
4803}
4804
4805struct ProjectSettingsUpdateQueue {
4806    tx: mpsc::UnboundedSender<ProjectSettingsUpdateEntry>,
4807    _task: Task<()>,
4808}
4809
4810impl Global for ProjectSettingsUpdateQueue {}
4811
4812impl ProjectSettingsUpdateQueue {
4813    fn new(cx: &mut App) -> Self {
4814        let (tx, mut rx) = mpsc::unbounded();
4815        let task = cx.spawn(async move |mut cx| {
4816            while let Some(entry) = rx.next().await {
4817                if let Err(err) = Self::process_entry(entry, &mut cx).await {
4818                    log::error!("Failed to update project settings: {err:?}");
4819                }
4820            }
4821        });
4822        Self { tx, _task: task }
4823    }
4824
4825    fn enqueue(cx: &mut App, entry: ProjectSettingsUpdateEntry) {
4826        cx.update_global::<Self, _>(|queue, _cx| {
4827            if let Err(err) = queue.tx.unbounded_send(entry) {
4828                log::error!("Failed to enqueue project settings update: {err}");
4829            }
4830        });
4831    }
4832
4833    async fn process_entry(entry: ProjectSettingsUpdateEntry, cx: &mut AsyncApp) -> Result<()> {
4834        let ProjectSettingsUpdateEntry {
4835            worktree_id,
4836            rel_path,
4837            settings_window,
4838            project,
4839            worktree,
4840            update,
4841        } = entry;
4842
4843        let project_path = ProjectPath {
4844            worktree_id,
4845            path: rel_path.clone(),
4846        };
4847
4848        let needs_creation = worktree.read_with(cx, |worktree, _| {
4849            worktree.entry_for_path(&rel_path).is_none()
4850        })?;
4851
4852        if needs_creation {
4853            worktree
4854                .update(cx, |worktree, cx| {
4855                    worktree.create_entry(rel_path.clone(), false, None, cx)
4856                })?
4857                .await?;
4858        }
4859
4860        let buffer_store = project.read_with(cx, |project, _cx| project.buffer_store().clone())?;
4861
4862        let cached_buffer = settings_window
4863            .read_with(cx, |settings_window, _| {
4864                settings_window
4865                    .project_setting_file_buffers
4866                    .get(&project_path)
4867                    .cloned()
4868            })
4869            .unwrap_or_default();
4870
4871        let buffer = if let Some(cached_buffer) = cached_buffer {
4872            let needs_reload = cached_buffer.read_with(cx, |buffer, _| buffer.has_conflict());
4873            if needs_reload {
4874                cached_buffer
4875                    .update(cx, |buffer, cx| buffer.reload(cx))
4876                    .await
4877                    .context("Failed to reload settings file")?;
4878            }
4879            cached_buffer
4880        } else {
4881            let buffer = buffer_store
4882                .update(cx, |store, cx| store.open_buffer(project_path.clone(), cx))
4883                .await
4884                .context("Failed to open settings file")?;
4885
4886            let _ = settings_window.update(cx, |this, _cx| {
4887                this.project_setting_file_buffers
4888                    .insert(project_path, buffer.clone());
4889            });
4890
4891            buffer
4892        };
4893
4894        buffer.update(cx, |buffer, cx| {
4895            let current_text = buffer.text();
4896            if let Some(new_text) = cx
4897                .global::<SettingsStore>()
4898                .new_text_for_update(current_text, |settings| update(settings, cx))
4899                .log_err()
4900            {
4901                buffer.edit([(0..buffer.len(), new_text)], None, cx);
4902            }
4903        });
4904
4905        buffer_store
4906            .update(cx, |store, cx| store.save_buffer(buffer, cx))
4907            .await
4908            .context("Failed to save settings file")?;
4909
4910        Ok(())
4911    }
4912}
4913
4914fn update_project_setting_file(
4915    worktree_id: WorktreeId,
4916    rel_path: Arc<RelPath>,
4917    update: impl 'static + FnOnce(&mut SettingsContent, &App),
4918    settings_window: Entity<SettingsWindow>,
4919    cx: &mut App,
4920) -> Result<()> {
4921    let Some((worktree, project)) =
4922        all_projects(settings_window.read(cx).original_window.as_ref(), cx).find_map(|project| {
4923            project
4924                .read(cx)
4925                .worktree_for_id(worktree_id, cx)
4926                .zip(Some(project))
4927        })
4928    else {
4929        anyhow::bail!("Could not find project with worktree id: {}", worktree_id);
4930    };
4931
4932    let entry = ProjectSettingsUpdateEntry {
4933        worktree_id,
4934        rel_path,
4935        settings_window: settings_window.downgrade(),
4936        project: project.downgrade(),
4937        worktree: worktree.downgrade(),
4938        update: Box::new(update),
4939    };
4940
4941    ProjectSettingsUpdateQueue::enqueue(cx, entry);
4942
4943    Ok(())
4944}
4945
4946/// Derives a human-readable label for assistive technology from a setting's
4947/// JSON path, e.g. `"buffer_font_size"` becomes `"Buffer Font Size"`.
4948struct CurrentSettingsValue<'a, T> {
4949    value: &'a T,
4950    disabled: bool,
4951}
4952
4953fn get_current_value<'a, T>(
4954    settings_store: &'a SettingsStore,
4955    file: &SettingsUiFile,
4956    field: &'a SettingField<T>,
4957    cx: &'a App,
4958) -> Option<CurrentSettingsValue<'a, T>> {
4959    let user_store = AppState::global(cx).user_store.read(cx);
4960    let org_config = user_store.current_organization_configuration();
4961
4962    let (_file, value) = settings_store.get_value_from_file(file.to_settings(), field.pick);
4963    let value = value?;
4964
4965    let org_value = org_config
4966        .zip(field.organization_override)
4967        .and_then(|(org_config, org_override)| (org_override)(org_config));
4968
4969    Some(CurrentSettingsValue {
4970        disabled: org_value.is_some(),
4971        value: org_value.unwrap_or(&value),
4972    })
4973}
4974
4975fn render_text_field<T: From<String> + Into<String> + AsRef<str> + Clone>(
4976    field: SettingField<T>,
4977    file: SettingsUiFile,
4978    metadata: Option<&SettingsFieldMetadata>,
4979    title: &'static str,
4980    description: &'static str,
4981    _window: &mut Window,
4982    cx: &mut App,
4983) -> AnyElement {
4984    let (_, initial_text) =
4985        SettingsStore::global(cx).get_value_from_file(file.to_settings(), field.pick);
4986    let initial_text = if metadata.is_some_and(|metadata| metadata.treat_missing_text_as_empty) {
4987        Some(
4988            initial_text
4989                .map(|text| text.as_ref().to_string())
4990                .unwrap_or_default(),
4991        )
4992    } else {
4993        initial_text
4994            .filter(|text| !text.as_ref().is_empty())
4995            .map(|text| text.as_ref().to_string())
4996    };
4997
4998    // The JSON path uniquely identifies the setting this field edits, making
4999    // it a stable, collision-free element ID within the page.
5000    SettingsInputField::new(field.json_path.unwrap_or("settings-text-field"))
5001        .tab_index(0)
5002        .aria_label(title)
5003        .when(!description.is_empty(), |editor| {
5004            editor.aria_description(description)
5005        })
5006        .when_some(initial_text, |editor, text| editor.with_initial_text(text))
5007        .when_some(
5008            metadata.and_then(|metadata| metadata.placeholder),
5009            |editor, placeholder| editor.with_placeholder(placeholder),
5010        )
5011        .when(
5012            metadata.is_some_and(|metadata| metadata.display_confirm_button),
5013            |editor| editor.display_confirm_button(),
5014        )
5015        .when(
5016            metadata.is_some_and(|metadata| metadata.display_clear_button),
5017            |editor| editor.display_clear_button(),
5018        )
5019        .when(
5020            metadata.is_some_and(|metadata| metadata.confirm_on_focus_out),
5021            |editor| editor.confirm_on_focus_out(),
5022        )
5023        .on_confirm({
5024            move |new_text, window, cx| {
5025                update_settings_file(
5026                    file.clone(),
5027                    field.json_path,
5028                    window,
5029                    cx,
5030                    move |settings, app| {
5031                        (field.write)(settings, new_text.map(Into::into), app);
5032                    },
5033                )
5034                .log_err(); // todo(settings_ui) don't log err
5035            }
5036        })
5037        .into_any_element()
5038}
5039
5040fn render_toggle_button<B: Into<bool> + From<bool> + Copy>(
5041    field: SettingField<B>,
5042    file: SettingsUiFile,
5043    _metadata: Option<&SettingsFieldMetadata>,
5044    title: &'static str,
5045    description: &'static str,
5046    _window: &mut Window,
5047    cx: &mut App,
5048) -> AnyElement {
5049    let value = get_current_value(&SettingsStore::global(cx), &file, &field, cx);
5050    let (value, disabled) = value
5051        .map(|current_value| (*current_value.value, current_value.disabled))
5052        .unwrap_or((false.into(), false));
5053
5054    let toggle_state = if value.into() {
5055        ToggleState::Selected
5056    } else {
5057        ToggleState::Unselected
5058    };
5059
5060    Switch::new("toggle_button", toggle_state)
5061        .tab_index(0_isize)
5062        .aria_label(title)
5063        .when(!description.is_empty(), |this| {
5064            this.aria_description(description)
5065        })
5066        .disabled(disabled)
5067        .on_click({
5068            move |state, window, cx| {
5069                telemetry::event!("Settings Change", setting = field.json_path, type = file.setting_type());
5070
5071                let state = *state == ui::ToggleState::Selected;
5072                update_settings_file(file.clone(), field.json_path, window, cx, move |settings, app| {
5073                    (field.write)(settings, Some(state.into()), app);
5074                })
5075                .log_err(); // todo(settings_ui) don't log err
5076            }
5077        })
5078        .into_any_element()
5079}
5080
5081fn render_editable_number_field<T: NumberFieldType + Send + Sync>(
5082    field: SettingField<T>,
5083    file: SettingsUiFile,
5084    _metadata: Option<&SettingsFieldMetadata>,
5085    title: &'static str,
5086    description: &'static str,
5087    window: &mut Window,
5088    cx: &mut App,
5089) -> AnyElement {
5090    let (_, value) = SettingsStore::global(cx).get_value_from_file(file.to_settings(), field.pick);
5091    let value = value.copied().unwrap_or_else(T::min_value);
5092
5093    let id = field
5094        .json_path
5095        .map(|p| format!("numeric_stepper_{}", p))
5096        .unwrap_or_else(|| "numeric_stepper".to_string());
5097
5098    NumberField::new(id, value, window, cx)
5099        .mode(NumberFieldMode::Edit, cx)
5100        .tab_index(0_isize)
5101        .aria_label(title)
5102        .when(!description.is_empty(), |this| {
5103            this.aria_description(description)
5104        })
5105        .on_change({
5106            move |value, window, cx| {
5107                let value = *value;
5108                update_settings_file(
5109                    file.clone(),
5110                    field.json_path,
5111                    window,
5112                    cx,
5113                    move |settings, app| {
5114                        (field.write)(settings, Some(value), app);
5115                    },
5116                )
5117                .log_err(); // todo(settings_ui) don't log err
5118            }
5119        })
5120        .into_any_element()
5121}
5122
5123fn render_dropdown<T>(
5124    field: SettingField<T>,
5125    file: SettingsUiFile,
5126    metadata: Option<&SettingsFieldMetadata>,
5127    title: &'static str,
5128    description: &'static str,
5129    _window: &mut Window,
5130    cx: &mut App,
5131) -> AnyElement
5132where
5133    T: strum::VariantArray + strum::VariantNames + Copy + PartialEq + Send + Sync + 'static,
5134{
5135    let variants = || -> &'static [T] { <T as strum::VariantArray>::VARIANTS };
5136    let labels = || -> &'static [&'static str] { <T as strum::VariantNames>::VARIANTS };
5137    let should_do_titlecase = metadata
5138        .and_then(|metadata| metadata.should_do_titlecase)
5139        .unwrap_or(true);
5140
5141    let current_value = get_current_value(&SettingsStore::global(cx), &file, &field, cx);
5142    let (current_value, disabled) = current_value
5143        .map(|current_value| (*current_value.value, current_value.disabled))
5144        .unwrap_or((variants()[0], false));
5145
5146    EnumVariantDropdown::new("dropdown", current_value, variants(), labels(), {
5147        move |value, window, cx| {
5148            if value == current_value {
5149                return;
5150            }
5151            update_settings_file(
5152                file.clone(),
5153                field.json_path,
5154                window,
5155                cx,
5156                move |settings, app| {
5157                    (field.write)(settings, Some(value), app);
5158                },
5159            )
5160            .log_err(); // todo(settings_ui) don't log err
5161        }
5162    })
5163    .aria_label(title)
5164    .when(!description.is_empty(), |this| {
5165        this.aria_description(description)
5166    })
5167    .disabled(disabled)
5168    .tab_index(0)
5169    .title_case(should_do_titlecase)
5170    .into_any_element()
5171}
5172
5173fn render_picker_trigger_button(id: SharedString, label: SharedString) -> Button {
5174    Button::new(id, label)
5175        .aria_role(Role::ComboBox)
5176        .tab_index(0_isize)
5177        .style(ButtonStyle::Outlined)
5178        .size(ButtonSize::Medium)
5179        .end_icon(
5180            Icon::new(IconName::ChevronUpDown)
5181                .size(IconSize::Small)
5182                .color(Color::Muted),
5183        )
5184}
5185
5186/// Wires the Expand/Collapse accessibility actions on a picker trigger button to
5187/// the popover handle, so assistive technology can open and close the picker
5188/// (used by UIA on Windows and AX on macOS; Linux/AT-SPI uses the click action).
5189fn wire_picker_trigger_a11y<M: gpui::ManagedView>(
5190    button: Button,
5191    handle: ui::PopoverMenuHandle<M>,
5192) -> Button {
5193    let show_handle = handle.clone();
5194    let hide_handle = handle;
5195    button
5196        .on_a11y_action(gpui::accesskit::Action::Expand, move |_, window, cx| {
5197            show_handle.show(window, cx);
5198        })
5199        .on_a11y_action(gpui::accesskit::Action::Collapse, move |_, _window, cx| {
5200            hide_handle.hide(cx);
5201        })
5202}
5203
5204fn render_font_picker(
5205    field: SettingField<settings::FontFamilyName>,
5206    file: SettingsUiFile,
5207    _metadata: Option<&SettingsFieldMetadata>,
5208    title: &'static str,
5209    description: &'static str,
5210    _window: &mut Window,
5211    cx: &mut App,
5212) -> AnyElement {
5213    let current_value = SettingsStore::global(cx)
5214        .get_value_from_file(file.to_settings(), field.pick)
5215        .1
5216        .cloned()
5217        .map_or_else(|| SharedString::default(), |value| value.into_gpui());
5218
5219    let handle = ui::PopoverMenuHandle::default();
5220    PopoverMenu::new("font-picker")
5221        .trigger(wire_picker_trigger_a11y(
5222            render_picker_trigger_button(
5223                "font_family_picker_trigger".into(),
5224                current_value.clone(),
5225            )
5226            .aria_label(title)
5227            .when(!description.is_empty(), |this| {
5228                this.aria_description(description)
5229            }),
5230            handle.clone(),
5231        ))
5232        .menu(move |window, cx| {
5233            let file = file.clone();
5234            let current_value = current_value.clone();
5235
5236            Some(cx.new(move |cx| {
5237                font_picker(
5238                    current_value,
5239                    move |font_name, window, cx| {
5240                        update_settings_file(
5241                            file.clone(),
5242                            field.json_path,
5243                            window,
5244                            cx,
5245                            move |settings, app| {
5246                                (field.write)(settings, Some(font_name.to_string().into()), app);
5247                            },
5248                        )
5249                        .log_err(); // todo(settings_ui) don't log err
5250                    },
5251                    window,
5252                    cx,
5253                )
5254            }))
5255        })
5256        .anchor(gpui::Anchor::TopLeft)
5257        .offset(gpui::Point {
5258            x: px(0.0),
5259            y: px(2.0),
5260        })
5261        .with_handle(handle)
5262        .into_any_element()
5263}
5264
5265fn render_theme_picker(
5266    field: SettingField<settings::ThemeName>,
5267    file: SettingsUiFile,
5268    _metadata: Option<&SettingsFieldMetadata>,
5269    title: &'static str,
5270    description: &'static str,
5271    _window: &mut Window,
5272    cx: &mut App,
5273) -> AnyElement {
5274    let (_, value) = SettingsStore::global(cx).get_value_from_file(file.to_settings(), field.pick);
5275    let current_value = value
5276        .cloned()
5277        .map(|theme_name| theme_name.0.into())
5278        .unwrap_or_else(|| cx.theme().name.clone());
5279
5280    let handle = ui::PopoverMenuHandle::default();
5281    PopoverMenu::new("theme-picker")
5282        .trigger(wire_picker_trigger_a11y(
5283            render_picker_trigger_button("theme_picker_trigger".into(), current_value.clone())
5284                .aria_label(title)
5285                .when(!description.is_empty(), |this| {
5286                    this.aria_description(description)
5287                }),
5288            handle.clone(),
5289        ))
5290        .menu(move |window, cx| {
5291            Some(cx.new(|cx| {
5292                let file = file.clone();
5293                let current_value = current_value.clone();
5294                theme_picker(
5295                    current_value,
5296                    move |theme_name, window, cx| {
5297                        update_settings_file(
5298                            file.clone(),
5299                            field.json_path,
5300                            window,
5301                            cx,
5302                            move |settings, app| {
5303                                (field.write)(
5304                                    settings,
5305                                    Some(settings::ThemeName(theme_name.into())),
5306                                    app,
5307                                );
5308                            },
5309                        )
5310                        .log_err(); // todo(settings_ui) don't log err
5311                    },
5312                    window,
5313                    cx,
5314                )
5315            }))
5316        })
5317        .anchor(gpui::Anchor::TopLeft)
5318        .offset(gpui::Point {
5319            x: px(0.0),
5320            y: px(2.0),
5321        })
5322        .with_handle(handle)
5323        .into_any_element()
5324}
5325
5326fn render_icon_theme_picker(
5327    field: SettingField<settings::IconThemeName>,
5328    file: SettingsUiFile,
5329    _metadata: Option<&SettingsFieldMetadata>,
5330    title: &'static str,
5331    description: &'static str,
5332    _window: &mut Window,
5333    cx: &mut App,
5334) -> AnyElement {
5335    let (_, value) = SettingsStore::global(cx).get_value_from_file(file.to_settings(), field.pick);
5336    let current_value = value
5337        .cloned()
5338        .map(|theme_name| theme_name.0.into())
5339        .unwrap_or_else(|| cx.theme().name.clone());
5340
5341    let handle = ui::PopoverMenuHandle::default();
5342    PopoverMenu::new("icon-theme-picker")
5343        .trigger(wire_picker_trigger_a11y(
5344            render_picker_trigger_button("icon_theme_picker_trigger".into(), current_value.clone())
5345                .aria_label(title)
5346                .when(!description.is_empty(), |this| {
5347                    this.aria_description(description)
5348                }),
5349            handle.clone(),
5350        ))
5351        .menu(move |window, cx| {
5352            Some(cx.new(|cx| {
5353                let file = file.clone();
5354                let current_value = current_value.clone();
5355                icon_theme_picker(
5356                    current_value,
5357                    move |theme_name, window, cx| {
5358                        update_settings_file(
5359                            file.clone(),
5360                            field.json_path,
5361                            window,
5362                            cx,
5363                            move |settings, app| {
5364                                (field.write)(
5365                                    settings,
5366                                    Some(settings::IconThemeName(theme_name.into())),
5367                                    app,
5368                                );
5369                            },
5370                        )
5371                        .log_err(); // todo(settings_ui) don't log err
5372                    },
5373                    window,
5374                    cx,
5375                )
5376            }))
5377        })
5378        .anchor(gpui::Anchor::TopLeft)
5379        .offset(gpui::Point {
5380            x: px(0.0),
5381            y: px(2.0),
5382        })
5383        .with_handle(handle)
5384        .into_any_element()
5385}
5386
5387#[cfg(test)]
5388pub mod test {
5389
5390    use super::*;
5391
5392    impl SettingsWindow {
5393        fn navbar_entry(&self) -> usize {
5394            self.navbar_entry
5395        }
5396
5397        #[cfg(any(test, feature = "test-support"))]
5398        pub fn test(window: &mut Window, cx: &mut Context<Self>) -> Self {
5399            let search_bar = cx.new(|cx| Editor::single_line(window, cx));
5400            let dummy_page = SettingsPage {
5401                title: "Test",
5402                items: Box::new([]),
5403            };
5404            Self {
5405                title_bar: None,
5406                original_window: None,
5407                worktree_root_dirs: HashMap::default(),
5408                files: Vec::default(),
5409                current_file: SettingsUiFile::User,
5410                project_setting_file_buffers: HashMap::default(),
5411                pages: vec![dummy_page],
5412                search_bar,
5413                navbar_entry: 0,
5414                navbar_entries: Vec::default(),
5415                navbar_scroll_handle: UniformListScrollHandle::default(),
5416                navbar_focus_subscriptions: Vec::default(),
5417                filter_table: Vec::default(),
5418                has_query: false,
5419                content_handles: Vec::default(),
5420                search_task: None,
5421                sub_page_stack: Vec::default(),
5422                opening_link: false,
5423                focus_handle: cx.focus_handle(),
5424                navbar_focus_handle: NonFocusableHandle::new(
5425                    NAVBAR_CONTAINER_TAB_INDEX,
5426                    false,
5427                    window,
5428                    cx,
5429                ),
5430                content_focus_handle: NonFocusableHandle::new(
5431                    CONTENT_CONTAINER_TAB_INDEX,
5432                    false,
5433                    window,
5434                    cx,
5435                ),
5436                files_focus_handle: cx.focus_handle(),
5437                search_index: None,
5438                list_state: ListState::new(0, gpui::ListAlignment::Top, px(0.0)),
5439                shown_errors: HashSet::default(),
5440                hidden_deleted_skill_directory_paths: HashSet::default(),
5441                regex_validation_error: None,
5442                sandbox_host_validation_error: None,
5443                last_copied_link_path: None,
5444                provider_configuration_views: HashMap::default(),
5445                configuring_provider: None,
5446                last_copied_skill_directory_path: None,
5447                llm_provider_form: None,
5448                llm_provider_add_focus_handle: cx.focus_handle(),
5449                mcp_server_form: None,
5450                mcp_add_server_focus_handle: cx.focus_handle(),
5451                custom_agent_form: None,
5452                external_agent_add_focus_handle: cx.focus_handle(),
5453                harness_maintenance: Default::default(),
5454                harness_maintenance_task: None,
5455                harness_maintenance_error: None,
5456                skill_creator_page: None,
5457            }
5458        }
5459    }
5460
5461    impl PartialEq for NavBarEntry {
5462        fn eq(&self, other: &Self) -> bool {
5463            self.title == other.title
5464                && self.is_root == other.is_root
5465                && self.expanded == other.expanded
5466                && self.page_index == other.page_index
5467                && self.item_index == other.item_index
5468            // ignoring focus_handle
5469        }
5470    }
5471
5472    pub fn register_settings(cx: &mut App) {
5473        settings::init(cx);
5474        theme_settings::init(theme::LoadThemes::JustBase, cx);
5475        editor::init(cx);
5476        menu::init();
5477        language_model::init(cx);
5478    }
5479
5480    fn parse(input: &'static str, window: &mut Window, cx: &mut App) -> SettingsWindow {
5481        struct PageBuilder {
5482            title: &'static str,
5483            items: Vec<SettingsPageItem>,
5484        }
5485        let mut page_builders: Vec<PageBuilder> = Vec::new();
5486        let mut expanded_pages = Vec::new();
5487        let mut selected_idx = None;
5488        let mut index = 0;
5489        let mut in_expanded_section = false;
5490
5491        for mut line in input
5492            .lines()
5493            .map(|line| line.trim())
5494            .filter(|line| !line.is_empty())
5495        {
5496            if let Some(pre) = line.strip_suffix('*') {
5497                assert!(selected_idx.is_none(), "Only one selected entry allowed");
5498                selected_idx = Some(index);
5499                line = pre;
5500            }
5501            let (kind, title) = line.split_once(" ").unwrap();
5502            assert_eq!(kind.len(), 1);
5503            let kind = kind.chars().next().unwrap();
5504            if kind == 'v' {
5505                let page_idx = page_builders.len();
5506                expanded_pages.push(page_idx);
5507                page_builders.push(PageBuilder {
5508                    title,
5509                    items: vec![],
5510                });
5511                index += 1;
5512                in_expanded_section = true;
5513            } else if kind == '>' {
5514                page_builders.push(PageBuilder {
5515                    title,
5516                    items: vec![],
5517                });
5518                index += 1;
5519                in_expanded_section = false;
5520            } else if kind == '-' {
5521                page_builders
5522                    .last_mut()
5523                    .unwrap()
5524                    .items
5525                    .push(SettingsPageItem::SectionHeader(title));
5526                if selected_idx == Some(index) && !in_expanded_section {
5527                    panic!("Items in unexpanded sections cannot be selected");
5528                }
5529                index += 1;
5530            } else {
5531                panic!(
5532                    "Entries must start with one of 'v', '>', or '-'\n line: {}",
5533                    line
5534                );
5535            }
5536        }
5537
5538        let pages: Vec<SettingsPage> = page_builders
5539            .into_iter()
5540            .map(|builder| SettingsPage {
5541                title: builder.title,
5542                items: builder.items.into_boxed_slice(),
5543            })
5544            .collect();
5545
5546        let mut settings_window = SettingsWindow {
5547            title_bar: None,
5548            original_window: None,
5549            worktree_root_dirs: HashMap::default(),
5550            files: Vec::default(),
5551            current_file: crate::SettingsUiFile::User,
5552            project_setting_file_buffers: HashMap::default(),
5553            pages,
5554            search_bar: cx.new(|cx| Editor::single_line(window, cx)),
5555            navbar_entry: selected_idx.expect("Must have a selected navbar entry"),
5556            navbar_entries: Vec::default(),
5557            navbar_scroll_handle: UniformListScrollHandle::default(),
5558            navbar_focus_subscriptions: vec![],
5559            filter_table: vec![],
5560            sub_page_stack: vec![],
5561            opening_link: false,
5562            has_query: false,
5563            content_handles: vec![],
5564            search_task: None,
5565            focus_handle: cx.focus_handle(),
5566            navbar_focus_handle: NonFocusableHandle::new(
5567                NAVBAR_CONTAINER_TAB_INDEX,
5568                false,
5569                window,
5570                cx,
5571            ),
5572            content_focus_handle: NonFocusableHandle::new(
5573                CONTENT_CONTAINER_TAB_INDEX,
5574                false,
5575                window,
5576                cx,
5577            ),
5578            files_focus_handle: cx.focus_handle(),
5579            search_index: None,
5580            list_state: ListState::new(0, gpui::ListAlignment::Top, px(0.0)),
5581            shown_errors: HashSet::default(),
5582            hidden_deleted_skill_directory_paths: HashSet::default(),
5583            regex_validation_error: None,
5584            sandbox_host_validation_error: None,
5585            last_copied_link_path: None,
5586            provider_configuration_views: HashMap::default(),
5587            configuring_provider: None,
5588            last_copied_skill_directory_path: None,
5589            llm_provider_form: None,
5590            llm_provider_add_focus_handle: cx.focus_handle(),
5591            mcp_server_form: None,
5592            mcp_add_server_focus_handle: cx.focus_handle(),
5593            custom_agent_form: None,
5594            external_agent_add_focus_handle: cx.focus_handle(),
5595            harness_maintenance: Default::default(),
5596            harness_maintenance_task: None,
5597            harness_maintenance_error: None,
5598            skill_creator_page: None,
5599        };
5600
5601        settings_window.build_filter_table();
5602        settings_window.build_navbar(cx);
5603        for expanded_page_index in expanded_pages {
5604            for entry in &mut settings_window.navbar_entries {
5605                if entry.page_index == expanded_page_index && entry.is_root {
5606                    entry.expanded = true;
5607                }
5608            }
5609        }
5610        settings_window
5611    }
5612
5613    #[track_caller]
5614    fn check_navbar_toggle(
5615        before: &'static str,
5616        toggle_page: &'static str,
5617        after: &'static str,
5618        window: &mut Window,
5619        cx: &mut App,
5620    ) {
5621        let mut settings_window = parse(before, window, cx);
5622        let toggle_page_idx = settings_window
5623            .pages
5624            .iter()
5625            .position(|page| page.title == toggle_page)
5626            .expect("page not found");
5627        let toggle_idx = settings_window
5628            .navbar_entries
5629            .iter()
5630            .position(|entry| entry.page_index == toggle_page_idx)
5631            .expect("page not found");
5632        settings_window.toggle_navbar_entry(toggle_idx);
5633
5634        let expected_settings_window = parse(after, window, cx);
5635
5636        pretty_assertions::assert_eq!(
5637            settings_window
5638                .visible_navbar_entries()
5639                .map(|(_, entry)| entry)
5640                .collect::<Vec<_>>(),
5641            expected_settings_window
5642                .visible_navbar_entries()
5643                .map(|(_, entry)| entry)
5644                .collect::<Vec<_>>(),
5645        );
5646        pretty_assertions::assert_eq!(
5647            settings_window.navbar_entries[settings_window.navbar_entry()],
5648            expected_settings_window.navbar_entries[expected_settings_window.navbar_entry()],
5649        );
5650    }
5651
5652    macro_rules! check_navbar_toggle {
5653        ($name:ident, before: $before:expr, toggle_page: $toggle_page:expr, after: $after:expr) => {
5654            #[gpui::test]
5655            fn $name(cx: &mut gpui::TestAppContext) {
5656                let window = cx.add_empty_window();
5657                window.update(|window, cx| {
5658                    register_settings(cx);
5659                    check_navbar_toggle($before, $toggle_page, $after, window, cx);
5660                });
5661            }
5662        };
5663    }
5664
5665    check_navbar_toggle!(
5666        navbar_basic_open,
5667        before: r"
5668        v General
5669        - General
5670        - Privacy*
5671        v Project
5672        - Project Settings
5673        ",
5674        toggle_page: "General",
5675        after: r"
5676        > General*
5677        v Project
5678        - Project Settings
5679        "
5680    );
5681
5682    check_navbar_toggle!(
5683        navbar_basic_close,
5684        before: r"
5685        > General*
5686        - General
5687        - Privacy
5688        v Project
5689        - Project Settings
5690        ",
5691        toggle_page: "General",
5692        after: r"
5693        v General*
5694        - General
5695        - Privacy
5696        v Project
5697        - Project Settings
5698        "
5699    );
5700
5701    check_navbar_toggle!(
5702        navbar_basic_second_root_entry_close,
5703        before: r"
5704        > General
5705        - General
5706        - Privacy
5707        v Project
5708        - Project Settings*
5709        ",
5710        toggle_page: "Project",
5711        after: r"
5712        > General
5713        > Project*
5714        "
5715    );
5716
5717    check_navbar_toggle!(
5718        navbar_toggle_subroot,
5719        before: r"
5720        v General Page
5721        - General
5722        - Privacy
5723        v Project
5724        - Worktree Settings Content*
5725        v AI
5726        - General
5727        > Appearance & Behavior
5728        ",
5729        toggle_page: "Project",
5730        after: r"
5731        v General Page
5732        - General
5733        - Privacy
5734        > Project*
5735        v AI
5736        - General
5737        > Appearance & Behavior
5738        "
5739    );
5740
5741    check_navbar_toggle!(
5742        navbar_toggle_close_propagates_selected_index,
5743        before: r"
5744        v General Page
5745        - General
5746        - Privacy
5747        v Project
5748        - Worktree Settings Content
5749        v AI
5750        - General*
5751        > Appearance & Behavior
5752        ",
5753        toggle_page: "General Page",
5754        after: r"
5755        > General Page*
5756        v Project
5757        - Worktree Settings Content
5758        v AI
5759        - General
5760        > Appearance & Behavior
5761        "
5762    );
5763
5764    check_navbar_toggle!(
5765        navbar_toggle_expand_propagates_selected_index,
5766        before: r"
5767        > General Page
5768        - General
5769        - Privacy
5770        v Project
5771        - Worktree Settings Content
5772        v AI
5773        - General*
5774        > Appearance & Behavior
5775        ",
5776        toggle_page: "General Page",
5777        after: r"
5778        v General Page*
5779        - General
5780        - Privacy
5781        v Project
5782        - Worktree Settings Content
5783        v AI
5784        - General
5785        > Appearance & Behavior
5786        "
5787    );
5788
5789    #[gpui::test]
5790    fn navbar_double_click_toggle(cx: &mut gpui::TestAppContext) {
5791        let (settings_window, cx) = cx.add_window_view(|window, cx| {
5792            register_settings(cx);
5793            let mut settings_window = parse(
5794                r"
5795                > General*
5796                - General
5797                - Privacy
5798                v Project
5799                - Project Settings
5800                ",
5801                window,
5802                cx,
5803            );
5804            settings_window.build_content_handles(window, cx);
5805            settings_window
5806        });
5807
5808        settings_window.update_in(cx, |settings_window, window, cx| {
5809            let general_idx = settings_window
5810                .navbar_entries
5811                .iter()
5812                .position(|entry| entry.title == "General" && entry.is_root)
5813                .expect("General root entry should exist");
5814            let privacy_idx = settings_window
5815                .navbar_entries
5816                .iter()
5817                .position(|entry| entry.title == "Privacy" && !entry.is_root)
5818                .expect("Privacy nested entry should exist");
5819
5820            let click_event = |click_count| {
5821                gpui::ClickEvent::Mouse(gpui::MouseClickEvent {
5822                    down: gpui::MouseDownEvent {
5823                        button: gpui::MouseButton::Left,
5824                        click_count,
5825                        ..Default::default()
5826                    },
5827                    up: gpui::MouseUpEvent {
5828                        button: gpui::MouseButton::Left,
5829                        click_count,
5830                        ..Default::default()
5831                    },
5832                })
5833            };
5834
5835            assert!(
5836                !settings_window.toggle_navbar_entry_on_double_click(
5837                    general_idx,
5838                    &click_event(1),
5839                    window,
5840                    cx,
5841                ),
5842                "single-clicks should use the normal navigation path"
5843            );
5844            assert!(!settings_window.navbar_entries[general_idx].expanded);
5845
5846            assert!(settings_window.toggle_navbar_entry_on_double_click(
5847                general_idx,
5848                &click_event(2),
5849                window,
5850                cx,
5851            ));
5852            assert!(settings_window.navbar_entries[general_idx].expanded);
5853
5854            assert!(
5855                !settings_window.toggle_navbar_entry_on_double_click(
5856                    general_idx,
5857                    &click_event(3),
5858                    window,
5859                    cx,
5860                ),
5861                "triple-clicks should not toggle the entry again"
5862            );
5863            assert!(settings_window.navbar_entries[general_idx].expanded);
5864
5865            assert!(!settings_window.toggle_navbar_entry_on_double_click(
5866                privacy_idx,
5867                &click_event(2),
5868                window,
5869                cx,
5870            ));
5871        });
5872    }
5873
5874    #[gpui::test]
5875    async fn test_settings_window_shows_worktrees_from_multiple_workspaces(
5876        cx: &mut gpui::TestAppContext,
5877    ) {
5878        use project::Project;
5879        use serde_json::json;
5880
5881        cx.update(|cx| {
5882            register_settings(cx);
5883        });
5884
5885        let app_state = cx.update(|cx| {
5886            let app_state = AppState::test(cx);
5887            AppState::set_global(app_state.clone(), cx);
5888            app_state
5889        });
5890
5891        let fake_fs = app_state.fs.as_fake();
5892
5893        fake_fs
5894            .insert_tree(
5895                "/workspace1",
5896                json!({
5897                    "worktree_a": {
5898                        "file1.rs": "fn main() {}"
5899                    },
5900                    "worktree_b": {
5901                        "file2.rs": "fn test() {}"
5902                    }
5903                }),
5904            )
5905            .await;
5906
5907        fake_fs
5908            .insert_tree(
5909                "/workspace2",
5910                json!({
5911                    "worktree_c": {
5912                        "file3.rs": "fn foo() {}"
5913                    }
5914                }),
5915            )
5916            .await;
5917
5918        let project1 = cx.update(|cx| {
5919            Project::local(
5920                app_state.client.clone(),
5921                app_state.node_runtime.clone(),
5922                app_state.user_store.clone(),
5923                app_state.languages.clone(),
5924                app_state.fs.clone(),
5925                None,
5926                project::LocalProjectFlags::default(),
5927                cx,
5928            )
5929        });
5930
5931        project1
5932            .update(cx, |project, cx| {
5933                project.find_or_create_worktree("/workspace1/worktree_a", true, cx)
5934            })
5935            .await
5936            .expect("Failed to create worktree_a");
5937        project1
5938            .update(cx, |project, cx| {
5939                project.find_or_create_worktree("/workspace1/worktree_b", true, cx)
5940            })
5941            .await
5942            .expect("Failed to create worktree_b");
5943
5944        let project2 = cx.update(|cx| {
5945            Project::local(
5946                app_state.client.clone(),
5947                app_state.node_runtime.clone(),
5948                app_state.user_store.clone(),
5949                app_state.languages.clone(),
5950                app_state.fs.clone(),
5951                None,
5952                project::LocalProjectFlags::default(),
5953                cx,
5954            )
5955        });
5956
5957        project2
5958            .update(cx, |project, cx| {
5959                project.find_or_create_worktree("/workspace2/worktree_c", true, cx)
5960            })
5961            .await
5962            .expect("Failed to create worktree_c");
5963
5964        let (_multi_workspace1, cx) = cx.add_window_view(|window, cx| {
5965            let workspace = cx.new(|cx| {
5966                Workspace::new(
5967                    Default::default(),
5968                    project1.clone(),
5969                    app_state.clone(),
5970                    window,
5971                    cx,
5972                )
5973            });
5974            MultiWorkspace::new(workspace, window, cx)
5975        });
5976
5977        let (_multi_workspace2, cx) = cx.add_window_view(|window, cx| {
5978            let workspace = cx.new(|cx| {
5979                Workspace::new(
5980                    Default::default(),
5981                    project2.clone(),
5982                    app_state.clone(),
5983                    window,
5984                    cx,
5985                )
5986            });
5987            MultiWorkspace::new(workspace, window, cx)
5988        });
5989
5990        let workspace2_handle = cx.window_handle().downcast::<MultiWorkspace>().unwrap();
5991
5992        cx.run_until_parked();
5993
5994        let (settings_window, cx) = cx
5995            .add_window_view(|window, cx| SettingsWindow::new(Some(workspace2_handle), window, cx));
5996
5997        cx.run_until_parked();
5998
5999        settings_window.read_with(cx, |settings_window, _| {
6000            let worktree_names: Vec<_> = settings_window
6001                .worktree_root_dirs
6002                .values()
6003                .cloned()
6004                .collect();
6005
6006            assert!(
6007                worktree_names.iter().any(|name| name == "worktree_a"),
6008                "Should contain worktree_a from workspace1, but found: {:?}",
6009                worktree_names
6010            );
6011            assert!(
6012                worktree_names.iter().any(|name| name == "worktree_b"),
6013                "Should contain worktree_b from workspace1, but found: {:?}",
6014                worktree_names
6015            );
6016            assert!(
6017                worktree_names.iter().any(|name| name == "worktree_c"),
6018                "Should contain worktree_c from workspace2, but found: {:?}",
6019                worktree_names
6020            );
6021
6022            assert_eq!(
6023                worktree_names.len(),
6024                3,
6025                "Should have exactly 3 worktrees from both workspaces, but found: {:?}",
6026                worktree_names
6027            );
6028
6029            let project_files: Vec<_> = settings_window
6030                .files
6031                .iter()
6032                .filter_map(|(f, _)| match f {
6033                    SettingsUiFile::Project((worktree_id, _)) => Some(*worktree_id),
6034                    _ => None,
6035                })
6036                .collect();
6037
6038            let unique_project_files: std::collections::HashSet<_> = project_files.iter().collect();
6039            assert_eq!(
6040                project_files.len(),
6041                unique_project_files.len(),
6042                "Should have no duplicate project files, but found duplicates. All files: {:?}",
6043                project_files
6044            );
6045        });
6046    }
6047
6048    #[gpui::test]
6049    async fn test_settings_window_updates_when_new_workspace_created(
6050        cx: &mut gpui::TestAppContext,
6051    ) {
6052        use project::Project;
6053        use serde_json::json;
6054
6055        cx.update(|cx| {
6056            register_settings(cx);
6057        });
6058
6059        let app_state = cx.update(|cx| {
6060            let app_state = AppState::test(cx);
6061            AppState::set_global(app_state.clone(), cx);
6062            app_state
6063        });
6064
6065        let fake_fs = app_state.fs.as_fake();
6066
6067        fake_fs
6068            .insert_tree(
6069                "/workspace1",
6070                json!({
6071                    "worktree_a": {
6072                        "file1.rs": "fn main() {}"
6073                    }
6074                }),
6075            )
6076            .await;
6077
6078        fake_fs
6079            .insert_tree(
6080                "/workspace2",
6081                json!({
6082                    "worktree_b": {
6083                        "file2.rs": "fn test() {}"
6084                    }
6085                }),
6086            )
6087            .await;
6088
6089        let project1 = cx.update(|cx| {
6090            Project::local(
6091                app_state.client.clone(),
6092                app_state.node_runtime.clone(),
6093                app_state.user_store.clone(),
6094                app_state.languages.clone(),
6095                app_state.fs.clone(),
6096                None,
6097                project::LocalProjectFlags::default(),
6098                cx,
6099            )
6100        });
6101
6102        project1
6103            .update(cx, |project, cx| {
6104                project.find_or_create_worktree("/workspace1/worktree_a", true, cx)
6105            })
6106            .await
6107            .expect("Failed to create worktree_a");
6108
6109        let (_multi_workspace1, cx) = cx.add_window_view(|window, cx| {
6110            let workspace = cx.new(|cx| {
6111                Workspace::new(
6112                    Default::default(),
6113                    project1.clone(),
6114                    app_state.clone(),
6115                    window,
6116                    cx,
6117                )
6118            });
6119            MultiWorkspace::new(workspace, window, cx)
6120        });
6121
6122        let workspace1_handle = cx.window_handle().downcast::<MultiWorkspace>().unwrap();
6123
6124        cx.run_until_parked();
6125
6126        let (settings_window, cx) = cx
6127            .add_window_view(|window, cx| SettingsWindow::new(Some(workspace1_handle), window, cx));
6128
6129        cx.run_until_parked();
6130
6131        settings_window.read_with(cx, |settings_window, _| {
6132            assert_eq!(
6133                settings_window.worktree_root_dirs.len(),
6134                1,
6135                "Should have 1 worktree initially"
6136            );
6137        });
6138
6139        let project2 = cx.update(|_, cx| {
6140            Project::local(
6141                app_state.client.clone(),
6142                app_state.node_runtime.clone(),
6143                app_state.user_store.clone(),
6144                app_state.languages.clone(),
6145                app_state.fs.clone(),
6146                None,
6147                project::LocalProjectFlags::default(),
6148                cx,
6149            )
6150        });
6151
6152        project2
6153            .update(&mut cx.cx, |project, cx| {
6154                project.find_or_create_worktree("/workspace2/worktree_b", true, cx)
6155            })
6156            .await
6157            .expect("Failed to create worktree_b");
6158
6159        let (_multi_workspace2, cx) = cx.add_window_view(|window, cx| {
6160            let workspace = cx.new(|cx| {
6161                Workspace::new(
6162                    Default::default(),
6163                    project2.clone(),
6164                    app_state.clone(),
6165                    window,
6166                    cx,
6167                )
6168            });
6169            MultiWorkspace::new(workspace, window, cx)
6170        });
6171
6172        cx.run_until_parked();
6173
6174        settings_window.read_with(cx, |settings_window, _| {
6175            let worktree_names: Vec<_> = settings_window
6176                .worktree_root_dirs
6177                .values()
6178                .cloned()
6179                .collect();
6180
6181            assert!(
6182                worktree_names.iter().any(|name| name == "worktree_a"),
6183                "Should contain worktree_a, but found: {:?}",
6184                worktree_names
6185            );
6186            assert!(
6187                worktree_names.iter().any(|name| name == "worktree_b"),
6188                "Should contain worktree_b from newly created workspace, but found: {:?}",
6189                worktree_names
6190            );
6191
6192            assert_eq!(
6193                worktree_names.len(),
6194                2,
6195                "Should have 2 worktrees after new workspace created, but found: {:?}",
6196                worktree_names
6197            );
6198
6199            let project_files: Vec<_> = settings_window
6200                .files
6201                .iter()
6202                .filter_map(|(f, _)| match f {
6203                    SettingsUiFile::Project((worktree_id, _)) => Some(*worktree_id),
6204                    _ => None,
6205                })
6206                .collect();
6207
6208            let unique_project_files: std::collections::HashSet<_> = project_files.iter().collect();
6209            assert_eq!(
6210                project_files.len(),
6211                unique_project_files.len(),
6212                "Should have no duplicate project files, but found duplicates. All files: {:?}",
6213                project_files
6214            );
6215        });
6216    }
6217
6218    #[gpui::test]
6219    async fn test_skills_page_scope_switch_updates_displayed_skills(cx: &mut gpui::TestAppContext) {
6220        use agent_skills::{
6221            ProjectSkillGroup, Skill, SkillScopeId, SkillSource, load_skills_from_directory,
6222        };
6223        use project::Project;
6224        use serde_json::json;
6225        use std::path::Path;
6226
6227        cx.update(|cx| {
6228            register_settings(cx);
6229        });
6230
6231        let app_state = cx.update(|cx| {
6232            let app_state = AppState::test(cx);
6233            AppState::set_global(app_state.clone(), cx);
6234            app_state
6235        });
6236
6237        let fake_fs = app_state.fs.as_fake();
6238
6239        fake_fs
6240            .insert_tree(
6241                "/global-skills",
6242                json!({
6243                    "global-skill": {
6244                        "SKILL.md": "---\nname: global-skill\ndescription: A user level skill\n---\n\nGlobal instructions."
6245                    }
6246                }),
6247            )
6248            .await;
6249
6250        fake_fs
6251            .insert_tree(
6252                "/project",
6253                json!({
6254                    ".agents": {
6255                        "skills": {
6256                            "project-skill": {
6257                                "SKILL.md": "---\nname: project-skill\ndescription: A project level skill\n---\n\nProject instructions."
6258                            }
6259                        }
6260                    },
6261                    "main.rs": "fn main() {}"
6262                }),
6263            )
6264            .await;
6265
6266        let project = cx.update(|cx| {
6267            Project::local(
6268                app_state.client.clone(),
6269                app_state.node_runtime.clone(),
6270                app_state.user_store.clone(),
6271                app_state.languages.clone(),
6272                app_state.fs.clone(),
6273                None,
6274                project::LocalProjectFlags::default(),
6275                cx,
6276            )
6277        });
6278
6279        let (worktree, _) = project
6280            .update(cx, |project, cx| {
6281                project.find_or_create_worktree("/project", true, cx)
6282            })
6283            .await
6284            .expect("Failed to create worktree");
6285        let worktree_id = worktree.read_with(cx, |worktree, _| worktree.id());
6286
6287        // Load both skills from the fake filesystem the same way the agent
6288        // does, then publish them as the global skill index.
6289        let fs = app_state.fs.clone();
6290        let global_skills: Vec<Skill> =
6291            load_skills_from_directory(&fs, Path::new("/global-skills"), SkillSource::Global)
6292                .await
6293                .into_iter()
6294                .map(|result| result.expect("global skill should load"))
6295                .collect();
6296        let project_skills: Vec<Skill> = load_skills_from_directory(
6297            &fs,
6298            Path::new("/project/.agents/skills"),
6299            SkillSource::ProjectLocal {
6300                worktree_id: SkillScopeId(worktree_id.to_usize()),
6301                worktree_root_name: "project".into(),
6302            },
6303        )
6304        .await
6305        .into_iter()
6306        .map(|result| result.expect("project skill should load"))
6307        .collect();
6308        assert_eq!(global_skills.len(), 1);
6309        assert_eq!(project_skills.len(), 1);
6310
6311        cx.update(|cx| {
6312            cx.set_global(SkillIndex {
6313                global_skills,
6314                project_skills: vec![ProjectSkillGroup {
6315                    worktree_id: SkillScopeId(worktree_id.to_usize()),
6316                    worktree_root_name: "project".into(),
6317                    skills: project_skills,
6318                }],
6319            });
6320        });
6321
6322        let (_multi_workspace, cx) = cx.add_window_view(|window, cx| {
6323            let workspace = cx.new(|cx| {
6324                Workspace::new(
6325                    Default::default(),
6326                    project.clone(),
6327                    app_state.clone(),
6328                    window,
6329                    cx,
6330                )
6331            });
6332            MultiWorkspace::new(workspace, window, cx)
6333        });
6334        let workspace_handle = cx.window_handle().downcast::<MultiWorkspace>().unwrap();
6335
6336        cx.run_until_parked();
6337
6338        let (settings_window, cx) = cx
6339            .add_window_view(|window, cx| SettingsWindow::new(Some(workspace_handle), window, cx));
6340
6341        cx.run_until_parked();
6342
6343        settings_window.update_in(cx, |settings_window, window, cx| {
6344            fn displayed_skill_names(settings_window: &SettingsWindow, cx: &App) -> Vec<String> {
6345                crate::pages::displayed_skills(settings_window, cx)
6346                    .iter()
6347                    .map(|skill| skill.name.to_string())
6348                    .collect()
6349            }
6350
6351            assert_eq!(settings_window.current_file, SettingsUiFile::User);
6352            assert!(
6353                settings_window.navigate_to_sub_page(AGENT_SKILLS_SETTINGS_PATH, window, cx),
6354                "Skills sub-page should exist"
6355            );
6356            assert_eq!(displayed_skill_names(settings_window, cx), ["global-skill"]);
6357
6358            let project_file_index = settings_window
6359                .files
6360                .iter()
6361                .position(|(file, _)| file.worktree_id() == Some(worktree_id))
6362                .expect("project settings file should be listed");
6363            settings_window.change_file_in_sub_page(project_file_index, window, cx);
6364
6365            assert_eq!(
6366                settings_window.current_file.worktree_id(),
6367                Some(worktree_id)
6368            );
6369            assert_eq!(
6370                settings_window.sub_page_stack.len(),
6371                1,
6372                "Skills sub-page should stay open when switching scope"
6373            );
6374            assert_eq!(settings_window.sub_page_stack[0].link.title, "Skills");
6375            assert_eq!(
6376                displayed_skill_names(settings_window, cx),
6377                ["project-skill"]
6378            );
6379
6380            let user_file_index = settings_window
6381                .files
6382                .iter()
6383                .position(|(file, _)| file == &SettingsUiFile::User)
6384                .expect("user settings file should be listed");
6385            settings_window.change_file_in_sub_page(user_file_index, window, cx);
6386
6387            assert_eq!(settings_window.current_file, SettingsUiFile::User);
6388            assert_eq!(settings_window.sub_page_stack.len(), 1);
6389            assert_eq!(displayed_skill_names(settings_window, cx), ["global-skill"]);
6390        });
6391    }
6392
6393    #[gpui::test]
6394    async fn test_open_skill_creator_navigates_to_sub_page(cx: &mut gpui::TestAppContext) {
6395        use project::Project;
6396
6397        cx.update(|cx| {
6398            register_settings(cx);
6399        });
6400
6401        let app_state = cx.update(|cx| {
6402            let app_state = AppState::test(cx);
6403            AppState::set_global(app_state.clone(), cx);
6404            app_state
6405        });
6406
6407        app_state
6408            .fs
6409            .as_fake()
6410            .insert_tree("/project", serde_json::json!({ "main.rs": "fn main() {}" }))
6411            .await;
6412
6413        let project = cx.update(|cx| {
6414            Project::local(
6415                app_state.client.clone(),
6416                app_state.node_runtime.clone(),
6417                app_state.user_store.clone(),
6418                app_state.languages.clone(),
6419                app_state.fs.clone(),
6420                None,
6421                project::LocalProjectFlags::default(),
6422                cx,
6423            )
6424        });
6425        project
6426            .update(cx, |project, cx| {
6427                project.find_or_create_worktree("/project", true, cx)
6428            })
6429            .await
6430            .expect("Failed to create worktree");
6431
6432        let (_multi_workspace, cx) = cx.add_window_view(|window, cx| {
6433            let workspace = cx.new(|cx| {
6434                Workspace::new(
6435                    Default::default(),
6436                    project.clone(),
6437                    app_state.clone(),
6438                    window,
6439                    cx,
6440                )
6441            });
6442            MultiWorkspace::new(workspace, window, cx)
6443        });
6444        let workspace_handle = cx.window_handle().downcast::<MultiWorkspace>().unwrap();
6445
6446        cx.run_until_parked();
6447
6448        let (settings_window, cx) = cx
6449            .add_window_view(|window, cx| SettingsWindow::new(Some(workspace_handle), window, cx));
6450
6451        cx.run_until_parked();
6452
6453        settings_window.update_in(cx, |settings_window, window, cx| {
6454            settings_window.navigate_to_skill_creator(
6455                pages::SkillCreatorOpenMode::Form,
6456                window,
6457                cx,
6458            );
6459        });
6460
6461        cx.run_until_parked();
6462
6463        settings_window.read_with(cx, |settings_window, _| {
6464            let titles: Vec<_> = settings_window
6465                .sub_page_stack
6466                .iter()
6467                .map(|sub_page| sub_page.link.title.to_string())
6468                .collect();
6469            assert_eq!(
6470                titles,
6471                ["Skills", "Create Skill"],
6472                "skill creator should be pushed on top of the skills page"
6473            );
6474            assert!(
6475                settings_window.skill_creator_page().is_some(),
6476                "skill creator page state should exist"
6477            );
6478        });
6479    }
6480
6481    #[gpui::test]
6482    async fn test_open_skill_creator_action_opens_settings_window_at_sub_page(
6483        cx: &mut gpui::TestAppContext,
6484    ) {
6485        use project::Project;
6486
6487        cx.update(|cx| {
6488            register_settings(cx);
6489            release_channel::init("0.0.0".parse().unwrap(), cx);
6490            crate::init(cx);
6491        });
6492
6493        let app_state = cx.update(|cx| {
6494            let app_state = AppState::test(cx);
6495            AppState::set_global(app_state.clone(), cx);
6496            app_state
6497        });
6498
6499        app_state
6500            .fs
6501            .as_fake()
6502            .insert_tree("/project", serde_json::json!({ "main.rs": "fn main() {}" }))
6503            .await;
6504
6505        let project = cx.update(|cx| {
6506            Project::local(
6507                app_state.client.clone(),
6508                app_state.node_runtime.clone(),
6509                app_state.user_store.clone(),
6510                app_state.languages.clone(),
6511                app_state.fs.clone(),
6512                None,
6513                project::LocalProjectFlags::default(),
6514                cx,
6515            )
6516        });
6517        project
6518            .update(cx, |project, cx| {
6519                project.find_or_create_worktree("/project", true, cx)
6520            })
6521            .await
6522            .expect("Failed to create worktree");
6523
6524        let (multi_workspace, cx) = cx.add_window_view(|window, cx| {
6525            let workspace = cx.new(|cx| {
6526                Workspace::new(
6527                    Default::default(),
6528                    project.clone(),
6529                    app_state.clone(),
6530                    window,
6531                    cx,
6532                )
6533            });
6534            MultiWorkspace::new(workspace, window, cx)
6535        });
6536
6537        cx.run_until_parked();
6538
6539        // Dispatch the action the way the command palette does: on the
6540        // workspace window.
6541        multi_workspace.update_in(cx, |_multi_workspace, window, cx| {
6542            window.dispatch_action(Box::new(zed_actions::assistant::OpenSkillCreator), cx);
6543        });
6544
6545        cx.run_until_parked();
6546
6547        let settings_window = cx
6548            .update(|_, cx| {
6549                cx.windows()
6550                    .into_iter()
6551                    .find_map(|window| window.downcast::<SettingsWindow>())
6552            })
6553            .expect("dispatching agent::OpenSkillCreator should open the settings window");
6554
6555        settings_window
6556            .read_with(cx, |settings_window, _| {
6557                let titles: Vec<_> = settings_window
6558                    .sub_page_stack
6559                    .iter()
6560                    .map(|sub_page| sub_page.link.title.to_string())
6561                    .collect();
6562                assert_eq!(
6563                    titles,
6564                    ["Skills", "Create Skill"],
6565                    "skill creator should be pushed on top of the skills page"
6566                );
6567            })
6568            .unwrap();
6569    }
6570}
6571
6572#[cfg(test)]
6573mod project_settings_update_tests {
6574    use super::*;
6575    use fs::{FakeFs, Fs as _};
6576    use gpui::TestAppContext;
6577    use project::Project;
6578    use serde_json::json;
6579    use std::sync::atomic::{AtomicUsize, Ordering};
6580
6581    struct TestSetup {
6582        fs: Arc<FakeFs>,
6583        project: Entity<Project>,
6584        worktree_id: WorktreeId,
6585        worktree: WeakEntity<Worktree>,
6586        rel_path: Arc<RelPath>,
6587        project_path: ProjectPath,
6588    }
6589
6590    async fn init_test(cx: &mut TestAppContext, initial_settings: Option<&str>) -> TestSetup {
6591        cx.update(|cx| {
6592            let store = settings::SettingsStore::test(cx);
6593            cx.set_global(store);
6594            theme_settings::init(theme::LoadThemes::JustBase, cx);
6595            editor::init(cx);
6596            menu::init();
6597            let queue = ProjectSettingsUpdateQueue::new(cx);
6598            cx.set_global(queue);
6599        });
6600
6601        let fs = FakeFs::new(cx.executor());
6602        let tree = if let Some(settings_content) = initial_settings {
6603            json!({
6604                ".zed": {
6605                    "settings.json": settings_content
6606                },
6607                "src": { "main.rs": "" }
6608            })
6609        } else {
6610            json!({ "src": { "main.rs": "" } })
6611        };
6612        fs.insert_tree("/project", tree).await;
6613
6614        let project = Project::test(fs.clone(), ["/project".as_ref()], cx).await;
6615
6616        let (worktree_id, worktree) = project.read_with(cx, |project, cx| {
6617            let worktree = project.worktrees(cx).next().unwrap();
6618            (worktree.read(cx).id(), worktree.downgrade())
6619        });
6620
6621        let rel_path: Arc<RelPath> = RelPath::from_unix_str(".zed/settings.json")
6622            .expect("valid path")
6623            .into_arc();
6624        let project_path = ProjectPath {
6625            worktree_id,
6626            path: rel_path.clone(),
6627        };
6628
6629        TestSetup {
6630            fs,
6631            project,
6632            worktree_id,
6633            worktree,
6634            rel_path,
6635            project_path,
6636        }
6637    }
6638
6639    #[gpui::test]
6640    async fn test_creates_settings_file_if_missing(cx: &mut TestAppContext) {
6641        let setup = init_test(cx, None).await;
6642
6643        let entry = ProjectSettingsUpdateEntry {
6644            worktree_id: setup.worktree_id,
6645            rel_path: setup.rel_path.clone(),
6646            settings_window: WeakEntity::new_invalid(),
6647            project: setup.project.downgrade(),
6648            worktree: setup.worktree,
6649            update: Box::new(|content, _cx| {
6650                content.project.all_languages.defaults.tab_size = Some(NonZeroU32::new(4).unwrap());
6651            }),
6652        };
6653
6654        cx.update(|cx| ProjectSettingsUpdateQueue::enqueue(cx, entry));
6655        cx.executor().run_until_parked();
6656
6657        let buffer_store = setup
6658            .project
6659            .read_with(cx, |project, _| project.buffer_store().clone());
6660        let buffer = buffer_store
6661            .update(cx, |store, cx| store.open_buffer(setup.project_path, cx))
6662            .await
6663            .expect("buffer should exist");
6664
6665        let text = buffer.read_with(cx, |buffer, _| buffer.text());
6666        assert!(
6667            text.contains("\"tab_size\": 4"),
6668            "Expected tab_size setting in: {}",
6669            text
6670        );
6671    }
6672
6673    #[gpui::test]
6674    async fn test_updates_existing_settings_file(cx: &mut TestAppContext) {
6675        let setup = init_test(cx, Some(r#"{ "tab_size": 2 }"#)).await;
6676
6677        let entry = ProjectSettingsUpdateEntry {
6678            worktree_id: setup.worktree_id,
6679            rel_path: setup.rel_path.clone(),
6680            settings_window: WeakEntity::new_invalid(),
6681            project: setup.project.downgrade(),
6682            worktree: setup.worktree,
6683            update: Box::new(|content, _cx| {
6684                content.project.all_languages.defaults.tab_size = Some(NonZeroU32::new(8).unwrap());
6685            }),
6686        };
6687
6688        cx.update(|cx| ProjectSettingsUpdateQueue::enqueue(cx, entry));
6689        cx.executor().run_until_parked();
6690
6691        let buffer_store = setup
6692            .project
6693            .read_with(cx, |project, _| project.buffer_store().clone());
6694        let buffer = buffer_store
6695            .update(cx, |store, cx| store.open_buffer(setup.project_path, cx))
6696            .await
6697            .expect("buffer should exist");
6698
6699        let text = buffer.read_with(cx, |buffer, _| buffer.text());
6700        assert!(
6701            text.contains("\"tab_size\": 8"),
6702            "Expected updated tab_size in: {}",
6703            text
6704        );
6705    }
6706
6707    #[gpui::test]
6708    async fn test_updates_are_serialized(cx: &mut TestAppContext) {
6709        let setup = init_test(cx, Some("{}")).await;
6710
6711        let update_order = Arc::new(std::sync::Mutex::new(Vec::new()));
6712
6713        for i in 1..=3 {
6714            let update_order = update_order.clone();
6715            let entry = ProjectSettingsUpdateEntry {
6716                worktree_id: setup.worktree_id,
6717                rel_path: setup.rel_path.clone(),
6718                settings_window: WeakEntity::new_invalid(),
6719                project: setup.project.downgrade(),
6720                worktree: setup.worktree.clone(),
6721                update: Box::new(move |content, _cx| {
6722                    update_order.lock().unwrap().push(i);
6723                    content.project.all_languages.defaults.tab_size =
6724                        Some(NonZeroU32::new(i).unwrap());
6725                }),
6726            };
6727            cx.update(|cx| ProjectSettingsUpdateQueue::enqueue(cx, entry));
6728        }
6729
6730        cx.executor().run_until_parked();
6731
6732        let order = update_order.lock().unwrap().clone();
6733        assert_eq!(order, vec![1, 2, 3], "Updates should be processed in order");
6734
6735        let buffer_store = setup
6736            .project
6737            .read_with(cx, |project, _| project.buffer_store().clone());
6738        let buffer = buffer_store
6739            .update(cx, |store, cx| store.open_buffer(setup.project_path, cx))
6740            .await
6741            .expect("buffer should exist");
6742
6743        let text = buffer.read_with(cx, |buffer, _| buffer.text());
6744        assert!(
6745            text.contains("\"tab_size\": 3"),
6746            "Final tab_size should be 3: {}",
6747            text
6748        );
6749    }
6750
6751    #[gpui::test]
6752    async fn test_queue_continues_after_failure(cx: &mut TestAppContext) {
6753        let setup = init_test(cx, Some("{}")).await;
6754
6755        let successful_updates = Arc::new(AtomicUsize::new(0));
6756
6757        {
6758            let successful_updates = successful_updates.clone();
6759            let entry = ProjectSettingsUpdateEntry {
6760                worktree_id: setup.worktree_id,
6761                rel_path: setup.rel_path.clone(),
6762                settings_window: WeakEntity::new_invalid(),
6763                project: setup.project.downgrade(),
6764                worktree: setup.worktree.clone(),
6765                update: Box::new(move |content, _cx| {
6766                    successful_updates.fetch_add(1, Ordering::SeqCst);
6767                    content.project.all_languages.defaults.tab_size =
6768                        Some(NonZeroU32::new(2).unwrap());
6769                }),
6770            };
6771            cx.update(|cx| ProjectSettingsUpdateQueue::enqueue(cx, entry));
6772        }
6773
6774        {
6775            let entry = ProjectSettingsUpdateEntry {
6776                worktree_id: setup.worktree_id,
6777                rel_path: setup.rel_path.clone(),
6778                settings_window: WeakEntity::new_invalid(),
6779                project: WeakEntity::new_invalid(),
6780                worktree: setup.worktree.clone(),
6781                update: Box::new(|content, _cx| {
6782                    content.project.all_languages.defaults.tab_size =
6783                        Some(NonZeroU32::new(99).unwrap());
6784                }),
6785            };
6786            cx.update(|cx| ProjectSettingsUpdateQueue::enqueue(cx, entry));
6787        }
6788
6789        {
6790            let successful_updates = successful_updates.clone();
6791            let entry = ProjectSettingsUpdateEntry {
6792                worktree_id: setup.worktree_id,
6793                rel_path: setup.rel_path.clone(),
6794                settings_window: WeakEntity::new_invalid(),
6795                project: setup.project.downgrade(),
6796                worktree: setup.worktree.clone(),
6797                update: Box::new(move |content, _cx| {
6798                    successful_updates.fetch_add(1, Ordering::SeqCst);
6799                    content.project.all_languages.defaults.tab_size =
6800                        Some(NonZeroU32::new(4).unwrap());
6801                }),
6802            };
6803            cx.update(|cx| ProjectSettingsUpdateQueue::enqueue(cx, entry));
6804        }
6805
6806        cx.executor().run_until_parked();
6807
6808        assert_eq!(
6809            successful_updates.load(Ordering::SeqCst),
6810            2,
6811            "Two updates should have succeeded despite middle failure"
6812        );
6813
6814        let buffer_store = setup
6815            .project
6816            .read_with(cx, |project, _| project.buffer_store().clone());
6817        let buffer = buffer_store
6818            .update(cx, |store, cx| store.open_buffer(setup.project_path, cx))
6819            .await
6820            .expect("buffer should exist");
6821
6822        let text = buffer.read_with(cx, |buffer, _| buffer.text());
6823        assert!(
6824            text.contains("\"tab_size\": 4"),
6825            "Final tab_size should be 4 (third update): {}",
6826            text
6827        );
6828    }
6829
6830    #[gpui::test]
6831    async fn test_handles_dropped_worktree(cx: &mut TestAppContext) {
6832        let setup = init_test(cx, Some("{}")).await;
6833
6834        let entry = ProjectSettingsUpdateEntry {
6835            worktree_id: setup.worktree_id,
6836            rel_path: setup.rel_path.clone(),
6837            settings_window: WeakEntity::new_invalid(),
6838            project: setup.project.downgrade(),
6839            worktree: WeakEntity::new_invalid(),
6840            update: Box::new(|content, _cx| {
6841                content.project.all_languages.defaults.tab_size =
6842                    Some(NonZeroU32::new(99).unwrap());
6843            }),
6844        };
6845
6846        cx.update(|cx| ProjectSettingsUpdateQueue::enqueue(cx, entry));
6847        cx.executor().run_until_parked();
6848
6849        let file_content = setup
6850            .fs
6851            .load("/project/.zed/settings.json".as_ref())
6852            .await
6853            .unwrap();
6854        assert_eq!(
6855            file_content, "{}",
6856            "File should be unchanged when worktree is dropped"
6857        );
6858    }
6859
6860    #[gpui::test]
6861    async fn test_reloads_conflicted_buffer(cx: &mut TestAppContext) {
6862        let setup = init_test(cx, Some(r#"{ "tab_size": 2 }"#)).await;
6863
6864        let buffer_store = setup
6865            .project
6866            .read_with(cx, |project, _| project.buffer_store().clone());
6867        let buffer = buffer_store
6868            .update(cx, |store, cx| {
6869                store.open_buffer(setup.project_path.clone(), cx)
6870            })
6871            .await
6872            .expect("buffer should exist");
6873
6874        buffer.update(cx, |buffer, cx| {
6875            buffer.edit([(0..0, "// comment\n")], None, cx);
6876        });
6877
6878        let has_unsaved_edits = buffer.read_with(cx, |buffer, _| buffer.has_unsaved_edits());
6879        assert!(has_unsaved_edits, "Buffer should have unsaved edits");
6880
6881        setup
6882            .fs
6883            .save(
6884                "/project/.zed/settings.json".as_ref(),
6885                &r#"{ "tab_size": 99 }"#.into(),
6886                Default::default(),
6887            )
6888            .await
6889            .expect("save should succeed");
6890
6891        cx.executor().run_until_parked();
6892
6893        let has_conflict = buffer.read_with(cx, |buffer, _| buffer.has_conflict());
6894        assert!(
6895            has_conflict,
6896            "Buffer should have conflict after external modification"
6897        );
6898
6899        let (settings_window, _) = cx.add_window_view(|window, cx| {
6900            let mut sw = SettingsWindow::test(window, cx);
6901            sw.project_setting_file_buffers
6902                .insert(setup.project_path.clone(), buffer.clone());
6903            sw
6904        });
6905
6906        let entry = ProjectSettingsUpdateEntry {
6907            worktree_id: setup.worktree_id,
6908            rel_path: setup.rel_path.clone(),
6909            settings_window: settings_window.downgrade(),
6910            project: setup.project.downgrade(),
6911            worktree: setup.worktree.clone(),
6912            update: Box::new(|content, _cx| {
6913                content.project.all_languages.defaults.tab_size = Some(NonZeroU32::new(4).unwrap());
6914            }),
6915        };
6916
6917        cx.update(|cx| ProjectSettingsUpdateQueue::enqueue(cx, entry));
6918        cx.executor().run_until_parked();
6919
6920        let text = buffer.read_with(cx, |buffer, _| buffer.text());
6921        assert!(
6922            text.contains("\"tab_size\": 4"),
6923            "Buffer should have the new tab_size after reload and update: {}",
6924            text
6925        );
6926        assert!(
6927            !text.contains("// comment"),
6928            "Buffer should not contain the unsaved edit after reload: {}",
6929            text
6930        );
6931        assert!(
6932            !text.contains("99"),
6933            "Buffer should not contain the external modification value: {}",
6934            text
6935        );
6936    }
6937}
6938
Served at tenant.openagents/omega Member data and write actions are omitted.