zed.rs

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