zed.rs

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