Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T05:38:32.432Z 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

zed.rs

7495 lines · 273.4 KB · rust
1mod app_menus;
2pub mod edit_prediction_registry;
3#[cfg(target_os = "macos")]
4pub(crate) mod mac_only_instance;
5mod migrate;
6#[cfg(target_os = "macos")]
7mod open_listener;
8mod open_url_modal;
9mod quick_action_bar;
10pub mod remote_debug;
11pub mod telemetry_log;
12#[cfg(all(target_os = "macos", feature = "visual-tests"))]
13pub mod visual_tests;
14#[cfg(target_os = "windows")]
15pub(crate) mod windows_only_instance;
16
17use agent_settings::{UserAgentsMdState, init_user_agents_md};
18use agent_ui::AgentDiffToolbar;
19use anyhow::Context as _;
20pub use app_menus::*;
21use assets::Assets;
22
23use agent_computer_ui::AgentComputerPanel;
24use breadcrumbs::Breadcrumbs;
25use client::zed_urls;
26use collections::VecDeque;
27use debugger_ui::debugger_panel::DebugPanel;
28use editor::{Editor, MultiBuffer};
29use extension_host::ExtensionStore;
30use feature_flags::{FeatureFlagAppExt as _, PanicFeatureFlag};
31use fs::Fs;
32use futures::FutureExt as _;
33use futures::{StreamExt, channel::mpsc, select_biased};
34use git_ui::branch_diff::BranchDiffToolbar;
35use git_ui::commit_view::CommitViewToolbar;
36use git_ui::git_panel::GitPanel;
37use git_ui::project_diff::ProjectDiffToolbar;
38use git_ui::solo_diff_view::{SoloDiffGitToolbar, SoloDiffStyleToolbar};
39use git_ui::staged_diff::StagedDiffToolbar;
40use git_ui::unstaged_diff::UnstagedDiffToolbar;
41use gpui::{
42    Action, App, AppContext as _, AsyncWindowContext, ClipboardItem, Context, DismissEvent,
43    Element, Entity, FocusHandle, Focusable, Image, ImageFormat, KeyBinding, ParentElement,
44    PathPromptOptions, PromptLevel, ReadGlobal, SharedString, Size, Task, TaskExt, TitlebarOptions,
45    UpdateGlobal, WeakEntity, Window, WindowBounds, WindowHandle, WindowKind, WindowOptions,
46    actions, image_cache, img, point, px, retain_all,
47};
48use image_viewer::ImageInfo;
49use language::Capability;
50use language_onboarding::BasedPyrightBanner;
51use language_tools::lsp_button::{self, LspButton};
52use language_tools::lsp_log_view::LspLogToolbarItemView;
53use markdown::{Markdown, MarkdownElement, MarkdownFont, MarkdownStyle};
54use migrate::{MigrationBanner, MigrationEvent, MigrationNotification, MigrationType};
55use migrator::migrate_keymap;
56use onboarding::{await_identity_ready, multibuffer_hint::MultibufferHint};
57pub use open_listener::*;
58use outline_panel::OutlinePanel;
59use paths::{
60    local_debug_file_relative_path, local_settings_file_relative_path,
61    local_tasks_file_relative_path,
62};
63use project::{DirectoryLister, DisableAiSettings, ProjectItem};
64use project_panel::ProjectPanel;
65use quick_action_bar::QuickActionBar;
66use recent_projects::open_remote_project;
67use release_channel::{AppCommitSha, AppVersion, ReleaseChannel};
68use rope::Rope;
69use search::project_search::ProjectSearchBar;
70use settings::{
71    BaseKeymap, DEFAULT_KEYMAP_PATH, DefaultOpenBehavior, InvalidSettingsError, KeybindSource,
72    KeymapFile, KeymapFileLoadResult, MigrationStatus, SPECIFIC_OVERRIDES_KEYMAP_PATH, Settings,
73    SettingsFile, SettingsStore, VIM_KEYMAP_PATH, initial_local_debug_tasks_content,
74    initial_project_settings_content, initial_tasks_content, update_settings_file,
75};
76use sidebar::Sidebar;
77use workroom_ui::SarahWorkroomPanel;
78#[cfg(debug_assertions)]
79use workspace::workspace_error::{ErrorAction, ErrorSeverity, WorkspaceError};
80
81use std::{
82    borrow::Cow,
83    path::{Path, PathBuf},
84    sync::Arc,
85    sync::atomic::{self, AtomicBool},
86};
87use terminal_view::terminal_panel::{self, TerminalPanel};
88use theme::{ActiveTheme, SystemAppearance, ThemeRegistry, deserialize_icon_theme};
89use theme_settings::{ThemeSettings, load_user_theme};
90use ui::{Navigable, NavigableEntry, PopoverMenuHandle, TintColor, prelude::*};
91use util::markdown::MarkdownString;
92use util::rel_path::RelPath;
93use util::{ResultExt, asset_str, maybe};
94use uuid::Uuid;
95use vim_mode_setting::VimModeSetting;
96use workspace::notifications::{NotificationId, dismiss_app_notification, show_app_notification};
97
98use workspace::{
99    AppState, MultiWorkspace, NewFile, NewWindow, OpenLog, Panel, Toast, Workspace,
100    WorkspaceSettings, create_and_open_local_file,
101    notifications::simple_message_notification::MessageNotification, open_new,
102};
103use workspace::{
104    CloseIntent, CloseProject, CloseWindow, RestoreBanner, with_active_or_new_workspace,
105};
106use workspace::{Pane, notifications::DetachAndPromptErr};
107use zed_actions::{
108    About, GetMerch, OpenAccountSettings, OpenBrowser, OpenDocs, OpenProjectTasks,
109    OpenServerSettings, OpenSettingsFile, OpenStatusPage, OpenAppUrl, Quit,
110};
111
112const DOCS_URL: &str = app_identity::PRODUCT_DOCS_URL;
113const STATUS_URL: &str = "https://www.openagents.com/";
114const MERCH_URL: &str = "https://www.openagents.com/";
115
116pub struct CrashHandler(pub Arc<crashes::Client>);
117
118impl gpui::Global for CrashHandler {}
119
120actions!(
121    omega,
122    [
123        /// Opens the element inspector for debugging UI.
124        #[action(deprecated_aliases = ["zed::DebugElements"])]
125        DebugElements,
126        /// Hides the application window.
127        #[action(deprecated_aliases = ["zed::Hide"])]
128        Hide,
129        /// Hides all other application windows.
130        #[action(deprecated_aliases = ["zed::HideOthers"])]
131        HideOthers,
132        /// Minimizes the current window.
133        #[action(deprecated_aliases = ["zed::Minimize"])]
134        Minimize,
135        /// Opens the default settings file.
136        #[action(deprecated_aliases = ["zed::OpenDefaultSettings"])]
137        OpenDefaultSettings,
138        /// Opens project-specific settings file.
139        #[action(deprecated_aliases = ["zed::OpenProjectSettingsFile"])]
140        OpenProjectSettingsFile,
141        /// Opens the tasks panel.
142        #[action(deprecated_aliases = ["zed::OpenTasks"])]
143        OpenTasks,
144        /// Opens debug tasks configuration.
145        #[action(deprecated_aliases = ["zed::OpenDebugTasks"])]
146        OpenDebugTasks,
147        /// Shows the default semantic token rules (read-only).
148        #[action(deprecated_aliases = ["zed::ShowDefaultSemanticTokenRules"])]
149        ShowDefaultSemanticTokenRules,
150        /// Resets the application database.
151        #[action(deprecated_aliases = ["zed::ResetDatabase"])]
152        ResetDatabase,
153        /// Shows all hidden windows.
154        #[action(deprecated_aliases = ["zed::ShowAll"])]
155        ShowAll,
156        /// Toggles fullscreen mode.
157        #[action(deprecated_aliases = ["zed::ToggleFullScreen"])]
158        ToggleFullScreen,
159        /// Zooms the window.
160        #[action(deprecated_aliases = ["zed::Zoom"])]
161        Zoom,
162        /// Triggers a test panic for debugging.
163        #[action(deprecated_aliases = ["zed::TestPanic"])]
164        TestPanic,
165        /// Triggers a hard crash for debugging.
166        #[action(deprecated_aliases = ["zed::TestCrash"])]
167        TestCrash,
168    ]
169);
170
171actions!(
172    dev,
173    [
174        /// Opens a prompt to enter a URL to open.
175        OpenUrlPrompt,
176        /// Dumps the current accessibility tree (the last update sent to the
177        /// platform adapter) to a new buffer as JSON, for debugging what is
178        /// exposed to assistive technology.
179        DumpAccessibilityTree,
180        /// Copies the current accessibility tree to the clipboard as JSON,
181        /// without opening a buffer. See [`DumpAccessibilityTree`].
182        CopyAccessibilityTree,
183    ]
184);
185
186/// Serializes the window's most recent accessibility tree to JSON for the
187/// `dev: dump/copy accessibility tree` actions, falling back to a friendly
188/// placeholder when no tree has been built yet.
189fn accessibility_tree_dump(window: &Window) -> String {
190    window.debug_a11y_tree_json().unwrap_or_else(|| {
191        "No accessibility tree has been built yet. The tree is only \
192         produced once assistive technology (e.g. a screen reader) is \
193         active for this window."
194            .to_string()
195    })
196}
197
198#[cfg(debug_assertions)]
199actions!(
200    dev,
201    [
202        /// Show an error on the workspace level.
203        ShowWorkspaceError
204    ]
205);
206
207pub fn init(cx: &mut App) {
208    #[cfg(target_os = "macos")]
209    cx.on_action(|_: &Hide, cx| cx.hide());
210    #[cfg(target_os = "macos")]
211    cx.on_action(|_: &HideOthers, cx| cx.hide_other_apps());
212    #[cfg(target_os = "macos")]
213    cx.on_action(|_: &ShowAll, cx| cx.unhide_other_apps());
214    cx.on_action(quit);
215
216    cx.on_action(|_: &RestoreBanner, cx| title_bar::restore_banner(cx));
217
218    cx.observe_flag::<PanicFeatureFlag, _>({
219        let mut added = false;
220        move |flag, cx| {
221            if added || !*flag {
222                return;
223            }
224            added = true;
225            cx.on_action(|_: &TestPanic, _| panic!("Ran the TestPanic action"))
226                .on_action(|_: &TestCrash, _| {
227                    unsafe extern "C" {
228                        fn puts(s: *const i8);
229                    }
230                    unsafe {
231                        puts(0xabad1d3a as *const i8);
232                    }
233                });
234        }
235    })
236    .detach();
237
238    // When Zed logs to stdout rather than the log file, avoid registering
239    // handlers for both `OpenLog` and `RevealLogInFileManager`, as the log file
240    // does not exist in that scenario and these actions would error.
241    if !crate::stdout_is_a_pty() {
242        cx.on_action(|_: &OpenLog, cx| {
243            with_active_or_new_workspace(cx, |workspace, window, cx| {
244                open_log_file(workspace, window, cx);
245            });
246        })
247        .on_action(|_: &workspace::RevealLogInFileManager, cx| {
248            cx.reveal_path(paths::log_file().as_path());
249        });
250    }
251
252    cx.on_action(|_: &zed_actions::OpenLicenses, cx| {
253        with_active_or_new_workspace(cx, |workspace, window, cx| {
254            open_bundled_file(
255                workspace,
256                asset_str::<Assets>("licenses.md"),
257                "Open Source License Attribution",
258                "Markdown",
259                window,
260                cx,
261            );
262        });
263    })
264    .on_action(|&zed_actions::OpenKeymapFile, cx| {
265        with_active_or_new_workspace(cx, |_, window, cx| {
266            open_settings_file(
267                paths::keymap_file(),
268                || settings::initial_keymap_content().as_ref().into(),
269                window,
270                cx,
271            );
272        });
273    })
274    .on_action(|_: &OpenSettingsFile, cx| {
275        with_active_or_new_workspace(cx, |_, window, cx| {
276            open_settings_file(
277                paths::settings_file(),
278                || settings::initial_user_settings_content().as_ref().into(),
279                window,
280                cx,
281            );
282        });
283    })
284    .on_action(|_: &OpenAccountSettings, cx| {
285        with_active_or_new_workspace(cx, |_, _, cx| {
286            cx.open_url(&zed_urls::account_url(cx));
287        });
288    })
289    .on_action(|_: &OpenTasks, cx| {
290        with_active_or_new_workspace(cx, |_, window, cx| {
291            open_settings_file(
292                paths::tasks_file(),
293                || settings::initial_tasks_content().as_ref().into(),
294                window,
295                cx,
296            );
297        });
298    })
299    .on_action(|_: &OpenDebugTasks, cx| {
300        with_active_or_new_workspace(cx, |_, window, cx| {
301            open_settings_file(
302                paths::debug_scenarios_file(),
303                || settings::initial_debug_tasks_content().as_ref().into(),
304                window,
305                cx,
306            );
307        });
308    })
309    .on_action(|_: &ShowDefaultSemanticTokenRules, cx| {
310        with_active_or_new_workspace(cx, |workspace, window, cx| {
311            open_bundled_file(
312                workspace,
313                settings::default_semantic_token_rules(),
314                "Default Semantic Token Rules",
315                "JSONC",
316                window,
317                cx,
318            );
319        });
320    })
321    .on_action(|_: &OpenDefaultSettings, cx| {
322        with_active_or_new_workspace(cx, |workspace, window, cx| {
323            open_bundled_file(
324                workspace,
325                settings::default_settings(),
326                "Default Settings",
327                "JSON",
328                window,
329                cx,
330            );
331        });
332    })
333    .on_action(|_: &zed_actions::OpenDefaultKeymap, cx| {
334        with_active_or_new_workspace(cx, |workspace, window, cx| {
335            open_bundled_file(
336                workspace,
337                settings::default_keymap(),
338                "Default Key Bindings",
339                "JSON",
340                window,
341                cx,
342            );
343        });
344    })
345    .on_action(|_: &About, cx| {
346        open_about_window(cx);
347    });
348}
349
350fn bind_on_window_closed(cx: &mut App) -> Option<gpui::Subscription> {
351    #[cfg(target_os = "macos")]
352    {
353        WorkspaceSettings::get_global(cx)
354            .on_last_window_closed
355            .is_quit_app()
356            .then(|| {
357                cx.on_window_closed(|cx, _window_id| {
358                    if cx.windows().is_empty() {
359                        cx.quit();
360                    }
361                })
362            })
363    }
364    #[cfg(not(target_os = "macos"))]
365    {
366        Some(cx.on_window_closed(|cx, _window_id| {
367            if cx.windows().is_empty() {
368                cx.quit();
369            }
370        }))
371    }
372}
373
374pub fn build_window_options(display_uuid: Option<Uuid>, cx: &mut App) -> WindowOptions {
375    let display = display_uuid.and_then(|uuid| {
376        cx.displays()
377            .into_iter()
378            .find(|display| display.uuid().ok() == Some(uuid))
379    });
380    let app_id = ReleaseChannel::global(cx).app_id();
381    let window_decorations = match std::env::var("ZED_WINDOW_DECORATIONS") {
382        Ok(val) if val == "server" => gpui::WindowDecorations::Server,
383        Ok(val) if val == "client" => gpui::WindowDecorations::Client,
384        _ => match WorkspaceSettings::get_global(cx).window_decorations {
385            settings::WindowDecorations::Server => gpui::WindowDecorations::Server,
386            settings::WindowDecorations::Client => gpui::WindowDecorations::Client,
387        },
388    };
389
390    let use_system_window_tabs = WorkspaceSettings::get_global(cx).use_system_window_tabs;
391
392    #[cfg(any(target_os = "linux", target_os = "freebsd"))]
393    static APP_ICON: std::sync::LazyLock<Option<std::sync::Arc<image::RgbaImage>>> =
394        std::sync::LazyLock::new(|| {
395            // this shouldn't fail since decode is checked in build.rs
396            const BYTES: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/app_icon.png"));
397            util::maybe!({
398                let image = image::ImageReader::new(std::io::Cursor::new(BYTES))
399                    .with_guessed_format()?
400                    .decode()?
401                    .into();
402                anyhow::Ok(Arc::new(image))
403            })
404            .log_err()
405        });
406
407    WindowOptions {
408        titlebar: Some(TitlebarOptions {
409            title: None,
410            appears_transparent: true,
411            traffic_light_position: Some(point(px(9.0), px(9.0))),
412        }),
413        window_bounds: None,
414        focus: false,
415        show: false,
416        kind: WindowKind::Normal,
417        is_movable: true,
418        // Zed draws its own titlebar and moves the window via [`Window::start_window_move`],
419        // so on macOS AppKit should not own titlebar dragging. This avoids the titlebar
420        // click delay from AppKit's drag disambiguation (first observed on macOS 27) while
421        // keeping the window movable and the Window-menu tiling items enabled. No-op on
422        // other platforms.
423        app_owns_titlebar_drag: true,
424        display_id: display.map(|display| display.id()),
425        window_background: cx.theme().window_background_appearance(),
426        app_id: Some(app_id.to_owned()),
427        #[cfg(any(target_os = "linux", target_os = "freebsd"))]
428        icon: APP_ICON.as_ref().cloned(),
429        window_decorations: Some(window_decorations),
430        window_min_size: Some(gpui::Size {
431            width: px(360.0),
432            height: px(240.0),
433        }),
434        tabbing_identifier: if use_system_window_tabs {
435            Some(String::from("zed"))
436        } else {
437            None
438        },
439        ..Default::default()
440    }
441}
442
443pub fn initialize_workspace(app_state: Arc<AppState>, cx: &mut App) {
444    let mut _on_close_subscription = bind_on_window_closed(cx);
445    cx.observe_global::<SettingsStore>(move |cx| {
446        // A 1.92 regression causes unused-assignment to trigger on this variable.
447        _ = _on_close_subscription.is_some();
448        _on_close_subscription = bind_on_window_closed(cx);
449    })
450    .detach();
451
452    init_cursor_hide_mode(cx);
453    init_reduce_motion(cx);
454
455    cx.observe_new(|_multi_workspace: &mut MultiWorkspace, window, cx| {
456        let Some(window) = window else {
457            return;
458        };
459
460        #[cfg(feature = "track-project-leak")]
461        {
462            let multi_workspace_handle = cx.weak_entity();
463            let workspace_handle = _multi_workspace.workspace().downgrade();
464            let project_handle = _multi_workspace.workspace().read(cx).project().downgrade();
465            let window_id_2 = window.window_handle().window_id();
466            cx.on_window_closed(move |cx, window_id| {
467                let multi_workspace_handle = multi_workspace_handle.clone();
468                let workspace_handle = workspace_handle.clone();
469                let project_handle = project_handle.clone();
470                if window_id != window_id_2 {
471                    return;
472                }
473                cx.spawn(async move |cx| {
474                    cx.background_executor()
475                        .timer(std::time::Duration::from_millis(1500))
476                        .await;
477
478                    multi_workspace_handle.assert_released();
479                    workspace_handle.assert_released();
480                    project_handle.assert_released();
481                })
482                .detach();
483            })
484            .detach();
485        }
486
487        cx.spawn_in(window, async move |_this, cx| {
488            const TELEMETRY_INTERVAL: std::time::Duration = std::time::Duration::from_secs(5 * 60);
489            loop {
490                cx.background_executor().timer(TELEMETRY_INTERVAL).await;
491                if cx
492                    .update(|window, cx| {
493                        input_latency_ui::report_input_latency_telemetry(window, cx);
494                    })
495                    .is_err()
496                {
497                    break;
498                }
499            }
500        })
501        .detach();
502
503        let multi_workspace_handle = cx.entity().downgrade();
504        window.on_window_should_close(cx, move |window, cx| {
505            multi_workspace_handle
506                .update(cx, |multi_workspace, cx| {
507                    // We'll handle closing asynchronously
508                    multi_workspace.close_window(&CloseWindow, window, cx);
509                    false
510                })
511                .unwrap_or(true)
512        });
513
514        let window_handle = window.window_handle();
515        let multi_workspace_handle = cx.entity();
516        cx.subscribe_in(
517            &multi_workspace_handle,
518            window,
519            |this, _multi_workspace, event: &workspace::MultiWorkspaceEvent, window, cx| {
520                let workspace::MultiWorkspaceEvent::ActiveWorkspaceChanged { source_workspace } =
521                    event
522                else {
523                    return;
524                };
525
526                let active_workspace = this.workspace().clone();
527                let source_workspace = source_workspace.clone();
528                active_workspace.update(cx, |workspace, cx| {
529                    if let Some(ref source) = source_workspace {
530                        if let Some(panel) = workspace.panel::<agent_ui::AgentPanel>(cx) {
531                            panel.update(cx, |panel, cx| {
532                                panel.initialize_from_source_workspace_if_needed(
533                                    source.clone(),
534                                    window,
535                                    cx,
536                                );
537                            });
538                        }
539                    }
540
541                    ensure_agent_panel_for_workspace(workspace, source_workspace, window, cx)
542                        .detach_and_log_err(cx);
543                });
544            },
545        )
546        .detach();
547
548        cx.defer(move |cx| {
549            window_handle
550                .update(cx, |_, window, cx| {
551                    let sidebar =
552                        cx.new(|cx| Sidebar::new(multi_workspace_handle.clone(), window, cx));
553                    multi_workspace_handle.update(cx, |multi_workspace, cx| {
554                        multi_workspace.register_sidebar(sidebar, cx);
555                    });
556                })
557                .ok();
558        });
559    })
560    .detach();
561
562    cx.observe_new(move |workspace: &mut Workspace, window, cx| {
563        let Some(window) = window else {
564            return;
565        };
566
567        let workspace_handle = cx.entity();
568        let center_pane = workspace.active_pane().clone();
569        initialize_pane(workspace, &center_pane, window, cx);
570
571        cx.subscribe_in(&workspace_handle, window, {
572            move |workspace, _, event, window, cx| match event {
573                workspace::Event::PaneAdded(pane) => {
574                    initialize_pane(workspace, pane, window, cx);
575                }
576                workspace::Event::OpenBundledFile {
577                    text,
578                    title,
579                    language,
580                } => open_bundled_file(workspace, text.clone(), title, language, window, cx),
581                _ => {}
582            }
583        })
584        .detach();
585
586        #[cfg(not(any(test, target_os = "macos")))]
587        initialize_file_watcher(window, cx);
588
589        if let Some(specs) = window.gpu_specs() {
590            log::info!("Using GPU: {:?}", specs);
591            show_software_emulation_warning_if_needed(specs.clone(), window, cx);
592            if let Some(crash_client) = cx.try_global::<CrashHandler>() {
593                crashes::set_gpu_info(&crash_client.0, specs);
594            }
595        }
596
597        let edit_prediction_menu_handle = PopoverMenuHandle::default();
598        let edit_prediction_ui = cx.new(|cx| {
599            edit_prediction_ui::EditPredictionButton::new(
600                app_state.fs.clone(),
601                app_state.user_store.clone(),
602                edit_prediction_menu_handle.clone(),
603                workspace.project().clone(),
604                cx,
605            )
606        });
607        workspace.register_action({
608            move |_, _: &edit_prediction_ui::ToggleMenu, window, cx| {
609                edit_prediction_menu_handle.toggle(window, cx);
610            }
611        });
612
613        let search_button = cx.new(|_| search::search_status_button::SearchButton::new());
614        let diagnostic_summary =
615            cx.new(|cx| diagnostics::items::DiagnosticIndicator::new(workspace, cx));
616        let active_file_name = cx.new(|_| workspace::active_file_name::ActiveFileName::new());
617        let activity_indicator = activity_indicator::ActivityIndicator::new(
618            workspace,
619            workspace.project().read(cx).languages().clone(),
620            window,
621            cx,
622        );
623        let active_buffer_encoding =
624            cx.new(|_| encoding_selector::ActiveBufferEncoding::new(workspace));
625        let active_buffer_language =
626            cx.new(|_| language_selector::ActiveBufferLanguage::new(workspace));
627        let active_toolchain_language =
628            cx.new(|cx| toolchain_selector::ActiveToolchain::new(workspace, window, cx));
629        let vim_mode_indicator = cx.new(|cx| vim::ModeIndicator::new(window, cx));
630        let image_info = cx.new(|_cx| ImageInfo::new(workspace));
631
632        let lsp_button_menu_handle = PopoverMenuHandle::default();
633        let lsp_button =
634            cx.new(|cx| LspButton::new(workspace, lsp_button_menu_handle.clone(), window, cx));
635        workspace.register_action({
636            move |_, _: &lsp_button::ToggleMenu, window, cx| {
637                lsp_button_menu_handle.toggle(window, cx);
638            }
639        });
640
641        let cursor_position =
642            cx.new(|_| go_to_line::cursor_position::CursorPosition::new(workspace));
643        let line_ending_indicator =
644            cx.new(|_| line_ending_selector::LineEndingIndicator::default());
645        let git_blame_status = cx.new(|_| git_ui::GitBlameStatus::default());
646        let merge_conflict_indicator =
647            cx.new(|cx| git_ui::MergeConflictIndicator::new(workspace, cx));
648        workspace.status_bar().update(cx, |status_bar, cx| {
649            status_bar.add_left_item(search_button, window, cx);
650            status_bar.add_left_item(lsp_button, window, cx);
651            status_bar.add_left_item(diagnostic_summary, window, cx);
652            status_bar.add_left_item(active_file_name, window, cx);
653            status_bar.add_left_item(git_blame_status, window, cx);
654            status_bar.add_left_item(merge_conflict_indicator, window, cx);
655            status_bar.add_left_item(activity_indicator, window, cx);
656            status_bar.add_right_item(edit_prediction_ui, window, cx);
657            status_bar.add_right_item(active_buffer_encoding, window, cx);
658            status_bar.add_right_item(active_buffer_language, window, cx);
659            status_bar.add_right_item(active_toolchain_language, window, cx);
660            status_bar.add_right_item(line_ending_indicator, window, cx);
661            status_bar.add_right_item(vim_mode_indicator, window, cx);
662            status_bar.add_right_item(cursor_position, window, cx);
663            status_bar.add_right_item(image_info, window, cx);
664        });
665
666        let panels_task = initialize_panels(window, cx);
667        workspace.set_panels_task(panels_task);
668        register_actions(app_state.clone(), workspace, window, cx);
669
670        if !workspace.has_active_modal(window, cx) {
671            workspace.focus_handle(cx).focus(window, cx);
672        }
673    })
674    .detach();
675}
676
677#[cfg(any(target_os = "linux", target_os = "freebsd"))]
678#[allow(unused)]
679fn initialize_file_watcher(window: &mut Window, cx: &mut Context<Workspace>) {
680    if let Err(e) = fs::fs_watcher::global(|_| {}) {
681        let message = format!(
682            db::indoc! {r#"
683            inotify_init returned {}
684
685            This may be due to system-wide limits on inotify instances. For troubleshooting see: https://zed.dev/docs/linux
686            "#},
687            e
688        );
689        let prompt = window.prompt(
690            PromptLevel::Critical,
691            "Could not start inotify",
692            Some(&message),
693            &["Troubleshoot and Quit"],
694            cx,
695        );
696        cx.spawn(async move |_, cx| {
697            if prompt.await == Ok(0) {
698                cx.update(|cx| {
699                    cx.open_url("https://zed.dev/docs/linux#could-not-start-inotify");
700                    cx.quit();
701                });
702            }
703        })
704        .detach()
705    }
706}
707
708#[cfg(target_os = "windows")]
709#[allow(unused)]
710fn initialize_file_watcher(window: &mut Window, cx: &mut Context<Workspace>) {
711    if let Err(e) = fs::fs_watcher::global(|_| {}) {
712        let message = format!(
713            db::indoc! {r#"
714            ReadDirectoryChangesW initialization failed: {}
715
716            This may occur on network filesystems and WSL paths. For troubleshooting see: https://zed.dev/docs/windows
717            "#},
718            e
719        );
720        let prompt = window.prompt(
721            PromptLevel::Critical,
722            "Could not start ReadDirectoryChangesW",
723            Some(&message),
724            &["Troubleshoot and Quit"],
725            cx,
726        );
727        cx.spawn(async move |_, cx| {
728            if prompt.await == Ok(0) {
729                cx.update(|cx| {
730                    cx.open_url("https://zed.dev/docs/windows");
731                    cx.quit()
732                });
733            }
734        })
735        .detach()
736    }
737}
738
739fn show_software_emulation_warning_if_needed(
740    specs: gpui::GpuSpecs,
741    window: &mut Window,
742    cx: &mut Context<Workspace>,
743) {
744    if specs.is_software_emulated && std::env::var("ZED_ALLOW_EMULATED_GPU").is_err() {
745        let (graphics_api, docs_url, open_url) = if cfg!(target_os = "windows") {
746            (
747                "DirectX",
748                app_identity::PRODUCT_DOCS_URL,
749                app_identity::PRODUCT_DOCS_URL,
750            )
751        } else {
752            (
753                "Vulkan",
754                app_identity::PRODUCT_DOCS_URL,
755                app_identity::PRODUCT_DOCS_URL,
756            )
757        };
758        let message = format!(
759            db::indoc! {r#"
760            Omega uses {} for rendering and requires a compatible GPU.
761
762            Currently you are using a software emulated GPU ({}) which
763            will result in awful performance.
764
765            For troubleshooting see: {}
766            Set ZED_ALLOW_EMULATED_GPU=1 env var to permanently override.
767            "#},
768            graphics_api, specs.device_name, docs_url
769        );
770        let prompt = window.prompt(
771            PromptLevel::Critical,
772            "Unsupported GPU",
773            Some(&message),
774            &["Skip", "Troubleshoot and Quit"],
775            cx,
776        );
777        cx.spawn(async move |_, cx| {
778            if prompt.await == Ok(1) {
779                cx.update(|cx| {
780                    cx.open_url(open_url);
781                    cx.quit();
782                });
783            }
784        })
785        .detach()
786    }
787}
788
789fn initialize_panels(window: &mut Window, cx: &mut Context<Workspace>) -> Task<anyhow::Result<()>> {
790    cx.spawn_in(window, async move |workspace_handle, cx| {
791        let project_panel = ProjectPanel::load(workspace_handle.clone(), cx.clone());
792        let outline_panel = OutlinePanel::load(workspace_handle.clone(), cx.clone());
793        let terminal_panel = TerminalPanel::load(workspace_handle.clone(), cx.clone());
794        let git_panel = GitPanel::load(workspace_handle.clone(), cx.clone());
795        let debug_panel = DebugPanel::load(workspace_handle.clone(), cx);
796        let agent_computer_panel = AgentComputerPanel::load(workspace_handle.clone(), cx.clone());
797        let sarah_workroom_panel = SarahWorkroomPanel::load(workspace_handle.clone(), cx.clone());
798
799        async fn add_panel_when_ready(
800            panel_task: impl Future<Output = anyhow::Result<Entity<impl workspace::Panel>>> + 'static,
801            workspace_handle: WeakEntity<Workspace>,
802            mut cx: gpui::AsyncWindowContext,
803        ) {
804            if let Some(panel) = panel_task.await.context("failed to load panel").log_err()
805            {
806                workspace_handle
807                    .update_in(&mut cx, |workspace, window, cx| {
808                        workspace.add_panel(panel, window, cx);
809                    })
810                    .log_err();
811            }
812        }
813
814        futures::join!(
815            add_panel_when_ready(project_panel, workspace_handle.clone(), cx.clone()),
816            add_panel_when_ready(outline_panel, workspace_handle.clone(), cx.clone()),
817            add_panel_when_ready(terminal_panel, workspace_handle.clone(), cx.clone()),
818            add_panel_when_ready(git_panel, workspace_handle.clone(), cx.clone()),
819            add_panel_when_ready(debug_panel, workspace_handle.clone(), cx.clone()),
820            add_panel_when_ready(agent_computer_panel, workspace_handle.clone(), cx.clone()),
821            add_panel_when_ready(sarah_workroom_panel, workspace_handle.clone(), cx.clone()),
822            initialize_agent_panel(workspace_handle.clone(), cx.clone()).map(|r| r.log_err()),
823        );
824
825        if workroom_ui::public_demo_mode() {
826            workspace_handle
827                .update_in(cx, |workspace, window, cx| {
828                    workspace.open_panel::<ProjectPanel>(window, cx);
829                    workspace.open_panel::<SarahWorkroomPanel>(window, cx);
830                })
831                .log_err();
832        }
833
834        anyhow::Ok(())
835    })
836}
837
838fn setup_or_teardown_ai_panel<P: Panel>(
839    workspace: &mut Workspace,
840    window: &mut Window,
841    cx: &mut Context<Workspace>,
842    load_panel: impl FnOnce(
843        WeakEntity<Workspace>,
844        AsyncWindowContext,
845    ) -> Task<anyhow::Result<Entity<P>>>
846    + 'static,
847) -> Task<anyhow::Result<()>> {
848    let disable_ai = SettingsStore::global(cx)
849        .get::<DisableAiSettings>(None)
850        .disable_ai
851        || cfg!(test);
852    let existing_panel = workspace.panel::<P>(cx);
853    match (disable_ai, existing_panel) {
854        (false, None) => cx.spawn_in(window, async move |workspace, cx| {
855            let panel = load_panel(workspace.clone(), cx.clone()).await?;
856            workspace.update_in(cx, |workspace, window, cx| {
857                let disable_ai = SettingsStore::global(cx)
858                    .get::<DisableAiSettings>(None)
859                    .disable_ai;
860                let have_panel = workspace.panel::<P>(cx).is_some();
861                if !disable_ai && !have_panel {
862                    workspace.add_panel(panel, window, cx);
863                }
864            })
865        }),
866        (true, Some(existing_panel)) => {
867            workspace.remove_panel::<P>(&existing_panel, window, cx);
868            Task::ready(Ok(()))
869        }
870        _ => Task::ready(Ok(())),
871    }
872}
873
874fn ensure_agent_panel_for_workspace(
875    workspace: &mut Workspace,
876    source_workspace: Option<WeakEntity<Workspace>>,
877    window: &mut Window,
878    cx: &mut Context<Workspace>,
879) -> Task<anyhow::Result<()>> {
880    let task = setup_or_teardown_ai_panel(workspace, window, cx, move |workspace, cx| {
881        agent_ui::AgentPanel::load(workspace, cx)
882    });
883
884    cx.spawn_in(window, async move |workspace, cx| {
885        task.await?;
886        workspace.update_in(cx, |workspace, window, cx| {
887            if let Some(source_workspace) = source_workspace.clone()
888                && let Some(panel) = workspace.panel::<agent_ui::AgentPanel>(cx)
889            {
890                panel.update(cx, |panel, cx| {
891                    panel.initialize_from_source_workspace_if_needed(source_workspace, window, cx);
892                });
893            }
894        })
895    })
896}
897
898async fn initialize_agent_panel(
899    workspace_handle: WeakEntity<Workspace>,
900    mut cx: AsyncWindowContext,
901) -> anyhow::Result<()> {
902    workspace_handle
903        .update_in(&mut cx, |workspace, window, cx| {
904            ensure_agent_panel_for_workspace(workspace, None, window, cx)
905        })?
906        .await?;
907
908    workspace_handle.update_in(&mut cx, |workspace, window, cx| {
909        cx.observe_global_in::<SettingsStore>(window, move |workspace, window, cx| {
910            ensure_agent_panel_for_workspace(workspace, None, window, cx).detach_and_log_err(cx);
911        })
912        .detach();
913
914        // Register the actions that are shared between `assistant` and `assistant2`.
915        //
916        // We need to do this here instead of within the individual `init`
917        // functions so that we only register the actions once.
918        //
919        // Once we ship `assistant2` we can push this back down into `agent::agent_panel::init`.
920        if !cfg!(test) {
921            workspace
922                .register_action(agent_ui::AgentPanel::toggle_focus)
923                .register_action(agent_ui::AgentPanel::focus)
924                .register_action(agent_ui::AgentPanel::toggle)
925                .register_action(agent_ui::InlineAssistant::inline_assist);
926        }
927    })?;
928
929    anyhow::Ok(())
930}
931
932fn register_actions(
933    app_state: Arc<AppState>,
934    workspace: &mut Workspace,
935    _: &mut Window,
936    cx: &mut Context<Workspace>,
937) {
938    workspace
939        .register_action(|_, _: &OpenDocs, _, cx| cx.open_url(DOCS_URL))
940        .register_action(|_, _: &OpenStatusPage, _, cx| cx.open_url(STATUS_URL))
941        .register_action(|_, _: &GetMerch, _, cx| cx.open_url(MERCH_URL))
942        .register_action(
943            |workspace: &mut Workspace,
944             _: &input_latency_ui::DumpInputLatencyHistogram,
945             window: &mut Window,
946             cx: &mut Context<Workspace>| {
947                let report =
948                    input_latency_ui::format_input_latency_report(window, cx);
949                let project = workspace.project().clone();
950                let buffer = project.update(cx, |project, cx| {
951                    project.create_local_buffer(&report, None, true, cx)
952                });
953                let editor =
954                    cx.new(|cx| Editor::for_buffer(buffer, Some(project), window, cx));
955                workspace.add_item_to_active_pane(Box::new(editor), None, true, window, cx);
956            },
957        )
958        .register_action(
959            |workspace: &mut Workspace,
960             _: &DumpAccessibilityTree,
961             window: &mut Window,
962             cx: &mut Context<Workspace>| {
963                let json = accessibility_tree_dump(window);
964                let language = workspace.app_state().languages.language_for_name("JSON");
965                cx.spawn_in(window, async move |workspace, cx| {
966                    let language = language.await.log_err();
967                    workspace
968                        .update_in(cx, |workspace, window, cx| {
969                            let project = workspace.project().clone();
970                            let buffer = project.update(cx, |project, cx| {
971                                project.create_local_buffer(&json, language, true, cx)
972                            });
973                            let title = "Accessibility Tree".to_string();
974                            let buffer = cx.new(|cx| {
975                                MultiBuffer::singleton(buffer, cx).with_title(title.clone())
976                            });
977                            let editor = cx.new(|cx| {
978                                let mut editor = Editor::for_multibuffer(
979                                    buffer,
980                                    Some(project),
981                                    window,
982                                    cx,
983                                );
984                                editor.set_breadcrumb_header(title);
985                                editor
986                            });
987                            workspace.add_item_to_active_pane(
988                                Box::new(editor),
989                                None,
990                                true,
991                                window,
992                                cx,
993                            );
994                        })
995                        .log_err();
996                })
997                .detach();
998            },
999        )
1000        .register_action(
1001            |_workspace: &mut Workspace,
1002             _: &CopyAccessibilityTree,
1003             window: &mut Window,
1004             cx: &mut Context<Workspace>| {
1005                let json = accessibility_tree_dump(window);
1006                cx.write_to_clipboard(ClipboardItem::new_string(json));
1007            },
1008        )
1009        .register_action(|_, _: &Minimize, window, _| {
1010            window.minimize_window();
1011        })
1012        .register_action(|_, _: &Zoom, window, _| {
1013            window.zoom_window();
1014        })
1015        .register_action(|_, _: &ToggleFullScreen, window, _| {
1016            window.toggle_fullscreen();
1017        })
1018        .register_action(|_, action: &OpenAppUrl, _, cx| {
1019            OpenListener::global(cx).open(RawOpenRequest {
1020                urls: vec![String::from(&*action.url)],
1021                ..Default::default()
1022            })
1023        })
1024        .register_action(|workspace, _: &OpenUrlPrompt, window, cx| {
1025            workspace.toggle_modal(window, cx, |window, cx| {
1026                open_url_modal::OpenUrlModal::new(window, cx)
1027            });
1028        })
1029        .register_action(|workspace, action: &OpenBrowser, _window, cx| {
1030            // Parse and validate the URL to ensure it's properly formatted
1031            match url::Url::parse(&action.url) {
1032                Ok(parsed_url) => {
1033                    // Use the parsed URL's string representation which is properly escaped
1034                    cx.open_url(parsed_url.as_str());
1035                }
1036                Err(e) => {
1037                    workspace.show_error(
1038                        format!(
1039                            "Opening this URL in a browser failed because the URL is invalid: {}\n\nError was: {e}",
1040                            action.url
1041                        ),
1042                        cx,
1043                    );
1044                }
1045            }
1046        })
1047        .register_action(|workspace, action: &workspace::Open, window, cx| {
1048            let app_state = workspace.app_state().clone();
1049            let create_new_window = action.create_new_window;
1050            cx.spawn_in(window, async move |this, cx| {
1051                await_identity_ready(app_state.clone(), cx).await?;
1052                this.update_in(cx, |workspace, window, cx| {
1053                    telemetry::event!("Project Opened");
1054                    workspace::prompt_for_open_path_and_open(
1055                        workspace,
1056                        app_state,
1057                        PathPromptOptions {
1058                            files: true,
1059                            directories: true,
1060                            multiple: true,
1061                            prompt: None,
1062                        },
1063                        create_new_window.unwrap_or_else(|| {
1064                            matches!(
1065                                WorkspaceSettings::get_global(cx).default_open_behavior,
1066                                DefaultOpenBehavior::NewWindow
1067                            )
1068                        }),
1069                        window,
1070                        cx,
1071                    );
1072                })?;
1073                anyhow::Ok(())
1074            })
1075            .detach_and_log_err(cx);
1076        })
1077        .register_action(|workspace, _: &workspace::OpenFiles, window, cx| {
1078            let app_state = workspace.app_state().clone();
1079            cx.spawn_in(window, async move |this, cx| {
1080                await_identity_ready(app_state.clone(), cx).await?;
1081                this.update_in(cx, |workspace, window, cx| {
1082                    let directories = cx.can_select_mixed_files_and_dirs();
1083                    workspace::prompt_for_open_path_and_open(
1084                        workspace,
1085                        app_state,
1086                        PathPromptOptions {
1087                            files: true,
1088                            directories,
1089                            multiple: true,
1090                            prompt: None,
1091                        },
1092                        true,
1093                        window,
1094                        cx,
1095                    );
1096                })?;
1097                anyhow::Ok(())
1098            })
1099            .detach_and_log_err(cx);
1100        })
1101        .register_action(|workspace, action: &zed_actions::OpenRemote, window, cx| {
1102            if !action.from_existing_connection {
1103                cx.propagate();
1104                return;
1105            }
1106            // You need existing remote connection to open it this way
1107            if workspace.project().read(cx).is_local() {
1108                return;
1109            }
1110            let app_state = workspace.app_state().clone();
1111            cx.spawn_in(window, async move |this, cx| {
1112                await_identity_ready(app_state, cx).await?;
1113                let paths = this.update_in(cx, |workspace, window, cx| {
1114                    telemetry::event!("Project Opened");
1115                    workspace.prompt_for_open_path(
1116                        PathPromptOptions {
1117                            files: true,
1118                            directories: true,
1119                            multiple: true,
1120                            prompt: None,
1121                        },
1122                        DirectoryLister::Project(workspace.project().clone()),
1123                        window,
1124                        cx,
1125                    )
1126                })?;
1127                let Some(paths) = paths.await.log_err().flatten() else {
1128                    return anyhow::Ok(());
1129                };
1130                if let Some(task) = this
1131                    .update_in(cx, |this, window, cx| {
1132                        open_new_ssh_project_from_project(this, paths, window, cx)
1133                    })
1134                    .log_err()
1135                {
1136                    task.await.log_err();
1137                }
1138                anyhow::Ok(())
1139            })
1140            .detach_and_log_err(cx)
1141        })
1142        .register_action({
1143            let fs = app_state.fs.clone();
1144            move |_, action: &zed_actions::IncreaseUiFontSize, _window, cx| {
1145                if action.persist {
1146                    update_settings_file(fs.clone(), cx, move |settings, cx| {
1147                        let ui_font_size = ThemeSettings::get_global(cx).ui_font_size(cx) + px(1.0);
1148                        let _ = settings
1149                            .theme
1150                            .ui_font_size
1151                            .insert(f32::from(theme_settings::clamp_font_size(ui_font_size)).into());
1152                    });
1153                } else {
1154                    theme_settings::adjust_ui_font_size(cx, |size| size + px(1.0));
1155                }
1156            }
1157        })
1158        .register_action({
1159            let fs = app_state.fs.clone();
1160            move |_, action: &zed_actions::DecreaseUiFontSize, _window, cx| {
1161                if action.persist {
1162                    update_settings_file(fs.clone(), cx, move |settings, cx| {
1163                        let ui_font_size = ThemeSettings::get_global(cx).ui_font_size(cx) - px(1.0);
1164                        let _ = settings
1165                            .theme
1166                            .ui_font_size
1167                            .insert(f32::from(theme_settings::clamp_font_size(ui_font_size)).into());
1168                    });
1169                } else {
1170                    theme_settings::adjust_ui_font_size(cx, |size| size - px(1.0));
1171                }
1172            }
1173        })
1174        .register_action({
1175            let fs = app_state.fs.clone();
1176            move |_, action: &zed_actions::ResetUiFontSize, _window, cx| {
1177                if action.persist {
1178                    update_settings_file(fs.clone(), cx, move |settings, _| {
1179                        settings.theme.ui_font_size = None;
1180                    });
1181                } else {
1182                    theme_settings::reset_ui_font_size(cx);
1183                }
1184            }
1185        })
1186        .register_action({
1187            let fs = app_state.fs.clone();
1188            move |_, action: &zed_actions::IncreaseBufferFontSize, _window, cx| {
1189                if action.persist {
1190                    update_settings_file(fs.clone(), cx, move |settings, cx| {
1191                        let buffer_font_size =
1192                            ThemeSettings::get_global(cx).buffer_font_size(cx) + px(1.0);
1193                        let _ = settings
1194                            .theme
1195                            .buffer_font_size
1196                            .insert(f32::from(theme_settings::clamp_font_size(buffer_font_size)).into());
1197                    });
1198                } else {
1199                    theme_settings::increase_buffer_font_size(cx);
1200                }
1201            }
1202        })
1203        .register_action({
1204            let fs = app_state.fs.clone();
1205            move |_, action: &zed_actions::DecreaseBufferFontSize, _window, cx| {
1206                if action.persist {
1207                    update_settings_file(fs.clone(), cx, move |settings, cx| {
1208                        let buffer_font_size =
1209                            ThemeSettings::get_global(cx).buffer_font_size(cx) - px(1.0);
1210                        let _ = settings
1211                            .theme
1212                            .buffer_font_size
1213                            .insert(f32::from(theme_settings::clamp_font_size(buffer_font_size)).into());
1214                    });
1215                } else {
1216                    theme_settings::decrease_buffer_font_size(cx);
1217                }
1218            }
1219        })
1220        .register_action({
1221            let fs = app_state.fs.clone();
1222            move |_, action: &zed_actions::ResetBufferFontSize, _window, cx| {
1223                if action.persist {
1224                    update_settings_file(fs.clone(), cx, move |settings, _| {
1225                        settings.theme.buffer_font_size = None;
1226                    });
1227                } else {
1228                    theme_settings::reset_buffer_font_size(cx);
1229                }
1230            }
1231        })
1232        .register_action({
1233            let fs = app_state.fs.clone();
1234            move |_, action: &zed_actions::ResetAllZoom, _window, cx| {
1235                if action.persist {
1236                    update_settings_file(fs.clone(), cx, move |settings, _| {
1237                        settings.theme.ui_font_size = None;
1238                        settings.theme.buffer_font_size = None;
1239                        settings.theme.agent_ui_font_size = None;
1240                        settings.theme.agent_buffer_font_size = None;
1241                    });
1242                } else {
1243                    theme_settings::reset_ui_font_size(cx);
1244                    theme_settings::reset_buffer_font_size(cx);
1245                    theme_settings::reset_agent_ui_font_size(cx);
1246                    theme_settings::reset_agent_buffer_font_size(cx);
1247                }
1248            }
1249        })
1250        .register_action(|_, _: &install_cli::RegisterAppScheme, window, cx| {
1251            cx.spawn_in(window, async move |workspace, cx| {
1252                install_cli::register_app_scheme(cx).await?;
1253                workspace.update_in(cx, |workspace, _, cx| {
1254                    struct RegisterAppScheme;
1255
1256                    workspace.show_toast(
1257                        Toast::new(
1258                            NotificationId::unique::<RegisterAppScheme>(),
1259                            format!(
1260                                "{}:// links will now open in {}.",
1261                                ReleaseChannel::global(cx).protocol_scheme(),
1262                                ReleaseChannel::global(cx).display_name()
1263                            ),
1264                        ),
1265                        cx,
1266                    )
1267                })?;
1268                Ok(())
1269            })
1270            .detach_and_prompt_err(
1271                "Error registering Omega URL scheme",
1272                window,
1273                cx,
1274                |_, _, _| None,
1275            );
1276        })
1277        .register_action(open_project_settings_file)
1278        .register_action(open_project_tasks_file)
1279        .register_action(open_worktree_setup_tasks_file)
1280        .register_action(open_project_debug_tasks_file)
1281        .register_action(
1282            |workspace: &mut Workspace,
1283             _: &zed_actions::project_panel::ToggleFocus,
1284             window: &mut Window,
1285             cx: &mut Context<Workspace>| {
1286                workspace.toggle_panel_focus::<ProjectPanel>(window, cx);
1287            },
1288        )
1289        .register_action(
1290            |workspace: &mut Workspace,
1291             _: &outline_panel::ToggleFocus,
1292             window: &mut Window,
1293             cx: &mut Context<Workspace>| {
1294                workspace.toggle_panel_focus::<OutlinePanel>(window, cx);
1295            },
1296        )
1297        .register_action(
1298            |workspace: &mut Workspace,
1299             _: &terminal_panel::ToggleFocus,
1300             window: &mut Window,
1301             cx: &mut Context<Workspace>| {
1302                workspace.toggle_panel_focus::<TerminalPanel>(window, cx);
1303            },
1304        )
1305        .register_action({
1306            let app_state = app_state.clone();
1307            move |_, _: &NewWindow, _, cx| {
1308                cx.spawn({
1309                    let app_state = app_state.clone();
1310                    async move |_, cx| {
1311                        await_identity_ready(app_state.clone(), cx).await?;
1312                        cx.update(|cx| {
1313                            open_new(
1314                                Default::default(),
1315                                app_state,
1316                                cx,
1317                                |workspace, window, cx| {
1318                                    cx.activate(true);
1319                                    // Create buffer synchronously to avoid flicker
1320                                    let project = workspace.project().clone();
1321                                    let buffer = project.update(cx, |project, cx| {
1322                                        project.create_local_buffer("", None, true, cx)
1323                                    });
1324                                    let editor = cx.new(|cx| {
1325                                        Editor::for_buffer(buffer, Some(project), window, cx)
1326                                    });
1327                                    workspace.add_item_to_active_pane(
1328                                        Box::new(editor),
1329                                        None,
1330                                        true,
1331                                        window,
1332                                        cx,
1333                                    );
1334                                },
1335                            )
1336                            .detach();
1337                        });
1338                        anyhow::Ok(())
1339                    }
1340                })
1341                .detach_and_log_err(cx);
1342            }
1343        })
1344        .register_action({
1345            move |workspace, _: &CloseProject, window, cx| {
1346                let Some(window_handle) = window.window_handle().downcast::<MultiWorkspace>() else {
1347                    return;
1348                };
1349                let old_group_key = workspace.project_group_key(cx);
1350                cx.spawn_in(window, async move |_, cx| {
1351                    let task = window_handle.update(cx, |multi_workspace, window, cx| {
1352                        multi_workspace.remove_project_group(&old_group_key, window, cx)
1353                    })?;
1354                    task.await?;
1355                    anyhow::Ok(())
1356                })
1357                .detach_and_log_err(cx);
1358            }
1359        })
1360        .register_action({
1361            let app_state = app_state.clone();
1362            move |_, _: &NewFile, _, cx| {
1363                cx.spawn({
1364                    let app_state = app_state.clone();
1365                    async move |_, cx| {
1366                        await_identity_ready(app_state.clone(), cx).await?;
1367                        cx.update(|cx| {
1368                            open_new(
1369                                Default::default(),
1370                                app_state,
1371                                cx,
1372                                |workspace, window, cx| {
1373                                    Editor::new_file(
1374                                        workspace,
1375                                        &Default::default(),
1376                                        window,
1377                                        cx,
1378                                    )
1379                                },
1380                            )
1381                            .detach_and_log_err(cx);
1382                        });
1383                        anyhow::Ok(())
1384                    }
1385                })
1386                .detach_and_log_err(cx);
1387            }
1388        });
1389
1390    #[cfg(not(target_os = "windows"))]
1391    workspace.register_action(install_cli);
1392
1393    if workspace.project().read(cx).is_via_remote_server() {
1394        workspace.register_action({
1395            move |workspace, _: &OpenServerSettings, window, cx| {
1396                let open_server_settings = workspace
1397                    .project()
1398                    .update(cx, |project, cx| project.open_server_settings(cx));
1399
1400                cx.spawn_in(window, async move |workspace, cx| {
1401                    let buffer = open_server_settings.await?;
1402
1403                    workspace
1404                        .update_in(cx, |workspace, window, cx| {
1405                            workspace.open_path(
1406                                buffer
1407                                    .read(cx)
1408                                    .project_path(cx)
1409                                    .expect("Settings file must have a location"),
1410                                None,
1411                                true,
1412                                window,
1413                                cx,
1414                            )
1415                        })?
1416                        .await?;
1417
1418                    anyhow::Ok(())
1419                })
1420                .detach_and_log_err(cx);
1421            }
1422        });
1423    }
1424
1425    workspace.register_action(sidebar::dump_workspace_info);
1426
1427    #[cfg(debug_assertions)]
1428    workspace.register_action(|workspace, _: &ShowWorkspaceError, _, cx| {
1429        struct DebugError;
1430        struct SecondDebugError;
1431
1432        impl WorkspaceError for DebugError {
1433            fn primary_message(&self) -> SharedString {
1434                SharedString::new_static(
1435                    "Error: Prepare rename via rust-analyzer failed: No references found at position",
1436                )
1437            }
1438
1439            fn severity(&self) -> ErrorSeverity {
1440                ErrorSeverity::Warning
1441            }
1442
1443            fn primary_action(&self) -> ErrorAction {
1444                ErrorAction::dismiss()
1445            }
1446        }
1447
1448        impl WorkspaceError for SecondDebugError {
1449            fn primary_message(&self) -> SharedString {
1450                SharedString::new_static("This is some error to ignore.")
1451            }
1452
1453            fn severity(&self) -> ErrorSeverity {
1454                ErrorSeverity::Error
1455            }
1456
1457            fn primary_action(&self) -> ErrorAction {
1458                ErrorAction::dismiss()
1459            }
1460        }
1461
1462        workspace.show_error(DebugError, cx);
1463        workspace.show_error(SecondDebugError, cx);
1464    });
1465}
1466
1467fn initialize_pane(
1468    workspace: &Workspace,
1469    pane: &Entity<Pane>,
1470    window: &mut Window,
1471    cx: &mut Context<Workspace>,
1472) {
1473    let workspace_handle = cx.weak_entity();
1474    pane.update(cx, |pane, cx| {
1475        pane.toolbar().update(cx, |toolbar, cx| {
1476            let multibuffer_hint = cx.new(|_| MultibufferHint::new());
1477            toolbar.add_item(multibuffer_hint, window, cx);
1478            let solo_diff_style_toolbar = cx.new(SoloDiffStyleToolbar::new);
1479            toolbar.add_item(solo_diff_style_toolbar, window, cx);
1480            let breadcrumbs = cx.new(|_| Breadcrumbs::new());
1481            toolbar.add_item(breadcrumbs, window, cx);
1482            let buffer_search_bar = cx.new(|cx| {
1483                search::BufferSearchBar::new(
1484                    Some(workspace.project().read(cx).languages().clone()),
1485                    window,
1486                    cx,
1487                )
1488            });
1489            toolbar.add_item(buffer_search_bar.clone(), window, cx);
1490            let quick_action_bar =
1491                cx.new(|cx| QuickActionBar::new(buffer_search_bar, workspace, cx));
1492            toolbar.add_item(quick_action_bar, window, cx);
1493            let diagnostic_editor_controls = cx.new(|_| diagnostics::ToolbarControls::new());
1494            toolbar.add_item(diagnostic_editor_controls, window, cx);
1495            let project_search_bar = cx.new(|_| ProjectSearchBar::new());
1496            toolbar.add_item(project_search_bar, window, cx);
1497            let lsp_log_item = cx.new(|_| LspLogToolbarItemView::new());
1498            toolbar.add_item(lsp_log_item, window, cx);
1499            let dap_log_item = cx.new(|_| debugger_tools::DapLogToolbarItemView::new());
1500            toolbar.add_item(dap_log_item, window, cx);
1501            let acp_tools_item = cx.new(|_| acp_tools::AcpToolsToolbarItemView::new());
1502            toolbar.add_item(acp_tools_item, window, cx);
1503            let telemetry_log_item =
1504                cx.new(|cx| telemetry_log::TelemetryLogToolbarItemView::new(window, cx));
1505            toolbar.add_item(telemetry_log_item, window, cx);
1506            let syntax_tree_item = cx.new(|_| language_tools::SyntaxTreeToolbarItemView::new());
1507            toolbar.add_item(syntax_tree_item, window, cx);
1508            let migration_banner =
1509                cx.new(|inner_cx| MigrationBanner::new(workspace_handle.clone(), inner_cx));
1510            toolbar.add_item(migration_banner, window, cx);
1511            let highlights_tree_item =
1512                cx.new(|_| language_tools::HighlightsTreeToolbarItemView::new());
1513            toolbar.add_item(highlights_tree_item, window, cx);
1514            let project_diff_toolbar = cx.new(|cx| ProjectDiffToolbar::new(workspace, cx));
1515            toolbar.add_item(project_diff_toolbar, window, cx);
1516            let staged_diff_toolbar = cx.new(|cx| StagedDiffToolbar::new(workspace, cx));
1517            toolbar.add_item(staged_diff_toolbar, window, cx);
1518            let unstaged_diff_toolbar = cx.new(|cx| UnstagedDiffToolbar::new(workspace, cx));
1519            toolbar.add_item(unstaged_diff_toolbar, window, cx);
1520            let branch_diff_toolbar = cx.new(BranchDiffToolbar::new);
1521            toolbar.add_item(branch_diff_toolbar, window, cx);
1522            let solo_diff_git_toolbar = cx.new(SoloDiffGitToolbar::new);
1523            toolbar.add_item(solo_diff_git_toolbar, window, cx);
1524            let commit_view_toolbar = cx.new(|_| CommitViewToolbar::new());
1525            toolbar.add_item(commit_view_toolbar, window, cx);
1526            let agent_diff_toolbar = cx.new(AgentDiffToolbar::new);
1527            toolbar.add_item(agent_diff_toolbar, window, cx);
1528            let basedpyright_banner = cx.new(|cx| BasedPyrightBanner::new(workspace, cx));
1529            toolbar.add_item(basedpyright_banner, window, cx);
1530            let image_view_toolbar = cx.new(|_| image_viewer::ImageViewToolbarControls::new());
1531            toolbar.add_item(image_view_toolbar, window, cx);
1532        })
1533    });
1534}
1535
1536fn open_about_window(cx: &mut App) {
1537    fn about_window_icon(release_channel: ReleaseChannel) -> Arc<Image> {
1538        let bytes = match release_channel {
1539            ReleaseChannel::Dev => include_bytes!("../resources/app-icon-dev.png").as_slice(),
1540            ReleaseChannel::Nightly => {
1541                include_bytes!("../resources/app-icon-nightly.png").as_slice()
1542            }
1543            ReleaseChannel::Preview => {
1544                include_bytes!("../resources/app-icon-preview.png").as_slice()
1545            }
1546            ReleaseChannel::Stable => include_bytes!("../resources/app-icon.png").as_slice(),
1547        };
1548
1549        Arc::new(Image::from_bytes(ImageFormat::Png, bytes.to_vec()))
1550    }
1551
1552    struct AboutWindow {
1553        focus_handle: FocusHandle,
1554        ok_entry: NavigableEntry,
1555        copy_entry: NavigableEntry,
1556        app_icon: Arc<Image>,
1557        message: SharedString,
1558        commit: Option<SharedString>,
1559        full_version: SharedString,
1560    }
1561
1562    impl AboutWindow {
1563        fn new(cx: &mut Context<Self>) -> Self {
1564            let release_channel = ReleaseChannel::global(cx);
1565            let release_channel_name = release_channel.display_name();
1566            let full_version: SharedString = AppVersion::global(cx).to_string().into();
1567            let version = env!("CARGO_PKG_VERSION");
1568
1569            let debug = if cfg!(debug_assertions) {
1570                "(debug)"
1571            } else {
1572                ""
1573            };
1574            let message: SharedString = format!("{release_channel_name} {version} {debug}").into();
1575            let commit = AppCommitSha::try_global(cx)
1576                .map(|sha| sha.full())
1577                .filter(|commit| !commit.is_empty())
1578                .map(SharedString::from);
1579
1580            Self {
1581                focus_handle: cx.focus_handle(),
1582                ok_entry: NavigableEntry::focusable(cx),
1583                copy_entry: NavigableEntry::focusable(cx),
1584                app_icon: about_window_icon(release_channel),
1585                message,
1586                commit,
1587                full_version,
1588            }
1589        }
1590
1591        fn copy_details(&self, window: &mut Window, cx: &mut Context<Self>) {
1592            let content = match self.commit.as_ref() {
1593                Some(commit) => {
1594                    format!(
1595                        "{}\nCommit: {}\nVersion: {}",
1596                        self.message, commit, self.full_version
1597                    )
1598                }
1599                None => format!("{}\nVersion: {}", self.message, self.full_version),
1600            };
1601            cx.write_to_clipboard(ClipboardItem::new_string(content));
1602            window.remove_window();
1603        }
1604    }
1605
1606    impl Render for AboutWindow {
1607        fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1608            let ok_is_focused = self.ok_entry.focus_handle.contains_focused(window, cx);
1609            let copy_is_focused = self.copy_entry.focus_handle.contains_focused(window, cx);
1610
1611            Navigable::new(
1612                v_flex()
1613                    .id("about-window")
1614                    .track_focus(&self.focus_handle)
1615                    .on_action(cx.listener(|_, _: &menu::Cancel, window, _cx| {
1616                        window.remove_window();
1617                    }))
1618                    .min_w_0()
1619                    .size_full()
1620                    .bg(cx.theme().colors().editor_background)
1621                    .text_color(cx.theme().colors().text)
1622                    .p_4()
1623                    .when(cfg!(target_os = "macos"), |this| this.pt_10())
1624                    .gap_4()
1625                    .text_center()
1626                    .justify_between()
1627                    .child(
1628                        v_flex()
1629                            .w_full()
1630                            .gap_2()
1631                            .items_center()
1632                            .child(img(self.app_icon.clone()).size_16().flex_none())
1633                            .child(Headline::new(self.message.clone()))
1634                            .when_some(self.commit.clone(), |this, commit| {
1635                                this.child(
1636                                    Label::new("Commit")
1637                                        .color(Color::Muted)
1638                                        .size(LabelSize::XSmall),
1639                                )
1640                                .child(Label::new(commit).size(LabelSize::Small))
1641                            })
1642                            .child(
1643                                Label::new("Version")
1644                                    .color(Color::Muted)
1645                                    .size(LabelSize::XSmall),
1646                            )
1647                            .child(Label::new(self.full_version.clone()).size(LabelSize::Small)),
1648                    )
1649                    .child(
1650                        h_flex()
1651                            .w_full()
1652                            .gap_1()
1653                            .child(
1654                                div()
1655                                    .flex_1()
1656                                    .track_focus(&self.ok_entry.focus_handle)
1657                                    .on_action(cx.listener(|_, _: &menu::Confirm, window, _cx| {
1658                                        window.remove_window();
1659                                    }))
1660                                    .child(
1661                                        Button::new("ok", "OK")
1662                                            .full_width()
1663                                            .style(ButtonStyle::OutlinedGhost)
1664                                            .toggle_state(ok_is_focused)
1665                                            .selected_style(ButtonStyle::Tinted(TintColor::Accent))
1666                                            .on_click(cx.listener(|_, _, window, _cx| {
1667                                                window.remove_window();
1668                                            })),
1669                                    ),
1670                            )
1671                            .child(
1672                                div()
1673                                    .flex_1()
1674                                    .track_focus(&self.copy_entry.focus_handle)
1675                                    .on_action(cx.listener(
1676                                        |this, _: &menu::Confirm, window, cx| {
1677                                            this.copy_details(window, cx);
1678                                        },
1679                                    ))
1680                                    .child(
1681                                        Button::new("copy", "Copy")
1682                                            .full_width()
1683                                            .style(ButtonStyle::Tinted(TintColor::Accent))
1684                                            .toggle_state(copy_is_focused)
1685                                            .selected_style(ButtonStyle::Tinted(TintColor::Accent))
1686                                            .on_click(cx.listener(|this, _event, window, cx| {
1687                                                this.copy_details(window, cx);
1688                                            })),
1689                                    ),
1690                            ),
1691                    )
1692                    .into_any_element(),
1693            )
1694            .entry(self.ok_entry.clone())
1695            .entry(self.copy_entry.clone())
1696        }
1697    }
1698
1699    impl Focusable for AboutWindow {
1700        fn focus_handle(&self, _cx: &App) -> FocusHandle {
1701            self.ok_entry.focus_handle.clone()
1702        }
1703    }
1704
1705    // Don't open about window twice
1706    if let Some(existing) = cx
1707        .windows()
1708        .into_iter()
1709        .find_map(|w| w.downcast::<AboutWindow>())
1710    {
1711        existing
1712            .update(cx, |about_window, window, cx| {
1713                window.activate_window();
1714                about_window.ok_entry.focus_handle.focus(window, cx);
1715            })
1716            .log_err();
1717        return;
1718    }
1719
1720    let window_size = Size {
1721        width: px(440.),
1722        height: px(300.),
1723    };
1724
1725    cx.open_window(
1726        WindowOptions {
1727            titlebar: Some(TitlebarOptions {
1728                title: Some("About Omega".into()),
1729                appears_transparent: true,
1730                traffic_light_position: Some(point(px(12.), px(12.))),
1731            }),
1732            window_bounds: Some(WindowBounds::centered(window_size, cx)),
1733            is_resizable: false,
1734            is_minimizable: false,
1735            kind: WindowKind::Floating,
1736            app_id: Some(ReleaseChannel::global(cx).app_id().to_owned()),
1737            ..Default::default()
1738        },
1739        |window, cx| {
1740            let about_window = cx.new(AboutWindow::new);
1741            let focus_handle = about_window.read(cx).ok_entry.focus_handle.clone();
1742            window.activate_window();
1743            focus_handle.focus(window, cx);
1744            about_window
1745        },
1746    )
1747    .log_err();
1748}
1749
1750#[cfg(not(target_os = "windows"))]
1751fn install_cli(
1752    _: &mut Workspace,
1753    _: &install_cli::InstallCliBinary,
1754    window: &mut Window,
1755    cx: &mut Context<Workspace>,
1756) {
1757    install_cli::install_cli_binary(window, cx)
1758}
1759
1760static WAITING_QUIT_CONFIRMATION: AtomicBool = AtomicBool::new(false);
1761fn quit(_: &Quit, cx: &mut App) {
1762    if WAITING_QUIT_CONFIRMATION.load(atomic::Ordering::Acquire) {
1763        return;
1764    }
1765
1766    let should_confirm = WorkspaceSettings::get_global(cx).confirm_quit;
1767    cx.spawn(async move |cx| {
1768        let mut workspace_windows: Vec<WindowHandle<MultiWorkspace>> = cx.update(|cx| {
1769            cx.windows()
1770                .into_iter()
1771                .filter_map(|window| window.downcast::<MultiWorkspace>())
1772                .collect::<Vec<_>>()
1773        });
1774
1775        // If multiple windows have unsaved changes, and need a save prompt,
1776        // prompt in the active window before switching to a different window.
1777        cx.update(|cx| {
1778            workspace_windows.sort_by_key(|window| window.is_active(cx) == Some(false));
1779        });
1780
1781        if should_confirm && let Some(multi_workspace) = workspace_windows.first() {
1782            let answer = multi_workspace
1783                .update(cx, |_, window, cx| {
1784                    window.prompt(
1785                        PromptLevel::Info,
1786                        "Are you sure you want to quit?",
1787                        None,
1788                        &["Quit", "Cancel"],
1789                        cx,
1790                    )
1791                })
1792                .log_err();
1793
1794            if let Some(answer) = answer {
1795                WAITING_QUIT_CONFIRMATION.store(true, atomic::Ordering::Release);
1796                let answer = answer.await.ok();
1797                WAITING_QUIT_CONFIRMATION.store(false, atomic::Ordering::Release);
1798                if answer != Some(0) {
1799                    return Ok(());
1800                }
1801            }
1802        }
1803
1804        // If the user cancels any save prompt, then keep the app open.
1805        for window in &workspace_windows {
1806            let window = *window;
1807            let active_and_workspaces = window
1808                .update(cx, |multi_workspace, _, _cx| {
1809                    (
1810                        multi_workspace.workspace().clone(),
1811                        multi_workspace.workspaces().cloned().collect::<Vec<_>>(),
1812                    )
1813                })
1814                .log_err();
1815
1816            let Some((originally_active, workspaces)) = active_and_workspaces else {
1817                continue;
1818            };
1819
1820            for workspace in workspaces {
1821                if let Some(should_close) = window
1822                    .update(cx, |multi_workspace, window, cx| {
1823                        multi_workspace.activate(workspace.clone(), None, window, cx);
1824                        window.activate_window();
1825                        workspace.update(cx, |workspace, cx| {
1826                            workspace.prepare_to_close(CloseIntent::Quit, window, cx)
1827                        })
1828                    })
1829                    .log_err()
1830                {
1831                    if !should_close.await? {
1832                        // Activating each workspace above to surface its save
1833                        // prompts changed which workspace is active. Restore the
1834                        // user's focused workspace before bailing so the window
1835                        // is left as they had it.
1836                        window
1837                            .update(cx, |multi_workspace, window, cx| {
1838                                multi_workspace.activate(
1839                                    originally_active.clone(),
1840                                    None,
1841                                    window,
1842                                    cx,
1843                                );
1844                            })
1845                            .log_err();
1846                        return Ok(());
1847                    }
1848                }
1849            }
1850
1851            // The loop above activated each workspace in turn, overwriting the
1852            // persisted active workspace. Re-activate the workspace the user
1853            // actually had focused so it is the one serialized (and restored on
1854            // next launch) as active, rather than whichever happened to be last.
1855            window
1856                .update(cx, |multi_workspace, window, cx| {
1857                    multi_workspace.activate(originally_active, None, window, cx);
1858                })
1859                .log_err();
1860        }
1861        // Flush all pending workspace serialization before quitting so that
1862        // session_id/window_id are up-to-date in the database.
1863        let mut flush_tasks = Vec::new();
1864        for window in &workspace_windows {
1865            window
1866                .update(cx, |multi_workspace, window, cx| {
1867                    for workspace in multi_workspace.workspaces() {
1868                        flush_tasks.push(workspace.update(cx, |workspace, cx| {
1869                            workspace.flush_serialization(window, cx)
1870                        }));
1871                    }
1872                    flush_tasks.append(&mut multi_workspace.take_pending_removal_tasks());
1873                    flush_tasks.push(multi_workspace.flush_serialization());
1874                })
1875                .log_err();
1876        }
1877        futures::future::join_all(flush_tasks).await;
1878
1879        cx.update(|cx| cx.quit());
1880        anyhow::Ok(())
1881    })
1882    .detach_and_log_err(cx);
1883}
1884
1885fn open_log_file(workspace: &mut Workspace, window: &mut Window, cx: &mut Context<Workspace>) {
1886    const MAX_LINES: usize = 1000;
1887    let app_state = workspace.app_state();
1888    let languages = app_state.languages.clone();
1889    let fs = app_state.fs.clone();
1890    cx.spawn_in(window, async move |workspace, cx| {
1891        let log = {
1892            let result = futures::join!(
1893                fs.load(&paths::old_log_file()),
1894                fs.load(&paths::log_file()),
1895                languages.language_for_name("log")
1896            );
1897            match result {
1898                (Err(_), Err(e), _) => Err(e),
1899                (old_log, new_log, lang) => {
1900                    let mut lines = VecDeque::with_capacity(MAX_LINES);
1901                    for line in old_log
1902                        .iter()
1903                        .flat_map(|log| log.lines())
1904                        .chain(new_log.iter().flat_map(|log| log.lines()))
1905                    {
1906                        if lines.len() == MAX_LINES {
1907                            lines.pop_front();
1908                        }
1909                        lines.push_back(line);
1910                    }
1911                    Ok((
1912                        lines
1913                            .into_iter()
1914                            .flat_map(|line| [line, "\n"])
1915                            .collect::<String>(),
1916                        lang.ok(),
1917                    ))
1918                }
1919            }
1920        };
1921
1922        let (log, log_language) = match log {
1923            Ok((log, log_language)) => (log, log_language),
1924            Err(e) => {
1925                struct OpenLogError;
1926
1927                workspace
1928                    .update(cx, |workspace, cx| {
1929                        workspace.show_notification(
1930                            NotificationId::unique::<OpenLogError>(),
1931                            cx,
1932                            |cx| {
1933                                cx.new(|cx| {
1934                                    MessageNotification::new(
1935                                        format!(
1936                                            "Unable to access/open log file at path \
1937                                                    {}: {e:#}",
1938                                            paths::log_file().display()
1939                                        ),
1940                                        cx,
1941                                    )
1942                                })
1943                            },
1944                        );
1945                    })
1946                    .ok();
1947                return;
1948            }
1949        };
1950        maybe!(async move {
1951            let project = workspace
1952                .read_with(cx, |workspace, _| workspace.project().clone())
1953                .ok()?;
1954            let buffer = project
1955                .update(cx, |project, cx| {
1956                    project.create_buffer(log_language, false, cx)
1957                })
1958                .await
1959                .ok()?;
1960            buffer.update(cx, |buffer, cx| {
1961                buffer.set_capability(Capability::ReadOnly, cx);
1962                buffer.set_text(log, cx);
1963            });
1964
1965            let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx).with_title("Log".into()));
1966
1967            let editor = cx
1968                .new_window_entity(|window, cx| {
1969                    let mut editor = Editor::for_multibuffer(buffer, Some(project), window, cx);
1970                    editor.set_read_only(true);
1971                    editor.set_breadcrumb_header(format!(
1972                        "Last {} lines in {}",
1973                        MAX_LINES,
1974                        paths::log_file().display()
1975                    ));
1976                    let last_multi_buffer_offset = editor.buffer().read(cx).len(cx);
1977                    editor.change_selections(Default::default(), window, cx, |s| {
1978                        s.select_ranges(Some(last_multi_buffer_offset..last_multi_buffer_offset));
1979                    });
1980                    editor
1981                })
1982                .ok()?;
1983
1984            workspace
1985                .update_in(cx, |workspace, window, cx| {
1986                    workspace.add_item_to_active_pane(Box::new(editor), None, true, window, cx);
1987                })
1988                .ok()
1989        })
1990        .await;
1991    })
1992    .detach();
1993}
1994
1995fn notify_settings_errors(result: settings::SettingsParseResult, is_user: bool, cx: &mut App) {
1996    if let settings::ParseStatus::Failed { error: err } = &result.parse_status {
1997        let settings_type = if is_user { "user" } else { "global" };
1998        log::error!("Failed to load {} settings: {err}", settings_type);
1999    }
2000
2001    let error = match result.parse_status {
2002        settings::ParseStatus::Failed { error } => Some(anyhow::format_err!(error)),
2003        settings::ParseStatus::Success => None,
2004        settings::ParseStatus::Unchanged => return,
2005    };
2006    let id = NotificationId::Named(format!("failed-to-parse-settings-{is_user}").into());
2007
2008    let showed_parse_error = match error {
2009        Some(error) => {
2010            if let Some(InvalidSettingsError::LocalSettings { .. }) =
2011                error.downcast_ref::<InvalidSettingsError>()
2012            {
2013                false
2014                // Local settings errors are displayed by the projects
2015            } else {
2016                show_app_notification(id, cx, move |cx| {
2017                    cx.new(|cx| {
2018                        MessageNotification::new(format!("Invalid user settings file\n{error}"), cx)
2019                            .primary_message("Open Settings File")
2020                            .primary_icon(IconName::Settings)
2021                            .primary_on_click(|window, cx| {
2022                                window.dispatch_action(
2023                                    zed_actions::OpenSettingsFile.boxed_clone(),
2024                                    cx,
2025                                );
2026                                cx.emit(DismissEvent);
2027                            })
2028                    })
2029                });
2030                true
2031            }
2032        }
2033        None => {
2034            dismiss_app_notification(&id, cx);
2035            false
2036        }
2037    };
2038    let id = NotificationId::Named(format!("failed-to-migrate-settings-{is_user}").into());
2039
2040    match result.migration_status {
2041        settings::MigrationStatus::Succeeded | settings::MigrationStatus::NotNeeded => {
2042            dismiss_app_notification(&id, cx);
2043        }
2044        settings::MigrationStatus::Failed { error: err } => {
2045            if !showed_parse_error {
2046                show_app_notification(id, cx, move |cx| {
2047                    cx.new(|cx| {
2048                        MessageNotification::new(
2049                            format!(
2050                                "Failed to migrate settings\n\
2051                                {err}"
2052                            ),
2053                            cx,
2054                        )
2055                        .primary_message("Open Settings File")
2056                        .primary_icon(IconName::Settings)
2057                        .primary_on_click(|window, cx| {
2058                            window.dispatch_action(zed_actions::OpenSettingsFile.boxed_clone(), cx);
2059                            cx.emit(DismissEvent);
2060                        })
2061                    })
2062                });
2063            }
2064        }
2065    };
2066}
2067
2068#[derive(Copy, Clone, Debug, settings::RegisterSetting)]
2069struct CursorHideModeSetting(gpui::CursorHideMode);
2070
2071impl Settings for CursorHideModeSetting {
2072    fn from_settings(content: &settings::SettingsContent) -> Self {
2073        Self(match content.hide_mouse.unwrap_or_default() {
2074            settings::HideMouseMode::Never => gpui::CursorHideMode::Never,
2075            settings::HideMouseMode::OnTyping => gpui::CursorHideMode::OnTyping,
2076            settings::HideMouseMode::OnTypingAndAction => gpui::CursorHideMode::OnTypingAndAction,
2077        })
2078    }
2079}
2080
2081fn init_cursor_hide_mode(cx: &mut App) {
2082    let apply = |cx: &mut App| cx.set_cursor_hide_mode(CursorHideModeSetting::get_global(cx).0);
2083    apply(cx);
2084    cx.observe_global::<SettingsStore>(apply).detach();
2085}
2086
2087#[derive(Copy, Clone, Debug, settings::RegisterSetting)]
2088struct ReduceMotionSetting(settings::ReduceMotionMode);
2089
2090impl Settings for ReduceMotionSetting {
2091    fn from_settings(content: &settings::SettingsContent) -> Self {
2092        Self(content.reduce_motion.unwrap_or_default())
2093    }
2094}
2095
2096fn init_reduce_motion(cx: &mut App) {
2097    let apply = |cx: &mut App| {
2098        let reduce_motion = ReduceMotionSetting::get_global(cx).0 == settings::ReduceMotionMode::On;
2099        cx.set_reduce_motion(reduce_motion);
2100    };
2101    apply(cx);
2102    cx.observe_global::<SettingsStore>(apply).detach();
2103}
2104
2105/// Starts watching `~/.config/zed/AGENTS.md` (or the platform equivalent) and
2106/// surfaces any read errors using the same notification UI as settings errors.
2107///
2108/// The file itself is loaded into [`agent_settings::UserAgentsMd`] for inclusion
2109/// in prompts.
2110pub fn watch_user_agents_md(fs: Arc<dyn fs::Fs>, cx: &mut App) {
2111    struct UserAgentsMdParseError;
2112    let notification_id = NotificationId::unique::<UserAgentsMdParseError>();
2113
2114    init_user_agents_md(fs, cx, move |state, cx| match state {
2115        UserAgentsMdState::Loaded(_) | UserAgentsMdState::Empty => {
2116            dismiss_app_notification(&notification_id, cx);
2117        }
2118        UserAgentsMdState::Error(message) => {
2119            let path = paths::agents_file().display().to_string();
2120            log::error!("Failed to load user AGENTS.md from {path}: {message}");
2121            let body = format!("Failed to load {path}\n{message}");
2122            let notification_id = notification_id.clone();
2123            show_app_notification(notification_id, cx, move |cx| {
2124                let body = body.clone();
2125                cx.new(|cx| MessageNotification::new(body, cx))
2126            });
2127        }
2128    });
2129}
2130
2131pub fn watch_settings_files(fs: Arc<dyn fs::Fs>, cx: &mut App) {
2132    MigrationNotification::set_global(cx.new(|_| MigrationNotification), cx);
2133
2134    SettingsStore::update_global(cx, move |store, cx| {
2135        store.watch_settings_files(fs, cx, |settings_file, result, cx| {
2136            let is_user = matches!(settings_file, SettingsFile::User);
2137            let migrating_in_memory =
2138                matches!(&result.migration_status, MigrationStatus::Succeeded);
2139            notify_settings_errors(result, is_user, cx);
2140            if let Some(notifier) = MigrationNotification::try_global(cx) {
2141                notifier.update(cx, |_, cx| {
2142                    cx.emit(MigrationEvent::ContentChanged {
2143                        migration_type: MigrationType::Settings,
2144                        migrating_in_memory,
2145                    });
2146                });
2147            }
2148        });
2149    });
2150}
2151
2152pub fn handle_keymap_file_changes(
2153    mut user_keymap_file_rx: mpsc::UnboundedReceiver<String>,
2154    user_keymap_watcher: gpui::Task<()>,
2155    cx: &mut App,
2156) {
2157    let (base_keymap_tx, mut base_keymap_rx) = mpsc::unbounded();
2158    let (keyboard_layout_tx, mut keyboard_layout_rx) = mpsc::unbounded();
2159    let mut old_base_keymap = *BaseKeymap::get_global(cx);
2160    let mut old_vim_enabled = VimModeSetting::get_global(cx).0;
2161    let mut old_helix_enabled = vim_mode_setting::HelixModeSetting::get_global(cx).0;
2162    let mut old_disable_ai = DisableAiSettings::get_global(cx).disable_ai;
2163
2164    cx.observe_global::<SettingsStore>(move |cx| {
2165        let new_base_keymap = *BaseKeymap::get_global(cx);
2166        let new_vim_enabled = VimModeSetting::get_global(cx).0;
2167        let new_helix_enabled = vim_mode_setting::HelixModeSetting::get_global(cx).0;
2168        let new_disable_ai = DisableAiSettings::get_global(cx).disable_ai;
2169
2170        if new_base_keymap != old_base_keymap
2171            || new_vim_enabled != old_vim_enabled
2172            || new_helix_enabled != old_helix_enabled
2173            || new_disable_ai != old_disable_ai
2174        {
2175            old_base_keymap = new_base_keymap;
2176            old_vim_enabled = new_vim_enabled;
2177            old_helix_enabled = new_helix_enabled;
2178            old_disable_ai = new_disable_ai;
2179
2180            base_keymap_tx.unbounded_send(()).unwrap();
2181        }
2182    })
2183    .detach();
2184
2185    #[cfg(target_os = "windows")]
2186    {
2187        let mut current_layout_id = cx.keyboard_layout().id().to_string();
2188        cx.on_keyboard_layout_change(move |cx| {
2189            let next_layout_id = cx.keyboard_layout().id();
2190            if next_layout_id != current_layout_id {
2191                current_layout_id = next_layout_id.to_string();
2192                keyboard_layout_tx.unbounded_send(()).ok();
2193            }
2194        })
2195        .detach();
2196    }
2197
2198    #[cfg(not(target_os = "windows"))]
2199    {
2200        let mut current_mapping = cx.keyboard_mapper().get_key_equivalents().cloned();
2201        cx.on_keyboard_layout_change(move |cx| {
2202            let next_mapping = cx.keyboard_mapper().get_key_equivalents();
2203            if current_mapping.as_ref() != next_mapping {
2204                current_mapping = next_mapping.cloned();
2205                keyboard_layout_tx.unbounded_send(()).ok();
2206            }
2207        })
2208        .detach();
2209    }
2210
2211    load_default_keymap(cx);
2212
2213    struct KeymapParseErrorNotification;
2214    let notification_id = NotificationId::unique::<KeymapParseErrorNotification>();
2215
2216    cx.spawn(async move |cx| {
2217        let _user_keymap_watcher = user_keymap_watcher;
2218        let mut user_keymap_content = String::new();
2219        let mut migrating_in_memory = false;
2220        loop {
2221            select_biased! {
2222                _ = base_keymap_rx.next() => {},
2223                _ = keyboard_layout_rx.next() => {},
2224                content = user_keymap_file_rx.next() => {
2225                    if let Some(content) = content {
2226                        if let Ok(Some(migrated_content)) = migrate_keymap(&content) {
2227                            user_keymap_content = migrated_content;
2228                            migrating_in_memory = true;
2229                        } else {
2230                            user_keymap_content = content;
2231                            migrating_in_memory = false;
2232                        }
2233                    }
2234                }
2235            };
2236            cx.update(|cx| {
2237                if let Some(notifier) = MigrationNotification::try_global(cx) {
2238                    notifier.update(cx, |_, cx| {
2239                        cx.emit(MigrationEvent::ContentChanged {
2240                            migration_type: MigrationType::Keymap,
2241                            migrating_in_memory,
2242                        });
2243                    });
2244                }
2245                let load_result = KeymapFile::load(&user_keymap_content, cx);
2246                match load_result {
2247                    KeymapFileLoadResult::Success { key_bindings } => {
2248                        reload_keymaps(cx, key_bindings);
2249                        dismiss_app_notification(&notification_id.clone(), cx);
2250                    }
2251                    KeymapFileLoadResult::SomeFailedToLoad {
2252                        key_bindings,
2253                        error_message,
2254                    } => {
2255                        if !key_bindings.is_empty() {
2256                            reload_keymaps(cx, key_bindings);
2257                        }
2258                        show_keymap_file_load_error(notification_id.clone(), error_message, cx);
2259                    }
2260                    KeymapFileLoadResult::JsonParseFailure { error } => {
2261                        show_keymap_file_json_error(notification_id.clone(), &error, cx)
2262                    }
2263                }
2264            });
2265        }
2266    })
2267    .detach();
2268}
2269
2270fn show_keymap_file_json_error(
2271    notification_id: NotificationId,
2272    error: &anyhow::Error,
2273    cx: &mut App,
2274) {
2275    let message: SharedString =
2276        format!("JSON parse error in keymap file. Bindings not reloaded.\n\n{error}").into();
2277    show_app_notification(notification_id, cx, move |cx| {
2278        cx.new(|cx| {
2279            MessageNotification::new(message.clone(), cx)
2280                .primary_message("Open Keymap File")
2281                .primary_icon(IconName::Settings)
2282                .primary_on_click(|window, cx| {
2283                    window.dispatch_action(zed_actions::OpenKeymapFile.boxed_clone(), cx);
2284                    cx.emit(DismissEvent);
2285                })
2286        })
2287    });
2288}
2289
2290fn show_keymap_file_load_error(
2291    notification_id: NotificationId,
2292    error_message: MarkdownString,
2293    cx: &mut App,
2294) {
2295    show_markdown_app_notification(
2296        notification_id,
2297        error_message,
2298        "Open Keymap File".into(),
2299        |window, cx| {
2300            window.dispatch_action(zed_actions::OpenKeymapFile.boxed_clone(), cx);
2301            cx.emit(DismissEvent);
2302        },
2303        cx,
2304    )
2305}
2306
2307fn show_markdown_app_notification<F>(
2308    notification_id: NotificationId,
2309    message: MarkdownString,
2310    primary_button_message: SharedString,
2311    primary_button_on_click: F,
2312    cx: &mut App,
2313) where
2314    F: 'static + Send + Sync + Fn(&mut Window, &mut Context<MessageNotification>),
2315{
2316    let markdown = cx.new(|cx| Markdown::new(message.0.into(), None, None, cx));
2317    let primary_button_on_click = Arc::new(primary_button_on_click);
2318
2319    show_app_notification(notification_id, cx, move |cx| {
2320        let markdown = markdown.clone();
2321        let primary_button_message = primary_button_message.clone();
2322        let primary_button_on_click = primary_button_on_click.clone();
2323
2324        cx.new(move |cx| {
2325            MessageNotification::new_from_builder(cx, move |window, cx| {
2326                image_cache(retain_all("notification-cache"))
2327                    .child(div().text_ui(cx).child(MarkdownElement::new(
2328                        markdown.clone(),
2329                        MarkdownStyle::themed(MarkdownFont::Editor, window, cx),
2330                    )))
2331                    .into_any()
2332            })
2333            .primary_message(primary_button_message)
2334            .primary_icon(IconName::Settings)
2335            .primary_on_click_arc(primary_button_on_click)
2336        })
2337    })
2338}
2339
2340fn reload_keymaps(cx: &mut App, mut user_key_bindings: Vec<KeyBinding>) {
2341    cx.clear_key_bindings();
2342    load_default_keymap(cx);
2343
2344    for key_binding in &mut user_key_bindings {
2345        key_binding.set_meta(KeybindSource::User.meta());
2346    }
2347    cx.bind_keys(filter_disabled_ai_bindings(user_key_bindings, cx));
2348
2349    let menus = app_menus(cx);
2350    cx.set_menus(menus);
2351    // On Windows, this is set in the `update_jump_list` method of the `HistoryManager`.
2352    #[cfg(not(target_os = "windows"))]
2353    cx.set_dock_menu(vec![gpui::MenuItem::action(
2354        "New Window",
2355        workspace::NewWindow,
2356    )]);
2357    // todo: nicer api here?
2358    keymap_editor::KeymapEventChannel::trigger_keymap_changed(cx);
2359}
2360
2361pub fn load_default_keymap(cx: &mut App) {
2362    let base_keymap = *BaseKeymap::get_global(cx);
2363    if base_keymap == BaseKeymap::None {
2364        return;
2365    }
2366
2367    cx.bind_keys(filter_disabled_ai_bindings(
2368        KeymapFile::load_asset(DEFAULT_KEYMAP_PATH, Some(KeybindSource::Default), cx).unwrap(),
2369        cx,
2370    ));
2371
2372    if let Some(asset_path) = base_keymap.asset_path() {
2373        cx.bind_keys(filter_disabled_ai_bindings(
2374            KeymapFile::load_asset(asset_path, Some(KeybindSource::Base), cx).unwrap(),
2375            cx,
2376        ));
2377    }
2378
2379    if VimModeSetting::get_global(cx).0 || vim_mode_setting::HelixModeSetting::get_global(cx).0 {
2380        cx.bind_keys(filter_disabled_ai_bindings(
2381            KeymapFile::load_asset(VIM_KEYMAP_PATH, Some(KeybindSource::Vim), cx).unwrap(),
2382            cx,
2383        ));
2384    }
2385
2386    cx.bind_keys(
2387        KeymapFile::load_asset(
2388            SPECIFIC_OVERRIDES_KEYMAP_PATH,
2389            Some(KeybindSource::Default),
2390            cx,
2391        )
2392        .unwrap(),
2393    );
2394}
2395
2396/// Namespaces of actions that are part of an AI feature. When the user opts out
2397/// of AI via the `disable_ai` setting, bindings to these actions are dropped so
2398/// that lower-precedence editor defaults (e.g. `editor::NewlineBelow` for
2399/// `ctrl-enter`) can fire instead of being shadowed by an action whose handler
2400/// silently no-ops.
2401const AI_ACTION_NAMESPACES: &[&str] = &[
2402    "acp::",
2403    "agent::",
2404    "assistant::",
2405    "edit_prediction::",
2406    "inline_assistant::",
2407    "zeta::",
2408];
2409
2410fn is_ai_keybinding(binding: &KeyBinding) -> bool {
2411    let name = binding.action().name();
2412    AI_ACTION_NAMESPACES
2413        .iter()
2414        .any(|namespace| name.starts_with(namespace))
2415}
2416
2417fn filter_disabled_ai_bindings(bindings: Vec<KeyBinding>, cx: &App) -> Vec<KeyBinding> {
2418    if !DisableAiSettings::get_global(cx).disable_ai {
2419        return bindings;
2420    }
2421    bindings
2422        .into_iter()
2423        .filter(|binding| !is_ai_keybinding(binding))
2424        .collect()
2425}
2426
2427pub fn open_new_ssh_project_from_project(
2428    workspace: &mut Workspace,
2429    paths: Vec<PathBuf>,
2430    window: &mut Window,
2431    cx: &mut Context<Workspace>,
2432) -> Task<anyhow::Result<()>> {
2433    let app_state = workspace.app_state().clone();
2434    let Some(ssh_client) = workspace.project().read(cx).remote_client() else {
2435        return Task::ready(Err(anyhow::anyhow!("Not an ssh project")));
2436    };
2437    let connection_options = ssh_client.read(cx).connection_options();
2438    cx.spawn_in(window, async move |_, cx| {
2439        open_remote_project(
2440            connection_options,
2441            paths,
2442            app_state,
2443            workspace::OpenOptions {
2444                workspace_matching: workspace::WorkspaceMatching::None,
2445                ..Default::default()
2446            },
2447            cx,
2448        )
2449        .await
2450        .map(|_| ())
2451    })
2452}
2453
2454fn open_project_settings_file(
2455    workspace: &mut Workspace,
2456    _: &OpenProjectSettingsFile,
2457    window: &mut Window,
2458    cx: &mut Context<Workspace>,
2459) {
2460    if let Some(task) = open_local_file(
2461        workspace,
2462        local_settings_file_relative_path(),
2463        initial_project_settings_content(),
2464        window,
2465        cx,
2466    ) {
2467        task.detach_and_log_err(cx);
2468    }
2469}
2470
2471fn open_project_tasks_file(
2472    workspace: &mut Workspace,
2473    _: &OpenProjectTasks,
2474    window: &mut Window,
2475    cx: &mut Context<Workspace>,
2476) {
2477    if let Some(task) = open_local_file(
2478        workspace,
2479        local_tasks_file_relative_path(),
2480        initial_tasks_content(),
2481        window,
2482        cx,
2483    ) {
2484        task.detach_and_log_err(cx);
2485    }
2486}
2487
2488fn open_worktree_setup_tasks_file(
2489    workspace: &mut Workspace,
2490    _: &zed_actions::OpenWorktreeSetupTasks,
2491    window: &mut Window,
2492    cx: &mut Context<Workspace>,
2493) {
2494    // Kept harmless on purpose: tasks with the `create_worktree` hook run automatically
2495    // when a worktree is created, so the example must be safe to save unedited.
2496    const WORKTREE_SETUP_TASK_EXAMPLE: &str = r#"  {
2497    // Runs automatically after Omega creates a new git worktree.
2498    // $ZED_WORKTREE_ROOT is the new worktree's root directory, and
2499    // $ZED_MAIN_GIT_WORKTREE is the original repository's working directory.
2500    "label": "Set up new worktree",
2501    "command": "echo \"Setting up $ZED_WORKTREE_ROOT — edit this command\"",
2502    "cwd": "$ZED_WORKTREE_ROOT",
2503    "hooks": ["create_worktree"]
2504  }"#;
2505
2506    let Some(open_task) = open_local_file(
2507        workspace,
2508        local_tasks_file_relative_path(),
2509        settings::initial_worktree_setup_tasks_content(),
2510        window,
2511        cx,
2512    ) else {
2513        return;
2514    };
2515
2516    cx.spawn_in(window, async move |_, cx| {
2517        let editor = open_task.await?;
2518        editor.update_in(cx, |editor, window, cx| {
2519            // Skip insertion if the file already mentions the hook (even in a comment,
2520            // like the seeded template's example — uncommenting it beats duplicating it).
2521            // `create_git_worktree` is a serde alias for the same hook.
2522            let text = editor.text(cx);
2523            if text.contains("create_worktree") || text.contains("create_git_worktree") {
2524                return anyhow::Ok(());
2525            }
2526            tasks_ui::insert_task_json_into_editor(
2527                editor,
2528                WORKTREE_SETUP_TASK_EXAMPLE.to_string(),
2529                window,
2530                cx,
2531            )
2532        })?
2533    })
2534    .detach_and_log_err(cx);
2535}
2536
2537fn open_project_debug_tasks_file(
2538    workspace: &mut Workspace,
2539    _: &zed_actions::OpenProjectDebugTasks,
2540    window: &mut Window,
2541    cx: &mut Context<Workspace>,
2542) {
2543    if let Some(task) = open_local_file(
2544        workspace,
2545        local_debug_file_relative_path(),
2546        initial_local_debug_tasks_content(),
2547        window,
2548        cx,
2549    ) {
2550        task.detach_and_log_err(cx);
2551    }
2552}
2553
2554fn open_local_file(
2555    workspace: &mut Workspace,
2556    settings_relative_path: &'static RelPath,
2557    initial_contents: Cow<'static, str>,
2558    window: &mut Window,
2559    cx: &mut Context<Workspace>,
2560) -> Option<gpui::Task<anyhow::Result<Entity<Editor>>>> {
2561    let project = workspace.project().clone();
2562    let worktree = project
2563        .read(cx)
2564        .visible_worktrees(cx)
2565        .find_map(|tree| tree.read(cx).root_entry()?.is_dir().then_some(tree));
2566    if let Some(worktree) = worktree {
2567        let tree_id = worktree.read(cx).id();
2568        Some(cx.spawn_in(window, async move |workspace, cx| {
2569            // Check if the file actually exists on disk (even if it's excluded from worktree)
2570            let file_exists = {
2571                let full_path = worktree.read_with(cx, |tree, _| {
2572                    tree.abs_path().join(settings_relative_path.as_std_path())
2573                });
2574
2575                let fs = project.read_with(cx, |project, _| project.fs().clone());
2576
2577                fs.metadata(&full_path)
2578                    .await
2579                    .ok()
2580                    .flatten()
2581                    .is_some_and(|metadata| !metadata.is_dir && !metadata.is_fifo)
2582            };
2583
2584            if !file_exists {
2585                if let Some(dir_path) = settings_relative_path.parent()
2586                    && worktree.read_with(cx, |tree, _| tree.entry_for_path(dir_path).is_none())
2587                {
2588                    project
2589                        .update(cx, |project, cx| {
2590                            project.create_entry((tree_id, dir_path), true, cx)
2591                        })
2592                        .await
2593                        .context("worktree was removed")?;
2594                }
2595
2596                if worktree.read_with(cx, |tree, _| {
2597                    tree.entry_for_path(settings_relative_path).is_none()
2598                }) {
2599                    project
2600                        .update(cx, |project, cx| {
2601                            project.create_entry((tree_id, settings_relative_path), false, cx)
2602                        })
2603                        .await
2604                        .context("worktree was removed")?;
2605                }
2606            }
2607
2608            let editor = workspace
2609                .update_in(cx, |workspace, window, cx| {
2610                    workspace.open_path((tree_id, settings_relative_path), None, true, window, cx)
2611                })?
2612                .await?
2613                .downcast::<Editor>()
2614                .context("unexpected item type: expected editor item")?;
2615
2616            editor.update(cx, |editor, cx| {
2617                if let Some(buffer) = editor.buffer().read(cx).as_singleton()
2618                    && buffer.read(cx).is_empty()
2619                {
2620                    buffer.update(cx, |buffer, cx| {
2621                        buffer.edit([(0..0, initial_contents)], None, cx)
2622                    });
2623                }
2624            });
2625
2626            anyhow::Ok(editor)
2627        }))
2628    } else {
2629        struct NoOpenFolders;
2630
2631        workspace.show_notification(NotificationId::unique::<NoOpenFolders>(), cx, |cx| {
2632            cx.new(|cx| MessageNotification::new("This project has no folders open.", cx))
2633        });
2634        None
2635    }
2636}
2637
2638fn open_bundled_file(
2639    workspace: &mut Workspace,
2640    text: Cow<'static, str>,
2641    title: &'static str,
2642    language: &'static str,
2643    window: &mut Window,
2644    cx: &mut Context<Workspace>,
2645) {
2646    let existing = workspace.items_of_type::<Editor>(cx).find(|editor| {
2647        editor.read_with(cx, |editor, cx| {
2648            editor.read_only(cx)
2649                && editor.title(cx).as_ref() == title
2650                && editor
2651                    .buffer()
2652                    .read(cx)
2653                    .as_singleton()
2654                    .is_some_and(|buffer| buffer.read(cx).file().is_none())
2655        })
2656    });
2657    if let Some(existing) = existing {
2658        workspace.activate_item(&existing, true, true, window, cx);
2659        return;
2660    }
2661
2662    let language = workspace.app_state().languages.language_for_name(language);
2663    cx.spawn_in(window, async move |workspace, cx| {
2664        let language = language.await.log_err();
2665        workspace
2666            .update_in(cx, move |workspace, window, cx| {
2667                let project = workspace.project().clone();
2668                let buffer = project.update(cx, move |project, cx| {
2669                    project.create_buffer(language, false, cx)
2670                });
2671                cx.spawn_in(window, async move |workspace, cx| {
2672                    let buffer = buffer.await?;
2673                    buffer.update(cx, |buffer, cx| {
2674                        buffer.set_text(text.into_owned(), cx);
2675                        buffer.set_capability(Capability::ReadOnly, cx);
2676                    });
2677                    let buffer =
2678                        cx.new(|cx| MultiBuffer::singleton(buffer, cx).with_title(title.into()));
2679                    workspace.update_in(cx, |workspace, window, cx| {
2680                        workspace.add_item_to_active_pane(
2681                            Box::new(cx.new(|cx| {
2682                                let mut editor = Editor::for_multibuffer(
2683                                    buffer,
2684                                    Some(project.clone()),
2685                                    window,
2686                                    cx,
2687                                );
2688                                editor.set_read_only(true);
2689                                editor.set_should_serialize(false, cx);
2690                                editor.set_breadcrumb_header(title.into());
2691                                editor
2692                            })),
2693                            None,
2694                            true,
2695                            window,
2696                            cx,
2697                        )
2698                    })
2699                })
2700            })?
2701            .await
2702    })
2703    .detach_and_log_err(cx);
2704}
2705
2706fn open_settings_file(
2707    abs_path: &'static Path,
2708    default_content: impl FnOnce() -> Rope + Send + 'static,
2709    window: &mut Window,
2710    cx: &mut Context<Workspace>,
2711) {
2712    cx.spawn_in(window, async move |workspace, cx| {
2713        workspace
2714            .update_in(cx, |workspace, window, cx| {
2715                workspace.with_local_or_wsl_workspace(window, cx, move |workspace, window, cx| {
2716                    let project = workspace.project().clone();
2717
2718                    cx.spawn_in(window, async move |workspace, cx| {
2719                        let config_dir = project
2720                            .update(cx, |project, cx| {
2721                                project.try_windows_path_to_wsl(paths::config_dir().as_path(), cx)
2722                            })
2723                            .await?;
2724                        // Set up a dedicated worktree for settings, since
2725                        // otherwise we're dropping and re-starting LSP servers
2726                        // for each file inside on every settings file
2727                        // close/open
2728
2729                        // TODO: Do note that all other external files (e.g.
2730                        // drag and drop from OS) still have their worktrees
2731                        // released on file close, causing LSP servers'
2732                        // restarts.
2733                        let (_worktree, _) = project
2734                            .update(cx, |project, cx| {
2735                                project.find_or_create_worktree(&config_dir, false, cx)
2736                            })
2737                            .await?;
2738
2739                        workspace
2740                            .update_in(cx, |_, window, cx| {
2741                                create_and_open_local_file(abs_path, window, cx, default_content)
2742                            })?
2743                            .await?;
2744                        anyhow::Ok(())
2745                    })
2746                })
2747            })?
2748            .await?
2749            .await?;
2750        anyhow::Ok(())
2751    })
2752    .detach_and_log_err(cx);
2753}
2754
2755/// Eagerly loads the active theme and icon theme based on the selections in the
2756/// theme settings.
2757///
2758/// This fast path exists to load these themes as soon as possible so the user
2759/// doesn't see the default themes while waiting on extensions to load.
2760pub(crate) fn eager_load_active_theme_and_icon_theme(fs: Arc<dyn Fs>, cx: &mut App) {
2761    let extension_store = ExtensionStore::global(cx);
2762    let theme_registry = ThemeRegistry::global(cx);
2763    let theme_settings = ThemeSettings::get_global(cx);
2764    let appearance = SystemAppearance::global(cx).0;
2765
2766    enum LoadTarget {
2767        Theme(PathBuf),
2768        IconTheme((PathBuf, PathBuf)),
2769    }
2770
2771    let theme_name = theme_settings.theme.name(appearance);
2772    let icon_theme_name = theme_settings.icon_theme.name(appearance);
2773    let themes_to_load = [
2774        theme_registry
2775            .get(&theme_name.0)
2776            .is_err()
2777            .then(|| {
2778                extension_store
2779                    .read(cx)
2780                    .path_to_extension_theme(&theme_name.0)
2781            })
2782            .flatten()
2783            .map(LoadTarget::Theme),
2784        theme_registry
2785            .get_icon_theme(&icon_theme_name.0)
2786            .is_err()
2787            .then(|| {
2788                extension_store
2789                    .read(cx)
2790                    .path_to_extension_icon_theme(&icon_theme_name.0)
2791            })
2792            .flatten()
2793            .map(LoadTarget::IconTheme),
2794    ];
2795
2796    enum ReloadTarget {
2797        Theme,
2798        IconTheme,
2799    }
2800
2801    let executor = cx.background_executor();
2802    let reload_tasks = parking_lot::Mutex::new(Vec::with_capacity(themes_to_load.len()));
2803
2804    let mut themes_to_load = themes_to_load.into_iter().flatten().peekable();
2805
2806    if themes_to_load.peek().is_none() {
2807        return;
2808    }
2809
2810    cx.foreground_executor().block_on(executor.scoped(|scope| {
2811        for load_target in themes_to_load {
2812            let theme_registry = &theme_registry;
2813            let reload_tasks = &reload_tasks;
2814            let fs = fs.clone();
2815
2816            scope.spawn(async move {
2817                match load_target {
2818                    LoadTarget::Theme(theme_path) => {
2819                        if let Some(bytes) = fs.load_bytes(&theme_path).await.log_err()
2820                            && load_user_theme(theme_registry, &bytes).log_err().is_some()
2821                        {
2822                            reload_tasks.lock().push(ReloadTarget::Theme);
2823                        }
2824                    }
2825                    LoadTarget::IconTheme((icon_theme_path, icons_root_path)) => {
2826                        if let Some(bytes) = fs.load_bytes(&icon_theme_path).await.log_err()
2827                            && let Some(icon_theme_family) =
2828                                deserialize_icon_theme(&bytes).log_err()
2829                            && theme_registry
2830                                .load_icon_theme(icon_theme_family, &icons_root_path)
2831                                .log_err()
2832                                .is_some()
2833                        {
2834                            reload_tasks.lock().push(ReloadTarget::IconTheme);
2835                        }
2836                    }
2837                }
2838            });
2839        }
2840    }));
2841
2842    for reload_target in reload_tasks.into_inner() {
2843        match reload_target {
2844            ReloadTarget::Theme => theme_settings::reload_theme(cx),
2845            ReloadTarget::IconTheme => theme_settings::reload_icon_theme(cx),
2846        };
2847    }
2848}
2849
2850#[cfg(test)]
2851mod tests {
2852    use super::*;
2853    use assets::Assets;
2854    use collections::HashSet;
2855    use editor::{
2856        DisplayPoint, Editor, MultiBufferOffset, SelectionEffects, display_map::DisplayRow,
2857    };
2858    use gpui::{
2859        Action, AnyWindowHandle, App, AssetSource, BorrowAppContext, Modifiers, TestAppContext,
2860        UpdateGlobal, VisualTestContext, WindowHandle, actions, point, px,
2861    };
2862    use language::LanguageRegistry;
2863    use languages::{markdown_lang, rust_lang};
2864    use pretty_assertions::{assert_eq, assert_ne};
2865    use project::{Project, ProjectPath};
2866    use prompt_store::PromptBuilder;
2867    use semver::Version;
2868    use serde_json::json;
2869    use settings::{SaturatingBool, SettingsStore, watch_config_file};
2870    use std::{
2871        path::{Path, PathBuf},
2872        sync::Arc,
2873        time::Duration,
2874    };
2875    use theme::ThemeRegistry;
2876    use util::{
2877        path,
2878        rel_path::{RelPath, rel_path},
2879    };
2880    use workspace::MultiWorkspace;
2881    use workspace::{
2882        NewFile, OpenOptions, OpenVisible, SERIALIZATION_THROTTLE_TIME, SaveIntent, SplitDirection,
2883        WorkspaceHandle,
2884        item::SaveOptions,
2885        item::{Item, ItemHandle},
2886        open_new, open_paths, pane,
2887    };
2888
2889    async fn flush_workspace_serialization(
2890        window: &WindowHandle<MultiWorkspace>,
2891        cx: &mut TestAppContext,
2892    ) {
2893        let all_tasks = window
2894            .update(cx, |multi_workspace, window, cx| {
2895                let mut tasks = multi_workspace
2896                    .workspaces()
2897                    .map(|workspace| {
2898                        workspace.update(cx, |workspace, cx| {
2899                            workspace.flush_serialization(window, cx)
2900                        })
2901                    })
2902                    .collect::<Vec<_>>();
2903                tasks.push(multi_workspace.flush_serialization());
2904                tasks
2905            })
2906            .unwrap();
2907
2908        futures::future::join_all(all_tasks).await;
2909    }
2910
2911    #[gpui::test]
2912    async fn test_open_non_existing_file(cx: &mut TestAppContext) {
2913        let app_state = init_test(cx);
2914        app_state
2915            .fs
2916            .as_fake()
2917            .insert_tree(
2918                path!("/root"),
2919                json!({
2920                    "a": {
2921                    },
2922                }),
2923            )
2924            .await;
2925
2926        cx.update(|cx| {
2927            open_paths(
2928                &[PathBuf::from(path!("/root/a/new"))],
2929                app_state.clone(),
2930                workspace::OpenOptions::default(),
2931                cx,
2932            )
2933        })
2934        .await
2935        .unwrap();
2936        assert_eq!(cx.read(|cx| cx.windows().len()), 1);
2937
2938        let multi_workspace = cx.windows()[0].downcast::<MultiWorkspace>().unwrap();
2939        multi_workspace
2940            .update(cx, |multi_workspace, _, cx| {
2941                multi_workspace.workspace().update(cx, |workspace, cx| {
2942                    assert!(workspace.active_item_as::<Editor>(cx).is_some())
2943                });
2944            })
2945            .unwrap();
2946    }
2947
2948    #[gpui::test]
2949    async fn test_open_paths_action(cx: &mut TestAppContext) {
2950        let app_state = init_test(cx);
2951        app_state
2952            .fs
2953            .as_fake()
2954            .insert_tree(
2955                path!("/root"),
2956                json!({
2957                    "a": {
2958                        "aa": null,
2959                        "ab": null,
2960                    },
2961                    "b": {
2962                        "ba": null,
2963                        "bb": null,
2964                    },
2965                    "c": {
2966                        "ca": null,
2967                        "cb": null,
2968                    },
2969                    "d": {
2970                        "da": null,
2971                        "db": null,
2972                    },
2973                    "e": {
2974                        "ea": null,
2975                        "eb": null,
2976                    }
2977                }),
2978            )
2979            .await;
2980
2981        cx.update(|cx| {
2982            open_paths(
2983                &[
2984                    PathBuf::from(path!("/root/a")),
2985                    PathBuf::from(path!("/root/b")),
2986                ],
2987                app_state.clone(),
2988                workspace::OpenOptions::default(),
2989                cx,
2990            )
2991        })
2992        .await
2993        .unwrap();
2994        assert_eq!(cx.read(|cx| cx.windows().len()), 1);
2995
2996        cx.update(|cx| {
2997            open_paths(
2998                &[PathBuf::from(path!("/root/a"))],
2999                app_state.clone(),
3000                workspace::OpenOptions::default(),
3001                cx,
3002            )
3003        })
3004        .await
3005        .unwrap();
3006        assert_eq!(cx.read(|cx| cx.windows().len()), 1);
3007        let multi_workspace_1 = cx
3008            .read(|cx| cx.windows()[0].downcast::<MultiWorkspace>())
3009            .unwrap();
3010        cx.run_until_parked();
3011        multi_workspace_1
3012            .update(cx, |multi_workspace, window, cx| {
3013                multi_workspace.workspace().update(cx, |workspace, cx| {
3014                    assert_eq!(workspace.worktrees(cx).count(), 2);
3015                    assert!(workspace.right_dock().read(cx).is_open());
3016                    assert!(
3017                        workspace
3018                            .active_pane()
3019                            .read(cx)
3020                            .focus_handle(cx)
3021                            .is_focused(window)
3022                    );
3023                });
3024            })
3025            .unwrap();
3026
3027        cx.update(|cx| {
3028            open_paths(
3029                &[
3030                    PathBuf::from(path!("/root/c")),
3031                    PathBuf::from(path!("/root/d")),
3032                ],
3033                app_state.clone(),
3034                workspace::OpenOptions::default(),
3035                cx,
3036            )
3037        })
3038        .await
3039        .unwrap();
3040        assert_eq!(cx.read(|cx| cx.windows().len()), 1);
3041        cx.run_until_parked();
3042        multi_workspace_1
3043            .update(cx, |multi_workspace, _window, cx| {
3044                assert_eq!(multi_workspace.workspaces().count(), 2);
3045                assert!(multi_workspace.sidebar_open());
3046                let workspace = multi_workspace.workspace().read(cx);
3047                assert_eq!(
3048                    workspace
3049                        .worktrees(cx)
3050                        .map(|w| w.read(cx).abs_path())
3051                        .collect::<Vec<_>>(),
3052                    &[
3053                        Path::new(path!("/root/c")).into(),
3054                        Path::new(path!("/root/d")).into(),
3055                    ]
3056                );
3057            })
3058            .unwrap();
3059
3060        // Opening with -n (reuse_worktrees: false) still creates a new window.
3061        cx.update(|cx| {
3062            open_paths(
3063                &[PathBuf::from(path!("/root/e"))],
3064                app_state,
3065                workspace::OpenOptions {
3066                    workspace_matching: workspace::WorkspaceMatching::None,
3067                    ..Default::default()
3068                },
3069                cx,
3070            )
3071        })
3072        .await
3073        .unwrap();
3074        cx.background_executor.run_until_parked();
3075        assert_eq!(cx.read(|cx| cx.windows().len()), 2);
3076    }
3077
3078    #[gpui::test]
3079    async fn test_open_add_new(cx: &mut TestAppContext) {
3080        let app_state = init_test(cx);
3081        app_state
3082            .fs
3083            .as_fake()
3084            .insert_tree(
3085                path!("/root"),
3086                json!({"a": "hey", "b": "", "dir": {"c": "f"}}),
3087            )
3088            .await;
3089
3090        cx.update(|cx| {
3091            open_paths(
3092                &[PathBuf::from(path!("/root/dir"))],
3093                app_state.clone(),
3094                workspace::OpenOptions::default(),
3095                cx,
3096            )
3097        })
3098        .await
3099        .unwrap();
3100        assert_eq!(cx.update(|cx| cx.windows().len()), 1);
3101
3102        cx.update(|cx| {
3103            open_paths(
3104                &[PathBuf::from(path!("/root/a"))],
3105                app_state.clone(),
3106                workspace::OpenOptions {
3107                    workspace_matching: workspace::WorkspaceMatching::MatchSubdirectory,
3108                    ..Default::default()
3109                },
3110                cx,
3111            )
3112        })
3113        .await
3114        .unwrap();
3115        assert_eq!(cx.update(|cx| cx.windows().len()), 1);
3116
3117        // Opening a file inside the existing worktree with -n creates a new window.
3118        cx.update(|cx| {
3119            open_paths(
3120                &[PathBuf::from(path!("/root/dir/c"))],
3121                app_state.clone(),
3122                workspace::OpenOptions {
3123                    workspace_matching: workspace::WorkspaceMatching::None,
3124                    ..Default::default()
3125                },
3126                cx,
3127            )
3128        })
3129        .await
3130        .unwrap();
3131        assert_eq!(cx.update(|cx| cx.windows().len()), 2);
3132
3133        // Opening a path NOT in any existing worktree with -n creates a new window.
3134        cx.update(|cx| {
3135            open_paths(
3136                &[PathBuf::from(path!("/root/b"))],
3137                app_state.clone(),
3138                workspace::OpenOptions {
3139                    workspace_matching: workspace::WorkspaceMatching::None,
3140                    ..Default::default()
3141                },
3142                cx,
3143            )
3144        })
3145        .await
3146        .unwrap();
3147        assert_eq!(cx.update(|cx| cx.windows().len()), 3);
3148    }
3149
3150    #[gpui::test]
3151    async fn test_open_file_in_many_spaces(cx: &mut TestAppContext) {
3152        let app_state = init_test(cx);
3153        app_state
3154            .fs
3155            .as_fake()
3156            .insert_tree(
3157                path!("/root"),
3158                json!({"dir1": {"a": "b"}, "dir2": {"c": "d"}}),
3159            )
3160            .await;
3161
3162        cx.update(|cx| {
3163            open_paths(
3164                &[PathBuf::from(path!("/root/dir1/a"))],
3165                app_state.clone(),
3166                workspace::OpenOptions::default(),
3167                cx,
3168            )
3169        })
3170        .await
3171        .unwrap();
3172        assert_eq!(cx.update(|cx| cx.windows().len()), 1);
3173
3174        cx.update(|cx| {
3175            open_paths(
3176                &[PathBuf::from(path!("/root/dir2/c"))],
3177                app_state.clone(),
3178                workspace::OpenOptions::default(),
3179                cx,
3180            )
3181        })
3182        .await
3183        .unwrap();
3184        assert_eq!(cx.update(|cx| cx.windows().len()), 1);
3185
3186        // Opening a directory with default options adds to the existing window
3187        // rather than creating a new one.
3188        cx.update(|cx| {
3189            open_paths(
3190                &[PathBuf::from(path!("/root/dir2"))],
3191                app_state.clone(),
3192                workspace::OpenOptions::default(),
3193                cx,
3194            )
3195        })
3196        .await
3197        .unwrap();
3198        assert_eq!(cx.update(|cx| cx.windows().len()), 1);
3199
3200        // Opening a directory already in a worktree with -n creates a new window.
3201        cx.update(|cx| {
3202            open_paths(
3203                &[PathBuf::from(path!("/root/dir2"))],
3204                app_state.clone(),
3205                workspace::OpenOptions {
3206                    workspace_matching: workspace::WorkspaceMatching::None,
3207                    ..Default::default()
3208                },
3209                cx,
3210            )
3211        })
3212        .await
3213        .unwrap();
3214        assert_eq!(cx.update(|cx| cx.windows().len()), 2);
3215
3216        // Opening a directory NOT in any worktree with -n creates a new window.
3217        cx.update(|cx| {
3218            open_paths(
3219                &[PathBuf::from(path!("/root"))],
3220                app_state.clone(),
3221                workspace::OpenOptions {
3222                    workspace_matching: workspace::WorkspaceMatching::None,
3223                    ..Default::default()
3224                },
3225                cx,
3226            )
3227        })
3228        .await
3229        .unwrap();
3230        assert_eq!(cx.update(|cx| cx.windows().len()), 3);
3231    }
3232
3233    #[gpui::test]
3234    async fn test_window_edit_state_restoring_disabled(cx: &mut TestAppContext) {
3235        let executor = cx.executor();
3236        let app_state = init_test(cx);
3237
3238        cx.update(|cx| {
3239            SettingsStore::update_global(cx, |store, cx| {
3240                store.update_user_settings(cx, |settings| {
3241                    settings
3242                        .session
3243                        .get_or_insert_default()
3244                        .restore_unsaved_buffers = Some(false)
3245                });
3246            });
3247        });
3248
3249        app_state
3250            .fs
3251            .as_fake()
3252            .insert_tree(path!("/root"), json!({"a": "hey"}))
3253            .await;
3254
3255        cx.update(|cx| {
3256            open_paths(
3257                &[PathBuf::from(path!("/root/a"))],
3258                app_state.clone(),
3259                workspace::OpenOptions::default(),
3260                cx,
3261            )
3262        })
3263        .await
3264        .unwrap();
3265        assert_eq!(cx.update(|cx| cx.windows().len()), 1);
3266
3267        // When opening the workspace, the window is not in a edited state.
3268        let window = cx.update(|cx| cx.windows()[0].downcast::<MultiWorkspace>().unwrap());
3269
3270        let window_is_edited = |window: WindowHandle<MultiWorkspace>, cx: &mut TestAppContext| {
3271            cx.update(|cx| window.read(cx).unwrap().workspace().read(cx).is_edited())
3272        };
3273        let pane = window
3274            .read_with(cx, |multi_workspace, cx| {
3275                multi_workspace.workspace().read(cx).active_pane().clone()
3276            })
3277            .unwrap();
3278        let editor = window
3279            .read_with(cx, |multi_workspace, cx| {
3280                multi_workspace
3281                    .workspace()
3282                    .read(cx)
3283                    .active_item(cx)
3284                    .unwrap()
3285                    .downcast::<Editor>()
3286                    .unwrap()
3287            })
3288            .unwrap();
3289
3290        assert!(!window_is_edited(window, cx));
3291
3292        // Editing a buffer marks the window as edited.
3293        window
3294            .update(cx, |_, window, cx| {
3295                editor.update(cx, |editor, cx| editor.insert("EDIT", window, cx));
3296            })
3297            .unwrap();
3298
3299        assert!(window_is_edited(window, cx));
3300
3301        // Undoing the edit restores the window's edited state.
3302        window
3303            .update(cx, |_, window, cx| {
3304                editor.update(cx, |editor, cx| {
3305                    editor.undo(&Default::default(), window, cx)
3306                });
3307            })
3308            .unwrap();
3309        assert!(!window_is_edited(window, cx));
3310
3311        // Redoing the edit marks the window as edited again.
3312        window
3313            .update(cx, |_, window, cx| {
3314                editor.update(cx, |editor, cx| {
3315                    editor.redo(&Default::default(), window, cx)
3316                });
3317            })
3318            .unwrap();
3319        assert!(window_is_edited(window, cx));
3320        let weak = editor.downgrade();
3321
3322        // Closing the item restores the window's edited state.
3323        let close = window
3324            .update(cx, |_, window, cx| {
3325                pane.update(cx, |pane, cx| {
3326                    drop(editor);
3327                    pane.close_active_item(&Default::default(), window, cx)
3328                })
3329            })
3330            .unwrap();
3331        executor.run_until_parked();
3332
3333        cx.simulate_prompt_answer("Don't Save");
3334        close.await.unwrap();
3335
3336        // Advance the clock to ensure that the item has been serialized and dropped from the queue
3337        cx.executor().advance_clock(Duration::from_secs(1));
3338
3339        weak.assert_released();
3340        assert!(!window_is_edited(window, cx));
3341        // Opening the buffer again doesn't impact the window's edited state.
3342        cx.update(|cx| {
3343            open_paths(
3344                &[PathBuf::from(path!("/root/a"))],
3345                app_state,
3346                workspace::OpenOptions::default(),
3347                cx,
3348            )
3349        })
3350        .await
3351        .unwrap();
3352        executor.run_until_parked();
3353
3354        window
3355            .update(cx, |multi_workspace, _, cx| {
3356                multi_workspace.workspace().update(cx, |workspace, cx| {
3357                    let editor = workspace
3358                        .active_item(cx)
3359                        .unwrap()
3360                        .downcast::<Editor>()
3361                        .unwrap();
3362
3363                    editor.update(cx, |editor, cx| {
3364                        assert_eq!(editor.text(cx), "hey");
3365                    });
3366                });
3367            })
3368            .unwrap();
3369
3370        let editor = window
3371            .read_with(cx, |multi_workspace, cx| {
3372                multi_workspace
3373                    .workspace()
3374                    .read(cx)
3375                    .active_item(cx)
3376                    .unwrap()
3377                    .downcast::<Editor>()
3378                    .unwrap()
3379            })
3380            .unwrap();
3381        assert!(!window_is_edited(window, cx));
3382
3383        // Editing the buffer marks the window as edited.
3384        window
3385            .update(cx, |_, window, cx| {
3386                editor.update(cx, |editor, cx| editor.insert("EDIT", window, cx));
3387            })
3388            .unwrap();
3389        executor.run_until_parked();
3390        assert!(window_is_edited(window, cx));
3391
3392        // Ensure closing the window via the mouse gets preempted due to the
3393        // buffer having unsaved changes.
3394        assert!(!VisualTestContext::from_window(window.into(), cx).simulate_close());
3395        executor.run_until_parked();
3396        assert_eq!(cx.update(|cx| cx.windows().len()), 1);
3397
3398        // The window is successfully closed after the user dismisses the prompt.
3399        cx.simulate_prompt_answer("Don't Save");
3400        executor.run_until_parked();
3401        assert_eq!(cx.update(|cx| cx.windows().len()), 0);
3402    }
3403
3404    #[ignore = "This test has timing issues across platforms."]
3405    #[gpui::test]
3406    async fn test_window_edit_state_restoring_enabled(cx: &mut TestAppContext) {
3407        let app_state = init_test(cx);
3408        app_state
3409            .fs
3410            .as_fake()
3411            .insert_tree(path!("/root"), json!({"a": "hey"}))
3412            .await;
3413
3414        cx.update(|cx| {
3415            open_paths(
3416                &[PathBuf::from(path!("/root/a"))],
3417                app_state.clone(),
3418                workspace::OpenOptions::default(),
3419                cx,
3420            )
3421        })
3422        .await
3423        .unwrap();
3424
3425        assert_eq!(cx.update(|cx| cx.windows().len()), 1);
3426
3427        // When opening the workspace, the window is not in a edited state.
3428        let window = cx.update(|cx| cx.windows()[0].downcast::<MultiWorkspace>().unwrap());
3429
3430        let window_is_edited = |window: WindowHandle<MultiWorkspace>, cx: &mut TestAppContext| {
3431            cx.update(|cx| window.read(cx).unwrap().workspace().read(cx).is_edited())
3432        };
3433        let workspace_database_id = |window: WindowHandle<MultiWorkspace>,
3434                                     cx: &mut TestAppContext| {
3435            cx.update(|cx| window.read(cx).unwrap().workspace().read(cx).database_id())
3436        };
3437
3438        let editor = window
3439            .read_with(cx, |multi_workspace, cx| {
3440                multi_workspace
3441                    .workspace()
3442                    .read(cx)
3443                    .active_item(cx)
3444                    .unwrap()
3445                    .downcast::<Editor>()
3446                    .unwrap()
3447            })
3448            .unwrap();
3449
3450        assert!(!window_is_edited(window, cx));
3451        let initial_database_id = workspace_database_id(window, cx);
3452        assert!(
3453            initial_database_id.is_some(),
3454            "a restored workspace must have a stable database id"
3455        );
3456
3457        // Editing a buffer marks the window as edited.
3458        window
3459            .update(cx, |_, window, cx| {
3460                editor.update(cx, |editor, cx| editor.insert("EDIT", window, cx));
3461            })
3462            .unwrap();
3463        cx.run_until_parked();
3464
3465        assert!(window_is_edited(window, cx));
3466
3467        // Advance the clock to make sure the workspace is serialized
3468        cx.executor().advance_clock(Duration::from_secs(1));
3469
3470        // When closing the window, no prompt shows up and the window is closed.
3471        // buffer having unsaved changes.
3472        assert!(!VisualTestContext::from_window(window.into(), cx).simulate_close());
3473        cx.run_until_parked();
3474        assert_eq!(cx.update(|cx| cx.windows().len()), 0);
3475
3476        // When we now reopen the window, the edited state and the edited buffer are back
3477        cx.update(|cx| {
3478            open_paths(
3479                &[PathBuf::from(path!("/root/a"))],
3480                app_state.clone(),
3481                workspace::OpenOptions::default(),
3482                cx,
3483            )
3484        })
3485        .await
3486        .unwrap();
3487
3488        assert_eq!(cx.update(|cx| cx.windows().len()), 1);
3489        assert!(cx.update(|cx| cx.active_window().is_some()));
3490
3491        cx.run_until_parked();
3492
3493        // When opening the workspace, the window is not in a edited state.
3494        let window = cx.update(|cx| {
3495            cx.active_window()
3496                .unwrap()
3497                .downcast::<MultiWorkspace>()
3498                .unwrap()
3499        });
3500        assert!(window_is_edited(window, cx));
3501        assert_eq!(
3502            workspace_database_id(window, cx),
3503            initial_database_id,
3504            "the workspace must keep the same database id across a close/reopen cycle"
3505        );
3506
3507        window
3508            .update(cx, |multi_workspace, _, cx| {
3509                multi_workspace.workspace().update(cx, |workspace, cx| {
3510                    let editor = workspace
3511                        .active_item(cx)
3512                        .unwrap()
3513                        .downcast::<editor::Editor>()
3514                        .unwrap();
3515                    editor.update(cx, |editor, cx| {
3516                        assert_eq!(editor.text(cx), "EDIThey");
3517                        assert!(editor.is_dirty(cx));
3518                    });
3519                });
3520            })
3521            .unwrap();
3522    }
3523
3524    #[gpui::test]
3525    async fn test_new_empty_workspace(cx: &mut TestAppContext) {
3526        let app_state = init_test(cx);
3527        cx.update(|cx| {
3528            open_new(
3529                Default::default(),
3530                app_state.clone(),
3531                cx,
3532                |workspace, window, cx| {
3533                    Editor::new_file(workspace, &Default::default(), window, cx)
3534                },
3535            )
3536        })
3537        .await
3538        .unwrap();
3539        cx.run_until_parked();
3540
3541        let multi_workspace = cx
3542            .update(|cx| cx.windows().first().unwrap().downcast::<MultiWorkspace>())
3543            .unwrap();
3544
3545        let editor = multi_workspace
3546            .update(cx, |multi_workspace, _, cx| {
3547                multi_workspace.workspace().update(cx, |workspace, cx| {
3548                    let editor = workspace
3549                        .active_item(cx)
3550                        .unwrap()
3551                        .downcast::<editor::Editor>()
3552                        .unwrap();
3553                    editor.update(cx, |editor, cx| {
3554                        assert!(editor.text(cx).is_empty());
3555                        assert!(!editor.is_dirty(cx));
3556                    });
3557
3558                    editor
3559                })
3560            })
3561            .unwrap();
3562
3563        let save_task = multi_workspace
3564            .update(cx, |multi_workspace, window, cx| {
3565                multi_workspace.workspace().update(cx, |workspace, cx| {
3566                    workspace.save_active_item(SaveIntent::Save, window, cx)
3567                })
3568            })
3569            .unwrap();
3570        app_state.fs.create_dir(Path::new("/root")).await.unwrap();
3571        cx.background_executor.run_until_parked();
3572        cx.simulate_new_path_selection(|_| Some(PathBuf::from("/root/the-new-name")));
3573        save_task.await.unwrap();
3574        multi_workspace
3575            .update(cx, |_, _, cx| {
3576                editor.update(cx, |editor, cx| {
3577                    assert!(!editor.is_dirty(cx));
3578                    assert_eq!(editor.title(cx), "the-new-name");
3579                });
3580            })
3581            .unwrap();
3582    }
3583
3584    #[gpui::test]
3585    async fn test_open_entry(cx: &mut TestAppContext) {
3586        let app_state = init_test(cx);
3587        app_state
3588            .fs
3589            .as_fake()
3590            .insert_tree(
3591                path!("/root"),
3592                json!({
3593                    "a": {
3594                        "file1": "contents 1",
3595                        "file2": "contents 2",
3596                        "file3": "contents 3",
3597                    },
3598                }),
3599            )
3600            .await;
3601
3602        let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
3603        project.update(cx, |project, _cx| project.languages().add(markdown_lang()));
3604        let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project, window, cx));
3605        let workspace = window
3606            .read_with(cx, |mw, _| mw.workspace().clone())
3607            .unwrap();
3608
3609        let entries = cx.read(|cx| workspace.file_project_paths(cx));
3610        let file1 = entries[0].clone();
3611        let file2 = entries[1].clone();
3612        let file3 = entries[2].clone();
3613
3614        // Open the first entry
3615        let entry_1 = window
3616            .update(cx, |_, window, cx| {
3617                workspace.update(cx, |w, cx| {
3618                    w.open_path(file1.clone(), None, true, window, cx)
3619                })
3620            })
3621            .unwrap()
3622            .await
3623            .unwrap();
3624        cx.read(|cx| {
3625            let pane = workspace.read(cx).active_pane().read(cx);
3626            assert_eq!(
3627                pane.active_item().unwrap().project_path(cx),
3628                Some(file1.clone())
3629            );
3630            assert_eq!(pane.items_len(), 1);
3631        });
3632
3633        // Open the second entry
3634        window
3635            .update(cx, |_, window, cx| {
3636                workspace.update(cx, |w, cx| {
3637                    w.open_path(file2.clone(), None, true, window, cx)
3638                })
3639            })
3640            .unwrap()
3641            .await
3642            .unwrap();
3643        cx.read(|cx| {
3644            let pane = workspace.read(cx).active_pane().read(cx);
3645            assert_eq!(
3646                pane.active_item().unwrap().project_path(cx),
3647                Some(file2.clone())
3648            );
3649            assert_eq!(pane.items_len(), 2);
3650        });
3651
3652        // Open the first entry again. The existing pane item is activated.
3653        let entry_1b = window
3654            .update(cx, |_, window, cx| {
3655                workspace.update(cx, |w, cx| {
3656                    w.open_path(file1.clone(), None, true, window, cx)
3657                })
3658            })
3659            .unwrap()
3660            .await
3661            .unwrap();
3662        assert_eq!(entry_1.item_id(), entry_1b.item_id());
3663
3664        cx.read(|cx| {
3665            let pane = workspace.read(cx).active_pane().read(cx);
3666            assert_eq!(
3667                pane.active_item().unwrap().project_path(cx),
3668                Some(file1.clone())
3669            );
3670            assert_eq!(pane.items_len(), 2);
3671        });
3672
3673        // Split the pane with the first entry, then open the second entry again.
3674        window
3675            .update(cx, |_, window, cx| {
3676                workspace.update(cx, |w, cx| {
3677                    w.split_and_clone(w.active_pane().clone(), SplitDirection::Right, window, cx)
3678                })
3679            })
3680            .unwrap()
3681            .await
3682            .unwrap();
3683        window
3684            .update(cx, |_, window, cx| {
3685                workspace.update(cx, |w, cx| {
3686                    w.open_path(file2.clone(), None, true, window, cx)
3687                })
3688            })
3689            .unwrap()
3690            .await
3691            .unwrap();
3692
3693        cx.read(|cx| {
3694            assert_eq!(
3695                workspace
3696                    .read(cx)
3697                    .active_pane()
3698                    .read(cx)
3699                    .active_item()
3700                    .unwrap()
3701                    .project_path(cx),
3702                Some(file2.clone())
3703            );
3704        });
3705
3706        // Open the third entry twice concurrently. Only one pane item is added.
3707        let (t1, t2) = window
3708            .update(cx, |_, window, cx| {
3709                workspace.update(cx, |w, cx| {
3710                    (
3711                        w.open_path(file3.clone(), None, true, window, cx),
3712                        w.open_path(file3.clone(), None, true, window, cx),
3713                    )
3714                })
3715            })
3716            .unwrap();
3717        t1.await.unwrap();
3718        t2.await.unwrap();
3719        cx.read(|cx| {
3720            let pane = workspace.read(cx).active_pane().read(cx);
3721            assert_eq!(
3722                pane.active_item().unwrap().project_path(cx),
3723                Some(file3.clone())
3724            );
3725            let pane_entries = pane
3726                .items()
3727                .map(|i| i.project_path(cx).unwrap())
3728                .collect::<Vec<_>>();
3729            assert_eq!(pane_entries, &[file1, file2, file3]);
3730        });
3731    }
3732
3733    #[gpui::test]
3734    async fn test_open_paths(cx: &mut TestAppContext) {
3735        let app_state = init_test(cx);
3736
3737        app_state
3738            .fs
3739            .as_fake()
3740            .insert_tree(
3741                path!("/"),
3742                json!({
3743                    "dir1": {
3744                        "a.txt": ""
3745                    },
3746                    "dir2": {
3747                        "b.txt": ""
3748                    },
3749                    "dir3": {
3750                        "c.txt": ""
3751                    },
3752                    "d.txt": ""
3753                }),
3754            )
3755            .await;
3756
3757        cx.update(|cx| {
3758            open_paths(
3759                &[PathBuf::from(path!("/dir1/"))],
3760                app_state,
3761                workspace::OpenOptions::default(),
3762                cx,
3763            )
3764        })
3765        .await
3766        .unwrap();
3767        cx.run_until_parked();
3768        assert_eq!(cx.update(|cx| cx.windows().len()), 1);
3769        let window = cx.update(|cx| cx.windows()[0].downcast::<MultiWorkspace>().unwrap());
3770        let workspace = window
3771            .read_with(cx, |mw, _| mw.workspace().clone())
3772            .unwrap();
3773
3774        #[track_caller]
3775        fn assert_project_panel_selection(
3776            workspace: &Workspace,
3777            expected_worktree_path: &Path,
3778            expected_entry_path: &RelPath,
3779            cx: &App,
3780        ) {
3781            let project_panel = [
3782                workspace.left_dock().read(cx).panel::<ProjectPanel>(),
3783                workspace.right_dock().read(cx).panel::<ProjectPanel>(),
3784                workspace.bottom_dock().read(cx).panel::<ProjectPanel>(),
3785            ]
3786            .into_iter()
3787            .find_map(std::convert::identity)
3788            .expect("found no project panels")
3789            .read(cx);
3790            let (selected_worktree, selected_entry) = project_panel
3791                .selected_entry(cx)
3792                .expect("project panel should have a selected entry");
3793            assert_eq!(
3794                selected_worktree.abs_path().as_ref(),
3795                expected_worktree_path,
3796                "Unexpected project panel selected worktree path"
3797            );
3798            assert_eq!(
3799                selected_entry.path.as_ref(),
3800                expected_entry_path,
3801                "Unexpected project panel selected entry path"
3802            );
3803        }
3804
3805        // Open a file within an existing worktree.
3806        window
3807            .update(cx, |multi_workspace, window, cx| {
3808                multi_workspace.workspace().update(cx, |workspace, cx| {
3809                    workspace.open_paths(
3810                        vec![path!("/dir1/a.txt").into()],
3811                        OpenOptions {
3812                            visible: Some(OpenVisible::All),
3813                            ..Default::default()
3814                        },
3815                        None,
3816                        window,
3817                        cx,
3818                    )
3819                })
3820            })
3821            .unwrap()
3822            .await;
3823        cx.run_until_parked();
3824        cx.read(|cx| {
3825            let workspace = workspace.read(cx);
3826            assert_project_panel_selection(
3827                workspace,
3828                Path::new(path!("/dir1")),
3829                rel_path("a.txt"),
3830                cx,
3831            );
3832            assert_eq!(
3833                workspace
3834                    .active_pane()
3835                    .read(cx)
3836                    .active_item()
3837                    .unwrap()
3838                    .act_as::<Editor>(cx)
3839                    .unwrap()
3840                    .read(cx)
3841                    .title(cx),
3842                "a.txt"
3843            );
3844        });
3845
3846        // Open a file outside of any existing worktree.
3847        window
3848            .update(cx, |multi_workspace, window, cx| {
3849                multi_workspace.workspace().update(cx, |workspace, cx| {
3850                    workspace.open_paths(
3851                        vec![path!("/dir2/b.txt").into()],
3852                        OpenOptions {
3853                            visible: Some(OpenVisible::All),
3854                            ..Default::default()
3855                        },
3856                        None,
3857                        window,
3858                        cx,
3859                    )
3860                })
3861            })
3862            .unwrap()
3863            .await;
3864        cx.run_until_parked();
3865        cx.read(|cx| {
3866            let workspace = workspace.read(cx);
3867            assert_project_panel_selection(
3868                workspace,
3869                Path::new(path!("/dir2/b.txt")),
3870                rel_path(""),
3871                cx,
3872            );
3873            let worktree_roots = workspace
3874                .worktrees(cx)
3875                .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref())
3876                .collect::<HashSet<_>>();
3877            assert_eq!(
3878                worktree_roots,
3879                vec![path!("/dir1"), path!("/dir2/b.txt")]
3880                    .into_iter()
3881                    .map(Path::new)
3882                    .collect(),
3883            );
3884            assert_eq!(
3885                workspace
3886                    .active_pane()
3887                    .read(cx)
3888                    .active_item()
3889                    .unwrap()
3890                    .act_as::<Editor>(cx)
3891                    .unwrap()
3892                    .read(cx)
3893                    .title(cx),
3894                "b.txt"
3895            );
3896        });
3897
3898        // Ensure opening a directory and one of its children only adds one worktree.
3899        window
3900            .update(cx, |multi_workspace, window, cx| {
3901                multi_workspace.workspace().update(cx, |workspace, cx| {
3902                    workspace.open_paths(
3903                        vec![path!("/dir3").into(), path!("/dir3/c.txt").into()],
3904                        OpenOptions {
3905                            visible: Some(OpenVisible::All),
3906                            ..Default::default()
3907                        },
3908                        None,
3909                        window,
3910                        cx,
3911                    )
3912                })
3913            })
3914            .unwrap()
3915            .await;
3916        cx.run_until_parked();
3917        cx.read(|cx| {
3918            let workspace = workspace.read(cx);
3919            assert_project_panel_selection(
3920                workspace,
3921                Path::new(path!("/dir3")),
3922                rel_path("c.txt"),
3923                cx,
3924            );
3925            let worktree_roots = workspace
3926                .worktrees(cx)
3927                .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref())
3928                .collect::<HashSet<_>>();
3929            assert_eq!(
3930                worktree_roots,
3931                vec![path!("/dir1"), path!("/dir2/b.txt"), path!("/dir3")]
3932                    .into_iter()
3933                    .map(Path::new)
3934                    .collect(),
3935            );
3936            assert_eq!(
3937                workspace
3938                    .active_pane()
3939                    .read(cx)
3940                    .active_item()
3941                    .unwrap()
3942                    .act_as::<Editor>(cx)
3943                    .unwrap()
3944                    .read(cx)
3945                    .title(cx),
3946                "c.txt"
3947            );
3948        });
3949
3950        // Ensure opening invisibly a file outside an existing worktree adds a new, invisible worktree.
3951        window
3952            .update(cx, |multi_workspace, window, cx| {
3953                multi_workspace.workspace().update(cx, |workspace, cx| {
3954                    workspace.open_paths(
3955                        vec![path!("/d.txt").into()],
3956                        OpenOptions {
3957                            visible: Some(OpenVisible::None),
3958                            ..Default::default()
3959                        },
3960                        None,
3961                        window,
3962                        cx,
3963                    )
3964                })
3965            })
3966            .unwrap()
3967            .await;
3968        cx.run_until_parked();
3969        cx.read(|cx| {
3970            let workspace = workspace.read(cx);
3971            assert_project_panel_selection(workspace, Path::new(path!("/d.txt")), rel_path(""), cx);
3972            let worktree_roots = workspace
3973                .worktrees(cx)
3974                .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref())
3975                .collect::<HashSet<_>>();
3976            assert_eq!(
3977                worktree_roots,
3978                vec![
3979                    path!("/dir1"),
3980                    path!("/dir2/b.txt"),
3981                    path!("/dir3"),
3982                    path!("/d.txt")
3983                ]
3984                .into_iter()
3985                .map(Path::new)
3986                .collect(),
3987            );
3988
3989            let visible_worktree_roots = workspace
3990                .visible_worktrees(cx)
3991                .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref())
3992                .collect::<HashSet<_>>();
3993            assert_eq!(
3994                visible_worktree_roots,
3995                vec![path!("/dir1"), path!("/dir2/b.txt"), path!("/dir3")]
3996                    .into_iter()
3997                    .map(Path::new)
3998                    .collect(),
3999            );
4000
4001            assert_eq!(
4002                workspace
4003                    .active_pane()
4004                    .read(cx)
4005                    .active_item()
4006                    .unwrap()
4007                    .act_as::<Editor>(cx)
4008                    .unwrap()
4009                    .read(cx)
4010                    .title(cx),
4011                "d.txt"
4012            );
4013        });
4014    }
4015
4016    #[gpui::test]
4017    async fn test_opening_excluded_paths(cx: &mut TestAppContext) {
4018        let app_state = init_test(cx);
4019        cx.update(|cx| {
4020            cx.update_global::<SettingsStore, _>(|store, cx| {
4021                store.update_user_settings(cx, |project_settings| {
4022                    project_settings.project.worktree.file_scan_exclusions =
4023                        Some(vec!["excluded_dir".to_string(), "**/.git".to_string()]);
4024                });
4025            });
4026        });
4027        app_state
4028            .fs
4029            .as_fake()
4030            .insert_tree(
4031                path!("/root"),
4032                json!({
4033                    ".gitignore": "ignored_dir\n",
4034                    ".git": {
4035                        "HEAD": "ref: refs/heads/main",
4036                    },
4037                    "regular_dir": {
4038                        "file": "regular file contents",
4039                    },
4040                    "ignored_dir": {
4041                        "ignored_subdir": {
4042                            "file": "ignored subfile contents",
4043                        },
4044                        "file": "ignored file contents",
4045                    },
4046                    "excluded_dir": {
4047                        "file": "excluded file contents",
4048                        "ignored_subdir": {
4049                            "file": "ignored subfile contents",
4050                        },
4051                    },
4052                }),
4053            )
4054            .await;
4055
4056        let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
4057        project.update(cx, |project, _cx| project.languages().add(markdown_lang()));
4058        let window = cx.add_window({
4059            let project = project.clone();
4060            |window, cx| MultiWorkspace::test_new(project, window, cx)
4061        });
4062        let workspace = window
4063            .read_with(cx, |mw, _| mw.workspace().clone())
4064            .unwrap();
4065
4066        let initial_entries = cx.read(|cx| workspace.file_project_paths(cx));
4067        let paths_to_open = [
4068            PathBuf::from(path!("/root/excluded_dir/file")),
4069            PathBuf::from(path!("/root/.git/HEAD")),
4070            PathBuf::from(path!("/root/excluded_dir/ignored_subdir")),
4071        ];
4072        let workspace::OpenResult {
4073            window: opened_workspace,
4074            opened_items: new_items,
4075            ..
4076        } = cx
4077            .update(|cx| {
4078                workspace::open_paths(
4079                    &paths_to_open,
4080                    app_state,
4081                    workspace::OpenOptions::default(),
4082                    cx,
4083                )
4084            })
4085            .await
4086            .unwrap();
4087
4088        assert_eq!(
4089            opened_workspace
4090                .read_with(cx, |mw, _| mw.workspace().entity_id())
4091                .unwrap(),
4092            workspace.entity_id(),
4093            "Excluded files in subfolders of a workspace root should be opened in the workspace"
4094        );
4095        let mut opened_paths = cx.read(|cx| {
4096            assert_eq!(
4097                new_items.len(),
4098                paths_to_open.len(),
4099                "Expect to get the same number of opened items as submitted paths to open"
4100            );
4101            new_items
4102                .iter()
4103                .zip(paths_to_open.iter())
4104                .map(|(i, path)| {
4105                    match i {
4106                        Some(Ok(i)) => Some(i.project_path(cx).map(|p| p.path)),
4107                        Some(Err(e)) => panic!("Excluded file {path:?} failed to open: {e:?}"),
4108                        None => None,
4109                    }
4110                    .flatten()
4111                })
4112                .collect::<Vec<_>>()
4113        });
4114        opened_paths.sort();
4115        assert_eq!(
4116            opened_paths,
4117            vec![
4118                None,
4119                Some(rel_path(".git/HEAD").into()),
4120                Some(rel_path("excluded_dir/file").into()),
4121            ],
4122            "Excluded files should get opened, excluded dir should not get opened"
4123        );
4124
4125        let entries = cx.read(|cx| workspace.file_project_paths(cx));
4126        assert_eq!(
4127            initial_entries, entries,
4128            "Workspace entries should not change after opening excluded files and directories paths"
4129        );
4130
4131        cx.read(|cx| {
4132                let pane = workspace.read(cx).active_pane().read(cx);
4133                let mut opened_buffer_paths = pane
4134                    .items()
4135                    .map(|i| {
4136                        i.project_path(cx)
4137                            .expect("all excluded files that got open should have a path")
4138                            .path
4139                    })
4140                    .collect::<Vec<_>>();
4141                opened_buffer_paths.sort();
4142                assert_eq!(
4143                    opened_buffer_paths,
4144                    vec![rel_path(".git/HEAD").into(), rel_path("excluded_dir/file").into()],
4145                    "Despite not being present in the worktrees, buffers for excluded files are opened and added to the pane"
4146                );
4147            });
4148    }
4149
4150    #[gpui::test]
4151    async fn test_save_conflicting_item(cx: &mut TestAppContext) {
4152        let app_state = init_test(cx);
4153        app_state
4154            .fs
4155            .as_fake()
4156            .insert_tree(path!("/root"), json!({ "a.txt": "" }))
4157            .await;
4158
4159        let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
4160        project.update(cx, |project, _cx| project.languages().add(markdown_lang()));
4161        let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project, window, cx));
4162        let workspace = window
4163            .read_with(cx, |mw, _| mw.workspace().clone())
4164            .unwrap();
4165
4166        // Open a file within an existing worktree.
4167        window
4168            .update(cx, |_, window, cx| {
4169                workspace.update(cx, |workspace, cx| {
4170                    workspace.open_paths(
4171                        vec![PathBuf::from(path!("/root/a.txt"))],
4172                        OpenOptions {
4173                            visible: Some(OpenVisible::All),
4174                            ..Default::default()
4175                        },
4176                        None,
4177                        window,
4178                        cx,
4179                    )
4180                })
4181            })
4182            .unwrap()
4183            .await;
4184        let editor = cx.read(|cx| {
4185            let pane = workspace.read(cx).active_pane().read(cx);
4186            let item = pane.active_item().unwrap();
4187            item.downcast::<Editor>().unwrap()
4188        });
4189
4190        window
4191            .update(cx, |_, window, cx| {
4192                editor.update(cx, |editor, cx| editor.handle_input("x", window, cx));
4193            })
4194            .unwrap();
4195
4196        app_state
4197            .fs
4198            .as_fake()
4199            .insert_file(path!("/root/a.txt"), b"changed".to_vec())
4200            .await;
4201
4202        cx.run_until_parked();
4203        cx.read(|cx| assert!(editor.is_dirty(cx)));
4204        cx.read(|cx| assert!(editor.has_conflict(cx)));
4205
4206        let save_task = window
4207            .update(cx, |_, window, cx| {
4208                workspace.update(cx, |workspace, cx| {
4209                    workspace.save_active_item(SaveIntent::Save, window, cx)
4210                })
4211            })
4212            .unwrap();
4213        cx.background_executor.run_until_parked();
4214        cx.simulate_prompt_answer("Overwrite");
4215        save_task.await.unwrap();
4216        window
4217            .update(cx, |_, _, cx| {
4218                editor.update(cx, |editor, cx| {
4219                    assert!(!editor.is_dirty(cx));
4220                    assert!(!editor.has_conflict(cx));
4221                });
4222            })
4223            .unwrap();
4224    }
4225
4226    #[gpui::test]
4227    async fn test_open_and_save_new_file(cx: &mut TestAppContext) {
4228        let app_state = init_test(cx);
4229        app_state
4230            .fs
4231            .create_dir(Path::new(path!("/root")))
4232            .await
4233            .unwrap();
4234
4235        let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
4236        project.update(cx, |project, _| {
4237            project.languages().add(markdown_lang());
4238            project.languages().add(rust_lang());
4239        });
4240        let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project, window, cx));
4241        let workspace = window
4242            .read_with(cx, |mw, _| mw.workspace().clone())
4243            .unwrap();
4244        let worktree = cx.read(|cx| workspace.read(cx).worktrees(cx).next().unwrap());
4245
4246        // Create a new untitled buffer
4247        cx.dispatch_action(window.into(), NewFile);
4248        let editor = cx.read(|cx| {
4249            workspace
4250                .read(cx)
4251                .active_item(cx)
4252                .unwrap()
4253                .downcast::<Editor>()
4254                .unwrap()
4255        });
4256
4257        window
4258            .update(cx, |_, window, cx| {
4259                editor.update(cx, |editor, cx| {
4260                    assert!(!editor.is_dirty(cx));
4261                    assert_eq!(editor.title(cx), "untitled");
4262                    assert!(Arc::ptr_eq(
4263                        &editor
4264                            .buffer()
4265                            .read(cx)
4266                            .language_at(MultiBufferOffset(0), cx)
4267                            .unwrap(),
4268                        &languages::PLAIN_TEXT
4269                    ));
4270                    editor.handle_input("hi", window, cx);
4271                    assert!(editor.is_dirty(cx));
4272                });
4273            })
4274            .unwrap();
4275
4276        // Save the buffer. This prompts for a filename.
4277        let save_task = window
4278            .update(cx, |_, window, cx| {
4279                workspace.update(cx, |workspace, cx| {
4280                    workspace.save_active_item(SaveIntent::Save, window, cx)
4281                })
4282            })
4283            .unwrap();
4284        cx.background_executor.run_until_parked();
4285        cx.simulate_new_path_selection(|parent_dir| {
4286            assert_eq!(parent_dir, Path::new(path!("/root")));
4287            Some(parent_dir.join("the-new-name.rs"))
4288        });
4289        cx.read(|cx| {
4290            assert!(editor.is_dirty(cx));
4291            assert_eq!(editor.read(cx).title(cx), "hi");
4292        });
4293
4294        // When the save completes, the buffer's title is updated and the language is assigned based
4295        // on the path.
4296        save_task.await.unwrap();
4297        window
4298            .update(cx, |_, _, cx| {
4299                editor.update(cx, |editor, cx| {
4300                    assert!(!editor.is_dirty(cx));
4301                    assert_eq!(editor.title(cx), "the-new-name.rs");
4302                    assert_eq!(
4303                        editor
4304                            .buffer()
4305                            .read(cx)
4306                            .language_at(MultiBufferOffset(0), cx)
4307                            .unwrap()
4308                            .name(),
4309                        "Rust"
4310                    );
4311                });
4312            })
4313            .unwrap();
4314
4315        // Edit the file and save it again. This time, there is no filename prompt.
4316        window
4317            .update(cx, |_, window, cx| {
4318                editor.update(cx, |editor, cx| {
4319                    editor.handle_input(" there", window, cx);
4320                    assert!(editor.is_dirty(cx));
4321                });
4322            })
4323            .unwrap();
4324
4325        let save_task = window
4326            .update(cx, |_, window, cx| {
4327                workspace.update(cx, |workspace, cx| {
4328                    workspace.save_active_item(SaveIntent::Save, window, cx)
4329                })
4330            })
4331            .unwrap();
4332        save_task.await.unwrap();
4333
4334        assert!(!cx.did_prompt_for_new_path());
4335        window
4336            .update(cx, |_, _, cx| {
4337                editor.update(cx, |editor, cx| {
4338                    assert!(!editor.is_dirty(cx));
4339                    assert_eq!(editor.title(cx), "the-new-name.rs")
4340                });
4341            })
4342            .unwrap();
4343
4344        // Open the same newly-created file in another pane item. The new editor should reuse
4345        // the same buffer.
4346        cx.dispatch_action(window.into(), NewFile);
4347        window
4348            .update(cx, |_, window, cx| {
4349                workspace.update(cx, |workspace, cx| {
4350                    workspace.split_and_clone(
4351                        workspace.active_pane().clone(),
4352                        SplitDirection::Right,
4353                        window,
4354                        cx,
4355                    )
4356                })
4357            })
4358            .unwrap()
4359            .await
4360            .unwrap();
4361        window
4362            .update(cx, |_, window, cx| {
4363                workspace.update(cx, |workspace, cx| {
4364                    workspace.open_path(
4365                        (worktree.read(cx).id(), rel_path("the-new-name.rs")),
4366                        None,
4367                        true,
4368                        window,
4369                        cx,
4370                    )
4371                })
4372            })
4373            .unwrap()
4374            .await
4375            .unwrap();
4376        let editor2 = cx.read(|cx| {
4377            workspace
4378                .read(cx)
4379                .active_item(cx)
4380                .unwrap()
4381                .downcast::<Editor>()
4382                .unwrap()
4383        });
4384        cx.read(|cx| {
4385            assert_eq!(
4386                editor2.read(cx).buffer().read(cx).as_singleton().unwrap(),
4387                editor.read(cx).buffer().read(cx).as_singleton().unwrap()
4388            );
4389        })
4390    }
4391
4392    #[gpui::test]
4393    async fn test_setting_language_when_saving_as_single_file_worktree(cx: &mut TestAppContext) {
4394        let app_state = init_test(cx);
4395        app_state.fs.create_dir(Path::new("/root")).await.unwrap();
4396
4397        let project = Project::test(app_state.fs.clone(), [], cx).await;
4398        project.update(cx, |project, _| {
4399            project.languages().add(language::rust_lang());
4400            project.languages().add(language::markdown_lang());
4401        });
4402        let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project, window, cx));
4403        let workspace = window
4404            .read_with(cx, |mw, _| mw.workspace().clone())
4405            .unwrap();
4406
4407        // Create a new untitled buffer
4408        cx.dispatch_action(window.into(), NewFile);
4409        let editor = cx.read(|cx| {
4410            workspace
4411                .read(cx)
4412                .active_item(cx)
4413                .unwrap()
4414                .downcast::<Editor>()
4415                .unwrap()
4416        });
4417        window
4418            .update(cx, |_, window, cx| {
4419                editor.update(cx, |editor, cx| {
4420                    assert!(Arc::ptr_eq(
4421                        &editor
4422                            .buffer()
4423                            .read(cx)
4424                            .language_at(MultiBufferOffset(0), cx)
4425                            .unwrap(),
4426                        &languages::PLAIN_TEXT
4427                    ));
4428                    editor.handle_input("hi", window, cx);
4429                    assert!(editor.is_dirty(cx));
4430                });
4431            })
4432            .unwrap();
4433
4434        // Save the buffer. This prompts for a filename.
4435        let save_task = window
4436            .update(cx, |_, window, cx| {
4437                workspace.update(cx, |workspace, cx| {
4438                    workspace.save_active_item(SaveIntent::Save, window, cx)
4439                })
4440            })
4441            .unwrap();
4442        cx.background_executor.run_until_parked();
4443        cx.simulate_new_path_selection(|_| Some(PathBuf::from("/root/the-new-name.rs")));
4444        save_task.await.unwrap();
4445        // The buffer is not dirty anymore and the language is assigned based on the path.
4446        window
4447            .update(cx, |_, _, cx| {
4448                editor.update(cx, |editor, cx| {
4449                    assert!(!editor.is_dirty(cx));
4450                    assert_eq!(
4451                        editor
4452                            .buffer()
4453                            .read(cx)
4454                            .language_at(MultiBufferOffset(0), cx)
4455                            .unwrap()
4456                            .name(),
4457                        "Rust"
4458                    )
4459                });
4460            })
4461            .unwrap();
4462    }
4463
4464    #[gpui::test]
4465    async fn test_pane_actions(cx: &mut TestAppContext) {
4466        let app_state = init_test(cx);
4467        app_state
4468            .fs
4469            .as_fake()
4470            .insert_tree(
4471                path!("/root"),
4472                json!({
4473                    "a": {
4474                        "file1": "contents 1",
4475                        "file2": "contents 2",
4476                        "file3": "contents 3",
4477                    },
4478                }),
4479            )
4480            .await;
4481
4482        let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
4483        project.update(cx, |project, _cx| project.languages().add(markdown_lang()));
4484        let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project, window, cx));
4485        let workspace = window
4486            .read_with(cx, |mw, _| mw.workspace().clone())
4487            .unwrap();
4488        let cx = &mut VisualTestContext::from_window(*window, cx);
4489
4490        let entries = cx.read(|cx| workspace.file_project_paths(cx));
4491        let file1 = entries[0].clone();
4492
4493        let pane_1 = cx.read(|cx| workspace.read(cx).active_pane().clone());
4494
4495        workspace
4496            .update_in(cx, |w, window, cx| {
4497                w.open_path(file1.clone(), None, true, window, cx)
4498            })
4499            .await
4500            .unwrap();
4501
4502        let (editor_1, buffer) = workspace.update_in(cx, |_, window, cx| {
4503            pane_1.update(cx, |pane_1, cx| {
4504                let editor = pane_1.active_item().unwrap().downcast::<Editor>().unwrap();
4505                assert_eq!(editor.read(cx).active_project_path(cx), Some(file1.clone()));
4506                let buffer = editor.update(cx, |editor, cx| {
4507                    editor.insert("dirt", window, cx);
4508                    editor.buffer().downgrade()
4509                });
4510                (editor.downgrade(), buffer)
4511            })
4512        });
4513
4514        cx.dispatch_action(pane::SplitRight::default());
4515        let editor_2 = cx.update(|_, cx| {
4516            let pane_2 = workspace.read(cx).active_pane().clone();
4517            assert_ne!(pane_1, pane_2);
4518
4519            let pane2_item = pane_2.read(cx).active_item().unwrap();
4520            assert_eq!(pane2_item.project_path(cx), Some(file1.clone()));
4521
4522            pane2_item.downcast::<Editor>().unwrap().downgrade()
4523        });
4524        cx.dispatch_action(workspace::CloseActiveItem {
4525            save_intent: None,
4526            close_pinned: false,
4527        });
4528
4529        cx.background_executor.run_until_parked();
4530        workspace.read_with(cx, |workspace, _| {
4531            assert_eq!(workspace.panes().len(), 1);
4532            assert_eq!(workspace.active_pane(), &pane_1);
4533        });
4534
4535        cx.dispatch_action(workspace::CloseActiveItem {
4536            save_intent: None,
4537            close_pinned: false,
4538        });
4539        cx.background_executor.run_until_parked();
4540        cx.simulate_prompt_answer("Don't Save");
4541        cx.background_executor.run_until_parked();
4542
4543        workspace.read_with(cx, |workspace, cx| {
4544            assert_eq!(workspace.panes().len(), 1);
4545            assert!(workspace.active_item(cx).is_none());
4546        });
4547
4548        cx.background_executor
4549            .advance_clock(SERIALIZATION_THROTTLE_TIME);
4550        cx.update(|_, _| {});
4551        editor_1.assert_released();
4552        editor_2.assert_released();
4553        buffer.assert_released();
4554    }
4555
4556    #[gpui::test]
4557    async fn test_editor_zoom_with_scroll_wheel(cx: &mut TestAppContext) {
4558        let app_state = init_test(cx);
4559        app_state
4560            .fs
4561            .as_fake()
4562            .insert_tree(path!("/root"), json!({ "file.txt": "hello\nworld\n" }))
4563            .await;
4564
4565        let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
4566        let window =
4567            cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
4568        let workspace = window
4569            .read_with(cx, |mw, _| mw.workspace().clone())
4570            .unwrap();
4571        let cx = &mut VisualTestContext::from_window(*window, cx);
4572
4573        let mouse_position = point(px(250.), px(250.));
4574
4575        let event_modifiers = {
4576            #[cfg(target_os = "macos")]
4577            {
4578                Modifiers {
4579                    platform: true,
4580                    ..Modifiers::default()
4581                }
4582            }
4583
4584            #[cfg(not(target_os = "macos"))]
4585            {
4586                Modifiers {
4587                    control: true,
4588                    ..Modifiers::default()
4589                }
4590            }
4591        };
4592
4593        workspace
4594            .update_in(cx, |workspace, window, cx| {
4595                workspace.open_abs_path(
4596                    PathBuf::from(path!("/root/file.txt")),
4597                    OpenOptions::default(),
4598                    window,
4599                    cx,
4600                )
4601            })
4602            .await
4603            .unwrap()
4604            .downcast::<Editor>()
4605            .unwrap();
4606
4607        cx.update(|window, cx| {
4608            window.draw(cx).clear(cx);
4609        });
4610
4611        // mouse_wheel_zoom is disabled by default — zoom should not work.
4612        let initial_font_size =
4613            cx.update(|_, cx| ThemeSettings::get_global(cx).buffer_font_size(cx).as_f32());
4614
4615        cx.simulate_event(gpui::ScrollWheelEvent {
4616            position: mouse_position,
4617            delta: gpui::ScrollDelta::Pixels(point(px(0.), px(1.))),
4618            modifiers: event_modifiers,
4619            ..Default::default()
4620        });
4621
4622        let font_size_after_disabled_zoom =
4623            cx.update(|_, cx| ThemeSettings::get_global(cx).buffer_font_size(cx).as_f32());
4624
4625        assert_eq!(
4626            initial_font_size, font_size_after_disabled_zoom,
4627            "Editor buffer font-size should not change when mouse_wheel_zoom is disabled"
4628        );
4629
4630        // Enable mouse_wheel_zoom and verify zoom works.
4631        cx.update(|_, cx| {
4632            SettingsStore::update_global(cx, |store, cx| {
4633                store.update_user_settings(cx, |settings| {
4634                    settings.editor.mouse_wheel_zoom = Some(true);
4635                });
4636            });
4637        });
4638
4639        cx.update(|window, cx| {
4640            window.draw(cx).clear(cx);
4641        });
4642
4643        cx.simulate_event(gpui::ScrollWheelEvent {
4644            position: mouse_position,
4645            delta: gpui::ScrollDelta::Pixels(point(px(0.), px(1.))),
4646            modifiers: event_modifiers,
4647            ..Default::default()
4648        });
4649
4650        let increased_font_size =
4651            cx.update(|_, cx| ThemeSettings::get_global(cx).buffer_font_size(cx).as_f32());
4652
4653        assert!(
4654            increased_font_size > initial_font_size,
4655            "Editor buffer font-size should have increased from scroll-zoom"
4656        );
4657
4658        cx.update(|window, cx| {
4659            window.draw(cx).clear(cx);
4660        });
4661
4662        cx.simulate_event(gpui::ScrollWheelEvent {
4663            position: mouse_position,
4664            delta: gpui::ScrollDelta::Pixels(point(px(0.), px(-1.))),
4665            modifiers: event_modifiers,
4666            ..Default::default()
4667        });
4668
4669        let decreased_font_size =
4670            cx.update(|_, cx| ThemeSettings::get_global(cx).buffer_font_size(cx).as_f32());
4671
4672        assert!(
4673            decreased_font_size < increased_font_size,
4674            "Editor buffer font-size should have decreased from scroll-zoom"
4675        );
4676
4677        // Disable mouse_wheel_zoom again and verify zoom stops working.
4678        cx.update(|_, cx| {
4679            SettingsStore::update_global(cx, |store, cx| {
4680                store.update_user_settings(cx, |settings| {
4681                    settings.editor.mouse_wheel_zoom = Some(false);
4682                });
4683            });
4684        });
4685
4686        let font_size_before =
4687            cx.update(|_, cx| ThemeSettings::get_global(cx).buffer_font_size(cx).as_f32());
4688
4689        cx.update(|window, cx| {
4690            window.draw(cx).clear(cx);
4691        });
4692
4693        cx.simulate_event(gpui::ScrollWheelEvent {
4694            position: mouse_position,
4695            delta: gpui::ScrollDelta::Pixels(point(px(0.), px(1.))),
4696            modifiers: event_modifiers,
4697            ..Default::default()
4698        });
4699
4700        let font_size_after =
4701            cx.update(|_, cx| ThemeSettings::get_global(cx).buffer_font_size(cx).as_f32());
4702
4703        assert_eq!(
4704            font_size_before, font_size_after,
4705            "Editor buffer font-size should not change when mouse_wheel_zoom is re-disabled"
4706        );
4707    }
4708
4709    #[gpui::test]
4710    async fn test_navigation(cx: &mut TestAppContext) {
4711        let app_state = init_test(cx);
4712        app_state
4713            .fs
4714            .as_fake()
4715            .insert_tree(
4716                path!("/root"),
4717                json!({
4718                    "a": {
4719                        "file1": "contents 1\n".repeat(20),
4720                        "file2": "contents 2\n".repeat(20),
4721                        "file3": "contents 3\n".repeat(20),
4722                    },
4723                }),
4724            )
4725            .await;
4726
4727        let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
4728        project.update(cx, |project, _cx| project.languages().add(markdown_lang()));
4729        let window =
4730            cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
4731        let workspace = window
4732            .read_with(cx, |mw, _| mw.workspace().clone())
4733            .unwrap();
4734        let cx = &mut VisualTestContext::from_window(*window, cx);
4735        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4736
4737        let entries = cx.read(|cx| workspace.file_project_paths(cx));
4738        let file1 = entries[0].clone();
4739        let file2 = entries[1].clone();
4740        let file3 = entries[2].clone();
4741
4742        let editor1 = workspace
4743            .update_in(cx, |w, window, cx| {
4744                w.open_path(file1.clone(), None, true, window, cx)
4745            })
4746            .await
4747            .unwrap()
4748            .downcast::<Editor>()
4749            .unwrap();
4750        workspace.update_in(cx, |_, window, cx| {
4751            editor1.update(cx, |editor, cx| {
4752                editor.change_selections(Default::default(), window, cx, |s| {
4753                    s.select_display_ranges([
4754                        DisplayPoint::new(DisplayRow(10), 0)..DisplayPoint::new(DisplayRow(10), 0)
4755                    ])
4756                });
4757            });
4758        });
4759
4760        let editor2 = workspace
4761            .update_in(cx, |w, window, cx| {
4762                w.open_path(file2.clone(), None, true, window, cx)
4763            })
4764            .await
4765            .unwrap()
4766            .downcast::<Editor>()
4767            .unwrap();
4768        let editor3 = workspace
4769            .update_in(cx, |w, window, cx| {
4770                w.open_path(file3.clone(), None, true, window, cx)
4771            })
4772            .await
4773            .unwrap()
4774            .downcast::<Editor>()
4775            .unwrap();
4776
4777        workspace
4778            .update_in(cx, |_, window, cx| {
4779                editor3.update(cx, |editor, cx| {
4780                    editor.change_selections(Default::default(), window, cx, |s| {
4781                        s.select_display_ranges([DisplayPoint::new(DisplayRow(12), 0)
4782                            ..DisplayPoint::new(DisplayRow(12), 0)])
4783                    });
4784                    editor.newline(&Default::default(), window, cx);
4785                    editor.newline(&Default::default(), window, cx);
4786                    editor.move_down(&Default::default(), window, cx);
4787                    editor.move_down(&Default::default(), window, cx);
4788                    editor.save(
4789                        SaveOptions {
4790                            format: true,
4791                            force_format: false,
4792                            autosave: false,
4793                        },
4794                        project.clone(),
4795                        window,
4796                        cx,
4797                    )
4798                })
4799            })
4800            .await
4801            .unwrap();
4802        workspace.update_in(cx, |_, window, cx| {
4803            editor3.update(cx, |editor, cx| {
4804                editor.set_scroll_position(point(0., 12.5), window, cx)
4805            });
4806        });
4807        assert_eq!(
4808            active_location(&workspace, cx),
4809            (file3.clone(), DisplayPoint::new(DisplayRow(16), 0), 12.5)
4810        );
4811
4812        workspace
4813            .update_in(cx, |w, window, cx| {
4814                w.go_back(w.active_pane().downgrade(), window, cx)
4815            })
4816            .await
4817            .unwrap();
4818        assert_eq!(
4819            active_location(&workspace, cx),
4820            (file3.clone(), DisplayPoint::new(DisplayRow(0), 0), 0.)
4821        );
4822
4823        workspace
4824            .update_in(cx, |w, window, cx| {
4825                w.go_back(w.active_pane().downgrade(), window, cx)
4826            })
4827            .await
4828            .unwrap();
4829        assert_eq!(
4830            active_location(&workspace, cx),
4831            (file2.clone(), DisplayPoint::new(DisplayRow(0), 0), 0.)
4832        );
4833
4834        workspace
4835            .update_in(cx, |w, window, cx| {
4836                w.go_back(w.active_pane().downgrade(), window, cx)
4837            })
4838            .await
4839            .unwrap();
4840        assert_eq!(
4841            active_location(&workspace, cx),
4842            (file1.clone(), DisplayPoint::new(DisplayRow(10), 0), 0.)
4843        );
4844
4845        workspace
4846            .update_in(cx, |w, window, cx| {
4847                w.go_back(w.active_pane().downgrade(), window, cx)
4848            })
4849            .await
4850            .unwrap();
4851        assert_eq!(
4852            active_location(&workspace, cx),
4853            (file1.clone(), DisplayPoint::new(DisplayRow(0), 0), 0.)
4854        );
4855
4856        // Go back one more time and ensure we don't navigate past the first item in the history.
4857        workspace
4858            .update_in(cx, |w, window, cx| {
4859                w.go_back(w.active_pane().downgrade(), window, cx)
4860            })
4861            .await
4862            .unwrap();
4863        assert_eq!(
4864            active_location(&workspace, cx),
4865            (file1.clone(), DisplayPoint::new(DisplayRow(0), 0), 0.)
4866        );
4867
4868        workspace
4869            .update_in(cx, |w, window, cx| {
4870                w.go_forward(w.active_pane().downgrade(), window, cx)
4871            })
4872            .await
4873            .unwrap();
4874        assert_eq!(
4875            active_location(&workspace, cx),
4876            (file1.clone(), DisplayPoint::new(DisplayRow(10), 0), 0.)
4877        );
4878
4879        workspace
4880            .update_in(cx, |w, window, cx| {
4881                w.go_forward(w.active_pane().downgrade(), window, cx)
4882            })
4883            .await
4884            .unwrap();
4885        assert_eq!(
4886            active_location(&workspace, cx),
4887            (file2.clone(), DisplayPoint::new(DisplayRow(0), 0), 0.)
4888        );
4889
4890        // Go forward to an item that has been closed, ensuring it gets re-opened at the same
4891        // location.
4892        workspace
4893            .update_in(cx, |_, window, cx| {
4894                pane.update(cx, |pane, cx| {
4895                    let editor3_id = editor3.entity_id();
4896                    drop(editor3);
4897                    pane.close_item_by_id(editor3_id, SaveIntent::Close, window, cx)
4898                })
4899            })
4900            .await
4901            .unwrap();
4902        workspace
4903            .update_in(cx, |w, window, cx| {
4904                w.go_forward(w.active_pane().downgrade(), window, cx)
4905            })
4906            .await
4907            .unwrap();
4908        assert_eq!(
4909            active_location(&workspace, cx),
4910            (file3.clone(), DisplayPoint::new(DisplayRow(0), 0), 0.)
4911        );
4912
4913        workspace
4914            .update_in(cx, |w, window, cx| {
4915                w.go_forward(w.active_pane().downgrade(), window, cx)
4916            })
4917            .await
4918            .unwrap();
4919        assert_eq!(
4920            active_location(&workspace, cx),
4921            (file3.clone(), DisplayPoint::new(DisplayRow(16), 0), 12.5)
4922        );
4923
4924        workspace
4925            .update_in(cx, |w, window, cx| {
4926                w.go_back(w.active_pane().downgrade(), window, cx)
4927            })
4928            .await
4929            .unwrap();
4930        assert_eq!(
4931            active_location(&workspace, cx),
4932            (file3.clone(), DisplayPoint::new(DisplayRow(0), 0), 0.)
4933        );
4934
4935        // Go back to an item that has been closed and removed from disk
4936        workspace
4937            .update_in(cx, |_, window, cx| {
4938                pane.update(cx, |pane, cx| {
4939                    let editor2_id = editor2.entity_id();
4940                    drop(editor2);
4941                    pane.close_item_by_id(editor2_id, SaveIntent::Close, window, cx)
4942                })
4943            })
4944            .await
4945            .unwrap();
4946        app_state
4947            .fs
4948            .remove_file(Path::new(path!("/root/a/file2")), Default::default())
4949            .await
4950            .unwrap();
4951        cx.background_executor.run_until_parked();
4952
4953        workspace
4954            .update_in(cx, |w, window, cx| {
4955                w.go_back(w.active_pane().downgrade(), window, cx)
4956            })
4957            .await
4958            .unwrap();
4959        assert_eq!(
4960            active_location(&workspace, cx),
4961            (file2.clone(), DisplayPoint::new(DisplayRow(0), 0), 0.)
4962        );
4963        workspace
4964            .update_in(cx, |w, window, cx| {
4965                w.go_forward(w.active_pane().downgrade(), window, cx)
4966            })
4967            .await
4968            .unwrap();
4969        assert_eq!(
4970            active_location(&workspace, cx),
4971            (file3.clone(), DisplayPoint::new(DisplayRow(0), 0), 0.)
4972        );
4973
4974        // Modify file to collapse multiple nav history entries into the same location.
4975        // Ensure we don't visit the same location twice when navigating.
4976        workspace.update_in(cx, |_, window, cx| {
4977            editor1.update(cx, |editor, cx| {
4978                editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4979                    s.select_display_ranges([
4980                        DisplayPoint::new(DisplayRow(15), 0)..DisplayPoint::new(DisplayRow(15), 0)
4981                    ])
4982                })
4983            });
4984        });
4985        for _ in 0..5 {
4986            workspace.update_in(cx, |_, window, cx| {
4987                editor1.update(cx, |editor, cx| {
4988                    editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4989                        s.select_display_ranges([DisplayPoint::new(DisplayRow(3), 0)
4990                            ..DisplayPoint::new(DisplayRow(3), 0)])
4991                    });
4992                });
4993            });
4994
4995            workspace.update_in(cx, |_, window, cx| {
4996                editor1.update(cx, |editor, cx| {
4997                    editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4998                        s.select_display_ranges([DisplayPoint::new(DisplayRow(13), 0)
4999                            ..DisplayPoint::new(DisplayRow(13), 0)])
5000                    });
5001                });
5002            });
5003        }
5004        workspace.update_in(cx, |_, window, cx| {
5005            editor1.update(cx, |editor, cx| {
5006                editor.transact(window, cx, |editor, window, cx| {
5007                    editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5008                        s.select_display_ranges([DisplayPoint::new(DisplayRow(2), 0)
5009                            ..DisplayPoint::new(DisplayRow(14), 0)])
5010                    });
5011                    editor.insert("", window, cx);
5012                })
5013            });
5014        });
5015
5016        workspace.update_in(cx, |_, window, cx| {
5017            editor1.update(cx, |editor, cx| {
5018                editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5019                    s.select_display_ranges([
5020                        DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
5021                    ])
5022                })
5023            });
5024        });
5025        workspace
5026            .update_in(cx, |w, window, cx| {
5027                w.go_back(w.active_pane().downgrade(), window, cx)
5028            })
5029            .await
5030            .unwrap();
5031        assert_eq!(
5032            active_location(&workspace, cx),
5033            (file1.clone(), DisplayPoint::new(DisplayRow(2), 0), 0.)
5034        );
5035        workspace
5036            .update_in(cx, |w, window, cx| {
5037                w.go_back(w.active_pane().downgrade(), window, cx)
5038            })
5039            .await
5040            .unwrap();
5041        assert_eq!(
5042            active_location(&workspace, cx),
5043            (file1.clone(), DisplayPoint::new(DisplayRow(3), 0), 0.)
5044        );
5045
5046        fn active_location(
5047            workspace: &Entity<Workspace>,
5048            cx: &mut VisualTestContext,
5049        ) -> (ProjectPath, DisplayPoint, f64) {
5050            workspace.update(cx, |workspace, cx| {
5051                let item = workspace.active_item(cx).unwrap();
5052                let editor = item.downcast::<Editor>().unwrap();
5053
5054                editor.update(cx, |editor_ref, cx| {
5055                    let selections = editor_ref
5056                        .selections
5057                        .display_ranges(&editor_ref.display_snapshot(cx));
5058                    let scroll_position = editor_ref.scroll_position(cx);
5059
5060                    (
5061                        editor_ref.active_project_path(cx).unwrap(),
5062                        selections[0].start,
5063                        scroll_position.y,
5064                    )
5065                })
5066            })
5067        }
5068    }
5069
5070    #[gpui::test]
5071    async fn test_reopening_closed_items(cx: &mut TestAppContext) {
5072        let app_state = init_test(cx);
5073        app_state
5074            .fs
5075            .as_fake()
5076            .insert_tree(
5077                path!("/root"),
5078                json!({
5079                    "a": {
5080                        "file1": "",
5081                        "file2": "",
5082                        "file3": "",
5083                        "file4": "",
5084                    },
5085                }),
5086            )
5087            .await;
5088
5089        let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
5090        project.update(cx, |project, _cx| project.languages().add(markdown_lang()));
5091        let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project, window, cx));
5092        let workspace = window
5093            .read_with(cx, |mw, _| mw.workspace().clone())
5094            .unwrap();
5095        let cx = &mut VisualTestContext::from_window(*window, cx);
5096        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
5097
5098        let entries = cx.read(|cx| workspace.file_project_paths(cx));
5099        let file1 = entries[0].clone();
5100        let file2 = entries[1].clone();
5101        let file3 = entries[2].clone();
5102        let file4 = entries[3].clone();
5103
5104        let file1_item_id = workspace
5105            .update_in(cx, |w, window, cx| {
5106                w.open_path(file1.clone(), None, true, window, cx)
5107            })
5108            .await
5109            .unwrap()
5110            .item_id();
5111        let file2_item_id = workspace
5112            .update_in(cx, |w, window, cx| {
5113                w.open_path(file2.clone(), None, true, window, cx)
5114            })
5115            .await
5116            .unwrap()
5117            .item_id();
5118        let file3_item_id = workspace
5119            .update_in(cx, |w, window, cx| {
5120                w.open_path(file3.clone(), None, true, window, cx)
5121            })
5122            .await
5123            .unwrap()
5124            .item_id();
5125        let file4_item_id = workspace
5126            .update_in(cx, |w, window, cx| {
5127                w.open_path(file4.clone(), None, true, window, cx)
5128            })
5129            .await
5130            .unwrap()
5131            .item_id();
5132        assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
5133
5134        // Close all the pane items in some arbitrary order.
5135        workspace
5136            .update_in(cx, |_, window, cx| {
5137                pane.update(cx, |pane, cx| {
5138                    pane.close_item_by_id(file1_item_id, SaveIntent::Close, window, cx)
5139                })
5140            })
5141            .await
5142            .unwrap();
5143        assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
5144
5145        workspace
5146            .update_in(cx, |_, window, cx| {
5147                pane.update(cx, |pane, cx| {
5148                    pane.close_item_by_id(file4_item_id, SaveIntent::Close, window, cx)
5149                })
5150            })
5151            .await
5152            .unwrap();
5153        assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
5154
5155        workspace
5156            .update_in(cx, |_, window, cx| {
5157                pane.update(cx, |pane, cx| {
5158                    pane.close_item_by_id(file2_item_id, SaveIntent::Close, window, cx)
5159                })
5160            })
5161            .await
5162            .unwrap();
5163        assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
5164        workspace
5165            .update_in(cx, |_, window, cx| {
5166                pane.update(cx, |pane, cx| {
5167                    pane.close_item_by_id(file3_item_id, SaveIntent::Close, window, cx)
5168                })
5169            })
5170            .await
5171            .unwrap();
5172
5173        assert_eq!(active_path(&workspace, cx), None);
5174
5175        // Reopen all the closed items, ensuring they are reopened in the same order
5176        // in which they were closed.
5177        workspace
5178            .update_in(cx, Workspace::reopen_closed_item)
5179            .await
5180            .unwrap();
5181        assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
5182
5183        workspace
5184            .update_in(cx, Workspace::reopen_closed_item)
5185            .await
5186            .unwrap();
5187        assert_eq!(active_path(&workspace, cx), Some(file2.clone()));
5188
5189        workspace
5190            .update_in(cx, Workspace::reopen_closed_item)
5191            .await
5192            .unwrap();
5193        assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
5194
5195        workspace
5196            .update_in(cx, Workspace::reopen_closed_item)
5197            .await
5198            .unwrap();
5199        assert_eq!(active_path(&workspace, cx), Some(file1.clone()));
5200
5201        // Reopening past the last closed item is a no-op.
5202        workspace
5203            .update_in(cx, Workspace::reopen_closed_item)
5204            .await
5205            .unwrap();
5206        assert_eq!(active_path(&workspace, cx), Some(file1.clone()));
5207
5208        // Reopening closed items doesn't interfere with navigation history.
5209        // Verify we can navigate back through the history after reopening items.
5210        workspace
5211            .update_in(cx, |workspace, window, cx| {
5212                workspace.go_back(workspace.active_pane().downgrade(), window, cx)
5213            })
5214            .await
5215            .unwrap();
5216
5217        // After go_back, we should be at a different file than file1
5218        let after_go_back = active_path(&workspace, cx);
5219        assert!(
5220            after_go_back.is_some() && after_go_back != Some(file1.clone()),
5221            "After go_back from file1, should be at a different file"
5222        );
5223
5224        pane.read_with(cx, |pane, _| {
5225            assert!(pane.can_navigate_forward(), "Should be able to go forward");
5226        });
5227
5228        fn active_path(
5229            workspace: &Entity<Workspace>,
5230            cx: &VisualTestContext,
5231        ) -> Option<ProjectPath> {
5232            workspace.read_with(cx, |workspace, cx| {
5233                let item = workspace.active_item(cx)?;
5234                item.project_path(cx)
5235            })
5236        }
5237    }
5238
5239    fn init_keymap_test(cx: &mut TestAppContext) -> Arc<AppState> {
5240        cx.update(|cx| {
5241            let app_state = AppState::test(cx);
5242
5243            theme_settings::init(theme::LoadThemes::JustBase, cx);
5244            client::init(&app_state.client, cx);
5245            workspace::init(app_state.clone(), cx);
5246            onboarding::init(cx);
5247            app_state
5248        })
5249    }
5250
5251    actions!(test_only, [ActionA, ActionB]);
5252
5253    /// The actions the emacs keymap resolves for `keystroke` in `context`.
5254    fn emacs_bindings_for(keystroke: &str, context: &str, cx: &mut TestAppContext) -> Vec<String> {
5255        cx.update(|cx| {
5256            let mut bindings = settings::KeymapFile::load_asset_allow_partial_failure(
5257                "keymaps/default-linux.json",
5258                cx,
5259            )
5260            .unwrap();
5261            for binding in &mut bindings {
5262                binding.set_meta(settings::KeybindSource::Default.meta());
5263            }
5264            let mut emacs_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
5265                "keymaps/linux/emacs.json",
5266                cx,
5267            )
5268            .unwrap();
5269            for binding in &mut emacs_bindings {
5270                binding.set_meta(settings::KeybindSource::Base.meta());
5271            }
5272            bindings.extend(emacs_bindings);
5273
5274            gpui::Keymap::new(bindings)
5275                .bindings_for_input(
5276                    &[gpui::Keystroke::parse(keystroke).unwrap()],
5277                    &[gpui::KeyContext::parse(context).unwrap()],
5278                )
5279                .0
5280                .iter()
5281                .map(|binding| binding.action().name().to_string())
5282                .collect()
5283        })
5284    }
5285
5286    /// `editor::MoveDown` and `editor::MoveUp` propagate when the cursor doesn't move, which at the
5287    /// ends of a buffer let `ctrl-n` and `ctrl-p` fall through to the default bindings and open a
5288    /// new file / the file finder.
5289    #[gpui::test]
5290    fn test_emacs_cursor_keys_do_not_fall_back_to_default_bindings(cx: &mut TestAppContext) {
5291        init_keymap_test(cx);
5292
5293        let ctrl_n = emacs_bindings_for("ctrl-n", "Workspace Editor", cx);
5294        assert!(
5295            ctrl_n.contains(&"editor::MoveDown".to_string()),
5296            "ctrl-n should still move down, got {ctrl_n:?}"
5297        );
5298        assert!(
5299            !ctrl_n.contains(&"workspace::NewFile".to_string()),
5300            "ctrl-n should not fall through to workspace::NewFile, got {ctrl_n:?}"
5301        );
5302
5303        let ctrl_p = emacs_bindings_for("ctrl-p", "Workspace Editor", cx);
5304        assert!(
5305            ctrl_p.contains(&"editor::MoveUp".to_string()),
5306            "ctrl-p should still move up, got {ctrl_p:?}"
5307        );
5308        assert!(
5309            !ctrl_p.contains(&"file_finder::Toggle".to_string()),
5310            "ctrl-p should not fall through to file_finder::Toggle, got {ctrl_p:?}"
5311        );
5312    }
5313
5314    /// The unbind above only targets `workspace::NewFile` / `file_finder::Toggle`, so the narrower
5315    /// `ctrl-n` and `ctrl-p` bindings still win where they apply.
5316    #[gpui::test]
5317    fn test_emacs_cursor_keys_keep_narrower_bindings(cx: &mut TestAppContext) {
5318        init_keymap_test(cx);
5319
5320        let completions = "Workspace Editor showing_completions";
5321        assert_eq!(
5322            emacs_bindings_for("ctrl-n", completions, cx).first(),
5323            Some(&"editor::ContextMenuNext".to_string())
5324        );
5325        assert_eq!(
5326            emacs_bindings_for("ctrl-p", completions, cx).first(),
5327            Some(&"editor::ContextMenuPrevious".to_string())
5328        );
5329
5330        let selection_mode = "Workspace Editor selection_mode";
5331        assert_eq!(
5332            emacs_bindings_for("ctrl-n", selection_mode, cx).first(),
5333            Some(&"editor::SelectDown".to_string())
5334        );
5335        assert_eq!(
5336            emacs_bindings_for("ctrl-p", selection_mode, cx).first(),
5337            Some(&"editor::SelectUp".to_string())
5338        );
5339    }
5340
5341    #[gpui::test]
5342    async fn test_base_keymap(cx: &mut gpui::TestAppContext) {
5343        let executor = cx.executor();
5344        let app_state = init_keymap_test(cx);
5345        let project = Project::test(app_state.fs.clone(), [], cx).await;
5346        let window =
5347            cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
5348        let workspace = window
5349            .read_with(cx, |mw, _| mw.workspace().clone())
5350            .unwrap();
5351
5352        // From the Atom keymap
5353        use workspace::ActivatePreviousPane;
5354        // From the JetBrains keymap
5355        use workspace::ActivatePreviousItem;
5356        // From the VSCode keymap
5357        use debugger_ui::Start;
5358
5359        app_state
5360            .fs
5361            .save(
5362                paths::settings_file(),
5363                &r#"{"base_keymap": "Atom"}"#.into(),
5364                Default::default(),
5365            )
5366            .await
5367            .unwrap();
5368
5369        app_state
5370            .fs
5371            .save(
5372                "/keymap.json".as_ref(),
5373                &r#"[{"bindings": {"backspace": "test_only::ActionA"}}]"#.into(),
5374                Default::default(),
5375            )
5376            .await
5377            .unwrap();
5378        executor.run_until_parked();
5379        cx.update(|cx| {
5380            let (keymap_rx, keymap_watcher) = watch_config_file(
5381                &executor,
5382                app_state.fs.clone(),
5383                PathBuf::from("/keymap.json"),
5384            );
5385            watch_settings_files(app_state.fs.clone(), cx);
5386            handle_keymap_file_changes(keymap_rx, keymap_watcher, cx);
5387        });
5388        window
5389            .update(cx, |_, _, cx| {
5390                workspace.update(cx, |workspace, cx| {
5391                    workspace.register_action(|_, _: &ActionA, _window, _cx| {});
5392                    workspace.register_action(|_, _: &ActionB, _window, _cx| {});
5393                    workspace.register_action(|_, _: &ActivatePreviousPane, _window, _cx| {});
5394                    workspace.register_action(|_, _: &ActivatePreviousItem, _window, _cx| {});
5395                    cx.notify();
5396                });
5397            })
5398            .unwrap();
5399        executor.run_until_parked();
5400        // Test loading the keymap base at all
5401        assert_key_bindings_for(
5402            window.into(),
5403            cx,
5404            vec![("backspace", &ActionA), ("k", &ActivatePreviousPane)],
5405            line!(),
5406        );
5407
5408        // Test modifying the users keymap, while retaining the base keymap
5409        app_state
5410            .fs
5411            .save(
5412                "/keymap.json".as_ref(),
5413                &r#"[{"bindings": {"backspace": "test_only::ActionB"}}]"#.into(),
5414                Default::default(),
5415            )
5416            .await
5417            .unwrap();
5418
5419        executor.run_until_parked();
5420
5421        assert_key_bindings_for(
5422            window.into(),
5423            cx,
5424            vec![("backspace", &ActionB), ("k", &ActivatePreviousPane)],
5425            line!(),
5426        );
5427
5428        // Test modifying the base, while retaining the users keymap
5429        app_state
5430            .fs
5431            .save(
5432                paths::settings_file(),
5433                &r#"{"base_keymap": "JetBrains"}"#.into(),
5434                Default::default(),
5435            )
5436            .await
5437            .unwrap();
5438
5439        executor.run_until_parked();
5440
5441        assert_key_bindings_for(
5442            window.into(),
5443            cx,
5444            vec![
5445                ("backspace", &ActionB),
5446                ("{", &ActivatePreviousItem::default()),
5447            ],
5448            line!(),
5449        );
5450
5451        // Test the VSCode keymap overlay
5452        app_state
5453            .fs
5454            .save(
5455                paths::settings_file(),
5456                &r#"{"base_keymap": "VSCode"}"#.into(),
5457                Default::default(),
5458            )
5459            .await
5460            .unwrap();
5461
5462        executor.run_until_parked();
5463
5464        window
5465            .update(cx, |_, _, cx| {
5466                workspace.update(cx, |workspace, cx| {
5467                    workspace.register_action(|_, _: &Start, _window, _cx| {});
5468                    cx.notify();
5469                });
5470            })
5471            .unwrap();
5472        executor.run_until_parked();
5473
5474        assert_key_bindings_for(
5475            window.into(),
5476            cx,
5477            vec![("backspace", &ActionB), ("f5", &Start)],
5478            line!(),
5479        );
5480    }
5481
5482    #[gpui::test]
5483    async fn test_disabled_keymap_binding(cx: &mut gpui::TestAppContext) {
5484        let executor = cx.executor();
5485        let app_state = init_keymap_test(cx);
5486        let project = Project::test(app_state.fs.clone(), [], cx).await;
5487        let window =
5488            cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
5489        let workspace = window
5490            .read_with(cx, |mw, _| mw.workspace().clone())
5491            .unwrap();
5492
5493        // From the Atom keymap
5494        use workspace::ActivatePreviousPane;
5495        // From the JetBrains keymap
5496        use diagnostics::Deploy;
5497
5498        window
5499            .update(cx, |_, _, cx| {
5500                workspace.update(cx, |workspace, cx| {
5501                    workspace.register_action(|_, _: &ActionA, _window, _cx| {});
5502                    workspace.register_action(|_, _: &ActionB, _window, _cx| {});
5503                    workspace.register_action(|_, _: &Deploy, _window, _cx| {});
5504                    cx.notify();
5505                });
5506            })
5507            .unwrap();
5508        app_state
5509            .fs
5510            .save(
5511                paths::settings_file(),
5512                &r#"{"base_keymap": "Atom"}"#.into(),
5513                Default::default(),
5514            )
5515            .await
5516            .unwrap();
5517        app_state
5518            .fs
5519            .save(
5520                "/keymap.json".as_ref(),
5521                &r#"[{"bindings": {"backspace": "test_only::ActionA"}}]"#.into(),
5522                Default::default(),
5523            )
5524            .await
5525            .unwrap();
5526
5527        cx.update(|cx| {
5528            let (keymap_rx, keymap_watcher) = watch_config_file(
5529                &executor,
5530                app_state.fs.clone(),
5531                PathBuf::from("/keymap.json"),
5532            );
5533
5534            watch_settings_files(app_state.fs.clone(), cx);
5535            handle_keymap_file_changes(keymap_rx, keymap_watcher, cx);
5536        });
5537
5538        cx.background_executor.run_until_parked();
5539
5540        cx.background_executor.run_until_parked();
5541        // Test loading the keymap base at all
5542        assert_key_bindings_for(
5543            window.into(),
5544            cx,
5545            vec![("backspace", &ActionA), ("k", &ActivatePreviousPane)],
5546            line!(),
5547        );
5548
5549        // Test disabling the key binding for the base keymap
5550        app_state
5551            .fs
5552            .save(
5553                "/keymap.json".as_ref(),
5554                &r#"[{"bindings": {"backspace": null}}]"#.into(),
5555                Default::default(),
5556            )
5557            .await
5558            .unwrap();
5559
5560        cx.background_executor.run_until_parked();
5561
5562        assert_key_bindings_for(
5563            window.into(),
5564            cx,
5565            vec![("k", &ActivatePreviousPane)],
5566            line!(),
5567        );
5568
5569        // Test modifying the base, while retaining the users keymap
5570        app_state
5571            .fs
5572            .save(
5573                paths::settings_file(),
5574                &r#"{"base_keymap": "JetBrains"}"#.into(),
5575                Default::default(),
5576            )
5577            .await
5578            .unwrap();
5579
5580        cx.background_executor.run_until_parked();
5581
5582        assert_key_bindings_for(window.into(), cx, vec![("6", &Deploy)], line!());
5583    }
5584
5585    #[gpui::test]
5586    async fn test_generate_keymap_json_schema_for_registered_actions(
5587        cx: &mut gpui::TestAppContext,
5588    ) {
5589        init_keymap_test(cx);
5590        cx.update(|cx| {
5591            // Make sure it doesn't panic.
5592            KeymapFile::generate_json_schema_for_registered_actions(cx);
5593        });
5594    }
5595
5596    /// Checks that action namespaces are the expected set. The purpose of this is to prevent typos
5597    /// and let you know when introducing a new namespace.
5598    #[gpui::test]
5599    async fn test_action_namespaces(cx: &mut gpui::TestAppContext) {
5600        use itertools::Itertools;
5601
5602        init_keymap_test(cx);
5603        cx.update(|cx| {
5604            let all_actions = cx.all_action_names();
5605
5606            let mut actions_without_namespace = Vec::new();
5607            let all_namespaces = all_actions
5608                .iter()
5609                .filter_map(|action_name| {
5610                    let namespace = action_name
5611                        .split("::")
5612                        .collect::<Vec<_>>()
5613                        .into_iter()
5614                        .rev()
5615                        .skip(1)
5616                        .rev()
5617                        .join("::");
5618                    if namespace.is_empty() {
5619                        actions_without_namespace.push(*action_name);
5620                    }
5621                    if &namespace == "test_only" || &namespace == "stories" {
5622                        None
5623                    } else {
5624                        Some(namespace)
5625                    }
5626                })
5627                .sorted()
5628                .dedup()
5629                .collect::<Vec<_>>();
5630            assert_eq!(actions_without_namespace, Vec::<&str>::new());
5631
5632            let expected_namespaces = vec![
5633                "action",
5634                "activity_indicator",
5635                "agent",
5636                "agents_sidebar",
5637                "app_menu",
5638                "assistant",
5639                "assistant2",
5640                "auto_update",
5641                "branch_picker",
5642                "bedrock",
5643                "branches",
5644                "buffer_search",
5645                "channel_modal",
5646                "cli",
5647                "client",
5648                "collab",
5649                "collab_panel",
5650                "command_palette",
5651                "console",
5652                "context_server",
5653                "copilot",
5654                "csv",
5655                "debug_panel",
5656                "debugger",
5657                "dev",
5658                "diagnostics",
5659                "edit_prediction",
5660                "editor",
5661                "encoding_selector",
5662                "feedback",
5663                "file_finder",
5664                "git",
5665                "git_graph",
5666                "git_onboarding",
5667                "git_panel",
5668                "git_picker",
5669                "go_to_line",
5670                "highlights_tree_view",
5671                "icon_theme_selector",
5672                "image_viewer",
5673                "inline_assistant",
5674                "journal",
5675                "keymap_editor",
5676                "keystroke_input",
5677                "language_selector",
5678                "welcome",
5679                "line_ending_selector",
5680                "lsp_tool",
5681                "markdown",
5682                "menu",
5683                "multi_workspace",
5684                "new_process_modal",
5685                "notebook",
5686                "onboarding",
5687                "outline",
5688                "outline_panel",
5689                "pane",
5690                "panel",
5691                "picker",
5692                "project_panel",
5693                "project_search",
5694                "project_symbols",
5695                "projects",
5696                "recent_projects",
5697                "remote_debug",
5698                "repl",
5699                "search",
5700                "settings_editor",
5701                "settings_profile_selector",
5702                "skill_creator",
5703                "snippets",
5704                "stash_picker",
5705                "svg",
5706                "syntax_tree_view",
5707                "tab_switcher",
5708                "task",
5709                "terminal",
5710                "terminal_panel",
5711                "text_finder",
5712                "theme",
5713                "theme_selector",
5714                "toast",
5715                "toolchain",
5716                "variable_list",
5717                "vim",
5718                "window",
5719                "workspace",
5720                "worktree_picker",
5721                "zed",
5722                "zed_actions",
5723                "omega_predict_onboarding",
5724                "zeta",
5725            ];
5726            assert_eq!(
5727                all_namespaces,
5728                expected_namespaces
5729                    .into_iter()
5730                    .map(|namespace| namespace.to_string())
5731                    .sorted()
5732                    .collect::<Vec<_>>()
5733            );
5734        });
5735    }
5736
5737    #[gpui::test]
5738    fn test_bundled_settings_and_themes(cx: &mut App) {
5739        cx.text_system()
5740            .add_fonts(vec![
5741                Assets
5742                    .load("fonts/lilex/Lilex-Regular.ttf")
5743                    .unwrap()
5744                    .unwrap(),
5745                Assets
5746                    .load("fonts/ibm-plex-sans/IBMPlexSans-Regular.ttf")
5747                    .unwrap()
5748                    .unwrap(),
5749            ])
5750            .unwrap();
5751        let themes = ThemeRegistry::default();
5752        settings::init(cx);
5753        theme_settings::init(theme::LoadThemes::JustBase, cx);
5754
5755        let mut has_default_theme = false;
5756        for theme_name in themes.list().into_iter().map(|meta| meta.name) {
5757            let theme = themes.get(&theme_name).unwrap();
5758            assert_eq!(theme.name, theme_name);
5759            if theme.name.as_ref() == "Aiur" {
5760                has_default_theme = true;
5761            }
5762        }
5763        assert!(has_default_theme);
5764    }
5765
5766    #[gpui::test]
5767    async fn test_bundled_files_editor(cx: &mut TestAppContext) {
5768        let app_state = init_test(cx);
5769        cx.update(init);
5770
5771        let project = Project::test(app_state.fs.clone(), [], cx).await;
5772        let _window = cx.add_window(|window, cx| MultiWorkspace::test_new(project, window, cx));
5773
5774        cx.update(|cx| {
5775            cx.dispatch_action(&OpenDefaultSettings);
5776        });
5777        cx.run_until_parked();
5778
5779        assert_eq!(cx.read(|cx| cx.windows().len()), 1);
5780
5781        let multi_workspace = cx.windows()[0].downcast::<MultiWorkspace>().unwrap();
5782        let active_editor = multi_workspace
5783            .update(cx, |multi_workspace, _, cx| {
5784                multi_workspace
5785                    .workspace()
5786                    .update(cx, |workspace, cx| workspace.active_item_as::<Editor>(cx))
5787            })
5788            .unwrap();
5789        assert!(
5790            active_editor.is_some(),
5791            "Settings action should have opened an editor with the default file contents"
5792        );
5793
5794        let active_editor = active_editor.unwrap();
5795        assert!(
5796            active_editor.read_with(cx, |editor, cx| editor.read_only(cx)),
5797            "Default settings should be readonly"
5798        );
5799        assert!(
5800            active_editor.read_with(cx, |editor, cx| editor.buffer().read(cx).read_only()),
5801            "The underlying buffer should also be readonly for the shipped default settings"
5802        );
5803    }
5804
5805    #[gpui::test]
5806    async fn test_bundled_files_reuse_existing_editor(cx: &mut TestAppContext) {
5807        let app_state = init_test(cx);
5808        cx.update(init);
5809
5810        let project = Project::test(app_state.fs.clone(), [], cx).await;
5811        let _window = cx.add_window(|window, cx| MultiWorkspace::test_new(project, window, cx));
5812
5813        cx.update(|cx| {
5814            cx.dispatch_action(&OpenDefaultSettings);
5815        });
5816        cx.run_until_parked();
5817
5818        let multi_workspace = cx.windows()[0].downcast::<MultiWorkspace>().unwrap();
5819        let first_item_id = multi_workspace
5820            .update(cx, |multi_workspace, _, cx| {
5821                multi_workspace.workspace().update(cx, |workspace, cx| {
5822                    workspace
5823                        .active_item(cx)
5824                        .expect("default settings should be open")
5825                        .item_id()
5826                })
5827            })
5828            .unwrap();
5829
5830        cx.update(|cx| {
5831            cx.dispatch_action(&OpenDefaultSettings);
5832        });
5833        cx.run_until_parked();
5834
5835        let (second_item_id, item_count) = multi_workspace
5836            .update(cx, |multi_workspace, _, cx| {
5837                multi_workspace.workspace().update(cx, |workspace, cx| {
5838                    let pane = workspace.active_pane().read(cx);
5839                    (
5840                        pane.active_item()
5841                            .expect("default settings should still be open")
5842                            .item_id(),
5843                        pane.items_len(),
5844                    )
5845                })
5846            })
5847            .unwrap();
5848
5849        assert_eq!(first_item_id, second_item_id);
5850        assert_eq!(item_count, 1);
5851    }
5852
5853    #[gpui::test]
5854    async fn test_bundled_languages(cx: &mut TestAppContext) {
5855        let fs = fs::FakeFs::new(cx.background_executor.clone());
5856        env_logger::builder().is_test(true).try_init().ok();
5857        let settings = cx.update(SettingsStore::test);
5858        cx.set_global(settings);
5859        let languages = LanguageRegistry::test(cx.executor());
5860        let languages = Arc::new(languages);
5861        let node_runtime = node_runtime::NodeRuntime::unavailable();
5862        cx.update(|cx| {
5863            languages::init(languages.clone(), fs, node_runtime, cx);
5864        });
5865        for name in languages.language_names() {
5866            languages
5867                .language_for_name(name.as_ref())
5868                .await
5869                .with_context(|| format!("language name {name}"))
5870                .unwrap();
5871        }
5872        cx.run_until_parked();
5873    }
5874
5875    pub(crate) fn init_test(cx: &mut TestAppContext) -> Arc<AppState> {
5876        init_test_with_state(cx, cx.update(AppState::test))
5877    }
5878
5879    fn init_test_with_state(
5880        cx: &mut TestAppContext,
5881        mut app_state: Arc<AppState>,
5882    ) -> Arc<AppState> {
5883        cx.update(move |cx| {
5884            env_logger::builder().is_test(true).try_init().ok();
5885
5886            let state = Arc::get_mut(&mut app_state).unwrap();
5887            state.build_window_options = build_window_options;
5888            app_state.languages.add(markdown_lang());
5889
5890            gpui_tokio::init(cx);
5891            AppState::set_global(app_state.clone(), cx);
5892            theme_settings::init(theme::LoadThemes::JustBase, cx);
5893            audio::init(cx);
5894            channel::init(&app_state.client, app_state.user_store.clone(), cx);
5895            call::init(app_state.client.clone(), app_state.user_store.clone(), cx);
5896            notifications::init(app_state.client.clone(), app_state.user_store.clone(), cx);
5897            workspace::init(app_state.clone(), cx);
5898            release_channel::init(Version::new(0, 0, 0), cx);
5899            command_palette::init(cx);
5900            editor::init(cx);
5901            git_ui::init(cx);
5902            project_panel::init(cx);
5903            outline_panel::init(cx);
5904            terminal_view::init(cx);
5905            copilot_chat::init(
5906                app_state.fs.clone(),
5907                app_state.client.http_client(),
5908                copilot_chat::CopilotChatConfiguration::default(),
5909                cx,
5910            );
5911            image_viewer::init(cx);
5912            language_model::init(cx);
5913            client::RefreshLlmTokenListener::register(
5914                app_state.client.clone(),
5915                app_state.user_store.clone(),
5916                cx,
5917            );
5918            language_models::init(app_state.user_store.clone(), app_state.client.clone(), cx);
5919            web_search::init(cx);
5920            web_search_providers::init(app_state.client.clone(), app_state.user_store.clone(), cx);
5921            let prompt_builder = PromptBuilder::load(app_state.fs.clone(), false, cx);
5922            project::AgentRegistryStore::init_global(
5923                cx,
5924                app_state.fs.clone(),
5925                app_state.client.http_client(),
5926            );
5927            agent_ui::init(
5928                app_state.fs.clone(),
5929                prompt_builder,
5930                app_state.languages.clone(),
5931                true,
5932                false,
5933                cx,
5934            );
5935
5936            repl::init(app_state.fs.clone(), cx);
5937            repl::notebook::init(cx);
5938            tasks_ui::init(cx);
5939            project::debugger::breakpoint_store::BreakpointStore::init(
5940                &app_state.client.clone().into(),
5941            );
5942            project::debugger::dap_store::DapStore::init(&app_state.client.clone().into(), cx);
5943            debugger_ui::init(cx);
5944            omega_effectd::init_openagents_session(cx);
5945            omega_effectd::init_openagents_binding(cx);
5946            omega_effectd::init_with_host_handler(
5947                Some(agent_ui::omega_effectd_host_handler(cx)),
5948                cx,
5949            );
5950            agent_computer_ui::init(cx);
5951            workroom_ui::init(cx);
5952            initialize_workspace(app_state.clone(), cx);
5953            search::init(cx);
5954            lsp_locations::init(cx);
5955            cx.set_global(workspace::PaneSearchBarCallbacks {
5956                setup_search_bar: |languages, toolbar, window, cx| {
5957                    let search_bar =
5958                        cx.new(|cx| search::BufferSearchBar::new(languages, window, cx));
5959                    toolbar.update(cx, |toolbar, cx| {
5960                        toolbar.add_item(search_bar, window, cx);
5961                    });
5962                },
5963                wrap_div_with_search_actions: search::buffer_search::register_pane_search_actions,
5964            });
5965            app_state
5966        })
5967    }
5968
5969    #[track_caller]
5970    fn assert_key_bindings_for(
5971        window: AnyWindowHandle,
5972        cx: &TestAppContext,
5973        actions: Vec<(&'static str, &dyn Action)>,
5974        line: u32,
5975    ) {
5976        let available_actions = cx
5977            .update(|cx| window.update(cx, |_, window, cx| window.available_actions(cx)))
5978            .unwrap();
5979        for (key, action) in actions {
5980            let bindings = cx
5981                .update(|cx| window.update(cx, |_, window, _| window.bindings_for_action(action)))
5982                .unwrap();
5983            // assert that...
5984            assert!(
5985                available_actions.iter().any(|bound_action| {
5986                    // actions match...
5987                    bound_action.partial_eq(action)
5988                }),
5989                "On {} Failed to find {}",
5990                line,
5991                action.name(),
5992            );
5993            assert!(
5994                // and key strokes contain the given key
5995                bindings
5996                    .into_iter()
5997                    .any(|binding| binding.keystrokes().iter().any(|k| k.key() == key)),
5998                "On {} Failed to find {} with key binding {}",
5999                line,
6000                action.name(),
6001                key
6002            );
6003        }
6004    }
6005
6006    #[gpui::test]
6007    async fn test_opening_project_settings_when_excluded(cx: &mut gpui::TestAppContext) {
6008        // Use the proper initialization for runtime state
6009        let app_state = init_keymap_test(cx);
6010
6011        eprintln!("Running test_opening_project_settings_when_excluded");
6012
6013        // 1. Set up a project with some project settings
6014        let settings_init =
6015            r#"{ "UNIQUEVALUE": true, "git": { "inline_blame": { "enabled": false } } }"#;
6016        app_state
6017            .fs
6018            .as_fake()
6019            .insert_tree(
6020                Path::new("/root"),
6021                json!({
6022                    ".zed": {
6023                        "settings.json": settings_init
6024                    }
6025                }),
6026            )
6027            .await;
6028
6029        eprintln!("Created project with .zed/settings.json containing UNIQUEVALUE");
6030
6031        // 2. Create a project with the file system and load it
6032        let project = Project::test(app_state.fs.clone(), [Path::new("/root")], cx).await;
6033
6034        // Save original settings content for comparison
6035        let original_settings = app_state
6036            .fs
6037            .load(Path::new("/root/.zed/settings.json"))
6038            .await
6039            .unwrap();
6040
6041        let original_settings_str = original_settings.clone();
6042
6043        // Verify settings exist on disk and have expected content
6044        eprintln!("Original settings content: {}", original_settings_str);
6045        assert!(
6046            original_settings_str.contains("UNIQUEVALUE"),
6047            "Test setup failed - settings file doesn't contain our marker"
6048        );
6049
6050        // 3. Add .zed to file scan exclusions in user settings
6051        cx.update_global::<SettingsStore, _>(|store, cx| {
6052            store.update_user_settings(cx, |worktree_settings| {
6053                worktree_settings.project.worktree.file_scan_exclusions =
6054                    Some(vec![".zed".to_string()]);
6055            });
6056        });
6057
6058        eprintln!("Added .zed to file_scan_exclusions in settings");
6059
6060        // 4. Run tasks to apply settings
6061        cx.background_executor.run_until_parked();
6062
6063        // 5. Critical: Verify .zed is actually excluded from worktree
6064        let worktree = cx.update(|cx| project.read(cx).worktrees(cx).next().unwrap());
6065
6066        let has_zed_entry =
6067            cx.update(|cx| worktree.read(cx).entry_for_path(rel_path(".zed")).is_some());
6068
6069        eprintln!(
6070            "Is .zed directory visible in worktree after exclusion: {}",
6071            has_zed_entry
6072        );
6073
6074        // This assertion verifies the test is set up correctly to show the bug
6075        // If .zed is not excluded, the test will fail here
6076        assert!(
6077            !has_zed_entry,
6078            "Test precondition failed: .zed directory should be excluded but was found in worktree"
6079        );
6080
6081        // 6. Create workspace and trigger the actual function that causes the bug
6082        let window =
6083            cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
6084        let workspace = window
6085            .read_with(cx, |mw, _| mw.workspace().clone())
6086            .unwrap();
6087        window
6088            .update(cx, |_, window, cx| {
6089                workspace.update(cx, |workspace, cx| {
6090                    // Call the exact function that contains the bug
6091                    eprintln!("About to call open_project_settings_file");
6092                    open_project_settings_file(workspace, &OpenProjectSettingsFile, window, cx);
6093                });
6094            })
6095            .unwrap();
6096
6097        // 7. Run background tasks until completion
6098        cx.background_executor.run_until_parked();
6099
6100        // 8. Verify file contents after calling function
6101        let new_content = app_state
6102            .fs
6103            .load(Path::new("/root/.zed/settings.json"))
6104            .await
6105            .unwrap();
6106
6107        let new_content_str = new_content;
6108        eprintln!("New settings content: {}", new_content_str);
6109
6110        // The bug causes the settings to be overwritten with empty settings
6111        // So if the unique value is no longer present, the bug has been reproduced
6112        let bug_exists = !new_content_str.contains("UNIQUEVALUE");
6113        eprintln!("Bug reproduced: {}", bug_exists);
6114
6115        // This assertion should fail if the bug exists - showing the bug is real
6116        assert!(
6117            new_content_str.contains("UNIQUEVALUE"),
6118            "BUG FOUND: Project settings were overwritten when opening via command - original custom content was lost"
6119        );
6120    }
6121
6122    #[gpui::test]
6123    async fn test_disable_ai_crash(cx: &mut gpui::TestAppContext) {
6124        let app_state = init_test(cx);
6125        cx.update(init);
6126        let project = Project::test(app_state.fs.clone(), [], cx).await;
6127        let _window = cx.add_window(|window, cx| MultiWorkspace::test_new(project, window, cx));
6128
6129        cx.run_until_parked();
6130
6131        cx.update(|cx| {
6132            SettingsStore::update_global(cx, |settings_store, cx| {
6133                settings_store.update_user_settings(cx, |settings| {
6134                    settings.project.disable_ai = Some(SaturatingBool(true));
6135                });
6136            });
6137        });
6138
6139        cx.run_until_parked();
6140
6141        // If this panics, the test has failed
6142    }
6143
6144    #[gpui::test]
6145    async fn test_disable_ai_filters_keybindings(cx: &mut gpui::TestAppContext) {
6146        let _app_state = init_keymap_test(cx);
6147
6148        // With AI enabled, the default keymap should include the assistant
6149        // bindings that intercept e.g. ctrl-enter in the editor.
6150        cx.update(load_default_keymap);
6151        cx.update(|cx| {
6152            let keymap = cx.key_bindings();
6153            let keymap = keymap.borrow();
6154            let has_ai_binding = keymap.bindings().any(|binding| is_ai_keybinding(binding));
6155            assert!(
6156                has_ai_binding,
6157                "expected AI-namespaced bindings in the default keymap before disabling AI"
6158            );
6159        });
6160
6161        cx.update(|cx| {
6162            SettingsStore::update_global(cx, |settings_store, cx| {
6163                settings_store.update_user_settings(cx, |settings| {
6164                    settings.project.disable_ai = Some(SaturatingBool(true));
6165                });
6166            });
6167        });
6168
6169        // The default keymap should drop every AI-namespaced binding so that
6170        // lower-precedence editor defaults can run instead.
6171        cx.update(|cx| {
6172            cx.clear_key_bindings();
6173            load_default_keymap(cx);
6174        });
6175        cx.update(|cx| {
6176            let keymap = cx.key_bindings();
6177            let keymap = keymap.borrow();
6178            if let Some(binding) = keymap.bindings().find(|b| is_ai_keybinding(b)) {
6179                panic!(
6180                    "expected no AI-namespaced bindings after disabling AI, but found `{}`",
6181                    binding.action().name()
6182                );
6183            }
6184        });
6185
6186        // User-defined bindings to AI actions should also be filtered.
6187        let user_binding = KeyBinding::new(
6188            "ctrl-enter",
6189            zed_actions::assistant::InlineAssist { prompt: None },
6190            None,
6191        );
6192        cx.update(|cx| reload_keymaps(cx, vec![user_binding]));
6193        cx.update(|cx| {
6194            let keymap = cx.key_bindings();
6195            let keymap = keymap.borrow();
6196            if let Some(binding) = keymap.bindings().find(|b| is_ai_keybinding(b)) {
6197                panic!(
6198                    "expected user binding `{}` to be filtered when AI is disabled",
6199                    binding.action().name()
6200                );
6201            }
6202        });
6203    }
6204
6205    #[gpui::test]
6206    async fn test_prefer_focused_window(cx: &mut gpui::TestAppContext) {
6207        let app_state = init_test(cx);
6208        let paths = [PathBuf::from(path!("/dir/document.txt"))];
6209
6210        app_state
6211            .fs
6212            .as_fake()
6213            .insert_tree(
6214                path!("/dir"),
6215                json!({
6216                    "document.txt": "Some of the documentation's content."
6217                }),
6218            )
6219            .await;
6220
6221        let project_a = Project::test(app_state.fs.clone(), [path!("/dir").as_ref()], cx).await;
6222        let window_a = cx.add_window({
6223            let project = project_a.clone();
6224            |window, cx| MultiWorkspace::test_new(project, window, cx)
6225        });
6226
6227        let project_b = Project::test(app_state.fs.clone(), [path!("/dir").as_ref()], cx).await;
6228        let window_b = cx.add_window({
6229            let project = project_b.clone();
6230            |window, cx| MultiWorkspace::test_new(project, window, cx)
6231        });
6232
6233        let project_c = Project::test(app_state.fs.clone(), [path!("/dir").as_ref()], cx).await;
6234        let window_c = cx.add_window({
6235            let project = project_c.clone();
6236            |window, cx| MultiWorkspace::test_new(project, window, cx)
6237        });
6238
6239        for window in [window_a, window_b, window_c] {
6240            let _ = cx.update_window(*window, |_, window, _| {
6241                window.activate_window();
6242            });
6243
6244            cx.update(|cx| {
6245                let open_options = OpenOptions {
6246                    wait: true,
6247                    ..Default::default()
6248                };
6249
6250                workspace::open_paths(&paths, app_state.clone(), open_options, cx)
6251            })
6252            .await
6253            .unwrap();
6254
6255            cx.update_window(*window, |_, window, _| assert!(window.is_window_active()))
6256                .unwrap();
6257
6258            let _ = window.read_with(cx, |multi_workspace, cx| {
6259                let pane = multi_workspace.workspace().read(cx).active_pane().read(cx);
6260                let project_path = pane.active_item().unwrap().project_path(cx).unwrap();
6261
6262                assert_eq!(
6263                    project_path.path.as_ref().as_std_path().to_str().unwrap(),
6264                    path!("document.txt")
6265                )
6266            });
6267        }
6268    }
6269
6270    #[gpui::test]
6271    async fn test_open_paths_switches_to_best_workspace(cx: &mut TestAppContext) {
6272        let app_state = init_test(cx);
6273
6274        app_state
6275            .fs
6276            .as_fake()
6277            .insert_tree(
6278                path!("/"),
6279                json!({
6280                    "dir1": {
6281                        "a.txt": "content a"
6282                    },
6283                    "dir2": {
6284                        "b.txt": "content b"
6285                    },
6286                    "dir3": {
6287                        "c.txt": "content c"
6288                    }
6289                }),
6290            )
6291            .await;
6292
6293        // Create a window with workspace 0 containing /dir1
6294        let project1 = Project::test(app_state.fs.clone(), [path!("/dir1").as_ref()], cx).await;
6295
6296        let window = cx.add_window({
6297            let project = project1.clone();
6298            |window, cx| MultiWorkspace::test_new(project, window, cx)
6299        });
6300        window
6301            .update(cx, |multi_workspace, _, cx| {
6302                multi_workspace.open_sidebar(cx);
6303            })
6304            .unwrap();
6305
6306        cx.run_until_parked();
6307        assert_eq!(cx.windows().len(), 1, "Should start with 1 window");
6308
6309        // Create workspace 2 with /dir2
6310        let project2 = Project::test(app_state.fs.clone(), [path!("/dir2").as_ref()], cx).await;
6311        let workspace2 = window
6312            .update(cx, |multi_workspace, window, cx| {
6313                multi_workspace.test_add_workspace(project2.clone(), window, cx)
6314            })
6315            .unwrap();
6316
6317        // Create workspace 3 with /dir3
6318        let project3 = Project::test(app_state.fs.clone(), [path!("/dir3").as_ref()], cx).await;
6319        let workspace3 = window
6320            .update(cx, |multi_workspace, window, cx| {
6321                multi_workspace.test_add_workspace(project3.clone(), window, cx)
6322            })
6323            .unwrap();
6324
6325        let workspace1 = window
6326            .read_with(cx, |multi_workspace, _| {
6327                multi_workspace.workspaces().next().unwrap().clone()
6328            })
6329            .unwrap();
6330
6331        window
6332            .update(cx, |multi_workspace, window, cx| {
6333                multi_workspace.activate(workspace2.clone(), None, window, cx);
6334                multi_workspace.activate(workspace3.clone(), None, window, cx);
6335                // Switch back to workspace1 for test setup
6336                multi_workspace.activate(workspace1.clone(), None, window, cx);
6337                assert_eq!(multi_workspace.workspace(), &workspace1);
6338            })
6339            .unwrap();
6340
6341        cx.run_until_parked();
6342
6343        // Verify setup: 3 workspaces, workspace 0 active, still 1 window
6344        window
6345            .read_with(cx, |multi_workspace, _| {
6346                assert_eq!(multi_workspace.workspaces().count(), 3);
6347                assert_eq!(multi_workspace.workspace(), &workspace1);
6348            })
6349            .unwrap();
6350        assert_eq!(cx.windows().len(), 1);
6351
6352        // Open a file in /dir3 - should switch to workspace 3 (not just "the other one")
6353        cx.update(|cx| {
6354            open_paths(
6355                &[PathBuf::from(path!("/dir3/c.txt"))],
6356                app_state.clone(),
6357                OpenOptions::default(),
6358                cx,
6359            )
6360        })
6361        .await
6362        .unwrap();
6363
6364        cx.run_until_parked();
6365
6366        // Verify workspace 2 is active and file opened there
6367        window
6368            .read_with(cx, |multi_workspace, cx| {
6369                assert_eq!(
6370                    multi_workspace.workspace(),
6371                    &workspace3,
6372                    "Should have switched to workspace 3 which contains /dir3"
6373                );
6374                let active_item = multi_workspace
6375                    .workspace()
6376                    .read(cx)
6377                    .active_pane()
6378                    .read(cx)
6379                    .active_item()
6380                    .expect("Should have an active item");
6381                assert_eq!(active_item.tab_content_text(0, cx), "c.txt");
6382            })
6383            .unwrap();
6384        assert_eq!(cx.windows().len(), 1, "Should reuse existing window");
6385
6386        // Open a file in /dir2 - should switch to workspace 2
6387        cx.update(|cx| {
6388            open_paths(
6389                &[PathBuf::from(path!("/dir2/b.txt"))],
6390                app_state.clone(),
6391                OpenOptions::default(),
6392                cx,
6393            )
6394        })
6395        .await
6396        .unwrap();
6397
6398        cx.run_until_parked();
6399
6400        // Verify workspace 1 is active and file opened there
6401        window
6402            .read_with(cx, |multi_workspace, cx| {
6403                assert_eq!(
6404                    multi_workspace.workspace(),
6405                    &workspace2,
6406                    "Should have switched to workspace 2 which contains /dir2"
6407                );
6408                let active_item = multi_workspace
6409                    .workspace()
6410                    .read(cx)
6411                    .active_pane()
6412                    .read(cx)
6413                    .active_item()
6414                    .expect("Should have an active item");
6415                assert_eq!(active_item.tab_content_text(0, cx), "b.txt");
6416            })
6417            .unwrap();
6418
6419        // Verify c.txt is still in workspace 3 (file opened in correct workspace, not active one)
6420        workspace3.read_with(cx, |workspace, cx| {
6421            let active_item = workspace
6422                .active_pane()
6423                .read(cx)
6424                .active_item()
6425                .expect("Workspace 2 should have an active item");
6426            assert_eq!(
6427                active_item.tab_content_text(0, cx),
6428                "c.txt",
6429                "c.txt should have been opened in workspace 3, not the active workspace"
6430            );
6431        });
6432
6433        assert_eq!(cx.windows().len(), 1, "Should still have only 1 window");
6434
6435        // Open a file in /dir1 - should switch back to workspace 0
6436        cx.update(|cx| {
6437            open_paths(
6438                &[PathBuf::from(path!("/dir1/a.txt"))],
6439                app_state.clone(),
6440                OpenOptions::default(),
6441                cx,
6442            )
6443        })
6444        .await
6445        .unwrap();
6446
6447        cx.run_until_parked();
6448
6449        // Verify workspace 0 is active and file opened there
6450        window
6451            .read_with(cx, |multi_workspace, cx| {
6452                assert_eq!(
6453                    multi_workspace.workspace(),
6454                    &workspace1,
6455                    "Should have switched back to workspace 0 which contains /dir1"
6456                );
6457                let active_item = multi_workspace
6458                    .workspace()
6459                    .read(cx)
6460                    .active_pane()
6461                    .read(cx)
6462                    .active_item()
6463                    .expect("Should have an active item");
6464                assert_eq!(active_item.tab_content_text(0, cx), "a.txt");
6465            })
6466            .unwrap();
6467        assert_eq!(cx.windows().len(), 1, "Should still have only 1 window");
6468    }
6469
6470    #[gpui::test]
6471    async fn test_open_paths_in_gitignored_dir_opens_new_workspace(cx: &mut TestAppContext) {
6472        let app_state = init_test(cx);
6473
6474        app_state
6475            .fs
6476            .as_fake()
6477            .insert_tree(
6478                path!("/project"),
6479                json!({
6480                    ".git": {},
6481                    ".gitignore": ".checkouts/\n",
6482                    "src": {
6483                        "main.rs": "fn main() {}"
6484                    },
6485                    ".checkouts": {
6486                        "worktrees": {
6487                            "foo": {
6488                                "README.md": "hello"
6489                            }
6490                        }
6491                    }
6492                }),
6493            )
6494            .await;
6495
6496        cx.update(|cx| {
6497            open_paths(
6498                &[PathBuf::from(path!("/project"))],
6499                app_state.clone(),
6500                workspace::OpenOptions::default(),
6501                cx,
6502            )
6503        })
6504        .await
6505        .unwrap();
6506        cx.run_until_parked();
6507        assert_eq!(cx.update(|cx| cx.windows().len()), 1);
6508
6509        // Opening a directory inside a gitignored folder must not be treated
6510        // as contained by the open project: its contents were never scanned,
6511        // and it may be an independent checkout (e.g. a git worktree kept in
6512        // an ignored directory). It should become its own workspace root
6513        // instead.
6514        cx.update(|cx| {
6515            open_paths(
6516                &[PathBuf::from(path!("/project/.checkouts/worktrees/foo"))],
6517                app_state.clone(),
6518                workspace::OpenOptions::default(),
6519                cx,
6520            )
6521        })
6522        .await
6523        .unwrap();
6524        cx.run_until_parked();
6525
6526        let workspace_roots = cx.update(|cx| {
6527            cx.windows()
6528                .into_iter()
6529                .filter_map(|window| window.downcast::<MultiWorkspace>())
6530                .flat_map(|window| {
6531                    let mut roots = Vec::new();
6532                    if let Ok(multi_workspace) = window.read(cx) {
6533                        for workspace in multi_workspace.workspaces() {
6534                            roots.push(
6535                                workspace
6536                                    .read(cx)
6537                                    .worktrees(cx)
6538                                    .map(|worktree| worktree.read(cx).abs_path().to_path_buf())
6539                                    .collect::<Vec<_>>(),
6540                            );
6541                        }
6542                    }
6543                    roots
6544                })
6545                .collect::<Vec<_>>()
6546        });
6547        assert!(
6548            workspace_roots.contains(&vec![PathBuf::from(path!(
6549                "/project/.checkouts/worktrees/foo"
6550            ))]),
6551            "the gitignored directory should be the root of its own workspace, got {workspace_roots:?}"
6552        );
6553        assert!(
6554            workspace_roots.contains(&vec![PathBuf::from(path!("/project"))]),
6555            "the original project workspace should be unchanged, got {workspace_roots:?}"
6556        );
6557    }
6558
6559    #[gpui::test]
6560    async fn test_quit_checks_all_workspaces_for_dirty_items(cx: &mut TestAppContext) {
6561        let app_state = init_test(cx);
6562        cx.update(init);
6563
6564        app_state
6565            .fs
6566            .as_fake()
6567            .insert_tree(
6568                path!("/"),
6569                json!({
6570                    "dir1": {
6571                        "a.txt": "content a"
6572                    },
6573                    "dir2": {
6574                        "b.txt": "content b"
6575                    },
6576                    "dir3": {
6577                        "c.txt": "content c"
6578                    }
6579                }),
6580            )
6581            .await;
6582
6583        // === Setup Window 1 with two workspaces ===
6584        let project1 = Project::test(app_state.fs.clone(), [path!("/dir1").as_ref()], cx).await;
6585        let window1 = cx.add_window({
6586            let project = project1.clone();
6587            |window, cx| MultiWorkspace::test_new(project, window, cx)
6588        });
6589        window1
6590            .update(cx, |multi_workspace, _, cx| {
6591                multi_workspace.open_sidebar(cx);
6592            })
6593            .unwrap();
6594
6595        cx.run_until_parked();
6596
6597        let project2 = Project::test(app_state.fs.clone(), [path!("/dir2").as_ref()], cx).await;
6598        let workspace1_1 = window1
6599            .read_with(cx, |multi_workspace, _| multi_workspace.workspace().clone())
6600            .unwrap();
6601        let workspace1_2 = window1
6602            .update(cx, |multi_workspace, window, cx| {
6603                multi_workspace.test_add_workspace(project2.clone(), window, cx)
6604            })
6605            .unwrap();
6606
6607        window1
6608            .update(cx, |multi_workspace, window, cx| {
6609                multi_workspace.activate(workspace1_2.clone(), None, window, cx);
6610                multi_workspace.activate(workspace1_1.clone(), None, window, cx);
6611            })
6612            .unwrap();
6613
6614        // === Setup Window 2 with one workspace ===
6615        let project3 = Project::test(app_state.fs.clone(), [path!("/dir3").as_ref()], cx).await;
6616        let window2 = cx.add_window({
6617            let project = project3.clone();
6618            |window, cx| MultiWorkspace::test_new(project, window, cx)
6619        });
6620        window2
6621            .update(cx, |multi_workspace, _, cx| {
6622                multi_workspace.open_sidebar(cx);
6623            })
6624            .unwrap();
6625
6626        cx.run_until_parked();
6627        assert_eq!(cx.windows().len(), 2);
6628
6629        // === Case 1: Active workspace has dirty item, quit can be cancelled ===
6630        let worktree1_id = project1.update(cx, |project, cx| {
6631            project.worktrees(cx).next().unwrap().read(cx).id()
6632        });
6633
6634        let editor1 = window1
6635            .update(cx, |_, window, cx| {
6636                workspace1_1.update(cx, |workspace, cx| {
6637                    workspace.open_path((worktree1_id, rel_path("a.txt")), None, true, window, cx)
6638                })
6639            })
6640            .unwrap()
6641            .await
6642            .unwrap()
6643            .downcast::<Editor>()
6644            .unwrap();
6645
6646        window1
6647            .update(cx, |_, window, cx| {
6648                editor1.update(cx, |editor, cx| {
6649                    editor.insert("dirty in active workspace", window, cx);
6650                });
6651            })
6652            .unwrap();
6653
6654        cx.run_until_parked();
6655
6656        // Verify workspace1_1 is active
6657        window1
6658            .read_with(cx, |multi_workspace, _| {
6659                assert_eq!(multi_workspace.workspace(), &workspace1_1);
6660            })
6661            .unwrap();
6662
6663        cx.dispatch_action(*window1, Quit);
6664        cx.run_until_parked();
6665
6666        assert!(
6667            cx.has_pending_prompt(),
6668            "Case 1: Should prompt to save dirty item in active workspace"
6669        );
6670
6671        cx.simulate_prompt_answer("Cancel");
6672        cx.run_until_parked();
6673
6674        assert_eq!(
6675            cx.windows().len(),
6676            2,
6677            "Case 1: Windows should still exist after cancelling quit"
6678        );
6679
6680        // Clean up Case 1: Close the dirty item without saving
6681        let close_task = window1
6682            .update(cx, |_, window, cx| {
6683                workspace1_1.update(cx, |workspace, cx| {
6684                    workspace.active_pane().update(cx, |pane, cx| {
6685                        pane.close_active_item(&Default::default(), window, cx)
6686                    })
6687                })
6688            })
6689            .unwrap();
6690        cx.run_until_parked();
6691        cx.simulate_prompt_answer("Don't Save");
6692        close_task.await.ok();
6693        cx.run_until_parked();
6694
6695        // === Case 2: Non-active workspace (same window) has dirty item ===
6696        let worktree2_id = project2.update(cx, |project, cx| {
6697            project.worktrees(cx).next().unwrap().read(cx).id()
6698        });
6699
6700        let editor2 = window1
6701            .update(cx, |_, window, cx| {
6702                workspace1_2.update(cx, |workspace, cx| {
6703                    workspace.open_path((worktree2_id, rel_path("b.txt")), None, true, window, cx)
6704                })
6705            })
6706            .unwrap()
6707            .await
6708            .unwrap()
6709            .downcast::<Editor>()
6710            .unwrap();
6711
6712        window1
6713            .update(cx, |_, window, cx| {
6714                editor2.update(cx, |editor, cx| {
6715                    editor.insert("dirty in non-active workspace", window, cx);
6716                });
6717            })
6718            .unwrap();
6719
6720        cx.run_until_parked();
6721
6722        // Verify workspace1_1 is still active (not workspace1_2 with dirty item)
6723        window1
6724            .read_with(cx, |multi_workspace, _| {
6725                assert_eq!(multi_workspace.workspace(), &workspace1_1);
6726            })
6727            .unwrap();
6728
6729        cx.dispatch_action(*window1, Quit);
6730        cx.run_until_parked();
6731
6732        // Verify the non-active workspace got activated to show the dirty item
6733        window1
6734            .read_with(cx, |multi_workspace, _| {
6735                assert_eq!(
6736                    multi_workspace.workspace(),
6737                    &workspace1_2,
6738                    "Case 2: Non-active workspace should be activated when it has dirty item"
6739                );
6740            })
6741            .unwrap();
6742
6743        assert!(
6744            cx.has_pending_prompt(),
6745            "Case 2: Should prompt to save dirty item in non-active workspace"
6746        );
6747
6748        cx.simulate_prompt_answer("Cancel");
6749        cx.run_until_parked();
6750
6751        assert_eq!(
6752            cx.windows().len(),
6753            2,
6754            "Case 2: Windows should still exist after cancelling quit"
6755        );
6756
6757        // Clean up Case 2: Close the dirty item without saving
6758        let close_task = window1
6759            .update(cx, |_, window, cx| {
6760                workspace1_2.update(cx, |workspace, cx| {
6761                    workspace.active_pane().update(cx, |pane, cx| {
6762                        pane.close_active_item(&Default::default(), window, cx)
6763                    })
6764                })
6765            })
6766            .unwrap();
6767        cx.run_until_parked();
6768        cx.simulate_prompt_answer("Don't Save");
6769        close_task.await.ok();
6770        cx.run_until_parked();
6771
6772        // === Case 3: Non-active window has dirty item ===
6773        let workspace3 = window2
6774            .read_with(cx, |multi_workspace, _| multi_workspace.workspace().clone())
6775            .unwrap();
6776
6777        let worktree3_id = project3.update(cx, |project, cx| {
6778            project.worktrees(cx).next().unwrap().read(cx).id()
6779        });
6780
6781        let editor3 = window2
6782            .update(cx, |_, window, cx| {
6783                workspace3.update(cx, |workspace, cx| {
6784                    workspace.open_path((worktree3_id, rel_path("c.txt")), None, true, window, cx)
6785                })
6786            })
6787            .unwrap()
6788            .await
6789            .unwrap()
6790            .downcast::<Editor>()
6791            .unwrap();
6792
6793        window2
6794            .update(cx, |_, window, cx| {
6795                editor3.update(cx, |editor, cx| {
6796                    editor.insert("dirty in other window", window, cx);
6797                });
6798            })
6799            .unwrap();
6800
6801        cx.run_until_parked();
6802
6803        // Activate window1 explicitly (editing in window2 may have activated it)
6804        window1
6805            .update(cx, |_, window, _| window.activate_window())
6806            .unwrap();
6807        cx.run_until_parked();
6808
6809        // Verify window2 is not active (window1 should still be active)
6810        assert_eq!(
6811            cx.update(|cx| window2.is_active(cx)),
6812            Some(false),
6813            "Case 3: window2 should not be active before quit"
6814        );
6815
6816        // Dispatch quit from window1 (window2 has the dirty item)
6817        cx.dispatch_action(*window1, Quit);
6818        cx.run_until_parked();
6819
6820        // Verify window2 is now active (quit handler activated it to show dirty item)
6821        assert_eq!(
6822            cx.update(|cx| window2.is_active(cx)),
6823            Some(true),
6824            "Case 3: window2 should be activated when it has dirty item"
6825        );
6826
6827        assert!(
6828            cx.has_pending_prompt(),
6829            "Case 3: Should prompt to save dirty item in non-active window"
6830        );
6831
6832        cx.simulate_prompt_answer("Cancel");
6833        cx.run_until_parked();
6834
6835        assert_eq!(
6836            cx.windows().len(),
6837            2,
6838            "Case 3: Windows should still exist after cancelling quit"
6839        );
6840    }
6841
6842    #[gpui::test]
6843    async fn test_multi_workspace_session_restore(cx: &mut TestAppContext) {
6844        use collections::HashMap;
6845        use session::Session;
6846        use util::path_list::PathList;
6847        use workspace::{OpenMode, ProjectGroupKey, Workspace, WorkspaceId};
6848
6849        let app_state = init_test(cx);
6850
6851        let dir1 = path!("/dir1");
6852        let dir2 = path!("/dir2");
6853        let dir3 = path!("/dir3");
6854
6855        let fs = app_state.fs.clone();
6856        let fake_fs = fs.as_fake();
6857        fake_fs.insert_tree(dir1, json!({})).await;
6858        fake_fs.insert_tree(dir2, json!({})).await;
6859        fake_fs.insert_tree(dir3, json!({})).await;
6860
6861        let session_id = cx.read(|cx| app_state.session.read(cx).id().to_owned());
6862
6863        // --- Create 3 workspaces in 2 windows ---
6864        //
6865        //   Window A: workspace for dir1, workspace for dir2
6866        //   Window B: workspace for dir3
6867        let workspace::OpenResult {
6868            window: window_a, ..
6869        } = cx
6870            .update(|cx| {
6871                Workspace::new_local(
6872                    vec![dir1.into()],
6873                    app_state.clone(),
6874                    None,
6875                    None,
6876                    None,
6877                    OpenMode::Activate,
6878                    cx,
6879                )
6880            })
6881            .await
6882            .expect("failed to open first workspace");
6883
6884        window_a
6885            .update(cx, |multi_workspace, _, cx| {
6886                multi_workspace.open_sidebar(cx);
6887            })
6888            .unwrap();
6889
6890        window_a
6891            .update(cx, |multi_workspace, window, cx| {
6892                multi_workspace.open_project(vec![dir2.into()], OpenMode::Activate, window, cx)
6893            })
6894            .unwrap()
6895            .await
6896            .expect("failed to open second workspace into window A");
6897        cx.run_until_parked();
6898
6899        let workspace::OpenResult {
6900            window: window_b, ..
6901        } = cx
6902            .update(|cx| {
6903                Workspace::new_local(
6904                    vec![dir3.into()],
6905                    app_state.clone(),
6906                    None,
6907                    None,
6908                    None,
6909                    OpenMode::Activate,
6910                    cx,
6911                )
6912            })
6913            .await
6914            .expect("failed to open third workspace");
6915
6916        window_b
6917            .update(cx, |multi_workspace, _, cx| {
6918                multi_workspace.open_sidebar(cx);
6919            })
6920            .unwrap();
6921
6922        // Currently dir2 is active because it was added last.
6923        // So, switch window_a's active workspace to dir1 (index 0).
6924        // This sets up a non-trivial assertion: after restore, dir1 should
6925        // still be active rather than whichever workspace happened to restore last.
6926        window_a
6927            .update(cx, |multi_workspace, window, cx| {
6928                let workspace = multi_workspace.workspaces().next().unwrap().clone();
6929                multi_workspace.activate(workspace, None, window, cx);
6930            })
6931            .unwrap();
6932
6933        cx.run_until_parked();
6934        flush_workspace_serialization(&window_a, cx).await;
6935        flush_workspace_serialization(&window_b, cx).await;
6936        cx.run_until_parked();
6937
6938        // Verify all workspaces retained their session_ids.
6939        let db = cx.update(|cx| workspace::WorkspaceDb::global(cx));
6940        let locations =
6941            workspace::last_session_workspace_locations(&db, &session_id, None, fs.as_ref())
6942                .await
6943                .expect("expected session workspace locations");
6944        assert_eq!(
6945            locations.len(),
6946            3,
6947            "all 3 workspaces should have session_ids in the DB"
6948        );
6949
6950        // Close the original windows.
6951        window_a
6952            .update(cx, |_, window, _| window.remove_window())
6953            .unwrap();
6954        window_b
6955            .update(cx, |_, window, _| window.remove_window())
6956            .unwrap();
6957        cx.run_until_parked();
6958
6959        // Simulate a new session launch: replace the session so that
6960        // `last_session_id()` returns the ID used during workspace creation.
6961        // `restore_on_startup` defaults to `LastSession`, which is what we need.
6962        cx.update(|cx| {
6963            app_state.session.update(cx, |app_session, _cx| {
6964                app_session
6965                    .replace_session_for_test(Session::test_with_old_session(session_id.clone()));
6966            });
6967        });
6968
6969        // --- Read back from DB and verify grouping ---
6970        let locations =
6971            workspace::last_session_workspace_locations(&db, &session_id, None, fs.as_ref())
6972                .await
6973                .expect("expected session workspace locations");
6974
6975        assert_eq!(locations.len(), 3, "expected 3 session workspaces");
6976
6977        let mut groups_by_window: HashMap<gpui::WindowId, Vec<WorkspaceId>> = HashMap::default();
6978        for session_workspace in &locations {
6979            if let Some(window_id) = session_workspace.window_id {
6980                groups_by_window
6981                    .entry(window_id)
6982                    .or_default()
6983                    .push(session_workspace.workspace_id);
6984            }
6985        }
6986        assert_eq!(
6987            groups_by_window.len(),
6988            2,
6989            "expected 2 window groups, got {groups_by_window:?}"
6990        );
6991        assert!(
6992            groups_by_window.values().any(|g| g.len() == 2),
6993            "expected one group with 2 workspaces"
6994        );
6995        assert!(
6996            groups_by_window.values().any(|g| g.len() == 1),
6997            "expected one group with 1 workspace"
6998        );
6999
7000        let mut async_cx = cx.to_async();
7001        crate::restore_or_create_workspace(app_state.clone(), &mut async_cx)
7002            .await
7003            .expect("failed to restore workspaces");
7004        cx.run_until_parked();
7005
7006        // --- Verify the restored windows ---
7007        let restored_windows: Vec<WindowHandle<MultiWorkspace>> = cx.read(|cx| {
7008            cx.windows()
7009                .into_iter()
7010                .filter_map(|window| window.downcast::<MultiWorkspace>())
7011                .collect()
7012        });
7013        assert_eq!(restored_windows.len(), 2,);
7014
7015        // Identify restored windows by their active workspace root paths.
7016        let (restored_a, restored_b) = {
7017            let (mut with_dir1, mut with_dir3) = (None, None);
7018            for window in &restored_windows {
7019                let active_paths = window
7020                    .read_with(cx, |mw, cx| mw.workspace().read(cx).root_paths(cx))
7021                    .unwrap();
7022                if active_paths.iter().any(|p| p.as_ref() == Path::new(dir1)) {
7023                    with_dir1 = Some(window);
7024                } else {
7025                    with_dir3 = Some(window);
7026                }
7027            }
7028            (
7029                with_dir1.expect("expected a window with dir1 active"),
7030                with_dir3.expect("expected a window with dir3 active"),
7031            )
7032        };
7033
7034        // Window A (dir1+dir2): 1 workspace restored, but 2 project group keys.
7035        restored_a
7036            .read_with(cx, |mw, _| {
7037                assert_eq!(
7038                    mw.project_group_keys(),
7039                    vec![
7040                        ProjectGroupKey::new(None, PathList::new(&[dir2])),
7041                        ProjectGroupKey::new(None, PathList::new(&[dir1])),
7042                    ]
7043                );
7044                assert_eq!(mw.workspaces().count(), 1);
7045            })
7046            .unwrap();
7047
7048        // Window B (dir3): 1 workspace, 1 project group key.
7049        restored_b
7050            .read_with(cx, |mw, _| {
7051                assert_eq!(
7052                    mw.project_group_keys(),
7053                    vec![ProjectGroupKey::new(None, PathList::new(&[dir3]))]
7054                );
7055                assert_eq!(mw.workspaces().count(), 1);
7056            })
7057            .unwrap();
7058    }
7059
7060    #[gpui::test]
7061    async fn test_quit_preserves_focused_workspace_for_restore(cx: &mut TestAppContext) {
7062        use session::Session;
7063        use workspace::{OpenMode, Workspace};
7064
7065        let app_state = init_test(cx);
7066        cx.update(init);
7067
7068        let dir1 = path!("/dir1");
7069        let dir2 = path!("/dir2");
7070
7071        let fs = app_state.fs.clone();
7072        let fake_fs = fs.as_fake();
7073        fake_fs.insert_tree(dir1, json!({})).await;
7074        fake_fs.insert_tree(dir2, json!({})).await;
7075
7076        let session_id = cx.read(|cx| app_state.session.read(cx).id().to_owned());
7077
7078        // Window with two retained workspaces: dir1 added first, dir2 second.
7079        let workspace::OpenResult { window, .. } = cx
7080            .update(|cx| {
7081                Workspace::new_local(
7082                    vec![dir1.into()],
7083                    app_state.clone(),
7084                    None,
7085                    None,
7086                    None,
7087                    OpenMode::Activate,
7088                    cx,
7089                )
7090            })
7091            .await
7092            .expect("failed to open first workspace");
7093
7094        window
7095            .update(cx, |multi_workspace, _, cx| {
7096                multi_workspace.open_sidebar(cx);
7097            })
7098            .unwrap();
7099
7100        window
7101            .update(cx, |multi_workspace, window, cx| {
7102                multi_workspace.open_project(vec![dir2.into()], OpenMode::Activate, window, cx)
7103            })
7104            .unwrap()
7105            .await
7106            .expect("failed to open second workspace");
7107        cx.run_until_parked();
7108
7109        // Focus dir1 (the first workspace). dir2 was activated last when it was
7110        // opened and is iterated last by the quit-time close-prompt loop, so
7111        // without the fix the persisted active workspace gets clobbered to dir2.
7112        window
7113            .update(cx, |multi_workspace, window, cx| {
7114                let workspace = multi_workspace.workspaces().next().unwrap().clone();
7115                multi_workspace.activate(workspace, None, window, cx);
7116            })
7117            .unwrap();
7118        cx.run_until_parked();
7119
7120        window
7121            .read_with(cx, |mw, cx| {
7122                assert!(
7123                    mw.workspace()
7124                        .read(cx)
7125                        .root_paths(cx)
7126                        .iter()
7127                        .any(|p| p.as_ref() == Path::new(dir1)),
7128                    "dir1 should be the focused workspace before quitting"
7129                );
7130            })
7131            .unwrap();
7132
7133        // Quit. With no dirty items there are no save prompts, so the quit flow
7134        // runs the prepare_to_close loop (which activates every workspace in
7135        // turn to surface prompts) and then flushes serialization. cx.quit() is
7136        // a no-op in tests, so the window stays around for inspection.
7137        cx.dispatch_action(*window, Quit);
7138        cx.run_until_parked();
7139
7140        // The fix re-activates the originally-focused workspace after the loop,
7141        // so the window must still be focused on dir1, not dir2.
7142        window
7143            .read_with(cx, |mw, cx| {
7144                let active = mw.workspace().read(cx).root_paths(cx);
7145                assert!(
7146                    active.iter().any(|p| p.as_ref() == Path::new(dir1)),
7147                    "quitting must not change which workspace is focused"
7148                );
7149                assert!(
7150                    !active.iter().any(|p| p.as_ref() == Path::new(dir2)),
7151                    "dir2 must not become the focused workspace after quitting"
7152                );
7153            })
7154            .unwrap();
7155
7156        // Simulate a fresh launch and verify dir1 is restored as the active
7157        // workspace rather than dir2 (or an empty window).
7158        window
7159            .update(cx, |_, window, _| window.remove_window())
7160            .unwrap();
7161        cx.run_until_parked();
7162
7163        cx.update(|cx| {
7164            app_state.session.update(cx, |app_session, _cx| {
7165                app_session
7166                    .replace_session_for_test(Session::test_with_old_session(session_id.clone()));
7167            });
7168        });
7169
7170        let mut async_cx = cx.to_async();
7171        crate::restore_or_create_workspace(app_state.clone(), &mut async_cx)
7172            .await
7173            .expect("failed to restore workspaces");
7174        cx.run_until_parked();
7175
7176        let restored_windows: Vec<WindowHandle<MultiWorkspace>> = cx.read(|cx| {
7177            cx.windows()
7178                .into_iter()
7179                .filter_map(|window| window.downcast::<MultiWorkspace>())
7180                .collect()
7181        });
7182        assert_eq!(restored_windows.len(), 1);
7183
7184        restored_windows[0]
7185            .read_with(cx, |mw, cx| {
7186                let active = mw.workspace().read(cx).root_paths(cx);
7187                assert!(
7188                    active.iter().any(|p| p.as_ref() == Path::new(dir1)),
7189                    "the focused workspace (dir1) must be restored as active"
7190                );
7191                assert!(
7192                    !active.iter().any(|p| p.as_ref() == Path::new(dir2)),
7193                    "dir2 must not be restored as the active workspace"
7194                );
7195            })
7196            .unwrap();
7197    }
7198
7199    #[gpui::test]
7200    async fn test_restored_project_groups_survive_workspace_key_change(cx: &mut TestAppContext) {
7201        use session::Session;
7202        use util::path_list::PathList;
7203        use workspace::{OpenMode, ProjectGroupKey};
7204
7205        let app_state = init_test(cx);
7206
7207        let fs = app_state.fs.clone();
7208        let fake_fs = fs.as_fake();
7209        fake_fs
7210            .insert_tree(path!("/root_a"), json!({ "file.txt": "" }))
7211            .await;
7212        fake_fs
7213            .insert_tree(path!("/root_b"), json!({ "file.txt": "" }))
7214            .await;
7215        fake_fs
7216            .insert_tree(path!("/root_c"), json!({ "file.txt": "" }))
7217            .await;
7218        fake_fs
7219            .insert_tree(path!("/root_d"), json!({ "other.txt": "" }))
7220            .await;
7221
7222        let session_id = cx.read(|cx| app_state.session.read(cx).id().to_owned());
7223
7224        // --- Phase 1: Build a multi-workspace with 3 project groups ---
7225
7226        let workspace::OpenResult { window, .. } = cx
7227            .update(|cx| {
7228                workspace::Workspace::new_local(
7229                    vec![path!("/root_a").into()],
7230                    app_state.clone(),
7231                    None,
7232                    None,
7233                    None,
7234                    OpenMode::Activate,
7235                    cx,
7236                )
7237            })
7238            .await
7239            .expect("failed to open workspace");
7240
7241        window.update(cx, |mw, _, cx| mw.open_sidebar(cx)).unwrap();
7242
7243        window
7244            .update(cx, |mw, window, cx| {
7245                mw.open_project(vec![path!("/root_b").into()], OpenMode::Add, window, cx)
7246            })
7247            .unwrap()
7248            .await
7249            .expect("failed to add root_b");
7250
7251        window
7252            .update(cx, |mw, window, cx| {
7253                mw.open_project(vec![path!("/root_c").into()], OpenMode::Add, window, cx)
7254            })
7255            .unwrap()
7256            .await
7257            .expect("failed to add root_c");
7258        cx.run_until_parked();
7259
7260        let key_b = ProjectGroupKey::new(None, PathList::new(&[path!("/root_b")]));
7261        let key_c = ProjectGroupKey::new(None, PathList::new(&[path!("/root_c")]));
7262
7263        // Make root_a the active workspace so it's the one eagerly restored.
7264        window
7265            .update(cx, |mw, window, cx| {
7266                let workspace_a = mw
7267                    .workspaces()
7268                    .find(|ws| {
7269                        ws.read(cx)
7270                            .root_paths(cx)
7271                            .iter()
7272                            .any(|p| p.as_ref() == Path::new(path!("/root_a")))
7273                    })
7274                    .expect("workspace_a should exist")
7275                    .clone();
7276                mw.activate(workspace_a, None, window, cx);
7277            })
7278            .unwrap();
7279        cx.run_until_parked();
7280
7281        // --- Phase 2: Serialize, close, and restore ---
7282
7283        flush_workspace_serialization(&window, cx).await;
7284        cx.run_until_parked();
7285
7286        window
7287            .update(cx, |_, window, _| window.remove_window())
7288            .unwrap();
7289        cx.run_until_parked();
7290
7291        cx.update(|cx| {
7292            app_state.session.update(cx, |app_session, _cx| {
7293                app_session
7294                    .replace_session_for_test(Session::test_with_old_session(session_id.clone()));
7295            });
7296        });
7297
7298        let mut async_cx = cx.to_async();
7299        crate::restore_or_create_workspace(app_state.clone(), &mut async_cx)
7300            .await
7301            .expect("failed to restore workspace");
7302        cx.run_until_parked();
7303
7304        let restored_windows: Vec<WindowHandle<MultiWorkspace>> = cx.read(|cx| {
7305            cx.windows()
7306                .into_iter()
7307                .filter_map(|w| w.downcast::<MultiWorkspace>())
7308                .collect()
7309        });
7310        assert_eq!(restored_windows.len(), 1);
7311        let restored = &restored_windows[0];
7312
7313        // Verify the restored window has all 3 project groups.
7314        restored
7315            .read_with(cx, |mw, _cx| {
7316                let keys = mw.project_group_keys();
7317                assert_eq!(
7318                    keys.len(),
7319                    3,
7320                    "restored window should have 3 groups; got {keys:?}"
7321                );
7322                assert!(keys.contains(&key_b), "should contain key_b");
7323                assert!(keys.contains(&key_c), "should contain key_c");
7324            })
7325            .unwrap();
7326
7327        // --- Phase 3: Trigger a workspace key change and verify survival ---
7328
7329        let active_project = restored
7330            .read_with(cx, |mw, cx| mw.workspace().read(cx).project().clone())
7331            .unwrap();
7332
7333        active_project
7334            .update(cx, |project, cx| {
7335                project.find_or_create_worktree(path!("/root_d"), true, cx)
7336            })
7337            .await
7338            .expect("adding worktree should succeed");
7339        cx.run_until_parked();
7340
7341        restored
7342            .read_with(cx, |mw, _cx| {
7343                let keys = mw.project_group_keys();
7344                assert!(
7345                    keys.contains(&key_b),
7346                    "restored group key_b should survive a workspace key change; got {keys:?}"
7347                );
7348                assert!(
7349                    keys.contains(&key_c),
7350                    "restored group key_c should survive a workspace key change; got {keys:?}"
7351                );
7352            })
7353            .unwrap();
7354    }
7355
7356    #[gpui::test]
7357    async fn test_close_project_removes_project_group(cx: &mut TestAppContext) {
7358        use util::path_list::PathList;
7359        use workspace::{OpenMode, ProjectGroupKey};
7360
7361        let app_state = init_test(cx);
7362        app_state
7363            .fs
7364            .as_fake()
7365            .insert_tree(path!("/my-project"), json!({}))
7366            .await;
7367
7368        let workspace::OpenResult { window, .. } = cx
7369            .update(|cx| {
7370                workspace::Workspace::new_local(
7371                    vec![path!("/my-project").into()],
7372                    app_state.clone(),
7373                    None,
7374                    None,
7375                    None,
7376                    OpenMode::Activate,
7377                    cx,
7378                )
7379            })
7380            .await
7381            .unwrap();
7382
7383        window.update(cx, |mw, _, cx| mw.open_sidebar(cx)).unwrap();
7384        cx.background_executor.run_until_parked();
7385
7386        let project_key = ProjectGroupKey::new(None, PathList::new(&[path!("/my-project")]));
7387        let keys = window
7388            .read_with(cx, |mw, _| mw.project_group_keys())
7389            .unwrap();
7390        assert_eq!(
7391            keys,
7392            vec![project_key],
7393            "project group should exist before CloseProject: {keys:?}"
7394        );
7395
7396        cx.dispatch_action(window.into(), CloseProject);
7397
7398        let keys = window
7399            .read_with(cx, |mw, _| mw.project_group_keys())
7400            .unwrap();
7401        assert!(
7402            keys.is_empty(),
7403            "project group should be removed after CloseProject: {keys:?}"
7404        );
7405    }
7406
7407    #[gpui::test]
7408    async fn test_close_project_switches_to_neighbor_in_multi_project(cx: &mut TestAppContext) {
7409        use workspace::OpenMode;
7410
7411        let app_state = init_test(cx);
7412        app_state
7413            .fs
7414            .as_fake()
7415            .insert_tree(path!("/project-a"), json!({}))
7416            .await;
7417        app_state
7418            .fs
7419            .as_fake()
7420            .insert_tree(path!("/project-b"), json!({}))
7421            .await;
7422
7423        let workspace::OpenResult {
7424            window,
7425            workspace: workspace_a,
7426            ..
7427        } = cx
7428            .update(|cx| {
7429                workspace::Workspace::new_local(
7430                    vec![path!("/project-a").into()],
7431                    app_state.clone(),
7432                    None,
7433                    None,
7434                    None,
7435                    OpenMode::Activate,
7436                    cx,
7437                )
7438            })
7439            .await
7440            .unwrap();
7441
7442        let project_b = Project::test(app_state.fs.clone(), [Path::new("/project-b")], cx).await;
7443
7444        window
7445            .update(cx, |multi_workspace, window, cx| {
7446                multi_workspace.test_add_workspace(project_b, window, cx);
7447            })
7448            .unwrap();
7449        cx.background_executor.run_until_parked();
7450
7451        // Reactivate workspace A so we close it via CloseProject.
7452        window
7453            .update(cx, |multi_workspace, window, cx| {
7454                multi_workspace.activate(workspace_a, None, window, cx);
7455            })
7456            .unwrap();
7457        cx.background_executor.run_until_parked();
7458
7459        let keys_before = window
7460            .read_with(cx, |multi_workspace, _| {
7461                multi_workspace.project_group_keys()
7462            })
7463            .unwrap();
7464        assert_eq!(
7465            keys_before.len(),
7466            2,
7467            "should have 2 project groups before CloseProject: {keys_before:?}"
7468        );
7469
7470        cx.dispatch_action(window.into(), CloseProject);
7471        cx.background_executor.run_until_parked();
7472
7473        let keys_after = window
7474            .read_with(cx, |multi_workspace, _| {
7475                multi_workspace.project_group_keys()
7476            })
7477            .unwrap();
7478        assert_eq!(
7479            keys_after.len(),
7480            1,
7481            "one project group should remain after CloseProject: {keys_after:?}"
7482        );
7483
7484        let active_paths = window
7485            .read_with(cx, |multi_workspace, cx| {
7486                multi_workspace.workspace().read(cx).root_paths(cx)
7487            })
7488            .unwrap();
7489        assert!(
7490            !active_paths.is_empty(),
7491            "active workspace should contain the remaining project, not be empty: {active_paths:?}"
7492        );
7493    }
7494}
7495
Served at tenant.openagents/omega Member data and write actions are omitted.