zed.rs

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