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