zed.rs

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