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(
 948        PromptLevel::Info,
 949        &message,
 950        detail.as_deref(),
 951        &["Copy", "OK"],
 952        cx,
 953    );
 954    cx.spawn(async move |_, cx| {
 955        if let Ok(0) = prompt.await {
 956            let content = format!("{}\n{}", message, detail.as_deref().unwrap_or(""));
 957            cx.update(|cx| {
 958                cx.write_to_clipboard(gpui::ClipboardItem::new_string(content));
 959            })
 960            .ok();
 961        }
 962    })
 963    .detach();
 964}
 965
 966fn test_panic(_: &TestPanic, _: &mut App) {
 967    panic!("Ran the TestPanic action")
 968}
 969
 970fn install_cli(
 971    _: &mut Workspace,
 972    _: &install_cli::Install,
 973    window: &mut Window,
 974    cx: &mut Context<Workspace>,
 975) {
 976    install_cli::install_cli(window, cx);
 977}
 978
 979static WAITING_QUIT_CONFIRMATION: AtomicBool = AtomicBool::new(false);
 980fn quit(_: &Quit, cx: &mut App) {
 981    if WAITING_QUIT_CONFIRMATION.load(atomic::Ordering::Acquire) {
 982        return;
 983    }
 984
 985    let should_confirm = WorkspaceSettings::get_global(cx).confirm_quit;
 986    cx.spawn(async move |cx| {
 987        let mut workspace_windows = cx.update(|cx| {
 988            cx.windows()
 989                .into_iter()
 990                .filter_map(|window| window.downcast::<Workspace>())
 991                .collect::<Vec<_>>()
 992        })?;
 993
 994        // If multiple windows have unsaved changes, and need a save prompt,
 995        // prompt in the active window before switching to a different window.
 996        cx.update(|cx| {
 997            workspace_windows.sort_by_key(|window| window.is_active(cx) == Some(false));
 998        })
 999        .log_err();
1000
1001        if should_confirm {
1002            if let Some(workspace) = workspace_windows.first() {
1003                let answer = workspace
1004                    .update(cx, |_, window, cx| {
1005                        window.prompt(
1006                            PromptLevel::Info,
1007                            "Are you sure you want to quit?",
1008                            None,
1009                            &["Quit", "Cancel"],
1010                            cx,
1011                        )
1012                    })
1013                    .log_err();
1014
1015                if let Some(answer) = answer {
1016                    WAITING_QUIT_CONFIRMATION.store(true, atomic::Ordering::Release);
1017                    let answer = answer.await.ok();
1018                    WAITING_QUIT_CONFIRMATION.store(false, atomic::Ordering::Release);
1019                    if answer != Some(0) {
1020                        return Ok(());
1021                    }
1022                }
1023            }
1024        }
1025
1026        // If the user cancels any save prompt, then keep the app open.
1027        for window in workspace_windows {
1028            if let Some(should_close) = window
1029                .update(cx, |workspace, window, cx| {
1030                    workspace.prepare_to_close(CloseIntent::Quit, window, cx)
1031                })
1032                .log_err()
1033            {
1034                if !should_close.await? {
1035                    return Ok(());
1036                }
1037            }
1038        }
1039        cx.update(|cx| cx.quit())?;
1040        anyhow::Ok(())
1041    })
1042    .detach_and_log_err(cx);
1043}
1044
1045fn open_log_file(workspace: &mut Workspace, window: &mut Window, cx: &mut Context<Workspace>) {
1046    const MAX_LINES: usize = 1000;
1047    workspace
1048        .with_local_workspace(window, cx, move |workspace, window, cx| {
1049            let fs = workspace.app_state().fs.clone();
1050            cx.spawn_in(window, async move |workspace, cx| {
1051                let (old_log, new_log) =
1052                    futures::join!(fs.load(paths::old_log_file()), fs.load(paths::log_file()));
1053                let log = match (old_log, new_log) {
1054                    (Err(_), Err(_)) => None,
1055                    (old_log, new_log) => {
1056                        let mut lines = VecDeque::with_capacity(MAX_LINES);
1057                        for line in old_log
1058                            .iter()
1059                            .flat_map(|log| log.lines())
1060                            .chain(new_log.iter().flat_map(|log| log.lines()))
1061                        {
1062                            if lines.len() == MAX_LINES {
1063                                lines.pop_front();
1064                            }
1065                            lines.push_back(line);
1066                        }
1067                        Some(
1068                            lines
1069                                .into_iter()
1070                                .flat_map(|line| [line, "\n"])
1071                                .collect::<String>(),
1072                        )
1073                    }
1074                };
1075
1076                workspace
1077                    .update_in(cx, |workspace, window, cx| {
1078                        let Some(log) = log else {
1079                            struct OpenLogError;
1080
1081                            workspace.show_notification(
1082                                NotificationId::unique::<OpenLogError>(),
1083                                cx,
1084                                |cx| {
1085                                    cx.new(|cx| {
1086                                        MessageNotification::new(
1087                                            format!(
1088                                                "Unable to access/open log file at path {:?}",
1089                                                paths::log_file().as_path()
1090                                            ),
1091                                            cx,
1092                                        )
1093                                    })
1094                                },
1095                            );
1096                            return;
1097                        };
1098                        let project = workspace.project().clone();
1099                        let buffer = project.update(cx, |project, cx| {
1100                            project.create_local_buffer(&log, None, cx)
1101                        });
1102
1103                        let buffer = cx
1104                            .new(|cx| MultiBuffer::singleton(buffer, cx).with_title("Log".into()));
1105                        let editor = cx.new(|cx| {
1106                            let mut editor =
1107                                Editor::for_multibuffer(buffer, Some(project), window, cx);
1108                            editor.set_read_only(true);
1109                            editor.set_breadcrumb_header(format!(
1110                                "Last {} lines in {}",
1111                                MAX_LINES,
1112                                paths::log_file().display()
1113                            ));
1114                            editor
1115                        });
1116
1117                        editor.update(cx, |editor, cx| {
1118                            let last_multi_buffer_offset = editor.buffer().read(cx).len(cx);
1119                            editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
1120                                s.select_ranges(Some(
1121                                    last_multi_buffer_offset..last_multi_buffer_offset,
1122                                ));
1123                            })
1124                        });
1125
1126                        workspace.add_item_to_active_pane(Box::new(editor), None, true, window, cx);
1127                    })
1128                    .log_err();
1129            })
1130            .detach();
1131        })
1132        .detach();
1133}
1134
1135pub fn handle_settings_file_changes(
1136    mut user_settings_file_rx: mpsc::UnboundedReceiver<String>,
1137    mut global_settings_file_rx: mpsc::UnboundedReceiver<String>,
1138    cx: &mut App,
1139    settings_changed: impl Fn(Option<anyhow::Error>, &mut App) + 'static,
1140) {
1141    MigrationNotification::set_global(cx.new(|_| MigrationNotification), cx);
1142
1143    // Helper function to process settings content
1144    let process_settings =
1145        move |content: String, is_user: bool, store: &mut SettingsStore, cx: &mut App| -> bool {
1146            // Apply migrations to both user and global settings
1147            let (processed_content, content_migrated) =
1148                if let Ok(Some(migrated_content)) = migrate_settings(&content) {
1149                    (migrated_content, true)
1150                } else {
1151                    (content, false)
1152                };
1153
1154            let result = if is_user {
1155                store.set_user_settings(&processed_content, cx)
1156            } else {
1157                store.set_global_settings(&processed_content, cx)
1158            };
1159
1160            if let Err(err) = &result {
1161                let settings_type = if is_user { "user" } else { "global" };
1162                log::error!("Failed to load {} settings: {err}", settings_type);
1163            }
1164
1165            settings_changed(result.err(), cx);
1166
1167            content_migrated
1168        };
1169
1170    // Initial load of both settings files
1171    let global_content = cx
1172        .background_executor()
1173        .block(global_settings_file_rx.next())
1174        .unwrap();
1175    let user_content = cx
1176        .background_executor()
1177        .block(user_settings_file_rx.next())
1178        .unwrap();
1179
1180    SettingsStore::update_global(cx, |store, cx| {
1181        process_settings(global_content, false, store, cx);
1182        process_settings(user_content, true, store, cx);
1183    });
1184
1185    // Watch for changes in both files
1186    cx.spawn(async move |cx| {
1187        let mut settings_streams = futures::stream::select(
1188            global_settings_file_rx.map(Either::Left),
1189            user_settings_file_rx.map(Either::Right),
1190        );
1191
1192        while let Some(content) = settings_streams.next().await {
1193            let (content, is_user) = match content {
1194                Either::Left(content) => (content, false),
1195                Either::Right(content) => (content, true),
1196            };
1197
1198            let result = cx.update_global(|store: &mut SettingsStore, cx| {
1199                let migrating_in_memory = process_settings(content, is_user, store, cx);
1200                if let Some(notifier) = MigrationNotification::try_global(cx) {
1201                    notifier.update(cx, |_, cx| {
1202                        cx.emit(MigrationEvent::ContentChanged {
1203                            migration_type: MigrationType::Settings,
1204                            migrating_in_memory,
1205                        });
1206                    });
1207                }
1208                cx.refresh_windows();
1209            });
1210
1211            if result.is_err() {
1212                break; // App dropped
1213            }
1214        }
1215    })
1216    .detach();
1217}
1218
1219pub fn handle_keymap_file_changes(
1220    mut user_keymap_file_rx: mpsc::UnboundedReceiver<String>,
1221    cx: &mut App,
1222) {
1223    BaseKeymap::register(cx);
1224    vim_mode_setting::init(cx);
1225
1226    let (base_keymap_tx, mut base_keymap_rx) = mpsc::unbounded();
1227    let (keyboard_layout_tx, mut keyboard_layout_rx) = mpsc::unbounded();
1228    let mut old_base_keymap = *BaseKeymap::get_global(cx);
1229    let mut old_vim_enabled = VimModeSetting::get_global(cx).0;
1230    let mut old_helix_enabled = vim_mode_setting::HelixModeSetting::get_global(cx).0;
1231
1232    cx.observe_global::<SettingsStore>(move |cx| {
1233        let new_base_keymap = *BaseKeymap::get_global(cx);
1234        let new_vim_enabled = VimModeSetting::get_global(cx).0;
1235        let new_helix_enabled = vim_mode_setting::HelixModeSetting::get_global(cx).0;
1236
1237        if new_base_keymap != old_base_keymap
1238            || new_vim_enabled != old_vim_enabled
1239            || new_helix_enabled != old_helix_enabled
1240        {
1241            old_base_keymap = new_base_keymap;
1242            old_vim_enabled = new_vim_enabled;
1243            old_helix_enabled = new_helix_enabled;
1244
1245            base_keymap_tx.unbounded_send(()).unwrap();
1246        }
1247    })
1248    .detach();
1249
1250    let mut current_mapping = settings::get_key_equivalents(cx.keyboard_layout().id());
1251    cx.on_keyboard_layout_change(move |cx| {
1252        let next_mapping = settings::get_key_equivalents(cx.keyboard_layout().id());
1253        if next_mapping != current_mapping {
1254            current_mapping = next_mapping;
1255            keyboard_layout_tx.unbounded_send(()).ok();
1256        }
1257    })
1258    .detach();
1259
1260    load_default_keymap(cx);
1261
1262    struct KeymapParseErrorNotification;
1263    let notification_id = NotificationId::unique::<KeymapParseErrorNotification>();
1264
1265    cx.spawn(async move |cx| {
1266        let mut user_keymap_content = String::new();
1267        let mut migrating_in_memory = false;
1268        loop {
1269            select_biased! {
1270                _ = base_keymap_rx.next() => {},
1271                _ = keyboard_layout_rx.next() => {},
1272                content = user_keymap_file_rx.next() => {
1273                    if let Some(content) = content {
1274                        if let Ok(Some(migrated_content)) = migrate_keymap(&content) {
1275                            user_keymap_content = migrated_content;
1276                            migrating_in_memory = true;
1277                        } else {
1278                            user_keymap_content = content;
1279                            migrating_in_memory = false;
1280                        }
1281                    }
1282                }
1283            };
1284            cx.update(|cx| {
1285                if let Some(notifier) = MigrationNotification::try_global(cx) {
1286                    notifier.update(cx, |_, cx| {
1287                        cx.emit(MigrationEvent::ContentChanged {
1288                            migration_type: MigrationType::Keymap,
1289                            migrating_in_memory,
1290                        });
1291                    });
1292                }
1293                let load_result = KeymapFile::load(&user_keymap_content, cx);
1294                match load_result {
1295                    KeymapFileLoadResult::Success { key_bindings } => {
1296                        reload_keymaps(cx, key_bindings);
1297                        dismiss_app_notification(&notification_id.clone(), cx);
1298                    }
1299                    KeymapFileLoadResult::SomeFailedToLoad {
1300                        key_bindings,
1301                        error_message,
1302                    } => {
1303                        if !key_bindings.is_empty() {
1304                            reload_keymaps(cx, key_bindings);
1305                        }
1306                        show_keymap_file_load_error(notification_id.clone(), error_message, cx);
1307                    }
1308                    KeymapFileLoadResult::JsonParseFailure { error } => {
1309                        show_keymap_file_json_error(notification_id.clone(), &error, cx)
1310                    }
1311                }
1312            })
1313            .ok();
1314        }
1315    })
1316    .detach();
1317}
1318
1319fn show_keymap_file_json_error(
1320    notification_id: NotificationId,
1321    error: &anyhow::Error,
1322    cx: &mut App,
1323) {
1324    let message: SharedString =
1325        format!("JSON parse error in keymap file. Bindings not reloaded.\n\n{error}").into();
1326    show_app_notification(notification_id, cx, move |cx| {
1327        cx.new(|cx| {
1328            MessageNotification::new(message.clone(), cx)
1329                .primary_message("Open Keymap File")
1330                .primary_on_click(|window, cx| {
1331                    window.dispatch_action(zed_actions::OpenKeymap.boxed_clone(), cx);
1332                    cx.emit(DismissEvent);
1333                })
1334        })
1335    });
1336}
1337
1338fn show_keymap_file_load_error(
1339    notification_id: NotificationId,
1340    error_message: MarkdownString,
1341    cx: &mut App,
1342) {
1343    show_markdown_app_notification(
1344        notification_id.clone(),
1345        error_message,
1346        "Open Keymap File".into(),
1347        |window, cx| {
1348            window.dispatch_action(zed_actions::OpenKeymap.boxed_clone(), cx);
1349            cx.emit(DismissEvent);
1350        },
1351        cx,
1352    )
1353}
1354
1355fn show_markdown_app_notification<F>(
1356    notification_id: NotificationId,
1357    message: MarkdownString,
1358    primary_button_message: SharedString,
1359    primary_button_on_click: F,
1360    cx: &mut App,
1361) where
1362    F: 'static + Send + Sync + Fn(&mut Window, &mut Context<MessageNotification>),
1363{
1364    let parsed_markdown = cx.background_spawn(async move {
1365        let file_location_directory = None;
1366        let language_registry = None;
1367        markdown_preview::markdown_parser::parse_markdown(
1368            &message.0,
1369            file_location_directory,
1370            language_registry,
1371        )
1372        .await
1373    });
1374
1375    cx.spawn(async move |cx| {
1376        let parsed_markdown = Arc::new(parsed_markdown.await);
1377        let primary_button_message = primary_button_message.clone();
1378        let primary_button_on_click = Arc::new(primary_button_on_click);
1379        cx.update(|cx| {
1380            show_app_notification(notification_id, cx, move |cx| {
1381                let workspace_handle = cx.entity().downgrade();
1382                let parsed_markdown = parsed_markdown.clone();
1383                let primary_button_message = primary_button_message.clone();
1384                let primary_button_on_click = primary_button_on_click.clone();
1385                cx.new(move |cx| {
1386                    MessageNotification::new_from_builder(cx, move |window, cx| {
1387                        image_cache(retain_all("notification-cache"))
1388                            .text_xs()
1389                            .child(markdown_preview::markdown_renderer::render_parsed_markdown(
1390                                &parsed_markdown.clone(),
1391                                Some(workspace_handle.clone()),
1392                                window,
1393                                cx,
1394                            ))
1395                            .into_any()
1396                    })
1397                    .primary_message(primary_button_message)
1398                    .primary_on_click_arc(primary_button_on_click)
1399                })
1400            })
1401        })
1402        .ok();
1403    })
1404    .detach();
1405}
1406
1407fn reload_keymaps(cx: &mut App, user_key_bindings: Vec<KeyBinding>) {
1408    cx.clear_key_bindings();
1409    load_default_keymap(cx);
1410    cx.bind_keys(user_key_bindings);
1411    cx.set_menus(app_menus());
1412    // On Windows, this is set in the `update_jump_list` method of the `HistoryManager`.
1413    #[cfg(not(target_os = "windows"))]
1414    cx.set_dock_menu(vec![gpui::MenuItem::action(
1415        "New Window",
1416        workspace::NewWindow,
1417    )]);
1418}
1419
1420pub fn load_default_keymap(cx: &mut App) {
1421    let base_keymap = *BaseKeymap::get_global(cx);
1422    if base_keymap == BaseKeymap::None {
1423        return;
1424    }
1425
1426    cx.bind_keys(KeymapFile::load_asset(DEFAULT_KEYMAP_PATH, cx).unwrap());
1427
1428    if let Some(asset_path) = base_keymap.asset_path() {
1429        cx.bind_keys(KeymapFile::load_asset(asset_path, cx).unwrap());
1430    }
1431
1432    if VimModeSetting::get_global(cx).0 || vim_mode_setting::HelixModeSetting::get_global(cx).0 {
1433        cx.bind_keys(KeymapFile::load_asset(VIM_KEYMAP_PATH, cx).unwrap());
1434    }
1435}
1436
1437pub fn handle_settings_changed(error: Option<anyhow::Error>, cx: &mut App) {
1438    struct SettingsParseErrorNotification;
1439    let id = NotificationId::unique::<SettingsParseErrorNotification>();
1440
1441    match error {
1442        Some(error) => {
1443            if let Some(InvalidSettingsError::LocalSettings { .. }) =
1444                error.downcast_ref::<InvalidSettingsError>()
1445            {
1446                // Local settings errors are displayed by the projects
1447                return;
1448            }
1449            show_app_notification(id, cx, move |cx| {
1450                cx.new(|cx| {
1451                    MessageNotification::new(format!("Invalid user settings file\n{error}"), cx)
1452                        .primary_message("Open Settings File")
1453                        .primary_icon(IconName::Settings)
1454                        .primary_on_click(|window, cx| {
1455                            window.dispatch_action(zed_actions::OpenSettings.boxed_clone(), cx);
1456                            cx.emit(DismissEvent);
1457                        })
1458                })
1459            });
1460        }
1461        None => {
1462            dismiss_app_notification(&id, cx);
1463        }
1464    }
1465}
1466
1467pub fn open_new_ssh_project_from_project(
1468    workspace: &mut Workspace,
1469    paths: Vec<PathBuf>,
1470    window: &mut Window,
1471    cx: &mut Context<Workspace>,
1472) -> Task<anyhow::Result<()>> {
1473    let app_state = workspace.app_state().clone();
1474    let Some(ssh_client) = workspace.project().read(cx).ssh_client() else {
1475        return Task::ready(Err(anyhow::anyhow!("Not an ssh project")));
1476    };
1477    let connection_options = ssh_client.read(cx).connection_options();
1478    cx.spawn_in(window, async move |_, cx| {
1479        open_ssh_project(
1480            connection_options,
1481            paths,
1482            app_state,
1483            workspace::OpenOptions {
1484                open_new_workspace: Some(true),
1485                ..Default::default()
1486            },
1487            cx,
1488        )
1489        .await
1490    })
1491}
1492
1493fn open_project_settings_file(
1494    workspace: &mut Workspace,
1495    _: &OpenProjectSettings,
1496    window: &mut Window,
1497    cx: &mut Context<Workspace>,
1498) {
1499    open_local_file(
1500        workspace,
1501        local_settings_file_relative_path(),
1502        initial_project_settings_content(),
1503        window,
1504        cx,
1505    )
1506}
1507
1508fn open_project_tasks_file(
1509    workspace: &mut Workspace,
1510    _: &OpenProjectTasks,
1511    window: &mut Window,
1512    cx: &mut Context<Workspace>,
1513) {
1514    open_local_file(
1515        workspace,
1516        local_tasks_file_relative_path(),
1517        initial_tasks_content(),
1518        window,
1519        cx,
1520    )
1521}
1522
1523fn open_project_debug_tasks_file(
1524    workspace: &mut Workspace,
1525    _: &zed_actions::OpenProjectDebugTasks,
1526    window: &mut Window,
1527    cx: &mut Context<Workspace>,
1528) {
1529    open_local_file(
1530        workspace,
1531        local_debug_file_relative_path(),
1532        initial_local_debug_tasks_content(),
1533        window,
1534        cx,
1535    )
1536}
1537
1538fn open_local_file(
1539    workspace: &mut Workspace,
1540    settings_relative_path: &'static Path,
1541    initial_contents: Cow<'static, str>,
1542    window: &mut Window,
1543    cx: &mut Context<Workspace>,
1544) {
1545    let project = workspace.project().clone();
1546    let worktree = project
1547        .read(cx)
1548        .visible_worktrees(cx)
1549        .find_map(|tree| tree.read(cx).root_entry()?.is_dir().then_some(tree));
1550    if let Some(worktree) = worktree {
1551        let tree_id = worktree.read(cx).id();
1552        cx.spawn_in(window, async move |workspace, cx| {
1553            // Check if the file actually exists on disk (even if it's excluded from worktree)
1554            let file_exists = {
1555                let full_path = worktree
1556                    .read_with(cx, |tree, _| tree.abs_path().join(settings_relative_path))?;
1557
1558                let fs = project.read_with(cx, |project, _| project.fs().clone())?;
1559                let file_exists = fs
1560                    .metadata(&full_path)
1561                    .await
1562                    .ok()
1563                    .flatten()
1564                    .map_or(false, |metadata| !metadata.is_dir && !metadata.is_fifo);
1565                file_exists
1566            };
1567
1568            if !file_exists {
1569                if let Some(dir_path) = settings_relative_path.parent() {
1570                    if worktree.read_with(cx, |tree, _| tree.entry_for_path(dir_path).is_none())? {
1571                        project
1572                            .update(cx, |project, cx| {
1573                                project.create_entry((tree_id, dir_path), true, cx)
1574                            })?
1575                            .await
1576                            .context("worktree was removed")?;
1577                    }
1578                }
1579
1580                if worktree.read_with(cx, |tree, _| {
1581                    tree.entry_for_path(settings_relative_path).is_none()
1582                })? {
1583                    project
1584                        .update(cx, |project, cx| {
1585                            project.create_entry((tree_id, settings_relative_path), false, cx)
1586                        })?
1587                        .await
1588                        .context("worktree was removed")?;
1589                }
1590            }
1591
1592            let editor = workspace
1593                .update_in(cx, |workspace, window, cx| {
1594                    workspace.open_path((tree_id, settings_relative_path), None, true, window, cx)
1595                })?
1596                .await?
1597                .downcast::<Editor>()
1598                .context("unexpected item type: expected editor item")?;
1599
1600            editor
1601                .downgrade()
1602                .update(cx, |editor, cx| {
1603                    if let Some(buffer) = editor.buffer().read(cx).as_singleton() {
1604                        if buffer.read(cx).is_empty() {
1605                            buffer.update(cx, |buffer, cx| {
1606                                buffer.edit([(0..0, initial_contents)], None, cx)
1607                            });
1608                        }
1609                    }
1610                })
1611                .ok();
1612
1613            anyhow::Ok(())
1614        })
1615        .detach();
1616    } else {
1617        struct NoOpenFolders;
1618
1619        workspace.show_notification(NotificationId::unique::<NoOpenFolders>(), cx, |cx| {
1620            cx.new(|cx| MessageNotification::new("This project has no folders open.", cx))
1621        })
1622    }
1623}
1624
1625fn open_telemetry_log_file(
1626    workspace: &mut Workspace,
1627    window: &mut Window,
1628    cx: &mut Context<Workspace>,
1629) {
1630    workspace.with_local_workspace(window, cx, move |workspace, window, cx| {
1631        let app_state = workspace.app_state().clone();
1632        cx.spawn_in(window, async move |workspace, cx| {
1633            async fn fetch_log_string(app_state: &Arc<AppState>) -> Option<String> {
1634                let path = client::telemetry::Telemetry::log_file_path();
1635                app_state.fs.load(&path).await.log_err()
1636            }
1637
1638            let log = fetch_log_string(&app_state).await.unwrap_or_else(|| "// No data has been collected yet".to_string());
1639
1640            const MAX_TELEMETRY_LOG_LEN: usize = 5 * 1024 * 1024;
1641            let mut start_offset = log.len().saturating_sub(MAX_TELEMETRY_LOG_LEN);
1642            if let Some(newline_offset) = log[start_offset..].find('\n') {
1643                start_offset += newline_offset + 1;
1644            }
1645            let log_suffix = &log[start_offset..];
1646            let header = concat!(
1647                "// Zed collects anonymous usage data to help us understand how people are using the app.\n",
1648                "// Telemetry can be disabled via the `settings.json` file.\n",
1649                "// Here is the data that has been reported for the current session:\n",
1650            );
1651            let content = format!("{}\n{}", header, log_suffix);
1652            let json = app_state.languages.language_for_name("JSON").await.log_err();
1653
1654            workspace.update_in( cx, |workspace, window, cx| {
1655                let project = workspace.project().clone();
1656                let buffer = project.update(cx, |project, cx| project.create_local_buffer(&content, json, cx));
1657                let buffer = cx.new(|cx| {
1658                    MultiBuffer::singleton(buffer, cx).with_title("Telemetry Log".into())
1659                });
1660                workspace.add_item_to_active_pane(
1661                    Box::new(cx.new(|cx| {
1662                        let mut editor = Editor::for_multibuffer(buffer, Some(project), window, cx);
1663                        editor.set_read_only(true);
1664                        editor.set_breadcrumb_header("Telemetry Log".into());
1665                        editor
1666                    })),
1667                    None,
1668                    true,
1669                    window, cx,
1670                );
1671            }).log_err()?;
1672
1673            Some(())
1674        })
1675        .detach();
1676    }).detach();
1677}
1678
1679fn open_bundled_file(
1680    workspace: &Workspace,
1681    text: Cow<'static, str>,
1682    title: &'static str,
1683    language: &'static str,
1684    window: &mut Window,
1685    cx: &mut Context<Workspace>,
1686) {
1687    let language = workspace.app_state().languages.language_for_name(language);
1688    cx.spawn_in(window, async move |workspace, cx| {
1689        let language = language.await.log_err();
1690        workspace
1691            .update_in(cx, |workspace, window, cx| {
1692                workspace.with_local_workspace(window, cx, |workspace, window, cx| {
1693                    let project = workspace.project();
1694                    let buffer = project.update(cx, move |project, cx| {
1695                        project.create_local_buffer(text.as_ref(), language, cx)
1696                    });
1697                    let buffer =
1698                        cx.new(|cx| MultiBuffer::singleton(buffer, cx).with_title(title.into()));
1699                    workspace.add_item_to_active_pane(
1700                        Box::new(cx.new(|cx| {
1701                            let mut editor =
1702                                Editor::for_multibuffer(buffer, Some(project.clone()), window, cx);
1703                            editor.set_read_only(true);
1704                            editor.set_breadcrumb_header(title.into());
1705                            editor
1706                        })),
1707                        None,
1708                        true,
1709                        window,
1710                        cx,
1711                    );
1712                })
1713            })?
1714            .await
1715    })
1716    .detach_and_log_err(cx);
1717}
1718
1719fn open_settings_file(
1720    abs_path: &'static Path,
1721    default_content: impl FnOnce() -> Rope + Send + 'static,
1722    window: &mut Window,
1723    cx: &mut Context<Workspace>,
1724) {
1725    cx.spawn_in(window, async move |workspace, cx| {
1726        let (worktree_creation_task, settings_open_task) = workspace
1727            .update_in(cx, |workspace, window, cx| {
1728                workspace.with_local_workspace(window, cx, move |workspace, window, cx| {
1729                    let worktree_creation_task = workspace.project().update(cx, |project, cx| {
1730                        // Set up a dedicated worktree for settings, since
1731                        // otherwise we're dropping and re-starting LSP servers
1732                        // for each file inside on every settings file
1733                        // close/open
1734
1735                        // TODO: Do note that all other external files (e.g.
1736                        // drag and drop from OS) still have their worktrees
1737                        // released on file close, causing LSP servers'
1738                        // restarts.
1739                        project.find_or_create_worktree(paths::config_dir().as_path(), false, cx)
1740                    });
1741                    let settings_open_task =
1742                        create_and_open_local_file(abs_path, window, cx, default_content);
1743                    (worktree_creation_task, settings_open_task)
1744                })
1745            })?
1746            .await?;
1747        let _ = worktree_creation_task.await?;
1748        let _ = settings_open_task.await?;
1749        anyhow::Ok(())
1750    })
1751    .detach_and_log_err(cx);
1752}
1753
1754#[cfg(test)]
1755mod tests {
1756    use super::*;
1757    use assets::Assets;
1758    use collections::HashSet;
1759    use editor::{DisplayPoint, Editor, display_map::DisplayRow, scroll::Autoscroll};
1760    use gpui::{
1761        Action, AnyWindowHandle, App, AssetSource, BorrowAppContext, SemanticVersion,
1762        TestAppContext, UpdateGlobal, VisualTestContext, WindowHandle, actions,
1763    };
1764    use language::{LanguageMatcher, LanguageRegistry};
1765    use project::{Project, ProjectPath, WorktreeSettings, project_settings::ProjectSettings};
1766    use serde_json::json;
1767    use settings::{SettingsStore, watch_config_file};
1768    use std::{
1769        path::{Path, PathBuf},
1770        time::Duration,
1771    };
1772    use theme::{ThemeRegistry, ThemeSettings};
1773    use util::path;
1774    use workspace::{
1775        NewFile, OpenOptions, OpenVisible, SERIALIZATION_THROTTLE_TIME, SaveIntent, SplitDirection,
1776        WorkspaceHandle,
1777        item::SaveOptions,
1778        item::{Item, ItemHandle},
1779        open_new, open_paths, pane,
1780    };
1781
1782    #[gpui::test]
1783    async fn test_open_non_existing_file(cx: &mut TestAppContext) {
1784        let app_state = init_test(cx);
1785        app_state
1786            .fs
1787            .as_fake()
1788            .insert_tree(
1789                path!("/root"),
1790                json!({
1791                    "a": {
1792                    },
1793                }),
1794            )
1795            .await;
1796
1797        cx.update(|cx| {
1798            open_paths(
1799                &[PathBuf::from(path!("/root/a/new"))],
1800                app_state.clone(),
1801                workspace::OpenOptions::default(),
1802                cx,
1803            )
1804        })
1805        .await
1806        .unwrap();
1807        assert_eq!(cx.read(|cx| cx.windows().len()), 1);
1808
1809        let workspace = cx.windows()[0].downcast::<Workspace>().unwrap();
1810        workspace
1811            .update(cx, |workspace, _, cx| {
1812                assert!(workspace.active_item_as::<Editor>(cx).is_some())
1813            })
1814            .unwrap();
1815    }
1816
1817    #[gpui::test]
1818    async fn test_open_paths_action(cx: &mut TestAppContext) {
1819        let app_state = init_test(cx);
1820        app_state
1821            .fs
1822            .as_fake()
1823            .insert_tree(
1824                "/root",
1825                json!({
1826                    "a": {
1827                        "aa": null,
1828                        "ab": null,
1829                    },
1830                    "b": {
1831                        "ba": null,
1832                        "bb": null,
1833                    },
1834                    "c": {
1835                        "ca": null,
1836                        "cb": null,
1837                    },
1838                    "d": {
1839                        "da": null,
1840                        "db": null,
1841                    },
1842                    "e": {
1843                        "ea": null,
1844                        "eb": null,
1845                    }
1846                }),
1847            )
1848            .await;
1849
1850        cx.update(|cx| {
1851            open_paths(
1852                &[PathBuf::from("/root/a"), PathBuf::from("/root/b")],
1853                app_state.clone(),
1854                workspace::OpenOptions::default(),
1855                cx,
1856            )
1857        })
1858        .await
1859        .unwrap();
1860        assert_eq!(cx.read(|cx| cx.windows().len()), 1);
1861
1862        cx.update(|cx| {
1863            open_paths(
1864                &[PathBuf::from("/root/a")],
1865                app_state.clone(),
1866                workspace::OpenOptions::default(),
1867                cx,
1868            )
1869        })
1870        .await
1871        .unwrap();
1872        assert_eq!(cx.read(|cx| cx.windows().len()), 1);
1873        let workspace_1 = cx
1874            .read(|cx| cx.windows()[0].downcast::<Workspace>())
1875            .unwrap();
1876        cx.run_until_parked();
1877        workspace_1
1878            .update(cx, |workspace, window, cx| {
1879                assert_eq!(workspace.worktrees(cx).count(), 2);
1880                assert!(workspace.left_dock().read(cx).is_open());
1881                assert!(
1882                    workspace
1883                        .active_pane()
1884                        .read(cx)
1885                        .focus_handle(cx)
1886                        .is_focused(window)
1887                );
1888            })
1889            .unwrap();
1890
1891        cx.update(|cx| {
1892            open_paths(
1893                &[PathBuf::from("/root/c"), PathBuf::from("/root/d")],
1894                app_state.clone(),
1895                workspace::OpenOptions::default(),
1896                cx,
1897            )
1898        })
1899        .await
1900        .unwrap();
1901        assert_eq!(cx.read(|cx| cx.windows().len()), 2);
1902
1903        // Replace existing windows
1904        let window = cx
1905            .update(|cx| cx.windows()[0].downcast::<Workspace>())
1906            .unwrap();
1907        cx.update(|cx| {
1908            open_paths(
1909                &[PathBuf::from("/root/e")],
1910                app_state,
1911                workspace::OpenOptions {
1912                    replace_window: Some(window),
1913                    ..Default::default()
1914                },
1915                cx,
1916            )
1917        })
1918        .await
1919        .unwrap();
1920        cx.background_executor.run_until_parked();
1921        assert_eq!(cx.read(|cx| cx.windows().len()), 2);
1922        let workspace_1 = cx
1923            .update(|cx| cx.windows()[0].downcast::<Workspace>())
1924            .unwrap();
1925        workspace_1
1926            .update(cx, |workspace, window, cx| {
1927                assert_eq!(
1928                    workspace
1929                        .worktrees(cx)
1930                        .map(|w| w.read(cx).abs_path())
1931                        .collect::<Vec<_>>(),
1932                    &[Path::new("/root/e").into()]
1933                );
1934                assert!(workspace.left_dock().read(cx).is_open());
1935                assert!(workspace.active_pane().focus_handle(cx).is_focused(window));
1936            })
1937            .unwrap();
1938    }
1939
1940    #[gpui::test]
1941    async fn test_open_add_new(cx: &mut TestAppContext) {
1942        let app_state = init_test(cx);
1943        app_state
1944            .fs
1945            .as_fake()
1946            .insert_tree(
1947                path!("/root"),
1948                json!({"a": "hey", "b": "", "dir": {"c": "f"}}),
1949            )
1950            .await;
1951
1952        cx.update(|cx| {
1953            open_paths(
1954                &[PathBuf::from(path!("/root/dir"))],
1955                app_state.clone(),
1956                workspace::OpenOptions::default(),
1957                cx,
1958            )
1959        })
1960        .await
1961        .unwrap();
1962        assert_eq!(cx.update(|cx| cx.windows().len()), 1);
1963
1964        cx.update(|cx| {
1965            open_paths(
1966                &[PathBuf::from(path!("/root/a"))],
1967                app_state.clone(),
1968                workspace::OpenOptions {
1969                    open_new_workspace: Some(false),
1970                    ..Default::default()
1971                },
1972                cx,
1973            )
1974        })
1975        .await
1976        .unwrap();
1977        assert_eq!(cx.update(|cx| cx.windows().len()), 1);
1978
1979        cx.update(|cx| {
1980            open_paths(
1981                &[PathBuf::from(path!("/root/dir/c"))],
1982                app_state.clone(),
1983                workspace::OpenOptions {
1984                    open_new_workspace: Some(true),
1985                    ..Default::default()
1986                },
1987                cx,
1988            )
1989        })
1990        .await
1991        .unwrap();
1992        assert_eq!(cx.update(|cx| cx.windows().len()), 2);
1993    }
1994
1995    #[gpui::test]
1996    async fn test_open_file_in_many_spaces(cx: &mut TestAppContext) {
1997        let app_state = init_test(cx);
1998        app_state
1999            .fs
2000            .as_fake()
2001            .insert_tree(
2002                path!("/root"),
2003                json!({"dir1": {"a": "b"}, "dir2": {"c": "d"}}),
2004            )
2005            .await;
2006
2007        cx.update(|cx| {
2008            open_paths(
2009                &[PathBuf::from(path!("/root/dir1/a"))],
2010                app_state.clone(),
2011                workspace::OpenOptions::default(),
2012                cx,
2013            )
2014        })
2015        .await
2016        .unwrap();
2017        assert_eq!(cx.update(|cx| cx.windows().len()), 1);
2018        let window1 = cx.update(|cx| cx.active_window().unwrap());
2019
2020        cx.update(|cx| {
2021            open_paths(
2022                &[PathBuf::from(path!("/root/dir2/c"))],
2023                app_state.clone(),
2024                workspace::OpenOptions::default(),
2025                cx,
2026            )
2027        })
2028        .await
2029        .unwrap();
2030        assert_eq!(cx.update(|cx| cx.windows().len()), 1);
2031
2032        cx.update(|cx| {
2033            open_paths(
2034                &[PathBuf::from(path!("/root/dir2"))],
2035                app_state.clone(),
2036                workspace::OpenOptions::default(),
2037                cx,
2038            )
2039        })
2040        .await
2041        .unwrap();
2042        assert_eq!(cx.update(|cx| cx.windows().len()), 2);
2043        let window2 = cx.update(|cx| cx.active_window().unwrap());
2044        assert!(window1 != window2);
2045        cx.update_window(window1, |_, window, _| window.activate_window())
2046            .unwrap();
2047
2048        cx.update(|cx| {
2049            open_paths(
2050                &[PathBuf::from(path!("/root/dir2/c"))],
2051                app_state.clone(),
2052                workspace::OpenOptions::default(),
2053                cx,
2054            )
2055        })
2056        .await
2057        .unwrap();
2058        assert_eq!(cx.update(|cx| cx.windows().len()), 2);
2059        // should have opened in window2 because that has dir2 visibly open (window1 has it open, but not in the project panel)
2060        assert!(cx.update(|cx| cx.active_window().unwrap()) == window2);
2061    }
2062
2063    #[gpui::test]
2064    async fn test_window_edit_state_restoring_disabled(cx: &mut TestAppContext) {
2065        let executor = cx.executor();
2066        let app_state = init_test(cx);
2067
2068        cx.update(|cx| {
2069            SettingsStore::update_global(cx, |store, cx| {
2070                store.update_user_settings::<ProjectSettings>(cx, |settings| {
2071                    settings.session.restore_unsaved_buffers = false
2072                });
2073            });
2074        });
2075
2076        app_state
2077            .fs
2078            .as_fake()
2079            .insert_tree(path!("/root"), json!({"a": "hey"}))
2080            .await;
2081
2082        cx.update(|cx| {
2083            open_paths(
2084                &[PathBuf::from(path!("/root/a"))],
2085                app_state.clone(),
2086                workspace::OpenOptions::default(),
2087                cx,
2088            )
2089        })
2090        .await
2091        .unwrap();
2092        assert_eq!(cx.update(|cx| cx.windows().len()), 1);
2093
2094        // When opening the workspace, the window is not in a edited state.
2095        let window = cx.update(|cx| cx.windows()[0].downcast::<Workspace>().unwrap());
2096
2097        let window_is_edited = |window: WindowHandle<Workspace>, cx: &mut TestAppContext| {
2098            cx.update(|cx| window.read(cx).unwrap().is_edited())
2099        };
2100        let pane = window
2101            .read_with(cx, |workspace, _| workspace.active_pane().clone())
2102            .unwrap();
2103        let editor = window
2104            .read_with(cx, |workspace, cx| {
2105                workspace
2106                    .active_item(cx)
2107                    .unwrap()
2108                    .downcast::<Editor>()
2109                    .unwrap()
2110            })
2111            .unwrap();
2112
2113        assert!(!window_is_edited(window, cx));
2114
2115        // Editing a buffer marks the window as edited.
2116        window
2117            .update(cx, |_, window, cx| {
2118                editor.update(cx, |editor, cx| editor.insert("EDIT", window, cx));
2119            })
2120            .unwrap();
2121
2122        assert!(window_is_edited(window, cx));
2123
2124        // Undoing the edit restores the window's edited state.
2125        window
2126            .update(cx, |_, window, cx| {
2127                editor.update(cx, |editor, cx| {
2128                    editor.undo(&Default::default(), window, cx)
2129                });
2130            })
2131            .unwrap();
2132        assert!(!window_is_edited(window, cx));
2133
2134        // Redoing the edit marks the window as edited again.
2135        window
2136            .update(cx, |_, window, cx| {
2137                editor.update(cx, |editor, cx| {
2138                    editor.redo(&Default::default(), window, cx)
2139                });
2140            })
2141            .unwrap();
2142        assert!(window_is_edited(window, cx));
2143        let weak = editor.downgrade();
2144
2145        // Closing the item restores the window's edited state.
2146        let close = window
2147            .update(cx, |_, window, cx| {
2148                pane.update(cx, |pane, cx| {
2149                    drop(editor);
2150                    pane.close_active_item(&Default::default(), window, cx)
2151                })
2152            })
2153            .unwrap();
2154        executor.run_until_parked();
2155
2156        cx.simulate_prompt_answer("Don't Save");
2157        close.await.unwrap();
2158
2159        // Advance the clock to ensure that the item has been serialized and dropped from the queue
2160        cx.executor().advance_clock(Duration::from_secs(1));
2161
2162        weak.assert_released();
2163        assert!(!window_is_edited(window, cx));
2164        // Opening the buffer again doesn't impact the window's edited state.
2165        cx.update(|cx| {
2166            open_paths(
2167                &[PathBuf::from(path!("/root/a"))],
2168                app_state,
2169                workspace::OpenOptions::default(),
2170                cx,
2171            )
2172        })
2173        .await
2174        .unwrap();
2175        executor.run_until_parked();
2176
2177        window
2178            .update(cx, |workspace, _, cx| {
2179                let editor = workspace
2180                    .active_item(cx)
2181                    .unwrap()
2182                    .downcast::<Editor>()
2183                    .unwrap();
2184
2185                editor.update(cx, |editor, cx| {
2186                    assert_eq!(editor.text(cx), "hey");
2187                });
2188            })
2189            .unwrap();
2190
2191        let editor = window
2192            .read_with(cx, |workspace, cx| {
2193                workspace
2194                    .active_item(cx)
2195                    .unwrap()
2196                    .downcast::<Editor>()
2197                    .unwrap()
2198            })
2199            .unwrap();
2200        assert!(!window_is_edited(window, cx));
2201
2202        // Editing the buffer marks the window as edited.
2203        window
2204            .update(cx, |_, window, cx| {
2205                editor.update(cx, |editor, cx| editor.insert("EDIT", window, cx));
2206            })
2207            .unwrap();
2208        executor.run_until_parked();
2209        assert!(window_is_edited(window, cx));
2210
2211        // Ensure closing the window via the mouse gets preempted due to the
2212        // buffer having unsaved changes.
2213        assert!(!VisualTestContext::from_window(window.into(), cx).simulate_close());
2214        executor.run_until_parked();
2215        assert_eq!(cx.update(|cx| cx.windows().len()), 1);
2216
2217        // The window is successfully closed after the user dismisses the prompt.
2218        cx.simulate_prompt_answer("Don't Save");
2219        executor.run_until_parked();
2220        assert_eq!(cx.update(|cx| cx.windows().len()), 0);
2221    }
2222
2223    #[gpui::test]
2224    async fn test_window_edit_state_restoring_enabled(cx: &mut TestAppContext) {
2225        let app_state = init_test(cx);
2226        app_state
2227            .fs
2228            .as_fake()
2229            .insert_tree(path!("/root"), json!({"a": "hey"}))
2230            .await;
2231
2232        cx.update(|cx| {
2233            open_paths(
2234                &[PathBuf::from(path!("/root/a"))],
2235                app_state.clone(),
2236                workspace::OpenOptions::default(),
2237                cx,
2238            )
2239        })
2240        .await
2241        .unwrap();
2242
2243        assert_eq!(cx.update(|cx| cx.windows().len()), 1);
2244
2245        // When opening the workspace, the window is not in a edited state.
2246        let window = cx.update(|cx| cx.windows()[0].downcast::<Workspace>().unwrap());
2247
2248        let window_is_edited = |window: WindowHandle<Workspace>, cx: &mut TestAppContext| {
2249            cx.update(|cx| window.read(cx).unwrap().is_edited())
2250        };
2251
2252        let editor = window
2253            .read_with(cx, |workspace, cx| {
2254                workspace
2255                    .active_item(cx)
2256                    .unwrap()
2257                    .downcast::<Editor>()
2258                    .unwrap()
2259            })
2260            .unwrap();
2261
2262        assert!(!window_is_edited(window, cx));
2263
2264        // Editing a buffer marks the window as edited.
2265        window
2266            .update(cx, |_, window, cx| {
2267                editor.update(cx, |editor, cx| editor.insert("EDIT", window, cx));
2268            })
2269            .unwrap();
2270
2271        assert!(window_is_edited(window, cx));
2272        cx.run_until_parked();
2273
2274        // Advance the clock to make sure the workspace is serialized
2275        cx.executor().advance_clock(Duration::from_secs(1));
2276
2277        // When closing the window, no prompt shows up and the window is closed.
2278        // buffer having unsaved changes.
2279        assert!(!VisualTestContext::from_window(window.into(), cx).simulate_close());
2280        cx.run_until_parked();
2281        assert_eq!(cx.update(|cx| cx.windows().len()), 0);
2282
2283        // When we now reopen the window, the edited state and the edited buffer are back
2284        cx.update(|cx| {
2285            open_paths(
2286                &[PathBuf::from(path!("/root/a"))],
2287                app_state.clone(),
2288                workspace::OpenOptions::default(),
2289                cx,
2290            )
2291        })
2292        .await
2293        .unwrap();
2294
2295        assert_eq!(cx.update(|cx| cx.windows().len()), 1);
2296        assert!(cx.update(|cx| cx.active_window().is_some()));
2297
2298        cx.run_until_parked();
2299
2300        // When opening the workspace, the window is not in a edited state.
2301        let window = cx.update(|cx| cx.active_window().unwrap().downcast::<Workspace>().unwrap());
2302        assert!(window_is_edited(window, cx));
2303
2304        window
2305            .update(cx, |workspace, _, cx| {
2306                let editor = workspace
2307                    .active_item(cx)
2308                    .unwrap()
2309                    .downcast::<editor::Editor>()
2310                    .unwrap();
2311                editor.update(cx, |editor, cx| {
2312                    assert_eq!(editor.text(cx), "EDIThey");
2313                    assert!(editor.is_dirty(cx));
2314                });
2315
2316                editor
2317            })
2318            .unwrap();
2319    }
2320
2321    #[gpui::test]
2322    async fn test_new_empty_workspace(cx: &mut TestAppContext) {
2323        let app_state = init_test(cx);
2324        cx.update(|cx| {
2325            open_new(
2326                Default::default(),
2327                app_state.clone(),
2328                cx,
2329                |workspace, window, cx| {
2330                    Editor::new_file(workspace, &Default::default(), window, cx)
2331                },
2332            )
2333        })
2334        .await
2335        .unwrap();
2336        cx.run_until_parked();
2337
2338        let workspace = cx
2339            .update(|cx| cx.windows().first().unwrap().downcast::<Workspace>())
2340            .unwrap();
2341
2342        let editor = workspace
2343            .update(cx, |workspace, _, cx| {
2344                let editor = workspace
2345                    .active_item(cx)
2346                    .unwrap()
2347                    .downcast::<editor::Editor>()
2348                    .unwrap();
2349                editor.update(cx, |editor, cx| {
2350                    assert!(editor.text(cx).is_empty());
2351                    assert!(!editor.is_dirty(cx));
2352                });
2353
2354                editor
2355            })
2356            .unwrap();
2357
2358        let save_task = workspace
2359            .update(cx, |workspace, window, cx| {
2360                workspace.save_active_item(SaveIntent::Save, window, cx)
2361            })
2362            .unwrap();
2363        app_state.fs.create_dir(Path::new("/root")).await.unwrap();
2364        cx.background_executor.run_until_parked();
2365        cx.simulate_new_path_selection(|_| Some(PathBuf::from("/root/the-new-name")));
2366        save_task.await.unwrap();
2367        workspace
2368            .update(cx, |_, _, cx| {
2369                editor.update(cx, |editor, cx| {
2370                    assert!(!editor.is_dirty(cx));
2371                    assert_eq!(editor.title(cx), "the-new-name");
2372                });
2373            })
2374            .unwrap();
2375    }
2376
2377    #[gpui::test]
2378    async fn test_open_entry(cx: &mut TestAppContext) {
2379        let app_state = init_test(cx);
2380        app_state
2381            .fs
2382            .as_fake()
2383            .insert_tree(
2384                path!("/root"),
2385                json!({
2386                    "a": {
2387                        "file1": "contents 1",
2388                        "file2": "contents 2",
2389                        "file3": "contents 3",
2390                    },
2391                }),
2392            )
2393            .await;
2394
2395        let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
2396        project.update(cx, |project, _cx| {
2397            project.languages().add(markdown_language())
2398        });
2399        let window = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
2400        let workspace = window.root(cx).unwrap();
2401
2402        let entries = cx.read(|cx| workspace.file_project_paths(cx));
2403        let file1 = entries[0].clone();
2404        let file2 = entries[1].clone();
2405        let file3 = entries[2].clone();
2406
2407        // Open the first entry
2408        let entry_1 = window
2409            .update(cx, |w, window, cx| {
2410                w.open_path(file1.clone(), None, true, window, cx)
2411            })
2412            .unwrap()
2413            .await
2414            .unwrap();
2415        cx.read(|cx| {
2416            let pane = workspace.read(cx).active_pane().read(cx);
2417            assert_eq!(
2418                pane.active_item().unwrap().project_path(cx),
2419                Some(file1.clone())
2420            );
2421            assert_eq!(pane.items_len(), 1);
2422        });
2423
2424        // Open the second entry
2425        window
2426            .update(cx, |w, window, cx| {
2427                w.open_path(file2.clone(), None, true, window, cx)
2428            })
2429            .unwrap()
2430            .await
2431            .unwrap();
2432        cx.read(|cx| {
2433            let pane = workspace.read(cx).active_pane().read(cx);
2434            assert_eq!(
2435                pane.active_item().unwrap().project_path(cx),
2436                Some(file2.clone())
2437            );
2438            assert_eq!(pane.items_len(), 2);
2439        });
2440
2441        // Open the first entry again. The existing pane item is activated.
2442        let entry_1b = window
2443            .update(cx, |w, window, cx| {
2444                w.open_path(file1.clone(), None, true, window, cx)
2445            })
2446            .unwrap()
2447            .await
2448            .unwrap();
2449        assert_eq!(entry_1.item_id(), entry_1b.item_id());
2450
2451        cx.read(|cx| {
2452            let pane = workspace.read(cx).active_pane().read(cx);
2453            assert_eq!(
2454                pane.active_item().unwrap().project_path(cx),
2455                Some(file1.clone())
2456            );
2457            assert_eq!(pane.items_len(), 2);
2458        });
2459
2460        // Split the pane with the first entry, then open the second entry again.
2461        window
2462            .update(cx, |w, window, cx| {
2463                w.split_and_clone(w.active_pane().clone(), SplitDirection::Right, window, cx);
2464                w.open_path(file2.clone(), None, true, window, cx)
2465            })
2466            .unwrap()
2467            .await
2468            .unwrap();
2469
2470        window
2471            .read_with(cx, |w, cx| {
2472                assert_eq!(
2473                    w.active_pane()
2474                        .read(cx)
2475                        .active_item()
2476                        .unwrap()
2477                        .project_path(cx),
2478                    Some(file2.clone())
2479                );
2480            })
2481            .unwrap();
2482
2483        // Open the third entry twice concurrently. Only one pane item is added.
2484        let (t1, t2) = window
2485            .update(cx, |w, window, cx| {
2486                (
2487                    w.open_path(file3.clone(), None, true, window, cx),
2488                    w.open_path(file3.clone(), None, true, window, cx),
2489                )
2490            })
2491            .unwrap();
2492        t1.await.unwrap();
2493        t2.await.unwrap();
2494        cx.read(|cx| {
2495            let pane = workspace.read(cx).active_pane().read(cx);
2496            assert_eq!(
2497                pane.active_item().unwrap().project_path(cx),
2498                Some(file3.clone())
2499            );
2500            let pane_entries = pane
2501                .items()
2502                .map(|i| i.project_path(cx).unwrap())
2503                .collect::<Vec<_>>();
2504            assert_eq!(pane_entries, &[file1, file2, file3]);
2505        });
2506    }
2507
2508    #[gpui::test]
2509    async fn test_open_paths(cx: &mut TestAppContext) {
2510        let app_state = init_test(cx);
2511
2512        app_state
2513            .fs
2514            .as_fake()
2515            .insert_tree(
2516                path!("/"),
2517                json!({
2518                    "dir1": {
2519                        "a.txt": ""
2520                    },
2521                    "dir2": {
2522                        "b.txt": ""
2523                    },
2524                    "dir3": {
2525                        "c.txt": ""
2526                    },
2527                    "d.txt": ""
2528                }),
2529            )
2530            .await;
2531
2532        cx.update(|cx| {
2533            open_paths(
2534                &[PathBuf::from(path!("/dir1/"))],
2535                app_state,
2536                workspace::OpenOptions::default(),
2537                cx,
2538            )
2539        })
2540        .await
2541        .unwrap();
2542        assert_eq!(cx.update(|cx| cx.windows().len()), 1);
2543        let window = cx.update(|cx| cx.windows()[0].downcast::<Workspace>().unwrap());
2544        let workspace = window.root(cx).unwrap();
2545
2546        #[track_caller]
2547        fn assert_project_panel_selection(
2548            workspace: &Workspace,
2549            expected_worktree_path: &Path,
2550            expected_entry_path: &Path,
2551            cx: &App,
2552        ) {
2553            let project_panel = [
2554                workspace.left_dock().read(cx).panel::<ProjectPanel>(),
2555                workspace.right_dock().read(cx).panel::<ProjectPanel>(),
2556                workspace.bottom_dock().read(cx).panel::<ProjectPanel>(),
2557            ]
2558            .into_iter()
2559            .find_map(std::convert::identity)
2560            .expect("found no project panels")
2561            .read(cx);
2562            let (selected_worktree, selected_entry) = project_panel
2563                .selected_entry(cx)
2564                .expect("project panel should have a selected entry");
2565            assert_eq!(
2566                selected_worktree.abs_path().as_ref(),
2567                expected_worktree_path,
2568                "Unexpected project panel selected worktree path"
2569            );
2570            assert_eq!(
2571                selected_entry.path.as_ref(),
2572                expected_entry_path,
2573                "Unexpected project panel selected entry path"
2574            );
2575        }
2576
2577        // Open a file within an existing worktree.
2578        window
2579            .update(cx, |workspace, window, cx| {
2580                workspace.open_paths(
2581                    vec![path!("/dir1/a.txt").into()],
2582                    OpenOptions {
2583                        visible: Some(OpenVisible::All),
2584                        ..Default::default()
2585                    },
2586                    None,
2587                    window,
2588                    cx,
2589                )
2590            })
2591            .unwrap()
2592            .await;
2593        cx.read(|cx| {
2594            let workspace = workspace.read(cx);
2595            assert_project_panel_selection(
2596                workspace,
2597                Path::new(path!("/dir1")),
2598                Path::new("a.txt"),
2599                cx,
2600            );
2601            assert_eq!(
2602                workspace
2603                    .active_pane()
2604                    .read(cx)
2605                    .active_item()
2606                    .unwrap()
2607                    .act_as::<Editor>(cx)
2608                    .unwrap()
2609                    .read(cx)
2610                    .title(cx),
2611                "a.txt"
2612            );
2613        });
2614
2615        // Open a file outside of any existing worktree.
2616        window
2617            .update(cx, |workspace, window, cx| {
2618                workspace.open_paths(
2619                    vec![path!("/dir2/b.txt").into()],
2620                    OpenOptions {
2621                        visible: Some(OpenVisible::All),
2622                        ..Default::default()
2623                    },
2624                    None,
2625                    window,
2626                    cx,
2627                )
2628            })
2629            .unwrap()
2630            .await;
2631        cx.read(|cx| {
2632            let workspace = workspace.read(cx);
2633            assert_project_panel_selection(
2634                workspace,
2635                Path::new(path!("/dir2/b.txt")),
2636                Path::new(""),
2637                cx,
2638            );
2639            let worktree_roots = workspace
2640                .worktrees(cx)
2641                .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref())
2642                .collect::<HashSet<_>>();
2643            assert_eq!(
2644                worktree_roots,
2645                vec![path!("/dir1"), path!("/dir2/b.txt")]
2646                    .into_iter()
2647                    .map(Path::new)
2648                    .collect(),
2649            );
2650            assert_eq!(
2651                workspace
2652                    .active_pane()
2653                    .read(cx)
2654                    .active_item()
2655                    .unwrap()
2656                    .act_as::<Editor>(cx)
2657                    .unwrap()
2658                    .read(cx)
2659                    .title(cx),
2660                "b.txt"
2661            );
2662        });
2663
2664        // Ensure opening a directory and one of its children only adds one worktree.
2665        window
2666            .update(cx, |workspace, window, cx| {
2667                workspace.open_paths(
2668                    vec![path!("/dir3").into(), path!("/dir3/c.txt").into()],
2669                    OpenOptions {
2670                        visible: Some(OpenVisible::All),
2671                        ..Default::default()
2672                    },
2673                    None,
2674                    window,
2675                    cx,
2676                )
2677            })
2678            .unwrap()
2679            .await;
2680        cx.read(|cx| {
2681            let workspace = workspace.read(cx);
2682            assert_project_panel_selection(
2683                workspace,
2684                Path::new(path!("/dir3")),
2685                Path::new("c.txt"),
2686                cx,
2687            );
2688            let worktree_roots = workspace
2689                .worktrees(cx)
2690                .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref())
2691                .collect::<HashSet<_>>();
2692            assert_eq!(
2693                worktree_roots,
2694                vec![path!("/dir1"), path!("/dir2/b.txt"), path!("/dir3")]
2695                    .into_iter()
2696                    .map(Path::new)
2697                    .collect(),
2698            );
2699            assert_eq!(
2700                workspace
2701                    .active_pane()
2702                    .read(cx)
2703                    .active_item()
2704                    .unwrap()
2705                    .act_as::<Editor>(cx)
2706                    .unwrap()
2707                    .read(cx)
2708                    .title(cx),
2709                "c.txt"
2710            );
2711        });
2712
2713        // Ensure opening invisibly a file outside an existing worktree adds a new, invisible worktree.
2714        window
2715            .update(cx, |workspace, window, cx| {
2716                workspace.open_paths(
2717                    vec![path!("/d.txt").into()],
2718                    OpenOptions {
2719                        visible: Some(OpenVisible::None),
2720                        ..Default::default()
2721                    },
2722                    None,
2723                    window,
2724                    cx,
2725                )
2726            })
2727            .unwrap()
2728            .await;
2729        cx.read(|cx| {
2730            let workspace = workspace.read(cx);
2731            assert_project_panel_selection(
2732                workspace,
2733                Path::new(path!("/d.txt")),
2734                Path::new(""),
2735                cx,
2736            );
2737            let worktree_roots = workspace
2738                .worktrees(cx)
2739                .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref())
2740                .collect::<HashSet<_>>();
2741            assert_eq!(
2742                worktree_roots,
2743                vec![
2744                    path!("/dir1"),
2745                    path!("/dir2/b.txt"),
2746                    path!("/dir3"),
2747                    path!("/d.txt")
2748                ]
2749                .into_iter()
2750                .map(Path::new)
2751                .collect(),
2752            );
2753
2754            let visible_worktree_roots = workspace
2755                .visible_worktrees(cx)
2756                .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref())
2757                .collect::<HashSet<_>>();
2758            assert_eq!(
2759                visible_worktree_roots,
2760                vec![path!("/dir1"), path!("/dir2/b.txt"), path!("/dir3")]
2761                    .into_iter()
2762                    .map(Path::new)
2763                    .collect(),
2764            );
2765
2766            assert_eq!(
2767                workspace
2768                    .active_pane()
2769                    .read(cx)
2770                    .active_item()
2771                    .unwrap()
2772                    .act_as::<Editor>(cx)
2773                    .unwrap()
2774                    .read(cx)
2775                    .title(cx),
2776                "d.txt"
2777            );
2778        });
2779    }
2780
2781    #[gpui::test]
2782    async fn test_opening_excluded_paths(cx: &mut TestAppContext) {
2783        let app_state = init_test(cx);
2784        cx.update(|cx| {
2785            cx.update_global::<SettingsStore, _>(|store, cx| {
2786                store.update_user_settings::<WorktreeSettings>(cx, |project_settings| {
2787                    project_settings.file_scan_exclusions =
2788                        Some(vec!["excluded_dir".to_string(), "**/.git".to_string()]);
2789                });
2790            });
2791        });
2792        app_state
2793            .fs
2794            .as_fake()
2795            .insert_tree(
2796                path!("/root"),
2797                json!({
2798                    ".gitignore": "ignored_dir\n",
2799                    ".git": {
2800                        "HEAD": "ref: refs/heads/main",
2801                    },
2802                    "regular_dir": {
2803                        "file": "regular file contents",
2804                    },
2805                    "ignored_dir": {
2806                        "ignored_subdir": {
2807                            "file": "ignored subfile contents",
2808                        },
2809                        "file": "ignored file contents",
2810                    },
2811                    "excluded_dir": {
2812                        "file": "excluded file contents",
2813                        "ignored_subdir": {
2814                            "file": "ignored subfile contents",
2815                        },
2816                    },
2817                }),
2818            )
2819            .await;
2820
2821        let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
2822        project.update(cx, |project, _cx| {
2823            project.languages().add(markdown_language())
2824        });
2825        let window = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
2826        let workspace = window.root(cx).unwrap();
2827
2828        let initial_entries = cx.read(|cx| workspace.file_project_paths(cx));
2829        let paths_to_open = [
2830            PathBuf::from(path!("/root/excluded_dir/file")),
2831            PathBuf::from(path!("/root/.git/HEAD")),
2832            PathBuf::from(path!("/root/excluded_dir/ignored_subdir")),
2833        ];
2834        let (opened_workspace, new_items) = cx
2835            .update(|cx| {
2836                workspace::open_paths(
2837                    &paths_to_open,
2838                    app_state,
2839                    workspace::OpenOptions::default(),
2840                    cx,
2841                )
2842            })
2843            .await
2844            .unwrap();
2845
2846        assert_eq!(
2847            opened_workspace.root(cx).unwrap().entity_id(),
2848            workspace.entity_id(),
2849            "Excluded files in subfolders of a workspace root should be opened in the workspace"
2850        );
2851        let mut opened_paths = cx.read(|cx| {
2852            assert_eq!(
2853                new_items.len(),
2854                paths_to_open.len(),
2855                "Expect to get the same number of opened items as submitted paths to open"
2856            );
2857            new_items
2858                .iter()
2859                .zip(paths_to_open.iter())
2860                .map(|(i, path)| {
2861                    match i {
2862                        Some(Ok(i)) => {
2863                            Some(i.project_path(cx).map(|p| p.path.display().to_string()))
2864                        }
2865                        Some(Err(e)) => panic!("Excluded file {path:?} failed to open: {e:?}"),
2866                        None => None,
2867                    }
2868                    .flatten()
2869                })
2870                .collect::<Vec<_>>()
2871        });
2872        opened_paths.sort();
2873        assert_eq!(
2874            opened_paths,
2875            vec![
2876                None,
2877                Some(path!(".git/HEAD").to_string()),
2878                Some(path!("excluded_dir/file").to_string()),
2879            ],
2880            "Excluded files should get opened, excluded dir should not get opened"
2881        );
2882
2883        let entries = cx.read(|cx| workspace.file_project_paths(cx));
2884        assert_eq!(
2885            initial_entries, entries,
2886            "Workspace entries should not change after opening excluded files and directories paths"
2887        );
2888
2889        cx.read(|cx| {
2890                let pane = workspace.read(cx).active_pane().read(cx);
2891                let mut opened_buffer_paths = pane
2892                    .items()
2893                    .map(|i| {
2894                        i.project_path(cx)
2895                            .expect("all excluded files that got open should have a path")
2896                            .path
2897                            .display()
2898                            .to_string()
2899                    })
2900                    .collect::<Vec<_>>();
2901                opened_buffer_paths.sort();
2902                assert_eq!(
2903                    opened_buffer_paths,
2904                    vec![path!(".git/HEAD").to_string(), path!("excluded_dir/file").to_string()],
2905                    "Despite not being present in the worktrees, buffers for excluded files are opened and added to the pane"
2906                );
2907            });
2908    }
2909
2910    #[gpui::test]
2911    async fn test_save_conflicting_item(cx: &mut TestAppContext) {
2912        let app_state = init_test(cx);
2913        app_state
2914            .fs
2915            .as_fake()
2916            .insert_tree(path!("/root"), json!({ "a.txt": "" }))
2917            .await;
2918
2919        let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
2920        project.update(cx, |project, _cx| {
2921            project.languages().add(markdown_language())
2922        });
2923        let window = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
2924        let workspace = window.root(cx).unwrap();
2925
2926        // Open a file within an existing worktree.
2927        window
2928            .update(cx, |workspace, window, cx| {
2929                workspace.open_paths(
2930                    vec![PathBuf::from(path!("/root/a.txt"))],
2931                    OpenOptions {
2932                        visible: Some(OpenVisible::All),
2933                        ..Default::default()
2934                    },
2935                    None,
2936                    window,
2937                    cx,
2938                )
2939            })
2940            .unwrap()
2941            .await;
2942        let editor = cx.read(|cx| {
2943            let pane = workspace.read(cx).active_pane().read(cx);
2944            let item = pane.active_item().unwrap();
2945            item.downcast::<Editor>().unwrap()
2946        });
2947
2948        window
2949            .update(cx, |_, window, cx| {
2950                editor.update(cx, |editor, cx| editor.handle_input("x", window, cx));
2951            })
2952            .unwrap();
2953
2954        app_state
2955            .fs
2956            .as_fake()
2957            .insert_file(path!("/root/a.txt"), b"changed".to_vec())
2958            .await;
2959
2960        cx.run_until_parked();
2961        cx.read(|cx| assert!(editor.is_dirty(cx)));
2962        cx.read(|cx| assert!(editor.has_conflict(cx)));
2963
2964        let save_task = window
2965            .update(cx, |workspace, window, cx| {
2966                workspace.save_active_item(SaveIntent::Save, window, cx)
2967            })
2968            .unwrap();
2969        cx.background_executor.run_until_parked();
2970        cx.simulate_prompt_answer("Overwrite");
2971        save_task.await.unwrap();
2972        window
2973            .update(cx, |_, _, cx| {
2974                editor.update(cx, |editor, cx| {
2975                    assert!(!editor.is_dirty(cx));
2976                    assert!(!editor.has_conflict(cx));
2977                });
2978            })
2979            .unwrap();
2980    }
2981
2982    #[gpui::test]
2983    async fn test_open_and_save_new_file(cx: &mut TestAppContext) {
2984        let app_state = init_test(cx);
2985        app_state
2986            .fs
2987            .create_dir(Path::new(path!("/root")))
2988            .await
2989            .unwrap();
2990
2991        let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
2992        project.update(cx, |project, _| {
2993            project.languages().add(markdown_language());
2994            project.languages().add(rust_lang());
2995        });
2996        let window = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
2997        let worktree = cx.update(|cx| window.read(cx).unwrap().worktrees(cx).next().unwrap());
2998
2999        // Create a new untitled buffer
3000        cx.dispatch_action(window.into(), NewFile);
3001        let editor = window
3002            .read_with(cx, |workspace, cx| {
3003                workspace
3004                    .active_item(cx)
3005                    .unwrap()
3006                    .downcast::<Editor>()
3007                    .unwrap()
3008            })
3009            .unwrap();
3010
3011        window
3012            .update(cx, |_, window, cx| {
3013                editor.update(cx, |editor, cx| {
3014                    assert!(!editor.is_dirty(cx));
3015                    assert_eq!(editor.title(cx), "untitled");
3016                    assert!(Arc::ptr_eq(
3017                        &editor.buffer().read(cx).language_at(0, cx).unwrap(),
3018                        &languages::PLAIN_TEXT
3019                    ));
3020                    editor.handle_input("hi", window, cx);
3021                    assert!(editor.is_dirty(cx));
3022                });
3023            })
3024            .unwrap();
3025
3026        // Save the buffer. This prompts for a filename.
3027        let save_task = window
3028            .update(cx, |workspace, window, cx| {
3029                workspace.save_active_item(SaveIntent::Save, window, cx)
3030            })
3031            .unwrap();
3032        cx.background_executor.run_until_parked();
3033        cx.simulate_new_path_selection(|parent_dir| {
3034            assert_eq!(parent_dir, Path::new(path!("/root")));
3035            Some(parent_dir.join("the-new-name.rs"))
3036        });
3037        cx.read(|cx| {
3038            assert!(editor.is_dirty(cx));
3039            assert_eq!(editor.read(cx).title(cx), "hi");
3040        });
3041
3042        // When the save completes, the buffer's title is updated and the language is assigned based
3043        // on the path.
3044        save_task.await.unwrap();
3045        window
3046            .update(cx, |_, _, cx| {
3047                editor.update(cx, |editor, cx| {
3048                    assert!(!editor.is_dirty(cx));
3049                    assert_eq!(editor.title(cx), "the-new-name.rs");
3050                    assert_eq!(
3051                        editor.buffer().read(cx).language_at(0, cx).unwrap().name(),
3052                        "Rust".into()
3053                    );
3054                });
3055            })
3056            .unwrap();
3057
3058        // Edit the file and save it again. This time, there is no filename prompt.
3059        window
3060            .update(cx, |_, window, cx| {
3061                editor.update(cx, |editor, cx| {
3062                    editor.handle_input(" there", window, cx);
3063                    assert!(editor.is_dirty(cx));
3064                });
3065            })
3066            .unwrap();
3067
3068        let save_task = window
3069            .update(cx, |workspace, window, cx| {
3070                workspace.save_active_item(SaveIntent::Save, window, cx)
3071            })
3072            .unwrap();
3073        save_task.await.unwrap();
3074
3075        assert!(!cx.did_prompt_for_new_path());
3076        window
3077            .update(cx, |_, _, cx| {
3078                editor.update(cx, |editor, cx| {
3079                    assert!(!editor.is_dirty(cx));
3080                    assert_eq!(editor.title(cx), "the-new-name.rs")
3081                });
3082            })
3083            .unwrap();
3084
3085        // Open the same newly-created file in another pane item. The new editor should reuse
3086        // the same buffer.
3087        cx.dispatch_action(window.into(), NewFile);
3088        window
3089            .update(cx, |workspace, window, cx| {
3090                workspace.split_and_clone(
3091                    workspace.active_pane().clone(),
3092                    SplitDirection::Right,
3093                    window,
3094                    cx,
3095                );
3096                workspace.open_path(
3097                    (worktree.read(cx).id(), "the-new-name.rs"),
3098                    None,
3099                    true,
3100                    window,
3101                    cx,
3102                )
3103            })
3104            .unwrap()
3105            .await
3106            .unwrap();
3107        let editor2 = window
3108            .update(cx, |workspace, _, cx| {
3109                workspace
3110                    .active_item(cx)
3111                    .unwrap()
3112                    .downcast::<Editor>()
3113                    .unwrap()
3114            })
3115            .unwrap();
3116        cx.read(|cx| {
3117            assert_eq!(
3118                editor2.read(cx).buffer().read(cx).as_singleton().unwrap(),
3119                editor.read(cx).buffer().read(cx).as_singleton().unwrap()
3120            );
3121        })
3122    }
3123
3124    #[gpui::test]
3125    async fn test_setting_language_when_saving_as_single_file_worktree(cx: &mut TestAppContext) {
3126        let app_state = init_test(cx);
3127        app_state.fs.create_dir(Path::new("/root")).await.unwrap();
3128
3129        let project = Project::test(app_state.fs.clone(), [], cx).await;
3130        project.update(cx, |project, _| {
3131            project.languages().add(rust_lang());
3132            project.languages().add(markdown_language());
3133        });
3134        let window = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
3135
3136        // Create a new untitled buffer
3137        cx.dispatch_action(window.into(), NewFile);
3138        let editor = window
3139            .read_with(cx, |workspace, cx| {
3140                workspace
3141                    .active_item(cx)
3142                    .unwrap()
3143                    .downcast::<Editor>()
3144                    .unwrap()
3145            })
3146            .unwrap();
3147        window
3148            .update(cx, |_, window, cx| {
3149                editor.update(cx, |editor, cx| {
3150                    assert!(Arc::ptr_eq(
3151                        &editor.buffer().read(cx).language_at(0, cx).unwrap(),
3152                        &languages::PLAIN_TEXT
3153                    ));
3154                    editor.handle_input("hi", window, cx);
3155                    assert!(editor.is_dirty(cx));
3156                });
3157            })
3158            .unwrap();
3159
3160        // Save the buffer. This prompts for a filename.
3161        let save_task = window
3162            .update(cx, |workspace, window, cx| {
3163                workspace.save_active_item(SaveIntent::Save, window, cx)
3164            })
3165            .unwrap();
3166        cx.background_executor.run_until_parked();
3167        cx.simulate_new_path_selection(|_| Some(PathBuf::from("/root/the-new-name.rs")));
3168        save_task.await.unwrap();
3169        // The buffer is not dirty anymore and the language is assigned based on the path.
3170        window
3171            .update(cx, |_, _, cx| {
3172                editor.update(cx, |editor, cx| {
3173                    assert!(!editor.is_dirty(cx));
3174                    assert_eq!(
3175                        editor.buffer().read(cx).language_at(0, cx).unwrap().name(),
3176                        "Rust".into()
3177                    )
3178                });
3179            })
3180            .unwrap();
3181    }
3182
3183    #[gpui::test]
3184    async fn test_pane_actions(cx: &mut TestAppContext) {
3185        let app_state = init_test(cx);
3186        app_state
3187            .fs
3188            .as_fake()
3189            .insert_tree(
3190                path!("/root"),
3191                json!({
3192                    "a": {
3193                        "file1": "contents 1",
3194                        "file2": "contents 2",
3195                        "file3": "contents 3",
3196                    },
3197                }),
3198            )
3199            .await;
3200
3201        let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
3202        project.update(cx, |project, _cx| {
3203            project.languages().add(markdown_language())
3204        });
3205        let window = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
3206        let workspace = window.root(cx).unwrap();
3207
3208        let entries = cx.read(|cx| workspace.file_project_paths(cx));
3209        let file1 = entries[0].clone();
3210
3211        let pane_1 = cx.read(|cx| workspace.read(cx).active_pane().clone());
3212
3213        window
3214            .update(cx, |w, window, cx| {
3215                w.open_path(file1.clone(), None, true, window, cx)
3216            })
3217            .unwrap()
3218            .await
3219            .unwrap();
3220
3221        let (editor_1, buffer) = window
3222            .update(cx, |_, window, cx| {
3223                pane_1.update(cx, |pane_1, cx| {
3224                    let editor = pane_1.active_item().unwrap().downcast::<Editor>().unwrap();
3225                    assert_eq!(editor.project_path(cx), Some(file1.clone()));
3226                    let buffer = editor.update(cx, |editor, cx| {
3227                        editor.insert("dirt", window, cx);
3228                        editor.buffer().downgrade()
3229                    });
3230                    (editor.downgrade(), buffer)
3231                })
3232            })
3233            .unwrap();
3234
3235        cx.dispatch_action(window.into(), pane::SplitRight);
3236        let editor_2 = cx.update(|cx| {
3237            let pane_2 = workspace.read(cx).active_pane().clone();
3238            assert_ne!(pane_1, pane_2);
3239
3240            let pane2_item = pane_2.read(cx).active_item().unwrap();
3241            assert_eq!(pane2_item.project_path(cx), Some(file1.clone()));
3242
3243            pane2_item.downcast::<Editor>().unwrap().downgrade()
3244        });
3245        cx.dispatch_action(
3246            window.into(),
3247            workspace::CloseActiveItem {
3248                save_intent: None,
3249                close_pinned: false,
3250            },
3251        );
3252
3253        cx.background_executor.run_until_parked();
3254        window
3255            .read_with(cx, |workspace, _| {
3256                assert_eq!(workspace.panes().len(), 1);
3257                assert_eq!(workspace.active_pane(), &pane_1);
3258            })
3259            .unwrap();
3260
3261        cx.dispatch_action(
3262            window.into(),
3263            workspace::CloseActiveItem {
3264                save_intent: None,
3265                close_pinned: false,
3266            },
3267        );
3268        cx.background_executor.run_until_parked();
3269        cx.simulate_prompt_answer("Don't Save");
3270        cx.background_executor.run_until_parked();
3271
3272        window
3273            .update(cx, |workspace, _, cx| {
3274                assert_eq!(workspace.panes().len(), 1);
3275                assert!(workspace.active_item(cx).is_none());
3276            })
3277            .unwrap();
3278
3279        cx.background_executor
3280            .advance_clock(SERIALIZATION_THROTTLE_TIME);
3281        cx.update(|_| {});
3282        editor_1.assert_released();
3283        editor_2.assert_released();
3284        buffer.assert_released();
3285    }
3286
3287    #[gpui::test]
3288    async fn test_navigation(cx: &mut TestAppContext) {
3289        let app_state = init_test(cx);
3290        app_state
3291            .fs
3292            .as_fake()
3293            .insert_tree(
3294                path!("/root"),
3295                json!({
3296                    "a": {
3297                        "file1": "contents 1\n".repeat(20),
3298                        "file2": "contents 2\n".repeat(20),
3299                        "file3": "contents 3\n".repeat(20),
3300                    },
3301                }),
3302            )
3303            .await;
3304
3305        let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
3306        project.update(cx, |project, _cx| {
3307            project.languages().add(markdown_language())
3308        });
3309        let workspace =
3310            cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
3311        let pane = workspace
3312            .read_with(cx, |workspace, _| workspace.active_pane().clone())
3313            .unwrap();
3314
3315        let entries = cx.update(|cx| workspace.root(cx).unwrap().file_project_paths(cx));
3316        let file1 = entries[0].clone();
3317        let file2 = entries[1].clone();
3318        let file3 = entries[2].clone();
3319
3320        let editor1 = workspace
3321            .update(cx, |w, window, cx| {
3322                w.open_path(file1.clone(), None, true, window, cx)
3323            })
3324            .unwrap()
3325            .await
3326            .unwrap()
3327            .downcast::<Editor>()
3328            .unwrap();
3329        workspace
3330            .update(cx, |_, window, cx| {
3331                editor1.update(cx, |editor, cx| {
3332                    editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
3333                        s.select_display_ranges([DisplayPoint::new(DisplayRow(10), 0)
3334                            ..DisplayPoint::new(DisplayRow(10), 0)])
3335                    });
3336                });
3337            })
3338            .unwrap();
3339
3340        let editor2 = workspace
3341            .update(cx, |w, window, cx| {
3342                w.open_path(file2.clone(), None, true, window, cx)
3343            })
3344            .unwrap()
3345            .await
3346            .unwrap()
3347            .downcast::<Editor>()
3348            .unwrap();
3349        let editor3 = workspace
3350            .update(cx, |w, window, cx| {
3351                w.open_path(file3.clone(), None, true, window, cx)
3352            })
3353            .unwrap()
3354            .await
3355            .unwrap()
3356            .downcast::<Editor>()
3357            .unwrap();
3358
3359        workspace
3360            .update(cx, |_, window, cx| {
3361                editor3.update(cx, |editor, cx| {
3362                    editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
3363                        s.select_display_ranges([DisplayPoint::new(DisplayRow(12), 0)
3364                            ..DisplayPoint::new(DisplayRow(12), 0)])
3365                    });
3366                    editor.newline(&Default::default(), window, cx);
3367                    editor.newline(&Default::default(), window, cx);
3368                    editor.move_down(&Default::default(), window, cx);
3369                    editor.move_down(&Default::default(), window, cx);
3370                    editor.save(
3371                        SaveOptions {
3372                            format: true,
3373                            autosave: false,
3374                        },
3375                        project.clone(),
3376                        window,
3377                        cx,
3378                    )
3379                })
3380            })
3381            .unwrap()
3382            .await
3383            .unwrap();
3384        workspace
3385            .update(cx, |_, window, cx| {
3386                editor3.update(cx, |editor, cx| {
3387                    editor.set_scroll_position(point(0., 12.5), window, cx)
3388                });
3389            })
3390            .unwrap();
3391        assert_eq!(
3392            active_location(&workspace, cx),
3393            (file3.clone(), DisplayPoint::new(DisplayRow(16), 0), 12.5)
3394        );
3395
3396        workspace
3397            .update(cx, |w, window, cx| {
3398                w.go_back(w.active_pane().downgrade(), window, cx)
3399            })
3400            .unwrap()
3401            .await
3402            .unwrap();
3403        assert_eq!(
3404            active_location(&workspace, cx),
3405            (file3.clone(), DisplayPoint::new(DisplayRow(0), 0), 0.)
3406        );
3407
3408        workspace
3409            .update(cx, |w, window, cx| {
3410                w.go_back(w.active_pane().downgrade(), window, cx)
3411            })
3412            .unwrap()
3413            .await
3414            .unwrap();
3415        assert_eq!(
3416            active_location(&workspace, cx),
3417            (file2.clone(), DisplayPoint::new(DisplayRow(0), 0), 0.)
3418        );
3419
3420        workspace
3421            .update(cx, |w, window, cx| {
3422                w.go_back(w.active_pane().downgrade(), window, cx)
3423            })
3424            .unwrap()
3425            .await
3426            .unwrap();
3427        assert_eq!(
3428            active_location(&workspace, cx),
3429            (file1.clone(), DisplayPoint::new(DisplayRow(10), 0), 0.)
3430        );
3431
3432        workspace
3433            .update(cx, |w, window, cx| {
3434                w.go_back(w.active_pane().downgrade(), window, cx)
3435            })
3436            .unwrap()
3437            .await
3438            .unwrap();
3439        assert_eq!(
3440            active_location(&workspace, cx),
3441            (file1.clone(), DisplayPoint::new(DisplayRow(0), 0), 0.)
3442        );
3443
3444        // Go back one more time and ensure we don't navigate past the first item in the history.
3445        workspace
3446            .update(cx, |w, window, cx| {
3447                w.go_back(w.active_pane().downgrade(), window, cx)
3448            })
3449            .unwrap()
3450            .await
3451            .unwrap();
3452        assert_eq!(
3453            active_location(&workspace, cx),
3454            (file1.clone(), DisplayPoint::new(DisplayRow(0), 0), 0.)
3455        );
3456
3457        workspace
3458            .update(cx, |w, window, cx| {
3459                w.go_forward(w.active_pane().downgrade(), window, cx)
3460            })
3461            .unwrap()
3462            .await
3463            .unwrap();
3464        assert_eq!(
3465            active_location(&workspace, cx),
3466            (file1.clone(), DisplayPoint::new(DisplayRow(10), 0), 0.)
3467        );
3468
3469        workspace
3470            .update(cx, |w, window, cx| {
3471                w.go_forward(w.active_pane().downgrade(), window, cx)
3472            })
3473            .unwrap()
3474            .await
3475            .unwrap();
3476        assert_eq!(
3477            active_location(&workspace, cx),
3478            (file2.clone(), DisplayPoint::new(DisplayRow(0), 0), 0.)
3479        );
3480
3481        // Go forward to an item that has been closed, ensuring it gets re-opened at the same
3482        // location.
3483        workspace
3484            .update(cx, |_, window, cx| {
3485                pane.update(cx, |pane, cx| {
3486                    let editor3_id = editor3.entity_id();
3487                    drop(editor3);
3488                    pane.close_item_by_id(editor3_id, SaveIntent::Close, window, cx)
3489                })
3490            })
3491            .unwrap()
3492            .await
3493            .unwrap();
3494        workspace
3495            .update(cx, |w, window, cx| {
3496                w.go_forward(w.active_pane().downgrade(), window, cx)
3497            })
3498            .unwrap()
3499            .await
3500            .unwrap();
3501        assert_eq!(
3502            active_location(&workspace, cx),
3503            (file3.clone(), DisplayPoint::new(DisplayRow(0), 0), 0.)
3504        );
3505
3506        workspace
3507            .update(cx, |w, window, cx| {
3508                w.go_forward(w.active_pane().downgrade(), window, cx)
3509            })
3510            .unwrap()
3511            .await
3512            .unwrap();
3513        assert_eq!(
3514            active_location(&workspace, cx),
3515            (file3.clone(), DisplayPoint::new(DisplayRow(16), 0), 12.5)
3516        );
3517
3518        workspace
3519            .update(cx, |w, window, cx| {
3520                w.go_back(w.active_pane().downgrade(), window, cx)
3521            })
3522            .unwrap()
3523            .await
3524            .unwrap();
3525        assert_eq!(
3526            active_location(&workspace, cx),
3527            (file3.clone(), DisplayPoint::new(DisplayRow(0), 0), 0.)
3528        );
3529
3530        // Go back to an item that has been closed and removed from disk
3531        workspace
3532            .update(cx, |_, window, cx| {
3533                pane.update(cx, |pane, cx| {
3534                    let editor2_id = editor2.entity_id();
3535                    drop(editor2);
3536                    pane.close_item_by_id(editor2_id, SaveIntent::Close, window, cx)
3537                })
3538            })
3539            .unwrap()
3540            .await
3541            .unwrap();
3542        app_state
3543            .fs
3544            .remove_file(Path::new(path!("/root/a/file2")), Default::default())
3545            .await
3546            .unwrap();
3547        cx.background_executor.run_until_parked();
3548
3549        workspace
3550            .update(cx, |w, window, cx| {
3551                w.go_back(w.active_pane().downgrade(), window, cx)
3552            })
3553            .unwrap()
3554            .await
3555            .unwrap();
3556        assert_eq!(
3557            active_location(&workspace, cx),
3558            (file2.clone(), DisplayPoint::new(DisplayRow(0), 0), 0.)
3559        );
3560        workspace
3561            .update(cx, |w, window, cx| {
3562                w.go_forward(w.active_pane().downgrade(), window, cx)
3563            })
3564            .unwrap()
3565            .await
3566            .unwrap();
3567        assert_eq!(
3568            active_location(&workspace, cx),
3569            (file3.clone(), DisplayPoint::new(DisplayRow(0), 0), 0.)
3570        );
3571
3572        // Modify file to collapse multiple nav history entries into the same location.
3573        // Ensure we don't visit the same location twice when navigating.
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(15), 0)
3579                            ..DisplayPoint::new(DisplayRow(15), 0)])
3580                    })
3581                });
3582            })
3583            .unwrap();
3584        for _ in 0..5 {
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(3), 0)
3590                                ..DisplayPoint::new(DisplayRow(3), 0)])
3591                        });
3592                    });
3593                })
3594                .unwrap();
3595
3596            workspace
3597                .update(cx, |_, window, cx| {
3598                    editor1.update(cx, |editor, cx| {
3599                        editor.change_selections(None, window, cx, |s| {
3600                            s.select_display_ranges([DisplayPoint::new(DisplayRow(13), 0)
3601                                ..DisplayPoint::new(DisplayRow(13), 0)])
3602                        })
3603                    });
3604                })
3605                .unwrap();
3606        }
3607        workspace
3608            .update(cx, |_, window, cx| {
3609                editor1.update(cx, |editor, cx| {
3610                    editor.transact(window, cx, |editor, window, cx| {
3611                        editor.change_selections(None, window, cx, |s| {
3612                            s.select_display_ranges([DisplayPoint::new(DisplayRow(2), 0)
3613                                ..DisplayPoint::new(DisplayRow(14), 0)])
3614                        });
3615                        editor.insert("", window, cx);
3616                    })
3617                });
3618            })
3619            .unwrap();
3620
3621        workspace
3622            .update(cx, |_, window, cx| {
3623                editor1.update(cx, |editor, cx| {
3624                    editor.change_selections(None, window, cx, |s| {
3625                        s.select_display_ranges([DisplayPoint::new(DisplayRow(1), 0)
3626                            ..DisplayPoint::new(DisplayRow(1), 0)])
3627                    })
3628                });
3629            })
3630            .unwrap();
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(2), 0), 0.)
3641        );
3642        workspace
3643            .update(cx, |w, window, cx| {
3644                w.go_back(w.active_pane().downgrade(), window, cx)
3645            })
3646            .unwrap()
3647            .await
3648            .unwrap();
3649        assert_eq!(
3650            active_location(&workspace, cx),
3651            (file1.clone(), DisplayPoint::new(DisplayRow(3), 0), 0.)
3652        );
3653
3654        fn active_location(
3655            workspace: &WindowHandle<Workspace>,
3656            cx: &mut TestAppContext,
3657        ) -> (ProjectPath, DisplayPoint, f32) {
3658            workspace
3659                .update(cx, |workspace, _, cx| {
3660                    let item = workspace.active_item(cx).unwrap();
3661                    let editor = item.downcast::<Editor>().unwrap();
3662                    let (selections, scroll_position) = editor.update(cx, |editor, cx| {
3663                        (
3664                            editor.selections.display_ranges(cx),
3665                            editor.scroll_position(cx),
3666                        )
3667                    });
3668                    (
3669                        item.project_path(cx).unwrap(),
3670                        selections[0].start,
3671                        scroll_position.y,
3672                    )
3673                })
3674                .unwrap()
3675        }
3676    }
3677
3678    #[gpui::test]
3679    async fn test_reopening_closed_items(cx: &mut TestAppContext) {
3680        let app_state = init_test(cx);
3681        app_state
3682            .fs
3683            .as_fake()
3684            .insert_tree(
3685                path!("/root"),
3686                json!({
3687                    "a": {
3688                        "file1": "",
3689                        "file2": "",
3690                        "file3": "",
3691                        "file4": "",
3692                    },
3693                }),
3694            )
3695            .await;
3696
3697        let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
3698        project.update(cx, |project, _cx| {
3699            project.languages().add(markdown_language())
3700        });
3701        let workspace = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
3702        let pane = workspace
3703            .read_with(cx, |workspace, _| workspace.active_pane().clone())
3704            .unwrap();
3705
3706        let entries = cx.update(|cx| workspace.root(cx).unwrap().file_project_paths(cx));
3707        let file1 = entries[0].clone();
3708        let file2 = entries[1].clone();
3709        let file3 = entries[2].clone();
3710        let file4 = entries[3].clone();
3711
3712        let file1_item_id = workspace
3713            .update(cx, |w, window, cx| {
3714                w.open_path(file1.clone(), None, true, window, cx)
3715            })
3716            .unwrap()
3717            .await
3718            .unwrap()
3719            .item_id();
3720        let file2_item_id = workspace
3721            .update(cx, |w, window, cx| {
3722                w.open_path(file2.clone(), None, true, window, cx)
3723            })
3724            .unwrap()
3725            .await
3726            .unwrap()
3727            .item_id();
3728        let file3_item_id = workspace
3729            .update(cx, |w, window, cx| {
3730                w.open_path(file3.clone(), None, true, window, cx)
3731            })
3732            .unwrap()
3733            .await
3734            .unwrap()
3735            .item_id();
3736        let file4_item_id = workspace
3737            .update(cx, |w, window, cx| {
3738                w.open_path(file4.clone(), None, true, window, cx)
3739            })
3740            .unwrap()
3741            .await
3742            .unwrap()
3743            .item_id();
3744        assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
3745
3746        // Close all the pane items in some arbitrary order.
3747        workspace
3748            .update(cx, |_, window, cx| {
3749                pane.update(cx, |pane, cx| {
3750                    pane.close_item_by_id(file1_item_id, SaveIntent::Close, window, cx)
3751                })
3752            })
3753            .unwrap()
3754            .await
3755            .unwrap();
3756        assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
3757
3758        workspace
3759            .update(cx, |_, window, cx| {
3760                pane.update(cx, |pane, cx| {
3761                    pane.close_item_by_id(file4_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
3769        workspace
3770            .update(cx, |_, window, cx| {
3771                pane.update(cx, |pane, cx| {
3772                    pane.close_item_by_id(file2_item_id, SaveIntent::Close, window, cx)
3773                })
3774            })
3775            .unwrap()
3776            .await
3777            .unwrap();
3778        assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
3779        workspace
3780            .update(cx, |_, window, cx| {
3781                pane.update(cx, |pane, cx| {
3782                    pane.close_item_by_id(file3_item_id, SaveIntent::Close, window, cx)
3783                })
3784            })
3785            .unwrap()
3786            .await
3787            .unwrap();
3788
3789        assert_eq!(active_path(&workspace, cx), None);
3790
3791        // Reopen all the closed items, ensuring they are reopened in the same order
3792        // in which they were closed.
3793        workspace
3794            .update(cx, Workspace::reopen_closed_item)
3795            .unwrap()
3796            .await
3797            .unwrap();
3798        assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
3799
3800        workspace
3801            .update(cx, Workspace::reopen_closed_item)
3802            .unwrap()
3803            .await
3804            .unwrap();
3805        assert_eq!(active_path(&workspace, cx), Some(file2.clone()));
3806
3807        workspace
3808            .update(cx, Workspace::reopen_closed_item)
3809            .unwrap()
3810            .await
3811            .unwrap();
3812        assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
3813
3814        workspace
3815            .update(cx, Workspace::reopen_closed_item)
3816            .unwrap()
3817            .await
3818            .unwrap();
3819        assert_eq!(active_path(&workspace, cx), Some(file1.clone()));
3820
3821        // Reopening past the last closed item is a no-op.
3822        workspace
3823            .update(cx, Workspace::reopen_closed_item)
3824            .unwrap()
3825            .await
3826            .unwrap();
3827        assert_eq!(active_path(&workspace, cx), Some(file1.clone()));
3828
3829        // Reopening closed items doesn't interfere with navigation history.
3830        workspace
3831            .update(cx, |workspace, window, cx| {
3832                workspace.go_back(workspace.active_pane().downgrade(), window, cx)
3833            })
3834            .unwrap()
3835            .await
3836            .unwrap();
3837        assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
3838
3839        workspace
3840            .update(cx, |workspace, window, cx| {
3841                workspace.go_back(workspace.active_pane().downgrade(), window, cx)
3842            })
3843            .unwrap()
3844            .await
3845            .unwrap();
3846        assert_eq!(active_path(&workspace, cx), Some(file2.clone()));
3847
3848        workspace
3849            .update(cx, |workspace, window, cx| {
3850                workspace.go_back(workspace.active_pane().downgrade(), window, cx)
3851            })
3852            .unwrap()
3853            .await
3854            .unwrap();
3855        assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
3856
3857        workspace
3858            .update(cx, |workspace, window, cx| {
3859                workspace.go_back(workspace.active_pane().downgrade(), window, cx)
3860            })
3861            .unwrap()
3862            .await
3863            .unwrap();
3864        assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
3865
3866        workspace
3867            .update(cx, |workspace, window, cx| {
3868                workspace.go_back(workspace.active_pane().downgrade(), window, cx)
3869            })
3870            .unwrap()
3871            .await
3872            .unwrap();
3873        assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
3874
3875        workspace
3876            .update(cx, |workspace, window, cx| {
3877                workspace.go_back(workspace.active_pane().downgrade(), window, cx)
3878            })
3879            .unwrap()
3880            .await
3881            .unwrap();
3882        assert_eq!(active_path(&workspace, cx), Some(file2.clone()));
3883
3884        workspace
3885            .update(cx, |workspace, window, cx| {
3886                workspace.go_back(workspace.active_pane().downgrade(), window, cx)
3887            })
3888            .unwrap()
3889            .await
3890            .unwrap();
3891        assert_eq!(active_path(&workspace, cx), Some(file1.clone()));
3892
3893        workspace
3894            .update(cx, |workspace, window, cx| {
3895                workspace.go_back(workspace.active_pane().downgrade(), window, cx)
3896            })
3897            .unwrap()
3898            .await
3899            .unwrap();
3900        assert_eq!(active_path(&workspace, cx), Some(file1.clone()));
3901
3902        fn active_path(
3903            workspace: &WindowHandle<Workspace>,
3904            cx: &TestAppContext,
3905        ) -> Option<ProjectPath> {
3906            workspace
3907                .read_with(cx, |workspace, cx| {
3908                    let item = workspace.active_item(cx)?;
3909                    item.project_path(cx)
3910                })
3911                .unwrap()
3912        }
3913    }
3914
3915    fn init_keymap_test(cx: &mut TestAppContext) -> Arc<AppState> {
3916        cx.update(|cx| {
3917            let app_state = AppState::test(cx);
3918
3919            theme::init(theme::LoadThemes::JustBase, cx);
3920            client::init(&app_state.client, cx);
3921            language::init(cx);
3922            workspace::init(app_state.clone(), cx);
3923            welcome::init(cx);
3924            Project::init_settings(cx);
3925            app_state
3926        })
3927    }
3928
3929    #[gpui::test]
3930    async fn test_base_keymap(cx: &mut gpui::TestAppContext) {
3931        let executor = cx.executor();
3932        let app_state = init_keymap_test(cx);
3933        let project = Project::test(app_state.fs.clone(), [], cx).await;
3934        let workspace =
3935            cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
3936
3937        actions!(test1, [A, B]);
3938        // From the Atom keymap
3939        use workspace::ActivatePreviousPane;
3940        // From the JetBrains keymap
3941        use workspace::ActivatePreviousItem;
3942
3943        app_state
3944            .fs
3945            .save(
3946                "/settings.json".as_ref(),
3947                &r#"{"base_keymap": "Atom"}"#.into(),
3948                Default::default(),
3949            )
3950            .await
3951            .unwrap();
3952
3953        app_state
3954            .fs
3955            .save(
3956                "/keymap.json".as_ref(),
3957                &r#"[{"bindings": {"backspace": "test1::A"}}]"#.into(),
3958                Default::default(),
3959            )
3960            .await
3961            .unwrap();
3962        executor.run_until_parked();
3963        cx.update(|cx| {
3964            let settings_rx = watch_config_file(
3965                &executor,
3966                app_state.fs.clone(),
3967                PathBuf::from("/settings.json"),
3968            );
3969            let keymap_rx = watch_config_file(
3970                &executor,
3971                app_state.fs.clone(),
3972                PathBuf::from("/keymap.json"),
3973            );
3974            let global_settings_rx = watch_config_file(
3975                &executor,
3976                app_state.fs.clone(),
3977                PathBuf::from("/global_settings.json"),
3978            );
3979            handle_settings_file_changes(settings_rx, global_settings_rx, cx, |_, _| {});
3980            handle_keymap_file_changes(keymap_rx, cx);
3981        });
3982        workspace
3983            .update(cx, |workspace, _, cx| {
3984                workspace.register_action(|_, _: &A, _window, _cx| {});
3985                workspace.register_action(|_, _: &B, _window, _cx| {});
3986                workspace.register_action(|_, _: &ActivatePreviousPane, _window, _cx| {});
3987                workspace.register_action(|_, _: &ActivatePreviousItem, _window, _cx| {});
3988                cx.notify();
3989            })
3990            .unwrap();
3991        executor.run_until_parked();
3992        // Test loading the keymap base at all
3993        assert_key_bindings_for(
3994            workspace.into(),
3995            cx,
3996            vec![("backspace", &A), ("k", &ActivatePreviousPane)],
3997            line!(),
3998        );
3999
4000        // Test modifying the users keymap, while retaining the base keymap
4001        app_state
4002            .fs
4003            .save(
4004                "/keymap.json".as_ref(),
4005                &r#"[{"bindings": {"backspace": "test1::B"}}]"#.into(),
4006                Default::default(),
4007            )
4008            .await
4009            .unwrap();
4010
4011        executor.run_until_parked();
4012
4013        assert_key_bindings_for(
4014            workspace.into(),
4015            cx,
4016            vec![("backspace", &B), ("k", &ActivatePreviousPane)],
4017            line!(),
4018        );
4019
4020        // Test modifying the base, while retaining the users keymap
4021        app_state
4022            .fs
4023            .save(
4024                "/settings.json".as_ref(),
4025                &r#"{"base_keymap": "JetBrains"}"#.into(),
4026                Default::default(),
4027            )
4028            .await
4029            .unwrap();
4030
4031        executor.run_until_parked();
4032
4033        assert_key_bindings_for(
4034            workspace.into(),
4035            cx,
4036            vec![("backspace", &B), ("{", &ActivatePreviousItem)],
4037            line!(),
4038        );
4039    }
4040
4041    #[gpui::test]
4042    async fn test_disabled_keymap_binding(cx: &mut gpui::TestAppContext) {
4043        let executor = cx.executor();
4044        let app_state = init_keymap_test(cx);
4045        let project = Project::test(app_state.fs.clone(), [], cx).await;
4046        let workspace =
4047            cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
4048
4049        actions!(test2, [A, B]);
4050        // From the Atom keymap
4051        use workspace::ActivatePreviousPane;
4052        // From the JetBrains keymap
4053        use diagnostics::Deploy;
4054
4055        workspace
4056            .update(cx, |workspace, _, _| {
4057                workspace.register_action(|_, _: &A, _window, _cx| {});
4058                workspace.register_action(|_, _: &B, _window, _cx| {});
4059                workspace.register_action(|_, _: &Deploy, _window, _cx| {});
4060            })
4061            .unwrap();
4062        app_state
4063            .fs
4064            .save(
4065                "/settings.json".as_ref(),
4066                &r#"{"base_keymap": "Atom"}"#.into(),
4067                Default::default(),
4068            )
4069            .await
4070            .unwrap();
4071        app_state
4072            .fs
4073            .save(
4074                "/keymap.json".as_ref(),
4075                &r#"[{"bindings": {"backspace": "test2::A"}}]"#.into(),
4076                Default::default(),
4077            )
4078            .await
4079            .unwrap();
4080
4081        cx.update(|cx| {
4082            let settings_rx = watch_config_file(
4083                &executor,
4084                app_state.fs.clone(),
4085                PathBuf::from("/settings.json"),
4086            );
4087            let keymap_rx = watch_config_file(
4088                &executor,
4089                app_state.fs.clone(),
4090                PathBuf::from("/keymap.json"),
4091            );
4092
4093            let global_settings_rx = watch_config_file(
4094                &executor,
4095                app_state.fs.clone(),
4096                PathBuf::from("/global_settings.json"),
4097            );
4098            handle_settings_file_changes(settings_rx, global_settings_rx, cx, |_, _| {});
4099            handle_keymap_file_changes(keymap_rx, cx);
4100        });
4101
4102        cx.background_executor.run_until_parked();
4103
4104        cx.background_executor.run_until_parked();
4105        // Test loading the keymap base at all
4106        assert_key_bindings_for(
4107            workspace.into(),
4108            cx,
4109            vec![("backspace", &A), ("k", &ActivatePreviousPane)],
4110            line!(),
4111        );
4112
4113        // Test disabling the key binding for the base keymap
4114        app_state
4115            .fs
4116            .save(
4117                "/keymap.json".as_ref(),
4118                &r#"[{"bindings": {"backspace": null}}]"#.into(),
4119                Default::default(),
4120            )
4121            .await
4122            .unwrap();
4123
4124        cx.background_executor.run_until_parked();
4125
4126        assert_key_bindings_for(
4127            workspace.into(),
4128            cx,
4129            vec![("k", &ActivatePreviousPane)],
4130            line!(),
4131        );
4132
4133        // Test modifying the base, while retaining the users keymap
4134        app_state
4135            .fs
4136            .save(
4137                "/settings.json".as_ref(),
4138                &r#"{"base_keymap": "JetBrains"}"#.into(),
4139                Default::default(),
4140            )
4141            .await
4142            .unwrap();
4143
4144        cx.background_executor.run_until_parked();
4145
4146        assert_key_bindings_for(workspace.into(), cx, vec![("6", &Deploy)], line!());
4147    }
4148
4149    #[gpui::test]
4150    async fn test_generate_keymap_json_schema_for_registered_actions(
4151        cx: &mut gpui::TestAppContext,
4152    ) {
4153        init_keymap_test(cx);
4154        cx.update(|cx| {
4155            // Make sure it doesn't panic.
4156            KeymapFile::generate_json_schema_for_registered_actions(cx);
4157        });
4158    }
4159
4160    /// Actions that don't build from empty input won't work from command palette invocation.
4161    #[gpui::test]
4162    async fn test_actions_build_with_empty_input(cx: &mut gpui::TestAppContext) {
4163        init_keymap_test(cx);
4164        cx.update(|cx| {
4165            let all_actions = cx.all_action_names();
4166            let mut failing_names = Vec::new();
4167            let mut errors = Vec::new();
4168            for action in all_actions {
4169                match action.to_string().as_str() {
4170                    "vim::FindCommand"
4171                    | "vim::Literal"
4172                    | "vim::ResizePane"
4173                    | "vim::PushObject"
4174                    | "vim::PushFindForward"
4175                    | "vim::PushFindBackward"
4176                    | "vim::PushSneak"
4177                    | "vim::PushSneakBackward"
4178                    | "vim::PushChangeSurrounds"
4179                    | "vim::PushJump"
4180                    | "vim::PushDigraph"
4181                    | "vim::PushLiteral"
4182                    | "vim::Number"
4183                    | "vim::SelectRegister"
4184                    | "git::StageAndNext"
4185                    | "git::UnstageAndNext"
4186                    | "terminal::SendText"
4187                    | "terminal::SendKeystroke"
4188                    | "app_menu::OpenApplicationMenu"
4189                    | "picker::ConfirmInput"
4190                    | "editor::HandleInput"
4191                    | "editor::FoldAtLevel"
4192                    | "pane::ActivateItem"
4193                    | "workspace::ActivatePane"
4194                    | "workspace::MoveItemToPane"
4195                    | "workspace::MoveItemToPaneInDirection"
4196                    | "workspace::OpenTerminal"
4197                    | "workspace::SendKeystrokes"
4198                    | "zed::OpenBrowser"
4199                    | "zed::OpenZedUrl" => {}
4200                    _ => {
4201                        let result = cx.build_action(action, None);
4202                        match &result {
4203                            Ok(_) => {}
4204                            Err(err) => {
4205                                failing_names.push(action);
4206                                errors.push(format!("{action} failed to build: {err:?}"));
4207                            }
4208                        }
4209                    }
4210                }
4211            }
4212            if errors.len() > 0 {
4213                panic!(
4214                    "Failed to build actions using {{}} as input: {:?}. Errors:\n{}",
4215                    failing_names,
4216                    errors.join("\n")
4217                );
4218            }
4219        });
4220    }
4221
4222    #[gpui::test]
4223    fn test_bundled_settings_and_themes(cx: &mut App) {
4224        cx.text_system()
4225            .add_fonts(vec![
4226                Assets
4227                    .load("fonts/plex-mono/ZedPlexMono-Regular.ttf")
4228                    .unwrap()
4229                    .unwrap(),
4230                Assets
4231                    .load("fonts/plex-sans/ZedPlexSans-Regular.ttf")
4232                    .unwrap()
4233                    .unwrap(),
4234            ])
4235            .unwrap();
4236        let themes = ThemeRegistry::default();
4237        settings::init(cx);
4238        theme::init(theme::LoadThemes::JustBase, cx);
4239
4240        let mut has_default_theme = false;
4241        for theme_name in themes.list().into_iter().map(|meta| meta.name) {
4242            let theme = themes.get(&theme_name).unwrap();
4243            assert_eq!(theme.name, theme_name);
4244            if theme.name == ThemeSettings::get(None, cx).active_theme.name {
4245                has_default_theme = true;
4246            }
4247        }
4248        assert!(has_default_theme);
4249    }
4250
4251    #[gpui::test]
4252    async fn test_bundled_languages(cx: &mut TestAppContext) {
4253        env_logger::builder().is_test(true).try_init().ok();
4254        let settings = cx.update(SettingsStore::test);
4255        cx.set_global(settings);
4256        let languages = LanguageRegistry::test(cx.executor());
4257        let languages = Arc::new(languages);
4258        let node_runtime = node_runtime::NodeRuntime::unavailable();
4259        cx.update(|cx| {
4260            languages::init(languages.clone(), node_runtime, cx);
4261        });
4262        for name in languages.language_names() {
4263            languages
4264                .language_for_name(&name)
4265                .await
4266                .with_context(|| format!("language name {name}"))
4267                .unwrap();
4268        }
4269        cx.run_until_parked();
4270    }
4271
4272    pub(crate) fn init_test(cx: &mut TestAppContext) -> Arc<AppState> {
4273        init_test_with_state(cx, cx.update(AppState::test))
4274    }
4275
4276    fn init_test_with_state(
4277        cx: &mut TestAppContext,
4278        mut app_state: Arc<AppState>,
4279    ) -> Arc<AppState> {
4280        cx.update(move |cx| {
4281            env_logger::builder().is_test(true).try_init().ok();
4282
4283            let state = Arc::get_mut(&mut app_state).unwrap();
4284            state.build_window_options = build_window_options;
4285
4286            app_state.languages.add(markdown_language());
4287
4288            gpui_tokio::init(cx);
4289            vim_mode_setting::init(cx);
4290            theme::init(theme::LoadThemes::JustBase, cx);
4291            audio::init((), cx);
4292            channel::init(&app_state.client, app_state.user_store.clone(), cx);
4293            call::init(app_state.client.clone(), app_state.user_store.clone(), cx);
4294            notifications::init(app_state.client.clone(), app_state.user_store.clone(), cx);
4295            workspace::init(app_state.clone(), cx);
4296            Project::init_settings(cx);
4297            release_channel::init(SemanticVersion::default(), cx);
4298            command_palette::init(cx);
4299            language::init(cx);
4300            editor::init(cx);
4301            collab_ui::init(&app_state, cx);
4302            git_ui::init(cx);
4303            project_panel::init(cx);
4304            outline_panel::init(cx);
4305            terminal_view::init(cx);
4306            copilot::copilot_chat::init(
4307                app_state.fs.clone(),
4308                app_state.client.http_client(),
4309                copilot::copilot_chat::CopilotChatConfiguration::default(),
4310                cx,
4311            );
4312            image_viewer::init(cx);
4313            language_model::init(app_state.client.clone(), cx);
4314            language_models::init(
4315                app_state.user_store.clone(),
4316                app_state.client.clone(),
4317                app_state.fs.clone(),
4318                cx,
4319            );
4320            web_search::init(cx);
4321            web_search_providers::init(app_state.client.clone(), cx);
4322            let prompt_builder = PromptBuilder::load(app_state.fs.clone(), false, cx);
4323            agent::init(
4324                app_state.fs.clone(),
4325                app_state.client.clone(),
4326                prompt_builder.clone(),
4327                app_state.languages.clone(),
4328                false,
4329                cx,
4330            );
4331            repl::init(app_state.fs.clone(), cx);
4332            repl::notebook::init(cx);
4333            tasks_ui::init(cx);
4334            project::debugger::breakpoint_store::BreakpointStore::init(
4335                &app_state.client.clone().into(),
4336            );
4337            project::debugger::dap_store::DapStore::init(&app_state.client.clone().into(), cx);
4338            debugger_ui::init(cx);
4339            initialize_workspace(app_state.clone(), prompt_builder, cx);
4340            search::init(cx);
4341            app_state
4342        })
4343    }
4344
4345    fn rust_lang() -> Arc<language::Language> {
4346        Arc::new(language::Language::new(
4347            language::LanguageConfig {
4348                name: "Rust".into(),
4349                matcher: LanguageMatcher {
4350                    path_suffixes: vec!["rs".to_string()],
4351                    ..Default::default()
4352                },
4353                ..Default::default()
4354            },
4355            Some(tree_sitter_rust::LANGUAGE.into()),
4356        ))
4357    }
4358
4359    fn markdown_language() -> Arc<language::Language> {
4360        Arc::new(language::Language::new(
4361            language::LanguageConfig {
4362                name: "Markdown".into(),
4363                matcher: LanguageMatcher {
4364                    path_suffixes: vec!["md".to_string()],
4365                    ..Default::default()
4366                },
4367                ..Default::default()
4368            },
4369            Some(tree_sitter_md::LANGUAGE.into()),
4370        ))
4371    }
4372
4373    #[track_caller]
4374    fn assert_key_bindings_for(
4375        window: AnyWindowHandle,
4376        cx: &TestAppContext,
4377        actions: Vec<(&'static str, &dyn Action)>,
4378        line: u32,
4379    ) {
4380        let available_actions = cx
4381            .update(|cx| window.update(cx, |_, window, cx| window.available_actions(cx)))
4382            .unwrap();
4383        for (key, action) in actions {
4384            let bindings = cx
4385                .update(|cx| window.update(cx, |_, window, _| window.bindings_for_action(action)))
4386                .unwrap();
4387            // assert that...
4388            assert!(
4389                available_actions.iter().any(|bound_action| {
4390                    // actions match...
4391                    bound_action.partial_eq(action)
4392                }),
4393                "On {} Failed to find {}",
4394                line,
4395                action.name(),
4396            );
4397            assert!(
4398                // and key strokes contain the given key
4399                bindings
4400                    .into_iter()
4401                    .any(|binding| binding.keystrokes().iter().any(|k| k.key == key)),
4402                "On {} Failed to find {} with key binding {}",
4403                line,
4404                action.name(),
4405                key
4406            );
4407        }
4408    }
4409
4410    #[gpui::test]
4411    async fn test_opening_project_settings_when_excluded(cx: &mut gpui::TestAppContext) {
4412        // Use the proper initialization for runtime state
4413        let app_state = init_keymap_test(cx);
4414
4415        eprintln!("Running test_opening_project_settings_when_excluded");
4416
4417        // 1. Set up a project with some project settings
4418        let settings_init =
4419            r#"{ "UNIQUEVALUE": true, "git": { "inline_blame": { "enabled": false } } }"#;
4420        app_state
4421            .fs
4422            .as_fake()
4423            .insert_tree(
4424                Path::new("/root"),
4425                json!({
4426                    ".zed": {
4427                        "settings.json": settings_init
4428                    }
4429                }),
4430            )
4431            .await;
4432
4433        eprintln!("Created project with .zed/settings.json containing UNIQUEVALUE");
4434
4435        // 2. Create a project with the file system and load it
4436        let project = Project::test(app_state.fs.clone(), [Path::new("/root")], cx).await;
4437
4438        // Save original settings content for comparison
4439        let original_settings = app_state
4440            .fs
4441            .load(Path::new("/root/.zed/settings.json"))
4442            .await
4443            .unwrap();
4444
4445        let original_settings_str = original_settings.clone();
4446
4447        // Verify settings exist on disk and have expected content
4448        eprintln!("Original settings content: {}", original_settings_str);
4449        assert!(
4450            original_settings_str.contains("UNIQUEVALUE"),
4451            "Test setup failed - settings file doesn't contain our marker"
4452        );
4453
4454        // 3. Add .zed to file scan exclusions in user settings
4455        cx.update_global::<SettingsStore, _>(|store, cx| {
4456            store.update_user_settings::<WorktreeSettings>(cx, |worktree_settings| {
4457                worktree_settings.file_scan_exclusions = Some(vec![".zed".to_string()]);
4458            });
4459        });
4460
4461        eprintln!("Added .zed to file_scan_exclusions in settings");
4462
4463        // 4. Run tasks to apply settings
4464        cx.background_executor.run_until_parked();
4465
4466        // 5. Critical: Verify .zed is actually excluded from worktree
4467        let worktree = cx.update(|cx| project.read(cx).worktrees(cx).next().unwrap().clone());
4468
4469        let has_zed_entry = cx.update(|cx| worktree.read(cx).entry_for_path(".zed").is_some());
4470
4471        eprintln!(
4472            "Is .zed directory visible in worktree after exclusion: {}",
4473            has_zed_entry
4474        );
4475
4476        // This assertion verifies the test is set up correctly to show the bug
4477        // If .zed is not excluded, the test will fail here
4478        assert!(
4479            !has_zed_entry,
4480            "Test precondition failed: .zed directory should be excluded but was found in worktree"
4481        );
4482
4483        // 6. Create workspace and trigger the actual function that causes the bug
4484        let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
4485        window
4486            .update(cx, |workspace, window, cx| {
4487                // Call the exact function that contains the bug
4488                eprintln!("About to call open_project_settings_file");
4489                open_project_settings_file(workspace, &OpenProjectSettings, window, cx);
4490            })
4491            .unwrap();
4492
4493        // 7. Run background tasks until completion
4494        cx.background_executor.run_until_parked();
4495
4496        // 8. Verify file contents after calling function
4497        let new_content = app_state
4498            .fs
4499            .load(Path::new("/root/.zed/settings.json"))
4500            .await
4501            .unwrap();
4502
4503        let new_content_str = new_content.clone();
4504        eprintln!("New settings content: {}", new_content_str);
4505
4506        // The bug causes the settings to be overwritten with empty settings
4507        // So if the unique value is no longer present, the bug has been reproduced
4508        let bug_exists = !new_content_str.contains("UNIQUEVALUE");
4509        eprintln!("Bug reproduced: {}", bug_exists);
4510
4511        // This assertion should fail if the bug exists - showing the bug is real
4512        assert!(
4513            new_content_str.contains("UNIQUEVALUE"),
4514            "BUG FOUND: Project settings were overwritten when opening via command - original custom content was lost"
4515        );
4516    }
4517}