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