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