zed.rs

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