zed.rs

   1mod app_menus;
   2pub mod inline_completion_registry;
   3#[cfg(target_os = "linux")]
   4pub(crate) mod linux_prompts;
   5#[cfg(not(target_os = "linux"))]
   6pub(crate) mod only_instance;
   7mod open_listener;
   8pub(crate) mod session;
   9mod ssh_connection_modal;
  10
  11pub use app_menus::*;
  12use breadcrumbs::Breadcrumbs;
  13use client::ZED_URL_SCHEME;
  14use collections::VecDeque;
  15use editor::{scroll::Autoscroll, Editor, MultiBuffer};
  16use gpui::{
  17    actions, point, px, AppContext, AsyncAppContext, Context, FocusableView, MenuItem, PromptLevel,
  18    ReadGlobal, TitlebarOptions, View, ViewContext, VisualContext, WindowKind, WindowOptions,
  19};
  20pub use open_listener::*;
  21
  22use anyhow::Context as _;
  23use assets::Assets;
  24use futures::{channel::mpsc, select_biased, StreamExt};
  25use outline_panel::OutlinePanel;
  26use project::TaskSourceKind;
  27use project_panel::ProjectPanel;
  28use quick_action_bar::QuickActionBar;
  29use release_channel::{AppCommitSha, ReleaseChannel};
  30use rope::Rope;
  31use search::project_search::ProjectSearchBar;
  32use settings::{
  33    initial_local_settings_content, initial_tasks_content, watch_config_file, KeymapFile, Settings,
  34    SettingsStore, DEFAULT_KEYMAP_PATH,
  35};
  36use std::{borrow::Cow, ops::Deref, path::Path, sync::Arc};
  37use task::static_source::{StaticSource, TrackedFile};
  38use theme::ActiveTheme;
  39use workspace::notifications::NotificationId;
  40
  41use paths::{local_settings_file_relative_path, local_tasks_file_relative_path};
  42use terminal_view::terminal_panel::{self, TerminalPanel};
  43use util::{asset_str, ResultExt};
  44use uuid::Uuid;
  45use vim::VimModeSetting;
  46use welcome::{BaseKeymap, MultibufferHint};
  47use workspace::{
  48    create_and_open_local_file, notifications::simple_message_notification::MessageNotification,
  49    open_new, AppState, NewFile, NewWindow, OpenLog, Toast, Workspace, WorkspaceSettings,
  50};
  51use workspace::{notifications::DetachAndPromptErr, Pane};
  52use zed_actions::{OpenBrowser, OpenSettings, OpenZedUrl, Quit};
  53
  54actions!(
  55    zed,
  56    [
  57        DebugElements,
  58        Hide,
  59        HideOthers,
  60        Minimize,
  61        OpenDefaultKeymap,
  62        OpenDefaultSettings,
  63        OpenLocalSettings,
  64        OpenLocalTasks,
  65        OpenTasks,
  66        ResetDatabase,
  67        ShowAll,
  68        ToggleFullScreen,
  69        Zoom,
  70        TestPanic,
  71    ]
  72);
  73
  74pub fn init(cx: &mut AppContext) {
  75    #[cfg(target_os = "macos")]
  76    cx.on_action(|_: &Hide, cx| cx.hide());
  77    #[cfg(target_os = "macos")]
  78    cx.on_action(|_: &HideOthers, cx| cx.hide_other_apps());
  79    #[cfg(target_os = "macos")]
  80    cx.on_action(|_: &ShowAll, cx| cx.unhide_other_apps());
  81    cx.on_action(quit);
  82
  83    if ReleaseChannel::global(cx) == ReleaseChannel::Dev {
  84        cx.on_action(test_panic);
  85    }
  86}
  87
  88pub fn build_window_options(display_uuid: Option<Uuid>, cx: &mut AppContext) -> WindowOptions {
  89    let display = display_uuid.and_then(|uuid| {
  90        cx.displays()
  91            .into_iter()
  92            .find(|display| display.uuid().ok() == Some(uuid))
  93    });
  94    let app_id = ReleaseChannel::global(cx).app_id();
  95    let window_decorations = match std::env::var("ZED_WINDOW_DECORATIONS") {
  96        Ok(val) if val == "server" => gpui::WindowDecorations::Server,
  97        Ok(val) if val == "client" => gpui::WindowDecorations::Client,
  98        _ => gpui::WindowDecorations::Client,
  99    };
 100
 101    WindowOptions {
 102        titlebar: Some(TitlebarOptions {
 103            title: None,
 104            appears_transparent: true,
 105            traffic_light_position: Some(point(px(9.0), px(9.0))),
 106        }),
 107        window_bounds: None,
 108        focus: false,
 109        show: false,
 110        kind: WindowKind::Normal,
 111        is_movable: true,
 112        display_id: display.map(|display| display.id()),
 113        window_background: cx.theme().window_background_appearance(),
 114        app_id: Some(app_id.to_owned()),
 115        window_decorations: Some(window_decorations),
 116        window_min_size: Some(gpui::Size {
 117            width: px(360.0),
 118            height: px(240.0),
 119        }),
 120    }
 121}
 122
 123pub fn initialize_workspace(app_state: Arc<AppState>, cx: &mut AppContext) {
 124    cx.observe_new_views(move |workspace: &mut Workspace, cx| {
 125        let workspace_handle = cx.view().clone();
 126        let center_pane = workspace.active_pane().clone();
 127        initialize_pane(workspace, &center_pane, cx);
 128        cx.subscribe(&workspace_handle, {
 129            move |workspace, _, event, cx| match event {
 130                workspace::Event::PaneAdded(pane) => {
 131                    initialize_pane(workspace, pane, cx);
 132                }
 133                workspace::Event::OpenBundledFile {
 134                    text,
 135                    title,
 136                    language,
 137                } => open_bundled_file(workspace, text.clone(), title, language, cx),
 138                _ => {}
 139            }
 140        })
 141        .detach();
 142
 143        #[cfg(target_os = "linux")]
 144        if let Err(e) = fs::watcher::global(|_| {}) {
 145            let message = format!(db::indoc!{r#"
 146                inotify_init returned {}
 147
 148                This may be due to system-wide limits on inotify instances. For troubleshooting see: https://zed.dev/docs/linux
 149                "#}, e);
 150            let prompt = cx.prompt(PromptLevel::Critical, "Could not start inotify", Some(&message),
 151                &["Troubleshoot and Quit"]);
 152            cx.spawn(|_, mut cx| async move {
 153                if prompt.await == Ok(0) {
 154                    cx.update(|cx| {
 155                        cx.open_url("https://zed.dev/docs/linux#could-not-start-inotify");
 156                        cx.quit();
 157                    }).ok();
 158                }
 159            }).detach()
 160        }
 161
 162        if let Some(specs) = cx.gpu_specs() {
 163            log::info!("Using GPU: {:?}", specs);
 164            if specs.is_software_emulated && std::env::var("ZED_ALLOW_EMULATED_GPU").is_err() {
 165            let message = format!(db::indoc!{r#"
 166                Zed uses Vulkan for rendering and requires a compatible GPU.
 167
 168                Currently you are using a software emulated GPU ({}) which
 169                will result in awful performance.
 170
 171                For troubleshooting see: https://zed.dev/docs/linux
 172                "#}, specs.device_name);
 173            let prompt = cx.prompt(PromptLevel::Critical, "Unsupported GPU", Some(&message),
 174                &["Troubleshoot and Quit"]);
 175            cx.spawn(|_, mut cx| async move {
 176                if prompt.await == Ok(0) {
 177                    cx.update(|cx| {
 178                        cx.open_url("https://zed.dev/docs/linux#zed-fails-to-open-windows");
 179                        cx.quit();
 180                    }).ok();
 181                }
 182            }).detach()
 183            }
 184        }
 185
 186        let inline_completion_button = cx.new_view(|cx| {
 187            inline_completion_button::InlineCompletionButton::new(app_state.fs.clone(), cx)
 188        });
 189
 190        let diagnostic_summary =
 191            cx.new_view(|cx| diagnostics::items::DiagnosticIndicator::new(workspace, cx));
 192        let activity_indicator =
 193            activity_indicator::ActivityIndicator::new(workspace, app_state.languages.clone(), cx);
 194        let active_buffer_language =
 195            cx.new_view(|_| language_selector::ActiveBufferLanguage::new(workspace));
 196        let vim_mode_indicator = cx.new_view(|cx| vim::ModeIndicator::new(cx));
 197        let cursor_position =
 198            cx.new_view(|_| go_to_line::cursor_position::CursorPosition::new(workspace));
 199        workspace.status_bar().update(cx, |status_bar, cx| {
 200            status_bar.add_left_item(diagnostic_summary, cx);
 201            status_bar.add_left_item(activity_indicator, cx);
 202            status_bar.add_right_item(inline_completion_button, cx);
 203            status_bar.add_right_item(active_buffer_language, cx);
 204            status_bar.add_right_item(vim_mode_indicator, cx);
 205            status_bar.add_right_item(cursor_position, cx);
 206        });
 207
 208        auto_update::notify_of_any_new_update(cx);
 209
 210        let handle = cx.view().downgrade();
 211        cx.on_window_should_close(move |cx| {
 212            handle
 213                .update(cx, |workspace, cx| {
 214                    // We'll handle closing asynchronously
 215                    workspace.close_window(&Default::default(), cx);
 216                    false
 217                })
 218                .unwrap_or(true)
 219        });
 220
 221        let project = workspace.project().clone();
 222        if project.update(cx, |project, cx| {
 223            project.is_local() || project.ssh_connection_string(cx).is_some()
 224        }) {
 225            project.update(cx, |project, cx| {
 226                let fs = app_state.fs.clone();
 227                project.task_inventory().update(cx, |inventory, cx| {
 228                    let tasks_file_rx =
 229                        watch_config_file(&cx.background_executor(), fs, paths::tasks_file().clone());
 230                    inventory.add_source(
 231                        TaskSourceKind::AbsPath {
 232                            id_base: "global_tasks".into(),
 233                            abs_path: paths::tasks_file().clone(),
 234                        },
 235                        |tx, cx| StaticSource::new(TrackedFile::new(tasks_file_rx, tx, cx)),
 236                        cx,
 237                    );
 238                })
 239            });
 240        }
 241
 242        cx.spawn(|workspace_handle, mut cx| async move {
 243            let assistant_panel =
 244                assistant::AssistantPanel::load(workspace_handle.clone(), cx.clone());
 245
 246            let project_panel = ProjectPanel::load(workspace_handle.clone(), cx.clone());
 247            let outline_panel = OutlinePanel::load(workspace_handle.clone(), cx.clone());
 248            let terminal_panel = TerminalPanel::load(workspace_handle.clone(), cx.clone());
 249            let channels_panel =
 250                collab_ui::collab_panel::CollabPanel::load(workspace_handle.clone(), cx.clone());
 251            let chat_panel =
 252                collab_ui::chat_panel::ChatPanel::load(workspace_handle.clone(), cx.clone());
 253            let notification_panel = collab_ui::notification_panel::NotificationPanel::load(
 254                workspace_handle.clone(),
 255                cx.clone(),
 256            );
 257
 258            let (
 259                project_panel,
 260                outline_panel,
 261                terminal_panel,
 262                assistant_panel,
 263                channels_panel,
 264                chat_panel,
 265                notification_panel,
 266            ) = futures::try_join!(
 267                project_panel,
 268                outline_panel,
 269                terminal_panel,
 270                assistant_panel,
 271                channels_panel,
 272                chat_panel,
 273                notification_panel,
 274            )?;
 275
 276            workspace_handle.update(&mut cx, |workspace, cx| {
 277                workspace.add_panel(assistant_panel, cx);
 278                workspace.add_panel(project_panel, cx);
 279                workspace.add_panel(outline_panel, cx);
 280                workspace.add_panel(terminal_panel, cx);
 281                workspace.add_panel(channels_panel, cx);
 282                workspace.add_panel(chat_panel, cx);
 283                workspace.add_panel(notification_panel, cx);
 284                cx.focus_self();
 285            })
 286        })
 287        .detach();
 288
 289        workspace
 290            .register_action(about)
 291            .register_action(|_, _: &Minimize, cx| {
 292                cx.minimize_window();
 293            })
 294            .register_action(|_, _: &Zoom, cx| {
 295                cx.zoom_window();
 296            })
 297            .register_action(|_, _: &ToggleFullScreen, cx| {
 298                cx.toggle_fullscreen();
 299            })
 300            .register_action(|_, action: &OpenZedUrl, cx| {
 301                OpenListener::global(cx).open_urls(vec![action.url.clone()])
 302            })
 303            .register_action(|_, action: &OpenBrowser, cx| cx.open_url(&action.url))
 304            .register_action(move |_, _: &zed_actions::IncreaseBufferFontSize, cx| {
 305                theme::adjust_buffer_font_size(cx, |size| *size += px(1.0))
 306            })
 307            .register_action(move |_, _: &zed_actions::DecreaseBufferFontSize, cx| {
 308                theme::adjust_buffer_font_size(cx, |size| *size -= px(1.0))
 309            })
 310            .register_action(move |_, _: &zed_actions::ResetBufferFontSize, cx| {
 311                theme::reset_buffer_font_size(cx)
 312            })
 313            .register_action(move |_, _: &zed_actions::IncreaseUiFontSize, cx| {
 314                theme::adjust_ui_font_size(cx, |size| *size += px(1.0))
 315            })
 316            .register_action(move |_, _: &zed_actions::DecreaseUiFontSize, cx| {
 317                theme::adjust_ui_font_size(cx, |size| *size -= px(1.0))
 318            })
 319            .register_action(move |_, _: &zed_actions::ResetUiFontSize, cx| {
 320                theme::reset_ui_font_size(cx)
 321            })
 322            .register_action(move |_, _: &zed_actions::IncreaseBufferFontSize, cx| {
 323                theme::adjust_buffer_font_size(cx, |size| *size += px(1.0))
 324            })
 325            .register_action(move |_, _: &zed_actions::DecreaseBufferFontSize, cx| {
 326                theme::adjust_buffer_font_size(cx, |size| *size -= px(1.0))
 327            })
 328            .register_action(move |_, _: &zed_actions::ResetBufferFontSize, cx| {
 329                theme::reset_buffer_font_size(cx)
 330            })
 331            .register_action(|_, _: &install_cli::Install, cx| {
 332                cx.spawn(|workspace, mut cx| async move {
 333                    if cfg!(target_os = "linux") {
 334                        let prompt = cx.prompt(
 335                            PromptLevel::Warning,
 336                            "CLI should already be installed",
 337                            Some("If you installed Zed from our official release add ~/.local/bin to your PATH.\n\nIf you installed Zed from a different source like your package manager, then you may need to create an alias/symlink manually.\n\nDepending on your package manager, the CLI might be named zeditor, zedit, zed-editor or something else."),
 338                            &["Ok"],
 339                        );
 340                        cx.background_executor().spawn(prompt).detach();
 341                        return Ok(());
 342                    }
 343                    let path = install_cli::install_cli(cx.deref())
 344                        .await
 345                        .context("error creating CLI symlink")?;
 346
 347                    workspace.update(&mut cx, |workspace, cx| {
 348                        struct InstalledZedCli;
 349
 350                        workspace.show_toast(
 351                            Toast::new(
 352                                NotificationId::unique::<InstalledZedCli>(),
 353                                format!(
 354                                    "Installed `zed` to {}. You can launch {} from your terminal.",
 355                                    path.to_string_lossy(),
 356                                    ReleaseChannel::global(cx).display_name()
 357                                ),
 358                            ),
 359                            cx,
 360                        )
 361                    })?;
 362                    register_zed_scheme(&cx).await.log_err();
 363                    Ok(())
 364                })
 365                .detach_and_prompt_err("Error installing zed cli", cx, |_, _| None);
 366            })
 367            .register_action(|_, _: &install_cli::RegisterZedScheme, cx| {
 368                cx.spawn(|workspace, mut cx| async move {
 369                    register_zed_scheme(&cx).await?;
 370                    workspace.update(&mut cx, |workspace, cx| {
 371                        struct RegisterZedScheme;
 372
 373                        workspace.show_toast(
 374                            Toast::new(
 375                                NotificationId::unique::<RegisterZedScheme>(),
 376                                format!(
 377                                    "zed:// links will now open in {}.",
 378                                    ReleaseChannel::global(cx).display_name()
 379                                ),
 380                            ),
 381                            cx,
 382                        )
 383                    })?;
 384                    Ok(())
 385                })
 386                .detach_and_prompt_err(
 387                    "Error registering zed:// scheme",
 388                    cx,
 389                    |_, _| None,
 390                );
 391            })
 392            .register_action(|workspace, _: &OpenLog, cx| {
 393                open_log_file(workspace, cx);
 394            })
 395            .register_action(|workspace, _: &zed_actions::OpenLicenses, cx| {
 396                open_bundled_file(
 397                    workspace,
 398                    asset_str::<Assets>("licenses.md"),
 399                    "Open Source License Attribution",
 400                    "Markdown",
 401                    cx,
 402                );
 403            })
 404            .register_action(
 405                move |workspace: &mut Workspace,
 406                      _: &zed_actions::OpenTelemetryLog,
 407                      cx: &mut ViewContext<Workspace>| {
 408                    open_telemetry_log_file(workspace, cx);
 409                },
 410            )
 411            .register_action(
 412                move |_: &mut Workspace,
 413                      _: &zed_actions::OpenKeymap,
 414                      cx: &mut ViewContext<Workspace>| {
 415                    open_settings_file(&paths::keymap_file(), || settings::initial_keymap_content().as_ref().into(), cx);
 416                },
 417            )
 418            .register_action(
 419                move |_: &mut Workspace, _: &OpenSettings, cx: &mut ViewContext<Workspace>| {
 420                    open_settings_file(
 421                        paths::settings_file(),
 422                        || settings::initial_user_settings_content().as_ref().into(),
 423                        cx,
 424                    );
 425                },
 426            )
 427            .register_action(
 428                move |_: &mut Workspace, _: &OpenTasks, cx: &mut ViewContext<Workspace>| {
 429                    open_settings_file(
 430                        paths::tasks_file(),
 431                        || settings::initial_tasks_content().as_ref().into(),
 432                        cx,
 433                    );
 434                },
 435            )
 436            .register_action(open_local_settings_file)
 437            .register_action(open_local_tasks_file)
 438            .register_action(
 439                move |workspace: &mut Workspace,
 440                      _: &OpenDefaultKeymap,
 441                      cx: &mut ViewContext<Workspace>| {
 442                    open_bundled_file(
 443                        workspace,
 444                        settings::default_keymap(),
 445                        "Default Key Bindings",
 446                        "JSON",
 447                        cx,
 448                    );
 449                },
 450            )
 451            .register_action(
 452                move |workspace: &mut Workspace,
 453                      _: &OpenDefaultSettings,
 454                      cx: &mut ViewContext<Workspace>| {
 455                    open_bundled_file(
 456                        workspace,
 457                        settings::default_settings(),
 458                        "Default Settings",
 459                        "JSON",
 460                        cx,
 461                    );
 462                },
 463            )
 464            .register_action(
 465                |workspace: &mut Workspace,
 466                 _: &project_panel::ToggleFocus,
 467                 cx: &mut ViewContext<Workspace>| {
 468                    workspace.toggle_panel_focus::<ProjectPanel>(cx);
 469                },
 470            )
 471            .register_action(
 472                |workspace: &mut Workspace,
 473                 _: &outline_panel::ToggleFocus,
 474                 cx: &mut ViewContext<Workspace>| {
 475                    workspace.toggle_panel_focus::<OutlinePanel>(cx);
 476                },
 477            )
 478            .register_action(
 479                |workspace: &mut Workspace,
 480                 _: &collab_ui::collab_panel::ToggleFocus,
 481                 cx: &mut ViewContext<Workspace>| {
 482                    workspace.toggle_panel_focus::<collab_ui::collab_panel::CollabPanel>(cx);
 483                },
 484            )
 485            .register_action(
 486                |workspace: &mut Workspace,
 487                 _: &collab_ui::chat_panel::ToggleFocus,
 488                 cx: &mut ViewContext<Workspace>| {
 489                    workspace.toggle_panel_focus::<collab_ui::chat_panel::ChatPanel>(cx);
 490                },
 491            )
 492            .register_action(
 493                |workspace: &mut Workspace,
 494                 _: &collab_ui::notification_panel::ToggleFocus,
 495                 cx: &mut ViewContext<Workspace>| {
 496                    workspace
 497                        .toggle_panel_focus::<collab_ui::notification_panel::NotificationPanel>(cx);
 498                },
 499            )
 500            .register_action(
 501                |workspace: &mut Workspace,
 502                 _: &terminal_panel::ToggleFocus,
 503                 cx: &mut ViewContext<Workspace>| {
 504                    workspace.toggle_panel_focus::<TerminalPanel>(cx);
 505                },
 506            )
 507            .register_action({
 508                let app_state = Arc::downgrade(&app_state);
 509                move |_, _: &NewWindow, cx| {
 510                    if let Some(app_state) = app_state.upgrade() {
 511                        open_new(app_state, cx, |workspace, cx| {
 512                            Editor::new_file(workspace, &Default::default(), cx)
 513                        })
 514                        .detach();
 515                    }
 516                }
 517            })
 518            .register_action({
 519                let app_state = Arc::downgrade(&app_state);
 520                move |_, _: &NewFile, cx| {
 521                    if let Some(app_state) = app_state.upgrade() {
 522                        open_new(app_state, cx, |workspace, cx| {
 523                            Editor::new_file(workspace, &Default::default(), cx)
 524                        })
 525                        .detach();
 526                    }
 527                }
 528            });
 529
 530        workspace.focus_handle(cx).focus(cx);
 531    })
 532    .detach();
 533}
 534
 535fn initialize_pane(workspace: &mut Workspace, pane: &View<Pane>, cx: &mut ViewContext<Workspace>) {
 536    pane.update(cx, |pane, cx| {
 537        pane.toolbar().update(cx, |toolbar, cx| {
 538            let multibuffer_hint = cx.new_view(|_| MultibufferHint::new());
 539            toolbar.add_item(multibuffer_hint, cx);
 540            let breadcrumbs = cx.new_view(|_| Breadcrumbs::new());
 541            toolbar.add_item(breadcrumbs, cx);
 542            let buffer_search_bar = cx.new_view(search::BufferSearchBar::new);
 543            toolbar.add_item(buffer_search_bar.clone(), cx);
 544
 545            let quick_action_bar =
 546                cx.new_view(|cx| QuickActionBar::new(buffer_search_bar, workspace, cx));
 547            toolbar.add_item(quick_action_bar, cx);
 548            let diagnostic_editor_controls = cx.new_view(|_| diagnostics::ToolbarControls::new());
 549            toolbar.add_item(diagnostic_editor_controls, cx);
 550            let project_search_bar = cx.new_view(|_| ProjectSearchBar::new());
 551            toolbar.add_item(project_search_bar, cx);
 552            let lsp_log_item = cx.new_view(|_| language_tools::LspLogToolbarItemView::new());
 553            toolbar.add_item(lsp_log_item, cx);
 554            let syntax_tree_item =
 555                cx.new_view(|_| language_tools::SyntaxTreeToolbarItemView::new());
 556            toolbar.add_item(syntax_tree_item, cx);
 557        })
 558    });
 559}
 560
 561fn about(_: &mut Workspace, _: &zed_actions::About, cx: &mut gpui::ViewContext<Workspace>) {
 562    let release_channel = ReleaseChannel::global(cx).display_name();
 563    let version = env!("CARGO_PKG_VERSION");
 564    let message = format!("{release_channel} {version}");
 565    let detail = AppCommitSha::try_global(cx).map(|sha| sha.0.clone());
 566
 567    let prompt = cx.prompt(PromptLevel::Info, &message, detail.as_deref(), &["OK"]);
 568    cx.foreground_executor()
 569        .spawn(async {
 570            prompt.await.ok();
 571        })
 572        .detach();
 573}
 574
 575fn test_panic(_: &TestPanic, _: &mut AppContext) {
 576    panic!("Ran the TestPanic action")
 577}
 578
 579fn quit(_: &Quit, cx: &mut AppContext) {
 580    let should_confirm = WorkspaceSettings::get_global(cx).confirm_quit;
 581    cx.spawn(|mut cx| async move {
 582        let mut workspace_windows = cx.update(|cx| {
 583            cx.windows()
 584                .into_iter()
 585                .filter_map(|window| window.downcast::<Workspace>())
 586                .collect::<Vec<_>>()
 587        })?;
 588
 589        // If multiple windows have unsaved changes, and need a save prompt,
 590        // prompt in the active window before switching to a different window.
 591        cx.update(|mut cx| {
 592            workspace_windows.sort_by_key(|window| window.is_active(&mut cx) == Some(false));
 593        })
 594        .log_err();
 595
 596        if let (true, Some(workspace)) = (should_confirm, workspace_windows.first().copied()) {
 597            let answer = workspace
 598                .update(&mut cx, |_, cx| {
 599                    cx.prompt(
 600                        PromptLevel::Info,
 601                        "Are you sure you want to quit?",
 602                        None,
 603                        &["Quit", "Cancel"],
 604                    )
 605                })
 606                .log_err();
 607
 608            if let Some(answer) = answer {
 609                let answer = answer.await.ok();
 610                if answer != Some(0) {
 611                    return Ok(());
 612                }
 613            }
 614        }
 615
 616        // If the user cancels any save prompt, then keep the app open.
 617        for window in workspace_windows {
 618            if let Some(should_close) = window
 619                .update(&mut cx, |workspace, cx| {
 620                    workspace.prepare_to_close(true, cx)
 621                })
 622                .log_err()
 623            {
 624                if !should_close.await? {
 625                    return Ok(());
 626                }
 627            }
 628        }
 629        cx.update(|cx| cx.quit())?;
 630        anyhow::Ok(())
 631    })
 632    .detach_and_log_err(cx);
 633}
 634
 635fn open_log_file(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) {
 636    const MAX_LINES: usize = 1000;
 637    workspace
 638        .with_local_workspace(cx, move |workspace, cx| {
 639            let fs = workspace.app_state().fs.clone();
 640            cx.spawn(|workspace, mut cx| async move {
 641                let (old_log, new_log) =
 642                    futures::join!(fs.load(paths::old_log_file()), fs.load(paths::log_file()));
 643                let log = match (old_log, new_log) {
 644                    (Err(_), Err(_)) => None,
 645                    (old_log, new_log) => {
 646                        let mut lines = VecDeque::with_capacity(MAX_LINES);
 647                        for line in old_log
 648                            .iter()
 649                            .flat_map(|log| log.lines())
 650                            .chain(new_log.iter().flat_map(|log| log.lines()))
 651                        {
 652                            if lines.len() == MAX_LINES {
 653                                lines.pop_front();
 654                            }
 655                            lines.push_back(line);
 656                        }
 657                        Some(
 658                            lines
 659                                .into_iter()
 660                                .flat_map(|line| [line, "\n"])
 661                                .collect::<String>(),
 662                        )
 663                    }
 664                };
 665
 666                workspace
 667                    .update(&mut cx, |workspace, cx| {
 668                        let Some(log) = log else {
 669                            struct OpenLogError;
 670
 671                            workspace.show_notification(
 672                                NotificationId::unique::<OpenLogError>(),
 673                                cx,
 674                                |cx| {
 675                                    cx.new_view(|_| {
 676                                        MessageNotification::new(format!(
 677                                            "Unable to access/open log file at path {:?}",
 678                                            paths::log_file().as_path()
 679                                        ))
 680                                    })
 681                                },
 682                            );
 683                            return;
 684                        };
 685                        let project = workspace.project().clone();
 686                        let buffer = project.update(cx, |project, cx| {
 687                            project.create_local_buffer(&log, None, cx)
 688                        });
 689
 690                        let buffer = cx.new_model(|cx| {
 691                            MultiBuffer::singleton(buffer, cx).with_title("Log".into())
 692                        });
 693                        let editor = cx.new_view(|cx| {
 694                            let mut editor =
 695                                Editor::for_multibuffer(buffer, Some(project), true, cx);
 696                            editor.set_breadcrumb_header(format!(
 697                                "Last {} lines in {}",
 698                                MAX_LINES,
 699                                paths::log_file().display()
 700                            ));
 701                            editor
 702                        });
 703
 704                        editor.update(cx, |editor, cx| {
 705                            let last_multi_buffer_offset = editor.buffer().read(cx).len(cx);
 706                            editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
 707                                s.select_ranges(Some(
 708                                    last_multi_buffer_offset..last_multi_buffer_offset,
 709                                ));
 710                            })
 711                        });
 712
 713                        workspace.add_item_to_active_pane(Box::new(editor), None, true, cx);
 714                    })
 715                    .log_err();
 716            })
 717            .detach();
 718        })
 719        .detach();
 720}
 721
 722pub fn handle_keymap_file_changes(
 723    mut user_keymap_file_rx: mpsc::UnboundedReceiver<String>,
 724    cx: &mut AppContext,
 725) {
 726    BaseKeymap::register(cx);
 727    VimModeSetting::register(cx);
 728
 729    let (base_keymap_tx, mut base_keymap_rx) = mpsc::unbounded();
 730    let mut old_base_keymap = *BaseKeymap::get_global(cx);
 731    let mut old_vim_enabled = VimModeSetting::get_global(cx).0;
 732    cx.observe_global::<SettingsStore>(move |cx| {
 733        let new_base_keymap = *BaseKeymap::get_global(cx);
 734        let new_vim_enabled = VimModeSetting::get_global(cx).0;
 735
 736        if new_base_keymap != old_base_keymap || new_vim_enabled != old_vim_enabled {
 737            old_base_keymap = new_base_keymap;
 738            old_vim_enabled = new_vim_enabled;
 739            base_keymap_tx.unbounded_send(()).unwrap();
 740        }
 741    })
 742    .detach();
 743
 744    load_default_keymap(cx);
 745
 746    cx.spawn(move |cx| async move {
 747        let mut user_keymap = KeymapFile::default();
 748        loop {
 749            select_biased! {
 750                _ = base_keymap_rx.next() => {}
 751                user_keymap_content = user_keymap_file_rx.next() => {
 752                    if let Some(user_keymap_content) = user_keymap_content {
 753                        if let Some(keymap_content) = KeymapFile::parse(&user_keymap_content).log_err() {
 754                            user_keymap = keymap_content;
 755                        } else {
 756                            continue
 757                        }
 758                    }
 759                }
 760            }
 761            cx.update(|cx| reload_keymaps(cx, &user_keymap)).ok();
 762        }
 763    })
 764    .detach();
 765}
 766
 767fn reload_keymaps(cx: &mut AppContext, keymap_content: &KeymapFile) {
 768    cx.clear_key_bindings();
 769    load_default_keymap(cx);
 770    keymap_content.clone().add_to_cx(cx).log_err();
 771    cx.set_menus(app_menus());
 772    cx.set_dock_menu(vec![MenuItem::action("New Window", workspace::NewWindow)])
 773}
 774
 775pub fn load_default_keymap(cx: &mut AppContext) {
 776    let base_keymap = *BaseKeymap::get_global(cx);
 777    if base_keymap == BaseKeymap::None {
 778        return;
 779    }
 780
 781    KeymapFile::load_asset(DEFAULT_KEYMAP_PATH, cx).unwrap();
 782    if VimModeSetting::get_global(cx).0 {
 783        KeymapFile::load_asset("keymaps/vim.json", cx).unwrap();
 784    }
 785
 786    if let Some(asset_path) = base_keymap.asset_path() {
 787        KeymapFile::load_asset(asset_path, cx).unwrap();
 788    }
 789}
 790
 791fn open_local_settings_file(
 792    workspace: &mut Workspace,
 793    _: &OpenLocalSettings,
 794    cx: &mut ViewContext<Workspace>,
 795) {
 796    open_local_file(
 797        workspace,
 798        local_settings_file_relative_path(),
 799        initial_local_settings_content(),
 800        cx,
 801    )
 802}
 803
 804fn open_local_tasks_file(
 805    workspace: &mut Workspace,
 806    _: &OpenLocalTasks,
 807    cx: &mut ViewContext<Workspace>,
 808) {
 809    open_local_file(
 810        workspace,
 811        local_tasks_file_relative_path(),
 812        initial_tasks_content(),
 813        cx,
 814    )
 815}
 816
 817fn open_local_file(
 818    workspace: &mut Workspace,
 819    settings_relative_path: &'static Path,
 820    initial_contents: Cow<'static, str>,
 821    cx: &mut ViewContext<Workspace>,
 822) {
 823    let project = workspace.project().clone();
 824    let worktree = project
 825        .read(cx)
 826        .visible_worktrees(cx)
 827        .find_map(|tree| tree.read(cx).root_entry()?.is_dir().then_some(tree));
 828    if let Some(worktree) = worktree {
 829        let tree_id = worktree.read(cx).id();
 830        cx.spawn(|workspace, mut cx| async move {
 831            if let Some(dir_path) = settings_relative_path.parent() {
 832                if worktree.update(&mut cx, |tree, _| tree.entry_for_path(dir_path).is_none())? {
 833                    project
 834                        .update(&mut cx, |project, cx| {
 835                            project.create_entry((tree_id, dir_path), true, cx)
 836                        })?
 837                        .await
 838                        .context("worktree was removed")?;
 839                }
 840            }
 841
 842            if worktree.update(&mut cx, |tree, _| {
 843                tree.entry_for_path(settings_relative_path).is_none()
 844            })? {
 845                project
 846                    .update(&mut cx, |project, cx| {
 847                        project.create_entry((tree_id, settings_relative_path), false, cx)
 848                    })?
 849                    .await
 850                    .context("worktree was removed")?;
 851            }
 852
 853            let editor = workspace
 854                .update(&mut cx, |workspace, cx| {
 855                    workspace.open_path((tree_id, settings_relative_path), None, true, cx)
 856                })?
 857                .await?
 858                .downcast::<Editor>()
 859                .context("unexpected item type: expected editor item")?;
 860
 861            editor
 862                .downgrade()
 863                .update(&mut cx, |editor, cx| {
 864                    if let Some(buffer) = editor.buffer().read(cx).as_singleton() {
 865                        if buffer.read(cx).is_empty() {
 866                            buffer.update(cx, |buffer, cx| {
 867                                buffer.edit([(0..0, initial_contents)], None, cx)
 868                            });
 869                        }
 870                    }
 871                })
 872                .ok();
 873
 874            anyhow::Ok(())
 875        })
 876        .detach();
 877    } else {
 878        struct NoOpenFolders;
 879
 880        workspace.show_notification(NotificationId::unique::<NoOpenFolders>(), cx, |cx| {
 881            cx.new_view(|_| MessageNotification::new("This project has no folders open."))
 882        })
 883    }
 884}
 885
 886fn open_telemetry_log_file(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) {
 887    workspace.with_local_workspace(cx, move |workspace, cx| {
 888        let app_state = workspace.app_state().clone();
 889        cx.spawn(|workspace, mut cx| async move {
 890            async fn fetch_log_string(app_state: &Arc<AppState>) -> Option<String> {
 891                let path = app_state.client.telemetry().log_file_path()?;
 892                app_state.fs.load(&path).await.log_err()
 893            }
 894
 895            let log = fetch_log_string(&app_state).await.unwrap_or_else(|| "// No data has been collected yet".to_string());
 896
 897            const MAX_TELEMETRY_LOG_LEN: usize = 5 * 1024 * 1024;
 898            let mut start_offset = log.len().saturating_sub(MAX_TELEMETRY_LOG_LEN);
 899            if let Some(newline_offset) = log[start_offset..].find('\n') {
 900                start_offset += newline_offset + 1;
 901            }
 902            let log_suffix = &log[start_offset..];
 903            let json = app_state.languages.language_for_name("JSON").await.log_err();
 904
 905            workspace.update(&mut cx, |workspace, cx| {
 906                let project = workspace.project().clone();
 907                let buffer = project
 908                    .update(cx, |project, cx| project.create_local_buffer("", None, cx));
 909                buffer.update(cx, |buffer, cx| {
 910                    buffer.set_language(json, cx);
 911                    buffer.edit(
 912                        [(
 913                            0..0,
 914                            concat!(
 915                                "// Zed collects anonymous usage data to help us understand how people are using the app.\n",
 916                                "// Telemetry can be disabled via the `settings.json` file.\n",
 917                                "// Here is the data that has been reported for the current session:\n",
 918                                "\n"
 919                            ),
 920                        )],
 921                        None,
 922                        cx,
 923                    );
 924                    buffer.edit([(buffer.len()..buffer.len(), log_suffix)], None, cx);
 925                });
 926
 927                let buffer = cx.new_model(|cx| {
 928                    MultiBuffer::singleton(buffer, cx).with_title("Telemetry Log".into())
 929                });
 930                workspace.add_item_to_active_pane(
 931                    Box::new(cx.new_view(|cx| Editor::for_multibuffer(buffer, Some(project), true, cx))),
 932                    None,
 933                    true,
 934                    cx,
 935                );
 936            }).log_err()?;
 937
 938            Some(())
 939        })
 940        .detach();
 941    }).detach();
 942}
 943
 944fn open_bundled_file(
 945    workspace: &mut Workspace,
 946    text: Cow<'static, str>,
 947    title: &'static str,
 948    language: &'static str,
 949    cx: &mut ViewContext<Workspace>,
 950) {
 951    let language = workspace.app_state().languages.language_for_name(language);
 952    cx.spawn(|workspace, mut cx| async move {
 953        let language = language.await.log_err();
 954        workspace
 955            .update(&mut cx, |workspace, cx| {
 956                workspace.with_local_workspace(cx, |workspace, cx| {
 957                    let project = workspace.project();
 958                    let buffer = project.update(cx, move |project, cx| {
 959                        project.create_local_buffer(text.as_ref(), language, cx)
 960                    });
 961                    let buffer = cx.new_model(|cx| {
 962                        MultiBuffer::singleton(buffer, cx).with_title(title.into())
 963                    });
 964                    workspace.add_item_to_active_pane(
 965                        Box::new(cx.new_view(|cx| {
 966                            Editor::for_multibuffer(buffer, Some(project.clone()), true, cx)
 967                        })),
 968                        None,
 969                        true,
 970                        cx,
 971                    );
 972                })
 973            })?
 974            .await
 975    })
 976    .detach_and_log_err(cx);
 977}
 978
 979fn open_settings_file(
 980    abs_path: &'static Path,
 981    default_content: impl FnOnce() -> Rope + Send + 'static,
 982    cx: &mut ViewContext<Workspace>,
 983) {
 984    cx.spawn(|workspace, mut cx| async move {
 985        let (worktree_creation_task, settings_open_task) =
 986            workspace.update(&mut cx, |workspace, cx| {
 987                let worktree_creation_task = workspace.project().update(cx, |project, cx| {
 988                    // Set up a dedicated worktree for settings, since otherwise we're dropping and re-starting LSP servers for each file inside on every settings file close/open
 989                    // TODO: Do note that all other external files (e.g. drag and drop from OS) still have their worktrees released on file close, causing LSP servers' restarts.
 990                    project.find_or_create_worktree(paths::config_dir().as_path(), false, cx)
 991                });
 992                let settings_open_task = create_and_open_local_file(&abs_path, cx, default_content);
 993                (worktree_creation_task, settings_open_task)
 994            })?;
 995
 996        let _ = worktree_creation_task.await?;
 997        let _ = settings_open_task.await?;
 998        anyhow::Ok(())
 999    })
1000    .detach_and_log_err(cx);
1001}
1002
1003#[cfg(test)]
1004mod tests {
1005    use super::*;
1006    use anyhow::anyhow;
1007    use assets::Assets;
1008    use collections::{HashMap, HashSet};
1009    use editor::{display_map::DisplayRow, scroll::Autoscroll, DisplayPoint, Editor};
1010    use gpui::{
1011        actions, Action, AnyWindowHandle, AppContext, AssetSource, BorrowAppContext, Entity,
1012        SemanticVersion, TestAppContext, UpdateGlobal, VisualTestContext, WindowHandle,
1013    };
1014    use language::{LanguageMatcher, LanguageRegistry};
1015    use project::{project_settings::ProjectSettings, Project, ProjectPath, WorktreeSettings};
1016    use serde_json::json;
1017    use settings::{handle_settings_file_changes, watch_config_file, SettingsStore};
1018    use std::{
1019        path::{Path, PathBuf},
1020        time::Duration,
1021    };
1022    use task::{HideStrategy, RevealStrategy, Shell, SpawnInTerminal};
1023    use theme::{ThemeRegistry, ThemeSettings};
1024    use workspace::{
1025        item::{Item, ItemHandle},
1026        open_new, open_paths, pane, NewFile, OpenVisible, SaveIntent, SplitDirection,
1027        WorkspaceHandle,
1028    };
1029
1030    #[gpui::test]
1031    async fn test_open_non_existing_file(cx: &mut TestAppContext) {
1032        let app_state = init_test(cx);
1033        app_state
1034            .fs
1035            .as_fake()
1036            .insert_tree(
1037                "/root",
1038                json!({
1039                    "a": {
1040                    },
1041                }),
1042            )
1043            .await;
1044
1045        cx.update(|cx| {
1046            open_paths(
1047                &[PathBuf::from("/root/a/new")],
1048                app_state.clone(),
1049                workspace::OpenOptions::default(),
1050                cx,
1051            )
1052        })
1053        .await
1054        .unwrap();
1055        assert_eq!(cx.read(|cx| cx.windows().len()), 1);
1056
1057        let workspace = cx.windows()[0].downcast::<Workspace>().unwrap();
1058        workspace
1059            .update(cx, |workspace, cx| {
1060                assert!(workspace.active_item_as::<Editor>(cx).is_some())
1061            })
1062            .unwrap();
1063    }
1064
1065    #[gpui::test]
1066    async fn test_open_paths_action(cx: &mut TestAppContext) {
1067        let app_state = init_test(cx);
1068        app_state
1069            .fs
1070            .as_fake()
1071            .insert_tree(
1072                "/root",
1073                json!({
1074                    "a": {
1075                        "aa": null,
1076                        "ab": null,
1077                    },
1078                    "b": {
1079                        "ba": null,
1080                        "bb": null,
1081                    },
1082                    "c": {
1083                        "ca": null,
1084                        "cb": null,
1085                    },
1086                    "d": {
1087                        "da": null,
1088                        "db": null,
1089                    },
1090                    "e": {
1091                        "ea": null,
1092                        "eb": null,
1093                    }
1094                }),
1095            )
1096            .await;
1097
1098        cx.update(|cx| {
1099            open_paths(
1100                &[PathBuf::from("/root/a"), PathBuf::from("/root/b")],
1101                app_state.clone(),
1102                workspace::OpenOptions::default(),
1103                cx,
1104            )
1105        })
1106        .await
1107        .unwrap();
1108        assert_eq!(cx.read(|cx| cx.windows().len()), 1);
1109
1110        cx.update(|cx| {
1111            open_paths(
1112                &[PathBuf::from("/root/a")],
1113                app_state.clone(),
1114                workspace::OpenOptions::default(),
1115                cx,
1116            )
1117        })
1118        .await
1119        .unwrap();
1120        assert_eq!(cx.read(|cx| cx.windows().len()), 1);
1121        let workspace_1 = cx
1122            .read(|cx| cx.windows()[0].downcast::<Workspace>())
1123            .unwrap();
1124        cx.run_until_parked();
1125        workspace_1
1126            .update(cx, |workspace, cx| {
1127                assert_eq!(workspace.worktrees(cx).count(), 2);
1128                assert!(workspace.left_dock().read(cx).is_open());
1129                assert!(workspace
1130                    .active_pane()
1131                    .read(cx)
1132                    .focus_handle(cx)
1133                    .is_focused(cx));
1134            })
1135            .unwrap();
1136
1137        cx.update(|cx| {
1138            open_paths(
1139                &[PathBuf::from("/root/c"), PathBuf::from("/root/d")],
1140                app_state.clone(),
1141                workspace::OpenOptions::default(),
1142                cx,
1143            )
1144        })
1145        .await
1146        .unwrap();
1147        assert_eq!(cx.read(|cx| cx.windows().len()), 2);
1148
1149        // Replace existing windows
1150        let window = cx
1151            .update(|cx| cx.windows()[0].downcast::<Workspace>())
1152            .unwrap();
1153        cx.update(|cx| {
1154            open_paths(
1155                &[PathBuf::from("/root/e")],
1156                app_state,
1157                workspace::OpenOptions {
1158                    replace_window: Some(window),
1159                    ..Default::default()
1160                },
1161                cx,
1162            )
1163        })
1164        .await
1165        .unwrap();
1166        cx.background_executor.run_until_parked();
1167        assert_eq!(cx.read(|cx| cx.windows().len()), 2);
1168        let workspace_1 = cx
1169            .update(|cx| cx.windows()[0].downcast::<Workspace>())
1170            .unwrap();
1171        workspace_1
1172            .update(cx, |workspace, cx| {
1173                assert_eq!(
1174                    workspace
1175                        .worktrees(cx)
1176                        .map(|w| w.read(cx).abs_path())
1177                        .collect::<Vec<_>>(),
1178                    &[Path::new("/root/e").into()]
1179                );
1180                assert!(workspace.left_dock().read(cx).is_open());
1181                assert!(workspace.active_pane().focus_handle(cx).is_focused(cx));
1182            })
1183            .unwrap();
1184    }
1185
1186    #[gpui::test]
1187    async fn test_open_add_new(cx: &mut TestAppContext) {
1188        let app_state = init_test(cx);
1189        app_state
1190            .fs
1191            .as_fake()
1192            .insert_tree("/root", json!({"a": "hey", "b": "", "dir": {"c": "f"}}))
1193            .await;
1194
1195        cx.update(|cx| {
1196            open_paths(
1197                &[PathBuf::from("/root/dir")],
1198                app_state.clone(),
1199                workspace::OpenOptions::default(),
1200                cx,
1201            )
1202        })
1203        .await
1204        .unwrap();
1205        assert_eq!(cx.update(|cx| cx.windows().len()), 1);
1206
1207        cx.update(|cx| {
1208            open_paths(
1209                &[PathBuf::from("/root/a")],
1210                app_state.clone(),
1211                workspace::OpenOptions {
1212                    open_new_workspace: Some(false),
1213                    ..Default::default()
1214                },
1215                cx,
1216            )
1217        })
1218        .await
1219        .unwrap();
1220        assert_eq!(cx.update(|cx| cx.windows().len()), 1);
1221
1222        cx.update(|cx| {
1223            open_paths(
1224                &[PathBuf::from("/root/dir/c")],
1225                app_state.clone(),
1226                workspace::OpenOptions {
1227                    open_new_workspace: Some(true),
1228                    ..Default::default()
1229                },
1230                cx,
1231            )
1232        })
1233        .await
1234        .unwrap();
1235        assert_eq!(cx.update(|cx| cx.windows().len()), 2);
1236    }
1237
1238    #[gpui::test]
1239    async fn test_open_file_in_many_spaces(cx: &mut TestAppContext) {
1240        let app_state = init_test(cx);
1241        app_state
1242            .fs
1243            .as_fake()
1244            .insert_tree("/root", json!({"dir1": {"a": "b"}, "dir2": {"c": "d"}}))
1245            .await;
1246
1247        cx.update(|cx| {
1248            open_paths(
1249                &[PathBuf::from("/root/dir1/a")],
1250                app_state.clone(),
1251                workspace::OpenOptions::default(),
1252                cx,
1253            )
1254        })
1255        .await
1256        .unwrap();
1257        assert_eq!(cx.update(|cx| cx.windows().len()), 1);
1258        let window1 = cx.update(|cx| cx.active_window().unwrap());
1259
1260        cx.update(|cx| {
1261            open_paths(
1262                &[PathBuf::from("/root/dir2/c")],
1263                app_state.clone(),
1264                workspace::OpenOptions::default(),
1265                cx,
1266            )
1267        })
1268        .await
1269        .unwrap();
1270        assert_eq!(cx.update(|cx| cx.windows().len()), 1);
1271
1272        cx.update(|cx| {
1273            open_paths(
1274                &[PathBuf::from("/root/dir2")],
1275                app_state.clone(),
1276                workspace::OpenOptions::default(),
1277                cx,
1278            )
1279        })
1280        .await
1281        .unwrap();
1282        assert_eq!(cx.update(|cx| cx.windows().len()), 2);
1283        let window2 = cx.update(|cx| cx.active_window().unwrap());
1284        assert!(window1 != window2);
1285        cx.update_window(window1, |_, cx| cx.activate_window())
1286            .unwrap();
1287
1288        cx.update(|cx| {
1289            open_paths(
1290                &[PathBuf::from("/root/dir2/c")],
1291                app_state.clone(),
1292                workspace::OpenOptions::default(),
1293                cx,
1294            )
1295        })
1296        .await
1297        .unwrap();
1298        assert_eq!(cx.update(|cx| cx.windows().len()), 2);
1299        // should have opened in window2 because that has dir2 visibly open (window1 has it open, but not in the project panel)
1300        assert!(cx.update(|cx| cx.active_window().unwrap()) == window2);
1301    }
1302
1303    #[gpui::test]
1304    async fn test_window_edit_state_restoring_disabled(cx: &mut TestAppContext) {
1305        let executor = cx.executor();
1306        let app_state = init_test(cx);
1307
1308        cx.update(|cx| {
1309            SettingsStore::update_global(cx, |store, cx| {
1310                store.update_user_settings::<ProjectSettings>(cx, |settings| {
1311                    settings.session.restore_unsaved_buffers = false
1312                });
1313            });
1314        });
1315
1316        app_state
1317            .fs
1318            .as_fake()
1319            .insert_tree("/root", json!({"a": "hey"}))
1320            .await;
1321
1322        cx.update(|cx| {
1323            open_paths(
1324                &[PathBuf::from("/root/a")],
1325                app_state.clone(),
1326                workspace::OpenOptions::default(),
1327                cx,
1328            )
1329        })
1330        .await
1331        .unwrap();
1332        assert_eq!(cx.update(|cx| cx.windows().len()), 1);
1333
1334        // When opening the workspace, the window is not in a edited state.
1335        let window = cx.update(|cx| cx.windows()[0].downcast::<Workspace>().unwrap());
1336
1337        let window_is_edited = |window: WindowHandle<Workspace>, cx: &mut TestAppContext| {
1338            cx.update(|cx| window.read(cx).unwrap().is_edited())
1339        };
1340        let pane = window
1341            .read_with(cx, |workspace, _| workspace.active_pane().clone())
1342            .unwrap();
1343        let editor = window
1344            .read_with(cx, |workspace, cx| {
1345                workspace
1346                    .active_item(cx)
1347                    .unwrap()
1348                    .downcast::<Editor>()
1349                    .unwrap()
1350            })
1351            .unwrap();
1352
1353        assert!(!window_is_edited(window, cx));
1354
1355        // Editing a buffer marks the window as edited.
1356        window
1357            .update(cx, |_, cx| {
1358                editor.update(cx, |editor, cx| editor.insert("EDIT", cx));
1359            })
1360            .unwrap();
1361
1362        assert!(window_is_edited(window, cx));
1363
1364        // Undoing the edit restores the window's edited state.
1365        window
1366            .update(cx, |_, cx| {
1367                editor.update(cx, |editor, cx| editor.undo(&Default::default(), cx));
1368            })
1369            .unwrap();
1370        assert!(!window_is_edited(window, cx));
1371
1372        // Redoing the edit marks the window as edited again.
1373        window
1374            .update(cx, |_, cx| {
1375                editor.update(cx, |editor, cx| editor.redo(&Default::default(), cx));
1376            })
1377            .unwrap();
1378        assert!(window_is_edited(window, cx));
1379
1380        // Closing the item restores the window's edited state.
1381        let close = window
1382            .update(cx, |_, cx| {
1383                pane.update(cx, |pane, cx| {
1384                    drop(editor);
1385                    pane.close_active_item(&Default::default(), cx).unwrap()
1386                })
1387            })
1388            .unwrap();
1389        executor.run_until_parked();
1390
1391        cx.simulate_prompt_answer(1);
1392        close.await.unwrap();
1393        assert!(!window_is_edited(window, cx));
1394
1395        // Advance the clock to ensure that the item has been serialized and dropped from the queue
1396        cx.executor().advance_clock(Duration::from_secs(1));
1397
1398        // Opening the buffer again doesn't impact the window's edited state.
1399        cx.update(|cx| {
1400            open_paths(
1401                &[PathBuf::from("/root/a")],
1402                app_state,
1403                workspace::OpenOptions::default(),
1404                cx,
1405            )
1406        })
1407        .await
1408        .unwrap();
1409        executor.run_until_parked();
1410
1411        window
1412            .update(cx, |workspace, cx| {
1413                let editor = workspace
1414                    .active_item(cx)
1415                    .unwrap()
1416                    .downcast::<Editor>()
1417                    .unwrap();
1418
1419                editor.update(cx, |editor, cx| {
1420                    assert_eq!(editor.text(cx), "hey");
1421                });
1422            })
1423            .unwrap();
1424
1425        let editor = window
1426            .read_with(cx, |workspace, cx| {
1427                workspace
1428                    .active_item(cx)
1429                    .unwrap()
1430                    .downcast::<Editor>()
1431                    .unwrap()
1432            })
1433            .unwrap();
1434        assert!(!window_is_edited(window, cx));
1435
1436        // Editing the buffer marks the window as edited.
1437        window
1438            .update(cx, |_, cx| {
1439                editor.update(cx, |editor, cx| editor.insert("EDIT", cx));
1440            })
1441            .unwrap();
1442        executor.run_until_parked();
1443        assert!(window_is_edited(window, cx));
1444
1445        // Ensure closing the window via the mouse gets preempted due to the
1446        // buffer having unsaved changes.
1447        assert!(!VisualTestContext::from_window(window.into(), cx).simulate_close());
1448        executor.run_until_parked();
1449        assert_eq!(cx.update(|cx| cx.windows().len()), 1);
1450
1451        // The window is successfully closed after the user dismisses the prompt.
1452        cx.simulate_prompt_answer(1);
1453        executor.run_until_parked();
1454        assert_eq!(cx.update(|cx| cx.windows().len()), 0);
1455    }
1456
1457    #[gpui::test]
1458    async fn test_window_edit_state_restoring_enabled(cx: &mut TestAppContext) {
1459        let app_state = init_test(cx);
1460        app_state
1461            .fs
1462            .as_fake()
1463            .insert_tree("/root", json!({"a": "hey"}))
1464            .await;
1465
1466        cx.update(|cx| {
1467            open_paths(
1468                &[PathBuf::from("/root/a")],
1469                app_state.clone(),
1470                workspace::OpenOptions::default(),
1471                cx,
1472            )
1473        })
1474        .await
1475        .unwrap();
1476
1477        assert_eq!(cx.update(|cx| cx.windows().len()), 1);
1478
1479        // When opening the workspace, the window is not in a edited state.
1480        let window = cx.update(|cx| cx.windows()[0].downcast::<Workspace>().unwrap());
1481
1482        let window_is_edited = |window: WindowHandle<Workspace>, cx: &mut TestAppContext| {
1483            cx.update(|cx| window.read(cx).unwrap().is_edited())
1484        };
1485
1486        let editor = window
1487            .read_with(cx, |workspace, cx| {
1488                workspace
1489                    .active_item(cx)
1490                    .unwrap()
1491                    .downcast::<Editor>()
1492                    .unwrap()
1493            })
1494            .unwrap();
1495
1496        assert!(!window_is_edited(window, cx));
1497
1498        // Editing a buffer marks the window as edited.
1499        window
1500            .update(cx, |_, cx| {
1501                editor.update(cx, |editor, cx| editor.insert("EDIT", cx));
1502            })
1503            .unwrap();
1504
1505        assert!(window_is_edited(window, cx));
1506        cx.run_until_parked();
1507
1508        // Advance the clock to make sure the workspace is serialized
1509        cx.executor().advance_clock(Duration::from_secs(1));
1510
1511        // When closing the window, no prompt shows up and the window is closed.
1512        // buffer having unsaved changes.
1513        assert!(!VisualTestContext::from_window(window.into(), cx).simulate_close());
1514        cx.run_until_parked();
1515        assert_eq!(cx.update(|cx| cx.windows().len()), 0);
1516
1517        // When we now reopen the window, the edited state and the edited buffer are back
1518        cx.update(|cx| {
1519            open_paths(
1520                &[PathBuf::from("/root/a")],
1521                app_state.clone(),
1522                workspace::OpenOptions::default(),
1523                cx,
1524            )
1525        })
1526        .await
1527        .unwrap();
1528
1529        assert_eq!(cx.update(|cx| cx.windows().len()), 1);
1530        assert!(cx.update(|cx| cx.active_window().is_some()));
1531
1532        // When opening the workspace, the window is not in a edited state.
1533        let window = cx.update(|cx| cx.active_window().unwrap().downcast::<Workspace>().unwrap());
1534        assert!(window_is_edited(window, cx));
1535
1536        window
1537            .update(cx, |workspace, cx| {
1538                let editor = workspace
1539                    .active_item(cx)
1540                    .unwrap()
1541                    .downcast::<editor::Editor>()
1542                    .unwrap();
1543                editor.update(cx, |editor, cx| {
1544                    assert_eq!(editor.text(cx), "EDIThey");
1545                    assert!(editor.is_dirty(cx));
1546                });
1547
1548                editor
1549            })
1550            .unwrap();
1551    }
1552
1553    #[gpui::test]
1554    async fn test_new_empty_workspace(cx: &mut TestAppContext) {
1555        let app_state = init_test(cx);
1556        cx.update(|cx| {
1557            open_new(app_state.clone(), cx, |workspace, cx| {
1558                Editor::new_file(workspace, &Default::default(), cx)
1559            })
1560        })
1561        .await
1562        .unwrap();
1563        cx.run_until_parked();
1564
1565        let workspace = cx
1566            .update(|cx| cx.windows().first().unwrap().downcast::<Workspace>())
1567            .unwrap();
1568
1569        let editor = workspace
1570            .update(cx, |workspace, cx| {
1571                let editor = workspace
1572                    .active_item(cx)
1573                    .unwrap()
1574                    .downcast::<editor::Editor>()
1575                    .unwrap();
1576                editor.update(cx, |editor, cx| {
1577                    assert!(editor.text(cx).is_empty());
1578                    assert!(!editor.is_dirty(cx));
1579                });
1580
1581                editor
1582            })
1583            .unwrap();
1584
1585        let save_task = workspace
1586            .update(cx, |workspace, cx| {
1587                workspace.save_active_item(SaveIntent::Save, cx)
1588            })
1589            .unwrap();
1590        app_state.fs.create_dir(Path::new("/root")).await.unwrap();
1591        cx.background_executor.run_until_parked();
1592        cx.simulate_new_path_selection(|_| Some(PathBuf::from("/root/the-new-name")));
1593        save_task.await.unwrap();
1594        workspace
1595            .update(cx, |_, cx| {
1596                editor.update(cx, |editor, cx| {
1597                    assert!(!editor.is_dirty(cx));
1598                    assert_eq!(editor.title(cx), "the-new-name");
1599                });
1600            })
1601            .unwrap();
1602    }
1603
1604    #[gpui::test]
1605    async fn test_open_entry(cx: &mut TestAppContext) {
1606        let app_state = init_test(cx);
1607        app_state
1608            .fs
1609            .as_fake()
1610            .insert_tree(
1611                "/root",
1612                json!({
1613                    "a": {
1614                        "file1": "contents 1",
1615                        "file2": "contents 2",
1616                        "file3": "contents 3",
1617                    },
1618                }),
1619            )
1620            .await;
1621
1622        let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
1623        project.update(cx, |project, _cx| {
1624            project.languages().add(markdown_language())
1625        });
1626        let window = cx.add_window(|cx| Workspace::test_new(project, cx));
1627        let workspace = window.root(cx).unwrap();
1628
1629        let entries = cx.read(|cx| workspace.file_project_paths(cx));
1630        let file1 = entries[0].clone();
1631        let file2 = entries[1].clone();
1632        let file3 = entries[2].clone();
1633
1634        // Open the first entry
1635        let entry_1 = window
1636            .update(cx, |w, cx| w.open_path(file1.clone(), None, true, cx))
1637            .unwrap()
1638            .await
1639            .unwrap();
1640        cx.read(|cx| {
1641            let pane = workspace.read(cx).active_pane().read(cx);
1642            assert_eq!(
1643                pane.active_item().unwrap().project_path(cx),
1644                Some(file1.clone())
1645            );
1646            assert_eq!(pane.items_len(), 1);
1647        });
1648
1649        // Open the second entry
1650        window
1651            .update(cx, |w, cx| w.open_path(file2.clone(), None, true, cx))
1652            .unwrap()
1653            .await
1654            .unwrap();
1655        cx.read(|cx| {
1656            let pane = workspace.read(cx).active_pane().read(cx);
1657            assert_eq!(
1658                pane.active_item().unwrap().project_path(cx),
1659                Some(file2.clone())
1660            );
1661            assert_eq!(pane.items_len(), 2);
1662        });
1663
1664        // Open the first entry again. The existing pane item is activated.
1665        let entry_1b = window
1666            .update(cx, |w, cx| w.open_path(file1.clone(), None, true, cx))
1667            .unwrap()
1668            .await
1669            .unwrap();
1670        assert_eq!(entry_1.item_id(), entry_1b.item_id());
1671
1672        cx.read(|cx| {
1673            let pane = workspace.read(cx).active_pane().read(cx);
1674            assert_eq!(
1675                pane.active_item().unwrap().project_path(cx),
1676                Some(file1.clone())
1677            );
1678            assert_eq!(pane.items_len(), 2);
1679        });
1680
1681        // Split the pane with the first entry, then open the second entry again.
1682        window
1683            .update(cx, |w, cx| {
1684                w.split_and_clone(w.active_pane().clone(), SplitDirection::Right, cx);
1685                w.open_path(file2.clone(), None, true, cx)
1686            })
1687            .unwrap()
1688            .await
1689            .unwrap();
1690
1691        window
1692            .read_with(cx, |w, cx| {
1693                assert_eq!(
1694                    w.active_pane()
1695                        .read(cx)
1696                        .active_item()
1697                        .unwrap()
1698                        .project_path(cx),
1699                    Some(file2.clone())
1700                );
1701            })
1702            .unwrap();
1703
1704        // Open the third entry twice concurrently. Only one pane item is added.
1705        let (t1, t2) = window
1706            .update(cx, |w, cx| {
1707                (
1708                    w.open_path(file3.clone(), None, true, cx),
1709                    w.open_path(file3.clone(), None, true, cx),
1710                )
1711            })
1712            .unwrap();
1713        t1.await.unwrap();
1714        t2.await.unwrap();
1715        cx.read(|cx| {
1716            let pane = workspace.read(cx).active_pane().read(cx);
1717            assert_eq!(
1718                pane.active_item().unwrap().project_path(cx),
1719                Some(file3.clone())
1720            );
1721            let pane_entries = pane
1722                .items()
1723                .map(|i| i.project_path(cx).unwrap())
1724                .collect::<Vec<_>>();
1725            assert_eq!(pane_entries, &[file1, file2, file3]);
1726        });
1727    }
1728
1729    #[gpui::test]
1730    async fn test_open_paths(cx: &mut TestAppContext) {
1731        let app_state = init_test(cx);
1732
1733        app_state
1734            .fs
1735            .as_fake()
1736            .insert_tree(
1737                "/",
1738                json!({
1739                    "dir1": {
1740                        "a.txt": ""
1741                    },
1742                    "dir2": {
1743                        "b.txt": ""
1744                    },
1745                    "dir3": {
1746                        "c.txt": ""
1747                    },
1748                    "d.txt": ""
1749                }),
1750            )
1751            .await;
1752
1753        cx.update(|cx| {
1754            open_paths(
1755                &[PathBuf::from("/dir1/")],
1756                app_state,
1757                workspace::OpenOptions::default(),
1758                cx,
1759            )
1760        })
1761        .await
1762        .unwrap();
1763        assert_eq!(cx.update(|cx| cx.windows().len()), 1);
1764        let window = cx.update(|cx| cx.windows()[0].downcast::<Workspace>().unwrap());
1765        let workspace = window.root(cx).unwrap();
1766
1767        #[track_caller]
1768        fn assert_project_panel_selection(
1769            workspace: &Workspace,
1770            expected_worktree_path: &Path,
1771            expected_entry_path: &Path,
1772            cx: &AppContext,
1773        ) {
1774            let project_panel = [
1775                workspace.left_dock().read(cx).panel::<ProjectPanel>(),
1776                workspace.right_dock().read(cx).panel::<ProjectPanel>(),
1777                workspace.bottom_dock().read(cx).panel::<ProjectPanel>(),
1778            ]
1779            .into_iter()
1780            .find_map(std::convert::identity)
1781            .expect("found no project panels")
1782            .read(cx);
1783            let (selected_worktree, selected_entry) = project_panel
1784                .selected_entry(cx)
1785                .expect("project panel should have a selected entry");
1786            assert_eq!(
1787                selected_worktree.abs_path().as_ref(),
1788                expected_worktree_path,
1789                "Unexpected project panel selected worktree path"
1790            );
1791            assert_eq!(
1792                selected_entry.path.as_ref(),
1793                expected_entry_path,
1794                "Unexpected project panel selected entry path"
1795            );
1796        }
1797
1798        // Open a file within an existing worktree.
1799        window
1800            .update(cx, |view, cx| {
1801                view.open_paths(vec!["/dir1/a.txt".into()], OpenVisible::All, None, cx)
1802            })
1803            .unwrap()
1804            .await;
1805        cx.read(|cx| {
1806            let workspace = workspace.read(cx);
1807            assert_project_panel_selection(workspace, Path::new("/dir1"), Path::new("a.txt"), cx);
1808            assert_eq!(
1809                workspace
1810                    .active_pane()
1811                    .read(cx)
1812                    .active_item()
1813                    .unwrap()
1814                    .act_as::<Editor>(cx)
1815                    .unwrap()
1816                    .read(cx)
1817                    .title(cx),
1818                "a.txt"
1819            );
1820        });
1821
1822        // Open a file outside of any existing worktree.
1823        window
1824            .update(cx, |view, cx| {
1825                view.open_paths(vec!["/dir2/b.txt".into()], OpenVisible::All, None, cx)
1826            })
1827            .unwrap()
1828            .await;
1829        cx.read(|cx| {
1830            let workspace = workspace.read(cx);
1831            assert_project_panel_selection(workspace, Path::new("/dir2/b.txt"), Path::new(""), cx);
1832            let worktree_roots = workspace
1833                .worktrees(cx)
1834                .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref())
1835                .collect::<HashSet<_>>();
1836            assert_eq!(
1837                worktree_roots,
1838                vec!["/dir1", "/dir2/b.txt"]
1839                    .into_iter()
1840                    .map(Path::new)
1841                    .collect(),
1842            );
1843            assert_eq!(
1844                workspace
1845                    .active_pane()
1846                    .read(cx)
1847                    .active_item()
1848                    .unwrap()
1849                    .act_as::<Editor>(cx)
1850                    .unwrap()
1851                    .read(cx)
1852                    .title(cx),
1853                "b.txt"
1854            );
1855        });
1856
1857        // Ensure opening a directory and one of its children only adds one worktree.
1858        window
1859            .update(cx, |view, cx| {
1860                view.open_paths(
1861                    vec!["/dir3".into(), "/dir3/c.txt".into()],
1862                    OpenVisible::All,
1863                    None,
1864                    cx,
1865                )
1866            })
1867            .unwrap()
1868            .await;
1869        cx.read(|cx| {
1870            let workspace = workspace.read(cx);
1871            assert_project_panel_selection(workspace, Path::new("/dir3"), Path::new("c.txt"), cx);
1872            let worktree_roots = workspace
1873                .worktrees(cx)
1874                .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref())
1875                .collect::<HashSet<_>>();
1876            assert_eq!(
1877                worktree_roots,
1878                vec!["/dir1", "/dir2/b.txt", "/dir3"]
1879                    .into_iter()
1880                    .map(Path::new)
1881                    .collect(),
1882            );
1883            assert_eq!(
1884                workspace
1885                    .active_pane()
1886                    .read(cx)
1887                    .active_item()
1888                    .unwrap()
1889                    .act_as::<Editor>(cx)
1890                    .unwrap()
1891                    .read(cx)
1892                    .title(cx),
1893                "c.txt"
1894            );
1895        });
1896
1897        // Ensure opening invisibly a file outside an existing worktree adds a new, invisible worktree.
1898        window
1899            .update(cx, |view, cx| {
1900                view.open_paths(vec!["/d.txt".into()], OpenVisible::None, None, cx)
1901            })
1902            .unwrap()
1903            .await;
1904        cx.read(|cx| {
1905            let workspace = workspace.read(cx);
1906            assert_project_panel_selection(workspace, Path::new("/d.txt"), Path::new(""), cx);
1907            let worktree_roots = workspace
1908                .worktrees(cx)
1909                .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref())
1910                .collect::<HashSet<_>>();
1911            assert_eq!(
1912                worktree_roots,
1913                vec!["/dir1", "/dir2/b.txt", "/dir3", "/d.txt"]
1914                    .into_iter()
1915                    .map(Path::new)
1916                    .collect(),
1917            );
1918
1919            let visible_worktree_roots = workspace
1920                .visible_worktrees(cx)
1921                .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref())
1922                .collect::<HashSet<_>>();
1923            assert_eq!(
1924                visible_worktree_roots,
1925                vec!["/dir1", "/dir2/b.txt", "/dir3"]
1926                    .into_iter()
1927                    .map(Path::new)
1928                    .collect(),
1929            );
1930
1931            assert_eq!(
1932                workspace
1933                    .active_pane()
1934                    .read(cx)
1935                    .active_item()
1936                    .unwrap()
1937                    .act_as::<Editor>(cx)
1938                    .unwrap()
1939                    .read(cx)
1940                    .title(cx),
1941                "d.txt"
1942            );
1943        });
1944    }
1945
1946    #[gpui::test]
1947    async fn test_opening_excluded_paths(cx: &mut TestAppContext) {
1948        let app_state = init_test(cx);
1949        cx.update(|cx| {
1950            cx.update_global::<SettingsStore, _>(|store, cx| {
1951                store.update_user_settings::<WorktreeSettings>(cx, |project_settings| {
1952                    project_settings.file_scan_exclusions =
1953                        Some(vec!["excluded_dir".to_string(), "**/.git".to_string()]);
1954                });
1955            });
1956        });
1957        app_state
1958            .fs
1959            .as_fake()
1960            .insert_tree(
1961                "/root",
1962                json!({
1963                    ".gitignore": "ignored_dir\n",
1964                    ".git": {
1965                        "HEAD": "ref: refs/heads/main",
1966                    },
1967                    "regular_dir": {
1968                        "file": "regular file contents",
1969                    },
1970                    "ignored_dir": {
1971                        "ignored_subdir": {
1972                            "file": "ignored subfile contents",
1973                        },
1974                        "file": "ignored file contents",
1975                    },
1976                    "excluded_dir": {
1977                        "file": "excluded file contents",
1978                        "ignored_subdir": {
1979                            "file": "ignored subfile contents",
1980                        },
1981                    },
1982                }),
1983            )
1984            .await;
1985
1986        let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
1987        project.update(cx, |project, _cx| {
1988            project.languages().add(markdown_language())
1989        });
1990        let window = cx.add_window(|cx| Workspace::test_new(project, cx));
1991        let workspace = window.root(cx).unwrap();
1992
1993        let initial_entries = cx.read(|cx| workspace.file_project_paths(cx));
1994        let paths_to_open = [
1995            Path::new("/root/excluded_dir/file").to_path_buf(),
1996            Path::new("/root/.git/HEAD").to_path_buf(),
1997            Path::new("/root/excluded_dir/ignored_subdir").to_path_buf(),
1998        ];
1999        let (opened_workspace, new_items) = cx
2000            .update(|cx| {
2001                workspace::open_paths(
2002                    &paths_to_open,
2003                    app_state,
2004                    workspace::OpenOptions::default(),
2005                    cx,
2006                )
2007            })
2008            .await
2009            .unwrap();
2010
2011        assert_eq!(
2012            opened_workspace.root_view(cx).unwrap().entity_id(),
2013            workspace.entity_id(),
2014            "Excluded files in subfolders of a workspace root should be opened in the workspace"
2015        );
2016        let mut opened_paths = cx.read(|cx| {
2017            assert_eq!(
2018                new_items.len(),
2019                paths_to_open.len(),
2020                "Expect to get the same number of opened items as submitted paths to open"
2021            );
2022            new_items
2023                .iter()
2024                .zip(paths_to_open.iter())
2025                .map(|(i, path)| {
2026                    match i {
2027                        Some(Ok(i)) => {
2028                            Some(i.project_path(cx).map(|p| p.path.display().to_string()))
2029                        }
2030                        Some(Err(e)) => panic!("Excluded file {path:?} failed to open: {e:?}"),
2031                        None => None,
2032                    }
2033                    .flatten()
2034                })
2035                .collect::<Vec<_>>()
2036        });
2037        opened_paths.sort();
2038        assert_eq!(
2039            opened_paths,
2040            vec![
2041                None,
2042                Some(".git/HEAD".to_string()),
2043                Some("excluded_dir/file".to_string()),
2044            ],
2045            "Excluded files should get opened, excluded dir should not get opened"
2046        );
2047
2048        let entries = cx.read(|cx| workspace.file_project_paths(cx));
2049        assert_eq!(
2050                initial_entries, entries,
2051                "Workspace entries should not change after opening excluded files and directories paths"
2052            );
2053
2054        cx.read(|cx| {
2055                let pane = workspace.read(cx).active_pane().read(cx);
2056                let mut opened_buffer_paths = pane
2057                    .items()
2058                    .map(|i| {
2059                        i.project_path(cx)
2060                            .expect("all excluded files that got open should have a path")
2061                            .path
2062                            .display()
2063                            .to_string()
2064                    })
2065                    .collect::<Vec<_>>();
2066                opened_buffer_paths.sort();
2067                assert_eq!(
2068                    opened_buffer_paths,
2069                    vec![".git/HEAD".to_string(), "excluded_dir/file".to_string()],
2070                    "Despite not being present in the worktrees, buffers for excluded files are opened and added to the pane"
2071                );
2072            });
2073    }
2074
2075    #[gpui::test]
2076    async fn test_save_conflicting_item(cx: &mut TestAppContext) {
2077        let app_state = init_test(cx);
2078        app_state
2079            .fs
2080            .as_fake()
2081            .insert_tree("/root", json!({ "a.txt": "" }))
2082            .await;
2083
2084        let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
2085        project.update(cx, |project, _cx| {
2086            project.languages().add(markdown_language())
2087        });
2088        let window = cx.add_window(|cx| Workspace::test_new(project, cx));
2089        let workspace = window.root(cx).unwrap();
2090
2091        // Open a file within an existing worktree.
2092        window
2093            .update(cx, |view, cx| {
2094                view.open_paths(
2095                    vec![PathBuf::from("/root/a.txt")],
2096                    OpenVisible::All,
2097                    None,
2098                    cx,
2099                )
2100            })
2101            .unwrap()
2102            .await;
2103        let editor = cx.read(|cx| {
2104            let pane = workspace.read(cx).active_pane().read(cx);
2105            let item = pane.active_item().unwrap();
2106            item.downcast::<Editor>().unwrap()
2107        });
2108
2109        window
2110            .update(cx, |_, cx| {
2111                editor.update(cx, |editor, cx| editor.handle_input("x", cx));
2112            })
2113            .unwrap();
2114
2115        app_state
2116            .fs
2117            .as_fake()
2118            .insert_file("/root/a.txt", b"changed".to_vec())
2119            .await;
2120
2121        cx.run_until_parked();
2122        cx.read(|cx| assert!(editor.is_dirty(cx)));
2123        cx.read(|cx| assert!(editor.has_conflict(cx)));
2124
2125        let save_task = window
2126            .update(cx, |workspace, cx| {
2127                workspace.save_active_item(SaveIntent::Save, cx)
2128            })
2129            .unwrap();
2130        cx.background_executor.run_until_parked();
2131        cx.simulate_prompt_answer(0);
2132        save_task.await.unwrap();
2133        window
2134            .update(cx, |_, cx| {
2135                editor.update(cx, |editor, cx| {
2136                    assert!(!editor.is_dirty(cx));
2137                    assert!(!editor.has_conflict(cx));
2138                });
2139            })
2140            .unwrap();
2141    }
2142
2143    #[gpui::test]
2144    async fn test_open_and_save_new_file(cx: &mut TestAppContext) {
2145        let app_state = init_test(cx);
2146        app_state.fs.create_dir(Path::new("/root")).await.unwrap();
2147
2148        let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
2149        project.update(cx, |project, _| {
2150            project.languages().add(markdown_language());
2151            project.languages().add(rust_lang());
2152        });
2153        let window = cx.add_window(|cx| Workspace::test_new(project, cx));
2154        let worktree = cx.update(|cx| window.read(cx).unwrap().worktrees(cx).next().unwrap());
2155
2156        // Create a new untitled buffer
2157        cx.dispatch_action(window.into(), NewFile);
2158        let editor = window
2159            .read_with(cx, |workspace, cx| {
2160                workspace
2161                    .active_item(cx)
2162                    .unwrap()
2163                    .downcast::<Editor>()
2164                    .unwrap()
2165            })
2166            .unwrap();
2167
2168        window
2169            .update(cx, |_, cx| {
2170                editor.update(cx, |editor, cx| {
2171                    assert!(!editor.is_dirty(cx));
2172                    assert_eq!(editor.title(cx), "untitled");
2173                    assert!(Arc::ptr_eq(
2174                        &editor.buffer().read(cx).language_at(0, cx).unwrap(),
2175                        &languages::PLAIN_TEXT
2176                    ));
2177                    editor.handle_input("hi", cx);
2178                    assert!(editor.is_dirty(cx));
2179                });
2180            })
2181            .unwrap();
2182
2183        // Save the buffer. This prompts for a filename.
2184        let save_task = window
2185            .update(cx, |workspace, cx| {
2186                workspace.save_active_item(SaveIntent::Save, cx)
2187            })
2188            .unwrap();
2189        cx.background_executor.run_until_parked();
2190        cx.simulate_new_path_selection(|parent_dir| {
2191            assert_eq!(parent_dir, Path::new("/root"));
2192            Some(parent_dir.join("the-new-name.rs"))
2193        });
2194        cx.read(|cx| {
2195            assert!(editor.is_dirty(cx));
2196            assert_eq!(editor.read(cx).title(cx), "untitled");
2197        });
2198
2199        // When the save completes, the buffer's title is updated and the language is assigned based
2200        // on the path.
2201        save_task.await.unwrap();
2202        window
2203            .update(cx, |_, cx| {
2204                editor.update(cx, |editor, cx| {
2205                    assert!(!editor.is_dirty(cx));
2206                    assert_eq!(editor.title(cx), "the-new-name.rs");
2207                    assert_eq!(
2208                        editor
2209                            .buffer()
2210                            .read(cx)
2211                            .language_at(0, cx)
2212                            .unwrap()
2213                            .name()
2214                            .as_ref(),
2215                        "Rust"
2216                    );
2217                });
2218            })
2219            .unwrap();
2220
2221        // Edit the file and save it again. This time, there is no filename prompt.
2222        window
2223            .update(cx, |_, cx| {
2224                editor.update(cx, |editor, cx| {
2225                    editor.handle_input(" there", cx);
2226                    assert!(editor.is_dirty(cx));
2227                });
2228            })
2229            .unwrap();
2230
2231        let save_task = window
2232            .update(cx, |workspace, cx| {
2233                workspace.save_active_item(SaveIntent::Save, cx)
2234            })
2235            .unwrap();
2236        save_task.await.unwrap();
2237
2238        assert!(!cx.did_prompt_for_new_path());
2239        window
2240            .update(cx, |_, cx| {
2241                editor.update(cx, |editor, cx| {
2242                    assert!(!editor.is_dirty(cx));
2243                    assert_eq!(editor.title(cx), "the-new-name.rs")
2244                });
2245            })
2246            .unwrap();
2247
2248        // Open the same newly-created file in another pane item. The new editor should reuse
2249        // the same buffer.
2250        cx.dispatch_action(window.into(), NewFile);
2251        window
2252            .update(cx, |workspace, cx| {
2253                workspace.split_and_clone(
2254                    workspace.active_pane().clone(),
2255                    SplitDirection::Right,
2256                    cx,
2257                );
2258                workspace.open_path((worktree.read(cx).id(), "the-new-name.rs"), None, true, cx)
2259            })
2260            .unwrap()
2261            .await
2262            .unwrap();
2263        let editor2 = window
2264            .update(cx, |workspace, cx| {
2265                workspace
2266                    .active_item(cx)
2267                    .unwrap()
2268                    .downcast::<Editor>()
2269                    .unwrap()
2270            })
2271            .unwrap();
2272        cx.read(|cx| {
2273            assert_eq!(
2274                editor2.read(cx).buffer().read(cx).as_singleton().unwrap(),
2275                editor.read(cx).buffer().read(cx).as_singleton().unwrap()
2276            );
2277        })
2278    }
2279
2280    #[gpui::test]
2281    async fn test_setting_language_when_saving_as_single_file_worktree(cx: &mut TestAppContext) {
2282        let app_state = init_test(cx);
2283        app_state.fs.create_dir(Path::new("/root")).await.unwrap();
2284
2285        let project = Project::test(app_state.fs.clone(), [], cx).await;
2286        project.update(cx, |project, _| {
2287            project.languages().add(rust_lang());
2288            project.languages().add(markdown_language());
2289        });
2290        let window = cx.add_window(|cx| Workspace::test_new(project, cx));
2291
2292        // Create a new untitled buffer
2293        cx.dispatch_action(window.into(), NewFile);
2294        let editor = window
2295            .read_with(cx, |workspace, cx| {
2296                workspace
2297                    .active_item(cx)
2298                    .unwrap()
2299                    .downcast::<Editor>()
2300                    .unwrap()
2301            })
2302            .unwrap();
2303        window
2304            .update(cx, |_, cx| {
2305                editor.update(cx, |editor, cx| {
2306                    assert!(Arc::ptr_eq(
2307                        &editor.buffer().read(cx).language_at(0, cx).unwrap(),
2308                        &languages::PLAIN_TEXT
2309                    ));
2310                    editor.handle_input("hi", cx);
2311                    assert!(editor.is_dirty(cx));
2312                });
2313            })
2314            .unwrap();
2315
2316        // Save the buffer. This prompts for a filename.
2317        let save_task = window
2318            .update(cx, |workspace, cx| {
2319                workspace.save_active_item(SaveIntent::Save, cx)
2320            })
2321            .unwrap();
2322        cx.background_executor.run_until_parked();
2323        cx.simulate_new_path_selection(|_| Some(PathBuf::from("/root/the-new-name.rs")));
2324        save_task.await.unwrap();
2325        // The buffer is not dirty anymore and the language is assigned based on the path.
2326        window
2327            .update(cx, |_, cx| {
2328                editor.update(cx, |editor, cx| {
2329                    assert!(!editor.is_dirty(cx));
2330                    assert_eq!(
2331                        editor
2332                            .buffer()
2333                            .read(cx)
2334                            .language_at(0, cx)
2335                            .unwrap()
2336                            .name()
2337                            .as_ref(),
2338                        "Rust"
2339                    )
2340                });
2341            })
2342            .unwrap();
2343    }
2344
2345    #[gpui::test]
2346    async fn test_pane_actions(cx: &mut TestAppContext) {
2347        let app_state = init_test(cx);
2348        app_state
2349            .fs
2350            .as_fake()
2351            .insert_tree(
2352                "/root",
2353                json!({
2354                    "a": {
2355                        "file1": "contents 1",
2356                        "file2": "contents 2",
2357                        "file3": "contents 3",
2358                    },
2359                }),
2360            )
2361            .await;
2362
2363        let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
2364        project.update(cx, |project, _cx| {
2365            project.languages().add(markdown_language())
2366        });
2367        let window = cx.add_window(|cx| Workspace::test_new(project, cx));
2368        let workspace = window.root(cx).unwrap();
2369
2370        let entries = cx.read(|cx| workspace.file_project_paths(cx));
2371        let file1 = entries[0].clone();
2372
2373        let pane_1 = cx.read(|cx| workspace.read(cx).active_pane().clone());
2374
2375        window
2376            .update(cx, |w, cx| w.open_path(file1.clone(), None, true, cx))
2377            .unwrap()
2378            .await
2379            .unwrap();
2380
2381        let (editor_1, buffer) = window
2382            .update(cx, |_, cx| {
2383                pane_1.update(cx, |pane_1, cx| {
2384                    let editor = pane_1.active_item().unwrap().downcast::<Editor>().unwrap();
2385                    assert_eq!(editor.project_path(cx), Some(file1.clone()));
2386                    let buffer = editor.update(cx, |editor, cx| {
2387                        editor.insert("dirt", cx);
2388                        editor.buffer().downgrade()
2389                    });
2390                    (editor.downgrade(), buffer)
2391                })
2392            })
2393            .unwrap();
2394
2395        cx.dispatch_action(window.into(), pane::SplitRight);
2396        let editor_2 = cx.update(|cx| {
2397            let pane_2 = workspace.read(cx).active_pane().clone();
2398            assert_ne!(pane_1, pane_2);
2399
2400            let pane2_item = pane_2.read(cx).active_item().unwrap();
2401            assert_eq!(pane2_item.project_path(cx), Some(file1.clone()));
2402
2403            pane2_item.downcast::<Editor>().unwrap().downgrade()
2404        });
2405        cx.dispatch_action(
2406            window.into(),
2407            workspace::CloseActiveItem { save_intent: None },
2408        );
2409
2410        cx.background_executor.run_until_parked();
2411        window
2412            .read_with(cx, |workspace, _| {
2413                assert_eq!(workspace.panes().len(), 1);
2414                assert_eq!(workspace.active_pane(), &pane_1);
2415            })
2416            .unwrap();
2417
2418        cx.dispatch_action(
2419            window.into(),
2420            workspace::CloseActiveItem { save_intent: None },
2421        );
2422        cx.background_executor.run_until_parked();
2423        cx.simulate_prompt_answer(1);
2424        cx.background_executor.run_until_parked();
2425
2426        window
2427            .update(cx, |workspace, cx| {
2428                assert_eq!(workspace.panes().len(), 1);
2429                assert!(workspace.active_item(cx).is_none());
2430            })
2431            .unwrap();
2432
2433        cx.run_until_parked();
2434        editor_1.assert_released();
2435        editor_2.assert_released();
2436        buffer.assert_released();
2437    }
2438
2439    #[gpui::test]
2440    async fn test_navigation(cx: &mut TestAppContext) {
2441        let app_state = init_test(cx);
2442        app_state
2443            .fs
2444            .as_fake()
2445            .insert_tree(
2446                "/root",
2447                json!({
2448                    "a": {
2449                        "file1": "contents 1\n".repeat(20),
2450                        "file2": "contents 2\n".repeat(20),
2451                        "file3": "contents 3\n".repeat(20),
2452                    },
2453                }),
2454            )
2455            .await;
2456
2457        let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
2458        project.update(cx, |project, _cx| {
2459            project.languages().add(markdown_language())
2460        });
2461        let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
2462        let pane = workspace
2463            .read_with(cx, |workspace, _| workspace.active_pane().clone())
2464            .unwrap();
2465
2466        let entries = cx.update(|cx| workspace.root(cx).unwrap().file_project_paths(cx));
2467        let file1 = entries[0].clone();
2468        let file2 = entries[1].clone();
2469        let file3 = entries[2].clone();
2470
2471        let editor1 = workspace
2472            .update(cx, |w, cx| w.open_path(file1.clone(), None, true, cx))
2473            .unwrap()
2474            .await
2475            .unwrap()
2476            .downcast::<Editor>()
2477            .unwrap();
2478        workspace
2479            .update(cx, |_, cx| {
2480                editor1.update(cx, |editor, cx| {
2481                    editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
2482                        s.select_display_ranges([DisplayPoint::new(DisplayRow(10), 0)
2483                            ..DisplayPoint::new(DisplayRow(10), 0)])
2484                    });
2485                });
2486            })
2487            .unwrap();
2488
2489        let editor2 = workspace
2490            .update(cx, |w, cx| w.open_path(file2.clone(), None, true, cx))
2491            .unwrap()
2492            .await
2493            .unwrap()
2494            .downcast::<Editor>()
2495            .unwrap();
2496        let editor3 = workspace
2497            .update(cx, |w, cx| w.open_path(file3.clone(), None, true, cx))
2498            .unwrap()
2499            .await
2500            .unwrap()
2501            .downcast::<Editor>()
2502            .unwrap();
2503
2504        workspace
2505            .update(cx, |_, cx| {
2506                editor3.update(cx, |editor, cx| {
2507                    editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
2508                        s.select_display_ranges([DisplayPoint::new(DisplayRow(12), 0)
2509                            ..DisplayPoint::new(DisplayRow(12), 0)])
2510                    });
2511                    editor.newline(&Default::default(), cx);
2512                    editor.newline(&Default::default(), cx);
2513                    editor.move_down(&Default::default(), cx);
2514                    editor.move_down(&Default::default(), cx);
2515                    editor.save(true, project.clone(), cx)
2516                })
2517            })
2518            .unwrap()
2519            .await
2520            .unwrap();
2521        workspace
2522            .update(cx, |_, cx| {
2523                editor3.update(cx, |editor, cx| {
2524                    editor.set_scroll_position(point(0., 12.5), cx)
2525                });
2526            })
2527            .unwrap();
2528        assert_eq!(
2529            active_location(&workspace, cx),
2530            (file3.clone(), DisplayPoint::new(DisplayRow(16), 0), 12.5)
2531        );
2532
2533        workspace
2534            .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx))
2535            .unwrap()
2536            .await
2537            .unwrap();
2538        assert_eq!(
2539            active_location(&workspace, cx),
2540            (file3.clone(), DisplayPoint::new(DisplayRow(0), 0), 0.)
2541        );
2542
2543        workspace
2544            .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx))
2545            .unwrap()
2546            .await
2547            .unwrap();
2548        assert_eq!(
2549            active_location(&workspace, cx),
2550            (file2.clone(), DisplayPoint::new(DisplayRow(0), 0), 0.)
2551        );
2552
2553        workspace
2554            .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx))
2555            .unwrap()
2556            .await
2557            .unwrap();
2558        assert_eq!(
2559            active_location(&workspace, cx),
2560            (file1.clone(), DisplayPoint::new(DisplayRow(10), 0), 0.)
2561        );
2562
2563        workspace
2564            .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx))
2565            .unwrap()
2566            .await
2567            .unwrap();
2568        assert_eq!(
2569            active_location(&workspace, cx),
2570            (file1.clone(), DisplayPoint::new(DisplayRow(0), 0), 0.)
2571        );
2572
2573        // Go back one more time and ensure we don't navigate past the first item in the history.
2574        workspace
2575            .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx))
2576            .unwrap()
2577            .await
2578            .unwrap();
2579        assert_eq!(
2580            active_location(&workspace, cx),
2581            (file1.clone(), DisplayPoint::new(DisplayRow(0), 0), 0.)
2582        );
2583
2584        workspace
2585            .update(cx, |w, cx| w.go_forward(w.active_pane().downgrade(), cx))
2586            .unwrap()
2587            .await
2588            .unwrap();
2589        assert_eq!(
2590            active_location(&workspace, cx),
2591            (file1.clone(), DisplayPoint::new(DisplayRow(10), 0), 0.)
2592        );
2593
2594        workspace
2595            .update(cx, |w, cx| w.go_forward(w.active_pane().downgrade(), cx))
2596            .unwrap()
2597            .await
2598            .unwrap();
2599        assert_eq!(
2600            active_location(&workspace, cx),
2601            (file2.clone(), DisplayPoint::new(DisplayRow(0), 0), 0.)
2602        );
2603
2604        // Go forward to an item that has been closed, ensuring it gets re-opened at the same
2605        // location.
2606        workspace
2607            .update(cx, |_, cx| {
2608                pane.update(cx, |pane, cx| {
2609                    let editor3_id = editor3.entity_id();
2610                    drop(editor3);
2611                    pane.close_item_by_id(editor3_id, SaveIntent::Close, cx)
2612                })
2613            })
2614            .unwrap()
2615            .await
2616            .unwrap();
2617        workspace
2618            .update(cx, |w, cx| w.go_forward(w.active_pane().downgrade(), cx))
2619            .unwrap()
2620            .await
2621            .unwrap();
2622        assert_eq!(
2623            active_location(&workspace, cx),
2624            (file3.clone(), DisplayPoint::new(DisplayRow(0), 0), 0.)
2625        );
2626
2627        workspace
2628            .update(cx, |w, cx| w.go_forward(w.active_pane().downgrade(), cx))
2629            .unwrap()
2630            .await
2631            .unwrap();
2632        assert_eq!(
2633            active_location(&workspace, cx),
2634            (file3.clone(), DisplayPoint::new(DisplayRow(16), 0), 12.5)
2635        );
2636
2637        workspace
2638            .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx))
2639            .unwrap()
2640            .await
2641            .unwrap();
2642        assert_eq!(
2643            active_location(&workspace, cx),
2644            (file3.clone(), DisplayPoint::new(DisplayRow(0), 0), 0.)
2645        );
2646
2647        // Go back to an item that has been closed and removed from disk
2648        workspace
2649            .update(cx, |_, cx| {
2650                pane.update(cx, |pane, cx| {
2651                    let editor2_id = editor2.entity_id();
2652                    drop(editor2);
2653                    pane.close_item_by_id(editor2_id, SaveIntent::Close, cx)
2654                })
2655            })
2656            .unwrap()
2657            .await
2658            .unwrap();
2659        app_state
2660            .fs
2661            .remove_file(Path::new("/root/a/file2"), Default::default())
2662            .await
2663            .unwrap();
2664        cx.background_executor.run_until_parked();
2665
2666        workspace
2667            .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx))
2668            .unwrap()
2669            .await
2670            .unwrap();
2671        assert_eq!(
2672            active_location(&workspace, cx),
2673            (file2.clone(), DisplayPoint::new(DisplayRow(0), 0), 0.)
2674        );
2675        workspace
2676            .update(cx, |w, cx| w.go_forward(w.active_pane().downgrade(), cx))
2677            .unwrap()
2678            .await
2679            .unwrap();
2680        assert_eq!(
2681            active_location(&workspace, cx),
2682            (file3.clone(), DisplayPoint::new(DisplayRow(0), 0), 0.)
2683        );
2684
2685        // Modify file to collapse multiple nav history entries into the same location.
2686        // Ensure we don't visit the same location twice when navigating.
2687        workspace
2688            .update(cx, |_, cx| {
2689                editor1.update(cx, |editor, cx| {
2690                    editor.change_selections(None, cx, |s| {
2691                        s.select_display_ranges([DisplayPoint::new(DisplayRow(15), 0)
2692                            ..DisplayPoint::new(DisplayRow(15), 0)])
2693                    })
2694                });
2695            })
2696            .unwrap();
2697        for _ in 0..5 {
2698            workspace
2699                .update(cx, |_, cx| {
2700                    editor1.update(cx, |editor, cx| {
2701                        editor.change_selections(None, cx, |s| {
2702                            s.select_display_ranges([DisplayPoint::new(DisplayRow(3), 0)
2703                                ..DisplayPoint::new(DisplayRow(3), 0)])
2704                        });
2705                    });
2706                })
2707                .unwrap();
2708
2709            workspace
2710                .update(cx, |_, cx| {
2711                    editor1.update(cx, |editor, cx| {
2712                        editor.change_selections(None, cx, |s| {
2713                            s.select_display_ranges([DisplayPoint::new(DisplayRow(13), 0)
2714                                ..DisplayPoint::new(DisplayRow(13), 0)])
2715                        })
2716                    });
2717                })
2718                .unwrap();
2719        }
2720        workspace
2721            .update(cx, |_, cx| {
2722                editor1.update(cx, |editor, cx| {
2723                    editor.transact(cx, |editor, cx| {
2724                        editor.change_selections(None, cx, |s| {
2725                            s.select_display_ranges([DisplayPoint::new(DisplayRow(2), 0)
2726                                ..DisplayPoint::new(DisplayRow(14), 0)])
2727                        });
2728                        editor.insert("", cx);
2729                    })
2730                });
2731            })
2732            .unwrap();
2733
2734        workspace
2735            .update(cx, |_, cx| {
2736                editor1.update(cx, |editor, cx| {
2737                    editor.change_selections(None, cx, |s| {
2738                        s.select_display_ranges([DisplayPoint::new(DisplayRow(1), 0)
2739                            ..DisplayPoint::new(DisplayRow(1), 0)])
2740                    })
2741                });
2742            })
2743            .unwrap();
2744        workspace
2745            .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx))
2746            .unwrap()
2747            .await
2748            .unwrap();
2749        assert_eq!(
2750            active_location(&workspace, cx),
2751            (file1.clone(), DisplayPoint::new(DisplayRow(2), 0), 0.)
2752        );
2753        workspace
2754            .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx))
2755            .unwrap()
2756            .await
2757            .unwrap();
2758        assert_eq!(
2759            active_location(&workspace, cx),
2760            (file1.clone(), DisplayPoint::new(DisplayRow(3), 0), 0.)
2761        );
2762
2763        fn active_location(
2764            workspace: &WindowHandle<Workspace>,
2765            cx: &mut TestAppContext,
2766        ) -> (ProjectPath, DisplayPoint, f32) {
2767            workspace
2768                .update(cx, |workspace, cx| {
2769                    let item = workspace.active_item(cx).unwrap();
2770                    let editor = item.downcast::<Editor>().unwrap();
2771                    let (selections, scroll_position) = editor.update(cx, |editor, cx| {
2772                        (
2773                            editor.selections.display_ranges(cx),
2774                            editor.scroll_position(cx),
2775                        )
2776                    });
2777                    (
2778                        item.project_path(cx).unwrap(),
2779                        selections[0].start,
2780                        scroll_position.y,
2781                    )
2782                })
2783                .unwrap()
2784        }
2785    }
2786
2787    #[gpui::test]
2788    async fn test_reopening_closed_items(cx: &mut TestAppContext) {
2789        let app_state = init_test(cx);
2790        app_state
2791            .fs
2792            .as_fake()
2793            .insert_tree(
2794                "/root",
2795                json!({
2796                    "a": {
2797                        "file1": "",
2798                        "file2": "",
2799                        "file3": "",
2800                        "file4": "",
2801                    },
2802                }),
2803            )
2804            .await;
2805
2806        let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
2807        project.update(cx, |project, _cx| {
2808            project.languages().add(markdown_language())
2809        });
2810        let workspace = cx.add_window(|cx| Workspace::test_new(project, cx));
2811        let pane = workspace
2812            .read_with(cx, |workspace, _| workspace.active_pane().clone())
2813            .unwrap();
2814
2815        let entries = cx.update(|cx| workspace.root(cx).unwrap().file_project_paths(cx));
2816        let file1 = entries[0].clone();
2817        let file2 = entries[1].clone();
2818        let file3 = entries[2].clone();
2819        let file4 = entries[3].clone();
2820
2821        let file1_item_id = workspace
2822            .update(cx, |w, cx| w.open_path(file1.clone(), None, true, cx))
2823            .unwrap()
2824            .await
2825            .unwrap()
2826            .item_id();
2827        let file2_item_id = workspace
2828            .update(cx, |w, cx| w.open_path(file2.clone(), None, true, cx))
2829            .unwrap()
2830            .await
2831            .unwrap()
2832            .item_id();
2833        let file3_item_id = workspace
2834            .update(cx, |w, cx| w.open_path(file3.clone(), None, true, cx))
2835            .unwrap()
2836            .await
2837            .unwrap()
2838            .item_id();
2839        let file4_item_id = workspace
2840            .update(cx, |w, cx| w.open_path(file4.clone(), None, true, cx))
2841            .unwrap()
2842            .await
2843            .unwrap()
2844            .item_id();
2845        assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
2846
2847        // Close all the pane items in some arbitrary order.
2848        workspace
2849            .update(cx, |_, cx| {
2850                pane.update(cx, |pane, cx| {
2851                    pane.close_item_by_id(file1_item_id, SaveIntent::Close, cx)
2852                })
2853            })
2854            .unwrap()
2855            .await
2856            .unwrap();
2857        assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
2858
2859        workspace
2860            .update(cx, |_, cx| {
2861                pane.update(cx, |pane, cx| {
2862                    pane.close_item_by_id(file4_item_id, SaveIntent::Close, cx)
2863                })
2864            })
2865            .unwrap()
2866            .await
2867            .unwrap();
2868        assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
2869
2870        workspace
2871            .update(cx, |_, cx| {
2872                pane.update(cx, |pane, cx| {
2873                    pane.close_item_by_id(file2_item_id, SaveIntent::Close, cx)
2874                })
2875            })
2876            .unwrap()
2877            .await
2878            .unwrap();
2879        assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
2880        workspace
2881            .update(cx, |_, cx| {
2882                pane.update(cx, |pane, cx| {
2883                    pane.close_item_by_id(file3_item_id, SaveIntent::Close, cx)
2884                })
2885            })
2886            .unwrap()
2887            .await
2888            .unwrap();
2889
2890        assert_eq!(active_path(&workspace, cx), None);
2891
2892        // Reopen all the closed items, ensuring they are reopened in the same order
2893        // in which they were closed.
2894        workspace
2895            .update(cx, Workspace::reopen_closed_item)
2896            .unwrap()
2897            .await
2898            .unwrap();
2899        assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
2900
2901        workspace
2902            .update(cx, Workspace::reopen_closed_item)
2903            .unwrap()
2904            .await
2905            .unwrap();
2906        assert_eq!(active_path(&workspace, cx), Some(file2.clone()));
2907
2908        workspace
2909            .update(cx, Workspace::reopen_closed_item)
2910            .unwrap()
2911            .await
2912            .unwrap();
2913        assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
2914
2915        workspace
2916            .update(cx, Workspace::reopen_closed_item)
2917            .unwrap()
2918            .await
2919            .unwrap();
2920        assert_eq!(active_path(&workspace, cx), Some(file1.clone()));
2921
2922        // Reopening past the last closed item is a no-op.
2923        workspace
2924            .update(cx, Workspace::reopen_closed_item)
2925            .unwrap()
2926            .await
2927            .unwrap();
2928        assert_eq!(active_path(&workspace, cx), Some(file1.clone()));
2929
2930        // Reopening closed items doesn't interfere with navigation history.
2931        workspace
2932            .update(cx, |workspace, cx| {
2933                workspace.go_back(workspace.active_pane().downgrade(), cx)
2934            })
2935            .unwrap()
2936            .await
2937            .unwrap();
2938        assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
2939
2940        workspace
2941            .update(cx, |workspace, cx| {
2942                workspace.go_back(workspace.active_pane().downgrade(), cx)
2943            })
2944            .unwrap()
2945            .await
2946            .unwrap();
2947        assert_eq!(active_path(&workspace, cx), Some(file2.clone()));
2948
2949        workspace
2950            .update(cx, |workspace, cx| {
2951                workspace.go_back(workspace.active_pane().downgrade(), cx)
2952            })
2953            .unwrap()
2954            .await
2955            .unwrap();
2956        assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
2957
2958        workspace
2959            .update(cx, |workspace, cx| {
2960                workspace.go_back(workspace.active_pane().downgrade(), cx)
2961            })
2962            .unwrap()
2963            .await
2964            .unwrap();
2965        assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
2966
2967        workspace
2968            .update(cx, |workspace, cx| {
2969                workspace.go_back(workspace.active_pane().downgrade(), cx)
2970            })
2971            .unwrap()
2972            .await
2973            .unwrap();
2974        assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
2975
2976        workspace
2977            .update(cx, |workspace, cx| {
2978                workspace.go_back(workspace.active_pane().downgrade(), cx)
2979            })
2980            .unwrap()
2981            .await
2982            .unwrap();
2983        assert_eq!(active_path(&workspace, cx), Some(file2.clone()));
2984
2985        workspace
2986            .update(cx, |workspace, cx| {
2987                workspace.go_back(workspace.active_pane().downgrade(), cx)
2988            })
2989            .unwrap()
2990            .await
2991            .unwrap();
2992        assert_eq!(active_path(&workspace, cx), Some(file1.clone()));
2993
2994        workspace
2995            .update(cx, |workspace, cx| {
2996                workspace.go_back(workspace.active_pane().downgrade(), cx)
2997            })
2998            .unwrap()
2999            .await
3000            .unwrap();
3001        assert_eq!(active_path(&workspace, cx), Some(file1.clone()));
3002
3003        fn active_path(
3004            workspace: &WindowHandle<Workspace>,
3005            cx: &TestAppContext,
3006        ) -> Option<ProjectPath> {
3007            workspace
3008                .read_with(cx, |workspace, cx| {
3009                    let item = workspace.active_item(cx)?;
3010                    item.project_path(cx)
3011                })
3012                .unwrap()
3013        }
3014    }
3015
3016    fn init_keymap_test(cx: &mut TestAppContext) -> Arc<AppState> {
3017        cx.update(|cx| {
3018            let app_state = AppState::test(cx);
3019
3020            theme::init(theme::LoadThemes::JustBase, cx);
3021            client::init(&app_state.client, cx);
3022            language::init(cx);
3023            workspace::init(app_state.clone(), cx);
3024            welcome::init(cx);
3025            Project::init_settings(cx);
3026            app_state
3027        })
3028    }
3029
3030    #[gpui::test]
3031    async fn test_base_keymap(cx: &mut gpui::TestAppContext) {
3032        let executor = cx.executor();
3033        let app_state = init_keymap_test(cx);
3034        let project = Project::test(app_state.fs.clone(), [], cx).await;
3035        let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
3036
3037        actions!(test1, [A, B]);
3038        // From the Atom keymap
3039        use workspace::ActivatePreviousPane;
3040        // From the JetBrains keymap
3041        use workspace::ActivatePrevItem;
3042
3043        app_state
3044            .fs
3045            .save(
3046                "/settings.json".as_ref(),
3047                &r#"
3048                {
3049                    "base_keymap": "Atom"
3050                }
3051                "#
3052                .into(),
3053                Default::default(),
3054            )
3055            .await
3056            .unwrap();
3057
3058        app_state
3059            .fs
3060            .save(
3061                "/keymap.json".as_ref(),
3062                &r#"
3063                [
3064                    {
3065                        "bindings": {
3066                            "backspace": "test1::A"
3067                        }
3068                    }
3069                ]
3070                "#
3071                .into(),
3072                Default::default(),
3073            )
3074            .await
3075            .unwrap();
3076        executor.run_until_parked();
3077        cx.update(|cx| {
3078            let settings_rx = watch_config_file(
3079                &executor,
3080                app_state.fs.clone(),
3081                PathBuf::from("/settings.json"),
3082            );
3083            let keymap_rx = watch_config_file(
3084                &executor,
3085                app_state.fs.clone(),
3086                PathBuf::from("/keymap.json"),
3087            );
3088            handle_settings_file_changes(settings_rx, cx);
3089            handle_keymap_file_changes(keymap_rx, cx);
3090        });
3091        workspace
3092            .update(cx, |workspace, cx| {
3093                workspace.register_action(|_, _: &A, _cx| {});
3094                workspace.register_action(|_, _: &B, _cx| {});
3095                workspace.register_action(|_, _: &ActivatePreviousPane, _cx| {});
3096                workspace.register_action(|_, _: &ActivatePrevItem, _cx| {});
3097                cx.notify();
3098            })
3099            .unwrap();
3100        executor.run_until_parked();
3101        // Test loading the keymap base at all
3102        assert_key_bindings_for(
3103            workspace.into(),
3104            cx,
3105            vec![("backspace", &A), ("k", &ActivatePreviousPane)],
3106            line!(),
3107        );
3108
3109        // Test modifying the users keymap, while retaining the base keymap
3110        app_state
3111            .fs
3112            .save(
3113                "/keymap.json".as_ref(),
3114                &r#"
3115                [
3116                    {
3117                        "bindings": {
3118                            "backspace": "test1::B"
3119                        }
3120                    }
3121                ]
3122                "#
3123                .into(),
3124                Default::default(),
3125            )
3126            .await
3127            .unwrap();
3128
3129        executor.run_until_parked();
3130
3131        assert_key_bindings_for(
3132            workspace.into(),
3133            cx,
3134            vec![("backspace", &B), ("k", &ActivatePreviousPane)],
3135            line!(),
3136        );
3137
3138        // Test modifying the base, while retaining the users keymap
3139        app_state
3140            .fs
3141            .save(
3142                "/settings.json".as_ref(),
3143                &r#"
3144                {
3145                    "base_keymap": "JetBrains"
3146                }
3147                "#
3148                .into(),
3149                Default::default(),
3150            )
3151            .await
3152            .unwrap();
3153
3154        executor.run_until_parked();
3155
3156        assert_key_bindings_for(
3157            workspace.into(),
3158            cx,
3159            vec![("backspace", &B), ("[", &ActivatePrevItem)],
3160            line!(),
3161        );
3162    }
3163
3164    #[gpui::test]
3165    async fn test_disabled_keymap_binding(cx: &mut gpui::TestAppContext) {
3166        let executor = cx.executor();
3167        let app_state = init_keymap_test(cx);
3168        let project = Project::test(app_state.fs.clone(), [], cx).await;
3169        let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
3170
3171        actions!(test2, [A, B]);
3172        // From the Atom keymap
3173        use workspace::ActivatePreviousPane;
3174        // From the JetBrains keymap
3175        use pane::ActivatePrevItem;
3176        workspace
3177            .update(cx, |workspace, _| {
3178                workspace
3179                    .register_action(|_, _: &A, _| {})
3180                    .register_action(|_, _: &B, _| {});
3181            })
3182            .unwrap();
3183        app_state
3184            .fs
3185            .save(
3186                "/settings.json".as_ref(),
3187                &r#"
3188                {
3189                    "base_keymap": "Atom"
3190                }
3191                "#
3192                .into(),
3193                Default::default(),
3194            )
3195            .await
3196            .unwrap();
3197        app_state
3198            .fs
3199            .save(
3200                "/keymap.json".as_ref(),
3201                &r#"
3202                [
3203                    {
3204                        "bindings": {
3205                            "backspace": "test2::A"
3206                        }
3207                    }
3208                ]
3209                "#
3210                .into(),
3211                Default::default(),
3212            )
3213            .await
3214            .unwrap();
3215
3216        cx.update(|cx| {
3217            let settings_rx = watch_config_file(
3218                &executor,
3219                app_state.fs.clone(),
3220                PathBuf::from("/settings.json"),
3221            );
3222            let keymap_rx = watch_config_file(
3223                &executor,
3224                app_state.fs.clone(),
3225                PathBuf::from("/keymap.json"),
3226            );
3227
3228            handle_settings_file_changes(settings_rx, cx);
3229            handle_keymap_file_changes(keymap_rx, cx);
3230        });
3231
3232        cx.background_executor.run_until_parked();
3233
3234        cx.background_executor.run_until_parked();
3235        // Test loading the keymap base at all
3236        assert_key_bindings_for(
3237            workspace.into(),
3238            cx,
3239            vec![("backspace", &A), ("k", &ActivatePreviousPane)],
3240            line!(),
3241        );
3242
3243        // Test disabling the key binding for the base keymap
3244        app_state
3245            .fs
3246            .save(
3247                "/keymap.json".as_ref(),
3248                &r#"
3249                [
3250                    {
3251                        "bindings": {
3252                            "backspace": null
3253                        }
3254                    }
3255                ]
3256                "#
3257                .into(),
3258                Default::default(),
3259            )
3260            .await
3261            .unwrap();
3262
3263        cx.background_executor.run_until_parked();
3264
3265        assert_key_bindings_for(
3266            workspace.into(),
3267            cx,
3268            vec![("k", &ActivatePreviousPane)],
3269            line!(),
3270        );
3271
3272        // Test modifying the base, while retaining the users keymap
3273        app_state
3274            .fs
3275            .save(
3276                "/settings.json".as_ref(),
3277                &r#"
3278                {
3279                    "base_keymap": "JetBrains"
3280                }
3281                "#
3282                .into(),
3283                Default::default(),
3284            )
3285            .await
3286            .unwrap();
3287
3288        cx.background_executor.run_until_parked();
3289
3290        assert_key_bindings_for(
3291            workspace.into(),
3292            cx,
3293            vec![("[", &ActivatePrevItem)],
3294            line!(),
3295        );
3296    }
3297
3298    #[gpui::test]
3299    fn test_bundled_settings_and_themes(cx: &mut AppContext) {
3300        cx.text_system()
3301            .add_fonts(vec![
3302                Assets
3303                    .load("fonts/plex-mono/ZedPlexMono-Regular.ttf")
3304                    .unwrap()
3305                    .unwrap(),
3306                Assets
3307                    .load("fonts/plex-sans/ZedPlexSans-Regular.ttf")
3308                    .unwrap()
3309                    .unwrap(),
3310            ])
3311            .unwrap();
3312        let themes = ThemeRegistry::default();
3313        settings::init(cx);
3314        theme::init(theme::LoadThemes::JustBase, cx);
3315
3316        let mut has_default_theme = false;
3317        for theme_name in themes.list(false).into_iter().map(|meta| meta.name) {
3318            let theme = themes.get(&theme_name).unwrap();
3319            assert_eq!(theme.name, theme_name);
3320            if theme.name == ThemeSettings::get(None, cx).active_theme.name {
3321                has_default_theme = true;
3322            }
3323        }
3324        assert!(has_default_theme);
3325    }
3326
3327    #[gpui::test]
3328    async fn test_bundled_languages(cx: &mut TestAppContext) {
3329        env_logger::builder().is_test(true).try_init().ok();
3330        let settings = cx.update(|cx| SettingsStore::test(cx));
3331        cx.set_global(settings);
3332        let languages = LanguageRegistry::test(cx.executor());
3333        let languages = Arc::new(languages);
3334        let node_runtime = node_runtime::FakeNodeRuntime::new();
3335        cx.update(|cx| {
3336            languages::init(languages.clone(), node_runtime, cx);
3337        });
3338        for name in languages.language_names() {
3339            languages
3340                .language_for_name(&name)
3341                .await
3342                .with_context(|| format!("language name {name}"))
3343                .unwrap();
3344        }
3345        cx.run_until_parked();
3346    }
3347
3348    #[gpui::test]
3349    async fn test_spawn_terminal_task_real_fs(cx: &mut TestAppContext) {
3350        let mut app_state = cx.update(|cx| AppState::test(cx));
3351        let state = Arc::get_mut(&mut app_state).unwrap();
3352        state.fs = Arc::new(fs::RealFs::default());
3353        let app_state = init_test_with_state(cx, app_state);
3354
3355        cx.executor().allow_parking();
3356        let project_root = util::test::temp_tree(json!({
3357            "sample.txt": ""
3358        }));
3359
3360        let spawn_in_terminal = SpawnInTerminal {
3361            command: "echo SAMPLE-OUTPUT".to_string(),
3362            cwd: None,
3363            env: HashMap::default(),
3364            id: task::TaskId(String::from("sample-id")),
3365            full_label: String::from("sample-full_label"),
3366            label: String::from("sample-label"),
3367            args: vec![],
3368            command_label: String::from("sample-command_label"),
3369            use_new_terminal: false,
3370            allow_concurrent_runs: false,
3371            reveal: RevealStrategy::Always,
3372            hide: HideStrategy::Never,
3373            shell: Shell::System,
3374        };
3375        let project = Project::test(app_state.fs.clone(), [project_root.path()], cx).await;
3376        let window = cx.add_window(|cx| Workspace::test_new(project, cx));
3377        cx.run_until_parked();
3378        cx.update(|cx| {
3379            window
3380                .update(cx, |_workspace, cx| {
3381                    cx.emit(workspace::Event::SpawnTask(Box::new(spawn_in_terminal)));
3382                })
3383                .unwrap();
3384        });
3385        cx.run_until_parked();
3386
3387        run_until(|| {
3388            cx.update(|cx| {
3389                window
3390                    .read_with(cx, |workspace, cx| {
3391                        let terminal = workspace
3392                            .project()
3393                            .read(cx)
3394                            .local_terminal_handles()
3395                            .first()
3396                            .unwrap()
3397                            .upgrade()
3398                            .unwrap()
3399                            .read(cx);
3400                        terminal
3401                            .last_n_non_empty_lines(99)
3402                            .join("")
3403                            .contains("SAMPLE-OUTPUT")
3404                    })
3405                    .unwrap()
3406            })
3407        })
3408        .await;
3409    }
3410
3411    async fn run_until(predicate: impl Fn() -> bool) {
3412        let timer = async { smol::Timer::after(std::time::Duration::from_secs(3)).await };
3413
3414        use futures::FutureExt as _;
3415        use smol::future::FutureExt as _;
3416
3417        async {
3418            loop {
3419                if predicate() {
3420                    return Ok(());
3421                }
3422                smol::Timer::after(std::time::Duration::from_millis(10)).await;
3423            }
3424        }
3425        .race(timer.map(|_| Err(anyhow!("condition timed out"))))
3426        .await
3427        .unwrap();
3428    }
3429
3430    pub(crate) fn init_test(cx: &mut TestAppContext) -> Arc<AppState> {
3431        init_test_with_state(cx, cx.update(|cx| AppState::test(cx)))
3432    }
3433
3434    fn init_test_with_state(
3435        cx: &mut TestAppContext,
3436        mut app_state: Arc<AppState>,
3437    ) -> Arc<AppState> {
3438        cx.update(move |cx| {
3439            env_logger::builder().is_test(true).try_init().ok();
3440
3441            let state = Arc::get_mut(&mut app_state).unwrap();
3442            state.build_window_options = build_window_options;
3443
3444            app_state.languages.add(markdown_language());
3445
3446            theme::init(theme::LoadThemes::JustBase, cx);
3447            audio::init((), cx);
3448            channel::init(&app_state.client, app_state.user_store.clone(), cx);
3449            call::init(app_state.client.clone(), app_state.user_store.clone(), cx);
3450            notifications::init(app_state.client.clone(), app_state.user_store.clone(), cx);
3451            workspace::init(app_state.clone(), cx);
3452            Project::init_settings(cx);
3453            release_channel::init(SemanticVersion::default(), cx);
3454            command_palette::init(cx);
3455            language::init(cx);
3456            editor::init(cx);
3457            collab_ui::init(&app_state, cx);
3458            project_panel::init((), cx);
3459            outline_panel::init((), cx);
3460            terminal_view::init(cx);
3461            language_model::init(app_state.client.clone(), cx);
3462            assistant::init(app_state.fs.clone(), app_state.client.clone(), cx);
3463            repl::init(app_state.fs.clone(), cx);
3464            tasks_ui::init(cx);
3465            initialize_workspace(app_state.clone(), cx);
3466            app_state
3467        })
3468    }
3469
3470    fn rust_lang() -> Arc<language::Language> {
3471        Arc::new(language::Language::new(
3472            language::LanguageConfig {
3473                name: "Rust".into(),
3474                matcher: LanguageMatcher {
3475                    path_suffixes: vec!["rs".to_string()],
3476                    ..Default::default()
3477                },
3478                ..Default::default()
3479            },
3480            Some(tree_sitter_rust::language()),
3481        ))
3482    }
3483
3484    fn markdown_language() -> Arc<language::Language> {
3485        Arc::new(language::Language::new(
3486            language::LanguageConfig {
3487                name: "Markdown".into(),
3488                matcher: LanguageMatcher {
3489                    path_suffixes: vec!["md".to_string()],
3490                    ..Default::default()
3491                },
3492                ..Default::default()
3493            },
3494            Some(tree_sitter_md::language()),
3495        ))
3496    }
3497
3498    #[track_caller]
3499    fn assert_key_bindings_for(
3500        window: AnyWindowHandle,
3501        cx: &TestAppContext,
3502        actions: Vec<(&'static str, &dyn Action)>,
3503        line: u32,
3504    ) {
3505        let available_actions = cx
3506            .update(|cx| window.update(cx, |_, cx| cx.available_actions()))
3507            .unwrap();
3508        for (key, action) in actions {
3509            let bindings = cx
3510                .update(|cx| window.update(cx, |_, cx| cx.bindings_for_action(action)))
3511                .unwrap();
3512            // assert that...
3513            assert!(
3514                available_actions.iter().any(|bound_action| {
3515                    // actions match...
3516                    bound_action.partial_eq(action)
3517                }),
3518                "On {} Failed to find {}",
3519                line,
3520                action.name(),
3521            );
3522            assert!(
3523                // and key strokes contain the given key
3524                bindings
3525                    .into_iter()
3526                    .any(|binding| binding.keystrokes().iter().any(|k| k.key == key)),
3527                "On {} Failed to find {} with key binding {}",
3528                line,
3529                action.name(),
3530                key
3531            );
3532        }
3533    }
3534}
3535
3536async fn register_zed_scheme(cx: &AsyncAppContext) -> anyhow::Result<()> {
3537    cx.update(|cx| cx.register_url_scheme(ZED_URL_SCHEME))?
3538        .await
3539}