zed.rs

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