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