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
1673                    .read_with(cx, |tree, _| tree.abs_path().join(settings_relative_path))?;
1674
1675                let fs = project.read_with(cx, |project, _| project.fs().clone())?;
1676
1677                fs.metadata(&full_path)
1678                    .await
1679                    .ok()
1680                    .flatten()
1681                    .is_some_and(|metadata| !metadata.is_dir && !metadata.is_fifo)
1682            };
1683
1684            if !file_exists {
1685                if let Some(dir_path) = settings_relative_path.parent()
1686                    && worktree.read_with(cx, |tree, _| tree.entry_for_path(dir_path).is_none())?
1687                {
1688                    project
1689                        .update(cx, |project, cx| {
1690                            project.create_entry((tree_id, dir_path), true, cx)
1691                        })?
1692                        .await
1693                        .context("worktree was removed")?;
1694                }
1695
1696                if worktree.read_with(cx, |tree, _| {
1697                    tree.entry_for_path(settings_relative_path).is_none()
1698                })? {
1699                    project
1700                        .update(cx, |project, cx| {
1701                            project.create_entry((tree_id, settings_relative_path), false, cx)
1702                        })?
1703                        .await
1704                        .context("worktree was removed")?;
1705                }
1706            }
1707
1708            let editor = workspace
1709                .update_in(cx, |workspace, window, cx| {
1710                    workspace.open_path((tree_id, settings_relative_path), None, true, window, cx)
1711                })?
1712                .await?
1713                .downcast::<Editor>()
1714                .context("unexpected item type: expected editor item")?;
1715
1716            editor
1717                .downgrade()
1718                .update(cx, |editor, cx| {
1719                    if let Some(buffer) = editor.buffer().read(cx).as_singleton()
1720                        && buffer.read(cx).is_empty()
1721                    {
1722                        buffer.update(cx, |buffer, cx| {
1723                            buffer.edit([(0..0, initial_contents)], None, cx)
1724                        });
1725                    }
1726                })
1727                .ok();
1728
1729            anyhow::Ok(())
1730        })
1731        .detach();
1732    } else {
1733        struct NoOpenFolders;
1734
1735        workspace.show_notification(NotificationId::unique::<NoOpenFolders>(), cx, |cx| {
1736            cx.new(|cx| MessageNotification::new("This project has no folders open.", cx))
1737        })
1738    }
1739}
1740
1741fn open_telemetry_log_file(
1742    workspace: &mut Workspace,
1743    window: &mut Window,
1744    cx: &mut Context<Workspace>,
1745) {
1746    workspace.with_local_workspace(window, cx, move |workspace, window, cx| {
1747        let app_state = workspace.app_state().clone();
1748        cx.spawn_in(window, async move |workspace, cx| {
1749            async fn fetch_log_string(app_state: &Arc<AppState>) -> Option<String> {
1750                let path = client::telemetry::Telemetry::log_file_path();
1751                app_state.fs.load(&path).await.log_err()
1752            }
1753
1754            let log = fetch_log_string(&app_state).await.unwrap_or_else(|| "// No data has been collected yet".to_string());
1755
1756            const MAX_TELEMETRY_LOG_LEN: usize = 5 * 1024 * 1024;
1757            let mut start_offset = log.len().saturating_sub(MAX_TELEMETRY_LOG_LEN);
1758            if let Some(newline_offset) = log[start_offset..].find('\n') {
1759                start_offset += newline_offset + 1;
1760            }
1761            let log_suffix = &log[start_offset..];
1762            let header = concat!(
1763                "// Zed collects anonymous usage data to help us understand how people are using the app.\n",
1764                "// Telemetry can be disabled via the `settings.json` file.\n",
1765                "// Here is the data that has been reported for the current session:\n",
1766            );
1767            let content = format!("{}\n{}", header, log_suffix);
1768            let json = app_state.languages.language_for_name("JSON").await.log_err();
1769
1770            workspace.update_in( cx, |workspace, window, cx| {
1771                let project = workspace.project().clone();
1772                let buffer = project.update(cx, |project, cx| project.create_local_buffer(&content, json,false, cx));
1773                let buffer = cx.new(|cx| {
1774                    MultiBuffer::singleton(buffer, cx).with_title("Telemetry Log".into())
1775                });
1776                workspace.add_item_to_active_pane(
1777                    Box::new(cx.new(|cx| {
1778                        let mut editor = Editor::for_multibuffer(buffer, Some(project), window, cx);
1779                        editor.set_read_only(true);
1780                        editor.set_breadcrumb_header("Telemetry Log".into());
1781                        editor
1782                    })),
1783                    None,
1784                    true,
1785                    window, cx,
1786                );
1787            }).log_err()?;
1788
1789            Some(())
1790        })
1791        .detach();
1792    }).detach();
1793}
1794
1795fn open_bundled_file(
1796    workspace: &Workspace,
1797    text: Cow<'static, str>,
1798    title: &'static str,
1799    language: &'static str,
1800    window: &mut Window,
1801    cx: &mut Context<Workspace>,
1802) {
1803    let language = workspace.app_state().languages.language_for_name(language);
1804    cx.spawn_in(window, async move |workspace, cx| {
1805        let language = language.await.log_err();
1806        workspace
1807            .update_in(cx, |workspace, window, cx| {
1808                workspace.with_local_workspace(window, cx, |workspace, window, cx| {
1809                    let project = workspace.project();
1810                    let buffer = project.update(cx, move |project, cx| {
1811                        let buffer =
1812                            project.create_local_buffer(text.as_ref(), language, false, cx);
1813                        buffer.update(cx, |buffer, cx| {
1814                            buffer.set_capability(Capability::ReadOnly, cx);
1815                        });
1816                        buffer
1817                    });
1818                    let buffer =
1819                        cx.new(|cx| MultiBuffer::singleton(buffer, cx).with_title(title.into()));
1820                    workspace.add_item_to_active_pane(
1821                        Box::new(cx.new(|cx| {
1822                            let mut editor =
1823                                Editor::for_multibuffer(buffer, Some(project.clone()), window, cx);
1824                            editor.set_read_only(true);
1825                            editor.set_breadcrumb_header(title.into());
1826                            editor
1827                        })),
1828                        None,
1829                        true,
1830                        window,
1831                        cx,
1832                    );
1833                })
1834            })?
1835            .await
1836    })
1837    .detach_and_log_err(cx);
1838}
1839
1840fn open_settings_file(
1841    abs_path: &'static Path,
1842    default_content: impl FnOnce() -> Rope + Send + 'static,
1843    window: &mut Window,
1844    cx: &mut Context<Workspace>,
1845) {
1846    cx.spawn_in(window, async move |workspace, cx| {
1847        let (worktree_creation_task, settings_open_task) = workspace
1848            .update_in(cx, |workspace, window, cx| {
1849                workspace.with_local_workspace(window, cx, move |workspace, window, cx| {
1850                    let worktree_creation_task = workspace.project().update(cx, |project, cx| {
1851                        // Set up a dedicated worktree for settings, since
1852                        // otherwise we're dropping and re-starting LSP servers
1853                        // for each file inside on every settings file
1854                        // close/open
1855
1856                        // TODO: Do note that all other external files (e.g.
1857                        // drag and drop from OS) still have their worktrees
1858                        // released on file close, causing LSP servers'
1859                        // restarts.
1860                        project.find_or_create_worktree(paths::config_dir().as_path(), false, cx)
1861                    });
1862                    let settings_open_task =
1863                        create_and_open_local_file(abs_path, window, cx, default_content);
1864                    (worktree_creation_task, settings_open_task)
1865                })
1866            })?
1867            .await?;
1868        let _ = worktree_creation_task.await?;
1869        let _ = settings_open_task.await?;
1870        anyhow::Ok(())
1871    })
1872    .detach_and_log_err(cx);
1873}
1874
1875fn capture_recent_audio(workspace: &mut Workspace, _: &mut Window, cx: &mut Context<Workspace>) {
1876    struct CaptureRecentAudioNotification {
1877        focus_handle: gpui::FocusHandle,
1878        save_result: Option<Result<(PathBuf, Duration), anyhow::Error>>,
1879        _save_task: Task<anyhow::Result<()>>,
1880    }
1881
1882    impl gpui::EventEmitter<DismissEvent> for CaptureRecentAudioNotification {}
1883    impl gpui::EventEmitter<SuppressEvent> for CaptureRecentAudioNotification {}
1884    impl gpui::Focusable for CaptureRecentAudioNotification {
1885        fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
1886            self.focus_handle.clone()
1887        }
1888    }
1889    impl workspace::notifications::Notification for CaptureRecentAudioNotification {}
1890
1891    impl Render for CaptureRecentAudioNotification {
1892        fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1893            let message = match &self.save_result {
1894                None => format!(
1895                    "Saving up to {} seconds of recent audio",
1896                    REPLAY_DURATION.as_secs(),
1897                ),
1898                Some(Ok((path, duration))) => format!(
1899                    "Saved {} seconds of all audio to {}",
1900                    duration.as_secs(),
1901                    path.display(),
1902                ),
1903                Some(Err(e)) => format!("Error saving audio replays: {e:?}"),
1904            };
1905
1906            NotificationFrame::new()
1907                .with_title(Some("Saved Audio"))
1908                .show_suppress_button(false)
1909                .on_close(cx.listener(|_, _, _, cx| {
1910                    cx.emit(DismissEvent);
1911                }))
1912                .with_content(message)
1913        }
1914    }
1915
1916    impl CaptureRecentAudioNotification {
1917        fn new(cx: &mut Context<Self>) -> Self {
1918            if AudioSettings::get_global(cx).rodio_audio {
1919                let executor = cx.background_executor().clone();
1920                let save_task = cx.default_global::<audio::Audio>().save_replays(executor);
1921                let _save_task = cx.spawn(async move |this, cx| {
1922                    let res = save_task.await;
1923                    this.update(cx, |this, cx| {
1924                        this.save_result = Some(res);
1925                        cx.notify();
1926                    })
1927                });
1928
1929                Self {
1930                    focus_handle: cx.focus_handle(),
1931                    _save_task,
1932                    save_result: None,
1933                }
1934            } else {
1935                Self {
1936                    focus_handle: cx.focus_handle(),
1937                    _save_task: Task::ready(Ok(())),
1938                    save_result: Some(Err(anyhow::anyhow!(
1939                        "Capturing recent audio is only supported on the experimental rodio audio pipeline"
1940                    ))),
1941                }
1942            }
1943        }
1944    }
1945
1946    workspace.show_notification(
1947        NotificationId::unique::<CaptureRecentAudioNotification>(),
1948        cx,
1949        |cx| cx.new(CaptureRecentAudioNotification::new),
1950    );
1951}
1952
1953#[cfg(test)]
1954mod tests {
1955    use super::*;
1956    use assets::Assets;
1957    use collections::HashSet;
1958    use editor::{DisplayPoint, Editor, SelectionEffects, display_map::DisplayRow};
1959    use gpui::{
1960        Action, AnyWindowHandle, App, AssetSource, BorrowAppContext, SemanticVersion,
1961        TestAppContext, UpdateGlobal, VisualTestContext, WindowHandle, actions,
1962    };
1963    use language::{LanguageMatcher, LanguageRegistry};
1964    use pretty_assertions::{assert_eq, assert_ne};
1965    use project::{Project, ProjectPath};
1966    use serde_json::json;
1967    use settings::{SettingsStore, watch_config_file};
1968    use std::{
1969        path::{Path, PathBuf},
1970        time::Duration,
1971    };
1972    use theme::{ThemeRegistry, ThemeSettings};
1973    use util::{path, rel_path::rel_path};
1974    use workspace::{
1975        NewFile, OpenOptions, OpenVisible, SERIALIZATION_THROTTLE_TIME, SaveIntent, SplitDirection,
1976        WorkspaceHandle,
1977        item::SaveOptions,
1978        item::{Item, ItemHandle},
1979        open_new, open_paths, pane,
1980    };
1981
1982    #[gpui::test]
1983    async fn test_open_non_existing_file(cx: &mut TestAppContext) {
1984        let app_state = init_test(cx);
1985        app_state
1986            .fs
1987            .as_fake()
1988            .insert_tree(
1989                path!("/root"),
1990                json!({
1991                    "a": {
1992                    },
1993                }),
1994            )
1995            .await;
1996
1997        cx.update(|cx| {
1998            open_paths(
1999                &[PathBuf::from(path!("/root/a/new"))],
2000                app_state.clone(),
2001                workspace::OpenOptions::default(),
2002                cx,
2003            )
2004        })
2005        .await
2006        .unwrap();
2007        assert_eq!(cx.read(|cx| cx.windows().len()), 1);
2008
2009        let workspace = cx.windows()[0].downcast::<Workspace>().unwrap();
2010        workspace
2011            .update(cx, |workspace, _, cx| {
2012                assert!(workspace.active_item_as::<Editor>(cx).is_some())
2013            })
2014            .unwrap();
2015    }
2016
2017    #[gpui::test]
2018    async fn test_open_paths_action(cx: &mut TestAppContext) {
2019        let app_state = init_test(cx);
2020        app_state
2021            .fs
2022            .as_fake()
2023            .insert_tree(
2024                "/root",
2025                json!({
2026                    "a": {
2027                        "aa": null,
2028                        "ab": null,
2029                    },
2030                    "b": {
2031                        "ba": null,
2032                        "bb": null,
2033                    },
2034                    "c": {
2035                        "ca": null,
2036                        "cb": null,
2037                    },
2038                    "d": {
2039                        "da": null,
2040                        "db": null,
2041                    },
2042                    "e": {
2043                        "ea": null,
2044                        "eb": null,
2045                    }
2046                }),
2047            )
2048            .await;
2049
2050        cx.update(|cx| {
2051            open_paths(
2052                &[PathBuf::from("/root/a"), PathBuf::from("/root/b")],
2053                app_state.clone(),
2054                workspace::OpenOptions::default(),
2055                cx,
2056            )
2057        })
2058        .await
2059        .unwrap();
2060        assert_eq!(cx.read(|cx| cx.windows().len()), 1);
2061
2062        cx.update(|cx| {
2063            open_paths(
2064                &[PathBuf::from("/root/a")],
2065                app_state.clone(),
2066                workspace::OpenOptions::default(),
2067                cx,
2068            )
2069        })
2070        .await
2071        .unwrap();
2072        assert_eq!(cx.read(|cx| cx.windows().len()), 1);
2073        let workspace_1 = cx
2074            .read(|cx| cx.windows()[0].downcast::<Workspace>())
2075            .unwrap();
2076        cx.run_until_parked();
2077        workspace_1
2078            .update(cx, |workspace, window, cx| {
2079                assert_eq!(workspace.worktrees(cx).count(), 2);
2080                assert!(workspace.left_dock().read(cx).is_open());
2081                assert!(
2082                    workspace
2083                        .active_pane()
2084                        .read(cx)
2085                        .focus_handle(cx)
2086                        .is_focused(window)
2087                );
2088            })
2089            .unwrap();
2090
2091        cx.update(|cx| {
2092            open_paths(
2093                &[PathBuf::from("/root/c"), PathBuf::from("/root/d")],
2094                app_state.clone(),
2095                workspace::OpenOptions::default(),
2096                cx,
2097            )
2098        })
2099        .await
2100        .unwrap();
2101        assert_eq!(cx.read(|cx| cx.windows().len()), 2);
2102
2103        // Replace existing windows
2104        let window = cx
2105            .update(|cx| cx.windows()[0].downcast::<Workspace>())
2106            .unwrap();
2107        cx.update(|cx| {
2108            open_paths(
2109                &[PathBuf::from("/root/e")],
2110                app_state,
2111                workspace::OpenOptions {
2112                    replace_window: Some(window),
2113                    ..Default::default()
2114                },
2115                cx,
2116            )
2117        })
2118        .await
2119        .unwrap();
2120        cx.background_executor.run_until_parked();
2121        assert_eq!(cx.read(|cx| cx.windows().len()), 2);
2122        let workspace_1 = cx
2123            .update(|cx| cx.windows()[0].downcast::<Workspace>())
2124            .unwrap();
2125        workspace_1
2126            .update(cx, |workspace, window, cx| {
2127                assert_eq!(
2128                    workspace
2129                        .worktrees(cx)
2130                        .map(|w| w.read(cx).abs_path())
2131                        .collect::<Vec<_>>(),
2132                    &[Path::new("/root/e").into()]
2133                );
2134                assert!(workspace.left_dock().read(cx).is_open());
2135                assert!(workspace.active_pane().focus_handle(cx).is_focused(window));
2136            })
2137            .unwrap();
2138    }
2139
2140    #[gpui::test]
2141    async fn test_open_add_new(cx: &mut TestAppContext) {
2142        let app_state = init_test(cx);
2143        app_state
2144            .fs
2145            .as_fake()
2146            .insert_tree(
2147                path!("/root"),
2148                json!({"a": "hey", "b": "", "dir": {"c": "f"}}),
2149            )
2150            .await;
2151
2152        cx.update(|cx| {
2153            open_paths(
2154                &[PathBuf::from(path!("/root/dir"))],
2155                app_state.clone(),
2156                workspace::OpenOptions::default(),
2157                cx,
2158            )
2159        })
2160        .await
2161        .unwrap();
2162        assert_eq!(cx.update(|cx| cx.windows().len()), 1);
2163
2164        cx.update(|cx| {
2165            open_paths(
2166                &[PathBuf::from(path!("/root/a"))],
2167                app_state.clone(),
2168                workspace::OpenOptions {
2169                    open_new_workspace: Some(false),
2170                    ..Default::default()
2171                },
2172                cx,
2173            )
2174        })
2175        .await
2176        .unwrap();
2177        assert_eq!(cx.update(|cx| cx.windows().len()), 1);
2178
2179        cx.update(|cx| {
2180            open_paths(
2181                &[PathBuf::from(path!("/root/dir/c"))],
2182                app_state.clone(),
2183                workspace::OpenOptions {
2184                    open_new_workspace: Some(true),
2185                    ..Default::default()
2186                },
2187                cx,
2188            )
2189        })
2190        .await
2191        .unwrap();
2192        assert_eq!(cx.update(|cx| cx.windows().len()), 2);
2193    }
2194
2195    #[gpui::test]
2196    async fn test_open_file_in_many_spaces(cx: &mut TestAppContext) {
2197        let app_state = init_test(cx);
2198        app_state
2199            .fs
2200            .as_fake()
2201            .insert_tree(
2202                path!("/root"),
2203                json!({"dir1": {"a": "b"}, "dir2": {"c": "d"}}),
2204            )
2205            .await;
2206
2207        cx.update(|cx| {
2208            open_paths(
2209                &[PathBuf::from(path!("/root/dir1/a"))],
2210                app_state.clone(),
2211                workspace::OpenOptions::default(),
2212                cx,
2213            )
2214        })
2215        .await
2216        .unwrap();
2217        assert_eq!(cx.update(|cx| cx.windows().len()), 1);
2218        let window1 = cx.update(|cx| cx.active_window().unwrap());
2219
2220        cx.update(|cx| {
2221            open_paths(
2222                &[PathBuf::from(path!("/root/dir2/c"))],
2223                app_state.clone(),
2224                workspace::OpenOptions::default(),
2225                cx,
2226            )
2227        })
2228        .await
2229        .unwrap();
2230        assert_eq!(cx.update(|cx| cx.windows().len()), 1);
2231
2232        cx.update(|cx| {
2233            open_paths(
2234                &[PathBuf::from(path!("/root/dir2"))],
2235                app_state.clone(),
2236                workspace::OpenOptions::default(),
2237                cx,
2238            )
2239        })
2240        .await
2241        .unwrap();
2242        assert_eq!(cx.update(|cx| cx.windows().len()), 2);
2243        let window2 = cx.update(|cx| cx.active_window().unwrap());
2244        assert!(window1 != window2);
2245        cx.update_window(window1, |_, window, _| window.activate_window())
2246            .unwrap();
2247
2248        cx.update(|cx| {
2249            open_paths(
2250                &[PathBuf::from(path!("/root/dir2/c"))],
2251                app_state.clone(),
2252                workspace::OpenOptions::default(),
2253                cx,
2254            )
2255        })
2256        .await
2257        .unwrap();
2258        assert_eq!(cx.update(|cx| cx.windows().len()), 2);
2259        // should have opened in window2 because that has dir2 visibly open (window1 has it open, but not in the project panel)
2260        assert!(cx.update(|cx| cx.active_window().unwrap()) == window2);
2261    }
2262
2263    #[gpui::test]
2264    async fn test_window_edit_state_restoring_disabled(cx: &mut TestAppContext) {
2265        let executor = cx.executor();
2266        let app_state = init_test(cx);
2267
2268        cx.update(|cx| {
2269            SettingsStore::update_global(cx, |store, cx| {
2270                store.update_user_settings(cx, |settings| {
2271                    settings
2272                        .session
2273                        .get_or_insert_default()
2274                        .restore_unsaved_buffers = Some(false)
2275                });
2276            });
2277        });
2278
2279        app_state
2280            .fs
2281            .as_fake()
2282            .insert_tree(path!("/root"), json!({"a": "hey"}))
2283            .await;
2284
2285        cx.update(|cx| {
2286            open_paths(
2287                &[PathBuf::from(path!("/root/a"))],
2288                app_state.clone(),
2289                workspace::OpenOptions::default(),
2290                cx,
2291            )
2292        })
2293        .await
2294        .unwrap();
2295        assert_eq!(cx.update(|cx| cx.windows().len()), 1);
2296
2297        // When opening the workspace, the window is not in a edited state.
2298        let window = cx.update(|cx| cx.windows()[0].downcast::<Workspace>().unwrap());
2299
2300        let window_is_edited = |window: WindowHandle<Workspace>, cx: &mut TestAppContext| {
2301            cx.update(|cx| window.read(cx).unwrap().is_edited())
2302        };
2303        let pane = window
2304            .read_with(cx, |workspace, _| workspace.active_pane().clone())
2305            .unwrap();
2306        let editor = window
2307            .read_with(cx, |workspace, cx| {
2308                workspace
2309                    .active_item(cx)
2310                    .unwrap()
2311                    .downcast::<Editor>()
2312                    .unwrap()
2313            })
2314            .unwrap();
2315
2316        assert!(!window_is_edited(window, cx));
2317
2318        // Editing a buffer marks the window as edited.
2319        window
2320            .update(cx, |_, window, cx| {
2321                editor.update(cx, |editor, cx| editor.insert("EDIT", window, cx));
2322            })
2323            .unwrap();
2324
2325        assert!(window_is_edited(window, cx));
2326
2327        // Undoing the edit restores the window's edited state.
2328        window
2329            .update(cx, |_, window, cx| {
2330                editor.update(cx, |editor, cx| {
2331                    editor.undo(&Default::default(), window, cx)
2332                });
2333            })
2334            .unwrap();
2335        assert!(!window_is_edited(window, cx));
2336
2337        // Redoing the edit marks the window as edited again.
2338        window
2339            .update(cx, |_, window, cx| {
2340                editor.update(cx, |editor, cx| {
2341                    editor.redo(&Default::default(), window, cx)
2342                });
2343            })
2344            .unwrap();
2345        assert!(window_is_edited(window, cx));
2346        let weak = editor.downgrade();
2347
2348        // Closing the item restores the window's edited state.
2349        let close = window
2350            .update(cx, |_, window, cx| {
2351                pane.update(cx, |pane, cx| {
2352                    drop(editor);
2353                    pane.close_active_item(&Default::default(), window, cx)
2354                })
2355            })
2356            .unwrap();
2357        executor.run_until_parked();
2358
2359        cx.simulate_prompt_answer("Don't Save");
2360        close.await.unwrap();
2361
2362        // Advance the clock to ensure that the item has been serialized and dropped from the queue
2363        cx.executor().advance_clock(Duration::from_secs(1));
2364
2365        weak.assert_released();
2366        assert!(!window_is_edited(window, cx));
2367        // Opening the buffer again doesn't impact the window's edited state.
2368        cx.update(|cx| {
2369            open_paths(
2370                &[PathBuf::from(path!("/root/a"))],
2371                app_state,
2372                workspace::OpenOptions::default(),
2373                cx,
2374            )
2375        })
2376        .await
2377        .unwrap();
2378        executor.run_until_parked();
2379
2380        window
2381            .update(cx, |workspace, _, cx| {
2382                let editor = workspace
2383                    .active_item(cx)
2384                    .unwrap()
2385                    .downcast::<Editor>()
2386                    .unwrap();
2387
2388                editor.update(cx, |editor, cx| {
2389                    assert_eq!(editor.text(cx), "hey");
2390                });
2391            })
2392            .unwrap();
2393
2394        let editor = window
2395            .read_with(cx, |workspace, cx| {
2396                workspace
2397                    .active_item(cx)
2398                    .unwrap()
2399                    .downcast::<Editor>()
2400                    .unwrap()
2401            })
2402            .unwrap();
2403        assert!(!window_is_edited(window, cx));
2404
2405        // Editing the buffer marks the window as edited.
2406        window
2407            .update(cx, |_, window, cx| {
2408                editor.update(cx, |editor, cx| editor.insert("EDIT", window, cx));
2409            })
2410            .unwrap();
2411        executor.run_until_parked();
2412        assert!(window_is_edited(window, cx));
2413
2414        // Ensure closing the window via the mouse gets preempted due to the
2415        // buffer having unsaved changes.
2416        assert!(!VisualTestContext::from_window(window.into(), cx).simulate_close());
2417        executor.run_until_parked();
2418        assert_eq!(cx.update(|cx| cx.windows().len()), 1);
2419
2420        // The window is successfully closed after the user dismisses the prompt.
2421        cx.simulate_prompt_answer("Don't Save");
2422        executor.run_until_parked();
2423        assert_eq!(cx.update(|cx| cx.windows().len()), 0);
2424    }
2425
2426    #[gpui::test]
2427    async fn test_window_edit_state_restoring_enabled(cx: &mut TestAppContext) {
2428        let app_state = init_test(cx);
2429        app_state
2430            .fs
2431            .as_fake()
2432            .insert_tree(path!("/root"), json!({"a": "hey"}))
2433            .await;
2434
2435        cx.update(|cx| {
2436            open_paths(
2437                &[PathBuf::from(path!("/root/a"))],
2438                app_state.clone(),
2439                workspace::OpenOptions::default(),
2440                cx,
2441            )
2442        })
2443        .await
2444        .unwrap();
2445
2446        assert_eq!(cx.update(|cx| cx.windows().len()), 1);
2447
2448        // When opening the workspace, the window is not in a edited state.
2449        let window = cx.update(|cx| cx.windows()[0].downcast::<Workspace>().unwrap());
2450
2451        let window_is_edited = |window: WindowHandle<Workspace>, cx: &mut TestAppContext| {
2452            cx.update(|cx| window.read(cx).unwrap().is_edited())
2453        };
2454
2455        let editor = window
2456            .read_with(cx, |workspace, cx| {
2457                workspace
2458                    .active_item(cx)
2459                    .unwrap()
2460                    .downcast::<Editor>()
2461                    .unwrap()
2462            })
2463            .unwrap();
2464
2465        assert!(!window_is_edited(window, cx));
2466
2467        // Editing a buffer marks the window as edited.
2468        window
2469            .update(cx, |_, window, cx| {
2470                editor.update(cx, |editor, cx| editor.insert("EDIT", window, cx));
2471            })
2472            .unwrap();
2473
2474        assert!(window_is_edited(window, cx));
2475        cx.run_until_parked();
2476
2477        // Advance the clock to make sure the workspace is serialized
2478        cx.executor().advance_clock(Duration::from_secs(1));
2479
2480        // When closing the window, no prompt shows up and the window is closed.
2481        // buffer having unsaved changes.
2482        assert!(!VisualTestContext::from_window(window.into(), cx).simulate_close());
2483        cx.run_until_parked();
2484        assert_eq!(cx.update(|cx| cx.windows().len()), 0);
2485
2486        // When we now reopen the window, the edited state and the edited buffer are back
2487        cx.update(|cx| {
2488            open_paths(
2489                &[PathBuf::from(path!("/root/a"))],
2490                app_state.clone(),
2491                workspace::OpenOptions::default(),
2492                cx,
2493            )
2494        })
2495        .await
2496        .unwrap();
2497
2498        assert_eq!(cx.update(|cx| cx.windows().len()), 1);
2499        assert!(cx.update(|cx| cx.active_window().is_some()));
2500
2501        cx.run_until_parked();
2502
2503        // When opening the workspace, the window is not in a edited state.
2504        let window = cx.update(|cx| cx.active_window().unwrap().downcast::<Workspace>().unwrap());
2505        assert!(window_is_edited(window, cx));
2506
2507        window
2508            .update(cx, |workspace, _, cx| {
2509                let editor = workspace
2510                    .active_item(cx)
2511                    .unwrap()
2512                    .downcast::<editor::Editor>()
2513                    .unwrap();
2514                editor.update(cx, |editor, cx| {
2515                    assert_eq!(editor.text(cx), "EDIThey");
2516                    assert!(editor.is_dirty(cx));
2517                });
2518
2519                editor
2520            })
2521            .unwrap();
2522    }
2523
2524    #[gpui::test]
2525    async fn test_new_empty_workspace(cx: &mut TestAppContext) {
2526        let app_state = init_test(cx);
2527        cx.update(|cx| {
2528            open_new(
2529                Default::default(),
2530                app_state.clone(),
2531                cx,
2532                |workspace, window, cx| {
2533                    Editor::new_file(workspace, &Default::default(), window, cx)
2534                },
2535            )
2536        })
2537        .await
2538        .unwrap();
2539        cx.run_until_parked();
2540
2541        let workspace = cx
2542            .update(|cx| cx.windows().first().unwrap().downcast::<Workspace>())
2543            .unwrap();
2544
2545        let editor = workspace
2546            .update(cx, |workspace, _, cx| {
2547                let editor = workspace
2548                    .active_item(cx)
2549                    .unwrap()
2550                    .downcast::<editor::Editor>()
2551                    .unwrap();
2552                editor.update(cx, |editor, cx| {
2553                    assert!(editor.text(cx).is_empty());
2554                    assert!(!editor.is_dirty(cx));
2555                });
2556
2557                editor
2558            })
2559            .unwrap();
2560
2561        let save_task = workspace
2562            .update(cx, |workspace, window, cx| {
2563                workspace.save_active_item(SaveIntent::Save, window, cx)
2564            })
2565            .unwrap();
2566        app_state.fs.create_dir(Path::new("/root")).await.unwrap();
2567        cx.background_executor.run_until_parked();
2568        cx.simulate_new_path_selection(|_| Some(PathBuf::from("/root/the-new-name")));
2569        save_task.await.unwrap();
2570        workspace
2571            .update(cx, |_, _, cx| {
2572                editor.update(cx, |editor, cx| {
2573                    assert!(!editor.is_dirty(cx));
2574                    assert_eq!(editor.title(cx), "the-new-name");
2575                });
2576            })
2577            .unwrap();
2578    }
2579
2580    #[gpui::test]
2581    async fn test_open_entry(cx: &mut TestAppContext) {
2582        let app_state = init_test(cx);
2583        app_state
2584            .fs
2585            .as_fake()
2586            .insert_tree(
2587                path!("/root"),
2588                json!({
2589                    "a": {
2590                        "file1": "contents 1",
2591                        "file2": "contents 2",
2592                        "file3": "contents 3",
2593                    },
2594                }),
2595            )
2596            .await;
2597
2598        let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
2599        project.update(cx, |project, _cx| {
2600            project.languages().add(markdown_language())
2601        });
2602        let window = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
2603        let workspace = window.root(cx).unwrap();
2604
2605        let entries = cx.read(|cx| workspace.file_project_paths(cx));
2606        let file1 = entries[0].clone();
2607        let file2 = entries[1].clone();
2608        let file3 = entries[2].clone();
2609
2610        // Open the first entry
2611        let entry_1 = window
2612            .update(cx, |w, window, cx| {
2613                w.open_path(file1.clone(), None, true, window, cx)
2614            })
2615            .unwrap()
2616            .await
2617            .unwrap();
2618        cx.read(|cx| {
2619            let pane = workspace.read(cx).active_pane().read(cx);
2620            assert_eq!(
2621                pane.active_item().unwrap().project_path(cx),
2622                Some(file1.clone())
2623            );
2624            assert_eq!(pane.items_len(), 1);
2625        });
2626
2627        // Open the second entry
2628        window
2629            .update(cx, |w, window, cx| {
2630                w.open_path(file2.clone(), None, true, window, cx)
2631            })
2632            .unwrap()
2633            .await
2634            .unwrap();
2635        cx.read(|cx| {
2636            let pane = workspace.read(cx).active_pane().read(cx);
2637            assert_eq!(
2638                pane.active_item().unwrap().project_path(cx),
2639                Some(file2.clone())
2640            );
2641            assert_eq!(pane.items_len(), 2);
2642        });
2643
2644        // Open the first entry again. The existing pane item is activated.
2645        let entry_1b = window
2646            .update(cx, |w, window, cx| {
2647                w.open_path(file1.clone(), None, true, window, cx)
2648            })
2649            .unwrap()
2650            .await
2651            .unwrap();
2652        assert_eq!(entry_1.item_id(), entry_1b.item_id());
2653
2654        cx.read(|cx| {
2655            let pane = workspace.read(cx).active_pane().read(cx);
2656            assert_eq!(
2657                pane.active_item().unwrap().project_path(cx),
2658                Some(file1.clone())
2659            );
2660            assert_eq!(pane.items_len(), 2);
2661        });
2662
2663        // Split the pane with the first entry, then open the second entry again.
2664        window
2665            .update(cx, |w, window, cx| {
2666                w.split_and_clone(w.active_pane().clone(), SplitDirection::Right, window, cx);
2667                w.open_path(file2.clone(), None, true, window, cx)
2668            })
2669            .unwrap()
2670            .await
2671            .unwrap();
2672
2673        window
2674            .read_with(cx, |w, cx| {
2675                assert_eq!(
2676                    w.active_pane()
2677                        .read(cx)
2678                        .active_item()
2679                        .unwrap()
2680                        .project_path(cx),
2681                    Some(file2.clone())
2682                );
2683            })
2684            .unwrap();
2685
2686        // Open the third entry twice concurrently. Only one pane item is added.
2687        let (t1, t2) = window
2688            .update(cx, |w, window, cx| {
2689                (
2690                    w.open_path(file3.clone(), None, true, window, cx),
2691                    w.open_path(file3.clone(), None, true, window, cx),
2692                )
2693            })
2694            .unwrap();
2695        t1.await.unwrap();
2696        t2.await.unwrap();
2697        cx.read(|cx| {
2698            let pane = workspace.read(cx).active_pane().read(cx);
2699            assert_eq!(
2700                pane.active_item().unwrap().project_path(cx),
2701                Some(file3.clone())
2702            );
2703            let pane_entries = pane
2704                .items()
2705                .map(|i| i.project_path(cx).unwrap())
2706                .collect::<Vec<_>>();
2707            assert_eq!(pane_entries, &[file1, file2, file3]);
2708        });
2709    }
2710
2711    #[gpui::test]
2712    async fn test_open_paths(cx: &mut TestAppContext) {
2713        let app_state = init_test(cx);
2714
2715        app_state
2716            .fs
2717            .as_fake()
2718            .insert_tree(
2719                path!("/"),
2720                json!({
2721                    "dir1": {
2722                        "a.txt": ""
2723                    },
2724                    "dir2": {
2725                        "b.txt": ""
2726                    },
2727                    "dir3": {
2728                        "c.txt": ""
2729                    },
2730                    "d.txt": ""
2731                }),
2732            )
2733            .await;
2734
2735        cx.update(|cx| {
2736            open_paths(
2737                &[PathBuf::from(path!("/dir1/"))],
2738                app_state,
2739                workspace::OpenOptions::default(),
2740                cx,
2741            )
2742        })
2743        .await
2744        .unwrap();
2745        assert_eq!(cx.update(|cx| cx.windows().len()), 1);
2746        let window = cx.update(|cx| cx.windows()[0].downcast::<Workspace>().unwrap());
2747        let workspace = window.root(cx).unwrap();
2748
2749        #[track_caller]
2750        fn assert_project_panel_selection(
2751            workspace: &Workspace,
2752            expected_worktree_path: &Path,
2753            expected_entry_path: &RelPath,
2754            cx: &App,
2755        ) {
2756            let project_panel = [
2757                workspace.left_dock().read(cx).panel::<ProjectPanel>(),
2758                workspace.right_dock().read(cx).panel::<ProjectPanel>(),
2759                workspace.bottom_dock().read(cx).panel::<ProjectPanel>(),
2760            ]
2761            .into_iter()
2762            .find_map(std::convert::identity)
2763            .expect("found no project panels")
2764            .read(cx);
2765            let (selected_worktree, selected_entry) = project_panel
2766                .selected_entry(cx)
2767                .expect("project panel should have a selected entry");
2768            assert_eq!(
2769                selected_worktree.abs_path().as_ref(),
2770                expected_worktree_path,
2771                "Unexpected project panel selected worktree path"
2772            );
2773            assert_eq!(
2774                selected_entry.path.as_ref(),
2775                expected_entry_path,
2776                "Unexpected project panel selected entry path"
2777            );
2778        }
2779
2780        // Open a file within an existing worktree.
2781        window
2782            .update(cx, |workspace, window, cx| {
2783                workspace.open_paths(
2784                    vec![path!("/dir1/a.txt").into()],
2785                    OpenOptions {
2786                        visible: Some(OpenVisible::All),
2787                        ..Default::default()
2788                    },
2789                    None,
2790                    window,
2791                    cx,
2792                )
2793            })
2794            .unwrap()
2795            .await;
2796        cx.read(|cx| {
2797            let workspace = workspace.read(cx);
2798            assert_project_panel_selection(
2799                workspace,
2800                Path::new(path!("/dir1")),
2801                rel_path("a.txt"),
2802                cx,
2803            );
2804            assert_eq!(
2805                workspace
2806                    .active_pane()
2807                    .read(cx)
2808                    .active_item()
2809                    .unwrap()
2810                    .act_as::<Editor>(cx)
2811                    .unwrap()
2812                    .read(cx)
2813                    .title(cx),
2814                "a.txt"
2815            );
2816        });
2817
2818        // Open a file outside of any existing worktree.
2819        window
2820            .update(cx, |workspace, window, cx| {
2821                workspace.open_paths(
2822                    vec![path!("/dir2/b.txt").into()],
2823                    OpenOptions {
2824                        visible: Some(OpenVisible::All),
2825                        ..Default::default()
2826                    },
2827                    None,
2828                    window,
2829                    cx,
2830                )
2831            })
2832            .unwrap()
2833            .await;
2834        cx.read(|cx| {
2835            let workspace = workspace.read(cx);
2836            assert_project_panel_selection(
2837                workspace,
2838                Path::new(path!("/dir2/b.txt")),
2839                rel_path(""),
2840                cx,
2841            );
2842            let worktree_roots = workspace
2843                .worktrees(cx)
2844                .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref())
2845                .collect::<HashSet<_>>();
2846            assert_eq!(
2847                worktree_roots,
2848                vec![path!("/dir1"), path!("/dir2/b.txt")]
2849                    .into_iter()
2850                    .map(Path::new)
2851                    .collect(),
2852            );
2853            assert_eq!(
2854                workspace
2855                    .active_pane()
2856                    .read(cx)
2857                    .active_item()
2858                    .unwrap()
2859                    .act_as::<Editor>(cx)
2860                    .unwrap()
2861                    .read(cx)
2862                    .title(cx),
2863                "b.txt"
2864            );
2865        });
2866
2867        // Ensure opening a directory and one of its children only adds one worktree.
2868        window
2869            .update(cx, |workspace, window, cx| {
2870                workspace.open_paths(
2871                    vec![path!("/dir3").into(), path!("/dir3/c.txt").into()],
2872                    OpenOptions {
2873                        visible: Some(OpenVisible::All),
2874                        ..Default::default()
2875                    },
2876                    None,
2877                    window,
2878                    cx,
2879                )
2880            })
2881            .unwrap()
2882            .await;
2883        cx.read(|cx| {
2884            let workspace = workspace.read(cx);
2885            assert_project_panel_selection(
2886                workspace,
2887                Path::new(path!("/dir3")),
2888                rel_path("c.txt"),
2889                cx,
2890            );
2891            let worktree_roots = workspace
2892                .worktrees(cx)
2893                .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref())
2894                .collect::<HashSet<_>>();
2895            assert_eq!(
2896                worktree_roots,
2897                vec![path!("/dir1"), path!("/dir2/b.txt"), path!("/dir3")]
2898                    .into_iter()
2899                    .map(Path::new)
2900                    .collect(),
2901            );
2902            assert_eq!(
2903                workspace
2904                    .active_pane()
2905                    .read(cx)
2906                    .active_item()
2907                    .unwrap()
2908                    .act_as::<Editor>(cx)
2909                    .unwrap()
2910                    .read(cx)
2911                    .title(cx),
2912                "c.txt"
2913            );
2914        });
2915
2916        // Ensure opening invisibly a file outside an existing worktree adds a new, invisible worktree.
2917        window
2918            .update(cx, |workspace, window, cx| {
2919                workspace.open_paths(
2920                    vec![path!("/d.txt").into()],
2921                    OpenOptions {
2922                        visible: Some(OpenVisible::None),
2923                        ..Default::default()
2924                    },
2925                    None,
2926                    window,
2927                    cx,
2928                )
2929            })
2930            .unwrap()
2931            .await;
2932        cx.read(|cx| {
2933            let workspace = workspace.read(cx);
2934            assert_project_panel_selection(workspace, Path::new(path!("/d.txt")), rel_path(""), cx);
2935            let worktree_roots = workspace
2936                .worktrees(cx)
2937                .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref())
2938                .collect::<HashSet<_>>();
2939            assert_eq!(
2940                worktree_roots,
2941                vec![
2942                    path!("/dir1"),
2943                    path!("/dir2/b.txt"),
2944                    path!("/dir3"),
2945                    path!("/d.txt")
2946                ]
2947                .into_iter()
2948                .map(Path::new)
2949                .collect(),
2950            );
2951
2952            let visible_worktree_roots = workspace
2953                .visible_worktrees(cx)
2954                .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref())
2955                .collect::<HashSet<_>>();
2956            assert_eq!(
2957                visible_worktree_roots,
2958                vec![path!("/dir1"), path!("/dir2/b.txt"), path!("/dir3")]
2959                    .into_iter()
2960                    .map(Path::new)
2961                    .collect(),
2962            );
2963
2964            assert_eq!(
2965                workspace
2966                    .active_pane()
2967                    .read(cx)
2968                    .active_item()
2969                    .unwrap()
2970                    .act_as::<Editor>(cx)
2971                    .unwrap()
2972                    .read(cx)
2973                    .title(cx),
2974                "d.txt"
2975            );
2976        });
2977    }
2978
2979    #[gpui::test]
2980    async fn test_opening_excluded_paths(cx: &mut TestAppContext) {
2981        let app_state = init_test(cx);
2982        cx.update(|cx| {
2983            cx.update_global::<SettingsStore, _>(|store, cx| {
2984                store.update_user_settings(cx, |project_settings| {
2985                    project_settings.project.worktree.file_scan_exclusions =
2986                        Some(vec!["excluded_dir".to_string(), "**/.git".to_string()]);
2987                });
2988            });
2989        });
2990        app_state
2991            .fs
2992            .as_fake()
2993            .insert_tree(
2994                path!("/root"),
2995                json!({
2996                    ".gitignore": "ignored_dir\n",
2997                    ".git": {
2998                        "HEAD": "ref: refs/heads/main",
2999                    },
3000                    "regular_dir": {
3001                        "file": "regular file contents",
3002                    },
3003                    "ignored_dir": {
3004                        "ignored_subdir": {
3005                            "file": "ignored subfile contents",
3006                        },
3007                        "file": "ignored file contents",
3008                    },
3009                    "excluded_dir": {
3010                        "file": "excluded file contents",
3011                        "ignored_subdir": {
3012                            "file": "ignored subfile contents",
3013                        },
3014                    },
3015                }),
3016            )
3017            .await;
3018
3019        let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
3020        project.update(cx, |project, _cx| {
3021            project.languages().add(markdown_language())
3022        });
3023        let window = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
3024        let workspace = window.root(cx).unwrap();
3025
3026        let initial_entries = cx.read(|cx| workspace.file_project_paths(cx));
3027        let paths_to_open = [
3028            PathBuf::from(path!("/root/excluded_dir/file")),
3029            PathBuf::from(path!("/root/.git/HEAD")),
3030            PathBuf::from(path!("/root/excluded_dir/ignored_subdir")),
3031        ];
3032        let (opened_workspace, new_items) = cx
3033            .update(|cx| {
3034                workspace::open_paths(
3035                    &paths_to_open,
3036                    app_state,
3037                    workspace::OpenOptions::default(),
3038                    cx,
3039                )
3040            })
3041            .await
3042            .unwrap();
3043
3044        assert_eq!(
3045            opened_workspace.root(cx).unwrap().entity_id(),
3046            workspace.entity_id(),
3047            "Excluded files in subfolders of a workspace root should be opened in the workspace"
3048        );
3049        let mut opened_paths = cx.read(|cx| {
3050            assert_eq!(
3051                new_items.len(),
3052                paths_to_open.len(),
3053                "Expect to get the same number of opened items as submitted paths to open"
3054            );
3055            new_items
3056                .iter()
3057                .zip(paths_to_open.iter())
3058                .map(|(i, path)| {
3059                    match i {
3060                        Some(Ok(i)) => Some(i.project_path(cx).map(|p| p.path)),
3061                        Some(Err(e)) => panic!("Excluded file {path:?} failed to open: {e:?}"),
3062                        None => None,
3063                    }
3064                    .flatten()
3065                })
3066                .collect::<Vec<_>>()
3067        });
3068        opened_paths.sort();
3069        assert_eq!(
3070            opened_paths,
3071            vec![
3072                None,
3073                Some(rel_path(".git/HEAD").into()),
3074                Some(rel_path("excluded_dir/file").into()),
3075            ],
3076            "Excluded files should get opened, excluded dir should not get opened"
3077        );
3078
3079        let entries = cx.read(|cx| workspace.file_project_paths(cx));
3080        assert_eq!(
3081            initial_entries, entries,
3082            "Workspace entries should not change after opening excluded files and directories paths"
3083        );
3084
3085        cx.read(|cx| {
3086                let pane = workspace.read(cx).active_pane().read(cx);
3087                let mut opened_buffer_paths = pane
3088                    .items()
3089                    .map(|i| {
3090                        i.project_path(cx)
3091                            .expect("all excluded files that got open should have a path")
3092                            .path
3093                    })
3094                    .collect::<Vec<_>>();
3095                opened_buffer_paths.sort();
3096                assert_eq!(
3097                    opened_buffer_paths,
3098                    vec![rel_path(".git/HEAD").into(), rel_path("excluded_dir/file").into()],
3099                    "Despite not being present in the worktrees, buffers for excluded files are opened and added to the pane"
3100                );
3101            });
3102    }
3103
3104    #[gpui::test]
3105    async fn test_save_conflicting_item(cx: &mut TestAppContext) {
3106        let app_state = init_test(cx);
3107        app_state
3108            .fs
3109            .as_fake()
3110            .insert_tree(path!("/root"), json!({ "a.txt": "" }))
3111            .await;
3112
3113        let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
3114        project.update(cx, |project, _cx| {
3115            project.languages().add(markdown_language())
3116        });
3117        let window = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
3118        let workspace = window.root(cx).unwrap();
3119
3120        // Open a file within an existing worktree.
3121        window
3122            .update(cx, |workspace, window, cx| {
3123                workspace.open_paths(
3124                    vec![PathBuf::from(path!("/root/a.txt"))],
3125                    OpenOptions {
3126                        visible: Some(OpenVisible::All),
3127                        ..Default::default()
3128                    },
3129                    None,
3130                    window,
3131                    cx,
3132                )
3133            })
3134            .unwrap()
3135            .await;
3136        let editor = cx.read(|cx| {
3137            let pane = workspace.read(cx).active_pane().read(cx);
3138            let item = pane.active_item().unwrap();
3139            item.downcast::<Editor>().unwrap()
3140        });
3141
3142        window
3143            .update(cx, |_, window, cx| {
3144                editor.update(cx, |editor, cx| editor.handle_input("x", window, cx));
3145            })
3146            .unwrap();
3147
3148        app_state
3149            .fs
3150            .as_fake()
3151            .insert_file(path!("/root/a.txt"), b"changed".to_vec())
3152            .await;
3153
3154        cx.run_until_parked();
3155        cx.read(|cx| assert!(editor.is_dirty(cx)));
3156        cx.read(|cx| assert!(editor.has_conflict(cx)));
3157
3158        let save_task = window
3159            .update(cx, |workspace, window, cx| {
3160                workspace.save_active_item(SaveIntent::Save, window, cx)
3161            })
3162            .unwrap();
3163        cx.background_executor.run_until_parked();
3164        cx.simulate_prompt_answer("Overwrite");
3165        save_task.await.unwrap();
3166        window
3167            .update(cx, |_, _, cx| {
3168                editor.update(cx, |editor, cx| {
3169                    assert!(!editor.is_dirty(cx));
3170                    assert!(!editor.has_conflict(cx));
3171                });
3172            })
3173            .unwrap();
3174    }
3175
3176    #[gpui::test]
3177    async fn test_open_and_save_new_file(cx: &mut TestAppContext) {
3178        let app_state = init_test(cx);
3179        app_state
3180            .fs
3181            .create_dir(Path::new(path!("/root")))
3182            .await
3183            .unwrap();
3184
3185        let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
3186        project.update(cx, |project, _| {
3187            project.languages().add(markdown_language());
3188            project.languages().add(rust_lang());
3189        });
3190        let window = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
3191        let worktree = cx.update(|cx| window.read(cx).unwrap().worktrees(cx).next().unwrap());
3192
3193        // Create a new untitled buffer
3194        cx.dispatch_action(window.into(), NewFile);
3195        let editor = window
3196            .read_with(cx, |workspace, cx| {
3197                workspace
3198                    .active_item(cx)
3199                    .unwrap()
3200                    .downcast::<Editor>()
3201                    .unwrap()
3202            })
3203            .unwrap();
3204
3205        window
3206            .update(cx, |_, window, cx| {
3207                editor.update(cx, |editor, cx| {
3208                    assert!(!editor.is_dirty(cx));
3209                    assert_eq!(editor.title(cx), "untitled");
3210                    assert!(Arc::ptr_eq(
3211                        &editor.buffer().read(cx).language_at(0, cx).unwrap(),
3212                        &languages::PLAIN_TEXT
3213                    ));
3214                    editor.handle_input("hi", window, cx);
3215                    assert!(editor.is_dirty(cx));
3216                });
3217            })
3218            .unwrap();
3219
3220        // Save the buffer. This prompts for a filename.
3221        let save_task = window
3222            .update(cx, |workspace, window, cx| {
3223                workspace.save_active_item(SaveIntent::Save, window, cx)
3224            })
3225            .unwrap();
3226        cx.background_executor.run_until_parked();
3227        cx.simulate_new_path_selection(|parent_dir| {
3228            assert_eq!(parent_dir, Path::new(path!("/root")));
3229            Some(parent_dir.join("the-new-name.rs"))
3230        });
3231        cx.read(|cx| {
3232            assert!(editor.is_dirty(cx));
3233            assert_eq!(editor.read(cx).title(cx), "hi");
3234        });
3235
3236        // When the save completes, the buffer's title is updated and the language is assigned based
3237        // on the path.
3238        save_task.await.unwrap();
3239        window
3240            .update(cx, |_, _, cx| {
3241                editor.update(cx, |editor, cx| {
3242                    assert!(!editor.is_dirty(cx));
3243                    assert_eq!(editor.title(cx), "the-new-name.rs");
3244                    assert_eq!(
3245                        editor.buffer().read(cx).language_at(0, cx).unwrap().name(),
3246                        "Rust".into()
3247                    );
3248                });
3249            })
3250            .unwrap();
3251
3252        // Edit the file and save it again. This time, there is no filename prompt.
3253        window
3254            .update(cx, |_, window, cx| {
3255                editor.update(cx, |editor, cx| {
3256                    editor.handle_input(" there", window, cx);
3257                    assert!(editor.is_dirty(cx));
3258                });
3259            })
3260            .unwrap();
3261
3262        let save_task = window
3263            .update(cx, |workspace, window, cx| {
3264                workspace.save_active_item(SaveIntent::Save, window, cx)
3265            })
3266            .unwrap();
3267        save_task.await.unwrap();
3268
3269        assert!(!cx.did_prompt_for_new_path());
3270        window
3271            .update(cx, |_, _, cx| {
3272                editor.update(cx, |editor, cx| {
3273                    assert!(!editor.is_dirty(cx));
3274                    assert_eq!(editor.title(cx), "the-new-name.rs")
3275                });
3276            })
3277            .unwrap();
3278
3279        // Open the same newly-created file in another pane item. The new editor should reuse
3280        // the same buffer.
3281        cx.dispatch_action(window.into(), NewFile);
3282        window
3283            .update(cx, |workspace, window, cx| {
3284                workspace.split_and_clone(
3285                    workspace.active_pane().clone(),
3286                    SplitDirection::Right,
3287                    window,
3288                    cx,
3289                );
3290                workspace.open_path(
3291                    (worktree.read(cx).id(), rel_path("the-new-name.rs")),
3292                    None,
3293                    true,
3294                    window,
3295                    cx,
3296                )
3297            })
3298            .unwrap()
3299            .await
3300            .unwrap();
3301        let editor2 = window
3302            .update(cx, |workspace, _, cx| {
3303                workspace
3304                    .active_item(cx)
3305                    .unwrap()
3306                    .downcast::<Editor>()
3307                    .unwrap()
3308            })
3309            .unwrap();
3310        cx.read(|cx| {
3311            assert_eq!(
3312                editor2.read(cx).buffer().read(cx).as_singleton().unwrap(),
3313                editor.read(cx).buffer().read(cx).as_singleton().unwrap()
3314            );
3315        })
3316    }
3317
3318    #[gpui::test]
3319    async fn test_setting_language_when_saving_as_single_file_worktree(cx: &mut TestAppContext) {
3320        let app_state = init_test(cx);
3321        app_state.fs.create_dir(Path::new("/root")).await.unwrap();
3322
3323        let project = Project::test(app_state.fs.clone(), [], cx).await;
3324        project.update(cx, |project, _| {
3325            project.languages().add(rust_lang());
3326            project.languages().add(markdown_language());
3327        });
3328        let window = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
3329
3330        // Create a new untitled buffer
3331        cx.dispatch_action(window.into(), NewFile);
3332        let editor = window
3333            .read_with(cx, |workspace, cx| {
3334                workspace
3335                    .active_item(cx)
3336                    .unwrap()
3337                    .downcast::<Editor>()
3338                    .unwrap()
3339            })
3340            .unwrap();
3341        window
3342            .update(cx, |_, window, cx| {
3343                editor.update(cx, |editor, cx| {
3344                    assert!(Arc::ptr_eq(
3345                        &editor.buffer().read(cx).language_at(0, cx).unwrap(),
3346                        &languages::PLAIN_TEXT
3347                    ));
3348                    editor.handle_input("hi", window, cx);
3349                    assert!(editor.is_dirty(cx));
3350                });
3351            })
3352            .unwrap();
3353
3354        // Save the buffer. This prompts for a filename.
3355        let save_task = window
3356            .update(cx, |workspace, window, cx| {
3357                workspace.save_active_item(SaveIntent::Save, window, cx)
3358            })
3359            .unwrap();
3360        cx.background_executor.run_until_parked();
3361        cx.simulate_new_path_selection(|_| Some(PathBuf::from("/root/the-new-name.rs")));
3362        save_task.await.unwrap();
3363        // The buffer is not dirty anymore and the language is assigned based on the path.
3364        window
3365            .update(cx, |_, _, cx| {
3366                editor.update(cx, |editor, cx| {
3367                    assert!(!editor.is_dirty(cx));
3368                    assert_eq!(
3369                        editor.buffer().read(cx).language_at(0, cx).unwrap().name(),
3370                        "Rust".into()
3371                    )
3372                });
3373            })
3374            .unwrap();
3375    }
3376
3377    #[gpui::test]
3378    async fn test_pane_actions(cx: &mut TestAppContext) {
3379        let app_state = init_test(cx);
3380        app_state
3381            .fs
3382            .as_fake()
3383            .insert_tree(
3384                path!("/root"),
3385                json!({
3386                    "a": {
3387                        "file1": "contents 1",
3388                        "file2": "contents 2",
3389                        "file3": "contents 3",
3390                    },
3391                }),
3392            )
3393            .await;
3394
3395        let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
3396        project.update(cx, |project, _cx| {
3397            project.languages().add(markdown_language())
3398        });
3399        let window = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
3400        let workspace = window.root(cx).unwrap();
3401
3402        let entries = cx.read(|cx| workspace.file_project_paths(cx));
3403        let file1 = entries[0].clone();
3404
3405        let pane_1 = cx.read(|cx| workspace.read(cx).active_pane().clone());
3406
3407        window
3408            .update(cx, |w, window, cx| {
3409                w.open_path(file1.clone(), None, true, window, cx)
3410            })
3411            .unwrap()
3412            .await
3413            .unwrap();
3414
3415        let (editor_1, buffer) = window
3416            .update(cx, |_, window, cx| {
3417                pane_1.update(cx, |pane_1, cx| {
3418                    let editor = pane_1.active_item().unwrap().downcast::<Editor>().unwrap();
3419                    assert_eq!(editor.project_path(cx), Some(file1.clone()));
3420                    let buffer = editor.update(cx, |editor, cx| {
3421                        editor.insert("dirt", window, cx);
3422                        editor.buffer().downgrade()
3423                    });
3424                    (editor.downgrade(), buffer)
3425                })
3426            })
3427            .unwrap();
3428
3429        cx.dispatch_action(window.into(), pane::SplitRight);
3430        let editor_2 = cx.update(|cx| {
3431            let pane_2 = workspace.read(cx).active_pane().clone();
3432            assert_ne!(pane_1, pane_2);
3433
3434            let pane2_item = pane_2.read(cx).active_item().unwrap();
3435            assert_eq!(pane2_item.project_path(cx), Some(file1.clone()));
3436
3437            pane2_item.downcast::<Editor>().unwrap().downgrade()
3438        });
3439        cx.dispatch_action(
3440            window.into(),
3441            workspace::CloseActiveItem {
3442                save_intent: None,
3443                close_pinned: false,
3444            },
3445        );
3446
3447        cx.background_executor.run_until_parked();
3448        window
3449            .read_with(cx, |workspace, _| {
3450                assert_eq!(workspace.panes().len(), 1);
3451                assert_eq!(workspace.active_pane(), &pane_1);
3452            })
3453            .unwrap();
3454
3455        cx.dispatch_action(
3456            window.into(),
3457            workspace::CloseActiveItem {
3458                save_intent: None,
3459                close_pinned: false,
3460            },
3461        );
3462        cx.background_executor.run_until_parked();
3463        cx.simulate_prompt_answer("Don't Save");
3464        cx.background_executor.run_until_parked();
3465
3466        window
3467            .update(cx, |workspace, _, cx| {
3468                assert_eq!(workspace.panes().len(), 1);
3469                assert!(workspace.active_item(cx).is_none());
3470            })
3471            .unwrap();
3472
3473        cx.background_executor
3474            .advance_clock(SERIALIZATION_THROTTLE_TIME);
3475        cx.update(|_| {});
3476        editor_1.assert_released();
3477        editor_2.assert_released();
3478        buffer.assert_released();
3479    }
3480
3481    #[gpui::test]
3482    async fn test_navigation(cx: &mut TestAppContext) {
3483        let app_state = init_test(cx);
3484        app_state
3485            .fs
3486            .as_fake()
3487            .insert_tree(
3488                path!("/root"),
3489                json!({
3490                    "a": {
3491                        "file1": "contents 1\n".repeat(20),
3492                        "file2": "contents 2\n".repeat(20),
3493                        "file3": "contents 3\n".repeat(20),
3494                    },
3495                }),
3496            )
3497            .await;
3498
3499        let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
3500        project.update(cx, |project, _cx| {
3501            project.languages().add(markdown_language())
3502        });
3503        let workspace =
3504            cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
3505        let pane = workspace
3506            .read_with(cx, |workspace, _| workspace.active_pane().clone())
3507            .unwrap();
3508
3509        let entries = cx.update(|cx| workspace.root(cx).unwrap().file_project_paths(cx));
3510        let file1 = entries[0].clone();
3511        let file2 = entries[1].clone();
3512        let file3 = entries[2].clone();
3513
3514        let editor1 = workspace
3515            .update(cx, |w, window, cx| {
3516                w.open_path(file1.clone(), None, true, window, cx)
3517            })
3518            .unwrap()
3519            .await
3520            .unwrap()
3521            .downcast::<Editor>()
3522            .unwrap();
3523        workspace
3524            .update(cx, |_, window, cx| {
3525                editor1.update(cx, |editor, cx| {
3526                    editor.change_selections(Default::default(), window, cx, |s| {
3527                        s.select_display_ranges([DisplayPoint::new(DisplayRow(10), 0)
3528                            ..DisplayPoint::new(DisplayRow(10), 0)])
3529                    });
3530                });
3531            })
3532            .unwrap();
3533
3534        let editor2 = workspace
3535            .update(cx, |w, window, cx| {
3536                w.open_path(file2.clone(), None, true, window, cx)
3537            })
3538            .unwrap()
3539            .await
3540            .unwrap()
3541            .downcast::<Editor>()
3542            .unwrap();
3543        let editor3 = workspace
3544            .update(cx, |w, window, cx| {
3545                w.open_path(file3.clone(), None, true, window, cx)
3546            })
3547            .unwrap()
3548            .await
3549            .unwrap()
3550            .downcast::<Editor>()
3551            .unwrap();
3552
3553        workspace
3554            .update(cx, |_, window, cx| {
3555                editor3.update(cx, |editor, cx| {
3556                    editor.change_selections(Default::default(), window, cx, |s| {
3557                        s.select_display_ranges([DisplayPoint::new(DisplayRow(12), 0)
3558                            ..DisplayPoint::new(DisplayRow(12), 0)])
3559                    });
3560                    editor.newline(&Default::default(), window, cx);
3561                    editor.newline(&Default::default(), window, cx);
3562                    editor.move_down(&Default::default(), window, cx);
3563                    editor.move_down(&Default::default(), window, cx);
3564                    editor.save(
3565                        SaveOptions {
3566                            format: true,
3567                            autosave: false,
3568                        },
3569                        project.clone(),
3570                        window,
3571                        cx,
3572                    )
3573                })
3574            })
3575            .unwrap()
3576            .await
3577            .unwrap();
3578        workspace
3579            .update(cx, |_, window, cx| {
3580                editor3.update(cx, |editor, cx| {
3581                    editor.set_scroll_position(point(0., 12.5), window, cx)
3582                });
3583            })
3584            .unwrap();
3585        assert_eq!(
3586            active_location(&workspace, cx),
3587            (file3.clone(), DisplayPoint::new(DisplayRow(16), 0), 12.5)
3588        );
3589
3590        workspace
3591            .update(cx, |w, window, cx| {
3592                w.go_back(w.active_pane().downgrade(), window, cx)
3593            })
3594            .unwrap()
3595            .await
3596            .unwrap();
3597        assert_eq!(
3598            active_location(&workspace, cx),
3599            (file3.clone(), DisplayPoint::new(DisplayRow(0), 0), 0.)
3600        );
3601
3602        workspace
3603            .update(cx, |w, window, cx| {
3604                w.go_back(w.active_pane().downgrade(), window, cx)
3605            })
3606            .unwrap()
3607            .await
3608            .unwrap();
3609        assert_eq!(
3610            active_location(&workspace, cx),
3611            (file2.clone(), DisplayPoint::new(DisplayRow(0), 0), 0.)
3612        );
3613
3614        workspace
3615            .update(cx, |w, window, cx| {
3616                w.go_back(w.active_pane().downgrade(), window, cx)
3617            })
3618            .unwrap()
3619            .await
3620            .unwrap();
3621        assert_eq!(
3622            active_location(&workspace, cx),
3623            (file1.clone(), DisplayPoint::new(DisplayRow(10), 0), 0.)
3624        );
3625
3626        workspace
3627            .update(cx, |w, window, cx| {
3628                w.go_back(w.active_pane().downgrade(), window, cx)
3629            })
3630            .unwrap()
3631            .await
3632            .unwrap();
3633        assert_eq!(
3634            active_location(&workspace, cx),
3635            (file1.clone(), DisplayPoint::new(DisplayRow(0), 0), 0.)
3636        );
3637
3638        // Go back one more time and ensure we don't navigate past the first item in the history.
3639        workspace
3640            .update(cx, |w, window, cx| {
3641                w.go_back(w.active_pane().downgrade(), window, cx)
3642            })
3643            .unwrap()
3644            .await
3645            .unwrap();
3646        assert_eq!(
3647            active_location(&workspace, cx),
3648            (file1.clone(), DisplayPoint::new(DisplayRow(0), 0), 0.)
3649        );
3650
3651        workspace
3652            .update(cx, |w, window, cx| {
3653                w.go_forward(w.active_pane().downgrade(), window, cx)
3654            })
3655            .unwrap()
3656            .await
3657            .unwrap();
3658        assert_eq!(
3659            active_location(&workspace, cx),
3660            (file1.clone(), DisplayPoint::new(DisplayRow(10), 0), 0.)
3661        );
3662
3663        workspace
3664            .update(cx, |w, window, cx| {
3665                w.go_forward(w.active_pane().downgrade(), window, cx)
3666            })
3667            .unwrap()
3668            .await
3669            .unwrap();
3670        assert_eq!(
3671            active_location(&workspace, cx),
3672            (file2.clone(), DisplayPoint::new(DisplayRow(0), 0), 0.)
3673        );
3674
3675        // Go forward to an item that has been closed, ensuring it gets re-opened at the same
3676        // location.
3677        workspace
3678            .update(cx, |_, window, cx| {
3679                pane.update(cx, |pane, cx| {
3680                    let editor3_id = editor3.entity_id();
3681                    drop(editor3);
3682                    pane.close_item_by_id(editor3_id, SaveIntent::Close, window, cx)
3683                })
3684            })
3685            .unwrap()
3686            .await
3687            .unwrap();
3688        workspace
3689            .update(cx, |w, window, cx| {
3690                w.go_forward(w.active_pane().downgrade(), window, cx)
3691            })
3692            .unwrap()
3693            .await
3694            .unwrap();
3695        assert_eq!(
3696            active_location(&workspace, cx),
3697            (file3.clone(), DisplayPoint::new(DisplayRow(0), 0), 0.)
3698        );
3699
3700        workspace
3701            .update(cx, |w, window, cx| {
3702                w.go_forward(w.active_pane().downgrade(), window, cx)
3703            })
3704            .unwrap()
3705            .await
3706            .unwrap();
3707        assert_eq!(
3708            active_location(&workspace, cx),
3709            (file3.clone(), DisplayPoint::new(DisplayRow(16), 0), 12.5)
3710        );
3711
3712        workspace
3713            .update(cx, |w, window, cx| {
3714                w.go_back(w.active_pane().downgrade(), window, cx)
3715            })
3716            .unwrap()
3717            .await
3718            .unwrap();
3719        assert_eq!(
3720            active_location(&workspace, cx),
3721            (file3.clone(), DisplayPoint::new(DisplayRow(0), 0), 0.)
3722        );
3723
3724        // Go back to an item that has been closed and removed from disk
3725        workspace
3726            .update(cx, |_, window, cx| {
3727                pane.update(cx, |pane, cx| {
3728                    let editor2_id = editor2.entity_id();
3729                    drop(editor2);
3730                    pane.close_item_by_id(editor2_id, SaveIntent::Close, window, cx)
3731                })
3732            })
3733            .unwrap()
3734            .await
3735            .unwrap();
3736        app_state
3737            .fs
3738            .remove_file(Path::new(path!("/root/a/file2")), Default::default())
3739            .await
3740            .unwrap();
3741        cx.background_executor.run_until_parked();
3742
3743        workspace
3744            .update(cx, |w, window, cx| {
3745                w.go_back(w.active_pane().downgrade(), window, cx)
3746            })
3747            .unwrap()
3748            .await
3749            .unwrap();
3750        assert_eq!(
3751            active_location(&workspace, cx),
3752            (file2.clone(), DisplayPoint::new(DisplayRow(0), 0), 0.)
3753        );
3754        workspace
3755            .update(cx, |w, window, cx| {
3756                w.go_forward(w.active_pane().downgrade(), window, cx)
3757            })
3758            .unwrap()
3759            .await
3760            .unwrap();
3761        assert_eq!(
3762            active_location(&workspace, cx),
3763            (file3.clone(), DisplayPoint::new(DisplayRow(0), 0), 0.)
3764        );
3765
3766        // Modify file to collapse multiple nav history entries into the same location.
3767        // Ensure we don't visit the same location twice when navigating.
3768        workspace
3769            .update(cx, |_, window, cx| {
3770                editor1.update(cx, |editor, cx| {
3771                    editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3772                        s.select_display_ranges([DisplayPoint::new(DisplayRow(15), 0)
3773                            ..DisplayPoint::new(DisplayRow(15), 0)])
3774                    })
3775                });
3776            })
3777            .unwrap();
3778        for _ in 0..5 {
3779            workspace
3780                .update(cx, |_, window, cx| {
3781                    editor1.update(cx, |editor, cx| {
3782                        editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3783                            s.select_display_ranges([DisplayPoint::new(DisplayRow(3), 0)
3784                                ..DisplayPoint::new(DisplayRow(3), 0)])
3785                        });
3786                    });
3787                })
3788                .unwrap();
3789
3790            workspace
3791                .update(cx, |_, window, cx| {
3792                    editor1.update(cx, |editor, cx| {
3793                        editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3794                            s.select_display_ranges([DisplayPoint::new(DisplayRow(13), 0)
3795                                ..DisplayPoint::new(DisplayRow(13), 0)])
3796                        })
3797                    });
3798                })
3799                .unwrap();
3800        }
3801        workspace
3802            .update(cx, |_, window, cx| {
3803                editor1.update(cx, |editor, cx| {
3804                    editor.transact(window, cx, |editor, window, cx| {
3805                        editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3806                            s.select_display_ranges([DisplayPoint::new(DisplayRow(2), 0)
3807                                ..DisplayPoint::new(DisplayRow(14), 0)])
3808                        });
3809                        editor.insert("", window, cx);
3810                    })
3811                });
3812            })
3813            .unwrap();
3814
3815        workspace
3816            .update(cx, |_, window, cx| {
3817                editor1.update(cx, |editor, cx| {
3818                    editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3819                        s.select_display_ranges([DisplayPoint::new(DisplayRow(1), 0)
3820                            ..DisplayPoint::new(DisplayRow(1), 0)])
3821                    })
3822                });
3823            })
3824            .unwrap();
3825        workspace
3826            .update(cx, |w, window, cx| {
3827                w.go_back(w.active_pane().downgrade(), window, cx)
3828            })
3829            .unwrap()
3830            .await
3831            .unwrap();
3832        assert_eq!(
3833            active_location(&workspace, cx),
3834            (file1.clone(), DisplayPoint::new(DisplayRow(2), 0), 0.)
3835        );
3836        workspace
3837            .update(cx, |w, window, cx| {
3838                w.go_back(w.active_pane().downgrade(), window, cx)
3839            })
3840            .unwrap()
3841            .await
3842            .unwrap();
3843        assert_eq!(
3844            active_location(&workspace, cx),
3845            (file1.clone(), DisplayPoint::new(DisplayRow(3), 0), 0.)
3846        );
3847
3848        fn active_location(
3849            workspace: &WindowHandle<Workspace>,
3850            cx: &mut TestAppContext,
3851        ) -> (ProjectPath, DisplayPoint, f32) {
3852            workspace
3853                .update(cx, |workspace, _, cx| {
3854                    let item = workspace.active_item(cx).unwrap();
3855                    let editor = item.downcast::<Editor>().unwrap();
3856                    let (selections, scroll_position) = editor.update(cx, |editor, cx| {
3857                        (
3858                            editor.selections.display_ranges(cx),
3859                            editor.scroll_position(cx),
3860                        )
3861                    });
3862                    (
3863                        item.project_path(cx).unwrap(),
3864                        selections[0].start,
3865                        scroll_position.y,
3866                    )
3867                })
3868                .unwrap()
3869        }
3870    }
3871
3872    #[gpui::test]
3873    async fn test_reopening_closed_items(cx: &mut TestAppContext) {
3874        let app_state = init_test(cx);
3875        app_state
3876            .fs
3877            .as_fake()
3878            .insert_tree(
3879                path!("/root"),
3880                json!({
3881                    "a": {
3882                        "file1": "",
3883                        "file2": "",
3884                        "file3": "",
3885                        "file4": "",
3886                    },
3887                }),
3888            )
3889            .await;
3890
3891        let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
3892        project.update(cx, |project, _cx| {
3893            project.languages().add(markdown_language())
3894        });
3895        let workspace = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
3896        let pane = workspace
3897            .read_with(cx, |workspace, _| workspace.active_pane().clone())
3898            .unwrap();
3899
3900        let entries = cx.update(|cx| workspace.root(cx).unwrap().file_project_paths(cx));
3901        let file1 = entries[0].clone();
3902        let file2 = entries[1].clone();
3903        let file3 = entries[2].clone();
3904        let file4 = entries[3].clone();
3905
3906        let file1_item_id = workspace
3907            .update(cx, |w, window, cx| {
3908                w.open_path(file1.clone(), None, true, window, cx)
3909            })
3910            .unwrap()
3911            .await
3912            .unwrap()
3913            .item_id();
3914        let file2_item_id = workspace
3915            .update(cx, |w, window, cx| {
3916                w.open_path(file2.clone(), None, true, window, cx)
3917            })
3918            .unwrap()
3919            .await
3920            .unwrap()
3921            .item_id();
3922        let file3_item_id = workspace
3923            .update(cx, |w, window, cx| {
3924                w.open_path(file3.clone(), None, true, window, cx)
3925            })
3926            .unwrap()
3927            .await
3928            .unwrap()
3929            .item_id();
3930        let file4_item_id = workspace
3931            .update(cx, |w, window, cx| {
3932                w.open_path(file4.clone(), None, true, window, cx)
3933            })
3934            .unwrap()
3935            .await
3936            .unwrap()
3937            .item_id();
3938        assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
3939
3940        // Close all the pane items in some arbitrary order.
3941        workspace
3942            .update(cx, |_, window, cx| {
3943                pane.update(cx, |pane, cx| {
3944                    pane.close_item_by_id(file1_item_id, SaveIntent::Close, window, cx)
3945                })
3946            })
3947            .unwrap()
3948            .await
3949            .unwrap();
3950        assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
3951
3952        workspace
3953            .update(cx, |_, window, cx| {
3954                pane.update(cx, |pane, cx| {
3955                    pane.close_item_by_id(file4_item_id, SaveIntent::Close, window, cx)
3956                })
3957            })
3958            .unwrap()
3959            .await
3960            .unwrap();
3961        assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
3962
3963        workspace
3964            .update(cx, |_, window, cx| {
3965                pane.update(cx, |pane, cx| {
3966                    pane.close_item_by_id(file2_item_id, SaveIntent::Close, window, cx)
3967                })
3968            })
3969            .unwrap()
3970            .await
3971            .unwrap();
3972        assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
3973        workspace
3974            .update(cx, |_, window, cx| {
3975                pane.update(cx, |pane, cx| {
3976                    pane.close_item_by_id(file3_item_id, SaveIntent::Close, window, cx)
3977                })
3978            })
3979            .unwrap()
3980            .await
3981            .unwrap();
3982
3983        assert_eq!(active_path(&workspace, cx), None);
3984
3985        // Reopen all the closed items, ensuring they are reopened in the same order
3986        // in which they were closed.
3987        workspace
3988            .update(cx, Workspace::reopen_closed_item)
3989            .unwrap()
3990            .await
3991            .unwrap();
3992        assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
3993
3994        workspace
3995            .update(cx, Workspace::reopen_closed_item)
3996            .unwrap()
3997            .await
3998            .unwrap();
3999        assert_eq!(active_path(&workspace, cx), Some(file2.clone()));
4000
4001        workspace
4002            .update(cx, Workspace::reopen_closed_item)
4003            .unwrap()
4004            .await
4005            .unwrap();
4006        assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
4007
4008        workspace
4009            .update(cx, Workspace::reopen_closed_item)
4010            .unwrap()
4011            .await
4012            .unwrap();
4013        assert_eq!(active_path(&workspace, cx), Some(file1.clone()));
4014
4015        // Reopening past the last closed item is a no-op.
4016        workspace
4017            .update(cx, Workspace::reopen_closed_item)
4018            .unwrap()
4019            .await
4020            .unwrap();
4021        assert_eq!(active_path(&workspace, cx), Some(file1.clone()));
4022
4023        // Reopening closed items doesn't interfere with navigation history.
4024        workspace
4025            .update(cx, |workspace, window, cx| {
4026                workspace.go_back(workspace.active_pane().downgrade(), window, cx)
4027            })
4028            .unwrap()
4029            .await
4030            .unwrap();
4031        assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
4032
4033        workspace
4034            .update(cx, |workspace, window, cx| {
4035                workspace.go_back(workspace.active_pane().downgrade(), window, cx)
4036            })
4037            .unwrap()
4038            .await
4039            .unwrap();
4040        assert_eq!(active_path(&workspace, cx), Some(file2.clone()));
4041
4042        workspace
4043            .update(cx, |workspace, window, cx| {
4044                workspace.go_back(workspace.active_pane().downgrade(), window, cx)
4045            })
4046            .unwrap()
4047            .await
4048            .unwrap();
4049        assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
4050
4051        workspace
4052            .update(cx, |workspace, window, cx| {
4053                workspace.go_back(workspace.active_pane().downgrade(), window, cx)
4054            })
4055            .unwrap()
4056            .await
4057            .unwrap();
4058        assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
4059
4060        workspace
4061            .update(cx, |workspace, window, cx| {
4062                workspace.go_back(workspace.active_pane().downgrade(), window, cx)
4063            })
4064            .unwrap()
4065            .await
4066            .unwrap();
4067        assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
4068
4069        workspace
4070            .update(cx, |workspace, window, cx| {
4071                workspace.go_back(workspace.active_pane().downgrade(), window, cx)
4072            })
4073            .unwrap()
4074            .await
4075            .unwrap();
4076        assert_eq!(active_path(&workspace, cx), Some(file2.clone()));
4077
4078        workspace
4079            .update(cx, |workspace, window, cx| {
4080                workspace.go_back(workspace.active_pane().downgrade(), window, cx)
4081            })
4082            .unwrap()
4083            .await
4084            .unwrap();
4085        assert_eq!(active_path(&workspace, cx), Some(file1.clone()));
4086
4087        workspace
4088            .update(cx, |workspace, window, cx| {
4089                workspace.go_back(workspace.active_pane().downgrade(), window, cx)
4090            })
4091            .unwrap()
4092            .await
4093            .unwrap();
4094        assert_eq!(active_path(&workspace, cx), Some(file1.clone()));
4095
4096        fn active_path(
4097            workspace: &WindowHandle<Workspace>,
4098            cx: &TestAppContext,
4099        ) -> Option<ProjectPath> {
4100            workspace
4101                .read_with(cx, |workspace, cx| {
4102                    let item = workspace.active_item(cx)?;
4103                    item.project_path(cx)
4104                })
4105                .unwrap()
4106        }
4107    }
4108
4109    fn init_keymap_test(cx: &mut TestAppContext) -> Arc<AppState> {
4110        cx.update(|cx| {
4111            let app_state = AppState::test(cx);
4112
4113            theme::init(theme::LoadThemes::JustBase, cx);
4114            client::init(&app_state.client, cx);
4115            language::init(cx);
4116            workspace::init(app_state.clone(), cx);
4117            onboarding::init(cx);
4118            Project::init_settings(cx);
4119            app_state
4120        })
4121    }
4122
4123    actions!(test_only, [ActionA, ActionB]);
4124
4125    #[gpui::test]
4126    async fn test_base_keymap(cx: &mut gpui::TestAppContext) {
4127        let executor = cx.executor();
4128        let app_state = init_keymap_test(cx);
4129        let project = Project::test(app_state.fs.clone(), [], cx).await;
4130        let workspace =
4131            cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
4132
4133        // From the Atom keymap
4134        use workspace::ActivatePreviousPane;
4135        // From the JetBrains keymap
4136        use workspace::ActivatePreviousItem;
4137
4138        app_state
4139            .fs
4140            .save(
4141                "/settings.json".as_ref(),
4142                &r#"{"base_keymap": "Atom"}"#.into(),
4143                Default::default(),
4144            )
4145            .await
4146            .unwrap();
4147
4148        app_state
4149            .fs
4150            .save(
4151                "/keymap.json".as_ref(),
4152                &r#"[{"bindings": {"backspace": "test_only::ActionA"}}]"#.into(),
4153                Default::default(),
4154            )
4155            .await
4156            .unwrap();
4157        executor.run_until_parked();
4158        cx.update(|cx| {
4159            let settings_rx = watch_config_file(
4160                &executor,
4161                app_state.fs.clone(),
4162                PathBuf::from("/settings.json"),
4163            );
4164            let keymap_rx = watch_config_file(
4165                &executor,
4166                app_state.fs.clone(),
4167                PathBuf::from("/keymap.json"),
4168            );
4169            let global_settings_rx = watch_config_file(
4170                &executor,
4171                app_state.fs.clone(),
4172                PathBuf::from("/global_settings.json"),
4173            );
4174            handle_settings_file_changes(settings_rx, global_settings_rx, cx, |_, _| {});
4175            handle_keymap_file_changes(keymap_rx, cx);
4176        });
4177        workspace
4178            .update(cx, |workspace, _, cx| {
4179                workspace.register_action(|_, _: &ActionA, _window, _cx| {});
4180                workspace.register_action(|_, _: &ActionB, _window, _cx| {});
4181                workspace.register_action(|_, _: &ActivatePreviousPane, _window, _cx| {});
4182                workspace.register_action(|_, _: &ActivatePreviousItem, _window, _cx| {});
4183                cx.notify();
4184            })
4185            .unwrap();
4186        executor.run_until_parked();
4187        // Test loading the keymap base at all
4188        assert_key_bindings_for(
4189            workspace.into(),
4190            cx,
4191            vec![("backspace", &ActionA), ("k", &ActivatePreviousPane)],
4192            line!(),
4193        );
4194
4195        // Test modifying the users keymap, while retaining the base keymap
4196        app_state
4197            .fs
4198            .save(
4199                "/keymap.json".as_ref(),
4200                &r#"[{"bindings": {"backspace": "test_only::ActionB"}}]"#.into(),
4201                Default::default(),
4202            )
4203            .await
4204            .unwrap();
4205
4206        executor.run_until_parked();
4207
4208        assert_key_bindings_for(
4209            workspace.into(),
4210            cx,
4211            vec![("backspace", &ActionB), ("k", &ActivatePreviousPane)],
4212            line!(),
4213        );
4214
4215        // Test modifying the base, while retaining the users keymap
4216        app_state
4217            .fs
4218            .save(
4219                "/settings.json".as_ref(),
4220                &r#"{"base_keymap": "JetBrains"}"#.into(),
4221                Default::default(),
4222            )
4223            .await
4224            .unwrap();
4225
4226        executor.run_until_parked();
4227
4228        assert_key_bindings_for(
4229            workspace.into(),
4230            cx,
4231            vec![("backspace", &ActionB), ("{", &ActivatePreviousItem)],
4232            line!(),
4233        );
4234    }
4235
4236    #[gpui::test]
4237    async fn test_disabled_keymap_binding(cx: &mut gpui::TestAppContext) {
4238        let executor = cx.executor();
4239        let app_state = init_keymap_test(cx);
4240        let project = Project::test(app_state.fs.clone(), [], cx).await;
4241        let workspace =
4242            cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
4243
4244        // From the Atom keymap
4245        use workspace::ActivatePreviousPane;
4246        // From the JetBrains keymap
4247        use diagnostics::Deploy;
4248
4249        workspace
4250            .update(cx, |workspace, _, _| {
4251                workspace.register_action(|_, _: &ActionA, _window, _cx| {});
4252                workspace.register_action(|_, _: &ActionB, _window, _cx| {});
4253                workspace.register_action(|_, _: &Deploy, _window, _cx| {});
4254            })
4255            .unwrap();
4256        app_state
4257            .fs
4258            .save(
4259                "/settings.json".as_ref(),
4260                &r#"{"base_keymap": "Atom"}"#.into(),
4261                Default::default(),
4262            )
4263            .await
4264            .unwrap();
4265        app_state
4266            .fs
4267            .save(
4268                "/keymap.json".as_ref(),
4269                &r#"[{"bindings": {"backspace": "test_only::ActionA"}}]"#.into(),
4270                Default::default(),
4271            )
4272            .await
4273            .unwrap();
4274
4275        cx.update(|cx| {
4276            let settings_rx = watch_config_file(
4277                &executor,
4278                app_state.fs.clone(),
4279                PathBuf::from("/settings.json"),
4280            );
4281            let keymap_rx = watch_config_file(
4282                &executor,
4283                app_state.fs.clone(),
4284                PathBuf::from("/keymap.json"),
4285            );
4286
4287            let global_settings_rx = watch_config_file(
4288                &executor,
4289                app_state.fs.clone(),
4290                PathBuf::from("/global_settings.json"),
4291            );
4292            handle_settings_file_changes(settings_rx, global_settings_rx, cx, |_, _| {});
4293            handle_keymap_file_changes(keymap_rx, cx);
4294        });
4295
4296        cx.background_executor.run_until_parked();
4297
4298        cx.background_executor.run_until_parked();
4299        // Test loading the keymap base at all
4300        assert_key_bindings_for(
4301            workspace.into(),
4302            cx,
4303            vec![("backspace", &ActionA), ("k", &ActivatePreviousPane)],
4304            line!(),
4305        );
4306
4307        // Test disabling the key binding for the base keymap
4308        app_state
4309            .fs
4310            .save(
4311                "/keymap.json".as_ref(),
4312                &r#"[{"bindings": {"backspace": null}}]"#.into(),
4313                Default::default(),
4314            )
4315            .await
4316            .unwrap();
4317
4318        cx.background_executor.run_until_parked();
4319
4320        assert_key_bindings_for(
4321            workspace.into(),
4322            cx,
4323            vec![("k", &ActivatePreviousPane)],
4324            line!(),
4325        );
4326
4327        // Test modifying the base, while retaining the users keymap
4328        app_state
4329            .fs
4330            .save(
4331                "/settings.json".as_ref(),
4332                &r#"{"base_keymap": "JetBrains"}"#.into(),
4333                Default::default(),
4334            )
4335            .await
4336            .unwrap();
4337
4338        cx.background_executor.run_until_parked();
4339
4340        assert_key_bindings_for(workspace.into(), cx, vec![("6", &Deploy)], line!());
4341    }
4342
4343    #[gpui::test]
4344    async fn test_generate_keymap_json_schema_for_registered_actions(
4345        cx: &mut gpui::TestAppContext,
4346    ) {
4347        init_keymap_test(cx);
4348        cx.update(|cx| {
4349            // Make sure it doesn't panic.
4350            KeymapFile::generate_json_schema_for_registered_actions(cx);
4351        });
4352    }
4353
4354    /// Actions that don't build from empty input won't work from command palette invocation.
4355    #[gpui::test]
4356    async fn test_actions_build_with_empty_input(cx: &mut gpui::TestAppContext) {
4357        init_keymap_test(cx);
4358        cx.update(|cx| {
4359            let all_actions = cx.all_action_names();
4360            let mut failing_names = Vec::new();
4361            let mut errors = Vec::new();
4362            for action in all_actions {
4363                match action.to_string().as_str() {
4364                    "vim::FindCommand"
4365                    | "vim::Literal"
4366                    | "vim::ResizePane"
4367                    | "vim::PushObject"
4368                    | "vim::PushFindForward"
4369                    | "vim::PushFindBackward"
4370                    | "vim::PushSneak"
4371                    | "vim::PushSneakBackward"
4372                    | "vim::PushChangeSurrounds"
4373                    | "vim::PushJump"
4374                    | "vim::PushDigraph"
4375                    | "vim::PushLiteral"
4376                    | "vim::PushHelixNext"
4377                    | "vim::PushHelixPrevious"
4378                    | "vim::Number"
4379                    | "vim::SelectRegister"
4380                    | "git::StageAndNext"
4381                    | "git::UnstageAndNext"
4382                    | "terminal::SendText"
4383                    | "terminal::SendKeystroke"
4384                    | "app_menu::OpenApplicationMenu"
4385                    | "picker::ConfirmInput"
4386                    | "editor::HandleInput"
4387                    | "editor::FoldAtLevel"
4388                    | "pane::ActivateItem"
4389                    | "workspace::ActivatePane"
4390                    | "workspace::MoveItemToPane"
4391                    | "workspace::MoveItemToPaneInDirection"
4392                    | "workspace::OpenTerminal"
4393                    | "workspace::SendKeystrokes"
4394                    | "agent::NewNativeAgentThreadFromSummary"
4395                    | "zed::OpenBrowser"
4396                    | "zed::OpenZedUrl" => {}
4397                    _ => {
4398                        let result = cx.build_action(action, None);
4399                        match &result {
4400                            Ok(_) => {}
4401                            Err(err) => {
4402                                failing_names.push(action);
4403                                errors.push(format!("{action} failed to build: {err:?}"));
4404                            }
4405                        }
4406                    }
4407                }
4408            }
4409            if !errors.is_empty() {
4410                panic!(
4411                    "Failed to build actions using {{}} as input: {:?}. Errors:\n{}",
4412                    failing_names,
4413                    errors.join("\n")
4414                );
4415            }
4416        });
4417    }
4418
4419    /// Checks that action namespaces are the expected set. The purpose of this is to prevent typos
4420    /// and let you know when introducing a new namespace.
4421    #[gpui::test]
4422    async fn test_action_namespaces(cx: &mut gpui::TestAppContext) {
4423        use itertools::Itertools;
4424
4425        init_keymap_test(cx);
4426        cx.update(|cx| {
4427            let all_actions = cx.all_action_names();
4428
4429            let mut actions_without_namespace = Vec::new();
4430            let all_namespaces = all_actions
4431                .iter()
4432                .filter_map(|action_name| {
4433                    let namespace = action_name
4434                        .split("::")
4435                        .collect::<Vec<_>>()
4436                        .into_iter()
4437                        .rev()
4438                        .skip(1)
4439                        .rev()
4440                        .join("::");
4441                    if namespace.is_empty() {
4442                        actions_without_namespace.push(*action_name);
4443                    }
4444                    if &namespace == "test_only" || &namespace == "stories" {
4445                        None
4446                    } else {
4447                        Some(namespace)
4448                    }
4449                })
4450                .sorted()
4451                .dedup()
4452                .collect::<Vec<_>>();
4453            assert_eq!(actions_without_namespace, Vec::<&str>::new());
4454
4455            let expected_namespaces = vec![
4456                "activity_indicator",
4457                "agent",
4458                #[cfg(not(target_os = "macos"))]
4459                "app_menu",
4460                "assistant",
4461                "assistant2",
4462                "auto_update",
4463                "branches",
4464                "buffer_search",
4465                "channel_modal",
4466                "cli",
4467                "client",
4468                "collab",
4469                "collab_panel",
4470                "command_palette",
4471                "console",
4472                "context_server",
4473                "copilot",
4474                "debug_panel",
4475                "debugger",
4476                "dev",
4477                "diagnostics",
4478                "edit_prediction",
4479                "editor",
4480                "feedback",
4481                "file_finder",
4482                "git",
4483                "git_onboarding",
4484                "git_panel",
4485                "go_to_line",
4486                "icon_theme_selector",
4487                "journal",
4488                "keymap_editor",
4489                "keystroke_input",
4490                "language_selector",
4491                "line_ending",
4492                "lsp_tool",
4493                "markdown",
4494                "menu",
4495                "notebook",
4496                "notification_panel",
4497                "onboarding",
4498                "outline",
4499                "outline_panel",
4500                "pane",
4501                "panel",
4502                "picker",
4503                "project_panel",
4504                "project_search",
4505                "project_symbols",
4506                "projects",
4507                "repl",
4508                "rules_library",
4509                "search",
4510                "settings_profile_selector",
4511                "snippets",
4512                "stash_picker",
4513                "supermaven",
4514                "svg",
4515                "syntax_tree_view",
4516                "tab_switcher",
4517                "task",
4518                "terminal",
4519                "terminal_panel",
4520                "theme_selector",
4521                "toast",
4522                "toolchain",
4523                "variable_list",
4524                "vim",
4525                "window",
4526                "workspace",
4527                "zed",
4528                "zed_predict_onboarding",
4529                "zeta",
4530            ];
4531            assert_eq!(
4532                all_namespaces,
4533                expected_namespaces
4534                    .into_iter()
4535                    .map(|namespace| namespace.to_string())
4536                    .sorted()
4537                    .collect::<Vec<_>>()
4538            );
4539        });
4540    }
4541
4542    #[gpui::test]
4543    fn test_bundled_settings_and_themes(cx: &mut App) {
4544        cx.text_system()
4545            .add_fonts(vec![
4546                Assets
4547                    .load("fonts/lilex/Lilex-Regular.ttf")
4548                    .unwrap()
4549                    .unwrap(),
4550                Assets
4551                    .load("fonts/ibm-plex-sans/IBMPlexSans-Regular.ttf")
4552                    .unwrap()
4553                    .unwrap(),
4554            ])
4555            .unwrap();
4556        let themes = ThemeRegistry::default();
4557        settings::init(cx);
4558        theme::init(theme::LoadThemes::JustBase, cx);
4559
4560        let mut has_default_theme = false;
4561        for theme_name in themes.list().into_iter().map(|meta| meta.name) {
4562            let theme = themes.get(&theme_name).unwrap();
4563            assert_eq!(theme.name, theme_name);
4564            if theme.name == ThemeSettings::get(None, cx).active_theme.name {
4565                has_default_theme = true;
4566            }
4567        }
4568        assert!(has_default_theme);
4569    }
4570
4571    #[gpui::test]
4572    async fn test_bundled_files_editor(cx: &mut TestAppContext) {
4573        let app_state = init_test(cx);
4574        cx.update(init);
4575
4576        let project = Project::test(app_state.fs.clone(), [], cx).await;
4577        let _window = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
4578
4579        cx.update(|cx| {
4580            cx.dispatch_action(&OpenDefaultSettings);
4581        });
4582        cx.run_until_parked();
4583
4584        assert_eq!(cx.read(|cx| cx.windows().len()), 1);
4585
4586        let workspace = cx.windows()[0].downcast::<Workspace>().unwrap();
4587        let active_editor = workspace
4588            .update(cx, |workspace, _, cx| {
4589                workspace.active_item_as::<Editor>(cx)
4590            })
4591            .unwrap();
4592        assert!(
4593            active_editor.is_some(),
4594            "Settings action should have opened an editor with the default file contents"
4595        );
4596
4597        let active_editor = active_editor.unwrap();
4598        assert!(
4599            active_editor.read_with(cx, |editor, cx| editor.read_only(cx)),
4600            "Default settings should be readonly"
4601        );
4602        assert!(
4603            active_editor.read_with(cx, |editor, cx| editor.buffer().read(cx).read_only()),
4604            "The underlying buffer should also be readonly for the shipped default settings"
4605        );
4606    }
4607
4608    #[gpui::test]
4609    async fn test_bundled_languages(cx: &mut TestAppContext) {
4610        let fs = fs::FakeFs::new(cx.background_executor.clone());
4611        env_logger::builder().is_test(true).try_init().ok();
4612        let settings = cx.update(SettingsStore::test);
4613        cx.set_global(settings);
4614        let languages = LanguageRegistry::test(cx.executor());
4615        let languages = Arc::new(languages);
4616        let node_runtime = node_runtime::NodeRuntime::unavailable();
4617        cx.update(|cx| {
4618            languages::init(languages.clone(), fs, node_runtime, cx);
4619        });
4620        for name in languages.language_names() {
4621            languages
4622                .language_for_name(name.as_ref())
4623                .await
4624                .with_context(|| format!("language name {name}"))
4625                .unwrap();
4626        }
4627        cx.run_until_parked();
4628    }
4629
4630    pub(crate) fn init_test(cx: &mut TestAppContext) -> Arc<AppState> {
4631        init_test_with_state(cx, cx.update(AppState::test))
4632    }
4633
4634    fn init_test_with_state(
4635        cx: &mut TestAppContext,
4636        mut app_state: Arc<AppState>,
4637    ) -> Arc<AppState> {
4638        cx.update(move |cx| {
4639            env_logger::builder().is_test(true).try_init().ok();
4640
4641            let state = Arc::get_mut(&mut app_state).unwrap();
4642            state.build_window_options = build_window_options;
4643
4644            app_state.languages.add(markdown_language());
4645
4646            gpui_tokio::init(cx);
4647            vim_mode_setting::init(cx);
4648            theme::init(theme::LoadThemes::JustBase, cx);
4649            audio::init(cx);
4650            channel::init(&app_state.client, app_state.user_store.clone(), cx);
4651            call::init(app_state.client.clone(), app_state.user_store.clone(), cx);
4652            notifications::init(app_state.client.clone(), app_state.user_store.clone(), cx);
4653            workspace::init(app_state.clone(), cx);
4654            Project::init_settings(cx);
4655            release_channel::init(SemanticVersion::default(), cx);
4656            command_palette::init(cx);
4657            language::init(cx);
4658            editor::init(cx);
4659            collab_ui::init(&app_state, cx);
4660            git_ui::init(cx);
4661            project_panel::init(cx);
4662            outline_panel::init(cx);
4663            terminal_view::init(cx);
4664            copilot::copilot_chat::init(
4665                app_state.fs.clone(),
4666                app_state.client.http_client(),
4667                copilot::copilot_chat::CopilotChatConfiguration::default(),
4668                cx,
4669            );
4670            image_viewer::init(cx);
4671            language_model::init(app_state.client.clone(), cx);
4672            language_models::init(app_state.user_store.clone(), app_state.client.clone(), cx);
4673            web_search::init(cx);
4674            web_search_providers::init(app_state.client.clone(), cx);
4675            let prompt_builder = PromptBuilder::load(app_state.fs.clone(), false, cx);
4676            agent_ui::init(
4677                app_state.fs.clone(),
4678                app_state.client.clone(),
4679                prompt_builder.clone(),
4680                app_state.languages.clone(),
4681                false,
4682                cx,
4683            );
4684            repl::init(app_state.fs.clone(), cx);
4685            repl::notebook::init(cx);
4686            tasks_ui::init(cx);
4687            project::debugger::breakpoint_store::BreakpointStore::init(
4688                &app_state.client.clone().into(),
4689            );
4690            project::debugger::dap_store::DapStore::init(&app_state.client.clone().into(), cx);
4691            debugger_ui::init(cx);
4692            initialize_workspace(app_state.clone(), prompt_builder, cx);
4693            search::init(cx);
4694            app_state
4695        })
4696    }
4697
4698    fn rust_lang() -> Arc<language::Language> {
4699        Arc::new(language::Language::new(
4700            language::LanguageConfig {
4701                name: "Rust".into(),
4702                matcher: LanguageMatcher {
4703                    path_suffixes: vec!["rs".to_string()],
4704                    ..Default::default()
4705                },
4706                ..Default::default()
4707            },
4708            Some(tree_sitter_rust::LANGUAGE.into()),
4709        ))
4710    }
4711
4712    fn markdown_language() -> Arc<language::Language> {
4713        Arc::new(language::Language::new(
4714            language::LanguageConfig {
4715                name: "Markdown".into(),
4716                matcher: LanguageMatcher {
4717                    path_suffixes: vec!["md".to_string()],
4718                    ..Default::default()
4719                },
4720                ..Default::default()
4721            },
4722            Some(tree_sitter_md::LANGUAGE.into()),
4723        ))
4724    }
4725
4726    #[track_caller]
4727    fn assert_key_bindings_for(
4728        window: AnyWindowHandle,
4729        cx: &TestAppContext,
4730        actions: Vec<(&'static str, &dyn Action)>,
4731        line: u32,
4732    ) {
4733        let available_actions = cx
4734            .update(|cx| window.update(cx, |_, window, cx| window.available_actions(cx)))
4735            .unwrap();
4736        for (key, action) in actions {
4737            let bindings = cx
4738                .update(|cx| window.update(cx, |_, window, _| window.bindings_for_action(action)))
4739                .unwrap();
4740            // assert that...
4741            assert!(
4742                available_actions.iter().any(|bound_action| {
4743                    // actions match...
4744                    bound_action.partial_eq(action)
4745                }),
4746                "On {} Failed to find {}",
4747                line,
4748                action.name(),
4749            );
4750            assert!(
4751                // and key strokes contain the given key
4752                bindings
4753                    .into_iter()
4754                    .any(|binding| binding.keystrokes().iter().any(|k| k.key() == key)),
4755                "On {} Failed to find {} with key binding {}",
4756                line,
4757                action.name(),
4758                key
4759            );
4760        }
4761    }
4762
4763    #[gpui::test]
4764    async fn test_opening_project_settings_when_excluded(cx: &mut gpui::TestAppContext) {
4765        // Use the proper initialization for runtime state
4766        let app_state = init_keymap_test(cx);
4767
4768        eprintln!("Running test_opening_project_settings_when_excluded");
4769
4770        // 1. Set up a project with some project settings
4771        let settings_init =
4772            r#"{ "UNIQUEVALUE": true, "git": { "inline_blame": { "enabled": false } } }"#;
4773        app_state
4774            .fs
4775            .as_fake()
4776            .insert_tree(
4777                Path::new("/root"),
4778                json!({
4779                    ".zed": {
4780                        "settings.json": settings_init
4781                    }
4782                }),
4783            )
4784            .await;
4785
4786        eprintln!("Created project with .zed/settings.json containing UNIQUEVALUE");
4787
4788        // 2. Create a project with the file system and load it
4789        let project = Project::test(app_state.fs.clone(), [Path::new("/root")], cx).await;
4790
4791        // Save original settings content for comparison
4792        let original_settings = app_state
4793            .fs
4794            .load(Path::new("/root/.zed/settings.json"))
4795            .await
4796            .unwrap();
4797
4798        let original_settings_str = original_settings.clone();
4799
4800        // Verify settings exist on disk and have expected content
4801        eprintln!("Original settings content: {}", original_settings_str);
4802        assert!(
4803            original_settings_str.contains("UNIQUEVALUE"),
4804            "Test setup failed - settings file doesn't contain our marker"
4805        );
4806
4807        // 3. Add .zed to file scan exclusions in user settings
4808        cx.update_global::<SettingsStore, _>(|store, cx| {
4809            store.update_user_settings(cx, |worktree_settings| {
4810                worktree_settings.project.worktree.file_scan_exclusions =
4811                    Some(vec![".zed".to_string()]);
4812            });
4813        });
4814
4815        eprintln!("Added .zed to file_scan_exclusions in settings");
4816
4817        // 4. Run tasks to apply settings
4818        cx.background_executor.run_until_parked();
4819
4820        // 5. Critical: Verify .zed is actually excluded from worktree
4821        let worktree = cx.update(|cx| project.read(cx).worktrees(cx).next().unwrap());
4822
4823        let has_zed_entry =
4824            cx.update(|cx| worktree.read(cx).entry_for_path(rel_path(".zed")).is_some());
4825
4826        eprintln!(
4827            "Is .zed directory visible in worktree after exclusion: {}",
4828            has_zed_entry
4829        );
4830
4831        // This assertion verifies the test is set up correctly to show the bug
4832        // If .zed is not excluded, the test will fail here
4833        assert!(
4834            !has_zed_entry,
4835            "Test precondition failed: .zed directory should be excluded but was found in worktree"
4836        );
4837
4838        // 6. Create workspace and trigger the actual function that causes the bug
4839        let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
4840        window
4841            .update(cx, |workspace, window, cx| {
4842                // Call the exact function that contains the bug
4843                eprintln!("About to call open_project_settings_file");
4844                open_project_settings_file(workspace, &OpenProjectSettings, window, cx);
4845            })
4846            .unwrap();
4847
4848        // 7. Run background tasks until completion
4849        cx.background_executor.run_until_parked();
4850
4851        // 8. Verify file contents after calling function
4852        let new_content = app_state
4853            .fs
4854            .load(Path::new("/root/.zed/settings.json"))
4855            .await
4856            .unwrap();
4857
4858        let new_content_str = new_content;
4859        eprintln!("New settings content: {}", new_content_str);
4860
4861        // The bug causes the settings to be overwritten with empty settings
4862        // So if the unique value is no longer present, the bug has been reproduced
4863        let bug_exists = !new_content_str.contains("UNIQUEVALUE");
4864        eprintln!("Bug reproduced: {}", bug_exists);
4865
4866        // This assertion should fail if the bug exists - showing the bug is real
4867        assert!(
4868            new_content_str.contains("UNIQUEVALUE"),
4869            "BUG FOUND: Project settings were overwritten when opening via command - original custom content was lost"
4870        );
4871    }
4872}