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