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