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