mod app_menus;
pub mod edit_prediction_registry;
#[cfg(target_os = "macos")]
pub(crate) mod mac_only_instance;
mod migrate;
#[cfg(target_os = "macos")]
pub(crate) mod move_to_applications;
mod open_listener;
mod open_url_modal;
mod quick_action_bar;
pub mod remote_debug;
pub mod telemetry_log;
#[cfg(all(target_os = "macos", feature = "visual-tests"))]
pub mod visual_tests;
#[cfg(target_os = "windows")]
pub(crate) mod windows_only_instance;

use agent_ui::AgentDiffToolbar;
use anyhow::Context as _;
pub use app_menus::*;
use assets::Assets;

use breadcrumbs::Breadcrumbs;
use client::zed_urls;
use collections::VecDeque;
use debugger_ui::debugger_panel::DebugPanel;
use editor::{Editor, MultiBuffer};
use extension_host::ExtensionStore;
use feature_flags::{FeatureFlagAppExt as _, PanicFeatureFlag};
use fs::Fs;
use futures::FutureExt as _;
use futures::{StreamExt, channel::mpsc, select_biased};
use git_ui::commit_view::CommitViewToolbar;
use git_ui::git_panel::GitPanel;
use git_ui::project_diff::{BranchDiffToolbar, ProjectDiffToolbar};
use gpui::{
    Action, App, AppContext as _, AsyncWindowContext, ClipboardItem, Context, DismissEvent,
    Element, Entity, FocusHandle, Focusable, Image, ImageFormat, KeyBinding, ParentElement,
    PathPromptOptions, PromptLevel, ReadGlobal, SharedString, Size, Task, TaskExt, TitlebarOptions,
    UpdateGlobal, WeakEntity, Window, WindowBounds, WindowHandle, WindowKind, WindowOptions,
    actions, image_cache, img, point, px, retain_all,
};
use image_viewer::ImageInfo;
use language::Capability;
use language_onboarding::BasedPyrightBanner;
use language_tools::lsp_button::{self, LspButton};
use language_tools::lsp_log_view::LspLogToolbarItemView;
use markdown::{Markdown, MarkdownElement, MarkdownFont, MarkdownStyle};
use migrate::{MigrationBanner, MigrationEvent, MigrationNotification, MigrationType};
use migrator::migrate_keymap;
use onboarding::DOCS_URL;
use onboarding::multibuffer_hint::MultibufferHint;
pub use open_listener::*;
use outline_panel::OutlinePanel;
use paths::{
    local_debug_file_relative_path, local_settings_file_relative_path,
    local_tasks_file_relative_path,
};
use project::{DirectoryLister, DisableAiSettings, ProjectItem};
use project_panel::ProjectPanel;
use quick_action_bar::QuickActionBar;
use recent_projects::open_remote_project;
use release_channel::{AppCommitSha, AppVersion, ReleaseChannel};
use rope::Rope;
use search::project_search::ProjectSearchBar;
use settings::{
    BaseKeymap, DEFAULT_KEYMAP_PATH, InvalidSettingsError, KeybindSource, KeymapFile,
    KeymapFileLoadResult, MigrationStatus, Settings, SettingsFile, SettingsStore, VIM_KEYMAP_PATH,
    initial_local_debug_tasks_content, initial_project_settings_content, initial_tasks_content,
    update_settings_file,
};
use sidebar::Sidebar;

use std::{
    borrow::Cow,
    path::{Path, PathBuf},
    sync::Arc,
    sync::atomic::{self, AtomicBool},
};
use terminal_view::terminal_panel::{self, TerminalPanel};
use theme::{ActiveTheme, SystemAppearance, ThemeRegistry, deserialize_icon_theme};
use theme_settings::{ThemeSettings, load_user_theme};
use ui::{Navigable, NavigableEntry, PopoverMenuHandle, TintColor, prelude::*};
use util::markdown::MarkdownString;
use util::rel_path::RelPath;
use util::{ResultExt, asset_str, maybe};
use uuid::Uuid;
use vim_mode_setting::VimModeSetting;
use workspace::notifications::{NotificationId, dismiss_app_notification, show_app_notification};

use workspace::{
    AppState, MultiWorkspace, NewFile, NewWindow, OpenLog, Panel, Toast, Workspace,
    WorkspaceSettings, create_and_open_local_file,
    notifications::simple_message_notification::MessageNotification, open_new,
};
use workspace::{
    CloseIntent, CloseProject, CloseWindow, RestoreBanner, with_active_or_new_workspace,
};
use workspace::{Pane, notifications::DetachAndPromptErr};
use zed_actions::{
    About, OpenAccountSettings, OpenBrowser, OpenDocs, OpenServerSettings, OpenSettingsFile,
    OpenZedUrl, Quit,
};

pub struct CrashHandler(pub Arc<crashes::Client>);

impl gpui::Global for CrashHandler {}

actions!(
    zed,
    [
        /// Opens the element inspector for debugging UI.
        DebugElements,
        /// Hides the application window.
        Hide,
        /// Hides all other application windows.
        HideOthers,
        /// Minimizes the current window.
        Minimize,
        /// Opens the default settings file.
        OpenDefaultSettings,
        /// Opens project-specific settings file.
        OpenProjectSettingsFile,
        /// Opens the project tasks configuration.
        OpenProjectTasks,
        /// Opens the tasks panel.
        OpenTasks,
        /// Opens debug tasks configuration.
        OpenDebugTasks,
        /// Shows the default semantic token rules (read-only).
        ShowDefaultSemanticTokenRules,
        /// Resets the application database.
        ResetDatabase,
        /// Shows all hidden windows.
        ShowAll,
        /// Toggles fullscreen mode.
        ToggleFullScreen,
        /// Zooms the window.
        Zoom,
        /// Triggers a test panic for debugging.
        TestPanic,
        /// Triggers a hard crash for debugging.
        TestCrash,
    ]
);

actions!(
    dev,
    [
        /// Opens a prompt to enter a URL to open.
        OpenUrlPrompt,
    ]
);

pub fn init(cx: &mut App) {
    #[cfg(target_os = "macos")]
    cx.on_action(|_: &Hide, cx| cx.hide());
    #[cfg(target_os = "macos")]
    cx.on_action(|_: &HideOthers, cx| cx.hide_other_apps());
    #[cfg(target_os = "macos")]
    cx.on_action(|_: &ShowAll, cx| cx.unhide_other_apps());
    cx.on_action(quit);

    cx.on_action(|_: &RestoreBanner, cx| title_bar::restore_banner(cx));

    cx.observe_flag::<PanicFeatureFlag, _>({
        let mut added = false;
        move |flag, cx| {
            if added || !*flag {
                return;
            }
            added = true;
            cx.on_action(|_: &TestPanic, _| panic!("Ran the TestPanic action"))
                .on_action(|_: &TestCrash, _| {
                    unsafe extern "C" {
                        fn puts(s: *const i8);
                    }
                    unsafe {
                        puts(0xabad1d3a as *const i8);
                    }
                });
        }
    })
    .detach();
    cx.on_action(|_: &OpenLog, cx| {
        with_active_or_new_workspace(cx, |workspace, window, cx| {
            open_log_file(workspace, window, cx);
        });
    })
    .on_action(|_: &workspace::RevealLogInFileManager, cx| {
        cx.reveal_path(paths::log_file().as_path());
    })
    .on_action(|_: &zed_actions::OpenLicenses, cx| {
        with_active_or_new_workspace(cx, |workspace, window, cx| {
            open_bundled_file(
                workspace,
                asset_str::<Assets>("licenses.md"),
                "Open Source License Attribution",
                "Markdown",
                window,
                cx,
            );
        });
    })
    .on_action(|&zed_actions::OpenKeymapFile, cx| {
        with_active_or_new_workspace(cx, |_, window, cx| {
            open_settings_file(
                paths::keymap_file(),
                || settings::initial_keymap_content().as_ref().into(),
                window,
                cx,
            );
        });
    })
    .on_action(|_: &OpenSettingsFile, cx| {
        with_active_or_new_workspace(cx, |_, window, cx| {
            open_settings_file(
                paths::settings_file(),
                || settings::initial_user_settings_content().as_ref().into(),
                window,
                cx,
            );
        });
    })
    .on_action(|_: &OpenAccountSettings, cx| {
        with_active_or_new_workspace(cx, |_, _, cx| {
            cx.open_url(&zed_urls::account_url(cx));
        });
    })
    .on_action(|_: &OpenTasks, cx| {
        with_active_or_new_workspace(cx, |_, window, cx| {
            open_settings_file(
                paths::tasks_file(),
                || settings::initial_tasks_content().as_ref().into(),
                window,
                cx,
            );
        });
    })
    .on_action(|_: &OpenDebugTasks, cx| {
        with_active_or_new_workspace(cx, |_, window, cx| {
            open_settings_file(
                paths::debug_scenarios_file(),
                || settings::initial_debug_tasks_content().as_ref().into(),
                window,
                cx,
            );
        });
    })
    .on_action(|_: &ShowDefaultSemanticTokenRules, cx| {
        with_active_or_new_workspace(cx, |workspace, window, cx| {
            open_bundled_file(
                workspace,
                settings::default_semantic_token_rules(),
                "Default Semantic Token Rules",
                "JSONC",
                window,
                cx,
            );
        });
    })
    .on_action(|_: &OpenDefaultSettings, cx| {
        with_active_or_new_workspace(cx, |workspace, window, cx| {
            open_bundled_file(
                workspace,
                settings::default_settings(),
                "Default Settings",
                "JSON",
                window,
                cx,
            );
        });
    })
    .on_action(|_: &zed_actions::OpenDefaultKeymap, cx| {
        with_active_or_new_workspace(cx, |workspace, window, cx| {
            open_bundled_file(
                workspace,
                settings::default_keymap(),
                "Default Key Bindings",
                "JSON",
                window,
                cx,
            );
        });
    })
    .on_action(|_: &About, cx| {
        open_about_window(cx);
    });
}

fn bind_on_window_closed(cx: &mut App) -> Option<gpui::Subscription> {
    #[cfg(target_os = "macos")]
    {
        WorkspaceSettings::get_global(cx)
            .on_last_window_closed
            .is_quit_app()
            .then(|| {
                cx.on_window_closed(|cx, _window_id| {
                    if cx.windows().is_empty() {
                        cx.quit();
                    }
                })
            })
    }
    #[cfg(not(target_os = "macos"))]
    {
        Some(cx.on_window_closed(|cx, _window_id| {
            if cx.windows().is_empty() {
                cx.quit();
            }
        }))
    }
}

pub fn build_window_options(display_uuid: Option<Uuid>, cx: &mut App) -> WindowOptions {
    let display = display_uuid.and_then(|uuid| {
        cx.displays()
            .into_iter()
            .find(|display| display.uuid().ok() == Some(uuid))
    });
    let app_id = ReleaseChannel::global(cx).app_id();
    let window_decorations = match std::env::var("ZED_WINDOW_DECORATIONS") {
        Ok(val) if val == "server" => gpui::WindowDecorations::Server,
        Ok(val) if val == "client" => gpui::WindowDecorations::Client,
        _ => match WorkspaceSettings::get_global(cx).window_decorations {
            settings::WindowDecorations::Server => gpui::WindowDecorations::Server,
            settings::WindowDecorations::Client => gpui::WindowDecorations::Client,
        },
    };

    let use_system_window_tabs = WorkspaceSettings::get_global(cx).use_system_window_tabs;

    #[cfg(any(target_os = "linux", target_os = "freebsd"))]
    static APP_ICON: std::sync::LazyLock<Option<std::sync::Arc<image::RgbaImage>>> =
        std::sync::LazyLock::new(|| {
            // this shouldn't fail since decode is checked in build.rs
            const BYTES: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/app_icon.png"));
            util::maybe!({
                let image = image::ImageReader::new(std::io::Cursor::new(BYTES))
                    .with_guessed_format()?
                    .decode()?
                    .into();
                anyhow::Ok(Arc::new(image))
            })
            .log_err()
        });

    WindowOptions {
        titlebar: Some(TitlebarOptions {
            title: None,
            appears_transparent: true,
            traffic_light_position: Some(point(px(9.0), px(9.0))),
        }),
        window_bounds: None,
        focus: false,
        show: false,
        kind: WindowKind::Normal,
        is_movable: true,
        display_id: display.map(|display| display.id()),
        window_background: cx.theme().window_background_appearance(),
        app_id: Some(app_id.to_owned()),
        #[cfg(any(target_os = "linux", target_os = "freebsd"))]
        icon: APP_ICON.as_ref().cloned(),
        window_decorations: Some(window_decorations),
        window_min_size: Some(gpui::Size {
            width: px(360.0),
            height: px(240.0),
        }),
        tabbing_identifier: if use_system_window_tabs {
            Some(String::from("zed"))
        } else {
            None
        },
        ..Default::default()
    }
}

pub fn initialize_workspace(app_state: Arc<AppState>, cx: &mut App) {
    let mut _on_close_subscription = bind_on_window_closed(cx);
    cx.observe_global::<SettingsStore>(move |cx| {
        // A 1.92 regression causes unused-assignment to trigger on this variable.
        _ = _on_close_subscription.is_some();
        _on_close_subscription = bind_on_window_closed(cx);
    })
    .detach();

    init_cursor_hide_mode(cx);

    cx.observe_new(|_multi_workspace: &mut MultiWorkspace, window, cx| {
        let Some(window) = window else {
            return;
        };

        #[cfg(feature = "track-project-leak")]
        {
            let multi_workspace_handle = cx.weak_entity();
            let workspace_handle = _multi_workspace.workspace().downgrade();
            let project_handle = _multi_workspace.workspace().read(cx).project().downgrade();
            let window_id_2 = window.window_handle().window_id();
            cx.on_window_closed(move |cx, window_id| {
                let multi_workspace_handle = multi_workspace_handle.clone();
                let workspace_handle = workspace_handle.clone();
                let project_handle = project_handle.clone();
                if window_id != window_id_2 {
                    return;
                }
                cx.spawn(async move |cx| {
                    cx.background_executor()
                        .timer(std::time::Duration::from_millis(1500))
                        .await;

                    multi_workspace_handle.assert_released();
                    workspace_handle.assert_released();
                    project_handle.assert_released();
                })
                .detach();
            })
            .detach();
        }

        cx.spawn_in(window, async move |_this, cx| {
            const TELEMETRY_INTERVAL: std::time::Duration = std::time::Duration::from_secs(5 * 60);
            loop {
                cx.background_executor().timer(TELEMETRY_INTERVAL).await;
                if cx
                    .update(|window, cx| {
                        input_latency_ui::report_input_latency_telemetry(window, cx);
                    })
                    .is_err()
                {
                    break;
                }
            }
        })
        .detach();

        let multi_workspace_handle = cx.entity().downgrade();
        window.on_window_should_close(cx, move |window, cx| {
            multi_workspace_handle
                .update(cx, |multi_workspace, cx| {
                    // We'll handle closing asynchronously
                    multi_workspace.close_window(&CloseWindow, window, cx);
                    false
                })
                .unwrap_or(true)
        });

        let window_handle = window.window_handle();
        let multi_workspace_handle = cx.entity();
        cx.subscribe_in(
            &multi_workspace_handle,
            window,
            |this, _multi_workspace, event: &workspace::MultiWorkspaceEvent, window, cx| {
                let workspace::MultiWorkspaceEvent::ActiveWorkspaceChanged { source_workspace } =
                    event
                else {
                    return;
                };

                let active_workspace = this.workspace().clone();
                let source_workspace = source_workspace.clone();
                active_workspace.update(cx, |workspace, cx| {
                    if let Some(ref source) = source_workspace {
                        if let Some(panel) = workspace.panel::<agent_ui::AgentPanel>(cx) {
                            panel.update(cx, |panel, cx| {
                                panel.initialize_from_source_workspace_if_needed(
                                    source.clone(),
                                    window,
                                    cx,
                                );
                            });
                        }
                    }

                    ensure_agent_panel_for_workspace(workspace, source_workspace, window, cx)
                        .detach_and_log_err(cx);
                });
            },
        )
        .detach();

        cx.defer(move |cx| {
            window_handle
                .update(cx, |_, window, cx| {
                    let sidebar =
                        cx.new(|cx| Sidebar::new(multi_workspace_handle.clone(), window, cx));
                    multi_workspace_handle.update(cx, |multi_workspace, cx| {
                        multi_workspace.register_sidebar(sidebar, cx);
                    });
                })
                .ok();
        });
    })
    .detach();

    cx.observe_new(move |workspace: &mut Workspace, window, cx| {
        let Some(window) = window else {
            return;
        };

        let workspace_handle = cx.entity();
        let center_pane = workspace.active_pane().clone();
        initialize_pane(workspace, &center_pane, window, cx);

        cx.subscribe_in(&workspace_handle, window, {
            move |workspace, _, event, window, cx| match event {
                workspace::Event::PaneAdded(pane) => {
                    initialize_pane(workspace, pane, window, cx);
                }
                workspace::Event::OpenBundledFile {
                    text,
                    title,
                    language,
                } => open_bundled_file(workspace, text.clone(), title, language, window, cx),
                _ => {}
            }
        })
        .detach();

        #[cfg(not(any(test, target_os = "macos")))]
        initialize_file_watcher(window, cx);

        if let Some(specs) = window.gpu_specs() {
            log::info!("Using GPU: {:?}", specs);
            show_software_emulation_warning_if_needed(specs.clone(), window, cx);
            if let Some(crash_client) = cx.try_global::<CrashHandler>() {
                crashes::set_gpu_info(&crash_client.0, specs);
            }
        }

        let edit_prediction_menu_handle = PopoverMenuHandle::default();
        let edit_prediction_ui = cx.new(|cx| {
            edit_prediction_ui::EditPredictionButton::new(
                app_state.fs.clone(),
                app_state.user_store.clone(),
                edit_prediction_menu_handle.clone(),
                workspace.project().clone(),
                cx,
            )
        });
        workspace.register_action({
            move |_, _: &edit_prediction_ui::ToggleMenu, window, cx| {
                edit_prediction_menu_handle.toggle(window, cx);
            }
        });

        let search_button = cx.new(|_| search::search_status_button::SearchButton::new());
        let diagnostic_summary =
            cx.new(|cx| diagnostics::items::DiagnosticIndicator::new(workspace, cx));
        let active_file_name = cx.new(|_| workspace::active_file_name::ActiveFileName::new());
        let activity_indicator = activity_indicator::ActivityIndicator::new(
            workspace,
            workspace.project().read(cx).languages().clone(),
            window,
            cx,
        );
        let active_buffer_encoding =
            cx.new(|_| encoding_selector::ActiveBufferEncoding::new(workspace));
        let active_buffer_language =
            cx.new(|_| language_selector::ActiveBufferLanguage::new(workspace));
        let active_toolchain_language =
            cx.new(|cx| toolchain_selector::ActiveToolchain::new(workspace, window, cx));
        let vim_mode_indicator = cx.new(|cx| vim::ModeIndicator::new(window, cx));
        let image_info = cx.new(|_cx| ImageInfo::new(workspace));

        let lsp_button_menu_handle = PopoverMenuHandle::default();
        let lsp_button =
            cx.new(|cx| LspButton::new(workspace, lsp_button_menu_handle.clone(), window, cx));
        workspace.register_action({
            move |_, _: &lsp_button::ToggleMenu, window, cx| {
                lsp_button_menu_handle.toggle(window, cx);
            }
        });

        let cursor_position =
            cx.new(|_| go_to_line::cursor_position::CursorPosition::new(workspace));
        let line_ending_indicator =
            cx.new(|_| line_ending_selector::LineEndingIndicator::default());
        let merge_conflict_indicator =
            cx.new(|cx| git_ui::MergeConflictIndicator::new(workspace, cx));
        workspace.status_bar().update(cx, |status_bar, cx| {
            status_bar.add_left_item(search_button, window, cx);
            status_bar.add_left_item(lsp_button, window, cx);
            status_bar.add_left_item(diagnostic_summary, window, cx);
            status_bar.add_left_item(active_file_name, window, cx);
            status_bar.add_left_item(merge_conflict_indicator, window, cx);
            status_bar.add_left_item(activity_indicator, window, cx);
            status_bar.add_right_item(edit_prediction_ui, window, cx);
            status_bar.add_right_item(active_buffer_encoding, window, cx);
            status_bar.add_right_item(active_buffer_language, window, cx);
            status_bar.add_right_item(active_toolchain_language, window, cx);
            status_bar.add_right_item(line_ending_indicator, window, cx);
            status_bar.add_right_item(vim_mode_indicator, window, cx);
            status_bar.add_right_item(cursor_position, window, cx);
            status_bar.add_right_item(image_info, window, cx);
        });

        let panels_task = initialize_panels(window, cx);
        workspace.set_panels_task(panels_task);
        register_actions(app_state.clone(), workspace, window, cx);

        if !workspace.has_active_modal(window, cx) {
            workspace.focus_handle(cx).focus(window, cx);
        }
    })
    .detach();
}

#[cfg(any(target_os = "linux", target_os = "freebsd"))]
#[allow(unused)]
fn initialize_file_watcher(window: &mut Window, cx: &mut Context<Workspace>) {
    if let Err(e) = fs::fs_watcher::global(|_| {}) {
        let message = format!(
            db::indoc! {r#"
            inotify_init returned {}

            This may be due to system-wide limits on inotify instances. For troubleshooting see: https://zed.dev/docs/linux
            "#},
            e
        );
        let prompt = window.prompt(
            PromptLevel::Critical,
            "Could not start inotify",
            Some(&message),
            &["Troubleshoot and Quit"],
            cx,
        );
        cx.spawn(async move |_, cx| {
            if prompt.await == Ok(0) {
                cx.update(|cx| {
                    cx.open_url("https://zed.dev/docs/linux#could-not-start-inotify");
                    cx.quit();
                });
            }
        })
        .detach()
    }
}

#[cfg(target_os = "windows")]
#[allow(unused)]
fn initialize_file_watcher(window: &mut Window, cx: &mut Context<Workspace>) {
    if let Err(e) = fs::fs_watcher::global(|_| {}) {
        let message = format!(
            db::indoc! {r#"
            ReadDirectoryChangesW initialization failed: {}

            This may occur on network filesystems and WSL paths. For troubleshooting see: https://zed.dev/docs/windows
            "#},
            e
        );
        let prompt = window.prompt(
            PromptLevel::Critical,
            "Could not start ReadDirectoryChangesW",
            Some(&message),
            &["Troubleshoot and Quit"],
            cx,
        );
        cx.spawn(async move |_, cx| {
            if prompt.await == Ok(0) {
                cx.update(|cx| {
                    cx.open_url("https://zed.dev/docs/windows");
                    cx.quit()
                });
            }
        })
        .detach()
    }
}

fn show_software_emulation_warning_if_needed(
    specs: gpui::GpuSpecs,
    window: &mut Window,
    cx: &mut Context<Workspace>,
) {
    if specs.is_software_emulated && std::env::var("ZED_ALLOW_EMULATED_GPU").is_err() {
        let (graphics_api, docs_url, open_url) = if cfg!(target_os = "windows") {
            (
                "DirectX",
                "https://zed.dev/docs/windows",
                "https://zed.dev/docs/windows",
            )
        } else {
            (
                "Vulkan",
                "https://zed.dev/docs/linux",
                "https://zed.dev/docs/linux#zed-fails-to-open-windows",
            )
        };
        let message = format!(
            db::indoc! {r#"
            Zed uses {} for rendering and requires a compatible GPU.

            Currently you are using a software emulated GPU ({}) which
            will result in awful performance.

            For troubleshooting see: {}
            Set ZED_ALLOW_EMULATED_GPU=1 env var to permanently override.
            "#},
            graphics_api, specs.device_name, docs_url
        );
        let prompt = window.prompt(
            PromptLevel::Critical,
            "Unsupported GPU",
            Some(&message),
            &["Skip", "Troubleshoot and Quit"],
            cx,
        );
        cx.spawn(async move |_, cx| {
            if prompt.await == Ok(1) {
                cx.update(|cx| {
                    cx.open_url(open_url);
                    cx.quit();
                });
            }
        })
        .detach()
    }
}

fn initialize_panels(window: &mut Window, cx: &mut Context<Workspace>) -> Task<anyhow::Result<()>> {
    cx.spawn_in(window, async move |workspace_handle, cx| {
        let project_panel = ProjectPanel::load(workspace_handle.clone(), cx.clone());
        let outline_panel = OutlinePanel::load(workspace_handle.clone(), cx.clone());
        let terminal_panel = TerminalPanel::load(workspace_handle.clone(), cx.clone());
        let git_panel = GitPanel::load(workspace_handle.clone(), cx.clone());
        let channels_panel =
            collab_ui::collab_panel::CollabPanel::load(workspace_handle.clone(), cx.clone());
        let debug_panel = DebugPanel::load(workspace_handle.clone(), cx);

        async fn add_panel_when_ready(
            panel_task: impl Future<Output = anyhow::Result<Entity<impl workspace::Panel>>> + 'static,
            workspace_handle: WeakEntity<Workspace>,
            mut cx: gpui::AsyncWindowContext,
        ) {
            if let Some(panel) = panel_task.await.context("failed to load panel").log_err()
            {
                workspace_handle
                    .update_in(&mut cx, |workspace, window, cx| {
                        workspace.add_panel(panel, window, cx);
                    })
                    .log_err();
            }
        }

        futures::join!(
            add_panel_when_ready(project_panel, workspace_handle.clone(), cx.clone()),
            add_panel_when_ready(outline_panel, workspace_handle.clone(), cx.clone()),
            add_panel_when_ready(terminal_panel, workspace_handle.clone(), cx.clone()),
            add_panel_when_ready(git_panel, workspace_handle.clone(), cx.clone()),
            add_panel_when_ready(channels_panel, workspace_handle.clone(), cx.clone()),
            add_panel_when_ready(debug_panel, workspace_handle.clone(), cx.clone()),
            initialize_agent_panel(workspace_handle, cx.clone()).map(|r| r.log_err()),
        );

        anyhow::Ok(())
    })
}

fn setup_or_teardown_ai_panel<P: Panel>(
    workspace: &mut Workspace,
    window: &mut Window,
    cx: &mut Context<Workspace>,
    load_panel: impl FnOnce(
        WeakEntity<Workspace>,
        AsyncWindowContext,
    ) -> Task<anyhow::Result<Entity<P>>>
    + 'static,
) -> Task<anyhow::Result<()>> {
    let disable_ai = SettingsStore::global(cx)
        .get::<DisableAiSettings>(None)
        .disable_ai
        || cfg!(test);
    let existing_panel = workspace.panel::<P>(cx);
    match (disable_ai, existing_panel) {
        (false, None) => cx.spawn_in(window, async move |workspace, cx| {
            let panel = load_panel(workspace.clone(), cx.clone()).await?;
            workspace.update_in(cx, |workspace, window, cx| {
                let disable_ai = SettingsStore::global(cx)
                    .get::<DisableAiSettings>(None)
                    .disable_ai;
                let have_panel = workspace.panel::<P>(cx).is_some();
                if !disable_ai && !have_panel {
                    workspace.add_panel(panel, window, cx);
                }
            })
        }),
        (true, Some(existing_panel)) => {
            workspace.remove_panel::<P>(&existing_panel, window, cx);
            Task::ready(Ok(()))
        }
        _ => Task::ready(Ok(())),
    }
}

fn ensure_agent_panel_for_workspace(
    workspace: &mut Workspace,
    source_workspace: Option<WeakEntity<Workspace>>,
    window: &mut Window,
    cx: &mut Context<Workspace>,
) -> Task<anyhow::Result<()>> {
    let task = setup_or_teardown_ai_panel(workspace, window, cx, move |workspace, cx| {
        agent_ui::AgentPanel::load(workspace, cx)
    });

    cx.spawn_in(window, async move |workspace, cx| {
        task.await?;
        workspace.update_in(cx, |workspace, window, cx| {
            if let Some(source_workspace) = source_workspace.clone()
                && let Some(panel) = workspace.panel::<agent_ui::AgentPanel>(cx)
            {
                panel.update(cx, |panel, cx| {
                    panel.initialize_from_source_workspace_if_needed(source_workspace, window, cx);
                });
            }
        })
    })
}

async fn initialize_agent_panel(
    workspace_handle: WeakEntity<Workspace>,
    mut cx: AsyncWindowContext,
) -> anyhow::Result<()> {
    workspace_handle
        .update_in(&mut cx, |workspace, window, cx| {
            ensure_agent_panel_for_workspace(workspace, None, window, cx)
        })?
        .await?;

    workspace_handle.update_in(&mut cx, |workspace, window, cx| {
        cx.observe_global_in::<SettingsStore>(window, move |workspace, window, cx| {
            ensure_agent_panel_for_workspace(workspace, None, window, cx).detach_and_log_err(cx);
        })
        .detach();

        // Register the actions that are shared between `assistant` and `assistant2`.
        //
        // We need to do this here instead of within the individual `init`
        // functions so that we only register the actions once.
        //
        // Once we ship `assistant2` we can push this back down into `agent::agent_panel::init`.
        if !cfg!(test) {
            workspace
                .register_action(agent_ui::AgentPanel::toggle_focus)
                .register_action(agent_ui::AgentPanel::focus)
                .register_action(agent_ui::AgentPanel::toggle)
                .register_action(agent_ui::InlineAssistant::inline_assist);
        }
    })?;

    anyhow::Ok(())
}

fn register_actions(
    app_state: Arc<AppState>,
    workspace: &mut Workspace,
    _: &mut Window,
    cx: &mut Context<Workspace>,
) {
    workspace
        .register_action(|_, _: &OpenDocs, _, cx| cx.open_url(DOCS_URL))
        .register_action(
            |workspace: &mut Workspace,
             _: &input_latency_ui::DumpInputLatencyHistogram,
             window: &mut Window,
             cx: &mut Context<Workspace>| {
                let report =
                    input_latency_ui::format_input_latency_report(window, cx);
                let project = workspace.project().clone();
                let buffer = project.update(cx, |project, cx| {
                    project.create_local_buffer(&report, None, true, cx)
                });
                let editor =
                    cx.new(|cx| Editor::for_buffer(buffer, Some(project), window, cx));
                workspace.add_item_to_active_pane(Box::new(editor), None, true, window, cx);
            },
        )
        .register_action(|_, _: &Minimize, window, _| {
            window.minimize_window();
        })
        .register_action(|_, _: &Zoom, window, _| {
            window.zoom_window();
        })
        .register_action(|_, _: &ToggleFullScreen, window, _| {
            window.toggle_fullscreen();
        })
        .register_action(|_, action: &OpenZedUrl, _, cx| {
            OpenListener::global(cx).open(RawOpenRequest {
                urls: vec![action.url.clone()],
                ..Default::default()
            })
        })
        .register_action(|workspace, _: &OpenUrlPrompt, window, cx| {
            workspace.toggle_modal(window, cx, |window, cx| {
                open_url_modal::OpenUrlModal::new(window, cx)
            });
        })
        .register_action(|workspace, action: &OpenBrowser, _window, cx| {
            // Parse and validate the URL to ensure it's properly formatted
            match url::Url::parse(&action.url) {
                Ok(parsed_url) => {
                    // Use the parsed URL's string representation which is properly escaped
                    cx.open_url(parsed_url.as_str());
                }
                Err(e) => {
                    workspace.show_error(
                        &anyhow::anyhow!(
                            "Opening this URL in a browser failed because the URL is invalid: {}\n\nError was: {e}",
                            action.url
                        ),
                        cx,
                    );
                }
            }
        })
        .register_action(|workspace, action: &workspace::Open, window, cx| {
            telemetry::event!("Project Opened");
            workspace::prompt_for_open_path_and_open(
                workspace,
                workspace.app_state().clone(),
                PathPromptOptions {
                    files: true,
                    directories: true,
                    multiple: true,
                    prompt: None,
                },
                action.create_new_window,
                window,
                cx,
            );
        })
        .register_action(|workspace, _: &workspace::OpenFiles, window, cx| {
            let directories = cx.can_select_mixed_files_and_dirs();
            workspace::prompt_for_open_path_and_open(
                workspace,
                workspace.app_state().clone(),
                PathPromptOptions {
                    files: true,
                    directories,
                    multiple: true,
                    prompt: None,
                },
                true,
                window,
                cx,
            );
        })
        .register_action(|workspace, action: &zed_actions::OpenRemote, window, cx| {
            if !action.from_existing_connection {
                cx.propagate();
                return;
            }
            // You need existing remote connection to open it this way
            if workspace.project().read(cx).is_local() {
                return;
            }
            telemetry::event!("Project Opened");
            let paths = workspace.prompt_for_open_path(
                PathPromptOptions {
                    files: true,
                    directories: true,
                    multiple: true,
                    prompt: None,
                },
                DirectoryLister::Project(workspace.project().clone()),
                window,
                cx,
            );
            cx.spawn_in(window, async move |this, cx| {
                let Some(paths) = paths.await.log_err().flatten() else {
                    return;
                };
                if let Some(task) = this
                    .update_in(cx, |this, window, cx| {
                        open_new_ssh_project_from_project(this, paths, window, cx)
                    })
                    .log_err()
                {
                    task.await.log_err();
                }
            })
            .detach()
        })
        .register_action({
            let fs = app_state.fs.clone();
            move |_, action: &zed_actions::IncreaseUiFontSize, _window, cx| {
                if action.persist {
                    update_settings_file(fs.clone(), cx, move |settings, cx| {
                        let ui_font_size = ThemeSettings::get_global(cx).ui_font_size(cx) + px(1.0);
                        let _ = settings
                            .theme
                            .ui_font_size
                            .insert(f32::from(theme_settings::clamp_font_size(ui_font_size)).into());
                    });
                } else {
                    theme_settings::adjust_ui_font_size(cx, |size| size + px(1.0));
                }
            }
        })
        .register_action({
            let fs = app_state.fs.clone();
            move |_, action: &zed_actions::DecreaseUiFontSize, _window, cx| {
                if action.persist {
                    update_settings_file(fs.clone(), cx, move |settings, cx| {
                        let ui_font_size = ThemeSettings::get_global(cx).ui_font_size(cx) - px(1.0);
                        let _ = settings
                            .theme
                            .ui_font_size
                            .insert(f32::from(theme_settings::clamp_font_size(ui_font_size)).into());
                    });
                } else {
                    theme_settings::adjust_ui_font_size(cx, |size| size - px(1.0));
                }
            }
        })
        .register_action({
            let fs = app_state.fs.clone();
            move |_, action: &zed_actions::ResetUiFontSize, _window, cx| {
                if action.persist {
                    update_settings_file(fs.clone(), cx, move |settings, _| {
                        settings.theme.ui_font_size = None;
                    });
                } else {
                    theme_settings::reset_ui_font_size(cx);
                }
            }
        })
        .register_action({
            let fs = app_state.fs.clone();
            move |_, action: &zed_actions::IncreaseBufferFontSize, _window, cx| {
                if action.persist {
                    update_settings_file(fs.clone(), cx, move |settings, cx| {
                        let buffer_font_size =
                            ThemeSettings::get_global(cx).buffer_font_size(cx) + px(1.0);
                        let _ = settings
                            .theme
                            .buffer_font_size
                            .insert(f32::from(theme_settings::clamp_font_size(buffer_font_size)).into());
                    });
                } else {
                    theme_settings::increase_buffer_font_size(cx);
                }
            }
        })
        .register_action({
            let fs = app_state.fs.clone();
            move |_, action: &zed_actions::DecreaseBufferFontSize, _window, cx| {
                if action.persist {
                    update_settings_file(fs.clone(), cx, move |settings, cx| {
                        let buffer_font_size =
                            ThemeSettings::get_global(cx).buffer_font_size(cx) - px(1.0);
                        let _ = settings
                            .theme
                            .buffer_font_size
                            .insert(f32::from(theme_settings::clamp_font_size(buffer_font_size)).into());
                    });
                } else {
                    theme_settings::decrease_buffer_font_size(cx);
                }
            }
        })
        .register_action({
            let fs = app_state.fs.clone();
            move |_, action: &zed_actions::ResetBufferFontSize, _window, cx| {
                if action.persist {
                    update_settings_file(fs.clone(), cx, move |settings, _| {
                        settings.theme.buffer_font_size = None;
                    });
                } else {
                    theme_settings::reset_buffer_font_size(cx);
                }
            }
        })
        .register_action({
            let fs = app_state.fs.clone();
            move |_, action: &zed_actions::ResetAllZoom, _window, cx| {
                if action.persist {
                    update_settings_file(fs.clone(), cx, move |settings, _| {
                        settings.theme.ui_font_size = None;
                        settings.theme.buffer_font_size = None;
                        settings.theme.agent_ui_font_size = None;
                        settings.theme.agent_buffer_font_size = None;
                    });
                } else {
                    theme_settings::reset_ui_font_size(cx);
                    theme_settings::reset_buffer_font_size(cx);
                    theme_settings::reset_agent_ui_font_size(cx);
                    theme_settings::reset_agent_buffer_font_size(cx);
                }
            }
        })
        .register_action(|_, _: &install_cli::RegisterZedScheme, window, cx| {
            cx.spawn_in(window, async move |workspace, cx| {
                install_cli::register_zed_scheme(cx).await?;
                workspace.update_in(cx, |workspace, _, cx| {
                    struct RegisterZedScheme;

                    workspace.show_toast(
                        Toast::new(
                            NotificationId::unique::<RegisterZedScheme>(),
                            format!(
                                "zed:// links will now open in {}.",
                                ReleaseChannel::global(cx).display_name()
                            ),
                        ),
                        cx,
                    )
                })?;
                Ok(())
            })
            .detach_and_prompt_err(
                "Error registering zed:// scheme",
                window,
                cx,
                |_, _, _| None,
            );
        })
        .register_action(open_project_settings_file)
        .register_action(open_project_tasks_file)
        .register_action(open_project_debug_tasks_file)
        .register_action(
            |workspace: &mut Workspace,
             _: &zed_actions::project_panel::ToggleFocus,
             window: &mut Window,
             cx: &mut Context<Workspace>| {
                workspace.toggle_panel_focus::<ProjectPanel>(window, cx);
            },
        )
        .register_action(
            |workspace: &mut Workspace,
             _: &outline_panel::ToggleFocus,
             window: &mut Window,
             cx: &mut Context<Workspace>| {
                workspace.toggle_panel_focus::<OutlinePanel>(window, cx);
            },
        )
        .register_action(
            |workspace: &mut Workspace,
             _: &collab_ui::collab_panel::ToggleFocus,
             window: &mut Window,
             cx: &mut Context<Workspace>| {
                workspace.toggle_panel_focus::<collab_ui::collab_panel::CollabPanel>(window, cx);
            },
        )
        .register_action(
            |workspace: &mut Workspace,
             _: &terminal_panel::ToggleFocus,
             window: &mut Window,
             cx: &mut Context<Workspace>| {
                workspace.toggle_panel_focus::<TerminalPanel>(window, cx);
            },
        )
        .register_action({
            let app_state = app_state.clone();
            move |_, _: &NewWindow, _, cx| {
                open_new(
                    Default::default(),
                    app_state.clone(),
                    cx,
                    |workspace, window, cx| {
                        cx.activate(true);
                        // Create buffer synchronously to avoid flicker
                        let project = workspace.project().clone();
                        let buffer = project.update(cx, |project, cx| {
                            project.create_local_buffer("", None, true, cx)
                        });
                        let editor = cx.new(|cx| {
                            Editor::for_buffer(buffer, Some(project), window, cx)
                        });
                        workspace.add_item_to_active_pane(
                            Box::new(editor),
                            None,
                            true,
                            window,
                            cx,
                        );
                    },
                )
                .detach();
            }
        })
        .register_action({
            let app_state = app_state.clone();
            move |workspace, _: &CloseProject, window, cx| {
                let Some(window_handle) = window.window_handle().downcast::<MultiWorkspace>() else {
                    return;
                };
                let app_state = app_state.clone();
                let old_group_key = workspace.project_group_key(cx);
                cx.spawn_in(window, async move |this, cx| {
                    let should_continue = this
                        .update_in(cx, |workspace, window, cx| {
                            workspace.prepare_to_close(
                                CloseIntent::ReplaceWindow,
                                window,
                                cx,
                            )
                        })?
                        .await?;
                    if should_continue {
                        let task = cx.update(|_window, cx| {
                            open_new(
                                workspace::OpenOptions {
                                    requesting_window: Some(window_handle),
                                    ..Default::default()
                                },
                                app_state,
                                cx,
                                |workspace, window, cx| {
                                    cx.activate(true);
                                    let project = workspace.project().clone();
                                    let buffer = project.update(cx, |project, cx| {
                                        project.create_local_buffer("", None, true, cx)
                                    });
                                    let editor = cx.new(|cx| {
                                        Editor::for_buffer(buffer, Some(project), window, cx)
                                    });
                                    workspace.add_item_to_active_pane(
                                        Box::new(editor),
                                        None,
                                        true,
                                        window,
                                        cx,
                                    );
                                },
                            )
                        })?;
                        task.await?;
                        window_handle.update(cx, |mw, window, cx| {
                            mw.remove_project_group(&old_group_key, window, cx)
                        })?.await.log_err();
                        Ok::<(), anyhow::Error>(())
                    } else {
                        Ok(())
                    }
                })
                .detach_and_log_err(cx);
            }
        })
        .register_action({
            let app_state = app_state.clone();
            move |_, _: &NewFile, _, cx| {
                open_new(
                    Default::default(),
                    app_state.clone(),
                    cx,
                    |workspace, window, cx| {
                        Editor::new_file(workspace, &Default::default(), window, cx)
                    },
                )
                .detach_and_log_err(cx);
            }
        });

    #[cfg(not(target_os = "windows"))]
    workspace.register_action(install_cli);

    if workspace.project().read(cx).is_via_remote_server() {
        workspace.register_action({
            move |workspace, _: &OpenServerSettings, window, cx| {
                let open_server_settings = workspace
                    .project()
                    .update(cx, |project, cx| project.open_server_settings(cx));

                cx.spawn_in(window, async move |workspace, cx| {
                    let buffer = open_server_settings.await?;

                    workspace
                        .update_in(cx, |workspace, window, cx| {
                            workspace.open_path(
                                buffer
                                    .read(cx)
                                    .project_path(cx)
                                    .expect("Settings file must have a location"),
                                None,
                                true,
                                window,
                                cx,
                            )
                        })?
                        .await?;

                    anyhow::Ok(())
                })
                .detach_and_log_err(cx);
            }
        });
    }

    workspace.register_action(sidebar::dump_workspace_info);
}

fn initialize_pane(
    workspace: &Workspace,
    pane: &Entity<Pane>,
    window: &mut Window,
    cx: &mut Context<Workspace>,
) {
    let workspace_handle = cx.weak_entity();
    pane.update(cx, |pane, cx| {
        pane.toolbar().update(cx, |toolbar, cx| {
            let multibuffer_hint = cx.new(|_| MultibufferHint::new());
            toolbar.add_item(multibuffer_hint, window, cx);
            let breadcrumbs = cx.new(|_| Breadcrumbs::new());
            toolbar.add_item(breadcrumbs, window, cx);
            let buffer_search_bar = cx.new(|cx| {
                search::BufferSearchBar::new(
                    Some(workspace.project().read(cx).languages().clone()),
                    window,
                    cx,
                )
            });
            toolbar.add_item(buffer_search_bar.clone(), window, cx);
            let quick_action_bar =
                cx.new(|cx| QuickActionBar::new(buffer_search_bar, workspace, cx));
            toolbar.add_item(quick_action_bar, window, cx);
            let diagnostic_editor_controls = cx.new(|_| diagnostics::ToolbarControls::new());
            toolbar.add_item(diagnostic_editor_controls, window, cx);
            let project_search_bar = cx.new(|_| ProjectSearchBar::new());
            toolbar.add_item(project_search_bar, window, cx);
            let lsp_log_item = cx.new(|_| LspLogToolbarItemView::new());
            toolbar.add_item(lsp_log_item, window, cx);
            let dap_log_item = cx.new(|_| debugger_tools::DapLogToolbarItemView::new());
            toolbar.add_item(dap_log_item, window, cx);
            let acp_tools_item = cx.new(|_| acp_tools::AcpToolsToolbarItemView::new());
            toolbar.add_item(acp_tools_item, window, cx);
            let telemetry_log_item =
                cx.new(|cx| telemetry_log::TelemetryLogToolbarItemView::new(window, cx));
            toolbar.add_item(telemetry_log_item, window, cx);
            let syntax_tree_item = cx.new(|_| language_tools::SyntaxTreeToolbarItemView::new());
            toolbar.add_item(syntax_tree_item, window, cx);
            let migration_banner =
                cx.new(|inner_cx| MigrationBanner::new(workspace_handle.clone(), inner_cx));
            toolbar.add_item(migration_banner, window, cx);
            let highlights_tree_item =
                cx.new(|_| language_tools::HighlightsTreeToolbarItemView::new());
            toolbar.add_item(highlights_tree_item, window, cx);
            let project_diff_toolbar = cx.new(|cx| ProjectDiffToolbar::new(workspace, cx));
            toolbar.add_item(project_diff_toolbar, window, cx);
            let branch_diff_toolbar = cx.new(BranchDiffToolbar::new);
            toolbar.add_item(branch_diff_toolbar, window, cx);
            let commit_view_toolbar = cx.new(|_| CommitViewToolbar::new());
            toolbar.add_item(commit_view_toolbar, window, cx);
            let agent_diff_toolbar = cx.new(AgentDiffToolbar::new);
            toolbar.add_item(agent_diff_toolbar, window, cx);
            let basedpyright_banner = cx.new(|cx| BasedPyrightBanner::new(workspace, cx));
            toolbar.add_item(basedpyright_banner, window, cx);
            let image_view_toolbar = cx.new(|_| image_viewer::ImageViewToolbarControls::new());
            toolbar.add_item(image_view_toolbar, window, cx);
        })
    });
}

fn open_about_window(cx: &mut App) {
    fn about_window_icon(release_channel: ReleaseChannel) -> Arc<Image> {
        let bytes = match release_channel {
            ReleaseChannel::Dev => include_bytes!("../resources/app-icon-dev.png").as_slice(),
            ReleaseChannel::Nightly => {
                include_bytes!("../resources/app-icon-nightly.png").as_slice()
            }
            ReleaseChannel::Preview => {
                include_bytes!("../resources/app-icon-preview.png").as_slice()
            }
            ReleaseChannel::Stable => include_bytes!("../resources/app-icon.png").as_slice(),
        };

        Arc::new(Image::from_bytes(ImageFormat::Png, bytes.to_vec()))
    }

    struct AboutWindow {
        focus_handle: FocusHandle,
        ok_entry: NavigableEntry,
        copy_entry: NavigableEntry,
        app_icon: Arc<Image>,
        message: SharedString,
        commit: Option<SharedString>,
        full_version: SharedString,
    }

    impl AboutWindow {
        fn new(cx: &mut Context<Self>) -> Self {
            let release_channel = ReleaseChannel::global(cx);
            let release_channel_name = release_channel.display_name();
            let full_version: SharedString = AppVersion::global(cx).to_string().into();
            let version = env!("CARGO_PKG_VERSION");

            let debug = if cfg!(debug_assertions) {
                "(debug)"
            } else {
                ""
            };
            let message: SharedString = format!("{release_channel_name} {version} {debug}").into();
            let commit = AppCommitSha::try_global(cx)
                .map(|sha| sha.full())
                .filter(|commit| !commit.is_empty())
                .map(SharedString::from);

            Self {
                focus_handle: cx.focus_handle(),
                ok_entry: NavigableEntry::focusable(cx),
                copy_entry: NavigableEntry::focusable(cx),
                app_icon: about_window_icon(release_channel),
                message,
                commit,
                full_version,
            }
        }

        fn copy_details(&self, window: &mut Window, cx: &mut Context<Self>) {
            let content = match self.commit.as_ref() {
                Some(commit) => {
                    format!(
                        "{}\nCommit: {}\nVersion: {}",
                        self.message, commit, self.full_version
                    )
                }
                None => format!("{}\nVersion: {}", self.message, self.full_version),
            };
            cx.write_to_clipboard(ClipboardItem::new_string(content));
            window.remove_window();
        }
    }

    impl Render for AboutWindow {
        fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
            let ok_is_focused = self.ok_entry.focus_handle.contains_focused(window, cx);
            let copy_is_focused = self.copy_entry.focus_handle.contains_focused(window, cx);

            Navigable::new(
                v_flex()
                    .id("about-window")
                    .track_focus(&self.focus_handle)
                    .on_action(cx.listener(|_, _: &menu::Cancel, window, _cx| {
                        window.remove_window();
                    }))
                    .min_w_0()
                    .size_full()
                    .bg(cx.theme().colors().editor_background)
                    .text_color(cx.theme().colors().text)
                    .p_4()
                    .when(cfg!(target_os = "macos"), |this| this.pt_10())
                    .gap_4()
                    .text_center()
                    .justify_between()
                    .child(
                        v_flex()
                            .w_full()
                            .gap_2()
                            .items_center()
                            .child(img(self.app_icon.clone()).size_16().flex_none())
                            .child(Headline::new(self.message.clone()))
                            .when_some(self.commit.clone(), |this, commit| {
                                this.child(
                                    Label::new("Commit")
                                        .color(Color::Muted)
                                        .size(LabelSize::XSmall),
                                )
                                .child(Label::new(commit).size(LabelSize::Small))
                            })
                            .child(
                                Label::new("Version")
                                    .color(Color::Muted)
                                    .size(LabelSize::XSmall),
                            )
                            .child(Label::new(self.full_version.clone()).size(LabelSize::Small)),
                    )
                    .child(
                        h_flex()
                            .w_full()
                            .gap_1()
                            .child(
                                div()
                                    .flex_1()
                                    .track_focus(&self.ok_entry.focus_handle)
                                    .on_action(cx.listener(|_, _: &menu::Confirm, window, _cx| {
                                        window.remove_window();
                                    }))
                                    .child(
                                        Button::new("ok", "Ok")
                                            .full_width()
                                            .style(ButtonStyle::OutlinedGhost)
                                            .toggle_state(ok_is_focused)
                                            .selected_style(ButtonStyle::Tinted(TintColor::Accent))
                                            .on_click(cx.listener(|_, _, window, _cx| {
                                                window.remove_window();
                                            })),
                                    ),
                            )
                            .child(
                                div()
                                    .flex_1()
                                    .track_focus(&self.copy_entry.focus_handle)
                                    .on_action(cx.listener(
                                        |this, _: &menu::Confirm, window, cx| {
                                            this.copy_details(window, cx);
                                        },
                                    ))
                                    .child(
                                        Button::new("copy", "Copy")
                                            .full_width()
                                            .style(ButtonStyle::Tinted(TintColor::Accent))
                                            .toggle_state(copy_is_focused)
                                            .selected_style(ButtonStyle::Tinted(TintColor::Accent))
                                            .on_click(cx.listener(|this, _event, window, cx| {
                                                this.copy_details(window, cx);
                                            })),
                                    ),
                            ),
                    )
                    .into_any_element(),
            )
            .entry(self.ok_entry.clone())
            .entry(self.copy_entry.clone())
        }
    }

    impl Focusable for AboutWindow {
        fn focus_handle(&self, _cx: &App) -> FocusHandle {
            self.ok_entry.focus_handle.clone()
        }
    }

    // Don't open about window twice
    if let Some(existing) = cx
        .windows()
        .into_iter()
        .find_map(|w| w.downcast::<AboutWindow>())
    {
        existing
            .update(cx, |about_window, window, cx| {
                window.activate_window();
                about_window.ok_entry.focus_handle.focus(window, cx);
            })
            .log_err();
        return;
    }

    let window_size = Size {
        width: px(440.),
        height: px(300.),
    };

    cx.open_window(
        WindowOptions {
            titlebar: Some(TitlebarOptions {
                title: Some("About Zed".into()),
                appears_transparent: true,
                traffic_light_position: Some(point(px(12.), px(12.))),
            }),
            window_bounds: Some(WindowBounds::centered(window_size, cx)),
            is_resizable: false,
            is_minimizable: false,
            kind: WindowKind::Normal,
            app_id: Some(ReleaseChannel::global(cx).app_id().to_owned()),
            ..Default::default()
        },
        |window, cx| {
            let about_window = cx.new(AboutWindow::new);
            let focus_handle = about_window.read(cx).ok_entry.focus_handle.clone();
            window.activate_window();
            focus_handle.focus(window, cx);
            about_window
        },
    )
    .log_err();
}

#[cfg(not(target_os = "windows"))]
fn install_cli(
    _: &mut Workspace,
    _: &install_cli::InstallCliBinary,
    window: &mut Window,
    cx: &mut Context<Workspace>,
) {
    install_cli::install_cli_binary(window, cx)
}

static WAITING_QUIT_CONFIRMATION: AtomicBool = AtomicBool::new(false);
fn quit(_: &Quit, cx: &mut App) {
    if WAITING_QUIT_CONFIRMATION.load(atomic::Ordering::Acquire) {
        return;
    }

    let should_confirm = WorkspaceSettings::get_global(cx).confirm_quit;
    cx.spawn(async move |cx| {
        let mut workspace_windows: Vec<WindowHandle<MultiWorkspace>> = cx.update(|cx| {
            cx.windows()
                .into_iter()
                .filter_map(|window| window.downcast::<MultiWorkspace>())
                .collect::<Vec<_>>()
        });

        // If multiple windows have unsaved changes, and need a save prompt,
        // prompt in the active window before switching to a different window.
        cx.update(|cx| {
            workspace_windows.sort_by_key(|window| window.is_active(cx) == Some(false));
        });

        if should_confirm && let Some(multi_workspace) = workspace_windows.first() {
            let answer = multi_workspace
                .update(cx, |_, window, cx| {
                    window.prompt(
                        PromptLevel::Info,
                        "Are you sure you want to quit?",
                        None,
                        &["Quit", "Cancel"],
                        cx,
                    )
                })
                .log_err();

            if let Some(answer) = answer {
                WAITING_QUIT_CONFIRMATION.store(true, atomic::Ordering::Release);
                let answer = answer.await.ok();
                WAITING_QUIT_CONFIRMATION.store(false, atomic::Ordering::Release);
                if answer != Some(0) {
                    return Ok(());
                }
            }
        }

        // If the user cancels any save prompt, then keep the app open.
        for window in &workspace_windows {
            let window = *window;
            let workspaces = window
                .update(cx, |multi_workspace, _, _cx| {
                    multi_workspace.workspaces().cloned().collect::<Vec<_>>()
                })
                .log_err();

            let Some(workspaces) = workspaces else {
                continue;
            };

            for workspace in workspaces {
                if let Some(should_close) = window
                    .update(cx, |multi_workspace, window, cx| {
                        multi_workspace.activate(workspace.clone(), None, window, cx);
                        window.activate_window();
                        workspace.update(cx, |workspace, cx| {
                            workspace.prepare_to_close(CloseIntent::Quit, window, cx)
                        })
                    })
                    .log_err()
                {
                    if !should_close.await? {
                        return Ok(());
                    }
                }
            }
        }
        // Flush all pending workspace serialization before quitting so that
        // session_id/window_id are up-to-date in the database.
        let mut flush_tasks = Vec::new();
        for window in &workspace_windows {
            window
                .update(cx, |multi_workspace, window, cx| {
                    for workspace in multi_workspace.workspaces() {
                        flush_tasks.push(workspace.update(cx, |workspace, cx| {
                            workspace.flush_serialization(window, cx)
                        }));
                    }
                    flush_tasks.append(&mut multi_workspace.take_pending_removal_tasks());
                    flush_tasks.push(multi_workspace.flush_serialization());
                })
                .log_err();
        }
        futures::future::join_all(flush_tasks).await;

        cx.update(|cx| cx.quit());
        anyhow::Ok(())
    })
    .detach_and_log_err(cx);
}

fn open_log_file(workspace: &mut Workspace, window: &mut Window, cx: &mut Context<Workspace>) {
    const MAX_LINES: usize = 1000;
    let app_state = workspace.app_state();
    let languages = app_state.languages.clone();
    let fs = app_state.fs.clone();
    cx.spawn_in(window, async move |workspace, cx| {
        let log = {
            let result = futures::join!(
                fs.load(&paths::old_log_file()),
                fs.load(&paths::log_file()),
                languages.language_for_name("log")
            );
            match result {
                (Err(_), Err(e), _) => Err(e),
                (old_log, new_log, lang) => {
                    let mut lines = VecDeque::with_capacity(MAX_LINES);
                    for line in old_log
                        .iter()
                        .flat_map(|log| log.lines())
                        .chain(new_log.iter().flat_map(|log| log.lines()))
                    {
                        if lines.len() == MAX_LINES {
                            lines.pop_front();
                        }
                        lines.push_back(line);
                    }
                    Ok((
                        lines
                            .into_iter()
                            .flat_map(|line| [line, "\n"])
                            .collect::<String>(),
                        lang.ok(),
                    ))
                }
            }
        };

        let (log, log_language) = match log {
            Ok((log, log_language)) => (log, log_language),
            Err(e) => {
                struct OpenLogError;

                workspace
                    .update(cx, |workspace, cx| {
                        workspace.show_notification(
                            NotificationId::unique::<OpenLogError>(),
                            cx,
                            |cx| {
                                cx.new(|cx| {
                                    MessageNotification::new(
                                        format!(
                                            "Unable to access/open log file at path \
                                                    {}: {e:#}",
                                            paths::log_file().display()
                                        ),
                                        cx,
                                    )
                                })
                            },
                        );
                    })
                    .ok();
                return;
            }
        };
        maybe!(async move {
            let project = workspace
                .read_with(cx, |workspace, _| workspace.project().clone())
                .ok()?;
            let buffer = project
                .update(cx, |project, cx| {
                    project.create_buffer(log_language, false, cx)
                })
                .await
                .ok()?;
            buffer.update(cx, |buffer, cx| {
                buffer.set_capability(Capability::ReadOnly, cx);
                buffer.set_text(log, cx);
            });

            let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx).with_title("Log".into()));

            let editor = cx
                .new_window_entity(|window, cx| {
                    let mut editor = Editor::for_multibuffer(buffer, Some(project), window, cx);
                    editor.set_read_only(true);
                    editor.set_breadcrumb_header(format!(
                        "Last {} lines in {}",
                        MAX_LINES,
                        paths::log_file().display()
                    ));
                    let last_multi_buffer_offset = editor.buffer().read(cx).len(cx);
                    editor.change_selections(Default::default(), window, cx, |s| {
                        s.select_ranges(Some(last_multi_buffer_offset..last_multi_buffer_offset));
                    });
                    editor
                })
                .ok()?;

            workspace
                .update_in(cx, |workspace, window, cx| {
                    workspace.add_item_to_active_pane(Box::new(editor), None, true, window, cx);
                })
                .ok()
        })
        .await;
    })
    .detach();
}

fn notify_settings_errors(result: settings::SettingsParseResult, is_user: bool, cx: &mut App) {
    if let settings::ParseStatus::Failed { error: err } = &result.parse_status {
        let settings_type = if is_user { "user" } else { "global" };
        log::error!("Failed to load {} settings: {err}", settings_type);
    }

    let error = match result.parse_status {
        settings::ParseStatus::Failed { error } => Some(anyhow::format_err!(error)),
        settings::ParseStatus::Success => None,
        settings::ParseStatus::Unchanged => return,
    };
    let id = NotificationId::Named(format!("failed-to-parse-settings-{is_user}").into());

    let showed_parse_error = match error {
        Some(error) => {
            if let Some(InvalidSettingsError::LocalSettings { .. }) =
                error.downcast_ref::<InvalidSettingsError>()
            {
                false
                // Local settings errors are displayed by the projects
            } else {
                show_app_notification(id, cx, move |cx| {
                    cx.new(|cx| {
                        MessageNotification::new(format!("Invalid user settings file\n{error}"), cx)
                            .primary_message("Open Settings File")
                            .primary_icon(IconName::Settings)
                            .primary_on_click(|window, cx| {
                                window.dispatch_action(
                                    zed_actions::OpenSettingsFile.boxed_clone(),
                                    cx,
                                );
                                cx.emit(DismissEvent);
                            })
                    })
                });
                true
            }
        }
        None => {
            dismiss_app_notification(&id, cx);
            false
        }
    };
    let id = NotificationId::Named(format!("failed-to-migrate-settings-{is_user}").into());

    match result.migration_status {
        settings::MigrationStatus::Succeeded | settings::MigrationStatus::NotNeeded => {
            dismiss_app_notification(&id, cx);
        }
        settings::MigrationStatus::Failed { error: err } => {
            if !showed_parse_error {
                show_app_notification(id, cx, move |cx| {
                    cx.new(|cx| {
                        MessageNotification::new(
                            format!(
                                "Failed to migrate settings\n\
                                {err}"
                            ),
                            cx,
                        )
                        .primary_message("Open Settings File")
                        .primary_icon(IconName::Settings)
                        .primary_on_click(|window, cx| {
                            window.dispatch_action(zed_actions::OpenSettingsFile.boxed_clone(), cx);
                            cx.emit(DismissEvent);
                        })
                    })
                });
            }
        }
    };
}

#[derive(Copy, Clone, Debug, settings::RegisterSetting)]
struct CursorHideModeSetting(gpui::CursorHideMode);

impl Settings for CursorHideModeSetting {
    fn from_settings(content: &settings::SettingsContent) -> Self {
        Self(match content.hide_mouse.unwrap_or_default() {
            settings::HideMouseMode::Never => gpui::CursorHideMode::Never,
            settings::HideMouseMode::OnTyping => gpui::CursorHideMode::OnTyping,
            settings::HideMouseMode::OnTypingAndAction => gpui::CursorHideMode::OnTypingAndAction,
        })
    }
}

fn init_cursor_hide_mode(cx: &mut App) {
    let apply = |cx: &mut App| cx.set_cursor_hide_mode(CursorHideModeSetting::get_global(cx).0);
    apply(cx);
    cx.observe_global::<SettingsStore>(apply).detach();
}

pub fn watch_settings_files(fs: Arc<dyn fs::Fs>, cx: &mut App) {
    MigrationNotification::set_global(cx.new(|_| MigrationNotification), cx);

    SettingsStore::update_global(cx, move |store, cx| {
        store.watch_settings_files(fs, cx, |settings_file, result, cx| {
            let is_user = matches!(settings_file, SettingsFile::User);
            let migrating_in_memory =
                matches!(&result.migration_status, MigrationStatus::Succeeded);
            notify_settings_errors(result, is_user, cx);
            if let Some(notifier) = MigrationNotification::try_global(cx) {
                notifier.update(cx, |_, cx| {
                    cx.emit(MigrationEvent::ContentChanged {
                        migration_type: MigrationType::Settings,
                        migrating_in_memory,
                    });
                });
            }
        });
    });
}

pub fn handle_keymap_file_changes(
    mut user_keymap_file_rx: mpsc::UnboundedReceiver<String>,
    user_keymap_watcher: gpui::Task<()>,
    cx: &mut App,
) {
    let (base_keymap_tx, mut base_keymap_rx) = mpsc::unbounded();
    let (keyboard_layout_tx, mut keyboard_layout_rx) = mpsc::unbounded();
    let mut old_base_keymap = *BaseKeymap::get_global(cx);
    let mut old_vim_enabled = VimModeSetting::get_global(cx).0;
    let mut old_helix_enabled = vim_mode_setting::HelixModeSetting::get_global(cx).0;

    cx.observe_global::<SettingsStore>(move |cx| {
        let new_base_keymap = *BaseKeymap::get_global(cx);
        let new_vim_enabled = VimModeSetting::get_global(cx).0;
        let new_helix_enabled = vim_mode_setting::HelixModeSetting::get_global(cx).0;

        if new_base_keymap != old_base_keymap
            || new_vim_enabled != old_vim_enabled
            || new_helix_enabled != old_helix_enabled
        {
            old_base_keymap = new_base_keymap;
            old_vim_enabled = new_vim_enabled;
            old_helix_enabled = new_helix_enabled;

            base_keymap_tx.unbounded_send(()).unwrap();
        }
    })
    .detach();

    #[cfg(target_os = "windows")]
    {
        let mut current_layout_id = cx.keyboard_layout().id().to_string();
        cx.on_keyboard_layout_change(move |cx| {
            let next_layout_id = cx.keyboard_layout().id();
            if next_layout_id != current_layout_id {
                current_layout_id = next_layout_id.to_string();
                keyboard_layout_tx.unbounded_send(()).ok();
            }
        })
        .detach();
    }

    #[cfg(not(target_os = "windows"))]
    {
        let mut current_mapping = cx.keyboard_mapper().get_key_equivalents().cloned();
        cx.on_keyboard_layout_change(move |cx| {
            let next_mapping = cx.keyboard_mapper().get_key_equivalents();
            if current_mapping.as_ref() != next_mapping {
                current_mapping = next_mapping.cloned();
                keyboard_layout_tx.unbounded_send(()).ok();
            }
        })
        .detach();
    }

    load_default_keymap(cx);

    struct KeymapParseErrorNotification;
    let notification_id = NotificationId::unique::<KeymapParseErrorNotification>();

    cx.spawn(async move |cx| {
        let _user_keymap_watcher = user_keymap_watcher;
        let mut user_keymap_content = String::new();
        let mut migrating_in_memory = false;
        loop {
            select_biased! {
                _ = base_keymap_rx.next() => {},
                _ = keyboard_layout_rx.next() => {},
                content = user_keymap_file_rx.next() => {
                    if let Some(content) = content {
                        if let Ok(Some(migrated_content)) = migrate_keymap(&content) {
                            user_keymap_content = migrated_content;
                            migrating_in_memory = true;
                        } else {
                            user_keymap_content = content;
                            migrating_in_memory = false;
                        }
                    }
                }
            };
            cx.update(|cx| {
                if let Some(notifier) = MigrationNotification::try_global(cx) {
                    notifier.update(cx, |_, cx| {
                        cx.emit(MigrationEvent::ContentChanged {
                            migration_type: MigrationType::Keymap,
                            migrating_in_memory,
                        });
                    });
                }
                let load_result = KeymapFile::load(&user_keymap_content, cx);
                match load_result {
                    KeymapFileLoadResult::Success { key_bindings } => {
                        reload_keymaps(cx, key_bindings);
                        dismiss_app_notification(&notification_id.clone(), cx);
                    }
                    KeymapFileLoadResult::SomeFailedToLoad {
                        key_bindings,
                        error_message,
                    } => {
                        if !key_bindings.is_empty() {
                            reload_keymaps(cx, key_bindings);
                        }
                        show_keymap_file_load_error(notification_id.clone(), error_message, cx);
                    }
                    KeymapFileLoadResult::JsonParseFailure { error } => {
                        show_keymap_file_json_error(notification_id.clone(), &error, cx)
                    }
                }
            });
        }
    })
    .detach();
}

fn show_keymap_file_json_error(
    notification_id: NotificationId,
    error: &anyhow::Error,
    cx: &mut App,
) {
    let message: SharedString =
        format!("JSON parse error in keymap file. Bindings not reloaded.\n\n{error}").into();
    show_app_notification(notification_id, cx, move |cx| {
        cx.new(|cx| {
            MessageNotification::new(message.clone(), cx)
                .primary_message("Open Keymap File")
                .primary_icon(IconName::Settings)
                .primary_on_click(|window, cx| {
                    window.dispatch_action(zed_actions::OpenKeymapFile.boxed_clone(), cx);
                    cx.emit(DismissEvent);
                })
        })
    });
}

fn show_keymap_file_load_error(
    notification_id: NotificationId,
    error_message: MarkdownString,
    cx: &mut App,
) {
    show_markdown_app_notification(
        notification_id,
        error_message,
        "Open Keymap File".into(),
        |window, cx| {
            window.dispatch_action(zed_actions::OpenKeymapFile.boxed_clone(), cx);
            cx.emit(DismissEvent);
        },
        cx,
    )
}

fn show_markdown_app_notification<F>(
    notification_id: NotificationId,
    message: MarkdownString,
    primary_button_message: SharedString,
    primary_button_on_click: F,
    cx: &mut App,
) where
    F: 'static + Send + Sync + Fn(&mut Window, &mut Context<MessageNotification>),
{
    let markdown = cx.new(|cx| Markdown::new(message.0.into(), None, None, cx));
    let primary_button_on_click = Arc::new(primary_button_on_click);

    show_app_notification(notification_id, cx, move |cx| {
        let markdown = markdown.clone();
        let primary_button_message = primary_button_message.clone();
        let primary_button_on_click = primary_button_on_click.clone();

        cx.new(move |cx| {
            MessageNotification::new_from_builder(cx, move |window, cx| {
                image_cache(retain_all("notification-cache"))
                    .child(div().text_ui(cx).child(MarkdownElement::new(
                        markdown.clone(),
                        MarkdownStyle::themed(MarkdownFont::Editor, window, cx),
                    )))
                    .into_any()
            })
            .primary_message(primary_button_message)
            .primary_icon(IconName::Settings)
            .primary_on_click_arc(primary_button_on_click)
        })
    })
}

fn reload_keymaps(cx: &mut App, mut user_key_bindings: Vec<KeyBinding>) {
    cx.clear_key_bindings();
    load_default_keymap(cx);

    for key_binding in &mut user_key_bindings {
        key_binding.set_meta(KeybindSource::User.meta());
    }
    cx.bind_keys(user_key_bindings);

    let menus = app_menus(cx);
    cx.set_menus(menus);
    // On Windows, this is set in the `update_jump_list` method of the `HistoryManager`.
    #[cfg(not(target_os = "windows"))]
    cx.set_dock_menu(vec![gpui::MenuItem::action(
        "New Window",
        workspace::NewWindow,
    )]);
    // todo: nicer api here?
    keymap_editor::KeymapEventChannel::trigger_keymap_changed(cx);
}

pub fn load_default_keymap(cx: &mut App) {
    let base_keymap = *BaseKeymap::get_global(cx);
    if base_keymap == BaseKeymap::None {
        return;
    }

    cx.bind_keys(
        KeymapFile::load_asset(DEFAULT_KEYMAP_PATH, Some(KeybindSource::Default), cx).unwrap(),
    );

    if let Some(asset_path) = base_keymap.asset_path() {
        cx.bind_keys(KeymapFile::load_asset(asset_path, Some(KeybindSource::Base), cx).unwrap());
    }

    if VimModeSetting::get_global(cx).0 || vim_mode_setting::HelixModeSetting::get_global(cx).0 {
        cx.bind_keys(
            KeymapFile::load_asset(VIM_KEYMAP_PATH, Some(KeybindSource::Vim), cx).unwrap(),
        );
    }
}

pub fn open_new_ssh_project_from_project(
    workspace: &mut Workspace,
    paths: Vec<PathBuf>,
    window: &mut Window,
    cx: &mut Context<Workspace>,
) -> Task<anyhow::Result<()>> {
    let app_state = workspace.app_state().clone();
    let Some(ssh_client) = workspace.project().read(cx).remote_client() else {
        return Task::ready(Err(anyhow::anyhow!("Not an ssh project")));
    };
    let connection_options = ssh_client.read(cx).connection_options();
    cx.spawn_in(window, async move |_, cx| {
        open_remote_project(
            connection_options,
            paths,
            app_state,
            workspace::OpenOptions {
                workspace_matching: workspace::WorkspaceMatching::None,
                ..Default::default()
            },
            cx,
        )
        .await
        .map(|_| ())
    })
}

fn open_project_settings_file(
    workspace: &mut Workspace,
    _: &OpenProjectSettingsFile,
    window: &mut Window,
    cx: &mut Context<Workspace>,
) {
    open_local_file(
        workspace,
        local_settings_file_relative_path(),
        initial_project_settings_content(),
        window,
        cx,
    )
}

fn open_project_tasks_file(
    workspace: &mut Workspace,
    _: &OpenProjectTasks,
    window: &mut Window,
    cx: &mut Context<Workspace>,
) {
    open_local_file(
        workspace,
        local_tasks_file_relative_path(),
        initial_tasks_content(),
        window,
        cx,
    )
}

fn open_project_debug_tasks_file(
    workspace: &mut Workspace,
    _: &zed_actions::OpenProjectDebugTasks,
    window: &mut Window,
    cx: &mut Context<Workspace>,
) {
    open_local_file(
        workspace,
        local_debug_file_relative_path(),
        initial_local_debug_tasks_content(),
        window,
        cx,
    )
}

fn open_local_file(
    workspace: &mut Workspace,
    settings_relative_path: &'static RelPath,
    initial_contents: Cow<'static, str>,
    window: &mut Window,
    cx: &mut Context<Workspace>,
) {
    let project = workspace.project().clone();
    let worktree = project
        .read(cx)
        .visible_worktrees(cx)
        .find_map(|tree| tree.read(cx).root_entry()?.is_dir().then_some(tree));
    if let Some(worktree) = worktree {
        let tree_id = worktree.read(cx).id();
        cx.spawn_in(window, async move |workspace, cx| {
            // Check if the file actually exists on disk (even if it's excluded from worktree)
            let file_exists = {
                let full_path = worktree.read_with(cx, |tree, _| {
                    tree.abs_path().join(settings_relative_path.as_std_path())
                });

                let fs = project.read_with(cx, |project, _| project.fs().clone());

                fs.metadata(&full_path)
                    .await
                    .ok()
                    .flatten()
                    .is_some_and(|metadata| !metadata.is_dir && !metadata.is_fifo)
            };

            if !file_exists {
                if let Some(dir_path) = settings_relative_path.parent()
                    && worktree.read_with(cx, |tree, _| tree.entry_for_path(dir_path).is_none())
                {
                    project
                        .update(cx, |project, cx| {
                            project.create_entry((tree_id, dir_path), true, cx)
                        })
                        .await
                        .context("worktree was removed")?;
                }

                if worktree.read_with(cx, |tree, _| {
                    tree.entry_for_path(settings_relative_path).is_none()
                }) {
                    project
                        .update(cx, |project, cx| {
                            project.create_entry((tree_id, settings_relative_path), false, cx)
                        })
                        .await
                        .context("worktree was removed")?;
                }
            }

            let editor = workspace
                .update_in(cx, |workspace, window, cx| {
                    workspace.open_path((tree_id, settings_relative_path), None, true, window, cx)
                })?
                .await?
                .downcast::<Editor>()
                .context("unexpected item type: expected editor item")?;

            editor
                .downgrade()
                .update(cx, |editor, cx| {
                    if let Some(buffer) = editor.buffer().read(cx).as_singleton()
                        && buffer.read(cx).is_empty()
                    {
                        buffer.update(cx, |buffer, cx| {
                            buffer.edit([(0..0, initial_contents)], None, cx)
                        });
                    }
                })
                .ok();

            anyhow::Ok(())
        })
        .detach();
    } else {
        struct NoOpenFolders;

        workspace.show_notification(NotificationId::unique::<NoOpenFolders>(), cx, |cx| {
            cx.new(|cx| MessageNotification::new("This project has no folders open.", cx))
        })
    }
}

fn open_bundled_file(
    workspace: &mut Workspace,
    text: Cow<'static, str>,
    title: &'static str,
    language: &'static str,
    window: &mut Window,
    cx: &mut Context<Workspace>,
) {
    let existing = workspace.items_of_type::<Editor>(cx).find(|editor| {
        editor.read_with(cx, |editor, cx| {
            editor.read_only(cx)
                && editor.title(cx).as_ref() == title
                && editor
                    .buffer()
                    .read(cx)
                    .as_singleton()
                    .is_some_and(|buffer| buffer.read(cx).file().is_none())
        })
    });
    if let Some(existing) = existing {
        workspace.activate_item(&existing, true, true, window, cx);
        return;
    }

    let language = workspace.app_state().languages.language_for_name(language);
    cx.spawn_in(window, async move |workspace, cx| {
        let language = language.await.log_err();
        workspace
            .update_in(cx, move |workspace, window, cx| {
                let project = workspace.project().clone();
                let buffer = project.update(cx, move |project, cx| {
                    project.create_buffer(language, false, cx)
                });
                cx.spawn_in(window, async move |workspace, cx| {
                    let buffer = buffer.await?;
                    buffer.update(cx, |buffer, cx| {
                        buffer.set_text(text.into_owned(), cx);
                        buffer.set_capability(Capability::ReadOnly, cx);
                    });
                    let buffer =
                        cx.new(|cx| MultiBuffer::singleton(buffer, cx).with_title(title.into()));
                    workspace.update_in(cx, |workspace, window, cx| {
                        workspace.add_item_to_active_pane(
                            Box::new(cx.new(|cx| {
                                let mut editor = Editor::for_multibuffer(
                                    buffer,
                                    Some(project.clone()),
                                    window,
                                    cx,
                                );
                                editor.set_read_only(true);
                                editor.set_should_serialize(false, cx);
                                editor.set_breadcrumb_header(title.into());
                                editor
                            })),
                            None,
                            true,
                            window,
                            cx,
                        )
                    })
                })
            })?
            .await
    })
    .detach_and_log_err(cx);
}

fn open_settings_file(
    abs_path: &'static Path,
    default_content: impl FnOnce() -> Rope + Send + 'static,
    window: &mut Window,
    cx: &mut Context<Workspace>,
) {
    cx.spawn_in(window, async move |workspace, cx| {
        workspace
            .update_in(cx, |workspace, window, cx| {
                workspace.with_local_or_wsl_workspace(window, cx, move |workspace, window, cx| {
                    let project = workspace.project().clone();

                    cx.spawn_in(window, async move |workspace, cx| {
                        let config_dir = project
                            .update(cx, |project, cx| {
                                project.try_windows_path_to_wsl(paths::config_dir().as_path(), cx)
                            })
                            .await?;
                        // Set up a dedicated worktree for settings, since
                        // otherwise we're dropping and re-starting LSP servers
                        // for each file inside on every settings file
                        // close/open

                        // TODO: Do note that all other external files (e.g.
                        // drag and drop from OS) still have their worktrees
                        // released on file close, causing LSP servers'
                        // restarts.
                        let (_worktree, _) = project
                            .update(cx, |project, cx| {
                                project.find_or_create_worktree(&config_dir, false, cx)
                            })
                            .await?;

                        workspace
                            .update_in(cx, |_, window, cx| {
                                create_and_open_local_file(abs_path, window, cx, default_content)
                            })?
                            .await?;
                        anyhow::Ok(())
                    })
                })
            })?
            .await?
            .await?;
        anyhow::Ok(())
    })
    .detach_and_log_err(cx);
}

/// Eagerly loads the active theme and icon theme based on the selections in the
/// theme settings.
///
/// This fast path exists to load these themes as soon as possible so the user
/// doesn't see the default themes while waiting on extensions to load.
pub(crate) fn eager_load_active_theme_and_icon_theme(fs: Arc<dyn Fs>, cx: &mut App) {
    let extension_store = ExtensionStore::global(cx);
    let theme_registry = ThemeRegistry::global(cx);
    let theme_settings = ThemeSettings::get_global(cx);
    let appearance = SystemAppearance::global(cx).0;

    enum LoadTarget {
        Theme(PathBuf),
        IconTheme((PathBuf, PathBuf)),
    }

    let theme_name = theme_settings.theme.name(appearance);
    let icon_theme_name = theme_settings.icon_theme.name(appearance);
    let themes_to_load = [
        theme_registry
            .get(&theme_name.0)
            .is_err()
            .then(|| {
                extension_store
                    .read(cx)
                    .path_to_extension_theme(&theme_name.0)
            })
            .flatten()
            .map(LoadTarget::Theme),
        theme_registry
            .get_icon_theme(&icon_theme_name.0)
            .is_err()
            .then(|| {
                extension_store
                    .read(cx)
                    .path_to_extension_icon_theme(&icon_theme_name.0)
            })
            .flatten()
            .map(LoadTarget::IconTheme),
    ];

    enum ReloadTarget {
        Theme,
        IconTheme,
    }

    let executor = cx.background_executor();
    let reload_tasks = parking_lot::Mutex::new(Vec::with_capacity(themes_to_load.len()));

    let mut themes_to_load = themes_to_load.into_iter().flatten().peekable();

    if themes_to_load.peek().is_none() {
        return;
    }

    cx.foreground_executor().block_on(executor.scoped(|scope| {
        for load_target in themes_to_load {
            let theme_registry = &theme_registry;
            let reload_tasks = &reload_tasks;
            let fs = fs.clone();

            scope.spawn(async move {
                match load_target {
                    LoadTarget::Theme(theme_path) => {
                        if let Some(bytes) = fs.load_bytes(&theme_path).await.log_err()
                            && load_user_theme(theme_registry, &bytes).log_err().is_some()
                        {
                            reload_tasks.lock().push(ReloadTarget::Theme);
                        }
                    }
                    LoadTarget::IconTheme((icon_theme_path, icons_root_path)) => {
                        if let Some(bytes) = fs.load_bytes(&icon_theme_path).await.log_err()
                            && let Some(icon_theme_family) =
                                deserialize_icon_theme(&bytes).log_err()
                            && theme_registry
                                .load_icon_theme(icon_theme_family, &icons_root_path)
                                .log_err()
                                .is_some()
                        {
                            reload_tasks.lock().push(ReloadTarget::IconTheme);
                        }
                    }
                }
            });
        }
    }));

    for reload_target in reload_tasks.into_inner() {
        match reload_target {
            ReloadTarget::Theme => theme_settings::reload_theme(cx),
            ReloadTarget::IconTheme => theme_settings::reload_icon_theme(cx),
        };
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use assets::Assets;
    use collections::HashSet;
    use editor::{
        DisplayPoint, Editor, MultiBufferOffset, SelectionEffects, display_map::DisplayRow,
    };
    use gpui::{
        Action, AnyWindowHandle, App, AssetSource, BorrowAppContext, Modifiers, TestAppContext,
        UpdateGlobal, VisualTestContext, WindowHandle, actions, point, px,
    };
    use language::LanguageRegistry;
    use languages::{markdown_lang, rust_lang};
    use pretty_assertions::{assert_eq, assert_ne};
    use project::{Project, ProjectPath};
    use prompt_store::PromptBuilder;
    use semver::Version;
    use serde_json::json;
    use settings::{SaturatingBool, SettingsStore, watch_config_file};
    use std::{
        path::{Path, PathBuf},
        sync::Arc,
        time::Duration,
    };
    use theme::ThemeRegistry;
    use util::{
        path,
        rel_path::{RelPath, rel_path},
    };
    use workspace::MultiWorkspace;
    use workspace::{
        NewFile, OpenOptions, OpenVisible, SERIALIZATION_THROTTLE_TIME, SaveIntent, SplitDirection,
        WorkspaceHandle,
        item::SaveOptions,
        item::{Item, ItemHandle},
        open_new, open_paths, pane,
    };

    async fn flush_workspace_serialization(
        window: &WindowHandle<MultiWorkspace>,
        cx: &mut TestAppContext,
    ) {
        let all_tasks = window
            .update(cx, |multi_workspace, window, cx| {
                let mut tasks = multi_workspace
                    .workspaces()
                    .map(|workspace| {
                        workspace.update(cx, |workspace, cx| {
                            workspace.flush_serialization(window, cx)
                        })
                    })
                    .collect::<Vec<_>>();
                tasks.push(multi_workspace.flush_serialization());
                tasks
            })
            .unwrap();

        futures::future::join_all(all_tasks).await;
    }

    #[gpui::test]
    async fn test_open_non_existing_file(cx: &mut TestAppContext) {
        let app_state = init_test(cx);
        app_state
            .fs
            .as_fake()
            .insert_tree(
                path!("/root"),
                json!({
                    "a": {
                    },
                }),
            )
            .await;

        cx.update(|cx| {
            open_paths(
                &[PathBuf::from(path!("/root/a/new"))],
                app_state.clone(),
                workspace::OpenOptions::default(),
                cx,
            )
        })
        .await
        .unwrap();
        assert_eq!(cx.read(|cx| cx.windows().len()), 1);

        let multi_workspace = cx.windows()[0].downcast::<MultiWorkspace>().unwrap();
        multi_workspace
            .update(cx, |multi_workspace, _, cx| {
                multi_workspace.workspace().update(cx, |workspace, cx| {
                    assert!(workspace.active_item_as::<Editor>(cx).is_some())
                });
            })
            .unwrap();
    }

    #[gpui::test]
    async fn test_open_paths_action(cx: &mut TestAppContext) {
        let app_state = init_test(cx);
        app_state
            .fs
            .as_fake()
            .insert_tree(
                path!("/root"),
                json!({
                    "a": {
                        "aa": null,
                        "ab": null,
                    },
                    "b": {
                        "ba": null,
                        "bb": null,
                    },
                    "c": {
                        "ca": null,
                        "cb": null,
                    },
                    "d": {
                        "da": null,
                        "db": null,
                    },
                    "e": {
                        "ea": null,
                        "eb": null,
                    }
                }),
            )
            .await;

        cx.update(|cx| {
            open_paths(
                &[
                    PathBuf::from(path!("/root/a")),
                    PathBuf::from(path!("/root/b")),
                ],
                app_state.clone(),
                workspace::OpenOptions::default(),
                cx,
            )
        })
        .await
        .unwrap();
        assert_eq!(cx.read(|cx| cx.windows().len()), 1);

        cx.update(|cx| {
            open_paths(
                &[PathBuf::from(path!("/root/a"))],
                app_state.clone(),
                workspace::OpenOptions::default(),
                cx,
            )
        })
        .await
        .unwrap();
        assert_eq!(cx.read(|cx| cx.windows().len()), 1);
        let multi_workspace_1 = cx
            .read(|cx| cx.windows()[0].downcast::<MultiWorkspace>())
            .unwrap();
        cx.run_until_parked();
        multi_workspace_1
            .update(cx, |multi_workspace, window, cx| {
                multi_workspace.workspace().update(cx, |workspace, cx| {
                    assert_eq!(workspace.worktrees(cx).count(), 2);
                    assert!(workspace.right_dock().read(cx).is_open());
                    assert!(
                        workspace
                            .active_pane()
                            .read(cx)
                            .focus_handle(cx)
                            .is_focused(window)
                    );
                });
            })
            .unwrap();

        cx.update(|cx| {
            open_paths(
                &[
                    PathBuf::from(path!("/root/c")),
                    PathBuf::from(path!("/root/d")),
                ],
                app_state.clone(),
                workspace::OpenOptions::default(),
                cx,
            )
        })
        .await
        .unwrap();
        assert_eq!(cx.read(|cx| cx.windows().len()), 1);
        cx.run_until_parked();
        multi_workspace_1
            .update(cx, |multi_workspace, _window, cx| {
                assert_eq!(multi_workspace.workspaces().count(), 2);
                assert!(multi_workspace.sidebar_open());
                let workspace = multi_workspace.workspace().read(cx);
                assert_eq!(
                    workspace
                        .worktrees(cx)
                        .map(|w| w.read(cx).abs_path())
                        .collect::<Vec<_>>(),
                    &[
                        Path::new(path!("/root/c")).into(),
                        Path::new(path!("/root/d")).into(),
                    ]
                );
            })
            .unwrap();

        // Opening with -n (reuse_worktrees: false) still creates a new window.
        cx.update(|cx| {
            open_paths(
                &[PathBuf::from(path!("/root/e"))],
                app_state,
                workspace::OpenOptions {
                    workspace_matching: workspace::WorkspaceMatching::None,
                    ..Default::default()
                },
                cx,
            )
        })
        .await
        .unwrap();
        cx.background_executor.run_until_parked();
        assert_eq!(cx.read(|cx| cx.windows().len()), 2);
    }

    #[gpui::test]
    async fn test_open_add_new(cx: &mut TestAppContext) {
        let app_state = init_test(cx);
        app_state
            .fs
            .as_fake()
            .insert_tree(
                path!("/root"),
                json!({"a": "hey", "b": "", "dir": {"c": "f"}}),
            )
            .await;

        cx.update(|cx| {
            open_paths(
                &[PathBuf::from(path!("/root/dir"))],
                app_state.clone(),
                workspace::OpenOptions::default(),
                cx,
            )
        })
        .await
        .unwrap();
        assert_eq!(cx.update(|cx| cx.windows().len()), 1);

        cx.update(|cx| {
            open_paths(
                &[PathBuf::from(path!("/root/a"))],
                app_state.clone(),
                workspace::OpenOptions {
                    workspace_matching: workspace::WorkspaceMatching::MatchSubdirectory,
                    ..Default::default()
                },
                cx,
            )
        })
        .await
        .unwrap();
        assert_eq!(cx.update(|cx| cx.windows().len()), 1);

        // Opening a file inside the existing worktree with -n creates a new window.
        cx.update(|cx| {
            open_paths(
                &[PathBuf::from(path!("/root/dir/c"))],
                app_state.clone(),
                workspace::OpenOptions {
                    workspace_matching: workspace::WorkspaceMatching::None,
                    ..Default::default()
                },
                cx,
            )
        })
        .await
        .unwrap();
        assert_eq!(cx.update(|cx| cx.windows().len()), 2);

        // Opening a path NOT in any existing worktree with -n creates a new window.
        cx.update(|cx| {
            open_paths(
                &[PathBuf::from(path!("/root/b"))],
                app_state.clone(),
                workspace::OpenOptions {
                    workspace_matching: workspace::WorkspaceMatching::None,
                    ..Default::default()
                },
                cx,
            )
        })
        .await
        .unwrap();
        assert_eq!(cx.update(|cx| cx.windows().len()), 3);
    }

    #[gpui::test]
    async fn test_open_file_in_many_spaces(cx: &mut TestAppContext) {
        let app_state = init_test(cx);
        app_state
            .fs
            .as_fake()
            .insert_tree(
                path!("/root"),
                json!({"dir1": {"a": "b"}, "dir2": {"c": "d"}}),
            )
            .await;

        cx.update(|cx| {
            open_paths(
                &[PathBuf::from(path!("/root/dir1/a"))],
                app_state.clone(),
                workspace::OpenOptions::default(),
                cx,
            )
        })
        .await
        .unwrap();
        assert_eq!(cx.update(|cx| cx.windows().len()), 1);

        cx.update(|cx| {
            open_paths(
                &[PathBuf::from(path!("/root/dir2/c"))],
                app_state.clone(),
                workspace::OpenOptions::default(),
                cx,
            )
        })
        .await
        .unwrap();
        assert_eq!(cx.update(|cx| cx.windows().len()), 1);

        // Opening a directory with default options adds to the existing window
        // rather than creating a new one.
        cx.update(|cx| {
            open_paths(
                &[PathBuf::from(path!("/root/dir2"))],
                app_state.clone(),
                workspace::OpenOptions::default(),
                cx,
            )
        })
        .await
        .unwrap();
        assert_eq!(cx.update(|cx| cx.windows().len()), 1);

        // Opening a directory already in a worktree with -n creates a new window.
        cx.update(|cx| {
            open_paths(
                &[PathBuf::from(path!("/root/dir2"))],
                app_state.clone(),
                workspace::OpenOptions {
                    workspace_matching: workspace::WorkspaceMatching::None,
                    ..Default::default()
                },
                cx,
            )
        })
        .await
        .unwrap();
        assert_eq!(cx.update(|cx| cx.windows().len()), 2);

        // Opening a directory NOT in any worktree with -n creates a new window.
        cx.update(|cx| {
            open_paths(
                &[PathBuf::from(path!("/root"))],
                app_state.clone(),
                workspace::OpenOptions {
                    workspace_matching: workspace::WorkspaceMatching::None,
                    ..Default::default()
                },
                cx,
            )
        })
        .await
        .unwrap();
        assert_eq!(cx.update(|cx| cx.windows().len()), 3);
    }

    #[gpui::test]
    async fn test_window_edit_state_restoring_disabled(cx: &mut TestAppContext) {
        let executor = cx.executor();
        let app_state = init_test(cx);

        cx.update(|cx| {
            SettingsStore::update_global(cx, |store, cx| {
                store.update_user_settings(cx, |settings| {
                    settings
                        .session
                        .get_or_insert_default()
                        .restore_unsaved_buffers = Some(false)
                });
            });
        });

        app_state
            .fs
            .as_fake()
            .insert_tree(path!("/root"), json!({"a": "hey"}))
            .await;

        cx.update(|cx| {
            open_paths(
                &[PathBuf::from(path!("/root/a"))],
                app_state.clone(),
                workspace::OpenOptions::default(),
                cx,
            )
        })
        .await
        .unwrap();
        assert_eq!(cx.update(|cx| cx.windows().len()), 1);

        // When opening the workspace, the window is not in a edited state.
        let window = cx.update(|cx| cx.windows()[0].downcast::<MultiWorkspace>().unwrap());

        let window_is_edited = |window: WindowHandle<MultiWorkspace>, cx: &mut TestAppContext| {
            cx.update(|cx| window.read(cx).unwrap().workspace().read(cx).is_edited())
        };
        let pane = window
            .read_with(cx, |multi_workspace, cx| {
                multi_workspace.workspace().read(cx).active_pane().clone()
            })
            .unwrap();
        let editor = window
            .read_with(cx, |multi_workspace, cx| {
                multi_workspace
                    .workspace()
                    .read(cx)
                    .active_item(cx)
                    .unwrap()
                    .downcast::<Editor>()
                    .unwrap()
            })
            .unwrap();

        assert!(!window_is_edited(window, cx));

        // Editing a buffer marks the window as edited.
        window
            .update(cx, |_, window, cx| {
                editor.update(cx, |editor, cx| editor.insert("EDIT", window, cx));
            })
            .unwrap();

        assert!(window_is_edited(window, cx));

        // Undoing the edit restores the window's edited state.
        window
            .update(cx, |_, window, cx| {
                editor.update(cx, |editor, cx| {
                    editor.undo(&Default::default(), window, cx)
                });
            })
            .unwrap();
        assert!(!window_is_edited(window, cx));

        // Redoing the edit marks the window as edited again.
        window
            .update(cx, |_, window, cx| {
                editor.update(cx, |editor, cx| {
                    editor.redo(&Default::default(), window, cx)
                });
            })
            .unwrap();
        assert!(window_is_edited(window, cx));
        let weak = editor.downgrade();

        // Closing the item restores the window's edited state.
        let close = window
            .update(cx, |_, window, cx| {
                pane.update(cx, |pane, cx| {
                    drop(editor);
                    pane.close_active_item(&Default::default(), window, cx)
                })
            })
            .unwrap();
        executor.run_until_parked();

        cx.simulate_prompt_answer("Don't Save");
        close.await.unwrap();

        // Advance the clock to ensure that the item has been serialized and dropped from the queue
        cx.executor().advance_clock(Duration::from_secs(1));

        weak.assert_released();
        assert!(!window_is_edited(window, cx));
        // Opening the buffer again doesn't impact the window's edited state.
        cx.update(|cx| {
            open_paths(
                &[PathBuf::from(path!("/root/a"))],
                app_state,
                workspace::OpenOptions::default(),
                cx,
            )
        })
        .await
        .unwrap();
        executor.run_until_parked();

        window
            .update(cx, |multi_workspace, _, cx| {
                multi_workspace.workspace().update(cx, |workspace, cx| {
                    let editor = workspace
                        .active_item(cx)
                        .unwrap()
                        .downcast::<Editor>()
                        .unwrap();

                    editor.update(cx, |editor, cx| {
                        assert_eq!(editor.text(cx), "hey");
                    });
                });
            })
            .unwrap();

        let editor = window
            .read_with(cx, |multi_workspace, cx| {
                multi_workspace
                    .workspace()
                    .read(cx)
                    .active_item(cx)
                    .unwrap()
                    .downcast::<Editor>()
                    .unwrap()
            })
            .unwrap();
        assert!(!window_is_edited(window, cx));

        // Editing the buffer marks the window as edited.
        window
            .update(cx, |_, window, cx| {
                editor.update(cx, |editor, cx| editor.insert("EDIT", window, cx));
            })
            .unwrap();
        executor.run_until_parked();
        assert!(window_is_edited(window, cx));

        // Ensure closing the window via the mouse gets preempted due to the
        // buffer having unsaved changes.
        assert!(!VisualTestContext::from_window(window.into(), cx).simulate_close());
        executor.run_until_parked();
        assert_eq!(cx.update(|cx| cx.windows().len()), 1);

        // The window is successfully closed after the user dismisses the prompt.
        cx.simulate_prompt_answer("Don't Save");
        executor.run_until_parked();
        assert_eq!(cx.update(|cx| cx.windows().len()), 0);
    }

    #[ignore = "This test has timing issues across platforms."]
    #[gpui::test]
    async fn test_window_edit_state_restoring_enabled(cx: &mut TestAppContext) {
        let app_state = init_test(cx);
        app_state
            .fs
            .as_fake()
            .insert_tree(path!("/root"), json!({"a": "hey"}))
            .await;

        cx.update(|cx| {
            open_paths(
                &[PathBuf::from(path!("/root/a"))],
                app_state.clone(),
                workspace::OpenOptions::default(),
                cx,
            )
        })
        .await
        .unwrap();

        assert_eq!(cx.update(|cx| cx.windows().len()), 1);

        // When opening the workspace, the window is not in a edited state.
        let window = cx.update(|cx| cx.windows()[0].downcast::<MultiWorkspace>().unwrap());

        let window_is_edited = |window: WindowHandle<MultiWorkspace>, cx: &mut TestAppContext| {
            cx.update(|cx| window.read(cx).unwrap().workspace().read(cx).is_edited())
        };
        let workspace_database_id = |window: WindowHandle<MultiWorkspace>,
                                     cx: &mut TestAppContext| {
            cx.update(|cx| window.read(cx).unwrap().workspace().read(cx).database_id())
        };

        let editor = window
            .read_with(cx, |multi_workspace, cx| {
                multi_workspace
                    .workspace()
                    .read(cx)
                    .active_item(cx)
                    .unwrap()
                    .downcast::<Editor>()
                    .unwrap()
            })
            .unwrap();

        assert!(!window_is_edited(window, cx));
        let initial_database_id = workspace_database_id(window, cx);
        assert!(
            initial_database_id.is_some(),
            "a restored workspace must have a stable database id"
        );

        // Editing a buffer marks the window as edited.
        window
            .update(cx, |_, window, cx| {
                editor.update(cx, |editor, cx| editor.insert("EDIT", window, cx));
            })
            .unwrap();
        cx.run_until_parked();

        assert!(window_is_edited(window, cx));

        // Advance the clock to make sure the workspace is serialized
        cx.executor().advance_clock(Duration::from_secs(1));

        // When closing the window, no prompt shows up and the window is closed.
        // buffer having unsaved changes.
        assert!(!VisualTestContext::from_window(window.into(), cx).simulate_close());
        cx.run_until_parked();
        assert_eq!(cx.update(|cx| cx.windows().len()), 0);

        // When we now reopen the window, the edited state and the edited buffer are back
        cx.update(|cx| {
            open_paths(
                &[PathBuf::from(path!("/root/a"))],
                app_state.clone(),
                workspace::OpenOptions::default(),
                cx,
            )
        })
        .await
        .unwrap();

        assert_eq!(cx.update(|cx| cx.windows().len()), 1);
        assert!(cx.update(|cx| cx.active_window().is_some()));

        cx.run_until_parked();

        // When opening the workspace, the window is not in a edited state.
        let window = cx.update(|cx| {
            cx.active_window()
                .unwrap()
                .downcast::<MultiWorkspace>()
                .unwrap()
        });
        assert!(window_is_edited(window, cx));
        assert_eq!(
            workspace_database_id(window, cx),
            initial_database_id,
            "the workspace must keep the same database id across a close/reopen cycle"
        );

        window
            .update(cx, |multi_workspace, _, cx| {
                multi_workspace.workspace().update(cx, |workspace, cx| {
                    let editor = workspace
                        .active_item(cx)
                        .unwrap()
                        .downcast::<editor::Editor>()
                        .unwrap();
                    editor.update(cx, |editor, cx| {
                        assert_eq!(editor.text(cx), "EDIThey");
                        assert!(editor.is_dirty(cx));
                    });
                });
            })
            .unwrap();
    }

    #[gpui::test]
    async fn test_new_empty_workspace(cx: &mut TestAppContext) {
        let app_state = init_test(cx);
        cx.update(|cx| {
            open_new(
                Default::default(),
                app_state.clone(),
                cx,
                |workspace, window, cx| {
                    Editor::new_file(workspace, &Default::default(), window, cx)
                },
            )
        })
        .await
        .unwrap();
        cx.run_until_parked();

        let multi_workspace = cx
            .update(|cx| cx.windows().first().unwrap().downcast::<MultiWorkspace>())
            .unwrap();

        let editor = multi_workspace
            .update(cx, |multi_workspace, _, cx| {
                multi_workspace.workspace().update(cx, |workspace, cx| {
                    let editor = workspace
                        .active_item(cx)
                        .unwrap()
                        .downcast::<editor::Editor>()
                        .unwrap();
                    editor.update(cx, |editor, cx| {
                        assert!(editor.text(cx).is_empty());
                        assert!(!editor.is_dirty(cx));
                    });

                    editor
                })
            })
            .unwrap();

        let save_task = multi_workspace
            .update(cx, |multi_workspace, window, cx| {
                multi_workspace.workspace().update(cx, |workspace, cx| {
                    workspace.save_active_item(SaveIntent::Save, window, cx)
                })
            })
            .unwrap();
        app_state.fs.create_dir(Path::new("/root")).await.unwrap();
        cx.background_executor.run_until_parked();
        cx.simulate_new_path_selection(|_| Some(PathBuf::from("/root/the-new-name")));
        save_task.await.unwrap();
        multi_workspace
            .update(cx, |_, _, cx| {
                editor.update(cx, |editor, cx| {
                    assert!(!editor.is_dirty(cx));
                    assert_eq!(editor.title(cx), "the-new-name");
                });
            })
            .unwrap();
    }

    #[gpui::test]
    async fn test_open_entry(cx: &mut TestAppContext) {
        let app_state = init_test(cx);
        app_state
            .fs
            .as_fake()
            .insert_tree(
                path!("/root"),
                json!({
                    "a": {
                        "file1": "contents 1",
                        "file2": "contents 2",
                        "file3": "contents 3",
                    },
                }),
            )
            .await;

        let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
        project.update(cx, |project, _cx| project.languages().add(markdown_lang()));
        let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project, window, cx));
        let workspace = window
            .read_with(cx, |mw, _| mw.workspace().clone())
            .unwrap();

        let entries = cx.read(|cx| workspace.file_project_paths(cx));
        let file1 = entries[0].clone();
        let file2 = entries[1].clone();
        let file3 = entries[2].clone();

        // Open the first entry
        let entry_1 = window
            .update(cx, |_, window, cx| {
                workspace.update(cx, |w, cx| {
                    w.open_path(file1.clone(), None, true, window, cx)
                })
            })
            .unwrap()
            .await
            .unwrap();
        cx.read(|cx| {
            let pane = workspace.read(cx).active_pane().read(cx);
            assert_eq!(
                pane.active_item().unwrap().project_path(cx),
                Some(file1.clone())
            );
            assert_eq!(pane.items_len(), 1);
        });

        // Open the second entry
        window
            .update(cx, |_, window, cx| {
                workspace.update(cx, |w, cx| {
                    w.open_path(file2.clone(), None, true, window, cx)
                })
            })
            .unwrap()
            .await
            .unwrap();
        cx.read(|cx| {
            let pane = workspace.read(cx).active_pane().read(cx);
            assert_eq!(
                pane.active_item().unwrap().project_path(cx),
                Some(file2.clone())
            );
            assert_eq!(pane.items_len(), 2);
        });

        // Open the first entry again. The existing pane item is activated.
        let entry_1b = window
            .update(cx, |_, window, cx| {
                workspace.update(cx, |w, cx| {
                    w.open_path(file1.clone(), None, true, window, cx)
                })
            })
            .unwrap()
            .await
            .unwrap();
        assert_eq!(entry_1.item_id(), entry_1b.item_id());

        cx.read(|cx| {
            let pane = workspace.read(cx).active_pane().read(cx);
            assert_eq!(
                pane.active_item().unwrap().project_path(cx),
                Some(file1.clone())
            );
            assert_eq!(pane.items_len(), 2);
        });

        // Split the pane with the first entry, then open the second entry again.
        window
            .update(cx, |_, window, cx| {
                workspace.update(cx, |w, cx| {
                    w.split_and_clone(w.active_pane().clone(), SplitDirection::Right, window, cx)
                })
            })
            .unwrap()
            .await
            .unwrap();
        window
            .update(cx, |_, window, cx| {
                workspace.update(cx, |w, cx| {
                    w.open_path(file2.clone(), None, true, window, cx)
                })
            })
            .unwrap()
            .await
            .unwrap();

        cx.read(|cx| {
            assert_eq!(
                workspace
                    .read(cx)
                    .active_pane()
                    .read(cx)
                    .active_item()
                    .unwrap()
                    .project_path(cx),
                Some(file2.clone())
            );
        });

        // Open the third entry twice concurrently. Only one pane item is added.
        let (t1, t2) = window
            .update(cx, |_, window, cx| {
                workspace.update(cx, |w, cx| {
                    (
                        w.open_path(file3.clone(), None, true, window, cx),
                        w.open_path(file3.clone(), None, true, window, cx),
                    )
                })
            })
            .unwrap();
        t1.await.unwrap();
        t2.await.unwrap();
        cx.read(|cx| {
            let pane = workspace.read(cx).active_pane().read(cx);
            assert_eq!(
                pane.active_item().unwrap().project_path(cx),
                Some(file3.clone())
            );
            let pane_entries = pane
                .items()
                .map(|i| i.project_path(cx).unwrap())
                .collect::<Vec<_>>();
            assert_eq!(pane_entries, &[file1, file2, file3]);
        });
    }

    #[gpui::test]
    async fn test_open_paths(cx: &mut TestAppContext) {
        let app_state = init_test(cx);

        app_state
            .fs
            .as_fake()
            .insert_tree(
                path!("/"),
                json!({
                    "dir1": {
                        "a.txt": ""
                    },
                    "dir2": {
                        "b.txt": ""
                    },
                    "dir3": {
                        "c.txt": ""
                    },
                    "d.txt": ""
                }),
            )
            .await;

        cx.update(|cx| {
            open_paths(
                &[PathBuf::from(path!("/dir1/"))],
                app_state,
                workspace::OpenOptions::default(),
                cx,
            )
        })
        .await
        .unwrap();
        cx.run_until_parked();
        assert_eq!(cx.update(|cx| cx.windows().len()), 1);
        let window = cx.update(|cx| cx.windows()[0].downcast::<MultiWorkspace>().unwrap());
        let workspace = window
            .read_with(cx, |mw, _| mw.workspace().clone())
            .unwrap();

        #[track_caller]
        fn assert_project_panel_selection(
            workspace: &Workspace,
            expected_worktree_path: &Path,
            expected_entry_path: &RelPath,
            cx: &App,
        ) {
            let project_panel = [
                workspace.left_dock().read(cx).panel::<ProjectPanel>(),
                workspace.right_dock().read(cx).panel::<ProjectPanel>(),
                workspace.bottom_dock().read(cx).panel::<ProjectPanel>(),
            ]
            .into_iter()
            .find_map(std::convert::identity)
            .expect("found no project panels")
            .read(cx);
            let (selected_worktree, selected_entry) = project_panel
                .selected_entry(cx)
                .expect("project panel should have a selected entry");
            assert_eq!(
                selected_worktree.abs_path().as_ref(),
                expected_worktree_path,
                "Unexpected project panel selected worktree path"
            );
            assert_eq!(
                selected_entry.path.as_ref(),
                expected_entry_path,
                "Unexpected project panel selected entry path"
            );
        }

        // Open a file within an existing worktree.
        window
            .update(cx, |multi_workspace, window, cx| {
                multi_workspace.workspace().update(cx, |workspace, cx| {
                    workspace.open_paths(
                        vec![path!("/dir1/a.txt").into()],
                        OpenOptions {
                            visible: Some(OpenVisible::All),
                            ..Default::default()
                        },
                        None,
                        window,
                        cx,
                    )
                })
            })
            .unwrap()
            .await;
        cx.run_until_parked();
        cx.read(|cx| {
            let workspace = workspace.read(cx);
            assert_project_panel_selection(
                workspace,
                Path::new(path!("/dir1")),
                rel_path("a.txt"),
                cx,
            );
            assert_eq!(
                workspace
                    .active_pane()
                    .read(cx)
                    .active_item()
                    .unwrap()
                    .act_as::<Editor>(cx)
                    .unwrap()
                    .read(cx)
                    .title(cx),
                "a.txt"
            );
        });

        // Open a file outside of any existing worktree.
        window
            .update(cx, |multi_workspace, window, cx| {
                multi_workspace.workspace().update(cx, |workspace, cx| {
                    workspace.open_paths(
                        vec![path!("/dir2/b.txt").into()],
                        OpenOptions {
                            visible: Some(OpenVisible::All),
                            ..Default::default()
                        },
                        None,
                        window,
                        cx,
                    )
                })
            })
            .unwrap()
            .await;
        cx.run_until_parked();
        cx.read(|cx| {
            let workspace = workspace.read(cx);
            assert_project_panel_selection(
                workspace,
                Path::new(path!("/dir2/b.txt")),
                rel_path(""),
                cx,
            );
            let worktree_roots = workspace
                .worktrees(cx)
                .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref())
                .collect::<HashSet<_>>();
            assert_eq!(
                worktree_roots,
                vec![path!("/dir1"), path!("/dir2/b.txt")]
                    .into_iter()
                    .map(Path::new)
                    .collect(),
            );
            assert_eq!(
                workspace
                    .active_pane()
                    .read(cx)
                    .active_item()
                    .unwrap()
                    .act_as::<Editor>(cx)
                    .unwrap()
                    .read(cx)
                    .title(cx),
                "b.txt"
            );
        });

        // Ensure opening a directory and one of its children only adds one worktree.
        window
            .update(cx, |multi_workspace, window, cx| {
                multi_workspace.workspace().update(cx, |workspace, cx| {
                    workspace.open_paths(
                        vec![path!("/dir3").into(), path!("/dir3/c.txt").into()],
                        OpenOptions {
                            visible: Some(OpenVisible::All),
                            ..Default::default()
                        },
                        None,
                        window,
                        cx,
                    )
                })
            })
            .unwrap()
            .await;
        cx.run_until_parked();
        cx.read(|cx| {
            let workspace = workspace.read(cx);
            assert_project_panel_selection(
                workspace,
                Path::new(path!("/dir3")),
                rel_path("c.txt"),
                cx,
            );
            let worktree_roots = workspace
                .worktrees(cx)
                .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref())
                .collect::<HashSet<_>>();
            assert_eq!(
                worktree_roots,
                vec![path!("/dir1"), path!("/dir2/b.txt"), path!("/dir3")]
                    .into_iter()
                    .map(Path::new)
                    .collect(),
            );
            assert_eq!(
                workspace
                    .active_pane()
                    .read(cx)
                    .active_item()
                    .unwrap()
                    .act_as::<Editor>(cx)
                    .unwrap()
                    .read(cx)
                    .title(cx),
                "c.txt"
            );
        });

        // Ensure opening invisibly a file outside an existing worktree adds a new, invisible worktree.
        window
            .update(cx, |multi_workspace, window, cx| {
                multi_workspace.workspace().update(cx, |workspace, cx| {
                    workspace.open_paths(
                        vec![path!("/d.txt").into()],
                        OpenOptions {
                            visible: Some(OpenVisible::None),
                            ..Default::default()
                        },
                        None,
                        window,
                        cx,
                    )
                })
            })
            .unwrap()
            .await;
        cx.run_until_parked();
        cx.read(|cx| {
            let workspace = workspace.read(cx);
            assert_project_panel_selection(workspace, Path::new(path!("/d.txt")), rel_path(""), cx);
            let worktree_roots = workspace
                .worktrees(cx)
                .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref())
                .collect::<HashSet<_>>();
            assert_eq!(
                worktree_roots,
                vec![
                    path!("/dir1"),
                    path!("/dir2/b.txt"),
                    path!("/dir3"),
                    path!("/d.txt")
                ]
                .into_iter()
                .map(Path::new)
                .collect(),
            );

            let visible_worktree_roots = workspace
                .visible_worktrees(cx)
                .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref())
                .collect::<HashSet<_>>();
            assert_eq!(
                visible_worktree_roots,
                vec![path!("/dir1"), path!("/dir2/b.txt"), path!("/dir3")]
                    .into_iter()
                    .map(Path::new)
                    .collect(),
            );

            assert_eq!(
                workspace
                    .active_pane()
                    .read(cx)
                    .active_item()
                    .unwrap()
                    .act_as::<Editor>(cx)
                    .unwrap()
                    .read(cx)
                    .title(cx),
                "d.txt"
            );
        });
    }

    #[gpui::test]
    async fn test_opening_excluded_paths(cx: &mut TestAppContext) {
        let app_state = init_test(cx);
        cx.update(|cx| {
            cx.update_global::<SettingsStore, _>(|store, cx| {
                store.update_user_settings(cx, |project_settings| {
                    project_settings.project.worktree.file_scan_exclusions =
                        Some(vec!["excluded_dir".to_string(), "**/.git".to_string()]);
                });
            });
        });
        app_state
            .fs
            .as_fake()
            .insert_tree(
                path!("/root"),
                json!({
                    ".gitignore": "ignored_dir\n",
                    ".git": {
                        "HEAD": "ref: refs/heads/main",
                    },
                    "regular_dir": {
                        "file": "regular file contents",
                    },
                    "ignored_dir": {
                        "ignored_subdir": {
                            "file": "ignored subfile contents",
                        },
                        "file": "ignored file contents",
                    },
                    "excluded_dir": {
                        "file": "excluded file contents",
                        "ignored_subdir": {
                            "file": "ignored subfile contents",
                        },
                    },
                }),
            )
            .await;

        let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
        project.update(cx, |project, _cx| project.languages().add(markdown_lang()));
        let window = cx.add_window({
            let project = project.clone();
            |window, cx| MultiWorkspace::test_new(project, window, cx)
        });
        let workspace = window
            .read_with(cx, |mw, _| mw.workspace().clone())
            .unwrap();

        let initial_entries = cx.read(|cx| workspace.file_project_paths(cx));
        let paths_to_open = [
            PathBuf::from(path!("/root/excluded_dir/file")),
            PathBuf::from(path!("/root/.git/HEAD")),
            PathBuf::from(path!("/root/excluded_dir/ignored_subdir")),
        ];
        let workspace::OpenResult {
            window: opened_workspace,
            opened_items: new_items,
            ..
        } = cx
            .update(|cx| {
                workspace::open_paths(
                    &paths_to_open,
                    app_state,
                    workspace::OpenOptions::default(),
                    cx,
                )
            })
            .await
            .unwrap();

        assert_eq!(
            opened_workspace
                .read_with(cx, |mw, _| mw.workspace().entity_id())
                .unwrap(),
            workspace.entity_id(),
            "Excluded files in subfolders of a workspace root should be opened in the workspace"
        );
        let mut opened_paths = cx.read(|cx| {
            assert_eq!(
                new_items.len(),
                paths_to_open.len(),
                "Expect to get the same number of opened items as submitted paths to open"
            );
            new_items
                .iter()
                .zip(paths_to_open.iter())
                .map(|(i, path)| {
                    match i {
                        Some(Ok(i)) => Some(i.project_path(cx).map(|p| p.path)),
                        Some(Err(e)) => panic!("Excluded file {path:?} failed to open: {e:?}"),
                        None => None,
                    }
                    .flatten()
                })
                .collect::<Vec<_>>()
        });
        opened_paths.sort();
        assert_eq!(
            opened_paths,
            vec![
                None,
                Some(rel_path(".git/HEAD").into()),
                Some(rel_path("excluded_dir/file").into()),
            ],
            "Excluded files should get opened, excluded dir should not get opened"
        );

        let entries = cx.read(|cx| workspace.file_project_paths(cx));
        assert_eq!(
            initial_entries, entries,
            "Workspace entries should not change after opening excluded files and directories paths"
        );

        cx.read(|cx| {
                let pane = workspace.read(cx).active_pane().read(cx);
                let mut opened_buffer_paths = pane
                    .items()
                    .map(|i| {
                        i.project_path(cx)
                            .expect("all excluded files that got open should have a path")
                            .path
                    })
                    .collect::<Vec<_>>();
                opened_buffer_paths.sort();
                assert_eq!(
                    opened_buffer_paths,
                    vec![rel_path(".git/HEAD").into(), rel_path("excluded_dir/file").into()],
                    "Despite not being present in the worktrees, buffers for excluded files are opened and added to the pane"
                );
            });
    }

    #[gpui::test]
    async fn test_save_conflicting_item(cx: &mut TestAppContext) {
        let app_state = init_test(cx);
        app_state
            .fs
            .as_fake()
            .insert_tree(path!("/root"), json!({ "a.txt": "" }))
            .await;

        let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
        project.update(cx, |project, _cx| project.languages().add(markdown_lang()));
        let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project, window, cx));
        let workspace = window
            .read_with(cx, |mw, _| mw.workspace().clone())
            .unwrap();

        // Open a file within an existing worktree.
        window
            .update(cx, |_, window, cx| {
                workspace.update(cx, |workspace, cx| {
                    workspace.open_paths(
                        vec![PathBuf::from(path!("/root/a.txt"))],
                        OpenOptions {
                            visible: Some(OpenVisible::All),
                            ..Default::default()
                        },
                        None,
                        window,
                        cx,
                    )
                })
            })
            .unwrap()
            .await;
        let editor = cx.read(|cx| {
            let pane = workspace.read(cx).active_pane().read(cx);
            let item = pane.active_item().unwrap();
            item.downcast::<Editor>().unwrap()
        });

        window
            .update(cx, |_, window, cx| {
                editor.update(cx, |editor, cx| editor.handle_input("x", window, cx));
            })
            .unwrap();

        app_state
            .fs
            .as_fake()
            .insert_file(path!("/root/a.txt"), b"changed".to_vec())
            .await;

        cx.run_until_parked();
        cx.read(|cx| assert!(editor.is_dirty(cx)));
        cx.read(|cx| assert!(editor.has_conflict(cx)));

        let save_task = window
            .update(cx, |_, window, cx| {
                workspace.update(cx, |workspace, cx| {
                    workspace.save_active_item(SaveIntent::Save, window, cx)
                })
            })
            .unwrap();
        cx.background_executor.run_until_parked();
        cx.simulate_prompt_answer("Overwrite");
        save_task.await.unwrap();
        window
            .update(cx, |_, _, cx| {
                editor.update(cx, |editor, cx| {
                    assert!(!editor.is_dirty(cx));
                    assert!(!editor.has_conflict(cx));
                });
            })
            .unwrap();
    }

    #[gpui::test]
    async fn test_open_and_save_new_file(cx: &mut TestAppContext) {
        let app_state = init_test(cx);
        app_state
            .fs
            .create_dir(Path::new(path!("/root")))
            .await
            .unwrap();

        let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
        project.update(cx, |project, _| {
            project.languages().add(markdown_lang());
            project.languages().add(rust_lang());
        });
        let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project, window, cx));
        let workspace = window
            .read_with(cx, |mw, _| mw.workspace().clone())
            .unwrap();
        let worktree = cx.read(|cx| workspace.read(cx).worktrees(cx).next().unwrap());

        // Create a new untitled buffer
        cx.dispatch_action(window.into(), NewFile);
        let editor = cx.read(|cx| {
            workspace
                .read(cx)
                .active_item(cx)
                .unwrap()
                .downcast::<Editor>()
                .unwrap()
        });

        window
            .update(cx, |_, window, cx| {
                editor.update(cx, |editor, cx| {
                    assert!(!editor.is_dirty(cx));
                    assert_eq!(editor.title(cx), "untitled");
                    assert!(Arc::ptr_eq(
                        &editor
                            .buffer()
                            .read(cx)
                            .language_at(MultiBufferOffset(0), cx)
                            .unwrap(),
                        &languages::PLAIN_TEXT
                    ));
                    editor.handle_input("hi", window, cx);
                    assert!(editor.is_dirty(cx));
                });
            })
            .unwrap();

        // Save the buffer. This prompts for a filename.
        let save_task = window
            .update(cx, |_, window, cx| {
                workspace.update(cx, |workspace, cx| {
                    workspace.save_active_item(SaveIntent::Save, window, cx)
                })
            })
            .unwrap();
        cx.background_executor.run_until_parked();
        cx.simulate_new_path_selection(|parent_dir| {
            assert_eq!(parent_dir, Path::new(path!("/root")));
            Some(parent_dir.join("the-new-name.rs"))
        });
        cx.read(|cx| {
            assert!(editor.is_dirty(cx));
            assert_eq!(editor.read(cx).title(cx), "hi");
        });

        // When the save completes, the buffer's title is updated and the language is assigned based
        // on the path.
        save_task.await.unwrap();
        window
            .update(cx, |_, _, cx| {
                editor.update(cx, |editor, cx| {
                    assert!(!editor.is_dirty(cx));
                    assert_eq!(editor.title(cx), "the-new-name.rs");
                    assert_eq!(
                        editor
                            .buffer()
                            .read(cx)
                            .language_at(MultiBufferOffset(0), cx)
                            .unwrap()
                            .name(),
                        "Rust"
                    );
                });
            })
            .unwrap();

        // Edit the file and save it again. This time, there is no filename prompt.
        window
            .update(cx, |_, window, cx| {
                editor.update(cx, |editor, cx| {
                    editor.handle_input(" there", window, cx);
                    assert!(editor.is_dirty(cx));
                });
            })
            .unwrap();

        let save_task = window
            .update(cx, |_, window, cx| {
                workspace.update(cx, |workspace, cx| {
                    workspace.save_active_item(SaveIntent::Save, window, cx)
                })
            })
            .unwrap();
        save_task.await.unwrap();

        assert!(!cx.did_prompt_for_new_path());
        window
            .update(cx, |_, _, cx| {
                editor.update(cx, |editor, cx| {
                    assert!(!editor.is_dirty(cx));
                    assert_eq!(editor.title(cx), "the-new-name.rs")
                });
            })
            .unwrap();

        // Open the same newly-created file in another pane item. The new editor should reuse
        // the same buffer.
        cx.dispatch_action(window.into(), NewFile);
        window
            .update(cx, |_, window, cx| {
                workspace.update(cx, |workspace, cx| {
                    workspace.split_and_clone(
                        workspace.active_pane().clone(),
                        SplitDirection::Right,
                        window,
                        cx,
                    )
                })
            })
            .unwrap()
            .await
            .unwrap();
        window
            .update(cx, |_, window, cx| {
                workspace.update(cx, |workspace, cx| {
                    workspace.open_path(
                        (worktree.read(cx).id(), rel_path("the-new-name.rs")),
                        None,
                        true,
                        window,
                        cx,
                    )
                })
            })
            .unwrap()
            .await
            .unwrap();
        let editor2 = cx.read(|cx| {
            workspace
                .read(cx)
                .active_item(cx)
                .unwrap()
                .downcast::<Editor>()
                .unwrap()
        });
        cx.read(|cx| {
            assert_eq!(
                editor2.read(cx).buffer().read(cx).as_singleton().unwrap(),
                editor.read(cx).buffer().read(cx).as_singleton().unwrap()
            );
        })
    }

    #[gpui::test]
    async fn test_setting_language_when_saving_as_single_file_worktree(cx: &mut TestAppContext) {
        let app_state = init_test(cx);
        app_state.fs.create_dir(Path::new("/root")).await.unwrap();

        let project = Project::test(app_state.fs.clone(), [], cx).await;
        project.update(cx, |project, _| {
            project.languages().add(language::rust_lang());
            project.languages().add(language::markdown_lang());
        });
        let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project, window, cx));
        let workspace = window
            .read_with(cx, |mw, _| mw.workspace().clone())
            .unwrap();

        // Create a new untitled buffer
        cx.dispatch_action(window.into(), NewFile);
        let editor = cx.read(|cx| {
            workspace
                .read(cx)
                .active_item(cx)
                .unwrap()
                .downcast::<Editor>()
                .unwrap()
        });
        window
            .update(cx, |_, window, cx| {
                editor.update(cx, |editor, cx| {
                    assert!(Arc::ptr_eq(
                        &editor
                            .buffer()
                            .read(cx)
                            .language_at(MultiBufferOffset(0), cx)
                            .unwrap(),
                        &languages::PLAIN_TEXT
                    ));
                    editor.handle_input("hi", window, cx);
                    assert!(editor.is_dirty(cx));
                });
            })
            .unwrap();

        // Save the buffer. This prompts for a filename.
        let save_task = window
            .update(cx, |_, window, cx| {
                workspace.update(cx, |workspace, cx| {
                    workspace.save_active_item(SaveIntent::Save, window, cx)
                })
            })
            .unwrap();
        cx.background_executor.run_until_parked();
        cx.simulate_new_path_selection(|_| Some(PathBuf::from("/root/the-new-name.rs")));
        save_task.await.unwrap();
        // The buffer is not dirty anymore and the language is assigned based on the path.
        window
            .update(cx, |_, _, cx| {
                editor.update(cx, |editor, cx| {
                    assert!(!editor.is_dirty(cx));
                    assert_eq!(
                        editor
                            .buffer()
                            .read(cx)
                            .language_at(MultiBufferOffset(0), cx)
                            .unwrap()
                            .name(),
                        "Rust"
                    )
                });
            })
            .unwrap();
    }

    #[gpui::test]
    async fn test_pane_actions(cx: &mut TestAppContext) {
        let app_state = init_test(cx);
        app_state
            .fs
            .as_fake()
            .insert_tree(
                path!("/root"),
                json!({
                    "a": {
                        "file1": "contents 1",
                        "file2": "contents 2",
                        "file3": "contents 3",
                    },
                }),
            )
            .await;

        let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
        project.update(cx, |project, _cx| project.languages().add(markdown_lang()));
        let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project, window, cx));
        let workspace = window
            .read_with(cx, |mw, _| mw.workspace().clone())
            .unwrap();
        let cx = &mut VisualTestContext::from_window(*window, cx);

        let entries = cx.read(|cx| workspace.file_project_paths(cx));
        let file1 = entries[0].clone();

        let pane_1 = cx.read(|cx| workspace.read(cx).active_pane().clone());

        workspace
            .update_in(cx, |w, window, cx| {
                w.open_path(file1.clone(), None, true, window, cx)
            })
            .await
            .unwrap();

        let (editor_1, buffer) = workspace.update_in(cx, |_, window, cx| {
            pane_1.update(cx, |pane_1, cx| {
                let editor = pane_1.active_item().unwrap().downcast::<Editor>().unwrap();
                assert_eq!(editor.project_path(cx), Some(file1.clone()));
                let buffer = editor.update(cx, |editor, cx| {
                    editor.insert("dirt", window, cx);
                    editor.buffer().downgrade()
                });
                (editor.downgrade(), buffer)
            })
        });

        cx.dispatch_action(pane::SplitRight::default());
        let editor_2 = cx.update(|_, cx| {
            let pane_2 = workspace.read(cx).active_pane().clone();
            assert_ne!(pane_1, pane_2);

            let pane2_item = pane_2.read(cx).active_item().unwrap();
            assert_eq!(pane2_item.project_path(cx), Some(file1.clone()));

            pane2_item.downcast::<Editor>().unwrap().downgrade()
        });
        cx.dispatch_action(workspace::CloseActiveItem {
            save_intent: None,
            close_pinned: false,
        });

        cx.background_executor.run_until_parked();
        workspace.read_with(cx, |workspace, _| {
            assert_eq!(workspace.panes().len(), 1);
            assert_eq!(workspace.active_pane(), &pane_1);
        });

        cx.dispatch_action(workspace::CloseActiveItem {
            save_intent: None,
            close_pinned: false,
        });
        cx.background_executor.run_until_parked();
        cx.simulate_prompt_answer("Don't Save");
        cx.background_executor.run_until_parked();

        workspace.read_with(cx, |workspace, cx| {
            assert_eq!(workspace.panes().len(), 1);
            assert!(workspace.active_item(cx).is_none());
        });

        cx.background_executor
            .advance_clock(SERIALIZATION_THROTTLE_TIME);
        cx.update(|_, _| {});
        editor_1.assert_released();
        editor_2.assert_released();
        buffer.assert_released();
    }

    #[gpui::test]
    async fn test_editor_zoom_with_scroll_wheel(cx: &mut TestAppContext) {
        let app_state = init_test(cx);
        app_state
            .fs
            .as_fake()
            .insert_tree(path!("/root"), json!({ "file.txt": "hello\nworld\n" }))
            .await;

        let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
        let window =
            cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
        let workspace = window
            .read_with(cx, |mw, _| mw.workspace().clone())
            .unwrap();
        let cx = &mut VisualTestContext::from_window(*window, cx);

        let mouse_position = point(px(250.), px(250.));

        let event_modifiers = {
            #[cfg(target_os = "macos")]
            {
                Modifiers {
                    platform: true,
                    ..Modifiers::default()
                }
            }

            #[cfg(not(target_os = "macos"))]
            {
                Modifiers {
                    control: true,
                    ..Modifiers::default()
                }
            }
        };

        workspace
            .update_in(cx, |workspace, window, cx| {
                workspace.open_abs_path(
                    PathBuf::from(path!("/root/file.txt")),
                    OpenOptions::default(),
                    window,
                    cx,
                )
            })
            .await
            .unwrap()
            .downcast::<Editor>()
            .unwrap();

        cx.update(|window, cx| {
            window.draw(cx).clear();
        });

        // mouse_wheel_zoom is disabled by default — zoom should not work.
        let initial_font_size =
            cx.update(|_, cx| ThemeSettings::get_global(cx).buffer_font_size(cx).as_f32());

        cx.simulate_event(gpui::ScrollWheelEvent {
            position: mouse_position,
            delta: gpui::ScrollDelta::Pixels(point(px(0.), px(1.))),
            modifiers: event_modifiers,
            ..Default::default()
        });

        let font_size_after_disabled_zoom =
            cx.update(|_, cx| ThemeSettings::get_global(cx).buffer_font_size(cx).as_f32());

        assert_eq!(
            initial_font_size, font_size_after_disabled_zoom,
            "Editor buffer font-size should not change when mouse_wheel_zoom is disabled"
        );

        // Enable mouse_wheel_zoom and verify zoom works.
        cx.update(|_, cx| {
            SettingsStore::update_global(cx, |store, cx| {
                store.update_user_settings(cx, |settings| {
                    settings.editor.mouse_wheel_zoom = Some(true);
                });
            });
        });

        cx.update(|window, cx| {
            window.draw(cx).clear();
        });

        cx.simulate_event(gpui::ScrollWheelEvent {
            position: mouse_position,
            delta: gpui::ScrollDelta::Pixels(point(px(0.), px(1.))),
            modifiers: event_modifiers,
            ..Default::default()
        });

        let increased_font_size =
            cx.update(|_, cx| ThemeSettings::get_global(cx).buffer_font_size(cx).as_f32());

        assert!(
            increased_font_size > initial_font_size,
            "Editor buffer font-size should have increased from scroll-zoom"
        );

        cx.update(|window, cx| {
            window.draw(cx).clear();
        });

        cx.simulate_event(gpui::ScrollWheelEvent {
            position: mouse_position,
            delta: gpui::ScrollDelta::Pixels(point(px(0.), px(-1.))),
            modifiers: event_modifiers,
            ..Default::default()
        });

        let decreased_font_size =
            cx.update(|_, cx| ThemeSettings::get_global(cx).buffer_font_size(cx).as_f32());

        assert!(
            decreased_font_size < increased_font_size,
            "Editor buffer font-size should have decreased from scroll-zoom"
        );

        // Disable mouse_wheel_zoom again and verify zoom stops working.
        cx.update(|_, cx| {
            SettingsStore::update_global(cx, |store, cx| {
                store.update_user_settings(cx, |settings| {
                    settings.editor.mouse_wheel_zoom = Some(false);
                });
            });
        });

        let font_size_before =
            cx.update(|_, cx| ThemeSettings::get_global(cx).buffer_font_size(cx).as_f32());

        cx.update(|window, cx| {
            window.draw(cx).clear();
        });

        cx.simulate_event(gpui::ScrollWheelEvent {
            position: mouse_position,
            delta: gpui::ScrollDelta::Pixels(point(px(0.), px(1.))),
            modifiers: event_modifiers,
            ..Default::default()
        });

        let font_size_after =
            cx.update(|_, cx| ThemeSettings::get_global(cx).buffer_font_size(cx).as_f32());

        assert_eq!(
            font_size_before, font_size_after,
            "Editor buffer font-size should not change when mouse_wheel_zoom is re-disabled"
        );
    }

    #[gpui::test]
    async fn test_navigation(cx: &mut TestAppContext) {
        let app_state = init_test(cx);
        app_state
            .fs
            .as_fake()
            .insert_tree(
                path!("/root"),
                json!({
                    "a": {
                        "file1": "contents 1\n".repeat(20),
                        "file2": "contents 2\n".repeat(20),
                        "file3": "contents 3\n".repeat(20),
                    },
                }),
            )
            .await;

        let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
        project.update(cx, |project, _cx| project.languages().add(markdown_lang()));
        let window =
            cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
        let workspace = window
            .read_with(cx, |mw, _| mw.workspace().clone())
            .unwrap();
        let cx = &mut VisualTestContext::from_window(*window, cx);
        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());

        let entries = cx.read(|cx| workspace.file_project_paths(cx));
        let file1 = entries[0].clone();
        let file2 = entries[1].clone();
        let file3 = entries[2].clone();

        let editor1 = workspace
            .update_in(cx, |w, window, cx| {
                w.open_path(file1.clone(), None, true, window, cx)
            })
            .await
            .unwrap()
            .downcast::<Editor>()
            .unwrap();
        workspace.update_in(cx, |_, window, cx| {
            editor1.update(cx, |editor, cx| {
                editor.change_selections(Default::default(), window, cx, |s| {
                    s.select_display_ranges([
                        DisplayPoint::new(DisplayRow(10), 0)..DisplayPoint::new(DisplayRow(10), 0)
                    ])
                });
            });
        });

        let editor2 = workspace
            .update_in(cx, |w, window, cx| {
                w.open_path(file2.clone(), None, true, window, cx)
            })
            .await
            .unwrap()
            .downcast::<Editor>()
            .unwrap();
        let editor3 = workspace
            .update_in(cx, |w, window, cx| {
                w.open_path(file3.clone(), None, true, window, cx)
            })
            .await
            .unwrap()
            .downcast::<Editor>()
            .unwrap();

        workspace
            .update_in(cx, |_, window, cx| {
                editor3.update(cx, |editor, cx| {
                    editor.change_selections(Default::default(), window, cx, |s| {
                        s.select_display_ranges([DisplayPoint::new(DisplayRow(12), 0)
                            ..DisplayPoint::new(DisplayRow(12), 0)])
                    });
                    editor.newline(&Default::default(), window, cx);
                    editor.newline(&Default::default(), window, cx);
                    editor.move_down(&Default::default(), window, cx);
                    editor.move_down(&Default::default(), window, cx);
                    editor.save(
                        SaveOptions {
                            format: true,
                            force_format: false,
                            autosave: false,
                        },
                        project.clone(),
                        window,
                        cx,
                    )
                })
            })
            .await
            .unwrap();
        workspace.update_in(cx, |_, window, cx| {
            editor3.update(cx, |editor, cx| {
                editor.set_scroll_position(point(0., 12.5), window, cx)
            });
        });
        assert_eq!(
            active_location(&workspace, cx),
            (file3.clone(), DisplayPoint::new(DisplayRow(16), 0), 12.5)
        );

        workspace
            .update_in(cx, |w, window, cx| {
                w.go_back(w.active_pane().downgrade(), window, cx)
            })
            .await
            .unwrap();
        assert_eq!(
            active_location(&workspace, cx),
            (file3.clone(), DisplayPoint::new(DisplayRow(0), 0), 0.)
        );

        workspace
            .update_in(cx, |w, window, cx| {
                w.go_back(w.active_pane().downgrade(), window, cx)
            })
            .await
            .unwrap();
        assert_eq!(
            active_location(&workspace, cx),
            (file2.clone(), DisplayPoint::new(DisplayRow(0), 0), 0.)
        );

        workspace
            .update_in(cx, |w, window, cx| {
                w.go_back(w.active_pane().downgrade(), window, cx)
            })
            .await
            .unwrap();
        assert_eq!(
            active_location(&workspace, cx),
            (file1.clone(), DisplayPoint::new(DisplayRow(10), 0), 0.)
        );

        workspace
            .update_in(cx, |w, window, cx| {
                w.go_back(w.active_pane().downgrade(), window, cx)
            })
            .await
            .unwrap();
        assert_eq!(
            active_location(&workspace, cx),
            (file1.clone(), DisplayPoint::new(DisplayRow(0), 0), 0.)
        );

        // Go back one more time and ensure we don't navigate past the first item in the history.
        workspace
            .update_in(cx, |w, window, cx| {
                w.go_back(w.active_pane().downgrade(), window, cx)
            })
            .await
            .unwrap();
        assert_eq!(
            active_location(&workspace, cx),
            (file1.clone(), DisplayPoint::new(DisplayRow(0), 0), 0.)
        );

        workspace
            .update_in(cx, |w, window, cx| {
                w.go_forward(w.active_pane().downgrade(), window, cx)
            })
            .await
            .unwrap();
        assert_eq!(
            active_location(&workspace, cx),
            (file1.clone(), DisplayPoint::new(DisplayRow(10), 0), 0.)
        );

        workspace
            .update_in(cx, |w, window, cx| {
                w.go_forward(w.active_pane().downgrade(), window, cx)
            })
            .await
            .unwrap();
        assert_eq!(
            active_location(&workspace, cx),
            (file2.clone(), DisplayPoint::new(DisplayRow(0), 0), 0.)
        );

        // Go forward to an item that has been closed, ensuring it gets re-opened at the same
        // location.
        workspace
            .update_in(cx, |_, window, cx| {
                pane.update(cx, |pane, cx| {
                    let editor3_id = editor3.entity_id();
                    drop(editor3);
                    pane.close_item_by_id(editor3_id, SaveIntent::Close, window, cx)
                })
            })
            .await
            .unwrap();
        workspace
            .update_in(cx, |w, window, cx| {
                w.go_forward(w.active_pane().downgrade(), window, cx)
            })
            .await
            .unwrap();
        assert_eq!(
            active_location(&workspace, cx),
            (file3.clone(), DisplayPoint::new(DisplayRow(0), 0), 0.)
        );

        workspace
            .update_in(cx, |w, window, cx| {
                w.go_forward(w.active_pane().downgrade(), window, cx)
            })
            .await
            .unwrap();
        assert_eq!(
            active_location(&workspace, cx),
            (file3.clone(), DisplayPoint::new(DisplayRow(16), 0), 12.5)
        );

        workspace
            .update_in(cx, |w, window, cx| {
                w.go_back(w.active_pane().downgrade(), window, cx)
            })
            .await
            .unwrap();
        assert_eq!(
            active_location(&workspace, cx),
            (file3.clone(), DisplayPoint::new(DisplayRow(0), 0), 0.)
        );

        // Go back to an item that has been closed and removed from disk
        workspace
            .update_in(cx, |_, window, cx| {
                pane.update(cx, |pane, cx| {
                    let editor2_id = editor2.entity_id();
                    drop(editor2);
                    pane.close_item_by_id(editor2_id, SaveIntent::Close, window, cx)
                })
            })
            .await
            .unwrap();
        app_state
            .fs
            .remove_file(Path::new(path!("/root/a/file2")), Default::default())
            .await
            .unwrap();
        cx.background_executor.run_until_parked();

        workspace
            .update_in(cx, |w, window, cx| {
                w.go_back(w.active_pane().downgrade(), window, cx)
            })
            .await
            .unwrap();
        assert_eq!(
            active_location(&workspace, cx),
            (file2.clone(), DisplayPoint::new(DisplayRow(0), 0), 0.)
        );
        workspace
            .update_in(cx, |w, window, cx| {
                w.go_forward(w.active_pane().downgrade(), window, cx)
            })
            .await
            .unwrap();
        assert_eq!(
            active_location(&workspace, cx),
            (file3.clone(), DisplayPoint::new(DisplayRow(0), 0), 0.)
        );

        // Modify file to collapse multiple nav history entries into the same location.
        // Ensure we don't visit the same location twice when navigating.
        workspace.update_in(cx, |_, window, cx| {
            editor1.update(cx, |editor, cx| {
                editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
                    s.select_display_ranges([
                        DisplayPoint::new(DisplayRow(15), 0)..DisplayPoint::new(DisplayRow(15), 0)
                    ])
                })
            });
        });
        for _ in 0..5 {
            workspace.update_in(cx, |_, window, cx| {
                editor1.update(cx, |editor, cx| {
                    editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
                        s.select_display_ranges([DisplayPoint::new(DisplayRow(3), 0)
                            ..DisplayPoint::new(DisplayRow(3), 0)])
                    });
                });
            });

            workspace.update_in(cx, |_, window, cx| {
                editor1.update(cx, |editor, cx| {
                    editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
                        s.select_display_ranges([DisplayPoint::new(DisplayRow(13), 0)
                            ..DisplayPoint::new(DisplayRow(13), 0)])
                    });
                });
            });
        }
        workspace.update_in(cx, |_, window, cx| {
            editor1.update(cx, |editor, cx| {
                editor.transact(window, cx, |editor, window, cx| {
                    editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
                        s.select_display_ranges([DisplayPoint::new(DisplayRow(2), 0)
                            ..DisplayPoint::new(DisplayRow(14), 0)])
                    });
                    editor.insert("", window, cx);
                })
            });
        });

        workspace.update_in(cx, |_, window, cx| {
            editor1.update(cx, |editor, cx| {
                editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
                    s.select_display_ranges([
                        DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
                    ])
                })
            });
        });
        workspace
            .update_in(cx, |w, window, cx| {
                w.go_back(w.active_pane().downgrade(), window, cx)
            })
            .await
            .unwrap();
        assert_eq!(
            active_location(&workspace, cx),
            (file1.clone(), DisplayPoint::new(DisplayRow(2), 0), 0.)
        );
        workspace
            .update_in(cx, |w, window, cx| {
                w.go_back(w.active_pane().downgrade(), window, cx)
            })
            .await
            .unwrap();
        assert_eq!(
            active_location(&workspace, cx),
            (file1.clone(), DisplayPoint::new(DisplayRow(3), 0), 0.)
        );

        fn active_location(
            workspace: &Entity<Workspace>,
            cx: &mut VisualTestContext,
        ) -> (ProjectPath, DisplayPoint, f64) {
            workspace.update(cx, |workspace, cx| {
                let item = workspace.active_item(cx).unwrap();
                let editor = item.downcast::<Editor>().unwrap();

                editor.update(cx, |editor_ref, cx| {
                    let selections = editor_ref
                        .selections
                        .display_ranges(&editor_ref.display_snapshot(cx));
                    let scroll_position = editor_ref.scroll_position(cx);

                    (
                        editor_ref.project_path(cx).unwrap(),
                        selections[0].start,
                        scroll_position.y,
                    )
                })
            })
        }
    }

    #[gpui::test]
    async fn test_reopening_closed_items(cx: &mut TestAppContext) {
        let app_state = init_test(cx);
        app_state
            .fs
            .as_fake()
            .insert_tree(
                path!("/root"),
                json!({
                    "a": {
                        "file1": "",
                        "file2": "",
                        "file3": "",
                        "file4": "",
                    },
                }),
            )
            .await;

        let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
        project.update(cx, |project, _cx| project.languages().add(markdown_lang()));
        let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project, window, cx));
        let workspace = window
            .read_with(cx, |mw, _| mw.workspace().clone())
            .unwrap();
        let cx = &mut VisualTestContext::from_window(*window, cx);
        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());

        let entries = cx.read(|cx| workspace.file_project_paths(cx));
        let file1 = entries[0].clone();
        let file2 = entries[1].clone();
        let file3 = entries[2].clone();
        let file4 = entries[3].clone();

        let file1_item_id = workspace
            .update_in(cx, |w, window, cx| {
                w.open_path(file1.clone(), None, true, window, cx)
            })
            .await
            .unwrap()
            .item_id();
        let file2_item_id = workspace
            .update_in(cx, |w, window, cx| {
                w.open_path(file2.clone(), None, true, window, cx)
            })
            .await
            .unwrap()
            .item_id();
        let file3_item_id = workspace
            .update_in(cx, |w, window, cx| {
                w.open_path(file3.clone(), None, true, window, cx)
            })
            .await
            .unwrap()
            .item_id();
        let file4_item_id = workspace
            .update_in(cx, |w, window, cx| {
                w.open_path(file4.clone(), None, true, window, cx)
            })
            .await
            .unwrap()
            .item_id();
        assert_eq!(active_path(&workspace, cx), Some(file4.clone()));

        // Close all the pane items in some arbitrary order.
        workspace
            .update_in(cx, |_, window, cx| {
                pane.update(cx, |pane, cx| {
                    pane.close_item_by_id(file1_item_id, SaveIntent::Close, window, cx)
                })
            })
            .await
            .unwrap();
        assert_eq!(active_path(&workspace, cx), Some(file4.clone()));

        workspace
            .update_in(cx, |_, window, cx| {
                pane.update(cx, |pane, cx| {
                    pane.close_item_by_id(file4_item_id, SaveIntent::Close, window, cx)
                })
            })
            .await
            .unwrap();
        assert_eq!(active_path(&workspace, cx), Some(file3.clone()));

        workspace
            .update_in(cx, |_, window, cx| {
                pane.update(cx, |pane, cx| {
                    pane.close_item_by_id(file2_item_id, SaveIntent::Close, window, cx)
                })
            })
            .await
            .unwrap();
        assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
        workspace
            .update_in(cx, |_, window, cx| {
                pane.update(cx, |pane, cx| {
                    pane.close_item_by_id(file3_item_id, SaveIntent::Close, window, cx)
                })
            })
            .await
            .unwrap();

        assert_eq!(active_path(&workspace, cx), None);

        // Reopen all the closed items, ensuring they are reopened in the same order
        // in which they were closed.
        workspace
            .update_in(cx, Workspace::reopen_closed_item)
            .await
            .unwrap();
        assert_eq!(active_path(&workspace, cx), Some(file3.clone()));

        workspace
            .update_in(cx, Workspace::reopen_closed_item)
            .await
            .unwrap();
        assert_eq!(active_path(&workspace, cx), Some(file2.clone()));

        workspace
            .update_in(cx, Workspace::reopen_closed_item)
            .await
            .unwrap();
        assert_eq!(active_path(&workspace, cx), Some(file4.clone()));

        workspace
            .update_in(cx, Workspace::reopen_closed_item)
            .await
            .unwrap();
        assert_eq!(active_path(&workspace, cx), Some(file1.clone()));

        // Reopening past the last closed item is a no-op.
        workspace
            .update_in(cx, Workspace::reopen_closed_item)
            .await
            .unwrap();
        assert_eq!(active_path(&workspace, cx), Some(file1.clone()));

        // Reopening closed items doesn't interfere with navigation history.
        // Verify we can navigate back through the history after reopening items.
        workspace
            .update_in(cx, |workspace, window, cx| {
                workspace.go_back(workspace.active_pane().downgrade(), window, cx)
            })
            .await
            .unwrap();

        // After go_back, we should be at a different file than file1
        let after_go_back = active_path(&workspace, cx);
        assert!(
            after_go_back.is_some() && after_go_back != Some(file1.clone()),
            "After go_back from file1, should be at a different file"
        );

        pane.read_with(cx, |pane, _| {
            assert!(pane.can_navigate_forward(), "Should be able to go forward");
        });

        fn active_path(
            workspace: &Entity<Workspace>,
            cx: &VisualTestContext,
        ) -> Option<ProjectPath> {
            workspace.read_with(cx, |workspace, cx| {
                let item = workspace.active_item(cx)?;
                item.project_path(cx)
            })
        }
    }

    fn init_keymap_test(cx: &mut TestAppContext) -> Arc<AppState> {
        cx.update(|cx| {
            let app_state = AppState::test(cx);

            theme_settings::init(theme::LoadThemes::JustBase, cx);
            client::init(&app_state.client, cx);
            workspace::init(app_state.clone(), cx);
            onboarding::init(cx);
            app_state
        })
    }

    actions!(test_only, [ActionA, ActionB]);

    #[gpui::test]
    async fn test_base_keymap(cx: &mut gpui::TestAppContext) {
        let executor = cx.executor();
        let app_state = init_keymap_test(cx);
        let project = Project::test(app_state.fs.clone(), [], cx).await;
        let window =
            cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
        let workspace = window
            .read_with(cx, |mw, _| mw.workspace().clone())
            .unwrap();

        // From the Atom keymap
        use workspace::ActivatePreviousPane;
        // From the JetBrains keymap
        use workspace::ActivatePreviousItem;

        app_state
            .fs
            .save(
                paths::settings_file(),
                &r#"{"base_keymap": "Atom"}"#.into(),
                Default::default(),
            )
            .await
            .unwrap();

        app_state
            .fs
            .save(
                "/keymap.json".as_ref(),
                &r#"[{"bindings": {"backspace": "test_only::ActionA"}}]"#.into(),
                Default::default(),
            )
            .await
            .unwrap();
        executor.run_until_parked();
        cx.update(|cx| {
            let (keymap_rx, keymap_watcher) = watch_config_file(
                &executor,
                app_state.fs.clone(),
                PathBuf::from("/keymap.json"),
            );
            watch_settings_files(app_state.fs.clone(), cx);
            handle_keymap_file_changes(keymap_rx, keymap_watcher, cx);
        });
        window
            .update(cx, |_, _, cx| {
                workspace.update(cx, |workspace, cx| {
                    workspace.register_action(|_, _: &ActionA, _window, _cx| {});
                    workspace.register_action(|_, _: &ActionB, _window, _cx| {});
                    workspace.register_action(|_, _: &ActivatePreviousPane, _window, _cx| {});
                    workspace.register_action(|_, _: &ActivatePreviousItem, _window, _cx| {});
                    cx.notify();
                });
            })
            .unwrap();
        executor.run_until_parked();
        // Test loading the keymap base at all
        assert_key_bindings_for(
            window.into(),
            cx,
            vec![("backspace", &ActionA), ("k", &ActivatePreviousPane)],
            line!(),
        );

        // Test modifying the users keymap, while retaining the base keymap
        app_state
            .fs
            .save(
                "/keymap.json".as_ref(),
                &r#"[{"bindings": {"backspace": "test_only::ActionB"}}]"#.into(),
                Default::default(),
            )
            .await
            .unwrap();

        executor.run_until_parked();

        assert_key_bindings_for(
            window.into(),
            cx,
            vec![("backspace", &ActionB), ("k", &ActivatePreviousPane)],
            line!(),
        );

        // Test modifying the base, while retaining the users keymap
        app_state
            .fs
            .save(
                paths::settings_file(),
                &r#"{"base_keymap": "JetBrains"}"#.into(),
                Default::default(),
            )
            .await
            .unwrap();

        executor.run_until_parked();

        assert_key_bindings_for(
            window.into(),
            cx,
            vec![
                ("backspace", &ActionB),
                ("{", &ActivatePreviousItem::default()),
            ],
            line!(),
        );
    }

    #[gpui::test]
    async fn test_disabled_keymap_binding(cx: &mut gpui::TestAppContext) {
        let executor = cx.executor();
        let app_state = init_keymap_test(cx);
        let project = Project::test(app_state.fs.clone(), [], cx).await;
        let window =
            cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
        let workspace = window
            .read_with(cx, |mw, _| mw.workspace().clone())
            .unwrap();

        // From the Atom keymap
        use workspace::ActivatePreviousPane;
        // From the JetBrains keymap
        use diagnostics::Deploy;

        window
            .update(cx, |_, _, cx| {
                workspace.update(cx, |workspace, cx| {
                    workspace.register_action(|_, _: &ActionA, _window, _cx| {});
                    workspace.register_action(|_, _: &ActionB, _window, _cx| {});
                    workspace.register_action(|_, _: &Deploy, _window, _cx| {});
                    cx.notify();
                });
            })
            .unwrap();
        app_state
            .fs
            .save(
                paths::settings_file(),
                &r#"{"base_keymap": "Atom"}"#.into(),
                Default::default(),
            )
            .await
            .unwrap();
        app_state
            .fs
            .save(
                "/keymap.json".as_ref(),
                &r#"[{"bindings": {"backspace": "test_only::ActionA"}}]"#.into(),
                Default::default(),
            )
            .await
            .unwrap();

        cx.update(|cx| {
            let (keymap_rx, keymap_watcher) = watch_config_file(
                &executor,
                app_state.fs.clone(),
                PathBuf::from("/keymap.json"),
            );

            watch_settings_files(app_state.fs.clone(), cx);
            handle_keymap_file_changes(keymap_rx, keymap_watcher, cx);
        });

        cx.background_executor.run_until_parked();

        cx.background_executor.run_until_parked();
        // Test loading the keymap base at all
        assert_key_bindings_for(
            window.into(),
            cx,
            vec![("backspace", &ActionA), ("k", &ActivatePreviousPane)],
            line!(),
        );

        // Test disabling the key binding for the base keymap
        app_state
            .fs
            .save(
                "/keymap.json".as_ref(),
                &r#"[{"bindings": {"backspace": null}}]"#.into(),
                Default::default(),
            )
            .await
            .unwrap();

        cx.background_executor.run_until_parked();

        assert_key_bindings_for(
            window.into(),
            cx,
            vec![("k", &ActivatePreviousPane)],
            line!(),
        );

        // Test modifying the base, while retaining the users keymap
        app_state
            .fs
            .save(
                paths::settings_file(),
                &r#"{"base_keymap": "JetBrains"}"#.into(),
                Default::default(),
            )
            .await
            .unwrap();

        cx.background_executor.run_until_parked();

        assert_key_bindings_for(window.into(), cx, vec![("6", &Deploy)], line!());
    }

    #[gpui::test]
    async fn test_generate_keymap_json_schema_for_registered_actions(
        cx: &mut gpui::TestAppContext,
    ) {
        init_keymap_test(cx);
        cx.update(|cx| {
            // Make sure it doesn't panic.
            KeymapFile::generate_json_schema_for_registered_actions(cx);
        });
    }

    /// Checks that action namespaces are the expected set. The purpose of this is to prevent typos
    /// and let you know when introducing a new namespace.
    #[gpui::test]
    async fn test_action_namespaces(cx: &mut gpui::TestAppContext) {
        use itertools::Itertools;

        init_keymap_test(cx);
        cx.update(|cx| {
            let all_actions = cx.all_action_names();

            let mut actions_without_namespace = Vec::new();
            let all_namespaces = all_actions
                .iter()
                .filter_map(|action_name| {
                    let namespace = action_name
                        .split("::")
                        .collect::<Vec<_>>()
                        .into_iter()
                        .rev()
                        .skip(1)
                        .rev()
                        .join("::");
                    if namespace.is_empty() {
                        actions_without_namespace.push(*action_name);
                    }
                    if &namespace == "test_only" || &namespace == "stories" {
                        None
                    } else {
                        Some(namespace)
                    }
                })
                .sorted()
                .dedup()
                .collect::<Vec<_>>();
            assert_eq!(actions_without_namespace, Vec::<&str>::new());

            let expected_namespaces = vec![
                "action",
                "activity_indicator",
                "agent",
                "agents_sidebar",
                "app_menu",
                "assistant",
                "assistant2",
                "auto_update",
                "branch_picker",
                "bedrock",
                "branches",
                "buffer_search",
                "channel_modal",
                "cli",
                "client",
                "collab",
                "collab_panel",
                "command_palette",
                "console",
                "context_server",
                "copilot",
                "csv",
                "debug_panel",
                "debugger",
                "dev",
                "diagnostics",
                "edit_prediction",
                "editor",
                "encoding_selector",
                "feedback",
                "file_finder",
                "git",
                "git_graph",
                "git_onboarding",
                "git_panel",
                "git_picker",
                "go_to_line",
                "highlights_tree_view",
                "icon_theme_selector",
                "image_viewer",
                "inline_assistant",
                "journal",
                "keymap_editor",
                "keystroke_input",
                "language_selector",
                "welcome",
                "line_ending_selector",
                "lsp_tool",
                "markdown",
                "menu",
                "multi_workspace",
                "new_process_modal",
                "notebook",
                "onboarding",
                "outline",
                "outline_panel",
                "pane",
                "panel",
                "picker",
                "project_panel",
                "project_search",
                "project_symbols",
                "projects",
                "recent_projects",
                "remote_debug",
                "repl",
                "rules_library",
                "search",
                "settings_editor",
                "settings_profile_selector",
                "snippets",
                "stash_picker",
                "svg",
                "syntax_tree_view",
                "tab_switcher",
                "task",
                "terminal",
                "terminal_panel",
                "theme",
                "theme_selector",
                "toast",
                "toolchain",
                "variable_list",
                "vim",
                "window",
                "workspace",
                "worktree_picker",
                "zed",
                "zed_actions",
                "zed_predict_onboarding",
                "zeta",
            ];
            assert_eq!(
                all_namespaces,
                expected_namespaces
                    .into_iter()
                    .map(|namespace| namespace.to_string())
                    .sorted()
                    .collect::<Vec<_>>()
            );
        });
    }

    #[gpui::test]
    fn test_bundled_settings_and_themes(cx: &mut App) {
        cx.text_system()
            .add_fonts(vec![
                Assets
                    .load("fonts/lilex/Lilex-Regular.ttf")
                    .unwrap()
                    .unwrap(),
                Assets
                    .load("fonts/ibm-plex-sans/IBMPlexSans-Regular.ttf")
                    .unwrap()
                    .unwrap(),
            ])
            .unwrap();
        let themes = ThemeRegistry::default();
        settings::init(cx);
        theme_settings::init(theme::LoadThemes::JustBase, cx);

        let mut has_default_theme = false;
        for theme_name in themes.list().into_iter().map(|meta| meta.name) {
            let theme = themes.get(&theme_name).unwrap();
            assert_eq!(theme.name, theme_name);
            if theme.name.as_ref() == "One Dark" {
                has_default_theme = true;
            }
        }
        assert!(has_default_theme);
    }

    #[gpui::test]
    async fn test_bundled_files_editor(cx: &mut TestAppContext) {
        let app_state = init_test(cx);
        cx.update(init);

        let project = Project::test(app_state.fs.clone(), [], cx).await;
        let _window = cx.add_window(|window, cx| MultiWorkspace::test_new(project, window, cx));

        cx.update(|cx| {
            cx.dispatch_action(&OpenDefaultSettings);
        });
        cx.run_until_parked();

        assert_eq!(cx.read(|cx| cx.windows().len()), 1);

        let multi_workspace = cx.windows()[0].downcast::<MultiWorkspace>().unwrap();
        let active_editor = multi_workspace
            .update(cx, |multi_workspace, _, cx| {
                multi_workspace
                    .workspace()
                    .update(cx, |workspace, cx| workspace.active_item_as::<Editor>(cx))
            })
            .unwrap();
        assert!(
            active_editor.is_some(),
            "Settings action should have opened an editor with the default file contents"
        );

        let active_editor = active_editor.unwrap();
        assert!(
            active_editor.read_with(cx, |editor, cx| editor.read_only(cx)),
            "Default settings should be readonly"
        );
        assert!(
            active_editor.read_with(cx, |editor, cx| editor.buffer().read(cx).read_only()),
            "The underlying buffer should also be readonly for the shipped default settings"
        );
    }

    #[gpui::test]
    async fn test_bundled_files_reuse_existing_editor(cx: &mut TestAppContext) {
        let app_state = init_test(cx);
        cx.update(init);

        let project = Project::test(app_state.fs.clone(), [], cx).await;
        let _window = cx.add_window(|window, cx| MultiWorkspace::test_new(project, window, cx));

        cx.update(|cx| {
            cx.dispatch_action(&OpenDefaultSettings);
        });
        cx.run_until_parked();

        let multi_workspace = cx.windows()[0].downcast::<MultiWorkspace>().unwrap();
        let first_item_id = multi_workspace
            .update(cx, |multi_workspace, _, cx| {
                multi_workspace.workspace().update(cx, |workspace, cx| {
                    workspace
                        .active_item(cx)
                        .expect("default settings should be open")
                        .item_id()
                })
            })
            .unwrap();

        cx.update(|cx| {
            cx.dispatch_action(&OpenDefaultSettings);
        });
        cx.run_until_parked();

        let (second_item_id, item_count) = multi_workspace
            .update(cx, |multi_workspace, _, cx| {
                multi_workspace.workspace().update(cx, |workspace, cx| {
                    let pane = workspace.active_pane().read(cx);
                    (
                        pane.active_item()
                            .expect("default settings should still be open")
                            .item_id(),
                        pane.items_len(),
                    )
                })
            })
            .unwrap();

        assert_eq!(first_item_id, second_item_id);
        assert_eq!(item_count, 1);
    }

    #[gpui::test]
    async fn test_bundled_languages(cx: &mut TestAppContext) {
        let fs = fs::FakeFs::new(cx.background_executor.clone());
        env_logger::builder().is_test(true).try_init().ok();
        let settings = cx.update(SettingsStore::test);
        cx.set_global(settings);
        let languages = LanguageRegistry::test(cx.executor());
        let languages = Arc::new(languages);
        let node_runtime = node_runtime::NodeRuntime::unavailable();
        cx.update(|cx| {
            languages::init(languages.clone(), fs, node_runtime, cx);
        });
        for name in languages.language_names() {
            languages
                .language_for_name(name.as_ref())
                .await
                .with_context(|| format!("language name {name}"))
                .unwrap();
        }
        cx.run_until_parked();
    }

    pub(crate) fn init_test(cx: &mut TestAppContext) -> Arc<AppState> {
        init_test_with_state(cx, cx.update(AppState::test))
    }

    fn init_test_with_state(
        cx: &mut TestAppContext,
        mut app_state: Arc<AppState>,
    ) -> Arc<AppState> {
        cx.update(move |cx| {
            env_logger::builder().is_test(true).try_init().ok();

            let state = Arc::get_mut(&mut app_state).unwrap();
            state.build_window_options = build_window_options;
            app_state.languages.add(markdown_lang());

            gpui_tokio::init(cx);
            AppState::set_global(app_state.clone(), cx);
            theme_settings::init(theme::LoadThemes::JustBase, cx);
            audio::init(cx);
            channel::init(&app_state.client, app_state.user_store.clone(), cx);
            call::init(app_state.client.clone(), app_state.user_store.clone(), cx);
            notifications::init(app_state.client.clone(), app_state.user_store.clone(), cx);
            workspace::init(app_state.clone(), cx);
            release_channel::init(Version::new(0, 0, 0), cx);
            command_palette::init(cx);
            editor::init(cx);
            collab_ui::init(&app_state, cx);
            git_ui::init(cx);
            project_panel::init(cx);
            outline_panel::init(cx);
            terminal_view::init(cx);
            copilot_chat::init(
                app_state.fs.clone(),
                app_state.client.http_client(),
                copilot_chat::CopilotChatConfiguration::default(),
                cx,
            );
            image_viewer::init(cx);
            language_model::init(cx);
            client::RefreshLlmTokenListener::register(
                app_state.client.clone(),
                app_state.user_store.clone(),
                cx,
            );
            language_models::init(app_state.user_store.clone(), app_state.client.clone(), cx);
            web_search::init(cx);
            git_graph::init(cx);
            web_search_providers::init(app_state.client.clone(), app_state.user_store.clone(), cx);
            let prompt_builder = PromptBuilder::load(app_state.fs.clone(), false, cx);
            project::AgentRegistryStore::init_global(
                cx,
                app_state.fs.clone(),
                app_state.client.http_client(),
            );
            agent_ui::init(
                app_state.fs.clone(),
                prompt_builder,
                app_state.languages.clone(),
                true,
                false,
                cx,
            );

            repl::init(app_state.fs.clone(), cx);
            repl::notebook::init(cx);
            tasks_ui::init(cx);
            project::debugger::breakpoint_store::BreakpointStore::init(
                &app_state.client.clone().into(),
            );
            project::debugger::dap_store::DapStore::init(&app_state.client.clone().into(), cx);
            debugger_ui::init(cx);
            initialize_workspace(app_state.clone(), cx);
            search::init(cx);
            cx.set_global(workspace::PaneSearchBarCallbacks {
                setup_search_bar: |languages, toolbar, window, cx| {
                    let search_bar =
                        cx.new(|cx| search::BufferSearchBar::new(languages, window, cx));
                    toolbar.update(cx, |toolbar, cx| {
                        toolbar.add_item(search_bar, window, cx);
                    });
                },
                wrap_div_with_search_actions: search::buffer_search::register_pane_search_actions,
            });
            app_state
        })
    }

    #[track_caller]
    fn assert_key_bindings_for(
        window: AnyWindowHandle,
        cx: &TestAppContext,
        actions: Vec<(&'static str, &dyn Action)>,
        line: u32,
    ) {
        let available_actions = cx
            .update(|cx| window.update(cx, |_, window, cx| window.available_actions(cx)))
            .unwrap();
        for (key, action) in actions {
            let bindings = cx
                .update(|cx| window.update(cx, |_, window, _| window.bindings_for_action(action)))
                .unwrap();
            // assert that...
            assert!(
                available_actions.iter().any(|bound_action| {
                    // actions match...
                    bound_action.partial_eq(action)
                }),
                "On {} Failed to find {}",
                line,
                action.name(),
            );
            assert!(
                // and key strokes contain the given key
                bindings
                    .into_iter()
                    .any(|binding| binding.keystrokes().iter().any(|k| k.key() == key)),
                "On {} Failed to find {} with key binding {}",
                line,
                action.name(),
                key
            );
        }
    }

    #[gpui::test]
    async fn test_opening_project_settings_when_excluded(cx: &mut gpui::TestAppContext) {
        // Use the proper initialization for runtime state
        let app_state = init_keymap_test(cx);

        eprintln!("Running test_opening_project_settings_when_excluded");

        // 1. Set up a project with some project settings
        let settings_init =
            r#"{ "UNIQUEVALUE": true, "git": { "inline_blame": { "enabled": false } } }"#;
        app_state
            .fs
            .as_fake()
            .insert_tree(
                Path::new("/root"),
                json!({
                    ".zed": {
                        "settings.json": settings_init
                    }
                }),
            )
            .await;

        eprintln!("Created project with .zed/settings.json containing UNIQUEVALUE");

        // 2. Create a project with the file system and load it
        let project = Project::test(app_state.fs.clone(), [Path::new("/root")], cx).await;

        // Save original settings content for comparison
        let original_settings = app_state
            .fs
            .load(Path::new("/root/.zed/settings.json"))
            .await
            .unwrap();

        let original_settings_str = original_settings.clone();

        // Verify settings exist on disk and have expected content
        eprintln!("Original settings content: {}", original_settings_str);
        assert!(
            original_settings_str.contains("UNIQUEVALUE"),
            "Test setup failed - settings file doesn't contain our marker"
        );

        // 3. Add .zed to file scan exclusions in user settings
        cx.update_global::<SettingsStore, _>(|store, cx| {
            store.update_user_settings(cx, |worktree_settings| {
                worktree_settings.project.worktree.file_scan_exclusions =
                    Some(vec![".zed".to_string()]);
            });
        });

        eprintln!("Added .zed to file_scan_exclusions in settings");

        // 4. Run tasks to apply settings
        cx.background_executor.run_until_parked();

        // 5. Critical: Verify .zed is actually excluded from worktree
        let worktree = cx.update(|cx| project.read(cx).worktrees(cx).next().unwrap());

        let has_zed_entry =
            cx.update(|cx| worktree.read(cx).entry_for_path(rel_path(".zed")).is_some());

        eprintln!(
            "Is .zed directory visible in worktree after exclusion: {}",
            has_zed_entry
        );

        // This assertion verifies the test is set up correctly to show the bug
        // If .zed is not excluded, the test will fail here
        assert!(
            !has_zed_entry,
            "Test precondition failed: .zed directory should be excluded but was found in worktree"
        );

        // 6. Create workspace and trigger the actual function that causes the bug
        let window =
            cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
        let workspace = window
            .read_with(cx, |mw, _| mw.workspace().clone())
            .unwrap();
        window
            .update(cx, |_, window, cx| {
                workspace.update(cx, |workspace, cx| {
                    // Call the exact function that contains the bug
                    eprintln!("About to call open_project_settings_file");
                    open_project_settings_file(workspace, &OpenProjectSettingsFile, window, cx);
                });
            })
            .unwrap();

        // 7. Run background tasks until completion
        cx.background_executor.run_until_parked();

        // 8. Verify file contents after calling function
        let new_content = app_state
            .fs
            .load(Path::new("/root/.zed/settings.json"))
            .await
            .unwrap();

        let new_content_str = new_content;
        eprintln!("New settings content: {}", new_content_str);

        // The bug causes the settings to be overwritten with empty settings
        // So if the unique value is no longer present, the bug has been reproduced
        let bug_exists = !new_content_str.contains("UNIQUEVALUE");
        eprintln!("Bug reproduced: {}", bug_exists);

        // This assertion should fail if the bug exists - showing the bug is real
        assert!(
            new_content_str.contains("UNIQUEVALUE"),
            "BUG FOUND: Project settings were overwritten when opening via command - original custom content was lost"
        );
    }

    #[gpui::test]
    async fn test_disable_ai_crash(cx: &mut gpui::TestAppContext) {
        let app_state = init_test(cx);
        cx.update(init);
        let project = Project::test(app_state.fs.clone(), [], cx).await;
        let _window = cx.add_window(|window, cx| MultiWorkspace::test_new(project, window, cx));

        cx.run_until_parked();

        cx.update(|cx| {
            SettingsStore::update_global(cx, |settings_store, cx| {
                settings_store.update_user_settings(cx, |settings| {
                    settings.project.disable_ai = Some(SaturatingBool(true));
                });
            });
        });

        cx.run_until_parked();

        // If this panics, the test has failed
    }

    #[gpui::test]
    async fn test_prefer_focused_window(cx: &mut gpui::TestAppContext) {
        let app_state = init_test(cx);
        let paths = [PathBuf::from(path!("/dir/document.txt"))];

        app_state
            .fs
            .as_fake()
            .insert_tree(
                path!("/dir"),
                json!({
                    "document.txt": "Some of the documentation's content."
                }),
            )
            .await;

        let project_a = Project::test(app_state.fs.clone(), [path!("/dir").as_ref()], cx).await;
        let window_a = cx.add_window({
            let project = project_a.clone();
            |window, cx| MultiWorkspace::test_new(project, window, cx)
        });

        let project_b = Project::test(app_state.fs.clone(), [path!("/dir").as_ref()], cx).await;
        let window_b = cx.add_window({
            let project = project_b.clone();
            |window, cx| MultiWorkspace::test_new(project, window, cx)
        });

        let project_c = Project::test(app_state.fs.clone(), [path!("/dir").as_ref()], cx).await;
        let window_c = cx.add_window({
            let project = project_c.clone();
            |window, cx| MultiWorkspace::test_new(project, window, cx)
        });

        for window in [window_a, window_b, window_c] {
            let _ = cx.update_window(*window, |_, window, _| {
                window.activate_window();
            });

            cx.update(|cx| {
                let open_options = OpenOptions {
                    wait: true,
                    ..Default::default()
                };

                workspace::open_paths(&paths, app_state.clone(), open_options, cx)
            })
            .await
            .unwrap();

            cx.update_window(*window, |_, window, _| assert!(window.is_window_active()))
                .unwrap();

            let _ = window.read_with(cx, |multi_workspace, cx| {
                let pane = multi_workspace.workspace().read(cx).active_pane().read(cx);
                let project_path = pane.active_item().unwrap().project_path(cx).unwrap();

                assert_eq!(
                    project_path.path.as_ref().as_std_path().to_str().unwrap(),
                    path!("document.txt")
                )
            });
        }
    }

    #[gpui::test]
    async fn test_open_paths_switches_to_best_workspace(cx: &mut TestAppContext) {
        let app_state = init_test(cx);

        app_state
            .fs
            .as_fake()
            .insert_tree(
                path!("/"),
                json!({
                    "dir1": {
                        "a.txt": "content a"
                    },
                    "dir2": {
                        "b.txt": "content b"
                    },
                    "dir3": {
                        "c.txt": "content c"
                    }
                }),
            )
            .await;

        // Create a window with workspace 0 containing /dir1
        let project1 = Project::test(app_state.fs.clone(), [path!("/dir1").as_ref()], cx).await;

        let window = cx.add_window({
            let project = project1.clone();
            |window, cx| MultiWorkspace::test_new(project, window, cx)
        });
        window
            .update(cx, |multi_workspace, _, cx| {
                multi_workspace.open_sidebar(cx);
            })
            .unwrap();

        cx.run_until_parked();
        assert_eq!(cx.windows().len(), 1, "Should start with 1 window");

        // Create workspace 2 with /dir2
        let project2 = Project::test(app_state.fs.clone(), [path!("/dir2").as_ref()], cx).await;
        let workspace2 = window
            .update(cx, |multi_workspace, window, cx| {
                multi_workspace.test_add_workspace(project2.clone(), window, cx)
            })
            .unwrap();

        // Create workspace 3 with /dir3
        let project3 = Project::test(app_state.fs.clone(), [path!("/dir3").as_ref()], cx).await;
        let workspace3 = window
            .update(cx, |multi_workspace, window, cx| {
                multi_workspace.test_add_workspace(project3.clone(), window, cx)
            })
            .unwrap();

        let workspace1 = window
            .read_with(cx, |multi_workspace, _| {
                multi_workspace.workspaces().next().unwrap().clone()
            })
            .unwrap();

        window
            .update(cx, |multi_workspace, window, cx| {
                multi_workspace.activate(workspace2.clone(), None, window, cx);
                multi_workspace.activate(workspace3.clone(), None, window, cx);
                // Switch back to workspace1 for test setup
                multi_workspace.activate(workspace1.clone(), None, window, cx);
                assert_eq!(multi_workspace.workspace(), &workspace1);
            })
            .unwrap();

        cx.run_until_parked();

        // Verify setup: 3 workspaces, workspace 0 active, still 1 window
        window
            .read_with(cx, |multi_workspace, _| {
                assert_eq!(multi_workspace.workspaces().count(), 3);
                assert_eq!(multi_workspace.workspace(), &workspace1);
            })
            .unwrap();
        assert_eq!(cx.windows().len(), 1);

        // Open a file in /dir3 - should switch to workspace 3 (not just "the other one")
        cx.update(|cx| {
            open_paths(
                &[PathBuf::from(path!("/dir3/c.txt"))],
                app_state.clone(),
                OpenOptions::default(),
                cx,
            )
        })
        .await
        .unwrap();

        cx.run_until_parked();

        // Verify workspace 2 is active and file opened there
        window
            .read_with(cx, |multi_workspace, cx| {
                assert_eq!(
                    multi_workspace.workspace(),
                    &workspace3,
                    "Should have switched to workspace 3 which contains /dir3"
                );
                let active_item = multi_workspace
                    .workspace()
                    .read(cx)
                    .active_pane()
                    .read(cx)
                    .active_item()
                    .expect("Should have an active item");
                assert_eq!(active_item.tab_content_text(0, cx), "c.txt");
            })
            .unwrap();
        assert_eq!(cx.windows().len(), 1, "Should reuse existing window");

        // Open a file in /dir2 - should switch to workspace 2
        cx.update(|cx| {
            open_paths(
                &[PathBuf::from(path!("/dir2/b.txt"))],
                app_state.clone(),
                OpenOptions::default(),
                cx,
            )
        })
        .await
        .unwrap();

        cx.run_until_parked();

        // Verify workspace 1 is active and file opened there
        window
            .read_with(cx, |multi_workspace, cx| {
                assert_eq!(
                    multi_workspace.workspace(),
                    &workspace2,
                    "Should have switched to workspace 2 which contains /dir2"
                );
                let active_item = multi_workspace
                    .workspace()
                    .read(cx)
                    .active_pane()
                    .read(cx)
                    .active_item()
                    .expect("Should have an active item");
                assert_eq!(active_item.tab_content_text(0, cx), "b.txt");
            })
            .unwrap();

        // Verify c.txt is still in workspace 3 (file opened in correct workspace, not active one)
        workspace3.read_with(cx, |workspace, cx| {
            let active_item = workspace
                .active_pane()
                .read(cx)
                .active_item()
                .expect("Workspace 2 should have an active item");
            assert_eq!(
                active_item.tab_content_text(0, cx),
                "c.txt",
                "c.txt should have been opened in workspace 3, not the active workspace"
            );
        });

        assert_eq!(cx.windows().len(), 1, "Should still have only 1 window");

        // Open a file in /dir1 - should switch back to workspace 0
        cx.update(|cx| {
            open_paths(
                &[PathBuf::from(path!("/dir1/a.txt"))],
                app_state.clone(),
                OpenOptions::default(),
                cx,
            )
        })
        .await
        .unwrap();

        cx.run_until_parked();

        // Verify workspace 0 is active and file opened there
        window
            .read_with(cx, |multi_workspace, cx| {
                assert_eq!(
                    multi_workspace.workspace(),
                    &workspace1,
                    "Should have switched back to workspace 0 which contains /dir1"
                );
                let active_item = multi_workspace
                    .workspace()
                    .read(cx)
                    .active_pane()
                    .read(cx)
                    .active_item()
                    .expect("Should have an active item");
                assert_eq!(active_item.tab_content_text(0, cx), "a.txt");
            })
            .unwrap();
        assert_eq!(cx.windows().len(), 1, "Should still have only 1 window");
    }

    #[gpui::test]
    async fn test_quit_checks_all_workspaces_for_dirty_items(cx: &mut TestAppContext) {
        let app_state = init_test(cx);
        cx.update(init);

        app_state
            .fs
            .as_fake()
            .insert_tree(
                path!("/"),
                json!({
                    "dir1": {
                        "a.txt": "content a"
                    },
                    "dir2": {
                        "b.txt": "content b"
                    },
                    "dir3": {
                        "c.txt": "content c"
                    }
                }),
            )
            .await;

        // === Setup Window 1 with two workspaces ===
        let project1 = Project::test(app_state.fs.clone(), [path!("/dir1").as_ref()], cx).await;
        let window1 = cx.add_window({
            let project = project1.clone();
            |window, cx| MultiWorkspace::test_new(project, window, cx)
        });
        window1
            .update(cx, |multi_workspace, _, cx| {
                multi_workspace.open_sidebar(cx);
            })
            .unwrap();

        cx.run_until_parked();

        let project2 = Project::test(app_state.fs.clone(), [path!("/dir2").as_ref()], cx).await;
        let workspace1_1 = window1
            .read_with(cx, |multi_workspace, _| multi_workspace.workspace().clone())
            .unwrap();
        let workspace1_2 = window1
            .update(cx, |multi_workspace, window, cx| {
                multi_workspace.test_add_workspace(project2.clone(), window, cx)
            })
            .unwrap();

        window1
            .update(cx, |multi_workspace, window, cx| {
                multi_workspace.activate(workspace1_2.clone(), None, window, cx);
                multi_workspace.activate(workspace1_1.clone(), None, window, cx);
            })
            .unwrap();

        // === Setup Window 2 with one workspace ===
        let project3 = Project::test(app_state.fs.clone(), [path!("/dir3").as_ref()], cx).await;
        let window2 = cx.add_window({
            let project = project3.clone();
            |window, cx| MultiWorkspace::test_new(project, window, cx)
        });
        window2
            .update(cx, |multi_workspace, _, cx| {
                multi_workspace.open_sidebar(cx);
            })
            .unwrap();

        cx.run_until_parked();
        assert_eq!(cx.windows().len(), 2);

        // === Case 1: Active workspace has dirty item, quit can be cancelled ===
        let worktree1_id = project1.update(cx, |project, cx| {
            project.worktrees(cx).next().unwrap().read(cx).id()
        });

        let editor1 = window1
            .update(cx, |_, window, cx| {
                workspace1_1.update(cx, |workspace, cx| {
                    workspace.open_path((worktree1_id, rel_path("a.txt")), None, true, window, cx)
                })
            })
            .unwrap()
            .await
            .unwrap()
            .downcast::<Editor>()
            .unwrap();

        window1
            .update(cx, |_, window, cx| {
                editor1.update(cx, |editor, cx| {
                    editor.insert("dirty in active workspace", window, cx);
                });
            })
            .unwrap();

        cx.run_until_parked();

        // Verify workspace1_1 is active
        window1
            .read_with(cx, |multi_workspace, _| {
                assert_eq!(multi_workspace.workspace(), &workspace1_1);
            })
            .unwrap();

        cx.dispatch_action(*window1, Quit);
        cx.run_until_parked();

        assert!(
            cx.has_pending_prompt(),
            "Case 1: Should prompt to save dirty item in active workspace"
        );

        cx.simulate_prompt_answer("Cancel");
        cx.run_until_parked();

        assert_eq!(
            cx.windows().len(),
            2,
            "Case 1: Windows should still exist after cancelling quit"
        );

        // Clean up Case 1: Close the dirty item without saving
        let close_task = window1
            .update(cx, |_, window, cx| {
                workspace1_1.update(cx, |workspace, cx| {
                    workspace.active_pane().update(cx, |pane, cx| {
                        pane.close_active_item(&Default::default(), window, cx)
                    })
                })
            })
            .unwrap();
        cx.run_until_parked();
        cx.simulate_prompt_answer("Don't Save");
        close_task.await.ok();
        cx.run_until_parked();

        // === Case 2: Non-active workspace (same window) has dirty item ===
        let worktree2_id = project2.update(cx, |project, cx| {
            project.worktrees(cx).next().unwrap().read(cx).id()
        });

        let editor2 = window1
            .update(cx, |_, window, cx| {
                workspace1_2.update(cx, |workspace, cx| {
                    workspace.open_path((worktree2_id, rel_path("b.txt")), None, true, window, cx)
                })
            })
            .unwrap()
            .await
            .unwrap()
            .downcast::<Editor>()
            .unwrap();

        window1
            .update(cx, |_, window, cx| {
                editor2.update(cx, |editor, cx| {
                    editor.insert("dirty in non-active workspace", window, cx);
                });
            })
            .unwrap();

        cx.run_until_parked();

        // Verify workspace1_1 is still active (not workspace1_2 with dirty item)
        window1
            .read_with(cx, |multi_workspace, _| {
                assert_eq!(multi_workspace.workspace(), &workspace1_1);
            })
            .unwrap();

        cx.dispatch_action(*window1, Quit);
        cx.run_until_parked();

        // Verify the non-active workspace got activated to show the dirty item
        window1
            .read_with(cx, |multi_workspace, _| {
                assert_eq!(
                    multi_workspace.workspace(),
                    &workspace1_2,
                    "Case 2: Non-active workspace should be activated when it has dirty item"
                );
            })
            .unwrap();

        assert!(
            cx.has_pending_prompt(),
            "Case 2: Should prompt to save dirty item in non-active workspace"
        );

        cx.simulate_prompt_answer("Cancel");
        cx.run_until_parked();

        assert_eq!(
            cx.windows().len(),
            2,
            "Case 2: Windows should still exist after cancelling quit"
        );

        // Clean up Case 2: Close the dirty item without saving
        let close_task = window1
            .update(cx, |_, window, cx| {
                workspace1_2.update(cx, |workspace, cx| {
                    workspace.active_pane().update(cx, |pane, cx| {
                        pane.close_active_item(&Default::default(), window, cx)
                    })
                })
            })
            .unwrap();
        cx.run_until_parked();
        cx.simulate_prompt_answer("Don't Save");
        close_task.await.ok();
        cx.run_until_parked();

        // === Case 3: Non-active window has dirty item ===
        let workspace3 = window2
            .read_with(cx, |multi_workspace, _| multi_workspace.workspace().clone())
            .unwrap();

        let worktree3_id = project3.update(cx, |project, cx| {
            project.worktrees(cx).next().unwrap().read(cx).id()
        });

        let editor3 = window2
            .update(cx, |_, window, cx| {
                workspace3.update(cx, |workspace, cx| {
                    workspace.open_path((worktree3_id, rel_path("c.txt")), None, true, window, cx)
                })
            })
            .unwrap()
            .await
            .unwrap()
            .downcast::<Editor>()
            .unwrap();

        window2
            .update(cx, |_, window, cx| {
                editor3.update(cx, |editor, cx| {
                    editor.insert("dirty in other window", window, cx);
                });
            })
            .unwrap();

        cx.run_until_parked();

        // Activate window1 explicitly (editing in window2 may have activated it)
        window1
            .update(cx, |_, window, _| window.activate_window())
            .unwrap();
        cx.run_until_parked();

        // Verify window2 is not active (window1 should still be active)
        assert_eq!(
            cx.update(|cx| window2.is_active(cx)),
            Some(false),
            "Case 3: window2 should not be active before quit"
        );

        // Dispatch quit from window1 (window2 has the dirty item)
        cx.dispatch_action(*window1, Quit);
        cx.run_until_parked();

        // Verify window2 is now active (quit handler activated it to show dirty item)
        assert_eq!(
            cx.update(|cx| window2.is_active(cx)),
            Some(true),
            "Case 3: window2 should be activated when it has dirty item"
        );

        assert!(
            cx.has_pending_prompt(),
            "Case 3: Should prompt to save dirty item in non-active window"
        );

        cx.simulate_prompt_answer("Cancel");
        cx.run_until_parked();

        assert_eq!(
            cx.windows().len(),
            2,
            "Case 3: Windows should still exist after cancelling quit"
        );
    }

    #[gpui::test]
    async fn test_multi_workspace_session_restore(cx: &mut TestAppContext) {
        use collections::HashMap;
        use session::Session;
        use util::path_list::PathList;
        use workspace::{OpenMode, ProjectGroupKey, Workspace, WorkspaceId};

        let app_state = init_test(cx);

        let dir1 = path!("/dir1");
        let dir2 = path!("/dir2");
        let dir3 = path!("/dir3");

        let fs = app_state.fs.clone();
        let fake_fs = fs.as_fake();
        fake_fs.insert_tree(dir1, json!({})).await;
        fake_fs.insert_tree(dir2, json!({})).await;
        fake_fs.insert_tree(dir3, json!({})).await;

        let session_id = cx.read(|cx| app_state.session.read(cx).id().to_owned());

        // --- Create 3 workspaces in 2 windows ---
        //
        //   Window A: workspace for dir1, workspace for dir2
        //   Window B: workspace for dir3
        let workspace::OpenResult {
            window: window_a, ..
        } = cx
            .update(|cx| {
                Workspace::new_local(
                    vec![dir1.into()],
                    app_state.clone(),
                    None,
                    None,
                    None,
                    OpenMode::Activate,
                    cx,
                )
            })
            .await
            .expect("failed to open first workspace");

        window_a
            .update(cx, |multi_workspace, _, cx| {
                multi_workspace.open_sidebar(cx);
            })
            .unwrap();

        window_a
            .update(cx, |multi_workspace, window, cx| {
                multi_workspace.open_project(vec![dir2.into()], OpenMode::Activate, window, cx)
            })
            .unwrap()
            .await
            .expect("failed to open second workspace into window A");
        cx.run_until_parked();

        let workspace::OpenResult {
            window: window_b, ..
        } = cx
            .update(|cx| {
                Workspace::new_local(
                    vec![dir3.into()],
                    app_state.clone(),
                    None,
                    None,
                    None,
                    OpenMode::Activate,
                    cx,
                )
            })
            .await
            .expect("failed to open third workspace");

        window_b
            .update(cx, |multi_workspace, _, cx| {
                multi_workspace.open_sidebar(cx);
            })
            .unwrap();

        // Currently dir2 is active because it was added last.
        // So, switch window_a's active workspace to dir1 (index 0).
        // This sets up a non-trivial assertion: after restore, dir1 should
        // still be active rather than whichever workspace happened to restore last.
        window_a
            .update(cx, |multi_workspace, window, cx| {
                let workspace = multi_workspace.workspaces().next().unwrap().clone();
                multi_workspace.activate(workspace, None, window, cx);
            })
            .unwrap();

        cx.run_until_parked();
        flush_workspace_serialization(&window_a, cx).await;
        flush_workspace_serialization(&window_b, cx).await;
        cx.run_until_parked();

        // Verify all workspaces retained their session_ids.
        let db = cx.update(|cx| workspace::WorkspaceDb::global(cx));
        let locations =
            workspace::last_session_workspace_locations(&db, &session_id, None, fs.as_ref())
                .await
                .expect("expected session workspace locations");
        assert_eq!(
            locations.len(),
            3,
            "all 3 workspaces should have session_ids in the DB"
        );

        // Close the original windows.
        window_a
            .update(cx, |_, window, _| window.remove_window())
            .unwrap();
        window_b
            .update(cx, |_, window, _| window.remove_window())
            .unwrap();
        cx.run_until_parked();

        // Simulate a new session launch: replace the session so that
        // `last_session_id()` returns the ID used during workspace creation.
        // `restore_on_startup` defaults to `LastSession`, which is what we need.
        cx.update(|cx| {
            app_state.session.update(cx, |app_session, _cx| {
                app_session
                    .replace_session_for_test(Session::test_with_old_session(session_id.clone()));
            });
        });

        // --- Read back from DB and verify grouping ---
        let locations =
            workspace::last_session_workspace_locations(&db, &session_id, None, fs.as_ref())
                .await
                .expect("expected session workspace locations");

        assert_eq!(locations.len(), 3, "expected 3 session workspaces");

        let mut groups_by_window: HashMap<gpui::WindowId, Vec<WorkspaceId>> = HashMap::default();
        for session_workspace in &locations {
            if let Some(window_id) = session_workspace.window_id {
                groups_by_window
                    .entry(window_id)
                    .or_default()
                    .push(session_workspace.workspace_id);
            }
        }
        assert_eq!(
            groups_by_window.len(),
            2,
            "expected 2 window groups, got {groups_by_window:?}"
        );
        assert!(
            groups_by_window.values().any(|g| g.len() == 2),
            "expected one group with 2 workspaces"
        );
        assert!(
            groups_by_window.values().any(|g| g.len() == 1),
            "expected one group with 1 workspace"
        );

        let mut async_cx = cx.to_async();
        crate::restore_or_create_workspace(app_state.clone(), &mut async_cx)
            .await
            .expect("failed to restore workspaces");
        cx.run_until_parked();

        // --- Verify the restored windows ---
        let restored_windows: Vec<WindowHandle<MultiWorkspace>> = cx.read(|cx| {
            cx.windows()
                .into_iter()
                .filter_map(|window| window.downcast::<MultiWorkspace>())
                .collect()
        });
        assert_eq!(restored_windows.len(), 2,);

        // Identify restored windows by their active workspace root paths.
        let (restored_a, restored_b) = {
            let (mut with_dir1, mut with_dir3) = (None, None);
            for window in &restored_windows {
                let active_paths = window
                    .read_with(cx, |mw, cx| mw.workspace().read(cx).root_paths(cx))
                    .unwrap();
                if active_paths.iter().any(|p| p.as_ref() == Path::new(dir1)) {
                    with_dir1 = Some(window);
                } else {
                    with_dir3 = Some(window);
                }
            }
            (
                with_dir1.expect("expected a window with dir1 active"),
                with_dir3.expect("expected a window with dir3 active"),
            )
        };

        // Window A (dir1+dir2): 1 workspace restored, but 2 project group keys.
        restored_a
            .read_with(cx, |mw, _| {
                assert_eq!(
                    mw.project_group_keys(),
                    vec![
                        ProjectGroupKey::new(None, PathList::new(&[dir2])),
                        ProjectGroupKey::new(None, PathList::new(&[dir1])),
                    ]
                );
                assert_eq!(mw.workspaces().count(), 1);
            })
            .unwrap();

        // Window B (dir3): 1 workspace, 1 project group key.
        restored_b
            .read_with(cx, |mw, _| {
                assert_eq!(
                    mw.project_group_keys(),
                    vec![ProjectGroupKey::new(None, PathList::new(&[dir3]))]
                );
                assert_eq!(mw.workspaces().count(), 1);
            })
            .unwrap();
    }

    #[gpui::test]
    async fn test_restored_project_groups_survive_workspace_key_change(cx: &mut TestAppContext) {
        use session::Session;
        use util::path_list::PathList;
        use workspace::{OpenMode, ProjectGroupKey};

        let app_state = init_test(cx);

        let fs = app_state.fs.clone();
        let fake_fs = fs.as_fake();
        fake_fs
            .insert_tree(path!("/root_a"), json!({ "file.txt": "" }))
            .await;
        fake_fs
            .insert_tree(path!("/root_b"), json!({ "file.txt": "" }))
            .await;
        fake_fs
            .insert_tree(path!("/root_c"), json!({ "file.txt": "" }))
            .await;
        fake_fs
            .insert_tree(path!("/root_d"), json!({ "other.txt": "" }))
            .await;

        let session_id = cx.read(|cx| app_state.session.read(cx).id().to_owned());

        // --- Phase 1: Build a multi-workspace with 3 project groups ---

        let workspace::OpenResult { window, .. } = cx
            .update(|cx| {
                workspace::Workspace::new_local(
                    vec![path!("/root_a").into()],
                    app_state.clone(),
                    None,
                    None,
                    None,
                    OpenMode::Activate,
                    cx,
                )
            })
            .await
            .expect("failed to open workspace");

        window.update(cx, |mw, _, cx| mw.open_sidebar(cx)).unwrap();

        window
            .update(cx, |mw, window, cx| {
                mw.open_project(vec![path!("/root_b").into()], OpenMode::Add, window, cx)
            })
            .unwrap()
            .await
            .expect("failed to add root_b");

        window
            .update(cx, |mw, window, cx| {
                mw.open_project(vec![path!("/root_c").into()], OpenMode::Add, window, cx)
            })
            .unwrap()
            .await
            .expect("failed to add root_c");
        cx.run_until_parked();

        let key_b = ProjectGroupKey::new(None, PathList::new(&[path!("/root_b")]));
        let key_c = ProjectGroupKey::new(None, PathList::new(&[path!("/root_c")]));

        // Make root_a the active workspace so it's the one eagerly restored.
        window
            .update(cx, |mw, window, cx| {
                let workspace_a = mw
                    .workspaces()
                    .find(|ws| {
                        ws.read(cx)
                            .root_paths(cx)
                            .iter()
                            .any(|p| p.as_ref() == Path::new(path!("/root_a")))
                    })
                    .expect("workspace_a should exist")
                    .clone();
                mw.activate(workspace_a, None, window, cx);
            })
            .unwrap();
        cx.run_until_parked();

        // --- Phase 2: Serialize, close, and restore ---

        flush_workspace_serialization(&window, cx).await;
        cx.run_until_parked();

        window
            .update(cx, |_, window, _| window.remove_window())
            .unwrap();
        cx.run_until_parked();

        cx.update(|cx| {
            app_state.session.update(cx, |app_session, _cx| {
                app_session
                    .replace_session_for_test(Session::test_with_old_session(session_id.clone()));
            });
        });

        let mut async_cx = cx.to_async();
        crate::restore_or_create_workspace(app_state.clone(), &mut async_cx)
            .await
            .expect("failed to restore workspace");
        cx.run_until_parked();

        let restored_windows: Vec<WindowHandle<MultiWorkspace>> = cx.read(|cx| {
            cx.windows()
                .into_iter()
                .filter_map(|w| w.downcast::<MultiWorkspace>())
                .collect()
        });
        assert_eq!(restored_windows.len(), 1);
        let restored = &restored_windows[0];

        // Verify the restored window has all 3 project groups.
        restored
            .read_with(cx, |mw, _cx| {
                let keys = mw.project_group_keys();
                assert_eq!(
                    keys.len(),
                    3,
                    "restored window should have 3 groups; got {keys:?}"
                );
                assert!(keys.contains(&key_b), "should contain key_b");
                assert!(keys.contains(&key_c), "should contain key_c");
            })
            .unwrap();

        // --- Phase 3: Trigger a workspace key change and verify survival ---

        let active_project = restored
            .read_with(cx, |mw, cx| mw.workspace().read(cx).project().clone())
            .unwrap();

        active_project
            .update(cx, |project, cx| {
                project.find_or_create_worktree(path!("/root_d"), true, cx)
            })
            .await
            .expect("adding worktree should succeed");
        cx.run_until_parked();

        restored
            .read_with(cx, |mw, _cx| {
                let keys = mw.project_group_keys();
                assert!(
                    keys.contains(&key_b),
                    "restored group key_b should survive a workspace key change; got {keys:?}"
                );
                assert!(
                    keys.contains(&key_c),
                    "restored group key_c should survive a workspace key change; got {keys:?}"
                );
            })
            .unwrap();
    }

    #[gpui::test]
    async fn test_close_project_removes_project_group(cx: &mut TestAppContext) {
        use util::path_list::PathList;
        use workspace::{OpenMode, ProjectGroupKey};

        let app_state = init_test(cx);
        app_state
            .fs
            .as_fake()
            .insert_tree(path!("/my-project"), json!({}))
            .await;

        let workspace::OpenResult { window, .. } = cx
            .update(|cx| {
                workspace::Workspace::new_local(
                    vec![path!("/my-project").into()],
                    app_state.clone(),
                    None,
                    None,
                    None,
                    OpenMode::Activate,
                    cx,
                )
            })
            .await
            .unwrap();

        window.update(cx, |mw, _, cx| mw.open_sidebar(cx)).unwrap();
        cx.background_executor.run_until_parked();

        let project_key = ProjectGroupKey::new(None, PathList::new(&[path!("/my-project")]));
        let keys = window
            .read_with(cx, |mw, _| mw.project_group_keys())
            .unwrap();
        assert_eq!(
            keys,
            vec![project_key],
            "project group should exist before CloseProject: {keys:?}"
        );

        cx.dispatch_action(window.into(), CloseProject);

        let keys = window
            .read_with(cx, |mw, _| mw.project_group_keys())
            .unwrap();
        assert!(
            keys.is_empty(),
            "project group should be removed after CloseProject: {keys:?}"
        );
    }
}
