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