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