zed.rs

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