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