zed.rs

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