zed.rs

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