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