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