zed.rs

   1mod app_menus;
   2pub mod component_preview;
   3pub mod edit_prediction_registry;
   4#[cfg(target_os = "macos")]
   5pub(crate) mod mac_only_instance;
   6mod migrate;
   7mod open_listener;
   8mod quick_action_bar;
   9#[cfg(target_os = "windows")]
  10pub(crate) mod windows_only_instance;
  11
  12use agent_ui::{AgentDiffToolbar, AgentPanelDelegate};
  13use agent_ui_v2::agents_panel::AgentsPanel;
  14use anyhow::Context as _;
  15pub use app_menus::*;
  16use assets::Assets;
  17use audio::{AudioSettings, REPLAY_DURATION};
  18use breadcrumbs::Breadcrumbs;
  19use client::zed_urls;
  20use collections::VecDeque;
  21use debugger_ui::debugger_panel::DebugPanel;
  22use editor::{Editor, MultiBuffer};
  23use extension_host::ExtensionStore;
  24use feature_flags::{FeatureFlagAppExt, PanicFeatureFlag};
  25use fs::Fs;
  26use futures::FutureExt as _;
  27use futures::future::Either;
  28use futures::{StreamExt, channel::mpsc, select_biased};
  29use git_ui::commit_view::CommitViewToolbar;
  30use git_ui::git_panel::GitPanel;
  31use git_ui::project_diff::ProjectDiffToolbar;
  32use gpui::{
  33    Action, App, AppContext as _, AsyncWindowContext, Context, DismissEvent, Element, Entity,
  34    Focusable, KeyBinding, ParentElement, PathPromptOptions, PromptLevel, ReadGlobal, SharedString,
  35    Task, TitlebarOptions, UpdateGlobal, WeakEntity, Window, WindowKind, WindowOptions, actions,
  36    image_cache, point, px, retain_all,
  37};
  38use image_viewer::ImageInfo;
  39use language::Capability;
  40use language_onboarding::BasedPyrightBanner;
  41use language_tools::lsp_button::{self, LspButton};
  42use language_tools::lsp_log_view::LspLogToolbarItemView;
  43use migrate::{MigrationBanner, MigrationEvent, MigrationNotification, MigrationType};
  44use migrator::migrate_keymap;
  45use onboarding::DOCS_URL;
  46use onboarding::multibuffer_hint::MultibufferHint;
  47pub use open_listener::*;
  48use outline_panel::OutlinePanel;
  49use paths::{
  50    local_debug_file_relative_path, local_settings_file_relative_path,
  51    local_tasks_file_relative_path,
  52};
  53use project::{DirectoryLister, DisableAiSettings, ProjectItem};
  54use project_panel::ProjectPanel;
  55use prompt_store::PromptBuilder;
  56use quick_action_bar::QuickActionBar;
  57use recent_projects::open_remote_project;
  58use release_channel::{AppCommitSha, AppVersion, ReleaseChannel};
  59use rope::Rope;
  60use search::project_search::ProjectSearchBar;
  61use settings::{
  62    BaseKeymap, DEFAULT_KEYMAP_PATH, InvalidSettingsError, KeybindSource, KeymapFile,
  63    KeymapFileLoadResult, MigrationStatus, Settings, SettingsStore, VIM_KEYMAP_PATH,
  64    initial_local_debug_tasks_content, initial_project_settings_content, initial_tasks_content,
  65    update_settings_file,
  66};
  67use std::time::Duration;
  68use std::{
  69    borrow::Cow,
  70    path::{Path, PathBuf},
  71    sync::Arc,
  72    sync::atomic::{self, AtomicBool},
  73};
  74use terminal_view::terminal_panel::{self, TerminalPanel};
  75use theme::{ActiveTheme, GlobalTheme, SystemAppearance, ThemeRegistry, ThemeSettings};
  76use ui::{PopoverMenuHandle, prelude::*};
  77use util::markdown::MarkdownString;
  78use util::rel_path::RelPath;
  79use util::{ResultExt, asset_str};
  80use uuid::Uuid;
  81use vim_mode_setting::VimModeSetting;
  82use workspace::notifications::{
  83    NotificationId, SuppressEvent, dismiss_app_notification, show_app_notification,
  84};
  85use workspace::utility_pane::utility_slot_for_dock_position;
  86use workspace::{
  87    AppState, NewFile, NewWindow, OpenLog, Panel, Toast, Workspace, WorkspaceSettings,
  88    create_and_open_local_file, notifications::simple_message_notification::MessageNotification,
  89    open_new,
  90};
  91use workspace::{
  92    CloseIntent, CloseWindow, NotificationFrame, RestoreBanner, with_active_or_new_workspace,
  93};
  94use workspace::{Pane, notifications::DetachAndPromptErr};
  95use zed_actions::{
  96    OpenAccountSettings, OpenBrowser, OpenDocs, OpenServerSettings, OpenSettingsFile, OpenZedUrl,
  97    Quit,
  98};
  99
 100actions!(
 101    zed,
 102    [
 103        /// Opens the element inspector for debugging UI.
 104        DebugElements,
 105        /// Hides the application window.
 106        Hide,
 107        /// Hides all other application windows.
 108        HideOthers,
 109        /// Minimizes the current window.
 110        Minimize,
 111        /// Opens the default settings file.
 112        OpenDefaultSettings,
 113        /// Opens project-specific settings file.
 114        OpenProjectSettingsFile,
 115        /// Opens the project tasks configuration.
 116        OpenProjectTasks,
 117        /// Opens the tasks panel.
 118        OpenTasks,
 119        /// Opens debug tasks configuration.
 120        OpenDebugTasks,
 121        /// Resets the application database.
 122        ResetDatabase,
 123        /// Shows all hidden windows.
 124        ShowAll,
 125        /// Toggles fullscreen mode.
 126        ToggleFullScreen,
 127        /// Zooms the window.
 128        Zoom,
 129        /// Triggers a test panic for debugging.
 130        TestPanic,
 131        /// Triggers a hard crash for debugging.
 132        TestCrash,
 133    ]
 134);
 135
 136actions!(
 137    dev,
 138    [
 139        /// Stores last 30s of audio from zed staff using the experimental rodio
 140        /// audio system (including yourself) on the current call in a tar file
 141        /// in the current working directory.
 142        CaptureRecentAudio,
 143    ]
 144);
 145
 146pub fn init(cx: &mut App) {
 147    #[cfg(target_os = "macos")]
 148    cx.on_action(|_: &Hide, cx| cx.hide());
 149    #[cfg(target_os = "macos")]
 150    cx.on_action(|_: &HideOthers, cx| cx.hide_other_apps());
 151    #[cfg(target_os = "macos")]
 152    cx.on_action(|_: &ShowAll, cx| cx.unhide_other_apps());
 153    cx.on_action(quit);
 154
 155    cx.on_action(|_: &RestoreBanner, cx| title_bar::restore_banner(cx));
 156    let flag = cx.wait_for_flag::<PanicFeatureFlag>();
 157    cx.spawn(async |cx| {
 158        if cx
 159            .update(|cx| ReleaseChannel::global(cx) == ReleaseChannel::Dev)
 160            .unwrap_or_default()
 161            || flag.await
 162        {
 163            cx.update(|cx| {
 164                cx.on_action(|_: &TestPanic, _| panic!("Ran the TestPanic action"))
 165                    .on_action(|_: &TestCrash, _| {
 166                        unsafe extern "C" {
 167                            fn puts(s: *const i8);
 168                        }
 169                        unsafe {
 170                            puts(0xabad1d3a as *const i8);
 171                        }
 172                    });
 173            })
 174            .ok();
 175        };
 176    })
 177    .detach();
 178    cx.on_action(|_: &OpenLog, cx| {
 179        with_active_or_new_workspace(cx, |workspace, window, cx| {
 180            open_log_file(workspace, window, cx);
 181        });
 182    })
 183    .on_action(|_: &workspace::RevealLogInFileManager, cx| {
 184        cx.reveal_path(paths::log_file().as_path());
 185    })
 186    .on_action(|_: &zed_actions::OpenLicenses, cx| {
 187        with_active_or_new_workspace(cx, |workspace, window, cx| {
 188            open_bundled_file(
 189                workspace,
 190                asset_str::<Assets>("licenses.md"),
 191                "Open Source License Attribution",
 192                "Markdown",
 193                window,
 194                cx,
 195            );
 196        });
 197    })
 198    .on_action(|_: &zed_actions::OpenTelemetryLog, cx| {
 199        with_active_or_new_workspace(cx, |workspace, window, cx| {
 200            open_telemetry_log_file(workspace, window, cx);
 201        });
 202    })
 203    .on_action(|&zed_actions::OpenKeymapFile, cx| {
 204        with_active_or_new_workspace(cx, |_, window, cx| {
 205            open_settings_file(
 206                paths::keymap_file(),
 207                || settings::initial_keymap_content().as_ref().into(),
 208                window,
 209                cx,
 210            );
 211        });
 212    })
 213    .on_action(|_: &OpenSettingsFile, cx| {
 214        with_active_or_new_workspace(cx, |_, window, cx| {
 215            open_settings_file(
 216                paths::settings_file(),
 217                || settings::initial_user_settings_content().as_ref().into(),
 218                window,
 219                cx,
 220            );
 221        });
 222    })
 223    .on_action(|_: &OpenAccountSettings, cx| {
 224        with_active_or_new_workspace(cx, |_, _, cx| {
 225            cx.open_url(&zed_urls::account_url(cx));
 226        });
 227    })
 228    .on_action(|_: &OpenTasks, cx| {
 229        with_active_or_new_workspace(cx, |_, window, cx| {
 230            open_settings_file(
 231                paths::tasks_file(),
 232                || settings::initial_tasks_content().as_ref().into(),
 233                window,
 234                cx,
 235            );
 236        });
 237    })
 238    .on_action(|_: &OpenDebugTasks, cx| {
 239        with_active_or_new_workspace(cx, |_, window, cx| {
 240            open_settings_file(
 241                paths::debug_scenarios_file(),
 242                || settings::initial_debug_tasks_content().as_ref().into(),
 243                window,
 244                cx,
 245            );
 246        });
 247    })
 248    .on_action(|_: &OpenDefaultSettings, cx| {
 249        with_active_or_new_workspace(cx, |workspace, window, cx| {
 250            open_bundled_file(
 251                workspace,
 252                settings::default_settings(),
 253                "Default Settings",
 254                "JSON",
 255                window,
 256                cx,
 257            );
 258        });
 259    })
 260    .on_action(|_: &zed_actions::OpenDefaultKeymap, cx| {
 261        with_active_or_new_workspace(cx, |workspace, window, cx| {
 262            open_bundled_file(
 263                workspace,
 264                settings::default_keymap(),
 265                "Default Key Bindings",
 266                "JSON",
 267                window,
 268                cx,
 269            );
 270        });
 271    })
 272    .on_action(|_: &zed_actions::About, cx| {
 273        with_active_or_new_workspace(cx, |workspace, window, cx| {
 274            about(workspace, window, cx);
 275        });
 276    });
 277}
 278
 279fn bind_on_window_closed(cx: &mut App) -> Option<gpui::Subscription> {
 280    #[cfg(target_os = "macos")]
 281    {
 282        WorkspaceSettings::get_global(cx)
 283            .on_last_window_closed
 284            .is_quit_app()
 285            .then(|| {
 286                cx.on_window_closed(|cx| {
 287                    if cx.windows().is_empty() {
 288                        cx.quit();
 289                    }
 290                })
 291            })
 292    }
 293    #[cfg(not(target_os = "macos"))]
 294    {
 295        Some(cx.on_window_closed(|cx| {
 296            if cx.windows().is_empty() {
 297                cx.quit();
 298            }
 299        }))
 300    }
 301}
 302
 303pub fn build_window_options(display_uuid: Option<Uuid>, cx: &mut App) -> WindowOptions {
 304    let display = display_uuid.and_then(|uuid| {
 305        cx.displays()
 306            .into_iter()
 307            .find(|display| display.uuid().ok() == Some(uuid))
 308    });
 309    let app_id = ReleaseChannel::global(cx).app_id();
 310    let window_decorations = match std::env::var("ZED_WINDOW_DECORATIONS") {
 311        Ok(val) if val == "server" => gpui::WindowDecorations::Server,
 312        Ok(val) if val == "client" => gpui::WindowDecorations::Client,
 313        _ => match WorkspaceSettings::get_global(cx).window_decorations {
 314            settings::WindowDecorations::Server => gpui::WindowDecorations::Server,
 315            settings::WindowDecorations::Client => gpui::WindowDecorations::Client,
 316        },
 317    };
 318
 319    let use_system_window_tabs = WorkspaceSettings::get_global(cx).use_system_window_tabs;
 320
 321    WindowOptions {
 322        titlebar: Some(TitlebarOptions {
 323            title: None,
 324            appears_transparent: true,
 325            traffic_light_position: Some(point(px(9.0), px(9.0))),
 326        }),
 327        window_bounds: None,
 328        focus: false,
 329        show: false,
 330        kind: WindowKind::Normal,
 331        is_movable: true,
 332        display_id: display.map(|display| display.id()),
 333        window_background: cx.theme().window_background_appearance(),
 334        app_id: Some(app_id.to_owned()),
 335        window_decorations: Some(window_decorations),
 336        window_min_size: Some(gpui::Size {
 337            width: px(360.0),
 338            height: px(240.0),
 339        }),
 340        tabbing_identifier: if use_system_window_tabs {
 341            Some(String::from("zed"))
 342        } else {
 343            None
 344        },
 345        ..Default::default()
 346    }
 347}
 348
 349pub fn initialize_workspace(
 350    app_state: Arc<AppState>,
 351    prompt_builder: Arc<PromptBuilder>,
 352    cx: &mut App,
 353) {
 354    let mut _on_close_subscription = bind_on_window_closed(cx);
 355    cx.observe_global::<SettingsStore>(move |cx| {
 356        _on_close_subscription = bind_on_window_closed(cx);
 357    })
 358    .detach();
 359
 360    cx.observe_new(move |workspace: &mut Workspace, window, cx| {
 361        let Some(window) = window else {
 362            return;
 363        };
 364
 365        let workspace_handle = cx.entity();
 366        let center_pane = workspace.active_pane().clone();
 367        initialize_pane(workspace, &center_pane, window, cx);
 368
 369        cx.subscribe_in(&workspace_handle, window, {
 370            move |workspace, _, event, window, cx| match event {
 371                workspace::Event::PaneAdded(pane) => {
 372                    initialize_pane(workspace, pane, window, cx);
 373                }
 374                workspace::Event::OpenBundledFile {
 375                    text,
 376                    title,
 377                    language,
 378                } => open_bundled_file(workspace, text.clone(), title, language, window, cx),
 379                _ => {}
 380            }
 381        })
 382        .detach();
 383
 384        #[cfg(not(target_os = "macos"))]
 385        initialize_file_watcher(window, cx);
 386
 387        if let Some(specs) = window.gpu_specs() {
 388            log::info!("Using GPU: {:?}", specs);
 389            show_software_emulation_warning_if_needed(specs.clone(), window, cx);
 390            if let Some((crash_server, message)) = crashes::CRASH_HANDLER
 391                .get()
 392                .zip(bincode::serialize(&specs).ok())
 393                && let Err(err) = crash_server.send_message(3, message)
 394            {
 395                log::warn!(
 396                    "Failed to store active gpu info for crash reporting: {}",
 397                    err
 398                );
 399            }
 400        }
 401
 402        #[cfg(target_os = "windows")]
 403        unstable_version_notification(cx);
 404
 405        let edit_prediction_menu_handle = PopoverMenuHandle::default();
 406        let edit_prediction_ui = cx.new(|cx| {
 407            edit_prediction_ui::EditPredictionButton::new(
 408                app_state.fs.clone(),
 409                app_state.user_store.clone(),
 410                edit_prediction_menu_handle.clone(),
 411                app_state.client.clone(),
 412                cx,
 413            )
 414        });
 415        workspace.register_action({
 416            move |_, _: &edit_prediction_ui::ToggleMenu, window, cx| {
 417                edit_prediction_menu_handle.toggle(window, cx);
 418            }
 419        });
 420
 421        let search_button = cx.new(|_| search::search_status_button::SearchButton::new());
 422        let diagnostic_summary =
 423            cx.new(|cx| diagnostics::items::DiagnosticIndicator::new(workspace, cx));
 424        let activity_indicator = activity_indicator::ActivityIndicator::new(
 425            workspace,
 426            workspace.project().read(cx).languages().clone(),
 427            window,
 428            cx,
 429        );
 430        let active_buffer_language =
 431            cx.new(|_| language_selector::ActiveBufferLanguage::new(workspace));
 432        let active_toolchain_language =
 433            cx.new(|cx| toolchain_selector::ActiveToolchain::new(workspace, window, cx));
 434        let vim_mode_indicator = cx.new(|cx| vim::ModeIndicator::new(window, cx));
 435        let image_info = cx.new(|_cx| ImageInfo::new(workspace));
 436
 437        let lsp_button_menu_handle = PopoverMenuHandle::default();
 438        let lsp_button =
 439            cx.new(|cx| LspButton::new(workspace, lsp_button_menu_handle.clone(), window, cx));
 440        workspace.register_action({
 441            move |_, _: &lsp_button::ToggleMenu, window, cx| {
 442                lsp_button_menu_handle.toggle(window, cx);
 443            }
 444        });
 445
 446        let cursor_position =
 447            cx.new(|_| go_to_line::cursor_position::CursorPosition::new(workspace));
 448        let line_ending_indicator =
 449            cx.new(|_| line_ending_selector::LineEndingIndicator::default());
 450        workspace.status_bar().update(cx, |status_bar, cx| {
 451            status_bar.add_left_item(search_button, window, cx);
 452            status_bar.add_left_item(lsp_button, window, cx);
 453            status_bar.add_left_item(diagnostic_summary, window, cx);
 454            status_bar.add_left_item(activity_indicator, window, cx);
 455            status_bar.add_right_item(edit_prediction_ui, window, cx);
 456            status_bar.add_right_item(active_buffer_language, window, cx);
 457            status_bar.add_right_item(active_toolchain_language, window, cx);
 458            status_bar.add_right_item(line_ending_indicator, window, cx);
 459            status_bar.add_right_item(vim_mode_indicator, window, cx);
 460            status_bar.add_right_item(cursor_position, window, cx);
 461            status_bar.add_right_item(image_info, window, cx);
 462        });
 463
 464        let handle = cx.entity().downgrade();
 465        window.on_window_should_close(cx, move |window, cx| {
 466            handle
 467                .update(cx, |workspace, cx| {
 468                    // We'll handle closing asynchronously
 469                    workspace.close_window(&CloseWindow, window, cx);
 470                    false
 471                })
 472                .unwrap_or(true)
 473        });
 474
 475        initialize_panels(prompt_builder.clone(), window, cx);
 476        register_actions(app_state.clone(), workspace, window, cx);
 477
 478        workspace.focus_handle(cx).focus(window);
 479    })
 480    .detach();
 481}
 482
 483#[cfg(target_os = "windows")]
 484fn unstable_version_notification(cx: &mut App) {
 485    if !matches!(
 486        ReleaseChannel::try_global(cx),
 487        Some(ReleaseChannel::Nightly)
 488    ) {
 489        return;
 490    }
 491    let db_key = "zed_windows_nightly_notif_shown_at".to_owned();
 492    let time = chrono::Utc::now();
 493    if let Some(last_shown) = db::kvp::KEY_VALUE_STORE
 494        .read_kvp(&db_key)
 495        .log_err()
 496        .flatten()
 497        .and_then(|timestamp| chrono::DateTime::parse_from_rfc3339(&timestamp).ok())
 498    {
 499        if time.fixed_offset() - last_shown < chrono::Duration::days(7) {
 500            return;
 501        }
 502    }
 503    cx.spawn(async move |_| {
 504        db::kvp::KEY_VALUE_STORE
 505            .write_kvp(db_key, time.to_rfc3339())
 506            .await
 507    })
 508    .detach_and_log_err(cx);
 509    struct WindowsNightly;
 510    show_app_notification(NotificationId::unique::<WindowsNightly>(), cx, |cx| {
 511        cx.new(|cx| {
 512            MessageNotification::new("You're using an unstable version of Zed (Nightly)", cx)
 513                .primary_message("Download Stable")
 514                .primary_icon_color(Color::Accent)
 515                .primary_icon(IconName::Download)
 516                .primary_on_click(|window, cx| {
 517                    window.dispatch_action(
 518                        zed_actions::OpenBrowser {
 519                            url: "https://zed.dev/download".to_string(),
 520                        }
 521                        .boxed_clone(),
 522                        cx,
 523                    );
 524                    cx.emit(DismissEvent);
 525                })
 526        })
 527    });
 528}
 529
 530#[cfg(any(target_os = "linux", target_os = "freebsd"))]
 531fn initialize_file_watcher(window: &mut Window, cx: &mut Context<Workspace>) {
 532    if let Err(e) = fs::fs_watcher::global(|_| {}) {
 533        let message = format!(
 534            db::indoc! {r#"
 535            inotify_init returned {}
 536
 537            This may be due to system-wide limits on inotify instances. For troubleshooting see: https://zed.dev/docs/linux
 538            "#},
 539            e
 540        );
 541        let prompt = window.prompt(
 542            PromptLevel::Critical,
 543            "Could not start inotify",
 544            Some(&message),
 545            &["Troubleshoot and Quit"],
 546            cx,
 547        );
 548        cx.spawn(async move |_, cx| {
 549            if prompt.await == Ok(0) {
 550                cx.update(|cx| {
 551                    cx.open_url("https://zed.dev/docs/linux#could-not-start-inotify");
 552                    cx.quit();
 553                })
 554                .ok();
 555            }
 556        })
 557        .detach()
 558    }
 559}
 560
 561#[cfg(target_os = "windows")]
 562fn initialize_file_watcher(window: &mut Window, cx: &mut Context<Workspace>) {
 563    if let Err(e) = fs::fs_watcher::global(|_| {}) {
 564        let message = format!(
 565            db::indoc! {r#"
 566            ReadDirectoryChangesW initialization failed: {}
 567
 568            This may occur on network filesystems and WSL paths. For troubleshooting see: https://zed.dev/docs/windows
 569            "#},
 570            e
 571        );
 572        let prompt = window.prompt(
 573            PromptLevel::Critical,
 574            "Could not start ReadDirectoryChangesW",
 575            Some(&message),
 576            &["Troubleshoot and Quit"],
 577            cx,
 578        );
 579        cx.spawn(async move |_, cx| {
 580            if prompt.await == Ok(0) {
 581                cx.update(|cx| {
 582                    cx.open_url("https://zed.dev/docs/windows");
 583                    cx.quit()
 584                })
 585                .ok();
 586            }
 587        })
 588        .detach()
 589    }
 590}
 591
 592fn show_software_emulation_warning_if_needed(
 593    specs: gpui::GpuSpecs,
 594    window: &mut Window,
 595    cx: &mut Context<Workspace>,
 596) {
 597    if specs.is_software_emulated && std::env::var("ZED_ALLOW_EMULATED_GPU").is_err() {
 598        let (graphics_api, docs_url, open_url) = if cfg!(target_os = "windows") {
 599            (
 600                "DirectX",
 601                "https://zed.dev/docs/windows",
 602                "https://zed.dev/docs/windows",
 603            )
 604        } else {
 605            (
 606                "Vulkan",
 607                "https://zed.dev/docs/linux",
 608                "https://zed.dev/docs/linux#zed-fails-to-open-windows",
 609            )
 610        };
 611        let message = format!(
 612            db::indoc! {r#"
 613            Zed uses {} for rendering and requires a compatible GPU.
 614
 615            Currently you are using a software emulated GPU ({}) which
 616            will result in awful performance.
 617
 618            For troubleshooting see: {}
 619            Set ZED_ALLOW_EMULATED_GPU=1 env var to permanently override.
 620            "#},
 621            graphics_api, specs.device_name, docs_url
 622        );
 623        let prompt = window.prompt(
 624            PromptLevel::Critical,
 625            "Unsupported GPU",
 626            Some(&message),
 627            &["Skip", "Troubleshoot and Quit"],
 628            cx,
 629        );
 630        cx.spawn(async move |_, cx| {
 631            if prompt.await == Ok(1) {
 632                cx.update(|cx| {
 633                    cx.open_url(open_url);
 634                    cx.quit();
 635                })
 636                .ok();
 637            }
 638        })
 639        .detach()
 640    }
 641}
 642
 643fn initialize_panels(
 644    prompt_builder: Arc<PromptBuilder>,
 645    window: &mut Window,
 646    cx: &mut Context<Workspace>,
 647) {
 648    cx.spawn_in(window, async move |workspace_handle, cx| {
 649        let project_panel = ProjectPanel::load(workspace_handle.clone(), cx.clone());
 650        let outline_panel = OutlinePanel::load(workspace_handle.clone(), cx.clone());
 651        let terminal_panel = TerminalPanel::load(workspace_handle.clone(), cx.clone());
 652        let git_panel = GitPanel::load(workspace_handle.clone(), cx.clone());
 653        let channels_panel =
 654            collab_ui::collab_panel::CollabPanel::load(workspace_handle.clone(), cx.clone());
 655        let notification_panel = collab_ui::notification_panel::NotificationPanel::load(
 656            workspace_handle.clone(),
 657            cx.clone(),
 658        );
 659        let debug_panel = DebugPanel::load(workspace_handle.clone(), cx);
 660
 661        async fn add_panel_when_ready(
 662            panel_task: impl Future<Output = anyhow::Result<Entity<impl workspace::Panel>>> + 'static,
 663            workspace_handle: WeakEntity<Workspace>,
 664            mut cx: gpui::AsyncWindowContext,
 665        ) {
 666            if let Some(panel) = panel_task.await.context("failed to load panel").log_err()
 667            {
 668                workspace_handle
 669                    .update_in(&mut cx, |workspace, window, cx| {
 670                        workspace.add_panel(panel, window, cx);
 671                    })
 672                    .log_err();
 673            }
 674        }
 675
 676        futures::join!(
 677            add_panel_when_ready(project_panel, workspace_handle.clone(), cx.clone()),
 678            add_panel_when_ready(outline_panel, workspace_handle.clone(), cx.clone()),
 679            add_panel_when_ready(terminal_panel, workspace_handle.clone(), cx.clone()),
 680            add_panel_when_ready(git_panel, workspace_handle.clone(), cx.clone()),
 681            add_panel_when_ready(channels_panel, workspace_handle.clone(), cx.clone()),
 682            add_panel_when_ready(notification_panel, workspace_handle.clone(), cx.clone()),
 683            add_panel_when_ready(debug_panel, workspace_handle.clone(), cx.clone()),
 684            initialize_agent_panel(workspace_handle.clone(), prompt_builder, cx.clone()).map(|r| r.log_err()),
 685            initialize_agents_panel(workspace_handle, cx.clone()).map(|r| r.log_err())
 686        );
 687
 688        anyhow::Ok(())
 689    })
 690    .detach();
 691}
 692
 693fn setup_or_teardown_ai_panel<P: Panel>(
 694    workspace: &mut Workspace,
 695    window: &mut Window,
 696    cx: &mut Context<Workspace>,
 697    load_panel: impl FnOnce(
 698        WeakEntity<Workspace>,
 699        AsyncWindowContext,
 700    ) -> Task<anyhow::Result<Entity<P>>>
 701    + 'static,
 702) -> Task<anyhow::Result<()>> {
 703    let disable_ai = SettingsStore::global(cx)
 704        .get::<DisableAiSettings>(None)
 705        .disable_ai
 706        || cfg!(test);
 707    let existing_panel = workspace.panel::<P>(cx);
 708
 709    match (disable_ai, existing_panel) {
 710        (false, None) => cx.spawn_in(window, async move |workspace, cx| {
 711            let panel = load_panel(workspace.clone(), cx.clone()).await?;
 712            workspace.update_in(cx, |workspace, window, cx| {
 713                let disable_ai = SettingsStore::global(cx)
 714                    .get::<DisableAiSettings>(None)
 715                    .disable_ai;
 716                let have_panel = workspace.panel::<P>(cx).is_some();
 717                if !disable_ai && !have_panel {
 718                    workspace.add_panel(panel, window, cx);
 719                }
 720            })
 721        }),
 722        (true, Some(existing_panel)) => {
 723            workspace.remove_panel::<P>(&existing_panel, window, cx);
 724            Task::ready(Ok(()))
 725        }
 726        _ => Task::ready(Ok(())),
 727    }
 728}
 729
 730async fn initialize_agent_panel(
 731    workspace_handle: WeakEntity<Workspace>,
 732    prompt_builder: Arc<PromptBuilder>,
 733    mut cx: AsyncWindowContext,
 734) -> anyhow::Result<()> {
 735    workspace_handle
 736        .update_in(&mut cx, |workspace, window, cx| {
 737            let prompt_builder = prompt_builder.clone();
 738            setup_or_teardown_ai_panel(workspace, window, cx, move |workspace, cx| {
 739                agent_ui::AgentPanel::load(workspace, prompt_builder, cx)
 740            })
 741        })?
 742        .await?;
 743
 744    workspace_handle.update_in(&mut cx, |workspace, window, cx| {
 745        let prompt_builder = prompt_builder.clone();
 746        cx.observe_global_in::<SettingsStore>(window, move |workspace, window, cx| {
 747            let prompt_builder = prompt_builder.clone();
 748            setup_or_teardown_ai_panel(workspace, window, cx, move |workspace, cx| {
 749                agent_ui::AgentPanel::load(workspace, prompt_builder, cx)
 750            })
 751            .detach_and_log_err(cx);
 752        })
 753        .detach();
 754
 755        // Register the actions that are shared between `assistant` and `assistant2`.
 756        //
 757        // We need to do this here instead of within the individual `init`
 758        // functions so that we only register the actions once.
 759        //
 760        // Once we ship `assistant2` we can push this back down into `agent::agent_panel::init`.
 761        if !cfg!(test) {
 762            <dyn AgentPanelDelegate>::set_global(
 763                Arc::new(agent_ui::ConcreteAssistantPanelDelegate),
 764                cx,
 765            );
 766
 767            workspace
 768                .register_action(agent_ui::AgentPanel::toggle_focus)
 769                .register_action(agent_ui::InlineAssistant::inline_assist);
 770        }
 771    })?;
 772
 773    anyhow::Ok(())
 774}
 775
 776async fn initialize_agents_panel(
 777    workspace_handle: WeakEntity<Workspace>,
 778    mut cx: AsyncWindowContext,
 779) -> anyhow::Result<()> {
 780    workspace_handle
 781        .update_in(&mut cx, |workspace, window, cx| {
 782            setup_or_teardown_ai_panel(workspace, window, cx, |workspace, cx| {
 783                AgentsPanel::load(workspace, cx)
 784            })
 785        })?
 786        .await?;
 787
 788    workspace_handle.update_in(&mut cx, |_workspace, window, cx| {
 789        cx.observe_global_in::<SettingsStore>(window, move |workspace, window, cx| {
 790            setup_or_teardown_ai_panel(workspace, window, cx, |workspace, cx| {
 791                AgentsPanel::load(workspace, cx)
 792            })
 793            .detach_and_log_err(cx);
 794        })
 795        .detach();
 796    })?;
 797
 798    anyhow::Ok(())
 799}
 800
 801fn register_actions(
 802    app_state: Arc<AppState>,
 803    workspace: &mut Workspace,
 804    _: &mut Window,
 805    cx: &mut Context<Workspace>,
 806) {
 807    workspace
 808        .register_action(|_, _: &OpenDocs, _, cx| cx.open_url(DOCS_URL))
 809        .register_action(|_, _: &Minimize, window, _| {
 810            window.minimize_window();
 811        })
 812        .register_action(|_, _: &Zoom, window, _| {
 813            window.zoom_window();
 814        })
 815        .register_action(|_, _: &ToggleFullScreen, window, _| {
 816            window.toggle_fullscreen();
 817        })
 818        .register_action(|_, action: &OpenZedUrl, _, cx| {
 819            OpenListener::global(cx).open(RawOpenRequest {
 820                urls: vec![action.url.clone()],
 821                ..Default::default()
 822            })
 823        })
 824        .register_action(|workspace, action: &OpenBrowser, _window, cx| {
 825            // Parse and validate the URL to ensure it's properly formatted
 826            match url::Url::parse(&action.url) {
 827                Ok(parsed_url) => {
 828                    // Use the parsed URL's string representation which is properly escaped
 829                    cx.open_url(parsed_url.as_str());
 830                }
 831                Err(e) => {
 832                    workspace.show_error(
 833                        &anyhow::anyhow!(
 834                            "Opening this URL in a browser failed because the URL is invalid: {}\n\nError was: {e}",
 835                            action.url
 836                        ),
 837                        cx,
 838                    );
 839                }
 840            }
 841        })
 842        .register_action(|workspace, _: &workspace::Open, window, cx| {
 843            telemetry::event!("Project Opened");
 844            let paths = workspace.prompt_for_open_path(
 845                PathPromptOptions {
 846                    files: true,
 847                    directories: true,
 848                    multiple: true,
 849                    prompt: None,
 850                },
 851                DirectoryLister::Local(
 852                    workspace.project().clone(),
 853                    workspace.app_state().fs.clone(),
 854                ),
 855                window,
 856                cx,
 857            );
 858
 859            cx.spawn_in(window, async move |this, cx| {
 860                let Some(paths) = paths.await.log_err().flatten() else {
 861                    return;
 862                };
 863
 864                if let Some(task) = this
 865                    .update_in(cx, |this, window, cx| {
 866                        this.open_workspace_for_paths(false, paths, window, cx)
 867                    })
 868                    .log_err()
 869                {
 870                    task.await.log_err();
 871                }
 872            })
 873            .detach()
 874        })
 875        .register_action(|workspace, action: &zed_actions::OpenRemote, window, cx| {
 876            if !action.from_existing_connection {
 877                cx.propagate();
 878                return;
 879            }
 880            // You need existing remote connection to open it this way
 881            if workspace.project().read(cx).is_local() {
 882                return;
 883            }
 884            telemetry::event!("Project Opened");
 885            let paths = workspace.prompt_for_open_path(
 886                PathPromptOptions {
 887                    files: true,
 888                    directories: true,
 889                    multiple: true,
 890                    prompt: None,
 891                },
 892                DirectoryLister::Project(workspace.project().clone()),
 893                window,
 894                cx,
 895            );
 896            cx.spawn_in(window, async move |this, cx| {
 897                let Some(paths) = paths.await.log_err().flatten() else {
 898                    return;
 899                };
 900                if let Some(task) = this
 901                    .update_in(cx, |this, window, cx| {
 902                        open_new_ssh_project_from_project(this, paths, window, cx)
 903                    })
 904                    .log_err()
 905                {
 906                    task.await.log_err();
 907                }
 908            })
 909            .detach()
 910        })
 911        .register_action({
 912            let fs = app_state.fs.clone();
 913            move |_, action: &zed_actions::IncreaseUiFontSize, _window, cx| {
 914                if action.persist {
 915                    update_settings_file(fs.clone(), cx, move |settings, cx| {
 916                        let ui_font_size = ThemeSettings::get_global(cx).ui_font_size(cx) + px(1.0);
 917                        let _ = settings
 918                            .theme
 919                            .ui_font_size
 920                            .insert(theme::clamp_font_size(ui_font_size).into());
 921                    });
 922                } else {
 923                    theme::adjust_ui_font_size(cx, |size| size + px(1.0));
 924                }
 925            }
 926        })
 927        .register_action({
 928            let fs = app_state.fs.clone();
 929            move |_, action: &zed_actions::DecreaseUiFontSize, _window, cx| {
 930                if action.persist {
 931                    update_settings_file(fs.clone(), cx, move |settings, cx| {
 932                        let ui_font_size = ThemeSettings::get_global(cx).ui_font_size(cx) - px(1.0);
 933                        let _ = settings
 934                            .theme
 935                            .ui_font_size
 936                            .insert(theme::clamp_font_size(ui_font_size).into());
 937                    });
 938                } else {
 939                    theme::adjust_ui_font_size(cx, |size| size - px(1.0));
 940                }
 941            }
 942        })
 943        .register_action({
 944            let fs = app_state.fs.clone();
 945            move |_, action: &zed_actions::ResetUiFontSize, _window, cx| {
 946                if action.persist {
 947                    update_settings_file(fs.clone(), cx, move |settings, _| {
 948                        settings.theme.ui_font_size = None;
 949                    });
 950                } else {
 951                    theme::reset_ui_font_size(cx);
 952                }
 953            }
 954        })
 955        .register_action({
 956            let fs = app_state.fs.clone();
 957            move |_, action: &zed_actions::IncreaseBufferFontSize, _window, cx| {
 958                if action.persist {
 959                    update_settings_file(fs.clone(), cx, move |settings, cx| {
 960                        let buffer_font_size =
 961                            ThemeSettings::get_global(cx).buffer_font_size(cx) + px(1.0);
 962                        let _ = settings
 963                            .theme
 964                            .buffer_font_size
 965                            .insert(theme::clamp_font_size(buffer_font_size).into());
 966                    });
 967                } else {
 968                    theme::adjust_buffer_font_size(cx, |size| size + px(1.0));
 969                }
 970            }
 971        })
 972        .register_action({
 973            let fs = app_state.fs.clone();
 974            move |_, action: &zed_actions::DecreaseBufferFontSize, _window, cx| {
 975                if action.persist {
 976                    update_settings_file(fs.clone(), cx, move |settings, cx| {
 977                        let buffer_font_size =
 978                            ThemeSettings::get_global(cx).buffer_font_size(cx) - px(1.0);
 979                        let _ = settings
 980                            .theme
 981                            .buffer_font_size
 982                            .insert(theme::clamp_font_size(buffer_font_size).into());
 983                    });
 984                } else {
 985                    theme::adjust_buffer_font_size(cx, |size| size - px(1.0));
 986                }
 987            }
 988        })
 989        .register_action({
 990            let fs = app_state.fs.clone();
 991            move |_, action: &zed_actions::ResetBufferFontSize, _window, cx| {
 992                if action.persist {
 993                    update_settings_file(fs.clone(), cx, move |settings, _| {
 994                        settings.theme.buffer_font_size = None;
 995                    });
 996                } else {
 997                    theme::reset_buffer_font_size(cx);
 998                }
 999            }
1000        })
1001        .register_action({
1002            let fs = app_state.fs.clone();
1003            move |_, action: &zed_actions::ResetAllZoom, _window, cx| {
1004                if action.persist {
1005                    update_settings_file(fs.clone(), cx, move |settings, _| {
1006                        settings.theme.ui_font_size = None;
1007                        settings.theme.buffer_font_size = None;
1008                        settings.theme.agent_ui_font_size = None;
1009                        settings.theme.agent_buffer_font_size = None;
1010                    });
1011                } else {
1012                    theme::reset_ui_font_size(cx);
1013                    theme::reset_buffer_font_size(cx);
1014                    theme::reset_agent_ui_font_size(cx);
1015                    theme::reset_agent_buffer_font_size(cx);
1016                }
1017            }
1018        })
1019        .register_action(|_, _: &install_cli::RegisterZedScheme, window, cx| {
1020            cx.spawn_in(window, async move |workspace, cx| {
1021                install_cli::register_zed_scheme(cx).await?;
1022                workspace.update_in(cx, |workspace, _, cx| {
1023                    struct RegisterZedScheme;
1024
1025                    workspace.show_toast(
1026                        Toast::new(
1027                            NotificationId::unique::<RegisterZedScheme>(),
1028                            format!(
1029                                "zed:// links will now open in {}.",
1030                                ReleaseChannel::global(cx).display_name()
1031                            ),
1032                        ),
1033                        cx,
1034                    )
1035                })?;
1036                Ok(())
1037            })
1038            .detach_and_prompt_err(
1039                "Error registering zed:// scheme",
1040                window,
1041                cx,
1042                |_, _, _| None,
1043            );
1044        })
1045        .register_action(open_project_settings_file)
1046        .register_action(open_project_tasks_file)
1047        .register_action(open_project_debug_tasks_file)
1048        .register_action(
1049            |workspace: &mut Workspace,
1050             _: &zed_actions::project_panel::ToggleFocus,
1051             window: &mut Window,
1052             cx: &mut Context<Workspace>| {
1053                workspace.toggle_panel_focus::<ProjectPanel>(window, cx);
1054            },
1055        )
1056        .register_action(
1057            |workspace: &mut Workspace,
1058             _: &outline_panel::ToggleFocus,
1059             window: &mut Window,
1060             cx: &mut Context<Workspace>| {
1061                workspace.toggle_panel_focus::<OutlinePanel>(window, cx);
1062            },
1063        )
1064        .register_action(
1065            |workspace: &mut Workspace,
1066             _: &collab_ui::collab_panel::ToggleFocus,
1067             window: &mut Window,
1068             cx: &mut Context<Workspace>| {
1069                workspace.toggle_panel_focus::<collab_ui::collab_panel::CollabPanel>(window, cx);
1070            },
1071        )
1072        .register_action(
1073            |workspace: &mut Workspace,
1074             _: &collab_ui::notification_panel::ToggleFocus,
1075             window: &mut Window,
1076             cx: &mut Context<Workspace>| {
1077                workspace.toggle_panel_focus::<collab_ui::notification_panel::NotificationPanel>(
1078                    window, cx,
1079                );
1080            },
1081        )
1082        .register_action(
1083            |workspace: &mut Workspace,
1084             _: &terminal_panel::ToggleFocus,
1085             window: &mut Window,
1086             cx: &mut Context<Workspace>| {
1087                workspace.toggle_panel_focus::<TerminalPanel>(window, cx);
1088            },
1089        )
1090        .register_action(
1091            |workspace: &mut Workspace,
1092             _: &zed_actions::agent::ToggleAgentPane,
1093             window: &mut Window,
1094             cx: &mut Context<Workspace>| {
1095                if let Some(panel) = workspace.panel::<AgentsPanel>(cx) {
1096                    let position = panel.read(cx).position(window, cx);
1097                    let slot = utility_slot_for_dock_position(position);
1098                    workspace.toggle_utility_pane(slot, window, cx);
1099                }
1100            },
1101        )
1102        .register_action({
1103            let app_state = Arc::downgrade(&app_state);
1104            move |_, _: &NewWindow, _, cx| {
1105                if let Some(app_state) = app_state.upgrade() {
1106                    open_new(
1107                        Default::default(),
1108                        app_state,
1109                        cx,
1110                        |workspace, window, cx| {
1111                            cx.activate(true);
1112                            Editor::new_file(workspace, &Default::default(), window, cx)
1113                        },
1114                    )
1115                    .detach();
1116                }
1117            }
1118        })
1119        .register_action({
1120            let app_state = Arc::downgrade(&app_state);
1121            move |_, _: &NewFile, _, cx| {
1122                if let Some(app_state) = app_state.upgrade() {
1123                    open_new(
1124                        Default::default(),
1125                        app_state,
1126                        cx,
1127                        |workspace, window, cx| {
1128                            Editor::new_file(workspace, &Default::default(), window, cx)
1129                        },
1130                    )
1131                    .detach();
1132                }
1133            }
1134        })
1135        .register_action(|workspace, _: &CaptureRecentAudio, window, cx| {
1136            capture_recent_audio(workspace, window, cx);
1137        });
1138
1139    #[cfg(not(target_os = "windows"))]
1140    workspace.register_action(install_cli);
1141
1142    if workspace.project().read(cx).is_via_remote_server() {
1143        workspace.register_action({
1144            move |workspace, _: &OpenServerSettings, window, cx| {
1145                let open_server_settings = workspace
1146                    .project()
1147                    .update(cx, |project, cx| project.open_server_settings(cx));
1148
1149                cx.spawn_in(window, async move |workspace, cx| {
1150                    let buffer = open_server_settings.await?;
1151
1152                    workspace
1153                        .update_in(cx, |workspace, window, cx| {
1154                            workspace.open_path(
1155                                buffer
1156                                    .read(cx)
1157                                    .project_path(cx)
1158                                    .expect("Settings file must have a location"),
1159                                None,
1160                                true,
1161                                window,
1162                                cx,
1163                            )
1164                        })?
1165                        .await?;
1166
1167                    anyhow::Ok(())
1168                })
1169                .detach_and_log_err(cx);
1170            }
1171        });
1172    }
1173}
1174
1175fn initialize_pane(
1176    workspace: &Workspace,
1177    pane: &Entity<Pane>,
1178    window: &mut Window,
1179    cx: &mut Context<Workspace>,
1180) {
1181    pane.update(cx, |pane, cx| {
1182        pane.toolbar().update(cx, |toolbar, cx| {
1183            let multibuffer_hint = cx.new(|_| MultibufferHint::new());
1184            toolbar.add_item(multibuffer_hint, window, cx);
1185            let breadcrumbs = cx.new(|_| Breadcrumbs::new());
1186            toolbar.add_item(breadcrumbs, window, cx);
1187            let buffer_search_bar = cx.new(|cx| {
1188                search::BufferSearchBar::new(
1189                    Some(workspace.project().read(cx).languages().clone()),
1190                    window,
1191                    cx,
1192                )
1193            });
1194            toolbar.add_item(buffer_search_bar.clone(), window, cx);
1195            let quick_action_bar =
1196                cx.new(|cx| QuickActionBar::new(buffer_search_bar, workspace, cx));
1197            toolbar.add_item(quick_action_bar, window, cx);
1198            let diagnostic_editor_controls = cx.new(|_| diagnostics::ToolbarControls::new());
1199            toolbar.add_item(diagnostic_editor_controls, window, cx);
1200            let project_search_bar = cx.new(|_| ProjectSearchBar::new());
1201            toolbar.add_item(project_search_bar, window, cx);
1202            let lsp_log_item = cx.new(|_| LspLogToolbarItemView::new());
1203            toolbar.add_item(lsp_log_item, window, cx);
1204            let dap_log_item = cx.new(|_| debugger_tools::DapLogToolbarItemView::new());
1205            toolbar.add_item(dap_log_item, window, cx);
1206            let acp_tools_item = cx.new(|_| acp_tools::AcpToolsToolbarItemView::new());
1207            toolbar.add_item(acp_tools_item, window, cx);
1208            let syntax_tree_item = cx.new(|_| language_tools::SyntaxTreeToolbarItemView::new());
1209            toolbar.add_item(syntax_tree_item, window, cx);
1210            let migration_banner = cx.new(|cx| MigrationBanner::new(workspace, cx));
1211            toolbar.add_item(migration_banner, window, cx);
1212            let project_diff_toolbar = cx.new(|cx| ProjectDiffToolbar::new(workspace, cx));
1213            toolbar.add_item(project_diff_toolbar, window, cx);
1214            let commit_view_toolbar = cx.new(|_| CommitViewToolbar::new());
1215            toolbar.add_item(commit_view_toolbar, window, cx);
1216            let agent_diff_toolbar = cx.new(AgentDiffToolbar::new);
1217            toolbar.add_item(agent_diff_toolbar, window, cx);
1218            let basedpyright_banner = cx.new(|cx| BasedPyrightBanner::new(workspace, cx));
1219            toolbar.add_item(basedpyright_banner, window, cx);
1220        })
1221    });
1222}
1223
1224fn about(_: &mut Workspace, window: &mut Window, cx: &mut Context<Workspace>) {
1225    use std::fmt::Write;
1226    let release_channel = ReleaseChannel::global(cx).display_name();
1227    let full_version = AppVersion::global(cx);
1228    let version = env!("CARGO_PKG_VERSION");
1229    let debug = if cfg!(debug_assertions) {
1230        "(debug)"
1231    } else {
1232        ""
1233    };
1234    let message = format!("{release_channel} {version} {debug}");
1235
1236    let mut detail = AppCommitSha::try_global(cx)
1237        .map(|sha| sha.full())
1238        .unwrap_or_default();
1239    if !detail.is_empty() {
1240        detail.push('\n');
1241    }
1242    _ = write!(&mut detail, "\n{full_version}");
1243
1244    let detail = Some(detail);
1245
1246    let prompt = window.prompt(
1247        PromptLevel::Info,
1248        &message,
1249        detail.as_deref(),
1250        &["Copy", "OK"],
1251        cx,
1252    );
1253    cx.spawn(async move |_, cx| {
1254        if let Ok(0) = prompt.await {
1255            let content = format!("{}\n{}", message, detail.as_deref().unwrap_or(""));
1256            cx.update(|cx| {
1257                cx.write_to_clipboard(gpui::ClipboardItem::new_string(content));
1258            })
1259            .ok();
1260        }
1261    })
1262    .detach();
1263}
1264
1265#[cfg(not(target_os = "windows"))]
1266fn install_cli(
1267    _: &mut Workspace,
1268    _: &install_cli::InstallCliBinary,
1269    window: &mut Window,
1270    cx: &mut Context<Workspace>,
1271) {
1272    install_cli::install_cli_binary(window, cx)
1273}
1274
1275static WAITING_QUIT_CONFIRMATION: AtomicBool = AtomicBool::new(false);
1276fn quit(_: &Quit, cx: &mut App) {
1277    if WAITING_QUIT_CONFIRMATION.load(atomic::Ordering::Acquire) {
1278        return;
1279    }
1280
1281    let should_confirm = WorkspaceSettings::get_global(cx).confirm_quit;
1282    cx.spawn(async move |cx| {
1283        let mut workspace_windows = cx.update(|cx| {
1284            cx.windows()
1285                .into_iter()
1286                .filter_map(|window| window.downcast::<Workspace>())
1287                .collect::<Vec<_>>()
1288        })?;
1289
1290        // If multiple windows have unsaved changes, and need a save prompt,
1291        // prompt in the active window before switching to a different window.
1292        cx.update(|cx| {
1293            workspace_windows.sort_by_key(|window| window.is_active(cx) == Some(false));
1294        })
1295        .log_err();
1296
1297        if should_confirm && let Some(workspace) = workspace_windows.first() {
1298            let answer = workspace
1299                .update(cx, |_, window, cx| {
1300                    window.prompt(
1301                        PromptLevel::Info,
1302                        "Are you sure you want to quit?",
1303                        None,
1304                        &["Quit", "Cancel"],
1305                        cx,
1306                    )
1307                })
1308                .log_err();
1309
1310            if let Some(answer) = answer {
1311                WAITING_QUIT_CONFIRMATION.store(true, atomic::Ordering::Release);
1312                let answer = answer.await.ok();
1313                WAITING_QUIT_CONFIRMATION.store(false, atomic::Ordering::Release);
1314                if answer != Some(0) {
1315                    return Ok(());
1316                }
1317            }
1318        }
1319
1320        // If the user cancels any save prompt, then keep the app open.
1321        for window in workspace_windows {
1322            if let Some(should_close) = window
1323                .update(cx, |workspace, window, cx| {
1324                    workspace.prepare_to_close(CloseIntent::Quit, window, cx)
1325                })
1326                .log_err()
1327                && !should_close.await?
1328            {
1329                return Ok(());
1330            }
1331        }
1332        cx.update(|cx| cx.quit())?;
1333        anyhow::Ok(())
1334    })
1335    .detach_and_log_err(cx);
1336}
1337
1338fn open_log_file(workspace: &mut Workspace, window: &mut Window, cx: &mut Context<Workspace>) {
1339    const MAX_LINES: usize = 1000;
1340    workspace
1341        .with_local_workspace(window, cx, move |workspace, window, cx| {
1342            let app_state = workspace.app_state();
1343            let languages = app_state.languages.clone();
1344            let fs = app_state.fs.clone();
1345            cx.spawn_in(window, async move |workspace, cx| {
1346                let (old_log, new_log, log_language) = futures::join!(
1347                    fs.load(paths::old_log_file()),
1348                    fs.load(paths::log_file()),
1349                    languages.language_for_name("log")
1350                );
1351                let log = match (old_log, new_log) {
1352                    (Err(_), Err(_)) => None,
1353                    (old_log, new_log) => {
1354                        let mut lines = VecDeque::with_capacity(MAX_LINES);
1355                        for line in old_log
1356                            .iter()
1357                            .flat_map(|log| log.lines())
1358                            .chain(new_log.iter().flat_map(|log| log.lines()))
1359                        {
1360                            if lines.len() == MAX_LINES {
1361                                lines.pop_front();
1362                            }
1363                            lines.push_back(line);
1364                        }
1365                        Some(
1366                            lines
1367                                .into_iter()
1368                                .flat_map(|line| [line, "\n"])
1369                                .collect::<String>(),
1370                        )
1371                    }
1372                };
1373                let log_language = log_language.ok();
1374
1375                workspace
1376                    .update_in(cx, |workspace, window, cx| {
1377                        let Some(log) = log else {
1378                            struct OpenLogError;
1379
1380                            workspace.show_notification(
1381                                NotificationId::unique::<OpenLogError>(),
1382                                cx,
1383                                |cx| {
1384                                    cx.new(|cx| {
1385                                        MessageNotification::new(
1386                                            format!(
1387                                                "Unable to access/open log file at path {:?}",
1388                                                paths::log_file().as_path()
1389                                            ),
1390                                            cx,
1391                                        )
1392                                    })
1393                                },
1394                            );
1395                            return;
1396                        };
1397                        let project = workspace.project().clone();
1398                        let buffer = project.update(cx, |project, cx| {
1399                            project.create_local_buffer(&log, log_language, false, cx)
1400                        });
1401
1402                        let buffer = cx
1403                            .new(|cx| MultiBuffer::singleton(buffer, cx).with_title("Log".into()));
1404                        let editor = cx.new(|cx| {
1405                            let mut editor =
1406                                Editor::for_multibuffer(buffer, Some(project), window, cx);
1407                            editor.set_read_only(true);
1408                            editor.set_breadcrumb_header(format!(
1409                                "Last {} lines in {}",
1410                                MAX_LINES,
1411                                paths::log_file().display()
1412                            ));
1413                            editor
1414                        });
1415
1416                        editor.update(cx, |editor, cx| {
1417                            let last_multi_buffer_offset = editor.buffer().read(cx).len(cx);
1418                            editor.change_selections(Default::default(), window, cx, |s| {
1419                                s.select_ranges(Some(
1420                                    last_multi_buffer_offset..last_multi_buffer_offset,
1421                                ));
1422                            })
1423                        });
1424
1425                        workspace.add_item_to_active_pane(Box::new(editor), None, true, window, cx);
1426                    })
1427                    .log_err();
1428            })
1429            .detach();
1430        })
1431        .detach();
1432}
1433
1434fn notify_settings_errors(result: settings::SettingsParseResult, is_user: bool, cx: &mut App) {
1435    if let settings::ParseStatus::Failed { error: err } = &result.parse_status {
1436        let settings_type = if is_user { "user" } else { "global" };
1437        log::error!("Failed to load {} settings: {err}", settings_type);
1438    }
1439
1440    let error = match result.parse_status {
1441        settings::ParseStatus::Failed { error } => Some(anyhow::format_err!(error)),
1442        settings::ParseStatus::Success => None,
1443    };
1444    let id = NotificationId::Named(format!("failed-to-parse-settings-{is_user}").into());
1445
1446    let showed_parse_error = match error {
1447        Some(error) => {
1448            if let Some(InvalidSettingsError::LocalSettings { .. }) =
1449                error.downcast_ref::<InvalidSettingsError>()
1450            {
1451                false
1452                // Local settings errors are displayed by the projects
1453            } else {
1454                show_app_notification(id, cx, move |cx| {
1455                    cx.new(|cx| {
1456                        MessageNotification::new(format!("Invalid user settings file\n{error}"), cx)
1457                            .primary_message("Open Settings File")
1458                            .primary_icon(IconName::Settings)
1459                            .primary_on_click(|window, cx| {
1460                                window.dispatch_action(
1461                                    zed_actions::OpenSettingsFile.boxed_clone(),
1462                                    cx,
1463                                );
1464                                cx.emit(DismissEvent);
1465                            })
1466                    })
1467                });
1468                true
1469            }
1470        }
1471        None => {
1472            dismiss_app_notification(&id, cx);
1473            false
1474        }
1475    };
1476    let id = NotificationId::Named(format!("failed-to-migrate-settings-{is_user}").into());
1477
1478    match result.migration_status {
1479        settings::MigrationStatus::Succeeded | settings::MigrationStatus::NotNeeded => {
1480            dismiss_app_notification(&id, cx);
1481        }
1482        settings::MigrationStatus::Failed { error: err } => {
1483            if !showed_parse_error {
1484                show_app_notification(id, cx, move |cx| {
1485                    cx.new(|cx| {
1486                        MessageNotification::new(
1487                            format!(
1488                                "Failed to migrate settings\n\
1489                                {err}"
1490                            ),
1491                            cx,
1492                        )
1493                        .primary_message("Open Settings File")
1494                        .primary_icon(IconName::Settings)
1495                        .primary_on_click(|window, cx| {
1496                            window.dispatch_action(zed_actions::OpenSettingsFile.boxed_clone(), cx);
1497                            cx.emit(DismissEvent);
1498                        })
1499                    })
1500                });
1501            }
1502        }
1503    };
1504}
1505
1506pub fn handle_settings_file_changes(
1507    mut user_settings_file_rx: mpsc::UnboundedReceiver<String>,
1508    mut global_settings_file_rx: mpsc::UnboundedReceiver<String>,
1509    cx: &mut App,
1510) {
1511    MigrationNotification::set_global(cx.new(|_| MigrationNotification), cx);
1512
1513    // Initial load of both settings files
1514    let global_content = cx
1515        .background_executor()
1516        .block(global_settings_file_rx.next())
1517        .unwrap();
1518    let user_content = cx
1519        .background_executor()
1520        .block(user_settings_file_rx.next())
1521        .unwrap();
1522
1523    SettingsStore::update_global(cx, |store, cx| {
1524        notify_settings_errors(store.set_user_settings(&user_content, cx), true, cx);
1525        notify_settings_errors(store.set_global_settings(&global_content, cx), false, cx);
1526    });
1527
1528    // Watch for changes in both files
1529    cx.spawn(async move |cx| {
1530        let mut settings_streams = futures::stream::select(
1531            global_settings_file_rx.map(Either::Left),
1532            user_settings_file_rx.map(Either::Right),
1533        );
1534
1535        while let Some(content) = settings_streams.next().await {
1536            let (content, is_user) = match content {
1537                Either::Left(content) => (content, false),
1538                Either::Right(content) => (content, true),
1539            };
1540
1541            let result = cx.update_global(|store: &mut SettingsStore, cx| {
1542                let result = if is_user {
1543                    store.set_user_settings(&content, cx)
1544                } else {
1545                    store.set_global_settings(&content, cx)
1546                };
1547                let migrating_in_memory =
1548                    matches!(&result.migration_status, MigrationStatus::Succeeded);
1549                notify_settings_errors(result, is_user, cx);
1550                if let Some(notifier) = MigrationNotification::try_global(cx) {
1551                    notifier.update(cx, |_, cx| {
1552                        cx.emit(MigrationEvent::ContentChanged {
1553                            migration_type: MigrationType::Settings,
1554                            migrating_in_memory,
1555                        });
1556                    });
1557                }
1558                cx.refresh_windows();
1559            });
1560
1561            if result.is_err() {
1562                break; // App dropped
1563            }
1564        }
1565    })
1566    .detach();
1567}
1568
1569pub fn handle_keymap_file_changes(
1570    mut user_keymap_file_rx: mpsc::UnboundedReceiver<String>,
1571    cx: &mut App,
1572) {
1573    let (base_keymap_tx, mut base_keymap_rx) = mpsc::unbounded();
1574    let (keyboard_layout_tx, mut keyboard_layout_rx) = mpsc::unbounded();
1575    let mut old_base_keymap = *BaseKeymap::get_global(cx);
1576    let mut old_vim_enabled = VimModeSetting::get_global(cx).0;
1577    let mut old_helix_enabled = vim_mode_setting::HelixModeSetting::get_global(cx).0;
1578
1579    cx.observe_global::<SettingsStore>(move |cx| {
1580        let new_base_keymap = *BaseKeymap::get_global(cx);
1581        let new_vim_enabled = VimModeSetting::get_global(cx).0;
1582        let new_helix_enabled = vim_mode_setting::HelixModeSetting::get_global(cx).0;
1583
1584        if new_base_keymap != old_base_keymap
1585            || new_vim_enabled != old_vim_enabled
1586            || new_helix_enabled != old_helix_enabled
1587        {
1588            old_base_keymap = new_base_keymap;
1589            old_vim_enabled = new_vim_enabled;
1590            old_helix_enabled = new_helix_enabled;
1591
1592            base_keymap_tx.unbounded_send(()).unwrap();
1593        }
1594    })
1595    .detach();
1596
1597    #[cfg(target_os = "windows")]
1598    {
1599        let mut current_layout_id = cx.keyboard_layout().id().to_string();
1600        cx.on_keyboard_layout_change(move |cx| {
1601            let next_layout_id = cx.keyboard_layout().id();
1602            if next_layout_id != current_layout_id {
1603                current_layout_id = next_layout_id.to_string();
1604                keyboard_layout_tx.unbounded_send(()).ok();
1605            }
1606        })
1607        .detach();
1608    }
1609
1610    #[cfg(not(target_os = "windows"))]
1611    {
1612        let mut current_mapping = cx.keyboard_mapper().get_key_equivalents().cloned();
1613        cx.on_keyboard_layout_change(move |cx| {
1614            let next_mapping = cx.keyboard_mapper().get_key_equivalents();
1615            if current_mapping.as_ref() != next_mapping {
1616                current_mapping = next_mapping.cloned();
1617                keyboard_layout_tx.unbounded_send(()).ok();
1618            }
1619        })
1620        .detach();
1621    }
1622
1623    load_default_keymap(cx);
1624
1625    struct KeymapParseErrorNotification;
1626    let notification_id = NotificationId::unique::<KeymapParseErrorNotification>();
1627
1628    cx.spawn(async move |cx| {
1629        let mut user_keymap_content = String::new();
1630        let mut migrating_in_memory = false;
1631        loop {
1632            select_biased! {
1633                _ = base_keymap_rx.next() => {},
1634                _ = keyboard_layout_rx.next() => {},
1635                content = user_keymap_file_rx.next() => {
1636                    if let Some(content) = content {
1637                        if let Ok(Some(migrated_content)) = migrate_keymap(&content) {
1638                            user_keymap_content = migrated_content;
1639                            migrating_in_memory = true;
1640                        } else {
1641                            user_keymap_content = content;
1642                            migrating_in_memory = false;
1643                        }
1644                    }
1645                }
1646            };
1647            cx.update(|cx| {
1648                if let Some(notifier) = MigrationNotification::try_global(cx) {
1649                    notifier.update(cx, |_, cx| {
1650                        cx.emit(MigrationEvent::ContentChanged {
1651                            migration_type: MigrationType::Keymap,
1652                            migrating_in_memory,
1653                        });
1654                    });
1655                }
1656                let load_result = KeymapFile::load(&user_keymap_content, cx);
1657                match load_result {
1658                    KeymapFileLoadResult::Success { key_bindings } => {
1659                        reload_keymaps(cx, key_bindings);
1660                        dismiss_app_notification(&notification_id.clone(), cx);
1661                    }
1662                    KeymapFileLoadResult::SomeFailedToLoad {
1663                        key_bindings,
1664                        error_message,
1665                    } => {
1666                        if !key_bindings.is_empty() {
1667                            reload_keymaps(cx, key_bindings);
1668                        }
1669                        show_keymap_file_load_error(notification_id.clone(), error_message, cx);
1670                    }
1671                    KeymapFileLoadResult::JsonParseFailure { error } => {
1672                        show_keymap_file_json_error(notification_id.clone(), &error, cx)
1673                    }
1674                }
1675            })
1676            .ok();
1677        }
1678    })
1679    .detach();
1680}
1681
1682fn show_keymap_file_json_error(
1683    notification_id: NotificationId,
1684    error: &anyhow::Error,
1685    cx: &mut App,
1686) {
1687    let message: SharedString =
1688        format!("JSON parse error in keymap file. Bindings not reloaded.\n\n{error}").into();
1689    show_app_notification(notification_id, cx, move |cx| {
1690        cx.new(|cx| {
1691            MessageNotification::new(message.clone(), cx)
1692                .primary_message("Open Keymap File")
1693                .primary_icon(IconName::Settings)
1694                .primary_on_click(|window, cx| {
1695                    window.dispatch_action(zed_actions::OpenKeymapFile.boxed_clone(), cx);
1696                    cx.emit(DismissEvent);
1697                })
1698        })
1699    });
1700}
1701
1702fn show_keymap_file_load_error(
1703    notification_id: NotificationId,
1704    error_message: MarkdownString,
1705    cx: &mut App,
1706) {
1707    show_markdown_app_notification(
1708        notification_id,
1709        error_message,
1710        "Open Keymap File".into(),
1711        |window, cx| {
1712            window.dispatch_action(zed_actions::OpenKeymapFile.boxed_clone(), cx);
1713            cx.emit(DismissEvent);
1714        },
1715        cx,
1716    )
1717}
1718
1719fn show_markdown_app_notification<F>(
1720    notification_id: NotificationId,
1721    message: MarkdownString,
1722    primary_button_message: SharedString,
1723    primary_button_on_click: F,
1724    cx: &mut App,
1725) where
1726    F: 'static + Send + Sync + Fn(&mut Window, &mut Context<MessageNotification>),
1727{
1728    let parsed_markdown = cx.background_spawn(async move {
1729        let file_location_directory = None;
1730        let language_registry = None;
1731        markdown_preview::markdown_parser::parse_markdown(
1732            &message.0,
1733            file_location_directory,
1734            language_registry,
1735        )
1736        .await
1737    });
1738
1739    cx.spawn(async move |cx| {
1740        let parsed_markdown = Arc::new(parsed_markdown.await);
1741        let primary_button_message = primary_button_message.clone();
1742        let primary_button_on_click = Arc::new(primary_button_on_click);
1743        cx.update(|cx| {
1744            show_app_notification(notification_id, cx, move |cx| {
1745                let workspace_handle = cx.entity().downgrade();
1746                let parsed_markdown = parsed_markdown.clone();
1747                let primary_button_message = primary_button_message.clone();
1748                let primary_button_on_click = primary_button_on_click.clone();
1749                cx.new(move |cx| {
1750                    MessageNotification::new_from_builder(cx, move |window, cx| {
1751                        image_cache(retain_all("notification-cache"))
1752                            .child(div().text_ui(cx).child(
1753                                markdown_preview::markdown_renderer::render_parsed_markdown(
1754                                    &parsed_markdown.clone(),
1755                                    Some(workspace_handle.clone()),
1756                                    window,
1757                                    cx,
1758                                ),
1759                            ))
1760                            .into_any()
1761                    })
1762                    .primary_message(primary_button_message)
1763                    .primary_icon(IconName::Settings)
1764                    .primary_on_click_arc(primary_button_on_click)
1765                })
1766            })
1767        })
1768        .ok();
1769    })
1770    .detach();
1771}
1772
1773fn reload_keymaps(cx: &mut App, mut user_key_bindings: Vec<KeyBinding>) {
1774    cx.clear_key_bindings();
1775    load_default_keymap(cx);
1776
1777    for key_binding in &mut user_key_bindings {
1778        key_binding.set_meta(KeybindSource::User.meta());
1779    }
1780    cx.bind_keys(user_key_bindings);
1781
1782    let menus = app_menus(cx);
1783    cx.set_menus(menus);
1784    // On Windows, this is set in the `update_jump_list` method of the `HistoryManager`.
1785    #[cfg(not(target_os = "windows"))]
1786    cx.set_dock_menu(vec![gpui::MenuItem::action(
1787        "New Window",
1788        workspace::NewWindow,
1789    )]);
1790    // todo: nicer api here?
1791    keymap_editor::KeymapEventChannel::trigger_keymap_changed(cx);
1792}
1793
1794pub fn load_default_keymap(cx: &mut App) {
1795    let base_keymap = *BaseKeymap::get_global(cx);
1796    if base_keymap == BaseKeymap::None {
1797        return;
1798    }
1799
1800    cx.bind_keys(
1801        KeymapFile::load_asset(DEFAULT_KEYMAP_PATH, Some(KeybindSource::Default), cx).unwrap(),
1802    );
1803
1804    if let Some(asset_path) = base_keymap.asset_path() {
1805        cx.bind_keys(KeymapFile::load_asset(asset_path, Some(KeybindSource::Base), cx).unwrap());
1806    }
1807
1808    if VimModeSetting::get_global(cx).0 || vim_mode_setting::HelixModeSetting::get_global(cx).0 {
1809        cx.bind_keys(
1810            KeymapFile::load_asset(VIM_KEYMAP_PATH, Some(KeybindSource::Vim), cx).unwrap(),
1811        );
1812    }
1813}
1814
1815pub fn open_new_ssh_project_from_project(
1816    workspace: &mut Workspace,
1817    paths: Vec<PathBuf>,
1818    window: &mut Window,
1819    cx: &mut Context<Workspace>,
1820) -> Task<anyhow::Result<()>> {
1821    let app_state = workspace.app_state().clone();
1822    let Some(ssh_client) = workspace.project().read(cx).remote_client() else {
1823        return Task::ready(Err(anyhow::anyhow!("Not an ssh project")));
1824    };
1825    let connection_options = ssh_client.read(cx).connection_options();
1826    cx.spawn_in(window, async move |_, cx| {
1827        open_remote_project(
1828            connection_options,
1829            paths,
1830            app_state,
1831            workspace::OpenOptions {
1832                open_new_workspace: Some(true),
1833                ..Default::default()
1834            },
1835            cx,
1836        )
1837        .await
1838    })
1839}
1840
1841fn open_project_settings_file(
1842    workspace: &mut Workspace,
1843    _: &OpenProjectSettingsFile,
1844    window: &mut Window,
1845    cx: &mut Context<Workspace>,
1846) {
1847    open_local_file(
1848        workspace,
1849        local_settings_file_relative_path(),
1850        initial_project_settings_content(),
1851        window,
1852        cx,
1853    )
1854}
1855
1856fn open_project_tasks_file(
1857    workspace: &mut Workspace,
1858    _: &OpenProjectTasks,
1859    window: &mut Window,
1860    cx: &mut Context<Workspace>,
1861) {
1862    open_local_file(
1863        workspace,
1864        local_tasks_file_relative_path(),
1865        initial_tasks_content(),
1866        window,
1867        cx,
1868    )
1869}
1870
1871fn open_project_debug_tasks_file(
1872    workspace: &mut Workspace,
1873    _: &zed_actions::OpenProjectDebugTasks,
1874    window: &mut Window,
1875    cx: &mut Context<Workspace>,
1876) {
1877    open_local_file(
1878        workspace,
1879        local_debug_file_relative_path(),
1880        initial_local_debug_tasks_content(),
1881        window,
1882        cx,
1883    )
1884}
1885
1886fn open_local_file(
1887    workspace: &mut Workspace,
1888    settings_relative_path: &'static RelPath,
1889    initial_contents: Cow<'static, str>,
1890    window: &mut Window,
1891    cx: &mut Context<Workspace>,
1892) {
1893    let project = workspace.project().clone();
1894    let worktree = project
1895        .read(cx)
1896        .visible_worktrees(cx)
1897        .find_map(|tree| tree.read(cx).root_entry()?.is_dir().then_some(tree));
1898    if let Some(worktree) = worktree {
1899        let tree_id = worktree.read(cx).id();
1900        cx.spawn_in(window, async move |workspace, cx| {
1901            // Check if the file actually exists on disk (even if it's excluded from worktree)
1902            let file_exists = {
1903                let full_path = worktree.read_with(cx, |tree, _| {
1904                    tree.abs_path().join(settings_relative_path.as_std_path())
1905                })?;
1906
1907                let fs = project.read_with(cx, |project, _| project.fs().clone())?;
1908
1909                fs.metadata(&full_path)
1910                    .await
1911                    .ok()
1912                    .flatten()
1913                    .is_some_and(|metadata| !metadata.is_dir && !metadata.is_fifo)
1914            };
1915
1916            if !file_exists {
1917                if let Some(dir_path) = settings_relative_path.parent()
1918                    && worktree.read_with(cx, |tree, _| tree.entry_for_path(dir_path).is_none())?
1919                {
1920                    project
1921                        .update(cx, |project, cx| {
1922                            project.create_entry((tree_id, dir_path), true, cx)
1923                        })?
1924                        .await
1925                        .context("worktree was removed")?;
1926                }
1927
1928                if worktree.read_with(cx, |tree, _| {
1929                    tree.entry_for_path(settings_relative_path).is_none()
1930                })? {
1931                    project
1932                        .update(cx, |project, cx| {
1933                            project.create_entry((tree_id, settings_relative_path), false, cx)
1934                        })?
1935                        .await
1936                        .context("worktree was removed")?;
1937                }
1938            }
1939
1940            let editor = workspace
1941                .update_in(cx, |workspace, window, cx| {
1942                    workspace.open_path((tree_id, settings_relative_path), None, true, window, cx)
1943                })?
1944                .await?
1945                .downcast::<Editor>()
1946                .context("unexpected item type: expected editor item")?;
1947
1948            editor
1949                .downgrade()
1950                .update(cx, |editor, cx| {
1951                    if let Some(buffer) = editor.buffer().read(cx).as_singleton()
1952                        && buffer.read(cx).is_empty()
1953                    {
1954                        buffer.update(cx, |buffer, cx| {
1955                            buffer.edit([(0..0, initial_contents)], None, cx)
1956                        });
1957                    }
1958                })
1959                .ok();
1960
1961            anyhow::Ok(())
1962        })
1963        .detach();
1964    } else {
1965        struct NoOpenFolders;
1966
1967        workspace.show_notification(NotificationId::unique::<NoOpenFolders>(), cx, |cx| {
1968            cx.new(|cx| MessageNotification::new("This project has no folders open.", cx))
1969        })
1970    }
1971}
1972
1973fn open_telemetry_log_file(
1974    workspace: &mut Workspace,
1975    window: &mut Window,
1976    cx: &mut Context<Workspace>,
1977) {
1978    const HEADER: &str = concat!(
1979        "// Zed collects anonymous usage data to help us understand how people are using the app.\n",
1980        "// Telemetry can be disabled via the `settings.json` file.\n",
1981        "// Here is the data that has been reported for the current session:\n",
1982    );
1983    workspace
1984        .with_local_workspace(window, cx, move |workspace, window, cx| {
1985            let app_state = workspace.app_state().clone();
1986            cx.spawn_in(window, async move |workspace, cx| {
1987                async fn fetch_log_string(app_state: &Arc<AppState>) -> Option<String> {
1988                    let path = client::telemetry::Telemetry::log_file_path();
1989                    app_state.fs.load(&path).await.log_err()
1990                }
1991
1992                let log = fetch_log_string(&app_state)
1993                    .await
1994                    .unwrap_or_else(|| "// No data has been collected yet".to_string());
1995
1996                const MAX_TELEMETRY_LOG_LEN: usize = 5 * 1024 * 1024;
1997                let mut start_offset = log.len().saturating_sub(MAX_TELEMETRY_LOG_LEN);
1998                if let Some(newline_offset) = log[start_offset..].find('\n') {
1999                    start_offset += newline_offset + 1;
2000                }
2001                let log_suffix = &log[start_offset..];
2002                let content = format!("{}\n{}", HEADER, log_suffix);
2003                let json = app_state
2004                    .languages
2005                    .language_for_name("JSON")
2006                    .await
2007                    .log_err();
2008
2009                workspace
2010                    .update_in(cx, |workspace, window, cx| {
2011                        let project = workspace.project().clone();
2012                        let buffer = project.update(cx, |project, cx| {
2013                            project.create_local_buffer(&content, json, false, cx)
2014                        });
2015                        let buffer = cx.new(|cx| {
2016                            MultiBuffer::singleton(buffer, cx).with_title("Telemetry Log".into())
2017                        });
2018                        workspace.add_item_to_active_pane(
2019                            Box::new(cx.new(|cx| {
2020                                let mut editor =
2021                                    Editor::for_multibuffer(buffer, Some(project), window, cx);
2022                                editor.set_read_only(true);
2023                                editor.set_breadcrumb_header("Telemetry Log".into());
2024                                editor
2025                            })),
2026                            None,
2027                            true,
2028                            window,
2029                            cx,
2030                        );
2031                    })
2032                    .log_err()?;
2033
2034                Some(())
2035            })
2036            .detach();
2037        })
2038        .detach();
2039}
2040
2041fn open_bundled_file(
2042    workspace: &Workspace,
2043    text: Cow<'static, str>,
2044    title: &'static str,
2045    language: &'static str,
2046    window: &mut Window,
2047    cx: &mut Context<Workspace>,
2048) {
2049    let language = workspace.app_state().languages.language_for_name(language);
2050    cx.spawn_in(window, async move |workspace, cx| {
2051        let language = language.await.log_err();
2052        workspace
2053            .update_in(cx, |workspace, window, cx| {
2054                workspace.with_local_workspace(window, cx, |workspace, window, cx| {
2055                    let project = workspace.project();
2056                    let buffer = project.update(cx, move |project, cx| {
2057                        let buffer =
2058                            project.create_local_buffer(text.as_ref(), language, false, cx);
2059                        buffer.update(cx, |buffer, cx| {
2060                            buffer.set_capability(Capability::ReadOnly, cx);
2061                        });
2062                        buffer
2063                    });
2064                    let buffer =
2065                        cx.new(|cx| MultiBuffer::singleton(buffer, cx).with_title(title.into()));
2066                    workspace.add_item_to_active_pane(
2067                        Box::new(cx.new(|cx| {
2068                            let mut editor =
2069                                Editor::for_multibuffer(buffer, Some(project.clone()), window, cx);
2070                            editor.set_read_only(true);
2071                            editor.set_should_serialize(false, cx);
2072                            editor.set_breadcrumb_header(title.into());
2073                            editor
2074                        })),
2075                        None,
2076                        true,
2077                        window,
2078                        cx,
2079                    );
2080                })
2081            })?
2082            .await
2083    })
2084    .detach_and_log_err(cx);
2085}
2086
2087fn open_settings_file(
2088    abs_path: &'static Path,
2089    default_content: impl FnOnce() -> Rope + Send + 'static,
2090    window: &mut Window,
2091    cx: &mut Context<Workspace>,
2092) {
2093    cx.spawn_in(window, async move |workspace, cx| {
2094        let (worktree_creation_task, settings_open_task) = workspace
2095            .update_in(cx, |workspace, window, cx| {
2096                workspace.with_local_workspace(window, cx, move |workspace, window, cx| {
2097                    let worktree_creation_task = workspace.project().update(cx, |project, cx| {
2098                        // Set up a dedicated worktree for settings, since
2099                        // otherwise we're dropping and re-starting LSP servers
2100                        // for each file inside on every settings file
2101                        // close/open
2102
2103                        // TODO: Do note that all other external files (e.g.
2104                        // drag and drop from OS) still have their worktrees
2105                        // released on file close, causing LSP servers'
2106                        // restarts.
2107                        project.find_or_create_worktree(paths::config_dir().as_path(), false, cx)
2108                    });
2109                    let settings_open_task =
2110                        create_and_open_local_file(abs_path, window, cx, default_content);
2111                    (worktree_creation_task, settings_open_task)
2112                })
2113            })?
2114            .await?;
2115        let _ = worktree_creation_task.await?;
2116        let _ = settings_open_task.await?;
2117        anyhow::Ok(())
2118    })
2119    .detach_and_log_err(cx);
2120}
2121
2122fn capture_recent_audio(workspace: &mut Workspace, _: &mut Window, cx: &mut Context<Workspace>) {
2123    struct CaptureRecentAudioNotification {
2124        focus_handle: gpui::FocusHandle,
2125        save_result: Option<Result<(PathBuf, Duration), anyhow::Error>>,
2126        _save_task: Task<anyhow::Result<()>>,
2127    }
2128
2129    impl gpui::EventEmitter<DismissEvent> for CaptureRecentAudioNotification {}
2130    impl gpui::EventEmitter<SuppressEvent> for CaptureRecentAudioNotification {}
2131    impl gpui::Focusable for CaptureRecentAudioNotification {
2132        fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
2133            self.focus_handle.clone()
2134        }
2135    }
2136    impl workspace::notifications::Notification for CaptureRecentAudioNotification {}
2137
2138    impl Render for CaptureRecentAudioNotification {
2139        fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
2140            let message = match &self.save_result {
2141                None => format!(
2142                    "Saving up to {} seconds of recent audio",
2143                    REPLAY_DURATION.as_secs(),
2144                ),
2145                Some(Ok((path, duration))) => format!(
2146                    "Saved {} seconds of all audio to {}",
2147                    duration.as_secs(),
2148                    path.display(),
2149                ),
2150                Some(Err(e)) => format!("Error saving audio replays: {e:?}"),
2151            };
2152
2153            NotificationFrame::new()
2154                .with_title(Some("Saved Audio"))
2155                .show_suppress_button(false)
2156                .on_close(cx.listener(|_, _, _, cx| {
2157                    cx.emit(DismissEvent);
2158                }))
2159                .with_content(message)
2160        }
2161    }
2162
2163    impl CaptureRecentAudioNotification {
2164        fn new(cx: &mut Context<Self>) -> Self {
2165            if AudioSettings::get_global(cx).rodio_audio {
2166                let executor = cx.background_executor().clone();
2167                let save_task = cx.default_global::<audio::Audio>().save_replays(executor);
2168                let _save_task = cx.spawn(async move |this, cx| {
2169                    let res = save_task.await;
2170                    this.update(cx, |this, cx| {
2171                        this.save_result = Some(res);
2172                        cx.notify();
2173                    })
2174                });
2175
2176                Self {
2177                    focus_handle: cx.focus_handle(),
2178                    _save_task,
2179                    save_result: None,
2180                }
2181            } else {
2182                Self {
2183                    focus_handle: cx.focus_handle(),
2184                    _save_task: Task::ready(Ok(())),
2185                    save_result: Some(Err(anyhow::anyhow!(
2186                        "Capturing recent audio is only supported on the experimental rodio audio pipeline"
2187                    ))),
2188                }
2189            }
2190        }
2191    }
2192
2193    workspace.show_notification(
2194        NotificationId::unique::<CaptureRecentAudioNotification>(),
2195        cx,
2196        |cx| cx.new(CaptureRecentAudioNotification::new),
2197    );
2198}
2199
2200/// Eagerly loads the active theme and icon theme based on the selections in the
2201/// theme settings.
2202///
2203/// This fast path exists to load these themes as soon as possible so the user
2204/// doesn't see the default themes while waiting on extensions to load.
2205pub(crate) fn eager_load_active_theme_and_icon_theme(fs: Arc<dyn Fs>, cx: &mut App) {
2206    let extension_store = ExtensionStore::global(cx);
2207    let theme_registry = ThemeRegistry::global(cx);
2208    let theme_settings = ThemeSettings::get_global(cx);
2209    let appearance = SystemAppearance::global(cx).0;
2210
2211    enum LoadTarget {
2212        Theme(PathBuf),
2213        IconTheme((PathBuf, PathBuf)),
2214    }
2215
2216    let theme_name = theme_settings.theme.name(appearance);
2217    let icon_theme_name = theme_settings.icon_theme.name(appearance);
2218    let themes_to_load = [
2219        theme_registry
2220            .get(&theme_name.0)
2221            .is_err()
2222            .then(|| {
2223                extension_store
2224                    .read(cx)
2225                    .path_to_extension_theme(&theme_name.0)
2226            })
2227            .flatten()
2228            .map(LoadTarget::Theme),
2229        theme_registry
2230            .get_icon_theme(&icon_theme_name.0)
2231            .is_err()
2232            .then(|| {
2233                extension_store
2234                    .read(cx)
2235                    .path_to_extension_icon_theme(&icon_theme_name.0)
2236            })
2237            .flatten()
2238            .map(LoadTarget::IconTheme),
2239    ];
2240
2241    enum ReloadTarget {
2242        Theme,
2243        IconTheme,
2244    }
2245
2246    let executor = cx.background_executor();
2247    let reload_tasks = parking_lot::Mutex::new(Vec::with_capacity(themes_to_load.len()));
2248
2249    let mut themes_to_load = themes_to_load.into_iter().flatten().peekable();
2250
2251    if themes_to_load.peek().is_none() {
2252        return;
2253    }
2254
2255    executor.block(executor.scoped(|scope| {
2256        for load_target in themes_to_load {
2257            let theme_registry = &theme_registry;
2258            let reload_tasks = &reload_tasks;
2259            let fs = fs.clone();
2260
2261            scope.spawn(async {
2262                match load_target {
2263                    LoadTarget::Theme(theme_path) => {
2264                        if theme_registry
2265                            .load_user_theme(&theme_path, fs)
2266                            .await
2267                            .log_err()
2268                            .is_some()
2269                        {
2270                            reload_tasks.lock().push(ReloadTarget::Theme);
2271                        }
2272                    }
2273                    LoadTarget::IconTheme((icon_theme_path, icons_root_path)) => {
2274                        if theme_registry
2275                            .load_icon_theme(&icon_theme_path, &icons_root_path, fs)
2276                            .await
2277                            .log_err()
2278                            .is_some()
2279                        {
2280                            reload_tasks.lock().push(ReloadTarget::IconTheme);
2281                        }
2282                    }
2283                }
2284            });
2285        }
2286    }));
2287
2288    for reload_target in reload_tasks.into_inner() {
2289        match reload_target {
2290            ReloadTarget::Theme => GlobalTheme::reload_theme(cx),
2291            ReloadTarget::IconTheme => GlobalTheme::reload_icon_theme(cx),
2292        };
2293    }
2294}
2295
2296#[cfg(test)]
2297mod tests {
2298    use super::*;
2299    use assets::Assets;
2300    use collections::HashSet;
2301    use editor::{
2302        DisplayPoint, Editor, MultiBufferOffset, SelectionEffects, display_map::DisplayRow,
2303    };
2304    use gpui::{
2305        Action, AnyWindowHandle, App, AssetSource, BorrowAppContext, TestAppContext, UpdateGlobal,
2306        VisualTestContext, WindowHandle, actions,
2307    };
2308    use language::LanguageRegistry;
2309    use languages::{markdown_lang, rust_lang};
2310    use pretty_assertions::{assert_eq, assert_ne};
2311    use project::{Project, ProjectPath};
2312    use semver::Version;
2313    use serde_json::json;
2314    use settings::{SettingsStore, watch_config_file};
2315    use std::{
2316        path::{Path, PathBuf},
2317        time::Duration,
2318    };
2319    use theme::ThemeRegistry;
2320    use util::{
2321        path,
2322        rel_path::{RelPath, rel_path},
2323    };
2324    use workspace::{
2325        NewFile, OpenOptions, OpenVisible, SERIALIZATION_THROTTLE_TIME, SaveIntent, SplitDirection,
2326        WorkspaceHandle,
2327        item::SaveOptions,
2328        item::{Item, ItemHandle},
2329        open_new, open_paths, pane,
2330    };
2331
2332    #[gpui::test]
2333    async fn test_open_non_existing_file(cx: &mut TestAppContext) {
2334        let app_state = init_test(cx);
2335        app_state
2336            .fs
2337            .as_fake()
2338            .insert_tree(
2339                path!("/root"),
2340                json!({
2341                    "a": {
2342                    },
2343                }),
2344            )
2345            .await;
2346
2347        cx.update(|cx| {
2348            open_paths(
2349                &[PathBuf::from(path!("/root/a/new"))],
2350                app_state.clone(),
2351                workspace::OpenOptions::default(),
2352                cx,
2353            )
2354        })
2355        .await
2356        .unwrap();
2357        assert_eq!(cx.read(|cx| cx.windows().len()), 1);
2358
2359        let workspace = cx.windows()[0].downcast::<Workspace>().unwrap();
2360        workspace
2361            .update(cx, |workspace, _, cx| {
2362                assert!(workspace.active_item_as::<Editor>(cx).is_some())
2363            })
2364            .unwrap();
2365    }
2366
2367    #[gpui::test]
2368    async fn test_open_paths_action(cx: &mut TestAppContext) {
2369        let app_state = init_test(cx);
2370        app_state
2371            .fs
2372            .as_fake()
2373            .insert_tree(
2374                "/root",
2375                json!({
2376                    "a": {
2377                        "aa": null,
2378                        "ab": null,
2379                    },
2380                    "b": {
2381                        "ba": null,
2382                        "bb": null,
2383                    },
2384                    "c": {
2385                        "ca": null,
2386                        "cb": null,
2387                    },
2388                    "d": {
2389                        "da": null,
2390                        "db": null,
2391                    },
2392                    "e": {
2393                        "ea": null,
2394                        "eb": null,
2395                    }
2396                }),
2397            )
2398            .await;
2399
2400        cx.update(|cx| {
2401            open_paths(
2402                &[PathBuf::from("/root/a"), PathBuf::from("/root/b")],
2403                app_state.clone(),
2404                workspace::OpenOptions::default(),
2405                cx,
2406            )
2407        })
2408        .await
2409        .unwrap();
2410        assert_eq!(cx.read(|cx| cx.windows().len()), 1);
2411
2412        cx.update(|cx| {
2413            open_paths(
2414                &[PathBuf::from("/root/a")],
2415                app_state.clone(),
2416                workspace::OpenOptions::default(),
2417                cx,
2418            )
2419        })
2420        .await
2421        .unwrap();
2422        assert_eq!(cx.read(|cx| cx.windows().len()), 1);
2423        let workspace_1 = cx
2424            .read(|cx| cx.windows()[0].downcast::<Workspace>())
2425            .unwrap();
2426        cx.run_until_parked();
2427        workspace_1
2428            .update(cx, |workspace, window, cx| {
2429                assert_eq!(workspace.worktrees(cx).count(), 2);
2430                assert!(workspace.left_dock().read(cx).is_open());
2431                assert!(
2432                    workspace
2433                        .active_pane()
2434                        .read(cx)
2435                        .focus_handle(cx)
2436                        .is_focused(window)
2437                );
2438            })
2439            .unwrap();
2440
2441        cx.update(|cx| {
2442            open_paths(
2443                &[PathBuf::from("/root/c"), PathBuf::from("/root/d")],
2444                app_state.clone(),
2445                workspace::OpenOptions::default(),
2446                cx,
2447            )
2448        })
2449        .await
2450        .unwrap();
2451        assert_eq!(cx.read(|cx| cx.windows().len()), 2);
2452
2453        // Replace existing windows
2454        let window = cx
2455            .update(|cx| cx.windows()[0].downcast::<Workspace>())
2456            .unwrap();
2457        cx.update(|cx| {
2458            open_paths(
2459                &[PathBuf::from("/root/e")],
2460                app_state,
2461                workspace::OpenOptions {
2462                    replace_window: Some(window),
2463                    ..Default::default()
2464                },
2465                cx,
2466            )
2467        })
2468        .await
2469        .unwrap();
2470        cx.background_executor.run_until_parked();
2471        assert_eq!(cx.read(|cx| cx.windows().len()), 2);
2472        let workspace_1 = cx
2473            .update(|cx| cx.windows()[0].downcast::<Workspace>())
2474            .unwrap();
2475        workspace_1
2476            .update(cx, |workspace, window, cx| {
2477                assert_eq!(
2478                    workspace
2479                        .worktrees(cx)
2480                        .map(|w| w.read(cx).abs_path())
2481                        .collect::<Vec<_>>(),
2482                    &[Path::new("/root/e").into()]
2483                );
2484                assert!(workspace.left_dock().read(cx).is_open());
2485                assert!(workspace.active_pane().focus_handle(cx).is_focused(window));
2486            })
2487            .unwrap();
2488    }
2489
2490    #[gpui::test]
2491    async fn test_open_add_new(cx: &mut TestAppContext) {
2492        let app_state = init_test(cx);
2493        app_state
2494            .fs
2495            .as_fake()
2496            .insert_tree(
2497                path!("/root"),
2498                json!({"a": "hey", "b": "", "dir": {"c": "f"}}),
2499            )
2500            .await;
2501
2502        cx.update(|cx| {
2503            open_paths(
2504                &[PathBuf::from(path!("/root/dir"))],
2505                app_state.clone(),
2506                workspace::OpenOptions::default(),
2507                cx,
2508            )
2509        })
2510        .await
2511        .unwrap();
2512        assert_eq!(cx.update(|cx| cx.windows().len()), 1);
2513
2514        cx.update(|cx| {
2515            open_paths(
2516                &[PathBuf::from(path!("/root/a"))],
2517                app_state.clone(),
2518                workspace::OpenOptions {
2519                    open_new_workspace: Some(false),
2520                    ..Default::default()
2521                },
2522                cx,
2523            )
2524        })
2525        .await
2526        .unwrap();
2527        assert_eq!(cx.update(|cx| cx.windows().len()), 1);
2528
2529        cx.update(|cx| {
2530            open_paths(
2531                &[PathBuf::from(path!("/root/dir/c"))],
2532                app_state.clone(),
2533                workspace::OpenOptions {
2534                    open_new_workspace: Some(true),
2535                    ..Default::default()
2536                },
2537                cx,
2538            )
2539        })
2540        .await
2541        .unwrap();
2542        assert_eq!(cx.update(|cx| cx.windows().len()), 2);
2543    }
2544
2545    #[gpui::test]
2546    async fn test_open_file_in_many_spaces(cx: &mut TestAppContext) {
2547        let app_state = init_test(cx);
2548        app_state
2549            .fs
2550            .as_fake()
2551            .insert_tree(
2552                path!("/root"),
2553                json!({"dir1": {"a": "b"}, "dir2": {"c": "d"}}),
2554            )
2555            .await;
2556
2557        cx.update(|cx| {
2558            open_paths(
2559                &[PathBuf::from(path!("/root/dir1/a"))],
2560                app_state.clone(),
2561                workspace::OpenOptions::default(),
2562                cx,
2563            )
2564        })
2565        .await
2566        .unwrap();
2567        assert_eq!(cx.update(|cx| cx.windows().len()), 1);
2568        let window1 = cx.update(|cx| cx.active_window().unwrap());
2569
2570        cx.update(|cx| {
2571            open_paths(
2572                &[PathBuf::from(path!("/root/dir2/c"))],
2573                app_state.clone(),
2574                workspace::OpenOptions::default(),
2575                cx,
2576            )
2577        })
2578        .await
2579        .unwrap();
2580        assert_eq!(cx.update(|cx| cx.windows().len()), 1);
2581
2582        cx.update(|cx| {
2583            open_paths(
2584                &[PathBuf::from(path!("/root/dir2"))],
2585                app_state.clone(),
2586                workspace::OpenOptions::default(),
2587                cx,
2588            )
2589        })
2590        .await
2591        .unwrap();
2592        assert_eq!(cx.update(|cx| cx.windows().len()), 2);
2593        let window2 = cx.update(|cx| cx.active_window().unwrap());
2594        assert!(window1 != window2);
2595        cx.update_window(window1, |_, window, _| window.activate_window())
2596            .unwrap();
2597
2598        cx.update(|cx| {
2599            open_paths(
2600                &[PathBuf::from(path!("/root/dir2/c"))],
2601                app_state.clone(),
2602                workspace::OpenOptions::default(),
2603                cx,
2604            )
2605        })
2606        .await
2607        .unwrap();
2608        assert_eq!(cx.update(|cx| cx.windows().len()), 2);
2609        // should have opened in window2 because that has dir2 visibly open (window1 has it open, but not in the project panel)
2610        assert!(cx.update(|cx| cx.active_window().unwrap()) == window2);
2611    }
2612
2613    #[gpui::test]
2614    async fn test_window_edit_state_restoring_disabled(cx: &mut TestAppContext) {
2615        let executor = cx.executor();
2616        let app_state = init_test(cx);
2617
2618        cx.update(|cx| {
2619            SettingsStore::update_global(cx, |store, cx| {
2620                store.update_user_settings(cx, |settings| {
2621                    settings
2622                        .session
2623                        .get_or_insert_default()
2624                        .restore_unsaved_buffers = Some(false)
2625                });
2626            });
2627        });
2628
2629        app_state
2630            .fs
2631            .as_fake()
2632            .insert_tree(path!("/root"), json!({"a": "hey"}))
2633            .await;
2634
2635        cx.update(|cx| {
2636            open_paths(
2637                &[PathBuf::from(path!("/root/a"))],
2638                app_state.clone(),
2639                workspace::OpenOptions::default(),
2640                cx,
2641            )
2642        })
2643        .await
2644        .unwrap();
2645        assert_eq!(cx.update(|cx| cx.windows().len()), 1);
2646
2647        // When opening the workspace, the window is not in a edited state.
2648        let window = cx.update(|cx| cx.windows()[0].downcast::<Workspace>().unwrap());
2649
2650        let window_is_edited = |window: WindowHandle<Workspace>, cx: &mut TestAppContext| {
2651            cx.update(|cx| window.read(cx).unwrap().is_edited())
2652        };
2653        let pane = window
2654            .read_with(cx, |workspace, _| workspace.active_pane().clone())
2655            .unwrap();
2656        let editor = window
2657            .read_with(cx, |workspace, cx| {
2658                workspace
2659                    .active_item(cx)
2660                    .unwrap()
2661                    .downcast::<Editor>()
2662                    .unwrap()
2663            })
2664            .unwrap();
2665
2666        assert!(!window_is_edited(window, cx));
2667
2668        // Editing a buffer marks the window as edited.
2669        window
2670            .update(cx, |_, window, cx| {
2671                editor.update(cx, |editor, cx| editor.insert("EDIT", window, cx));
2672            })
2673            .unwrap();
2674
2675        assert!(window_is_edited(window, cx));
2676
2677        // Undoing the edit restores the window's edited state.
2678        window
2679            .update(cx, |_, window, cx| {
2680                editor.update(cx, |editor, cx| {
2681                    editor.undo(&Default::default(), window, cx)
2682                });
2683            })
2684            .unwrap();
2685        assert!(!window_is_edited(window, cx));
2686
2687        // Redoing the edit marks the window as edited again.
2688        window
2689            .update(cx, |_, window, cx| {
2690                editor.update(cx, |editor, cx| {
2691                    editor.redo(&Default::default(), window, cx)
2692                });
2693            })
2694            .unwrap();
2695        assert!(window_is_edited(window, cx));
2696        let weak = editor.downgrade();
2697
2698        // Closing the item restores the window's edited state.
2699        let close = window
2700            .update(cx, |_, window, cx| {
2701                pane.update(cx, |pane, cx| {
2702                    drop(editor);
2703                    pane.close_active_item(&Default::default(), window, cx)
2704                })
2705            })
2706            .unwrap();
2707        executor.run_until_parked();
2708
2709        cx.simulate_prompt_answer("Don't Save");
2710        close.await.unwrap();
2711
2712        // Advance the clock to ensure that the item has been serialized and dropped from the queue
2713        cx.executor().advance_clock(Duration::from_secs(1));
2714
2715        weak.assert_released();
2716        assert!(!window_is_edited(window, cx));
2717        // Opening the buffer again doesn't impact the window's edited state.
2718        cx.update(|cx| {
2719            open_paths(
2720                &[PathBuf::from(path!("/root/a"))],
2721                app_state,
2722                workspace::OpenOptions::default(),
2723                cx,
2724            )
2725        })
2726        .await
2727        .unwrap();
2728        executor.run_until_parked();
2729
2730        window
2731            .update(cx, |workspace, _, cx| {
2732                let editor = workspace
2733                    .active_item(cx)
2734                    .unwrap()
2735                    .downcast::<Editor>()
2736                    .unwrap();
2737
2738                editor.update(cx, |editor, cx| {
2739                    assert_eq!(editor.text(cx), "hey");
2740                });
2741            })
2742            .unwrap();
2743
2744        let editor = window
2745            .read_with(cx, |workspace, cx| {
2746                workspace
2747                    .active_item(cx)
2748                    .unwrap()
2749                    .downcast::<Editor>()
2750                    .unwrap()
2751            })
2752            .unwrap();
2753        assert!(!window_is_edited(window, cx));
2754
2755        // Editing the buffer marks the window as edited.
2756        window
2757            .update(cx, |_, window, cx| {
2758                editor.update(cx, |editor, cx| editor.insert("EDIT", window, cx));
2759            })
2760            .unwrap();
2761        executor.run_until_parked();
2762        assert!(window_is_edited(window, cx));
2763
2764        // Ensure closing the window via the mouse gets preempted due to the
2765        // buffer having unsaved changes.
2766        assert!(!VisualTestContext::from_window(window.into(), cx).simulate_close());
2767        executor.run_until_parked();
2768        assert_eq!(cx.update(|cx| cx.windows().len()), 1);
2769
2770        // The window is successfully closed after the user dismisses the prompt.
2771        cx.simulate_prompt_answer("Don't Save");
2772        executor.run_until_parked();
2773        assert_eq!(cx.update(|cx| cx.windows().len()), 0);
2774    }
2775
2776    #[gpui::test]
2777    async fn test_window_edit_state_restoring_enabled(cx: &mut TestAppContext) {
2778        let app_state = init_test(cx);
2779        app_state
2780            .fs
2781            .as_fake()
2782            .insert_tree(path!("/root"), json!({"a": "hey"}))
2783            .await;
2784
2785        cx.update(|cx| {
2786            open_paths(
2787                &[PathBuf::from(path!("/root/a"))],
2788                app_state.clone(),
2789                workspace::OpenOptions::default(),
2790                cx,
2791            )
2792        })
2793        .await
2794        .unwrap();
2795
2796        assert_eq!(cx.update(|cx| cx.windows().len()), 1);
2797
2798        // When opening the workspace, the window is not in a edited state.
2799        let window = cx.update(|cx| cx.windows()[0].downcast::<Workspace>().unwrap());
2800
2801        let window_is_edited = |window: WindowHandle<Workspace>, cx: &mut TestAppContext| {
2802            cx.update(|cx| window.read(cx).unwrap().is_edited())
2803        };
2804
2805        let editor = window
2806            .read_with(cx, |workspace, cx| {
2807                workspace
2808                    .active_item(cx)
2809                    .unwrap()
2810                    .downcast::<Editor>()
2811                    .unwrap()
2812            })
2813            .unwrap();
2814
2815        assert!(!window_is_edited(window, cx));
2816
2817        // Editing a buffer marks the window as edited.
2818        window
2819            .update(cx, |_, window, cx| {
2820                editor.update(cx, |editor, cx| editor.insert("EDIT", window, cx));
2821            })
2822            .unwrap();
2823
2824        assert!(window_is_edited(window, cx));
2825        cx.run_until_parked();
2826
2827        // Advance the clock to make sure the workspace is serialized
2828        cx.executor().advance_clock(Duration::from_secs(1));
2829
2830        // When closing the window, no prompt shows up and the window is closed.
2831        // buffer having unsaved changes.
2832        assert!(!VisualTestContext::from_window(window.into(), cx).simulate_close());
2833        cx.run_until_parked();
2834        assert_eq!(cx.update(|cx| cx.windows().len()), 0);
2835
2836        // When we now reopen the window, the edited state and the edited buffer are back
2837        cx.update(|cx| {
2838            open_paths(
2839                &[PathBuf::from(path!("/root/a"))],
2840                app_state.clone(),
2841                workspace::OpenOptions::default(),
2842                cx,
2843            )
2844        })
2845        .await
2846        .unwrap();
2847
2848        assert_eq!(cx.update(|cx| cx.windows().len()), 1);
2849        assert!(cx.update(|cx| cx.active_window().is_some()));
2850
2851        cx.run_until_parked();
2852
2853        // When opening the workspace, the window is not in a edited state.
2854        let window = cx.update(|cx| cx.active_window().unwrap().downcast::<Workspace>().unwrap());
2855        assert!(window_is_edited(window, cx));
2856
2857        window
2858            .update(cx, |workspace, _, cx| {
2859                let editor = workspace
2860                    .active_item(cx)
2861                    .unwrap()
2862                    .downcast::<editor::Editor>()
2863                    .unwrap();
2864                editor.update(cx, |editor, cx| {
2865                    assert_eq!(editor.text(cx), "EDIThey");
2866                    assert!(editor.is_dirty(cx));
2867                });
2868
2869                editor
2870            })
2871            .unwrap();
2872    }
2873
2874    #[gpui::test]
2875    async fn test_new_empty_workspace(cx: &mut TestAppContext) {
2876        let app_state = init_test(cx);
2877        cx.update(|cx| {
2878            open_new(
2879                Default::default(),
2880                app_state.clone(),
2881                cx,
2882                |workspace, window, cx| {
2883                    Editor::new_file(workspace, &Default::default(), window, cx)
2884                },
2885            )
2886        })
2887        .await
2888        .unwrap();
2889        cx.run_until_parked();
2890
2891        let workspace = cx
2892            .update(|cx| cx.windows().first().unwrap().downcast::<Workspace>())
2893            .unwrap();
2894
2895        let editor = workspace
2896            .update(cx, |workspace, _, cx| {
2897                let editor = workspace
2898                    .active_item(cx)
2899                    .unwrap()
2900                    .downcast::<editor::Editor>()
2901                    .unwrap();
2902                editor.update(cx, |editor, cx| {
2903                    assert!(editor.text(cx).is_empty());
2904                    assert!(!editor.is_dirty(cx));
2905                });
2906
2907                editor
2908            })
2909            .unwrap();
2910
2911        let save_task = workspace
2912            .update(cx, |workspace, window, cx| {
2913                workspace.save_active_item(SaveIntent::Save, window, cx)
2914            })
2915            .unwrap();
2916        app_state.fs.create_dir(Path::new("/root")).await.unwrap();
2917        cx.background_executor.run_until_parked();
2918        cx.simulate_new_path_selection(|_| Some(PathBuf::from("/root/the-new-name")));
2919        save_task.await.unwrap();
2920        workspace
2921            .update(cx, |_, _, cx| {
2922                editor.update(cx, |editor, cx| {
2923                    assert!(!editor.is_dirty(cx));
2924                    assert_eq!(editor.title(cx), "the-new-name");
2925                });
2926            })
2927            .unwrap();
2928    }
2929
2930    #[gpui::test]
2931    async fn test_open_entry(cx: &mut TestAppContext) {
2932        let app_state = init_test(cx);
2933        app_state
2934            .fs
2935            .as_fake()
2936            .insert_tree(
2937                path!("/root"),
2938                json!({
2939                    "a": {
2940                        "file1": "contents 1",
2941                        "file2": "contents 2",
2942                        "file3": "contents 3",
2943                    },
2944                }),
2945            )
2946            .await;
2947
2948        let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
2949        project.update(cx, |project, _cx| project.languages().add(markdown_lang()));
2950        let window = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
2951        let workspace = window.root(cx).unwrap();
2952
2953        let entries = cx.read(|cx| workspace.file_project_paths(cx));
2954        let file1 = entries[0].clone();
2955        let file2 = entries[1].clone();
2956        let file3 = entries[2].clone();
2957
2958        // Open the first entry
2959        let entry_1 = window
2960            .update(cx, |w, window, cx| {
2961                w.open_path(file1.clone(), None, true, window, cx)
2962            })
2963            .unwrap()
2964            .await
2965            .unwrap();
2966        cx.read(|cx| {
2967            let pane = workspace.read(cx).active_pane().read(cx);
2968            assert_eq!(
2969                pane.active_item().unwrap().project_path(cx),
2970                Some(file1.clone())
2971            );
2972            assert_eq!(pane.items_len(), 1);
2973        });
2974
2975        // Open the second entry
2976        window
2977            .update(cx, |w, window, cx| {
2978                w.open_path(file2.clone(), None, true, window, cx)
2979            })
2980            .unwrap()
2981            .await
2982            .unwrap();
2983        cx.read(|cx| {
2984            let pane = workspace.read(cx).active_pane().read(cx);
2985            assert_eq!(
2986                pane.active_item().unwrap().project_path(cx),
2987                Some(file2.clone())
2988            );
2989            assert_eq!(pane.items_len(), 2);
2990        });
2991
2992        // Open the first entry again. The existing pane item is activated.
2993        let entry_1b = window
2994            .update(cx, |w, window, cx| {
2995                w.open_path(file1.clone(), None, true, window, cx)
2996            })
2997            .unwrap()
2998            .await
2999            .unwrap();
3000        assert_eq!(entry_1.item_id(), entry_1b.item_id());
3001
3002        cx.read(|cx| {
3003            let pane = workspace.read(cx).active_pane().read(cx);
3004            assert_eq!(
3005                pane.active_item().unwrap().project_path(cx),
3006                Some(file1.clone())
3007            );
3008            assert_eq!(pane.items_len(), 2);
3009        });
3010
3011        // Split the pane with the first entry, then open the second entry again.
3012        window
3013            .update(cx, |w, window, cx| {
3014                w.split_and_clone(w.active_pane().clone(), SplitDirection::Right, window, cx)
3015            })
3016            .unwrap()
3017            .await
3018            .unwrap();
3019        window
3020            .update(cx, |w, window, cx| {
3021                w.open_path(file2.clone(), None, true, window, cx)
3022            })
3023            .unwrap()
3024            .await
3025            .unwrap();
3026
3027        window
3028            .read_with(cx, |w, cx| {
3029                assert_eq!(
3030                    w.active_pane()
3031                        .read(cx)
3032                        .active_item()
3033                        .unwrap()
3034                        .project_path(cx),
3035                    Some(file2.clone())
3036                );
3037            })
3038            .unwrap();
3039
3040        // Open the third entry twice concurrently. Only one pane item is added.
3041        let (t1, t2) = window
3042            .update(cx, |w, window, cx| {
3043                (
3044                    w.open_path(file3.clone(), None, true, window, cx),
3045                    w.open_path(file3.clone(), None, true, window, cx),
3046                )
3047            })
3048            .unwrap();
3049        t1.await.unwrap();
3050        t2.await.unwrap();
3051        cx.read(|cx| {
3052            let pane = workspace.read(cx).active_pane().read(cx);
3053            assert_eq!(
3054                pane.active_item().unwrap().project_path(cx),
3055                Some(file3.clone())
3056            );
3057            let pane_entries = pane
3058                .items()
3059                .map(|i| i.project_path(cx).unwrap())
3060                .collect::<Vec<_>>();
3061            assert_eq!(pane_entries, &[file1, file2, file3]);
3062        });
3063    }
3064
3065    #[gpui::test]
3066    async fn test_open_paths(cx: &mut TestAppContext) {
3067        let app_state = init_test(cx);
3068
3069        app_state
3070            .fs
3071            .as_fake()
3072            .insert_tree(
3073                path!("/"),
3074                json!({
3075                    "dir1": {
3076                        "a.txt": ""
3077                    },
3078                    "dir2": {
3079                        "b.txt": ""
3080                    },
3081                    "dir3": {
3082                        "c.txt": ""
3083                    },
3084                    "d.txt": ""
3085                }),
3086            )
3087            .await;
3088
3089        cx.update(|cx| {
3090            open_paths(
3091                &[PathBuf::from(path!("/dir1/"))],
3092                app_state,
3093                workspace::OpenOptions::default(),
3094                cx,
3095            )
3096        })
3097        .await
3098        .unwrap();
3099        cx.run_until_parked();
3100        assert_eq!(cx.update(|cx| cx.windows().len()), 1);
3101        let window = cx.update(|cx| cx.windows()[0].downcast::<Workspace>().unwrap());
3102        let workspace = window.root(cx).unwrap();
3103
3104        #[track_caller]
3105        fn assert_project_panel_selection(
3106            workspace: &Workspace,
3107            expected_worktree_path: &Path,
3108            expected_entry_path: &RelPath,
3109            cx: &App,
3110        ) {
3111            let project_panel = [
3112                workspace.left_dock().read(cx).panel::<ProjectPanel>(),
3113                workspace.right_dock().read(cx).panel::<ProjectPanel>(),
3114                workspace.bottom_dock().read(cx).panel::<ProjectPanel>(),
3115            ]
3116            .into_iter()
3117            .find_map(std::convert::identity)
3118            .expect("found no project panels")
3119            .read(cx);
3120            let (selected_worktree, selected_entry) = project_panel
3121                .selected_entry(cx)
3122                .expect("project panel should have a selected entry");
3123            assert_eq!(
3124                selected_worktree.abs_path().as_ref(),
3125                expected_worktree_path,
3126                "Unexpected project panel selected worktree path"
3127            );
3128            assert_eq!(
3129                selected_entry.path.as_ref(),
3130                expected_entry_path,
3131                "Unexpected project panel selected entry path"
3132            );
3133        }
3134
3135        // Open a file within an existing worktree.
3136        window
3137            .update(cx, |workspace, window, cx| {
3138                workspace.open_paths(
3139                    vec![path!("/dir1/a.txt").into()],
3140                    OpenOptions {
3141                        visible: Some(OpenVisible::All),
3142                        ..Default::default()
3143                    },
3144                    None,
3145                    window,
3146                    cx,
3147                )
3148            })
3149            .unwrap()
3150            .await;
3151        cx.run_until_parked();
3152        cx.read(|cx| {
3153            let workspace = workspace.read(cx);
3154            assert_project_panel_selection(
3155                workspace,
3156                Path::new(path!("/dir1")),
3157                rel_path("a.txt"),
3158                cx,
3159            );
3160            assert_eq!(
3161                workspace
3162                    .active_pane()
3163                    .read(cx)
3164                    .active_item()
3165                    .unwrap()
3166                    .act_as::<Editor>(cx)
3167                    .unwrap()
3168                    .read(cx)
3169                    .title(cx),
3170                "a.txt"
3171            );
3172        });
3173
3174        // Open a file outside of any existing worktree.
3175        window
3176            .update(cx, |workspace, window, cx| {
3177                workspace.open_paths(
3178                    vec![path!("/dir2/b.txt").into()],
3179                    OpenOptions {
3180                        visible: Some(OpenVisible::All),
3181                        ..Default::default()
3182                    },
3183                    None,
3184                    window,
3185                    cx,
3186                )
3187            })
3188            .unwrap()
3189            .await;
3190        cx.run_until_parked();
3191        cx.read(|cx| {
3192            let workspace = workspace.read(cx);
3193            assert_project_panel_selection(
3194                workspace,
3195                Path::new(path!("/dir2/b.txt")),
3196                rel_path(""),
3197                cx,
3198            );
3199            let worktree_roots = workspace
3200                .worktrees(cx)
3201                .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref())
3202                .collect::<HashSet<_>>();
3203            assert_eq!(
3204                worktree_roots,
3205                vec![path!("/dir1"), path!("/dir2/b.txt")]
3206                    .into_iter()
3207                    .map(Path::new)
3208                    .collect(),
3209            );
3210            assert_eq!(
3211                workspace
3212                    .active_pane()
3213                    .read(cx)
3214                    .active_item()
3215                    .unwrap()
3216                    .act_as::<Editor>(cx)
3217                    .unwrap()
3218                    .read(cx)
3219                    .title(cx),
3220                "b.txt"
3221            );
3222        });
3223
3224        // Ensure opening a directory and one of its children only adds one worktree.
3225        window
3226            .update(cx, |workspace, window, cx| {
3227                workspace.open_paths(
3228                    vec![path!("/dir3").into(), path!("/dir3/c.txt").into()],
3229                    OpenOptions {
3230                        visible: Some(OpenVisible::All),
3231                        ..Default::default()
3232                    },
3233                    None,
3234                    window,
3235                    cx,
3236                )
3237            })
3238            .unwrap()
3239            .await;
3240        cx.run_until_parked();
3241        cx.read(|cx| {
3242            let workspace = workspace.read(cx);
3243            assert_project_panel_selection(
3244                workspace,
3245                Path::new(path!("/dir3")),
3246                rel_path("c.txt"),
3247                cx,
3248            );
3249            let worktree_roots = workspace
3250                .worktrees(cx)
3251                .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref())
3252                .collect::<HashSet<_>>();
3253            assert_eq!(
3254                worktree_roots,
3255                vec![path!("/dir1"), path!("/dir2/b.txt"), path!("/dir3")]
3256                    .into_iter()
3257                    .map(Path::new)
3258                    .collect(),
3259            );
3260            assert_eq!(
3261                workspace
3262                    .active_pane()
3263                    .read(cx)
3264                    .active_item()
3265                    .unwrap()
3266                    .act_as::<Editor>(cx)
3267                    .unwrap()
3268                    .read(cx)
3269                    .title(cx),
3270                "c.txt"
3271            );
3272        });
3273
3274        // Ensure opening invisibly a file outside an existing worktree adds a new, invisible worktree.
3275        window
3276            .update(cx, |workspace, window, cx| {
3277                workspace.open_paths(
3278                    vec![path!("/d.txt").into()],
3279                    OpenOptions {
3280                        visible: Some(OpenVisible::None),
3281                        ..Default::default()
3282                    },
3283                    None,
3284                    window,
3285                    cx,
3286                )
3287            })
3288            .unwrap()
3289            .await;
3290        cx.run_until_parked();
3291        cx.read(|cx| {
3292            let workspace = workspace.read(cx);
3293            assert_project_panel_selection(workspace, Path::new(path!("/d.txt")), rel_path(""), cx);
3294            let worktree_roots = workspace
3295                .worktrees(cx)
3296                .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref())
3297                .collect::<HashSet<_>>();
3298            assert_eq!(
3299                worktree_roots,
3300                vec![
3301                    path!("/dir1"),
3302                    path!("/dir2/b.txt"),
3303                    path!("/dir3"),
3304                    path!("/d.txt")
3305                ]
3306                .into_iter()
3307                .map(Path::new)
3308                .collect(),
3309            );
3310
3311            let visible_worktree_roots = workspace
3312                .visible_worktrees(cx)
3313                .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref())
3314                .collect::<HashSet<_>>();
3315            assert_eq!(
3316                visible_worktree_roots,
3317                vec![path!("/dir1"), path!("/dir2/b.txt"), path!("/dir3")]
3318                    .into_iter()
3319                    .map(Path::new)
3320                    .collect(),
3321            );
3322
3323            assert_eq!(
3324                workspace
3325                    .active_pane()
3326                    .read(cx)
3327                    .active_item()
3328                    .unwrap()
3329                    .act_as::<Editor>(cx)
3330                    .unwrap()
3331                    .read(cx)
3332                    .title(cx),
3333                "d.txt"
3334            );
3335        });
3336    }
3337
3338    #[gpui::test]
3339    async fn test_opening_excluded_paths(cx: &mut TestAppContext) {
3340        let app_state = init_test(cx);
3341        cx.update(|cx| {
3342            cx.update_global::<SettingsStore, _>(|store, cx| {
3343                store.update_user_settings(cx, |project_settings| {
3344                    project_settings.project.worktree.file_scan_exclusions =
3345                        Some(vec!["excluded_dir".to_string(), "**/.git".to_string()]);
3346                });
3347            });
3348        });
3349        app_state
3350            .fs
3351            .as_fake()
3352            .insert_tree(
3353                path!("/root"),
3354                json!({
3355                    ".gitignore": "ignored_dir\n",
3356                    ".git": {
3357                        "HEAD": "ref: refs/heads/main",
3358                    },
3359                    "regular_dir": {
3360                        "file": "regular file contents",
3361                    },
3362                    "ignored_dir": {
3363                        "ignored_subdir": {
3364                            "file": "ignored subfile contents",
3365                        },
3366                        "file": "ignored file contents",
3367                    },
3368                    "excluded_dir": {
3369                        "file": "excluded file contents",
3370                        "ignored_subdir": {
3371                            "file": "ignored subfile contents",
3372                        },
3373                    },
3374                }),
3375            )
3376            .await;
3377
3378        let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
3379        project.update(cx, |project, _cx| project.languages().add(markdown_lang()));
3380        let window = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
3381        let workspace = window.root(cx).unwrap();
3382
3383        let initial_entries = cx.read(|cx| workspace.file_project_paths(cx));
3384        let paths_to_open = [
3385            PathBuf::from(path!("/root/excluded_dir/file")),
3386            PathBuf::from(path!("/root/.git/HEAD")),
3387            PathBuf::from(path!("/root/excluded_dir/ignored_subdir")),
3388        ];
3389        let (opened_workspace, new_items) = cx
3390            .update(|cx| {
3391                workspace::open_paths(
3392                    &paths_to_open,
3393                    app_state,
3394                    workspace::OpenOptions::default(),
3395                    cx,
3396                )
3397            })
3398            .await
3399            .unwrap();
3400
3401        assert_eq!(
3402            opened_workspace.root(cx).unwrap().entity_id(),
3403            workspace.entity_id(),
3404            "Excluded files in subfolders of a workspace root should be opened in the workspace"
3405        );
3406        let mut opened_paths = cx.read(|cx| {
3407            assert_eq!(
3408                new_items.len(),
3409                paths_to_open.len(),
3410                "Expect to get the same number of opened items as submitted paths to open"
3411            );
3412            new_items
3413                .iter()
3414                .zip(paths_to_open.iter())
3415                .map(|(i, path)| {
3416                    match i {
3417                        Some(Ok(i)) => Some(i.project_path(cx).map(|p| p.path)),
3418                        Some(Err(e)) => panic!("Excluded file {path:?} failed to open: {e:?}"),
3419                        None => None,
3420                    }
3421                    .flatten()
3422                })
3423                .collect::<Vec<_>>()
3424        });
3425        opened_paths.sort();
3426        assert_eq!(
3427            opened_paths,
3428            vec![
3429                None,
3430                Some(rel_path(".git/HEAD").into()),
3431                Some(rel_path("excluded_dir/file").into()),
3432            ],
3433            "Excluded files should get opened, excluded dir should not get opened"
3434        );
3435
3436        let entries = cx.read(|cx| workspace.file_project_paths(cx));
3437        assert_eq!(
3438            initial_entries, entries,
3439            "Workspace entries should not change after opening excluded files and directories paths"
3440        );
3441
3442        cx.read(|cx| {
3443                let pane = workspace.read(cx).active_pane().read(cx);
3444                let mut opened_buffer_paths = pane
3445                    .items()
3446                    .map(|i| {
3447                        i.project_path(cx)
3448                            .expect("all excluded files that got open should have a path")
3449                            .path
3450                    })
3451                    .collect::<Vec<_>>();
3452                opened_buffer_paths.sort();
3453                assert_eq!(
3454                    opened_buffer_paths,
3455                    vec![rel_path(".git/HEAD").into(), rel_path("excluded_dir/file").into()],
3456                    "Despite not being present in the worktrees, buffers for excluded files are opened and added to the pane"
3457                );
3458            });
3459    }
3460
3461    #[gpui::test]
3462    async fn test_save_conflicting_item(cx: &mut TestAppContext) {
3463        let app_state = init_test(cx);
3464        app_state
3465            .fs
3466            .as_fake()
3467            .insert_tree(path!("/root"), json!({ "a.txt": "" }))
3468            .await;
3469
3470        let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
3471        project.update(cx, |project, _cx| project.languages().add(markdown_lang()));
3472        let window = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
3473        let workspace = window.root(cx).unwrap();
3474
3475        // Open a file within an existing worktree.
3476        window
3477            .update(cx, |workspace, window, cx| {
3478                workspace.open_paths(
3479                    vec![PathBuf::from(path!("/root/a.txt"))],
3480                    OpenOptions {
3481                        visible: Some(OpenVisible::All),
3482                        ..Default::default()
3483                    },
3484                    None,
3485                    window,
3486                    cx,
3487                )
3488            })
3489            .unwrap()
3490            .await;
3491        let editor = cx.read(|cx| {
3492            let pane = workspace.read(cx).active_pane().read(cx);
3493            let item = pane.active_item().unwrap();
3494            item.downcast::<Editor>().unwrap()
3495        });
3496
3497        window
3498            .update(cx, |_, window, cx| {
3499                editor.update(cx, |editor, cx| editor.handle_input("x", window, cx));
3500            })
3501            .unwrap();
3502
3503        app_state
3504            .fs
3505            .as_fake()
3506            .insert_file(path!("/root/a.txt"), b"changed".to_vec())
3507            .await;
3508
3509        cx.run_until_parked();
3510        cx.read(|cx| assert!(editor.is_dirty(cx)));
3511        cx.read(|cx| assert!(editor.has_conflict(cx)));
3512
3513        let save_task = window
3514            .update(cx, |workspace, window, cx| {
3515                workspace.save_active_item(SaveIntent::Save, window, cx)
3516            })
3517            .unwrap();
3518        cx.background_executor.run_until_parked();
3519        cx.simulate_prompt_answer("Overwrite");
3520        save_task.await.unwrap();
3521        window
3522            .update(cx, |_, _, cx| {
3523                editor.update(cx, |editor, cx| {
3524                    assert!(!editor.is_dirty(cx));
3525                    assert!(!editor.has_conflict(cx));
3526                });
3527            })
3528            .unwrap();
3529    }
3530
3531    #[gpui::test]
3532    async fn test_open_and_save_new_file(cx: &mut TestAppContext) {
3533        let app_state = init_test(cx);
3534        app_state
3535            .fs
3536            .create_dir(Path::new(path!("/root")))
3537            .await
3538            .unwrap();
3539
3540        let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
3541        project.update(cx, |project, _| {
3542            project.languages().add(markdown_lang());
3543            project.languages().add(rust_lang());
3544        });
3545        let window = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
3546        let worktree = cx.update(|cx| window.read(cx).unwrap().worktrees(cx).next().unwrap());
3547
3548        // Create a new untitled buffer
3549        cx.dispatch_action(window.into(), NewFile);
3550        let editor = window
3551            .read_with(cx, |workspace, cx| {
3552                workspace
3553                    .active_item(cx)
3554                    .unwrap()
3555                    .downcast::<Editor>()
3556                    .unwrap()
3557            })
3558            .unwrap();
3559
3560        window
3561            .update(cx, |_, window, cx| {
3562                editor.update(cx, |editor, cx| {
3563                    assert!(!editor.is_dirty(cx));
3564                    assert_eq!(editor.title(cx), "untitled");
3565                    assert!(Arc::ptr_eq(
3566                        &editor
3567                            .buffer()
3568                            .read(cx)
3569                            .language_at(MultiBufferOffset(0), cx)
3570                            .unwrap(),
3571                        &languages::PLAIN_TEXT
3572                    ));
3573                    editor.handle_input("hi", window, cx);
3574                    assert!(editor.is_dirty(cx));
3575                });
3576            })
3577            .unwrap();
3578
3579        // Save the buffer. This prompts for a filename.
3580        let save_task = window
3581            .update(cx, |workspace, window, cx| {
3582                workspace.save_active_item(SaveIntent::Save, window, cx)
3583            })
3584            .unwrap();
3585        cx.background_executor.run_until_parked();
3586        cx.simulate_new_path_selection(|parent_dir| {
3587            assert_eq!(parent_dir, Path::new(path!("/root")));
3588            Some(parent_dir.join("the-new-name.rs"))
3589        });
3590        cx.read(|cx| {
3591            assert!(editor.is_dirty(cx));
3592            assert_eq!(editor.read(cx).title(cx), "hi");
3593        });
3594
3595        // When the save completes, the buffer's title is updated and the language is assigned based
3596        // on the path.
3597        save_task.await.unwrap();
3598        window
3599            .update(cx, |_, _, cx| {
3600                editor.update(cx, |editor, cx| {
3601                    assert!(!editor.is_dirty(cx));
3602                    assert_eq!(editor.title(cx), "the-new-name.rs");
3603                    assert_eq!(
3604                        editor
3605                            .buffer()
3606                            .read(cx)
3607                            .language_at(MultiBufferOffset(0), cx)
3608                            .unwrap()
3609                            .name(),
3610                        "Rust".into()
3611                    );
3612                });
3613            })
3614            .unwrap();
3615
3616        // Edit the file and save it again. This time, there is no filename prompt.
3617        window
3618            .update(cx, |_, window, cx| {
3619                editor.update(cx, |editor, cx| {
3620                    editor.handle_input(" there", window, cx);
3621                    assert!(editor.is_dirty(cx));
3622                });
3623            })
3624            .unwrap();
3625
3626        let save_task = window
3627            .update(cx, |workspace, window, cx| {
3628                workspace.save_active_item(SaveIntent::Save, window, cx)
3629            })
3630            .unwrap();
3631        save_task.await.unwrap();
3632
3633        assert!(!cx.did_prompt_for_new_path());
3634        window
3635            .update(cx, |_, _, cx| {
3636                editor.update(cx, |editor, cx| {
3637                    assert!(!editor.is_dirty(cx));
3638                    assert_eq!(editor.title(cx), "the-new-name.rs")
3639                });
3640            })
3641            .unwrap();
3642
3643        // Open the same newly-created file in another pane item. The new editor should reuse
3644        // the same buffer.
3645        cx.dispatch_action(window.into(), NewFile);
3646        window
3647            .update(cx, |workspace, window, cx| {
3648                workspace.split_and_clone(
3649                    workspace.active_pane().clone(),
3650                    SplitDirection::Right,
3651                    window,
3652                    cx,
3653                )
3654            })
3655            .unwrap()
3656            .await
3657            .unwrap();
3658        window
3659            .update(cx, |workspace, window, cx| {
3660                workspace.open_path(
3661                    (worktree.read(cx).id(), rel_path("the-new-name.rs")),
3662                    None,
3663                    true,
3664                    window,
3665                    cx,
3666                )
3667            })
3668            .unwrap()
3669            .await
3670            .unwrap();
3671        let editor2 = window
3672            .update(cx, |workspace, _, cx| {
3673                workspace
3674                    .active_item(cx)
3675                    .unwrap()
3676                    .downcast::<Editor>()
3677                    .unwrap()
3678            })
3679            .unwrap();
3680        cx.read(|cx| {
3681            assert_eq!(
3682                editor2.read(cx).buffer().read(cx).as_singleton().unwrap(),
3683                editor.read(cx).buffer().read(cx).as_singleton().unwrap()
3684            );
3685        })
3686    }
3687
3688    #[gpui::test]
3689    async fn test_setting_language_when_saving_as_single_file_worktree(cx: &mut TestAppContext) {
3690        let app_state = init_test(cx);
3691        app_state.fs.create_dir(Path::new("/root")).await.unwrap();
3692
3693        let project = Project::test(app_state.fs.clone(), [], cx).await;
3694        project.update(cx, |project, _| {
3695            project.languages().add(language::rust_lang());
3696            project.languages().add(language::markdown_lang());
3697        });
3698        let window = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
3699
3700        // Create a new untitled buffer
3701        cx.dispatch_action(window.into(), NewFile);
3702        let editor = window
3703            .read_with(cx, |workspace, cx| {
3704                workspace
3705                    .active_item(cx)
3706                    .unwrap()
3707                    .downcast::<Editor>()
3708                    .unwrap()
3709            })
3710            .unwrap();
3711        window
3712            .update(cx, |_, window, cx| {
3713                editor.update(cx, |editor, cx| {
3714                    assert!(Arc::ptr_eq(
3715                        &editor
3716                            .buffer()
3717                            .read(cx)
3718                            .language_at(MultiBufferOffset(0), cx)
3719                            .unwrap(),
3720                        &languages::PLAIN_TEXT
3721                    ));
3722                    editor.handle_input("hi", window, cx);
3723                    assert!(editor.is_dirty(cx));
3724                });
3725            })
3726            .unwrap();
3727
3728        // Save the buffer. This prompts for a filename.
3729        let save_task = window
3730            .update(cx, |workspace, window, cx| {
3731                workspace.save_active_item(SaveIntent::Save, window, cx)
3732            })
3733            .unwrap();
3734        cx.background_executor.run_until_parked();
3735        cx.simulate_new_path_selection(|_| Some(PathBuf::from("/root/the-new-name.rs")));
3736        save_task.await.unwrap();
3737        // The buffer is not dirty anymore and the language is assigned based on the path.
3738        window
3739            .update(cx, |_, _, cx| {
3740                editor.update(cx, |editor, cx| {
3741                    assert!(!editor.is_dirty(cx));
3742                    assert_eq!(
3743                        editor
3744                            .buffer()
3745                            .read(cx)
3746                            .language_at(MultiBufferOffset(0), cx)
3747                            .unwrap()
3748                            .name(),
3749                        "Rust".into()
3750                    )
3751                });
3752            })
3753            .unwrap();
3754    }
3755
3756    #[gpui::test]
3757    async fn test_pane_actions(cx: &mut TestAppContext) {
3758        let app_state = init_test(cx);
3759        app_state
3760            .fs
3761            .as_fake()
3762            .insert_tree(
3763                path!("/root"),
3764                json!({
3765                    "a": {
3766                        "file1": "contents 1",
3767                        "file2": "contents 2",
3768                        "file3": "contents 3",
3769                    },
3770                }),
3771            )
3772            .await;
3773
3774        let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
3775        project.update(cx, |project, _cx| project.languages().add(markdown_lang()));
3776        let window = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
3777        let workspace = window.root(cx).unwrap();
3778
3779        let entries = cx.read(|cx| workspace.file_project_paths(cx));
3780        let file1 = entries[0].clone();
3781
3782        let pane_1 = cx.read(|cx| workspace.read(cx).active_pane().clone());
3783
3784        window
3785            .update(cx, |w, window, cx| {
3786                w.open_path(file1.clone(), None, true, window, cx)
3787            })
3788            .unwrap()
3789            .await
3790            .unwrap();
3791
3792        let (editor_1, buffer) = window
3793            .update(cx, |_, window, cx| {
3794                pane_1.update(cx, |pane_1, cx| {
3795                    let editor = pane_1.active_item().unwrap().downcast::<Editor>().unwrap();
3796                    assert_eq!(editor.project_path(cx), Some(file1.clone()));
3797                    let buffer = editor.update(cx, |editor, cx| {
3798                        editor.insert("dirt", window, cx);
3799                        editor.buffer().downgrade()
3800                    });
3801                    (editor.downgrade(), buffer)
3802                })
3803            })
3804            .unwrap();
3805
3806        cx.dispatch_action(window.into(), pane::SplitRight);
3807        let editor_2 = cx.update(|cx| {
3808            let pane_2 = workspace.read(cx).active_pane().clone();
3809            assert_ne!(pane_1, pane_2);
3810
3811            let pane2_item = pane_2.read(cx).active_item().unwrap();
3812            assert_eq!(pane2_item.project_path(cx), Some(file1.clone()));
3813
3814            pane2_item.downcast::<Editor>().unwrap().downgrade()
3815        });
3816        cx.dispatch_action(
3817            window.into(),
3818            workspace::CloseActiveItem {
3819                save_intent: None,
3820                close_pinned: false,
3821            },
3822        );
3823
3824        cx.background_executor.run_until_parked();
3825        window
3826            .read_with(cx, |workspace, _| {
3827                assert_eq!(workspace.panes().len(), 1);
3828                assert_eq!(workspace.active_pane(), &pane_1);
3829            })
3830            .unwrap();
3831
3832        cx.dispatch_action(
3833            window.into(),
3834            workspace::CloseActiveItem {
3835                save_intent: None,
3836                close_pinned: false,
3837            },
3838        );
3839        cx.background_executor.run_until_parked();
3840        cx.simulate_prompt_answer("Don't Save");
3841        cx.background_executor.run_until_parked();
3842
3843        window
3844            .update(cx, |workspace, _, cx| {
3845                assert_eq!(workspace.panes().len(), 1);
3846                assert!(workspace.active_item(cx).is_none());
3847            })
3848            .unwrap();
3849
3850        cx.background_executor
3851            .advance_clock(SERIALIZATION_THROTTLE_TIME);
3852        cx.update(|_| {});
3853        editor_1.assert_released();
3854        editor_2.assert_released();
3855        buffer.assert_released();
3856    }
3857
3858    #[gpui::test]
3859    async fn test_navigation(cx: &mut TestAppContext) {
3860        let app_state = init_test(cx);
3861        app_state
3862            .fs
3863            .as_fake()
3864            .insert_tree(
3865                path!("/root"),
3866                json!({
3867                    "a": {
3868                        "file1": "contents 1\n".repeat(20),
3869                        "file2": "contents 2\n".repeat(20),
3870                        "file3": "contents 3\n".repeat(20),
3871                    },
3872                }),
3873            )
3874            .await;
3875
3876        let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
3877        project.update(cx, |project, _cx| project.languages().add(markdown_lang()));
3878        let workspace =
3879            cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
3880        let pane = workspace
3881            .read_with(cx, |workspace, _| workspace.active_pane().clone())
3882            .unwrap();
3883
3884        let entries = cx.update(|cx| workspace.root(cx).unwrap().file_project_paths(cx));
3885        let file1 = entries[0].clone();
3886        let file2 = entries[1].clone();
3887        let file3 = entries[2].clone();
3888
3889        let editor1 = workspace
3890            .update(cx, |w, window, cx| {
3891                w.open_path(file1.clone(), None, true, window, cx)
3892            })
3893            .unwrap()
3894            .await
3895            .unwrap()
3896            .downcast::<Editor>()
3897            .unwrap();
3898        workspace
3899            .update(cx, |_, window, cx| {
3900                editor1.update(cx, |editor, cx| {
3901                    editor.change_selections(Default::default(), window, cx, |s| {
3902                        s.select_display_ranges([DisplayPoint::new(DisplayRow(10), 0)
3903                            ..DisplayPoint::new(DisplayRow(10), 0)])
3904                    });
3905                });
3906            })
3907            .unwrap();
3908
3909        let editor2 = workspace
3910            .update(cx, |w, window, cx| {
3911                w.open_path(file2.clone(), None, true, window, cx)
3912            })
3913            .unwrap()
3914            .await
3915            .unwrap()
3916            .downcast::<Editor>()
3917            .unwrap();
3918        let editor3 = workspace
3919            .update(cx, |w, window, cx| {
3920                w.open_path(file3.clone(), None, true, window, cx)
3921            })
3922            .unwrap()
3923            .await
3924            .unwrap()
3925            .downcast::<Editor>()
3926            .unwrap();
3927
3928        workspace
3929            .update(cx, |_, window, cx| {
3930                editor3.update(cx, |editor, cx| {
3931                    editor.change_selections(Default::default(), window, cx, |s| {
3932                        s.select_display_ranges([DisplayPoint::new(DisplayRow(12), 0)
3933                            ..DisplayPoint::new(DisplayRow(12), 0)])
3934                    });
3935                    editor.newline(&Default::default(), window, cx);
3936                    editor.newline(&Default::default(), window, cx);
3937                    editor.move_down(&Default::default(), window, cx);
3938                    editor.move_down(&Default::default(), window, cx);
3939                    editor.save(
3940                        SaveOptions {
3941                            format: true,
3942                            autosave: false,
3943                        },
3944                        project.clone(),
3945                        window,
3946                        cx,
3947                    )
3948                })
3949            })
3950            .unwrap()
3951            .await
3952            .unwrap();
3953        workspace
3954            .update(cx, |_, window, cx| {
3955                editor3.update(cx, |editor, cx| {
3956                    editor.set_scroll_position(point(0., 12.5), window, cx)
3957                });
3958            })
3959            .unwrap();
3960        assert_eq!(
3961            active_location(&workspace, cx),
3962            (file3.clone(), DisplayPoint::new(DisplayRow(16), 0), 12.5)
3963        );
3964
3965        workspace
3966            .update(cx, |w, window, cx| {
3967                w.go_back(w.active_pane().downgrade(), window, cx)
3968            })
3969            .unwrap()
3970            .await
3971            .unwrap();
3972        assert_eq!(
3973            active_location(&workspace, cx),
3974            (file3.clone(), DisplayPoint::new(DisplayRow(0), 0), 0.)
3975        );
3976
3977        workspace
3978            .update(cx, |w, window, cx| {
3979                w.go_back(w.active_pane().downgrade(), window, cx)
3980            })
3981            .unwrap()
3982            .await
3983            .unwrap();
3984        assert_eq!(
3985            active_location(&workspace, cx),
3986            (file2.clone(), DisplayPoint::new(DisplayRow(0), 0), 0.)
3987        );
3988
3989        workspace
3990            .update(cx, |w, window, cx| {
3991                w.go_back(w.active_pane().downgrade(), window, cx)
3992            })
3993            .unwrap()
3994            .await
3995            .unwrap();
3996        assert_eq!(
3997            active_location(&workspace, cx),
3998            (file1.clone(), DisplayPoint::new(DisplayRow(10), 0), 0.)
3999        );
4000
4001        workspace
4002            .update(cx, |w, window, cx| {
4003                w.go_back(w.active_pane().downgrade(), window, cx)
4004            })
4005            .unwrap()
4006            .await
4007            .unwrap();
4008        assert_eq!(
4009            active_location(&workspace, cx),
4010            (file1.clone(), DisplayPoint::new(DisplayRow(0), 0), 0.)
4011        );
4012
4013        // Go back one more time and ensure we don't navigate past the first item in the history.
4014        workspace
4015            .update(cx, |w, window, cx| {
4016                w.go_back(w.active_pane().downgrade(), window, cx)
4017            })
4018            .unwrap()
4019            .await
4020            .unwrap();
4021        assert_eq!(
4022            active_location(&workspace, cx),
4023            (file1.clone(), DisplayPoint::new(DisplayRow(0), 0), 0.)
4024        );
4025
4026        workspace
4027            .update(cx, |w, window, cx| {
4028                w.go_forward(w.active_pane().downgrade(), window, cx)
4029            })
4030            .unwrap()
4031            .await
4032            .unwrap();
4033        assert_eq!(
4034            active_location(&workspace, cx),
4035            (file1.clone(), DisplayPoint::new(DisplayRow(10), 0), 0.)
4036        );
4037
4038        workspace
4039            .update(cx, |w, window, cx| {
4040                w.go_forward(w.active_pane().downgrade(), window, cx)
4041            })
4042            .unwrap()
4043            .await
4044            .unwrap();
4045        assert_eq!(
4046            active_location(&workspace, cx),
4047            (file2.clone(), DisplayPoint::new(DisplayRow(0), 0), 0.)
4048        );
4049
4050        // Go forward to an item that has been closed, ensuring it gets re-opened at the same
4051        // location.
4052        workspace
4053            .update(cx, |_, window, cx| {
4054                pane.update(cx, |pane, cx| {
4055                    let editor3_id = editor3.entity_id();
4056                    drop(editor3);
4057                    pane.close_item_by_id(editor3_id, SaveIntent::Close, window, cx)
4058                })
4059            })
4060            .unwrap()
4061            .await
4062            .unwrap();
4063        workspace
4064            .update(cx, |w, window, cx| {
4065                w.go_forward(w.active_pane().downgrade(), window, cx)
4066            })
4067            .unwrap()
4068            .await
4069            .unwrap();
4070        assert_eq!(
4071            active_location(&workspace, cx),
4072            (file3.clone(), DisplayPoint::new(DisplayRow(0), 0), 0.)
4073        );
4074
4075        workspace
4076            .update(cx, |w, window, cx| {
4077                w.go_forward(w.active_pane().downgrade(), window, cx)
4078            })
4079            .unwrap()
4080            .await
4081            .unwrap();
4082        assert_eq!(
4083            active_location(&workspace, cx),
4084            (file3.clone(), DisplayPoint::new(DisplayRow(16), 0), 12.5)
4085        );
4086
4087        workspace
4088            .update(cx, |w, window, cx| {
4089                w.go_back(w.active_pane().downgrade(), window, cx)
4090            })
4091            .unwrap()
4092            .await
4093            .unwrap();
4094        assert_eq!(
4095            active_location(&workspace, cx),
4096            (file3.clone(), DisplayPoint::new(DisplayRow(0), 0), 0.)
4097        );
4098
4099        // Go back to an item that has been closed and removed from disk
4100        workspace
4101            .update(cx, |_, window, cx| {
4102                pane.update(cx, |pane, cx| {
4103                    let editor2_id = editor2.entity_id();
4104                    drop(editor2);
4105                    pane.close_item_by_id(editor2_id, SaveIntent::Close, window, cx)
4106                })
4107            })
4108            .unwrap()
4109            .await
4110            .unwrap();
4111        app_state
4112            .fs
4113            .remove_file(Path::new(path!("/root/a/file2")), Default::default())
4114            .await
4115            .unwrap();
4116        cx.background_executor.run_until_parked();
4117
4118        workspace
4119            .update(cx, |w, window, cx| {
4120                w.go_back(w.active_pane().downgrade(), window, cx)
4121            })
4122            .unwrap()
4123            .await
4124            .unwrap();
4125        assert_eq!(
4126            active_location(&workspace, cx),
4127            (file2.clone(), DisplayPoint::new(DisplayRow(0), 0), 0.)
4128        );
4129        workspace
4130            .update(cx, |w, window, cx| {
4131                w.go_forward(w.active_pane().downgrade(), window, cx)
4132            })
4133            .unwrap()
4134            .await
4135            .unwrap();
4136        assert_eq!(
4137            active_location(&workspace, cx),
4138            (file3.clone(), DisplayPoint::new(DisplayRow(0), 0), 0.)
4139        );
4140
4141        // Modify file to collapse multiple nav history entries into the same location.
4142        // Ensure we don't visit the same location twice when navigating.
4143        workspace
4144            .update(cx, |_, window, cx| {
4145                editor1.update(cx, |editor, cx| {
4146                    editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4147                        s.select_display_ranges([DisplayPoint::new(DisplayRow(15), 0)
4148                            ..DisplayPoint::new(DisplayRow(15), 0)])
4149                    })
4150                });
4151            })
4152            .unwrap();
4153        for _ in 0..5 {
4154            workspace
4155                .update(cx, |_, window, cx| {
4156                    editor1.update(cx, |editor, cx| {
4157                        editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4158                            s.select_display_ranges([DisplayPoint::new(DisplayRow(3), 0)
4159                                ..DisplayPoint::new(DisplayRow(3), 0)])
4160                        });
4161                    });
4162                })
4163                .unwrap();
4164
4165            workspace
4166                .update(cx, |_, window, cx| {
4167                    editor1.update(cx, |editor, cx| {
4168                        editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4169                            s.select_display_ranges([DisplayPoint::new(DisplayRow(13), 0)
4170                                ..DisplayPoint::new(DisplayRow(13), 0)])
4171                        })
4172                    });
4173                })
4174                .unwrap();
4175        }
4176        workspace
4177            .update(cx, |_, window, cx| {
4178                editor1.update(cx, |editor, cx| {
4179                    editor.transact(window, cx, |editor, window, cx| {
4180                        editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4181                            s.select_display_ranges([DisplayPoint::new(DisplayRow(2), 0)
4182                                ..DisplayPoint::new(DisplayRow(14), 0)])
4183                        });
4184                        editor.insert("", window, cx);
4185                    })
4186                });
4187            })
4188            .unwrap();
4189
4190        workspace
4191            .update(cx, |_, window, cx| {
4192                editor1.update(cx, |editor, cx| {
4193                    editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4194                        s.select_display_ranges([DisplayPoint::new(DisplayRow(1), 0)
4195                            ..DisplayPoint::new(DisplayRow(1), 0)])
4196                    })
4197                });
4198            })
4199            .unwrap();
4200        workspace
4201            .update(cx, |w, window, cx| {
4202                w.go_back(w.active_pane().downgrade(), window, cx)
4203            })
4204            .unwrap()
4205            .await
4206            .unwrap();
4207        assert_eq!(
4208            active_location(&workspace, cx),
4209            (file1.clone(), DisplayPoint::new(DisplayRow(2), 0), 0.)
4210        );
4211        workspace
4212            .update(cx, |w, window, cx| {
4213                w.go_back(w.active_pane().downgrade(), window, cx)
4214            })
4215            .unwrap()
4216            .await
4217            .unwrap();
4218        assert_eq!(
4219            active_location(&workspace, cx),
4220            (file1.clone(), DisplayPoint::new(DisplayRow(3), 0), 0.)
4221        );
4222
4223        fn active_location(
4224            workspace: &WindowHandle<Workspace>,
4225            cx: &mut TestAppContext,
4226        ) -> (ProjectPath, DisplayPoint, f64) {
4227            workspace
4228                .update(cx, |workspace, _, cx| {
4229                    let item = workspace.active_item(cx).unwrap();
4230                    let editor = item.downcast::<Editor>().unwrap();
4231                    let (selections, scroll_position) = editor.update(cx, |editor, cx| {
4232                        (
4233                            editor
4234                                .selections
4235                                .display_ranges(&editor.display_snapshot(cx)),
4236                            editor.scroll_position(cx),
4237                        )
4238                    });
4239                    (
4240                        item.project_path(cx).unwrap(),
4241                        selections[0].start,
4242                        scroll_position.y,
4243                    )
4244                })
4245                .unwrap()
4246        }
4247    }
4248
4249    #[gpui::test]
4250    async fn test_reopening_closed_items(cx: &mut TestAppContext) {
4251        let app_state = init_test(cx);
4252        app_state
4253            .fs
4254            .as_fake()
4255            .insert_tree(
4256                path!("/root"),
4257                json!({
4258                    "a": {
4259                        "file1": "",
4260                        "file2": "",
4261                        "file3": "",
4262                        "file4": "",
4263                    },
4264                }),
4265            )
4266            .await;
4267
4268        let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
4269        project.update(cx, |project, _cx| project.languages().add(markdown_lang()));
4270        let workspace = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
4271        let pane = workspace
4272            .read_with(cx, |workspace, _| workspace.active_pane().clone())
4273            .unwrap();
4274
4275        let entries = cx.update(|cx| workspace.root(cx).unwrap().file_project_paths(cx));
4276        let file1 = entries[0].clone();
4277        let file2 = entries[1].clone();
4278        let file3 = entries[2].clone();
4279        let file4 = entries[3].clone();
4280
4281        let file1_item_id = workspace
4282            .update(cx, |w, window, cx| {
4283                w.open_path(file1.clone(), None, true, window, cx)
4284            })
4285            .unwrap()
4286            .await
4287            .unwrap()
4288            .item_id();
4289        let file2_item_id = workspace
4290            .update(cx, |w, window, cx| {
4291                w.open_path(file2.clone(), None, true, window, cx)
4292            })
4293            .unwrap()
4294            .await
4295            .unwrap()
4296            .item_id();
4297        let file3_item_id = workspace
4298            .update(cx, |w, window, cx| {
4299                w.open_path(file3.clone(), None, true, window, cx)
4300            })
4301            .unwrap()
4302            .await
4303            .unwrap()
4304            .item_id();
4305        let file4_item_id = workspace
4306            .update(cx, |w, window, cx| {
4307                w.open_path(file4.clone(), None, true, window, cx)
4308            })
4309            .unwrap()
4310            .await
4311            .unwrap()
4312            .item_id();
4313        assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
4314
4315        // Close all the pane items in some arbitrary order.
4316        workspace
4317            .update(cx, |_, window, cx| {
4318                pane.update(cx, |pane, cx| {
4319                    pane.close_item_by_id(file1_item_id, SaveIntent::Close, window, cx)
4320                })
4321            })
4322            .unwrap()
4323            .await
4324            .unwrap();
4325        assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
4326
4327        workspace
4328            .update(cx, |_, window, cx| {
4329                pane.update(cx, |pane, cx| {
4330                    pane.close_item_by_id(file4_item_id, SaveIntent::Close, window, cx)
4331                })
4332            })
4333            .unwrap()
4334            .await
4335            .unwrap();
4336        assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
4337
4338        workspace
4339            .update(cx, |_, window, cx| {
4340                pane.update(cx, |pane, cx| {
4341                    pane.close_item_by_id(file2_item_id, SaveIntent::Close, window, cx)
4342                })
4343            })
4344            .unwrap()
4345            .await
4346            .unwrap();
4347        assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
4348        workspace
4349            .update(cx, |_, window, cx| {
4350                pane.update(cx, |pane, cx| {
4351                    pane.close_item_by_id(file3_item_id, SaveIntent::Close, window, cx)
4352                })
4353            })
4354            .unwrap()
4355            .await
4356            .unwrap();
4357
4358        assert_eq!(active_path(&workspace, cx), None);
4359
4360        // Reopen all the closed items, ensuring they are reopened in the same order
4361        // in which they were closed.
4362        workspace
4363            .update(cx, Workspace::reopen_closed_item)
4364            .unwrap()
4365            .await
4366            .unwrap();
4367        assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
4368
4369        workspace
4370            .update(cx, Workspace::reopen_closed_item)
4371            .unwrap()
4372            .await
4373            .unwrap();
4374        assert_eq!(active_path(&workspace, cx), Some(file2.clone()));
4375
4376        workspace
4377            .update(cx, Workspace::reopen_closed_item)
4378            .unwrap()
4379            .await
4380            .unwrap();
4381        assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
4382
4383        workspace
4384            .update(cx, Workspace::reopen_closed_item)
4385            .unwrap()
4386            .await
4387            .unwrap();
4388        assert_eq!(active_path(&workspace, cx), Some(file1.clone()));
4389
4390        // Reopening past the last closed item is a no-op.
4391        workspace
4392            .update(cx, Workspace::reopen_closed_item)
4393            .unwrap()
4394            .await
4395            .unwrap();
4396        assert_eq!(active_path(&workspace, cx), Some(file1.clone()));
4397
4398        // Reopening closed items doesn't interfere with navigation history.
4399        workspace
4400            .update(cx, |workspace, window, cx| {
4401                workspace.go_back(workspace.active_pane().downgrade(), window, cx)
4402            })
4403            .unwrap()
4404            .await
4405            .unwrap();
4406        assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
4407
4408        workspace
4409            .update(cx, |workspace, window, cx| {
4410                workspace.go_back(workspace.active_pane().downgrade(), window, cx)
4411            })
4412            .unwrap()
4413            .await
4414            .unwrap();
4415        assert_eq!(active_path(&workspace, cx), Some(file2.clone()));
4416
4417        workspace
4418            .update(cx, |workspace, window, cx| {
4419                workspace.go_back(workspace.active_pane().downgrade(), window, cx)
4420            })
4421            .unwrap()
4422            .await
4423            .unwrap();
4424        assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
4425
4426        workspace
4427            .update(cx, |workspace, window, cx| {
4428                workspace.go_back(workspace.active_pane().downgrade(), window, cx)
4429            })
4430            .unwrap()
4431            .await
4432            .unwrap();
4433        assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
4434
4435        workspace
4436            .update(cx, |workspace, window, cx| {
4437                workspace.go_back(workspace.active_pane().downgrade(), window, cx)
4438            })
4439            .unwrap()
4440            .await
4441            .unwrap();
4442        assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
4443
4444        workspace
4445            .update(cx, |workspace, window, cx| {
4446                workspace.go_back(workspace.active_pane().downgrade(), window, cx)
4447            })
4448            .unwrap()
4449            .await
4450            .unwrap();
4451        assert_eq!(active_path(&workspace, cx), Some(file2.clone()));
4452
4453        workspace
4454            .update(cx, |workspace, window, cx| {
4455                workspace.go_back(workspace.active_pane().downgrade(), window, cx)
4456            })
4457            .unwrap()
4458            .await
4459            .unwrap();
4460        assert_eq!(active_path(&workspace, cx), Some(file1.clone()));
4461
4462        workspace
4463            .update(cx, |workspace, window, cx| {
4464                workspace.go_back(workspace.active_pane().downgrade(), window, cx)
4465            })
4466            .unwrap()
4467            .await
4468            .unwrap();
4469        assert_eq!(active_path(&workspace, cx), Some(file1.clone()));
4470
4471        fn active_path(
4472            workspace: &WindowHandle<Workspace>,
4473            cx: &TestAppContext,
4474        ) -> Option<ProjectPath> {
4475            workspace
4476                .read_with(cx, |workspace, cx| {
4477                    let item = workspace.active_item(cx)?;
4478                    item.project_path(cx)
4479                })
4480                .unwrap()
4481        }
4482    }
4483
4484    fn init_keymap_test(cx: &mut TestAppContext) -> Arc<AppState> {
4485        cx.update(|cx| {
4486            let app_state = AppState::test(cx);
4487
4488            theme::init(theme::LoadThemes::JustBase, cx);
4489            client::init(&app_state.client, cx);
4490            workspace::init(app_state.clone(), cx);
4491            onboarding::init(cx);
4492            app_state
4493        })
4494    }
4495
4496    actions!(test_only, [ActionA, ActionB]);
4497
4498    #[gpui::test]
4499    async fn test_base_keymap(cx: &mut gpui::TestAppContext) {
4500        let executor = cx.executor();
4501        let app_state = init_keymap_test(cx);
4502        let project = Project::test(app_state.fs.clone(), [], cx).await;
4503        let workspace =
4504            cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
4505
4506        // From the Atom keymap
4507        use workspace::ActivatePreviousPane;
4508        // From the JetBrains keymap
4509        use workspace::ActivatePreviousItem;
4510
4511        app_state
4512            .fs
4513            .save(
4514                "/settings.json".as_ref(),
4515                &r#"{"base_keymap": "Atom"}"#.into(),
4516                Default::default(),
4517            )
4518            .await
4519            .unwrap();
4520
4521        app_state
4522            .fs
4523            .save(
4524                "/keymap.json".as_ref(),
4525                &r#"[{"bindings": {"backspace": "test_only::ActionA"}}]"#.into(),
4526                Default::default(),
4527            )
4528            .await
4529            .unwrap();
4530        executor.run_until_parked();
4531        cx.update(|cx| {
4532            let settings_rx = watch_config_file(
4533                &executor,
4534                app_state.fs.clone(),
4535                PathBuf::from("/settings.json"),
4536            );
4537            let keymap_rx = watch_config_file(
4538                &executor,
4539                app_state.fs.clone(),
4540                PathBuf::from("/keymap.json"),
4541            );
4542            let global_settings_rx = watch_config_file(
4543                &executor,
4544                app_state.fs.clone(),
4545                PathBuf::from("/global_settings.json"),
4546            );
4547            handle_settings_file_changes(settings_rx, global_settings_rx, cx);
4548            handle_keymap_file_changes(keymap_rx, cx);
4549        });
4550        workspace
4551            .update(cx, |workspace, _, cx| {
4552                workspace.register_action(|_, _: &ActionA, _window, _cx| {});
4553                workspace.register_action(|_, _: &ActionB, _window, _cx| {});
4554                workspace.register_action(|_, _: &ActivatePreviousPane, _window, _cx| {});
4555                workspace.register_action(|_, _: &ActivatePreviousItem, _window, _cx| {});
4556                cx.notify();
4557            })
4558            .unwrap();
4559        executor.run_until_parked();
4560        // Test loading the keymap base at all
4561        assert_key_bindings_for(
4562            workspace.into(),
4563            cx,
4564            vec![("backspace", &ActionA), ("k", &ActivatePreviousPane)],
4565            line!(),
4566        );
4567
4568        // Test modifying the users keymap, while retaining the base keymap
4569        app_state
4570            .fs
4571            .save(
4572                "/keymap.json".as_ref(),
4573                &r#"[{"bindings": {"backspace": "test_only::ActionB"}}]"#.into(),
4574                Default::default(),
4575            )
4576            .await
4577            .unwrap();
4578
4579        executor.run_until_parked();
4580
4581        assert_key_bindings_for(
4582            workspace.into(),
4583            cx,
4584            vec![("backspace", &ActionB), ("k", &ActivatePreviousPane)],
4585            line!(),
4586        );
4587
4588        // Test modifying the base, while retaining the users keymap
4589        app_state
4590            .fs
4591            .save(
4592                "/settings.json".as_ref(),
4593                &r#"{"base_keymap": "JetBrains"}"#.into(),
4594                Default::default(),
4595            )
4596            .await
4597            .unwrap();
4598
4599        executor.run_until_parked();
4600
4601        assert_key_bindings_for(
4602            workspace.into(),
4603            cx,
4604            vec![("backspace", &ActionB), ("{", &ActivatePreviousItem)],
4605            line!(),
4606        );
4607    }
4608
4609    #[gpui::test]
4610    async fn test_disabled_keymap_binding(cx: &mut gpui::TestAppContext) {
4611        let executor = cx.executor();
4612        let app_state = init_keymap_test(cx);
4613        let project = Project::test(app_state.fs.clone(), [], cx).await;
4614        let workspace =
4615            cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
4616
4617        // From the Atom keymap
4618        use workspace::ActivatePreviousPane;
4619        // From the JetBrains keymap
4620        use diagnostics::Deploy;
4621
4622        workspace
4623            .update(cx, |workspace, _, _| {
4624                workspace.register_action(|_, _: &ActionA, _window, _cx| {});
4625                workspace.register_action(|_, _: &ActionB, _window, _cx| {});
4626                workspace.register_action(|_, _: &Deploy, _window, _cx| {});
4627            })
4628            .unwrap();
4629        app_state
4630            .fs
4631            .save(
4632                "/settings.json".as_ref(),
4633                &r#"{"base_keymap": "Atom"}"#.into(),
4634                Default::default(),
4635            )
4636            .await
4637            .unwrap();
4638        app_state
4639            .fs
4640            .save(
4641                "/keymap.json".as_ref(),
4642                &r#"[{"bindings": {"backspace": "test_only::ActionA"}}]"#.into(),
4643                Default::default(),
4644            )
4645            .await
4646            .unwrap();
4647
4648        cx.update(|cx| {
4649            let settings_rx = watch_config_file(
4650                &executor,
4651                app_state.fs.clone(),
4652                PathBuf::from("/settings.json"),
4653            );
4654            let keymap_rx = watch_config_file(
4655                &executor,
4656                app_state.fs.clone(),
4657                PathBuf::from("/keymap.json"),
4658            );
4659
4660            let global_settings_rx = watch_config_file(
4661                &executor,
4662                app_state.fs.clone(),
4663                PathBuf::from("/global_settings.json"),
4664            );
4665            handle_settings_file_changes(settings_rx, global_settings_rx, cx);
4666            handle_keymap_file_changes(keymap_rx, cx);
4667        });
4668
4669        cx.background_executor.run_until_parked();
4670
4671        cx.background_executor.run_until_parked();
4672        // Test loading the keymap base at all
4673        assert_key_bindings_for(
4674            workspace.into(),
4675            cx,
4676            vec![("backspace", &ActionA), ("k", &ActivatePreviousPane)],
4677            line!(),
4678        );
4679
4680        // Test disabling the key binding for the base keymap
4681        app_state
4682            .fs
4683            .save(
4684                "/keymap.json".as_ref(),
4685                &r#"[{"bindings": {"backspace": null}}]"#.into(),
4686                Default::default(),
4687            )
4688            .await
4689            .unwrap();
4690
4691        cx.background_executor.run_until_parked();
4692
4693        assert_key_bindings_for(
4694            workspace.into(),
4695            cx,
4696            vec![("k", &ActivatePreviousPane)],
4697            line!(),
4698        );
4699
4700        // Test modifying the base, while retaining the users keymap
4701        app_state
4702            .fs
4703            .save(
4704                "/settings.json".as_ref(),
4705                &r#"{"base_keymap": "JetBrains"}"#.into(),
4706                Default::default(),
4707            )
4708            .await
4709            .unwrap();
4710
4711        cx.background_executor.run_until_parked();
4712
4713        assert_key_bindings_for(workspace.into(), cx, vec![("6", &Deploy)], line!());
4714    }
4715
4716    #[gpui::test]
4717    async fn test_generate_keymap_json_schema_for_registered_actions(
4718        cx: &mut gpui::TestAppContext,
4719    ) {
4720        init_keymap_test(cx);
4721        cx.update(|cx| {
4722            // Make sure it doesn't panic.
4723            KeymapFile::generate_json_schema_for_registered_actions(cx);
4724        });
4725    }
4726
4727    /// Checks that action namespaces are the expected set. The purpose of this is to prevent typos
4728    /// and let you know when introducing a new namespace.
4729    #[gpui::test]
4730    async fn test_action_namespaces(cx: &mut gpui::TestAppContext) {
4731        use itertools::Itertools;
4732
4733        init_keymap_test(cx);
4734        cx.update(|cx| {
4735            let all_actions = cx.all_action_names();
4736
4737            let mut actions_without_namespace = Vec::new();
4738            let all_namespaces = all_actions
4739                .iter()
4740                .filter_map(|action_name| {
4741                    let namespace = action_name
4742                        .split("::")
4743                        .collect::<Vec<_>>()
4744                        .into_iter()
4745                        .rev()
4746                        .skip(1)
4747                        .rev()
4748                        .join("::");
4749                    if namespace.is_empty() {
4750                        actions_without_namespace.push(*action_name);
4751                    }
4752                    if &namespace == "test_only" || &namespace == "stories" {
4753                        None
4754                    } else {
4755                        Some(namespace)
4756                    }
4757                })
4758                .sorted()
4759                .dedup()
4760                .collect::<Vec<_>>();
4761            assert_eq!(actions_without_namespace, Vec::<&str>::new());
4762
4763            let expected_namespaces = vec![
4764                "action",
4765                "activity_indicator",
4766                "agent",
4767                "agents",
4768                #[cfg(not(target_os = "macos"))]
4769                "app_menu",
4770                "assistant",
4771                "assistant2",
4772                "auto_update",
4773                "branch_picker",
4774                "bedrock",
4775                "branches",
4776                "buffer_search",
4777                "channel_modal",
4778                "cli",
4779                "client",
4780                "collab",
4781                "collab_panel",
4782                "command_palette",
4783                "console",
4784                "context_server",
4785                "copilot",
4786                "debug_panel",
4787                "debugger",
4788                "dev",
4789                "diagnostics",
4790                "edit_prediction",
4791                "editor",
4792                "feedback",
4793                "file_finder",
4794                "git",
4795                "git_onboarding",
4796                "git_panel",
4797                "go_to_line",
4798                "icon_theme_selector",
4799                "inline_assistant",
4800                "journal",
4801                "keymap_editor",
4802                "keystroke_input",
4803                "language_selector",
4804                "welcome",
4805                "line_ending_selector",
4806                "lsp_tool",
4807                "markdown",
4808                "menu",
4809                "notebook",
4810                "notification_panel",
4811                "onboarding",
4812                "outline",
4813                "outline_panel",
4814                "pane",
4815                "panel",
4816                "picker",
4817                "project_panel",
4818                "project_search",
4819                "project_symbols",
4820                "projects",
4821                "repl",
4822                "rules_library",
4823                "search",
4824                "settings_editor",
4825                "settings_profile_selector",
4826                "snippets",
4827                "stash_picker",
4828                "supermaven",
4829                "svg",
4830                "syntax_tree_view",
4831                "tab_switcher",
4832                "task",
4833                "terminal",
4834                "terminal_panel",
4835                "theme_selector",
4836                "toast",
4837                "toolchain",
4838                "variable_list",
4839                "vim",
4840                "window",
4841                "workspace",
4842                "zed",
4843                "zed_actions",
4844                "zed_predict_onboarding",
4845                "zeta",
4846            ];
4847            assert_eq!(
4848                all_namespaces,
4849                expected_namespaces
4850                    .into_iter()
4851                    .map(|namespace| namespace.to_string())
4852                    .sorted()
4853                    .collect::<Vec<_>>()
4854            );
4855        });
4856    }
4857
4858    #[gpui::test]
4859    fn test_bundled_settings_and_themes(cx: &mut App) {
4860        cx.text_system()
4861            .add_fonts(vec![
4862                Assets
4863                    .load("fonts/lilex/Lilex-Regular.ttf")
4864                    .unwrap()
4865                    .unwrap(),
4866                Assets
4867                    .load("fonts/ibm-plex-sans/IBMPlexSans-Regular.ttf")
4868                    .unwrap()
4869                    .unwrap(),
4870            ])
4871            .unwrap();
4872        let themes = ThemeRegistry::default();
4873        settings::init(cx);
4874        theme::init(theme::LoadThemes::JustBase, cx);
4875
4876        let mut has_default_theme = false;
4877        for theme_name in themes.list().into_iter().map(|meta| meta.name) {
4878            let theme = themes.get(&theme_name).unwrap();
4879            assert_eq!(theme.name, theme_name);
4880            if theme.name.as_ref() == "One Dark" {
4881                has_default_theme = true;
4882            }
4883        }
4884        assert!(has_default_theme);
4885    }
4886
4887    #[gpui::test]
4888    async fn test_bundled_files_editor(cx: &mut TestAppContext) {
4889        let app_state = init_test(cx);
4890        cx.update(init);
4891
4892        let project = Project::test(app_state.fs.clone(), [], cx).await;
4893        let _window = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
4894
4895        cx.update(|cx| {
4896            cx.dispatch_action(&OpenDefaultSettings);
4897        });
4898        cx.run_until_parked();
4899
4900        assert_eq!(cx.read(|cx| cx.windows().len()), 1);
4901
4902        let workspace = cx.windows()[0].downcast::<Workspace>().unwrap();
4903        let active_editor = workspace
4904            .update(cx, |workspace, _, cx| {
4905                workspace.active_item_as::<Editor>(cx)
4906            })
4907            .unwrap();
4908        assert!(
4909            active_editor.is_some(),
4910            "Settings action should have opened an editor with the default file contents"
4911        );
4912
4913        let active_editor = active_editor.unwrap();
4914        assert!(
4915            active_editor.read_with(cx, |editor, cx| editor.read_only(cx)),
4916            "Default settings should be readonly"
4917        );
4918        assert!(
4919            active_editor.read_with(cx, |editor, cx| editor.buffer().read(cx).read_only()),
4920            "The underlying buffer should also be readonly for the shipped default settings"
4921        );
4922    }
4923
4924    #[gpui::test]
4925    async fn test_bundled_languages(cx: &mut TestAppContext) {
4926        let fs = fs::FakeFs::new(cx.background_executor.clone());
4927        env_logger::builder().is_test(true).try_init().ok();
4928        let settings = cx.update(SettingsStore::test);
4929        cx.set_global(settings);
4930        let languages = LanguageRegistry::test(cx.executor());
4931        let languages = Arc::new(languages);
4932        let node_runtime = node_runtime::NodeRuntime::unavailable();
4933        cx.update(|cx| {
4934            languages::init(languages.clone(), fs, node_runtime, cx);
4935        });
4936        for name in languages.language_names() {
4937            languages
4938                .language_for_name(name.as_ref())
4939                .await
4940                .with_context(|| format!("language name {name}"))
4941                .unwrap();
4942        }
4943        cx.run_until_parked();
4944    }
4945
4946    pub(crate) fn init_test(cx: &mut TestAppContext) -> Arc<AppState> {
4947        init_test_with_state(cx, cx.update(AppState::test))
4948    }
4949
4950    fn init_test_with_state(
4951        cx: &mut TestAppContext,
4952        mut app_state: Arc<AppState>,
4953    ) -> Arc<AppState> {
4954        cx.update(move |cx| {
4955            env_logger::builder().is_test(true).try_init().ok();
4956
4957            let state = Arc::get_mut(&mut app_state).unwrap();
4958            state.build_window_options = build_window_options;
4959            app_state.languages.add(markdown_lang());
4960
4961            gpui_tokio::init(cx);
4962            theme::init(theme::LoadThemes::JustBase, cx);
4963            audio::init(cx);
4964            channel::init(&app_state.client, app_state.user_store.clone(), cx);
4965            call::init(app_state.client.clone(), app_state.user_store.clone(), cx);
4966            notifications::init(app_state.client.clone(), app_state.user_store.clone(), cx);
4967            workspace::init(app_state.clone(), cx);
4968            release_channel::init(Version::new(0, 0, 0), cx);
4969            command_palette::init(cx);
4970            editor::init(cx);
4971            collab_ui::init(&app_state, cx);
4972            git_ui::init(cx);
4973            project_panel::init(cx);
4974            outline_panel::init(cx);
4975            terminal_view::init(cx);
4976            copilot::copilot_chat::init(
4977                app_state.fs.clone(),
4978                app_state.client.http_client(),
4979                copilot::copilot_chat::CopilotChatConfiguration::default(),
4980                cx,
4981            );
4982            image_viewer::init(cx);
4983            language_model::init(app_state.client.clone(), cx);
4984            language_models::init(app_state.user_store.clone(), app_state.client.clone(), cx);
4985            web_search::init(cx);
4986            web_search_providers::init(app_state.client.clone(), cx);
4987            let prompt_builder = PromptBuilder::load(app_state.fs.clone(), false, cx);
4988            agent_ui::init(
4989                app_state.fs.clone(),
4990                app_state.client.clone(),
4991                prompt_builder.clone(),
4992                app_state.languages.clone(),
4993                false,
4994                cx,
4995            );
4996            agent_ui_v2::agents_panel::init(cx);
4997            repl::init(app_state.fs.clone(), cx);
4998            repl::notebook::init(cx);
4999            tasks_ui::init(cx);
5000            project::debugger::breakpoint_store::BreakpointStore::init(
5001                &app_state.client.clone().into(),
5002            );
5003            project::debugger::dap_store::DapStore::init(&app_state.client.clone().into(), cx);
5004            debugger_ui::init(cx);
5005            initialize_workspace(app_state.clone(), prompt_builder, cx);
5006            search::init(cx);
5007            app_state
5008        })
5009    }
5010
5011    #[track_caller]
5012    fn assert_key_bindings_for(
5013        window: AnyWindowHandle,
5014        cx: &TestAppContext,
5015        actions: Vec<(&'static str, &dyn Action)>,
5016        line: u32,
5017    ) {
5018        let available_actions = cx
5019            .update(|cx| window.update(cx, |_, window, cx| window.available_actions(cx)))
5020            .unwrap();
5021        for (key, action) in actions {
5022            let bindings = cx
5023                .update(|cx| window.update(cx, |_, window, _| window.bindings_for_action(action)))
5024                .unwrap();
5025            // assert that...
5026            assert!(
5027                available_actions.iter().any(|bound_action| {
5028                    // actions match...
5029                    bound_action.partial_eq(action)
5030                }),
5031                "On {} Failed to find {}",
5032                line,
5033                action.name(),
5034            );
5035            assert!(
5036                // and key strokes contain the given key
5037                bindings
5038                    .into_iter()
5039                    .any(|binding| binding.keystrokes().iter().any(|k| k.key() == key)),
5040                "On {} Failed to find {} with key binding {}",
5041                line,
5042                action.name(),
5043                key
5044            );
5045        }
5046    }
5047
5048    #[gpui::test]
5049    async fn test_opening_project_settings_when_excluded(cx: &mut gpui::TestAppContext) {
5050        // Use the proper initialization for runtime state
5051        let app_state = init_keymap_test(cx);
5052
5053        eprintln!("Running test_opening_project_settings_when_excluded");
5054
5055        // 1. Set up a project with some project settings
5056        let settings_init =
5057            r#"{ "UNIQUEVALUE": true, "git": { "inline_blame": { "enabled": false } } }"#;
5058        app_state
5059            .fs
5060            .as_fake()
5061            .insert_tree(
5062                Path::new("/root"),
5063                json!({
5064                    ".zed": {
5065                        "settings.json": settings_init
5066                    }
5067                }),
5068            )
5069            .await;
5070
5071        eprintln!("Created project with .zed/settings.json containing UNIQUEVALUE");
5072
5073        // 2. Create a project with the file system and load it
5074        let project = Project::test(app_state.fs.clone(), [Path::new("/root")], cx).await;
5075
5076        // Save original settings content for comparison
5077        let original_settings = app_state
5078            .fs
5079            .load(Path::new("/root/.zed/settings.json"))
5080            .await
5081            .unwrap();
5082
5083        let original_settings_str = original_settings.clone();
5084
5085        // Verify settings exist on disk and have expected content
5086        eprintln!("Original settings content: {}", original_settings_str);
5087        assert!(
5088            original_settings_str.contains("UNIQUEVALUE"),
5089            "Test setup failed - settings file doesn't contain our marker"
5090        );
5091
5092        // 3. Add .zed to file scan exclusions in user settings
5093        cx.update_global::<SettingsStore, _>(|store, cx| {
5094            store.update_user_settings(cx, |worktree_settings| {
5095                worktree_settings.project.worktree.file_scan_exclusions =
5096                    Some(vec![".zed".to_string()]);
5097            });
5098        });
5099
5100        eprintln!("Added .zed to file_scan_exclusions in settings");
5101
5102        // 4. Run tasks to apply settings
5103        cx.background_executor.run_until_parked();
5104
5105        // 5. Critical: Verify .zed is actually excluded from worktree
5106        let worktree = cx.update(|cx| project.read(cx).worktrees(cx).next().unwrap());
5107
5108        let has_zed_entry =
5109            cx.update(|cx| worktree.read(cx).entry_for_path(rel_path(".zed")).is_some());
5110
5111        eprintln!(
5112            "Is .zed directory visible in worktree after exclusion: {}",
5113            has_zed_entry
5114        );
5115
5116        // This assertion verifies the test is set up correctly to show the bug
5117        // If .zed is not excluded, the test will fail here
5118        assert!(
5119            !has_zed_entry,
5120            "Test precondition failed: .zed directory should be excluded but was found in worktree"
5121        );
5122
5123        // 6. Create workspace and trigger the actual function that causes the bug
5124        let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
5125        window
5126            .update(cx, |workspace, window, cx| {
5127                // Call the exact function that contains the bug
5128                eprintln!("About to call open_project_settings_file");
5129                open_project_settings_file(workspace, &OpenProjectSettingsFile, window, cx);
5130            })
5131            .unwrap();
5132
5133        // 7. Run background tasks until completion
5134        cx.background_executor.run_until_parked();
5135
5136        // 8. Verify file contents after calling function
5137        let new_content = app_state
5138            .fs
5139            .load(Path::new("/root/.zed/settings.json"))
5140            .await
5141            .unwrap();
5142
5143        let new_content_str = new_content;
5144        eprintln!("New settings content: {}", new_content_str);
5145
5146        // The bug causes the settings to be overwritten with empty settings
5147        // So if the unique value is no longer present, the bug has been reproduced
5148        let bug_exists = !new_content_str.contains("UNIQUEVALUE");
5149        eprintln!("Bug reproduced: {}", bug_exists);
5150
5151        // This assertion should fail if the bug exists - showing the bug is real
5152        assert!(
5153            new_content_str.contains("UNIQUEVALUE"),
5154            "BUG FOUND: Project settings were overwritten when opening via command - original custom content was lost"
5155        );
5156    }
5157
5158    #[gpui::test]
5159    async fn test_prefer_focused_window(cx: &mut gpui::TestAppContext) {
5160        let app_state = init_test(cx);
5161        let paths = [PathBuf::from(path!("/dir/document.txt"))];
5162
5163        app_state
5164            .fs
5165            .as_fake()
5166            .insert_tree(
5167                path!("/dir"),
5168                json!({
5169                    "document.txt": "Some of the documentation's content."
5170                }),
5171            )
5172            .await;
5173
5174        let project_a = Project::test(app_state.fs.clone(), [path!("/dir").as_ref()], cx).await;
5175        let window_a =
5176            cx.add_window(|window, cx| Workspace::test_new(project_a.clone(), window, cx));
5177
5178        let project_b = Project::test(app_state.fs.clone(), [path!("/dir").as_ref()], cx).await;
5179        let window_b =
5180            cx.add_window(|window, cx| Workspace::test_new(project_b.clone(), window, cx));
5181
5182        let project_c = Project::test(app_state.fs.clone(), [path!("/dir").as_ref()], cx).await;
5183        let window_c =
5184            cx.add_window(|window, cx| Workspace::test_new(project_c.clone(), window, cx));
5185
5186        for window in [window_a, window_b, window_c] {
5187            let _ = cx.update_window(*window, |_, window, _| {
5188                window.activate_window();
5189            });
5190
5191            cx.update(|cx| {
5192                let open_options = OpenOptions {
5193                    prefer_focused_window: true,
5194                    ..Default::default()
5195                };
5196
5197                workspace::open_paths(&paths, app_state.clone(), open_options, cx)
5198            })
5199            .await
5200            .unwrap();
5201
5202            cx.update_window(*window, |_, window, _| assert!(window.is_window_active()))
5203                .unwrap();
5204
5205            let _ = window.read_with(cx, |workspace, cx| {
5206                let pane = workspace.active_pane().read(cx);
5207                let project_path = pane.active_item().unwrap().project_path(cx).unwrap();
5208
5209                assert_eq!(
5210                    project_path.path.as_ref().as_std_path().to_str().unwrap(),
5211                    path!("document.txt")
5212                )
5213            });
5214        }
5215    }
5216}