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