zed.rs

   1mod feedback;
   2pub mod languages;
   3pub mod menus;
   4pub mod paths;
   5#[cfg(any(test, feature = "test-support"))]
   6pub mod test;
   7
   8use anyhow::{anyhow, Context, Result};
   9use assets::Assets;
  10use breadcrumbs::Breadcrumbs;
  11pub use client;
  12use collab_ui::{CollabTitlebarItem, ToggleCollaborationMenu};
  13use collections::VecDeque;
  14pub use editor;
  15use editor::{Editor, MultiBuffer};
  16
  17use gpui::{
  18    actions,
  19    geometry::vector::vec2f,
  20    impl_actions,
  21    platform::{WindowBounds, WindowOptions},
  22    AssetSource, AsyncAppContext, TitlebarOptions, ViewContext, WindowKind,
  23};
  24use language::Rope;
  25pub use lsp;
  26pub use project;
  27use project_panel::ProjectPanel;
  28use search::{BufferSearchBar, ProjectSearchBar};
  29use serde::Deserialize;
  30use serde_json::to_string_pretty;
  31use settings::{keymap_file_json_schema, settings_file_json_schema, Settings};
  32use std::{env, path::Path, str, sync::Arc};
  33use util::ResultExt;
  34pub use workspace;
  35use workspace::{sidebar::SidebarSide, AppState, Workspace};
  36
  37#[derive(Deserialize, Clone, PartialEq)]
  38struct OpenBrowser {
  39    url: Arc<str>,
  40}
  41
  42impl_actions!(zed, [OpenBrowser]);
  43
  44actions!(
  45    zed,
  46    [
  47        About,
  48        Hide,
  49        HideOthers,
  50        ShowAll,
  51        Minimize,
  52        Zoom,
  53        ToggleFullScreen,
  54        Quit,
  55        DebugElements,
  56        OpenSettings,
  57        OpenLog,
  58        OpenTelemetryLog,
  59        OpenKeymap,
  60        OpenDefaultSettings,
  61        OpenDefaultKeymap,
  62        IncreaseBufferFontSize,
  63        DecreaseBufferFontSize,
  64        ResetBufferFontSize,
  65        InstallCommandLineInterface,
  66        ResetDatabase,
  67    ]
  68);
  69
  70const MIN_FONT_SIZE: f32 = 6.0;
  71
  72pub fn init(app_state: &Arc<AppState>, cx: &mut gpui::MutableAppContext) {
  73    cx.add_action(about);
  74    cx.add_global_action(|_: &Hide, cx: &mut gpui::MutableAppContext| {
  75        cx.platform().hide();
  76    });
  77    cx.add_global_action(|_: &HideOthers, cx: &mut gpui::MutableAppContext| {
  78        cx.platform().hide_other_apps();
  79    });
  80    cx.add_global_action(|_: &ShowAll, cx: &mut gpui::MutableAppContext| {
  81        cx.platform().unhide_other_apps();
  82    });
  83    cx.add_action(
  84        |_: &mut Workspace, _: &Minimize, cx: &mut ViewContext<Workspace>| {
  85            cx.minimize_window();
  86        },
  87    );
  88    cx.add_action(
  89        |_: &mut Workspace, _: &Zoom, cx: &mut ViewContext<Workspace>| {
  90            cx.zoom_window();
  91        },
  92    );
  93    cx.add_action(
  94        |_: &mut Workspace, _: &ToggleFullScreen, cx: &mut ViewContext<Workspace>| {
  95            cx.toggle_full_screen();
  96        },
  97    );
  98    cx.add_action(
  99        |workspace: &mut Workspace,
 100         _: &ToggleCollaborationMenu,
 101         cx: &mut ViewContext<Workspace>| {
 102            if let Some(item) = workspace
 103                .titlebar_item()
 104                .and_then(|item| item.downcast::<CollabTitlebarItem>())
 105            {
 106                cx.as_mut().defer(move |cx| {
 107                    item.update(cx, |item, cx| {
 108                        item.toggle_contacts_popover(&Default::default(), cx);
 109                    });
 110                });
 111            }
 112        },
 113    );
 114    cx.add_global_action(quit);
 115    cx.add_global_action(move |action: &OpenBrowser, cx| cx.platform().open_url(&action.url));
 116    cx.add_global_action(move |_: &IncreaseBufferFontSize, cx| {
 117        cx.update_global::<Settings, _, _>(|settings, cx| {
 118            settings.buffer_font_size = (settings.buffer_font_size + 1.0).max(MIN_FONT_SIZE);
 119            if let Some(terminal_font_size) = settings.terminal_overrides.font_size.as_mut() {
 120                *terminal_font_size = (*terminal_font_size + 1.0).max(MIN_FONT_SIZE);
 121            }
 122            cx.refresh_windows();
 123        });
 124    });
 125    cx.add_global_action(move |_: &DecreaseBufferFontSize, cx| {
 126        cx.update_global::<Settings, _, _>(|settings, cx| {
 127            settings.buffer_font_size = (settings.buffer_font_size - 1.0).max(MIN_FONT_SIZE);
 128            if let Some(terminal_font_size) = settings.terminal_overrides.font_size.as_mut() {
 129                *terminal_font_size = (*terminal_font_size - 1.0).max(MIN_FONT_SIZE);
 130            }
 131            cx.refresh_windows();
 132        });
 133    });
 134    cx.add_global_action(move |_: &ResetBufferFontSize, cx| {
 135        cx.update_global::<Settings, _, _>(|settings, cx| {
 136            settings.buffer_font_size = settings.default_buffer_font_size;
 137            settings.terminal_overrides.font_size = settings.terminal_defaults.font_size;
 138            cx.refresh_windows();
 139        });
 140    });
 141    cx.add_global_action(move |_: &InstallCommandLineInterface, cx| {
 142        cx.spawn(|cx| async move { install_cli(&cx).await.context("error creating CLI symlink") })
 143            .detach_and_log_err(cx);
 144    });
 145    cx.add_action({
 146        let app_state = app_state.clone();
 147        move |_: &mut Workspace, _: &OpenSettings, cx: &mut ViewContext<Workspace>| {
 148            open_config_file(&paths::SETTINGS, app_state.clone(), cx, || {
 149                str::from_utf8(
 150                    Assets
 151                        .load("settings/initial_user_settings.json")
 152                        .unwrap()
 153                        .as_ref(),
 154                )
 155                .unwrap()
 156                .into()
 157            });
 158        }
 159    });
 160    cx.add_action({
 161        let app_state = app_state.clone();
 162        move |workspace: &mut Workspace, _: &OpenLog, cx: &mut ViewContext<Workspace>| {
 163            open_log_file(workspace, app_state.clone(), cx);
 164        }
 165    });
 166    cx.add_action({
 167        let app_state = app_state.clone();
 168        move |workspace: &mut Workspace, _: &OpenTelemetryLog, cx: &mut ViewContext<Workspace>| {
 169            open_telemetry_log_file(workspace, app_state.clone(), cx);
 170        }
 171    });
 172    cx.add_action({
 173        let app_state = app_state.clone();
 174        move |_: &mut Workspace, _: &OpenKeymap, cx: &mut ViewContext<Workspace>| {
 175            open_config_file(&paths::KEYMAP, app_state.clone(), cx, Default::default);
 176        }
 177    });
 178    cx.add_action({
 179        let app_state = app_state.clone();
 180        move |workspace: &mut Workspace, _: &OpenDefaultKeymap, cx: &mut ViewContext<Workspace>| {
 181            open_bundled_config_file(
 182                workspace,
 183                app_state.clone(),
 184                "keymaps/default.json",
 185                "Default Key Bindings",
 186                cx,
 187            );
 188        }
 189    });
 190    cx.add_action({
 191        let app_state = app_state.clone();
 192        move |workspace: &mut Workspace,
 193              _: &OpenDefaultSettings,
 194              cx: &mut ViewContext<Workspace>| {
 195            open_bundled_config_file(
 196                workspace,
 197                app_state.clone(),
 198                "settings/default.json",
 199                "Default Settings",
 200                cx,
 201            );
 202        }
 203    });
 204    cx.add_action(
 205        |workspace: &mut Workspace, _: &DebugElements, cx: &mut ViewContext<Workspace>| {
 206            let content = to_string_pretty(&cx.debug_elements()).unwrap();
 207            let project = workspace.project().clone();
 208            let json_language = project.read(cx).languages().get_language("JSON").unwrap();
 209            if project.read(cx).is_remote() {
 210                cx.propagate_action();
 211            } else if let Some(buffer) = project
 212                .update(cx, |project, cx| {
 213                    project.create_buffer(&content, Some(json_language), cx)
 214                })
 215                .log_err()
 216            {
 217                workspace.add_item(
 218                    Box::new(
 219                        cx.add_view(|cx| Editor::for_buffer(buffer, Some(project.clone()), cx)),
 220                    ),
 221                    cx,
 222                );
 223            }
 224        },
 225    );
 226    cx.add_action(
 227        |workspace: &mut Workspace,
 228         _: &project_panel::ToggleFocus,
 229         cx: &mut ViewContext<Workspace>| {
 230            workspace.toggle_sidebar_item_focus(SidebarSide::Left, 0, cx);
 231        },
 232    );
 233
 234    activity_indicator::init(cx);
 235    call::init(app_state.client.clone(), app_state.user_store.clone(), cx);
 236    settings::KeymapFileContent::load_defaults(cx);
 237}
 238
 239pub fn initialize_workspace(
 240    workspace: &mut Workspace,
 241    app_state: &Arc<AppState>,
 242    cx: &mut ViewContext<Workspace>,
 243) {
 244    let workspace_handle = cx.handle();
 245    cx.subscribe(&workspace_handle, {
 246        move |_, _, event, cx| {
 247            if let workspace::Event::PaneAdded(pane) = event {
 248                pane.update(cx, |pane, cx| {
 249                    pane.toolbar().update(cx, |toolbar, cx| {
 250                        let breadcrumbs = cx.add_view(|_| Breadcrumbs::new());
 251                        toolbar.add_item(breadcrumbs, cx);
 252                        let buffer_search_bar = cx.add_view(BufferSearchBar::new);
 253                        toolbar.add_item(buffer_search_bar, cx);
 254                        let project_search_bar = cx.add_view(|_| ProjectSearchBar::new());
 255                        toolbar.add_item(project_search_bar, cx);
 256                    })
 257                });
 258            }
 259        }
 260    })
 261    .detach();
 262
 263    cx.emit(workspace::Event::PaneAdded(workspace.active_pane().clone()));
 264    cx.emit(workspace::Event::PaneAdded(workspace.dock_pane().clone()));
 265
 266    let settings = cx.global::<Settings>();
 267
 268    let theme_names = app_state
 269        .themes
 270        .list(
 271            settings.staff_mode,
 272            settings.experiments.experimental_themes,
 273        )
 274        .map(|meta| meta.name)
 275        .collect();
 276    let language_names = &languages::LANGUAGE_NAMES;
 277
 278    workspace.project().update(cx, |project, cx| {
 279        let action_names = cx.all_action_names().collect::<Vec<_>>();
 280        project.set_language_server_settings(serde_json::json!({
 281            "json": {
 282                "format": {
 283                    "enable": true,
 284                },
 285                "schemas": [
 286                    {
 287                        "fileMatch": [schema_file_match(&*paths::SETTINGS)],
 288                        "schema": settings_file_json_schema(theme_names, language_names),
 289                    },
 290                    {
 291                        "fileMatch": [schema_file_match(&*paths::KEYMAP)],
 292                        "schema": keymap_file_json_schema(&action_names),
 293                    }
 294                ]
 295            }
 296        }));
 297    });
 298
 299    let collab_titlebar_item =
 300        cx.add_view(|cx| CollabTitlebarItem::new(&workspace_handle, &app_state.user_store, cx));
 301    workspace.set_titlebar_item(collab_titlebar_item, cx);
 302
 303    let project_panel = ProjectPanel::new(workspace.project().clone(), cx);
 304    workspace.left_sidebar().update(cx, |sidebar, cx| {
 305        sidebar.add_item(
 306            "icons/folder_tree_16.svg",
 307            "Project Panel".to_string(),
 308            project_panel,
 309            cx,
 310        )
 311    });
 312
 313    let diagnostic_summary =
 314        cx.add_view(|cx| diagnostics::items::DiagnosticIndicator::new(workspace.project(), cx));
 315    let activity_indicator =
 316        activity_indicator::ActivityIndicator::new(workspace, app_state.languages.clone(), cx);
 317    let cursor_position = cx.add_view(|_| editor::items::CursorPosition::new());
 318    let feedback_link = cx.add_view(|_| feedback::FeedbackLink);
 319    workspace.status_bar().update(cx, |status_bar, cx| {
 320        status_bar.add_left_item(diagnostic_summary, cx);
 321        status_bar.add_left_item(activity_indicator, cx);
 322        status_bar.add_right_item(cursor_position, cx);
 323        status_bar.add_right_item(feedback_link, cx);
 324    });
 325
 326    auto_update::notify_of_any_new_update(cx.weak_handle(), cx);
 327
 328    cx.on_window_should_close(|workspace, cx| {
 329        if let Some(task) = workspace.close(&Default::default(), cx) {
 330            task.detach_and_log_err(cx);
 331        }
 332        false
 333    });
 334}
 335
 336pub fn build_window_options() -> WindowOptions<'static> {
 337    WindowOptions {
 338        bounds: WindowBounds::Maximized,
 339        titlebar: Some(TitlebarOptions {
 340            title: None,
 341            appears_transparent: true,
 342            traffic_light_position: Some(vec2f(8., 8.)),
 343        }),
 344        center: false,
 345        kind: WindowKind::Normal,
 346        is_movable: true,
 347    }
 348}
 349
 350fn quit(_: &Quit, cx: &mut gpui::MutableAppContext) {
 351    let mut workspaces = cx
 352        .window_ids()
 353        .filter_map(|window_id| cx.root_view::<Workspace>(window_id))
 354        .collect::<Vec<_>>();
 355
 356    // If multiple windows have unsaved changes, and need a save prompt,
 357    // prompt in the active window before switching to a different window.
 358    workspaces.sort_by_key(|workspace| !cx.window_is_active(workspace.window_id()));
 359
 360    cx.spawn(|mut cx| async move {
 361        // If the user cancels any save prompt, then keep the app open.
 362        for workspace in workspaces {
 363            if !workspace
 364                .update(&mut cx, |workspace, cx| {
 365                    workspace.prepare_to_close(true, cx)
 366                })
 367                .await?
 368            {
 369                return Ok(());
 370            }
 371        }
 372        cx.platform().quit();
 373        anyhow::Ok(())
 374    })
 375    .detach_and_log_err(cx);
 376}
 377
 378fn about(_: &mut Workspace, _: &About, cx: &mut gpui::ViewContext<Workspace>) {
 379    cx.prompt(
 380        gpui::PromptLevel::Info,
 381        &format!("Zed {}", env!("CARGO_PKG_VERSION")),
 382        &["OK"],
 383    );
 384}
 385
 386async fn install_cli(cx: &AsyncAppContext) -> Result<()> {
 387    let cli_path = cx.platform().path_for_auxiliary_executable("cli")?;
 388    let link_path = Path::new("/usr/local/bin/zed");
 389    let bin_dir_path = link_path.parent().unwrap();
 390
 391    // Don't re-create symlink if it points to the same CLI binary.
 392    if smol::fs::read_link(link_path).await.ok().as_ref() == Some(&cli_path) {
 393        return Ok(());
 394    }
 395
 396    // If the symlink is not there or is outdated, first try replacing it
 397    // without escalating.
 398    smol::fs::remove_file(link_path).await.log_err();
 399    if smol::fs::unix::symlink(&cli_path, link_path)
 400        .await
 401        .log_err()
 402        .is_some()
 403    {
 404        return Ok(());
 405    }
 406
 407    // The symlink could not be created, so use osascript with admin privileges
 408    // to create it.
 409    let status = smol::process::Command::new("osascript")
 410        .args([
 411            "-e",
 412            &format!(
 413                "do shell script \" \
 414                    mkdir -p \'{}\' && \
 415                    ln -sf \'{}\' \'{}\' \
 416                \" with administrator privileges",
 417                bin_dir_path.to_string_lossy(),
 418                cli_path.to_string_lossy(),
 419                link_path.to_string_lossy(),
 420            ),
 421        ])
 422        .stdout(smol::process::Stdio::inherit())
 423        .stderr(smol::process::Stdio::inherit())
 424        .output()
 425        .await?
 426        .status;
 427    if status.success() {
 428        Ok(())
 429    } else {
 430        Err(anyhow!("error running osascript"))
 431    }
 432}
 433
 434fn open_config_file(
 435    path: &'static Path,
 436    app_state: Arc<AppState>,
 437    cx: &mut ViewContext<Workspace>,
 438    default_content: impl 'static + Send + FnOnce() -> Rope,
 439) {
 440    cx.spawn(|workspace, mut cx| async move {
 441        let fs = &app_state.fs;
 442        if !fs.is_file(path).await {
 443            fs.create_file(path, Default::default()).await?;
 444            fs.save(path, &default_content(), Default::default())
 445                .await?;
 446        }
 447
 448        workspace
 449            .update(&mut cx, |workspace, cx| {
 450                workspace.with_local_workspace(cx, app_state, |workspace, cx| {
 451                    workspace.open_paths(vec![path.to_path_buf()], false, cx)
 452                })
 453            })
 454            .await;
 455        Ok::<_, anyhow::Error>(())
 456    })
 457    .detach_and_log_err(cx)
 458}
 459
 460fn open_log_file(
 461    workspace: &mut Workspace,
 462    app_state: Arc<AppState>,
 463    cx: &mut ViewContext<Workspace>,
 464) {
 465    const MAX_LINES: usize = 1000;
 466
 467    workspace.with_local_workspace(cx, app_state.clone(), |_, cx| {
 468        cx.spawn_weak(|workspace, mut cx| async move {
 469            let (old_log, new_log) = futures::join!(
 470                app_state.fs.load(&paths::OLD_LOG),
 471                app_state.fs.load(&paths::LOG)
 472            );
 473
 474            if let Some(workspace) = workspace.upgrade(&cx) {
 475                let mut lines = VecDeque::with_capacity(MAX_LINES);
 476                for line in old_log
 477                    .iter()
 478                    .flat_map(|log| log.lines())
 479                    .chain(new_log.iter().flat_map(|log| log.lines()))
 480                {
 481                    if lines.len() == MAX_LINES {
 482                        lines.pop_front();
 483                    }
 484                    lines.push_back(line);
 485                }
 486                let log = lines
 487                    .into_iter()
 488                    .flat_map(|line| [line, "\n"])
 489                    .collect::<String>();
 490
 491                workspace.update(&mut cx, |workspace, cx| {
 492                    let project = workspace.project().clone();
 493                    let buffer = project
 494                        .update(cx, |project, cx| project.create_buffer("", None, cx))
 495                        .expect("creating buffers on a local workspace always succeeds");
 496                    buffer.update(cx, |buffer, cx| buffer.edit([(0..0, log)], None, cx));
 497
 498                    let buffer = cx.add_model(|cx| {
 499                        MultiBuffer::singleton(buffer, cx).with_title("Log".into())
 500                    });
 501                    workspace.add_item(
 502                        Box::new(
 503                            cx.add_view(|cx| Editor::for_multibuffer(buffer, Some(project), cx)),
 504                        ),
 505                        cx,
 506                    );
 507                });
 508            }
 509        })
 510        .detach();
 511    });
 512}
 513
 514fn open_telemetry_log_file(
 515    workspace: &mut Workspace,
 516    app_state: Arc<AppState>,
 517    cx: &mut ViewContext<Workspace>,
 518) {
 519    workspace.with_local_workspace(cx, app_state.clone(), |_, cx| {
 520        cx.spawn_weak(|workspace, mut cx| async move {
 521            let workspace = workspace.upgrade(&cx)?;
 522            let path = app_state.client.telemetry_log_file_path()?;
 523            let log = app_state.fs.load(&path).await.log_err()?;
 524
 525            const MAX_TELEMETRY_LOG_LEN: usize = 5 * 1024 * 1024;
 526            let mut start_offset = log.len().saturating_sub(MAX_TELEMETRY_LOG_LEN);
 527            if let Some(newline_offset) = log[start_offset..].find('\n') {
 528                start_offset += newline_offset + 1;
 529            }
 530            let log_suffix = &log[start_offset..];
 531
 532            workspace.update(&mut cx, |workspace, cx| {
 533                let project = workspace.project().clone();
 534                let buffer = project
 535                    .update(cx, |project, cx| project.create_buffer("", None, cx))
 536                    .expect("creating buffers on a local workspace always succeeds");
 537                buffer.update(cx, |buffer, cx| {
 538                    buffer.set_language(app_state.languages.get_language("JSON"), cx);
 539                    buffer.edit(
 540                        [(
 541                            0..0,
 542                            concat!(
 543                                "// Zed collects anonymous usage data to help us understand how people are using the app.\n",
 544                                "// After the beta release, we'll provide the ability to opt out of this telemetry.\n",
 545                                "// Here is the data that has been reported for the current session:\n",
 546                                "\n"
 547                            ),
 548                        )],
 549                        None,
 550                        cx,
 551                    );
 552                    buffer.edit([(buffer.len()..buffer.len(), log_suffix)], None, cx);
 553                });
 554
 555                let buffer = cx.add_model(|cx| {
 556                    MultiBuffer::singleton(buffer, cx).with_title("Telemetry Log".into())
 557                });
 558                workspace.add_item(
 559                    Box::new(cx.add_view(|cx| Editor::for_multibuffer(buffer, Some(project), cx))),
 560                    cx,
 561                );
 562            });
 563
 564            Some(())
 565        })
 566        .detach();
 567    });
 568}
 569
 570fn open_bundled_config_file(
 571    workspace: &mut Workspace,
 572    app_state: Arc<AppState>,
 573    asset_path: &'static str,
 574    title: &str,
 575    cx: &mut ViewContext<Workspace>,
 576) {
 577    workspace.with_local_workspace(cx, app_state, |workspace, cx| {
 578        let project = workspace.project().clone();
 579        let buffer = project.update(cx, |project, cx| {
 580            let text = Assets::get(asset_path).unwrap().data;
 581            let text = str::from_utf8(text.as_ref()).unwrap();
 582            project
 583                .create_buffer(text, project.languages().get_language("JSON"), cx)
 584                .expect("creating buffers on a local workspace always succeeds")
 585        });
 586        let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx).with_title(title.into()));
 587        workspace.add_item(
 588            Box::new(cx.add_view(|cx| Editor::for_multibuffer(buffer, Some(project.clone()), cx))),
 589            cx,
 590        );
 591    });
 592}
 593
 594fn schema_file_match(path: &Path) -> &Path {
 595    path.strip_prefix(path.parent().unwrap().parent().unwrap())
 596        .unwrap()
 597}
 598
 599#[cfg(test)]
 600mod tests {
 601    use super::*;
 602    use assets::Assets;
 603    use editor::{Autoscroll, DisplayPoint, Editor};
 604    use gpui::{
 605        executor::Deterministic, AssetSource, MutableAppContext, TestAppContext, ViewHandle,
 606    };
 607    use project::{Project, ProjectPath};
 608    use serde_json::json;
 609    use std::{
 610        collections::HashSet,
 611        path::{Path, PathBuf},
 612    };
 613    use theme::ThemeRegistry;
 614    use workspace::{
 615        open_paths, pane, Item, ItemHandle, NewFile, Pane, SplitDirection, WorkspaceHandle,
 616    };
 617
 618    #[gpui::test]
 619    async fn test_open_paths_action(cx: &mut TestAppContext) {
 620        let app_state = init(cx);
 621        app_state
 622            .fs
 623            .as_fake()
 624            .insert_tree(
 625                "/root",
 626                json!({
 627                    "a": {
 628                        "aa": null,
 629                        "ab": null,
 630                    },
 631                    "b": {
 632                        "ba": null,
 633                        "bb": null,
 634                    },
 635                    "c": {
 636                        "ca": null,
 637                        "cb": null,
 638                    },
 639                }),
 640            )
 641            .await;
 642
 643        cx.update(|cx| {
 644            open_paths(
 645                &[PathBuf::from("/root/a"), PathBuf::from("/root/b")],
 646                &app_state,
 647                cx,
 648            )
 649        })
 650        .await;
 651        assert_eq!(cx.window_ids().len(), 1);
 652
 653        cx.update(|cx| open_paths(&[PathBuf::from("/root/a")], &app_state, cx))
 654            .await;
 655        assert_eq!(cx.window_ids().len(), 1);
 656        let workspace_1 = cx.root_view::<Workspace>(cx.window_ids()[0]).unwrap();
 657        workspace_1.update(cx, |workspace, cx| {
 658            assert_eq!(workspace.worktrees(cx).count(), 2);
 659            assert!(workspace.left_sidebar().read(cx).is_open());
 660            assert!(workspace.active_pane().is_focused(cx));
 661        });
 662
 663        cx.update(|cx| {
 664            open_paths(
 665                &[PathBuf::from("/root/b"), PathBuf::from("/root/c")],
 666                &app_state,
 667                cx,
 668            )
 669        })
 670        .await;
 671        assert_eq!(cx.window_ids().len(), 2);
 672    }
 673
 674    #[gpui::test]
 675    async fn test_window_edit_state(executor: Arc<Deterministic>, cx: &mut TestAppContext) {
 676        let app_state = init(cx);
 677        app_state
 678            .fs
 679            .as_fake()
 680            .insert_tree("/root", json!({"a": "hey"}))
 681            .await;
 682
 683        cx.update(|cx| open_paths(&[PathBuf::from("/root/a")], &app_state, cx))
 684            .await;
 685        assert_eq!(cx.window_ids().len(), 1);
 686
 687        // When opening the workspace, the window is not in a edited state.
 688        let workspace = cx.root_view::<Workspace>(cx.window_ids()[0]).unwrap();
 689        let editor = workspace.read_with(cx, |workspace, cx| {
 690            workspace
 691                .active_item(cx)
 692                .unwrap()
 693                .downcast::<Editor>()
 694                .unwrap()
 695        });
 696        assert!(!cx.is_window_edited(workspace.window_id()));
 697
 698        // Editing a buffer marks the window as edited.
 699        editor.update(cx, |editor, cx| editor.insert("EDIT", cx));
 700        assert!(cx.is_window_edited(workspace.window_id()));
 701
 702        // Undoing the edit restores the window's edited state.
 703        editor.update(cx, |editor, cx| editor.undo(&Default::default(), cx));
 704        assert!(!cx.is_window_edited(workspace.window_id()));
 705
 706        // Redoing the edit marks the window as edited again.
 707        editor.update(cx, |editor, cx| editor.redo(&Default::default(), cx));
 708        assert!(cx.is_window_edited(workspace.window_id()));
 709
 710        // Closing the item restores the window's edited state.
 711        let close = workspace.update(cx, |workspace, cx| {
 712            drop(editor);
 713            Pane::close_active_item(workspace, &Default::default(), cx).unwrap()
 714        });
 715        executor.run_until_parked();
 716        cx.simulate_prompt_answer(workspace.window_id(), 1);
 717        close.await.unwrap();
 718        assert!(!cx.is_window_edited(workspace.window_id()));
 719
 720        // Opening the buffer again doesn't impact the window's edited state.
 721        cx.update(|cx| open_paths(&[PathBuf::from("/root/a")], &app_state, cx))
 722            .await;
 723        let editor = workspace.read_with(cx, |workspace, cx| {
 724            workspace
 725                .active_item(cx)
 726                .unwrap()
 727                .downcast::<Editor>()
 728                .unwrap()
 729        });
 730        assert!(!cx.is_window_edited(workspace.window_id()));
 731
 732        // Editing the buffer marks the window as edited.
 733        editor.update(cx, |editor, cx| editor.insert("EDIT", cx));
 734        assert!(cx.is_window_edited(workspace.window_id()));
 735
 736        // Ensure closing the window via the mouse gets preempted due to the
 737        // buffer having unsaved changes.
 738        assert!(!cx.simulate_window_close(workspace.window_id()));
 739        executor.run_until_parked();
 740        assert_eq!(cx.window_ids().len(), 1);
 741
 742        // The window is successfully closed after the user dismisses the prompt.
 743        cx.simulate_prompt_answer(workspace.window_id(), 1);
 744        executor.run_until_parked();
 745        assert_eq!(cx.window_ids().len(), 0);
 746    }
 747
 748    #[gpui::test]
 749    async fn test_new_empty_workspace(cx: &mut TestAppContext) {
 750        let app_state = init(cx);
 751        cx.dispatch_global_action(workspace::NewFile);
 752        let window_id = *cx.window_ids().first().unwrap();
 753        let workspace = cx.root_view::<Workspace>(window_id).unwrap();
 754        let editor = workspace.update(cx, |workspace, cx| {
 755            workspace
 756                .active_item(cx)
 757                .unwrap()
 758                .downcast::<editor::Editor>()
 759                .unwrap()
 760        });
 761
 762        editor.update(cx, |editor, cx| {
 763            assert!(editor.text(cx).is_empty());
 764        });
 765
 766        let save_task = workspace.update(cx, |workspace, cx| workspace.save_active_item(false, cx));
 767        app_state.fs.create_dir(Path::new("/root")).await.unwrap();
 768        cx.simulate_new_path_selection(|_| Some(PathBuf::from("/root/the-new-name")));
 769        save_task.await.unwrap();
 770        editor.read_with(cx, |editor, cx| {
 771            assert!(!editor.is_dirty(cx));
 772            assert_eq!(editor.title(cx), "the-new-name");
 773        });
 774    }
 775
 776    #[gpui::test]
 777    async fn test_open_entry(cx: &mut TestAppContext) {
 778        let app_state = init(cx);
 779        app_state
 780            .fs
 781            .as_fake()
 782            .insert_tree(
 783                "/root",
 784                json!({
 785                    "a": {
 786                        "file1": "contents 1",
 787                        "file2": "contents 2",
 788                        "file3": "contents 3",
 789                    },
 790                }),
 791            )
 792            .await;
 793
 794        let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
 795        let (_, workspace) =
 796            cx.add_window(|cx| Workspace::new(project, |_, _| unimplemented!(), cx));
 797
 798        let entries = cx.read(|cx| workspace.file_project_paths(cx));
 799        let file1 = entries[0].clone();
 800        let file2 = entries[1].clone();
 801        let file3 = entries[2].clone();
 802
 803        // Open the first entry
 804        let entry_1 = workspace
 805            .update(cx, |w, cx| w.open_path(file1.clone(), true, cx))
 806            .await
 807            .unwrap();
 808        cx.read(|cx| {
 809            let pane = workspace.read(cx).active_pane().read(cx);
 810            assert_eq!(
 811                pane.active_item().unwrap().project_path(cx),
 812                Some(file1.clone())
 813            );
 814            assert_eq!(pane.items().count(), 1);
 815        });
 816
 817        // Open the second entry
 818        workspace
 819            .update(cx, |w, cx| w.open_path(file2.clone(), true, cx))
 820            .await
 821            .unwrap();
 822        cx.read(|cx| {
 823            let pane = workspace.read(cx).active_pane().read(cx);
 824            assert_eq!(
 825                pane.active_item().unwrap().project_path(cx),
 826                Some(file2.clone())
 827            );
 828            assert_eq!(pane.items().count(), 2);
 829        });
 830
 831        // Open the first entry again. The existing pane item is activated.
 832        let entry_1b = workspace
 833            .update(cx, |w, cx| w.open_path(file1.clone(), true, cx))
 834            .await
 835            .unwrap();
 836        assert_eq!(entry_1.id(), entry_1b.id());
 837
 838        cx.read(|cx| {
 839            let pane = workspace.read(cx).active_pane().read(cx);
 840            assert_eq!(
 841                pane.active_item().unwrap().project_path(cx),
 842                Some(file1.clone())
 843            );
 844            assert_eq!(pane.items().count(), 2);
 845        });
 846
 847        // Split the pane with the first entry, then open the second entry again.
 848        workspace
 849            .update(cx, |w, cx| {
 850                w.split_pane(w.active_pane().clone(), SplitDirection::Right, cx);
 851                w.open_path(file2.clone(), true, cx)
 852            })
 853            .await
 854            .unwrap();
 855
 856        workspace.read_with(cx, |w, cx| {
 857            assert_eq!(
 858                w.active_pane()
 859                    .read(cx)
 860                    .active_item()
 861                    .unwrap()
 862                    .project_path(cx.as_ref()),
 863                Some(file2.clone())
 864            );
 865        });
 866
 867        // Open the third entry twice concurrently. Only one pane item is added.
 868        let (t1, t2) = workspace.update(cx, |w, cx| {
 869            (
 870                w.open_path(file3.clone(), true, cx),
 871                w.open_path(file3.clone(), true, cx),
 872            )
 873        });
 874        t1.await.unwrap();
 875        t2.await.unwrap();
 876        cx.read(|cx| {
 877            let pane = workspace.read(cx).active_pane().read(cx);
 878            assert_eq!(
 879                pane.active_item().unwrap().project_path(cx),
 880                Some(file3.clone())
 881            );
 882            let pane_entries = pane
 883                .items()
 884                .map(|i| i.project_path(cx).unwrap())
 885                .collect::<Vec<_>>();
 886            assert_eq!(pane_entries, &[file1, file2, file3]);
 887        });
 888    }
 889
 890    #[gpui::test]
 891    async fn test_open_paths(cx: &mut TestAppContext) {
 892        let app_state = init(cx);
 893
 894        app_state
 895            .fs
 896            .as_fake()
 897            .insert_tree(
 898                "/",
 899                json!({
 900                    "dir1": {
 901                        "a.txt": ""
 902                    },
 903                    "dir2": {
 904                        "b.txt": ""
 905                    },
 906                    "dir3": {
 907                        "c.txt": ""
 908                    },
 909                    "d.txt": ""
 910                }),
 911            )
 912            .await;
 913
 914        let project = Project::test(app_state.fs.clone(), ["/dir1".as_ref()], cx).await;
 915        let (_, workspace) =
 916            cx.add_window(|cx| Workspace::new(project, |_, _| unimplemented!(), cx));
 917
 918        // Open a file within an existing worktree.
 919        cx.update(|cx| {
 920            workspace.update(cx, |view, cx| {
 921                view.open_paths(vec!["/dir1/a.txt".into()], true, cx)
 922            })
 923        })
 924        .await;
 925        cx.read(|cx| {
 926            assert_eq!(
 927                workspace
 928                    .read(cx)
 929                    .active_pane()
 930                    .read(cx)
 931                    .active_item()
 932                    .unwrap()
 933                    .to_any()
 934                    .downcast::<Editor>()
 935                    .unwrap()
 936                    .read(cx)
 937                    .title(cx),
 938                "a.txt"
 939            );
 940        });
 941
 942        // Open a file outside of any existing worktree.
 943        cx.update(|cx| {
 944            workspace.update(cx, |view, cx| {
 945                view.open_paths(vec!["/dir2/b.txt".into()], true, cx)
 946            })
 947        })
 948        .await;
 949        cx.read(|cx| {
 950            let worktree_roots = workspace
 951                .read(cx)
 952                .worktrees(cx)
 953                .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref())
 954                .collect::<HashSet<_>>();
 955            assert_eq!(
 956                worktree_roots,
 957                vec!["/dir1", "/dir2/b.txt"]
 958                    .into_iter()
 959                    .map(Path::new)
 960                    .collect(),
 961            );
 962            assert_eq!(
 963                workspace
 964                    .read(cx)
 965                    .active_pane()
 966                    .read(cx)
 967                    .active_item()
 968                    .unwrap()
 969                    .to_any()
 970                    .downcast::<Editor>()
 971                    .unwrap()
 972                    .read(cx)
 973                    .title(cx),
 974                "b.txt"
 975            );
 976        });
 977
 978        // Ensure opening a directory and one of its children only adds one worktree.
 979        cx.update(|cx| {
 980            workspace.update(cx, |view, cx| {
 981                view.open_paths(vec!["/dir3".into(), "/dir3/c.txt".into()], true, cx)
 982            })
 983        })
 984        .await;
 985        cx.read(|cx| {
 986            let worktree_roots = workspace
 987                .read(cx)
 988                .worktrees(cx)
 989                .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref())
 990                .collect::<HashSet<_>>();
 991            assert_eq!(
 992                worktree_roots,
 993                vec!["/dir1", "/dir2/b.txt", "/dir3"]
 994                    .into_iter()
 995                    .map(Path::new)
 996                    .collect(),
 997            );
 998            assert_eq!(
 999                workspace
1000                    .read(cx)
1001                    .active_pane()
1002                    .read(cx)
1003                    .active_item()
1004                    .unwrap()
1005                    .to_any()
1006                    .downcast::<Editor>()
1007                    .unwrap()
1008                    .read(cx)
1009                    .title(cx),
1010                "c.txt"
1011            );
1012        });
1013
1014        // Ensure opening invisibly a file outside an existing worktree adds a new, invisible worktree.
1015        cx.update(|cx| {
1016            workspace.update(cx, |view, cx| {
1017                view.open_paths(vec!["/d.txt".into()], false, cx)
1018            })
1019        })
1020        .await;
1021        cx.read(|cx| {
1022            let worktree_roots = workspace
1023                .read(cx)
1024                .worktrees(cx)
1025                .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref())
1026                .collect::<HashSet<_>>();
1027            assert_eq!(
1028                worktree_roots,
1029                vec!["/dir1", "/dir2/b.txt", "/dir3", "/d.txt"]
1030                    .into_iter()
1031                    .map(Path::new)
1032                    .collect(),
1033            );
1034
1035            let visible_worktree_roots = workspace
1036                .read(cx)
1037                .visible_worktrees(cx)
1038                .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref())
1039                .collect::<HashSet<_>>();
1040            assert_eq!(
1041                visible_worktree_roots,
1042                vec!["/dir1", "/dir2/b.txt", "/dir3"]
1043                    .into_iter()
1044                    .map(Path::new)
1045                    .collect(),
1046            );
1047
1048            assert_eq!(
1049                workspace
1050                    .read(cx)
1051                    .active_pane()
1052                    .read(cx)
1053                    .active_item()
1054                    .unwrap()
1055                    .to_any()
1056                    .downcast::<Editor>()
1057                    .unwrap()
1058                    .read(cx)
1059                    .title(cx),
1060                "d.txt"
1061            );
1062        });
1063    }
1064
1065    #[gpui::test]
1066    async fn test_save_conflicting_item(cx: &mut TestAppContext) {
1067        let app_state = init(cx);
1068        app_state
1069            .fs
1070            .as_fake()
1071            .insert_tree("/root", json!({ "a.txt": "" }))
1072            .await;
1073
1074        let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
1075        let (window_id, workspace) =
1076            cx.add_window(|cx| Workspace::new(project, |_, _| unimplemented!(), cx));
1077
1078        // Open a file within an existing worktree.
1079        cx.update(|cx| {
1080            workspace.update(cx, |view, cx| {
1081                view.open_paths(vec![PathBuf::from("/root/a.txt")], true, cx)
1082            })
1083        })
1084        .await;
1085        let editor = cx.read(|cx| {
1086            let pane = workspace.read(cx).active_pane().read(cx);
1087            let item = pane.active_item().unwrap();
1088            item.downcast::<Editor>().unwrap()
1089        });
1090
1091        cx.update(|cx| editor.update(cx, |editor, cx| editor.handle_input("x", cx)));
1092        app_state
1093            .fs
1094            .as_fake()
1095            .insert_file("/root/a.txt", "changed".to_string())
1096            .await;
1097        editor
1098            .condition(cx, |editor, cx| editor.has_conflict(cx))
1099            .await;
1100        cx.read(|cx| assert!(editor.is_dirty(cx)));
1101
1102        let save_task = workspace.update(cx, |workspace, cx| workspace.save_active_item(false, cx));
1103        cx.simulate_prompt_answer(window_id, 0);
1104        save_task.await.unwrap();
1105        editor.read_with(cx, |editor, cx| {
1106            assert!(!editor.is_dirty(cx));
1107            assert!(!editor.has_conflict(cx));
1108        });
1109    }
1110
1111    #[gpui::test]
1112    async fn test_open_and_save_new_file(cx: &mut TestAppContext) {
1113        let app_state = init(cx);
1114        app_state.fs.create_dir(Path::new("/root")).await.unwrap();
1115
1116        let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
1117        project.update(cx, |project, _| project.languages().add(rust_lang()));
1118        let (window_id, workspace) =
1119            cx.add_window(|cx| Workspace::new(project, |_, _| unimplemented!(), cx));
1120        let worktree = cx.read(|cx| workspace.read(cx).worktrees(cx).next().unwrap());
1121
1122        // Create a new untitled buffer
1123        cx.dispatch_action(window_id, NewFile);
1124        let editor = workspace.read_with(cx, |workspace, cx| {
1125            workspace
1126                .active_item(cx)
1127                .unwrap()
1128                .downcast::<Editor>()
1129                .unwrap()
1130        });
1131
1132        editor.update(cx, |editor, cx| {
1133            assert!(!editor.is_dirty(cx));
1134            assert_eq!(editor.title(cx), "untitled");
1135            assert!(Arc::ptr_eq(
1136                &editor.language_at(0, cx).unwrap(),
1137                &languages::PLAIN_TEXT
1138            ));
1139            editor.handle_input("hi", cx);
1140            assert!(editor.is_dirty(cx));
1141        });
1142
1143        // Save the buffer. This prompts for a filename.
1144        let save_task = workspace.update(cx, |workspace, cx| workspace.save_active_item(false, cx));
1145        cx.simulate_new_path_selection(|parent_dir| {
1146            assert_eq!(parent_dir, Path::new("/root"));
1147            Some(parent_dir.join("the-new-name.rs"))
1148        });
1149        cx.read(|cx| {
1150            assert!(editor.is_dirty(cx));
1151            assert_eq!(editor.read(cx).title(cx), "untitled");
1152        });
1153
1154        // When the save completes, the buffer's title is updated and the language is assigned based
1155        // on the path.
1156        save_task.await.unwrap();
1157        editor.read_with(cx, |editor, cx| {
1158            assert!(!editor.is_dirty(cx));
1159            assert_eq!(editor.title(cx), "the-new-name.rs");
1160            assert_eq!(editor.language_at(0, cx).unwrap().name().as_ref(), "Rust");
1161        });
1162
1163        // Edit the file and save it again. This time, there is no filename prompt.
1164        editor.update(cx, |editor, cx| {
1165            editor.handle_input(" there", cx);
1166            assert!(editor.is_dirty(cx.as_ref()));
1167        });
1168        let save_task = workspace.update(cx, |workspace, cx| workspace.save_active_item(false, cx));
1169        save_task.await.unwrap();
1170        assert!(!cx.did_prompt_for_new_path());
1171        editor.read_with(cx, |editor, cx| {
1172            assert!(!editor.is_dirty(cx));
1173            assert_eq!(editor.title(cx), "the-new-name.rs")
1174        });
1175
1176        // Open the same newly-created file in another pane item. The new editor should reuse
1177        // the same buffer.
1178        cx.dispatch_action(window_id, NewFile);
1179        workspace
1180            .update(cx, |workspace, cx| {
1181                workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Right, cx);
1182                workspace.open_path((worktree.read(cx).id(), "the-new-name.rs"), true, cx)
1183            })
1184            .await
1185            .unwrap();
1186        let editor2 = workspace.update(cx, |workspace, cx| {
1187            workspace
1188                .active_item(cx)
1189                .unwrap()
1190                .downcast::<Editor>()
1191                .unwrap()
1192        });
1193        cx.read(|cx| {
1194            assert_eq!(
1195                editor2.read(cx).buffer().read(cx).as_singleton().unwrap(),
1196                editor.read(cx).buffer().read(cx).as_singleton().unwrap()
1197            );
1198        })
1199    }
1200
1201    #[gpui::test]
1202    async fn test_setting_language_when_saving_as_single_file_worktree(cx: &mut TestAppContext) {
1203        let app_state = init(cx);
1204        app_state.fs.create_dir(Path::new("/root")).await.unwrap();
1205
1206        let project = Project::test(app_state.fs.clone(), [], cx).await;
1207        project.update(cx, |project, _| project.languages().add(rust_lang()));
1208        let (window_id, workspace) =
1209            cx.add_window(|cx| Workspace::new(project, |_, _| unimplemented!(), cx));
1210
1211        // Create a new untitled buffer
1212        cx.dispatch_action(window_id, NewFile);
1213        let editor = workspace.read_with(cx, |workspace, cx| {
1214            workspace
1215                .active_item(cx)
1216                .unwrap()
1217                .downcast::<Editor>()
1218                .unwrap()
1219        });
1220
1221        editor.update(cx, |editor, cx| {
1222            assert!(Arc::ptr_eq(
1223                &editor.language_at(0, cx).unwrap(),
1224                &languages::PLAIN_TEXT
1225            ));
1226            editor.handle_input("hi", cx);
1227            assert!(editor.is_dirty(cx.as_ref()));
1228        });
1229
1230        // Save the buffer. This prompts for a filename.
1231        let save_task = workspace.update(cx, |workspace, cx| workspace.save_active_item(false, cx));
1232        cx.simulate_new_path_selection(|_| Some(PathBuf::from("/root/the-new-name.rs")));
1233        save_task.await.unwrap();
1234        // The buffer is not dirty anymore and the language is assigned based on the path.
1235        editor.read_with(cx, |editor, cx| {
1236            assert!(!editor.is_dirty(cx));
1237            assert_eq!(editor.language_at(0, cx).unwrap().name().as_ref(), "Rust")
1238        });
1239    }
1240
1241    #[gpui::test]
1242    async fn test_pane_actions(cx: &mut TestAppContext) {
1243        init(cx);
1244
1245        let app_state = cx.update(AppState::test);
1246        app_state
1247            .fs
1248            .as_fake()
1249            .insert_tree(
1250                "/root",
1251                json!({
1252                    "a": {
1253                        "file1": "contents 1",
1254                        "file2": "contents 2",
1255                        "file3": "contents 3",
1256                    },
1257                }),
1258            )
1259            .await;
1260
1261        let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
1262        let (window_id, workspace) =
1263            cx.add_window(|cx| Workspace::new(project, |_, _| unimplemented!(), cx));
1264
1265        let entries = cx.read(|cx| workspace.file_project_paths(cx));
1266        let file1 = entries[0].clone();
1267
1268        let pane_1 = cx.read(|cx| workspace.read(cx).active_pane().clone());
1269
1270        workspace
1271            .update(cx, |w, cx| w.open_path(file1.clone(), true, cx))
1272            .await
1273            .unwrap();
1274
1275        let (editor_1, buffer) = pane_1.update(cx, |pane_1, cx| {
1276            let editor = pane_1.active_item().unwrap().downcast::<Editor>().unwrap();
1277            assert_eq!(editor.project_path(cx), Some(file1.clone()));
1278            let buffer = editor.update(cx, |editor, cx| {
1279                editor.insert("dirt", cx);
1280                editor.buffer().downgrade()
1281            });
1282            (editor.downgrade(), buffer)
1283        });
1284
1285        cx.dispatch_action(window_id, pane::SplitRight);
1286        let editor_2 = cx.update(|cx| {
1287            let pane_2 = workspace.read(cx).active_pane().clone();
1288            assert_ne!(pane_1, pane_2);
1289
1290            let pane2_item = pane_2.read(cx).active_item().unwrap();
1291            assert_eq!(pane2_item.project_path(cx.as_ref()), Some(file1.clone()));
1292
1293            pane2_item.downcast::<Editor>().unwrap().downgrade()
1294        });
1295        cx.dispatch_action(window_id, workspace::CloseActiveItem);
1296
1297        cx.foreground().run_until_parked();
1298        workspace.read_with(cx, |workspace, _| {
1299            assert_eq!(workspace.panes().len(), 2); //Center pane + Dock pane
1300            assert_eq!(workspace.active_pane(), &pane_1);
1301        });
1302
1303        cx.dispatch_action(window_id, workspace::CloseActiveItem);
1304        cx.foreground().run_until_parked();
1305        cx.simulate_prompt_answer(window_id, 1);
1306        cx.foreground().run_until_parked();
1307
1308        workspace.read_with(cx, |workspace, cx| {
1309            assert_eq!(workspace.panes().len(), 2);
1310            assert!(workspace.active_item(cx).is_none());
1311        });
1312
1313        cx.assert_dropped(editor_1);
1314        cx.assert_dropped(editor_2);
1315        cx.assert_dropped(buffer);
1316    }
1317
1318    #[gpui::test]
1319    async fn test_navigation(cx: &mut TestAppContext) {
1320        let app_state = init(cx);
1321        app_state
1322            .fs
1323            .as_fake()
1324            .insert_tree(
1325                "/root",
1326                json!({
1327                    "a": {
1328                        "file1": "contents 1\n".repeat(20),
1329                        "file2": "contents 2\n".repeat(20),
1330                        "file3": "contents 3\n".repeat(20),
1331                    },
1332                }),
1333            )
1334            .await;
1335
1336        let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
1337        let (_, workspace) =
1338            cx.add_window(|cx| Workspace::new(project.clone(), |_, _| unimplemented!(), cx));
1339
1340        let entries = cx.read(|cx| workspace.file_project_paths(cx));
1341        let file1 = entries[0].clone();
1342        let file2 = entries[1].clone();
1343        let file3 = entries[2].clone();
1344
1345        let editor1 = workspace
1346            .update(cx, |w, cx| w.open_path(file1.clone(), true, cx))
1347            .await
1348            .unwrap()
1349            .downcast::<Editor>()
1350            .unwrap();
1351        editor1.update(cx, |editor, cx| {
1352            editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
1353                s.select_display_ranges([DisplayPoint::new(10, 0)..DisplayPoint::new(10, 0)])
1354            });
1355        });
1356        let editor2 = workspace
1357            .update(cx, |w, cx| w.open_path(file2.clone(), true, cx))
1358            .await
1359            .unwrap()
1360            .downcast::<Editor>()
1361            .unwrap();
1362        let editor3 = workspace
1363            .update(cx, |w, cx| w.open_path(file3.clone(), true, cx))
1364            .await
1365            .unwrap()
1366            .downcast::<Editor>()
1367            .unwrap();
1368
1369        editor3
1370            .update(cx, |editor, cx| {
1371                editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
1372                    s.select_display_ranges([DisplayPoint::new(12, 0)..DisplayPoint::new(12, 0)])
1373                });
1374                editor.newline(&Default::default(), cx);
1375                editor.newline(&Default::default(), cx);
1376                editor.move_down(&Default::default(), cx);
1377                editor.move_down(&Default::default(), cx);
1378                editor.save(project.clone(), cx)
1379            })
1380            .await
1381            .unwrap();
1382        editor3.update(cx, |editor, cx| {
1383            editor.set_scroll_position(vec2f(0., 12.5), cx)
1384        });
1385        assert_eq!(
1386            active_location(&workspace, cx),
1387            (file3.clone(), DisplayPoint::new(16, 0), 12.5)
1388        );
1389
1390        workspace
1391            .update(cx, |w, cx| Pane::go_back(w, None, cx))
1392            .await;
1393        assert_eq!(
1394            active_location(&workspace, cx),
1395            (file3.clone(), DisplayPoint::new(0, 0), 0.)
1396        );
1397
1398        workspace
1399            .update(cx, |w, cx| Pane::go_back(w, None, cx))
1400            .await;
1401        assert_eq!(
1402            active_location(&workspace, cx),
1403            (file2.clone(), DisplayPoint::new(0, 0), 0.)
1404        );
1405
1406        workspace
1407            .update(cx, |w, cx| Pane::go_back(w, None, cx))
1408            .await;
1409        assert_eq!(
1410            active_location(&workspace, cx),
1411            (file1.clone(), DisplayPoint::new(10, 0), 0.)
1412        );
1413
1414        workspace
1415            .update(cx, |w, cx| Pane::go_back(w, None, cx))
1416            .await;
1417        assert_eq!(
1418            active_location(&workspace, cx),
1419            (file1.clone(), DisplayPoint::new(0, 0), 0.)
1420        );
1421
1422        // Go back one more time and ensure we don't navigate past the first item in the history.
1423        workspace
1424            .update(cx, |w, cx| Pane::go_back(w, None, cx))
1425            .await;
1426        assert_eq!(
1427            active_location(&workspace, cx),
1428            (file1.clone(), DisplayPoint::new(0, 0), 0.)
1429        );
1430
1431        workspace
1432            .update(cx, |w, cx| Pane::go_forward(w, None, cx))
1433            .await;
1434        assert_eq!(
1435            active_location(&workspace, cx),
1436            (file1.clone(), DisplayPoint::new(10, 0), 0.)
1437        );
1438
1439        workspace
1440            .update(cx, |w, cx| Pane::go_forward(w, None, cx))
1441            .await;
1442        assert_eq!(
1443            active_location(&workspace, cx),
1444            (file2.clone(), DisplayPoint::new(0, 0), 0.)
1445        );
1446
1447        // Go forward to an item that has been closed, ensuring it gets re-opened at the same
1448        // location.
1449        workspace
1450            .update(cx, |workspace, cx| {
1451                let editor3_id = editor3.id();
1452                drop(editor3);
1453                Pane::close_item(workspace, workspace.active_pane().clone(), editor3_id, cx)
1454            })
1455            .await
1456            .unwrap();
1457        workspace
1458            .update(cx, |w, cx| Pane::go_forward(w, None, cx))
1459            .await;
1460        assert_eq!(
1461            active_location(&workspace, cx),
1462            (file3.clone(), DisplayPoint::new(0, 0), 0.)
1463        );
1464
1465        workspace
1466            .update(cx, |w, cx| Pane::go_forward(w, None, cx))
1467            .await;
1468        assert_eq!(
1469            active_location(&workspace, cx),
1470            (file3.clone(), DisplayPoint::new(16, 0), 12.5)
1471        );
1472
1473        workspace
1474            .update(cx, |w, cx| Pane::go_back(w, None, cx))
1475            .await;
1476        assert_eq!(
1477            active_location(&workspace, cx),
1478            (file3.clone(), DisplayPoint::new(0, 0), 0.)
1479        );
1480
1481        // Go back to an item that has been closed and removed from disk, ensuring it gets skipped.
1482        workspace
1483            .update(cx, |workspace, cx| {
1484                let editor2_id = editor2.id();
1485                drop(editor2);
1486                Pane::close_item(workspace, workspace.active_pane().clone(), editor2_id, cx)
1487            })
1488            .await
1489            .unwrap();
1490        app_state
1491            .fs
1492            .remove_file(Path::new("/root/a/file2"), Default::default())
1493            .await
1494            .unwrap();
1495        workspace
1496            .update(cx, |w, cx| Pane::go_back(w, None, cx))
1497            .await;
1498        assert_eq!(
1499            active_location(&workspace, cx),
1500            (file1.clone(), DisplayPoint::new(10, 0), 0.)
1501        );
1502        workspace
1503            .update(cx, |w, cx| Pane::go_forward(w, None, cx))
1504            .await;
1505        assert_eq!(
1506            active_location(&workspace, cx),
1507            (file3.clone(), DisplayPoint::new(0, 0), 0.)
1508        );
1509
1510        // Modify file to collapse multiple nav history entries into the same location.
1511        // Ensure we don't visit the same location twice when navigating.
1512        editor1.update(cx, |editor, cx| {
1513            editor.change_selections(None, cx, |s| {
1514                s.select_display_ranges([DisplayPoint::new(15, 0)..DisplayPoint::new(15, 0)])
1515            })
1516        });
1517
1518        for _ in 0..5 {
1519            editor1.update(cx, |editor, cx| {
1520                editor.change_selections(None, cx, |s| {
1521                    s.select_display_ranges([DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0)])
1522                });
1523            });
1524            editor1.update(cx, |editor, cx| {
1525                editor.change_selections(None, cx, |s| {
1526                    s.select_display_ranges([DisplayPoint::new(13, 0)..DisplayPoint::new(13, 0)])
1527                })
1528            });
1529        }
1530
1531        editor1.update(cx, |editor, cx| {
1532            editor.transact(cx, |editor, cx| {
1533                editor.change_selections(None, cx, |s| {
1534                    s.select_display_ranges([DisplayPoint::new(2, 0)..DisplayPoint::new(14, 0)])
1535                });
1536                editor.insert("", cx);
1537            })
1538        });
1539
1540        editor1.update(cx, |editor, cx| {
1541            editor.change_selections(None, cx, |s| {
1542                s.select_display_ranges([DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)])
1543            })
1544        });
1545        workspace
1546            .update(cx, |w, cx| Pane::go_back(w, None, cx))
1547            .await;
1548        assert_eq!(
1549            active_location(&workspace, cx),
1550            (file1.clone(), DisplayPoint::new(2, 0), 0.)
1551        );
1552        workspace
1553            .update(cx, |w, cx| Pane::go_back(w, None, cx))
1554            .await;
1555        assert_eq!(
1556            active_location(&workspace, cx),
1557            (file1.clone(), DisplayPoint::new(3, 0), 0.)
1558        );
1559
1560        fn active_location(
1561            workspace: &ViewHandle<Workspace>,
1562            cx: &mut TestAppContext,
1563        ) -> (ProjectPath, DisplayPoint, f32) {
1564            workspace.update(cx, |workspace, cx| {
1565                let item = workspace.active_item(cx).unwrap();
1566                let editor = item.downcast::<Editor>().unwrap();
1567                let (selections, scroll_position) = editor.update(cx, |editor, cx| {
1568                    (
1569                        editor.selections.display_ranges(cx),
1570                        editor.scroll_position(cx),
1571                    )
1572                });
1573                (
1574                    item.project_path(cx).unwrap(),
1575                    selections[0].start,
1576                    scroll_position.y(),
1577                )
1578            })
1579        }
1580    }
1581
1582    #[gpui::test]
1583    async fn test_reopening_closed_items(cx: &mut TestAppContext) {
1584        let app_state = init(cx);
1585        app_state
1586            .fs
1587            .as_fake()
1588            .insert_tree(
1589                "/root",
1590                json!({
1591                    "a": {
1592                        "file1": "",
1593                        "file2": "",
1594                        "file3": "",
1595                        "file4": "",
1596                    },
1597                }),
1598            )
1599            .await;
1600
1601        let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
1602        let (_, workspace) =
1603            cx.add_window(|cx| Workspace::new(project.clone(), |_, _| unimplemented!(), cx));
1604        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
1605
1606        let entries = cx.read(|cx| workspace.file_project_paths(cx));
1607        let file1 = entries[0].clone();
1608        let file2 = entries[1].clone();
1609        let file3 = entries[2].clone();
1610        let file4 = entries[3].clone();
1611
1612        let file1_item_id = workspace
1613            .update(cx, |w, cx| w.open_path(file1.clone(), true, cx))
1614            .await
1615            .unwrap()
1616            .id();
1617        let file2_item_id = workspace
1618            .update(cx, |w, cx| w.open_path(file2.clone(), true, cx))
1619            .await
1620            .unwrap()
1621            .id();
1622        let file3_item_id = workspace
1623            .update(cx, |w, cx| w.open_path(file3.clone(), true, cx))
1624            .await
1625            .unwrap()
1626            .id();
1627        let file4_item_id = workspace
1628            .update(cx, |w, cx| w.open_path(file4.clone(), true, cx))
1629            .await
1630            .unwrap()
1631            .id();
1632        assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
1633
1634        // Close all the pane items in some arbitrary order.
1635        workspace
1636            .update(cx, |workspace, cx| {
1637                Pane::close_item(workspace, pane.clone(), file1_item_id, cx)
1638            })
1639            .await
1640            .unwrap();
1641        assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
1642
1643        workspace
1644            .update(cx, |workspace, cx| {
1645                Pane::close_item(workspace, pane.clone(), file4_item_id, cx)
1646            })
1647            .await
1648            .unwrap();
1649        assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
1650
1651        workspace
1652            .update(cx, |workspace, cx| {
1653                Pane::close_item(workspace, pane.clone(), file2_item_id, cx)
1654            })
1655            .await
1656            .unwrap();
1657        assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
1658
1659        workspace
1660            .update(cx, |workspace, cx| {
1661                Pane::close_item(workspace, pane.clone(), file3_item_id, cx)
1662            })
1663            .await
1664            .unwrap();
1665        assert_eq!(active_path(&workspace, cx), None);
1666
1667        // Reopen all the closed items, ensuring they are reopened in the same order
1668        // in which they were closed.
1669        workspace.update(cx, Pane::reopen_closed_item).await;
1670        assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
1671
1672        workspace.update(cx, Pane::reopen_closed_item).await;
1673        assert_eq!(active_path(&workspace, cx), Some(file2.clone()));
1674
1675        workspace.update(cx, Pane::reopen_closed_item).await;
1676        assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
1677
1678        workspace.update(cx, Pane::reopen_closed_item).await;
1679        assert_eq!(active_path(&workspace, cx), Some(file1.clone()));
1680
1681        // Reopening past the last closed item is a no-op.
1682        workspace.update(cx, Pane::reopen_closed_item).await;
1683        assert_eq!(active_path(&workspace, cx), Some(file1.clone()));
1684
1685        // Reopening closed items doesn't interfere with navigation history.
1686        workspace
1687            .update(cx, |workspace, cx| Pane::go_back(workspace, None, cx))
1688            .await;
1689        assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
1690
1691        workspace
1692            .update(cx, |workspace, cx| Pane::go_back(workspace, None, cx))
1693            .await;
1694        assert_eq!(active_path(&workspace, cx), Some(file2.clone()));
1695
1696        workspace
1697            .update(cx, |workspace, cx| Pane::go_back(workspace, None, cx))
1698            .await;
1699        assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
1700
1701        workspace
1702            .update(cx, |workspace, cx| Pane::go_back(workspace, None, cx))
1703            .await;
1704        assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
1705
1706        workspace
1707            .update(cx, |workspace, cx| Pane::go_back(workspace, None, cx))
1708            .await;
1709        assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
1710
1711        workspace
1712            .update(cx, |workspace, cx| Pane::go_back(workspace, None, cx))
1713            .await;
1714        assert_eq!(active_path(&workspace, cx), Some(file2.clone()));
1715
1716        workspace
1717            .update(cx, |workspace, cx| Pane::go_back(workspace, None, cx))
1718            .await;
1719        assert_eq!(active_path(&workspace, cx), Some(file1.clone()));
1720
1721        workspace
1722            .update(cx, |workspace, cx| Pane::go_back(workspace, None, cx))
1723            .await;
1724        assert_eq!(active_path(&workspace, cx), Some(file1.clone()));
1725
1726        fn active_path(
1727            workspace: &ViewHandle<Workspace>,
1728            cx: &TestAppContext,
1729        ) -> Option<ProjectPath> {
1730            workspace.read_with(cx, |workspace, cx| {
1731                let item = workspace.active_item(cx)?;
1732                item.project_path(cx)
1733            })
1734        }
1735    }
1736
1737    #[gpui::test]
1738    fn test_bundled_settings_and_themes(cx: &mut MutableAppContext) {
1739        cx.platform()
1740            .fonts()
1741            .add_fonts(&[
1742                Assets
1743                    .load("fonts/zed-sans/zed-sans-extended.ttf")
1744                    .unwrap()
1745                    .to_vec()
1746                    .into(),
1747                Assets
1748                    .load("fonts/zed-mono/zed-mono-extended.ttf")
1749                    .unwrap()
1750                    .to_vec()
1751                    .into(),
1752            ])
1753            .unwrap();
1754        let themes = ThemeRegistry::new(Assets, cx.font_cache().clone());
1755        let settings = Settings::defaults(Assets, cx.font_cache(), &themes);
1756
1757        let mut has_default_theme = false;
1758        for theme_name in themes.list(false, false).map(|meta| meta.name) {
1759            let theme = themes.get(&theme_name).unwrap();
1760            if theme.meta.name == settings.theme.meta.name {
1761                has_default_theme = true;
1762            }
1763            assert_eq!(theme.meta.name, theme_name);
1764        }
1765        assert!(has_default_theme);
1766    }
1767
1768    fn init(cx: &mut TestAppContext) -> Arc<AppState> {
1769        cx.foreground().forbid_parking();
1770        cx.update(|cx| {
1771            let mut app_state = AppState::test(cx);
1772            let state = Arc::get_mut(&mut app_state).unwrap();
1773            state.initialize_workspace = initialize_workspace;
1774            state.build_window_options = build_window_options;
1775            call::init(app_state.client.clone(), app_state.user_store.clone(), cx);
1776            workspace::init(app_state.clone(), cx);
1777            editor::init(cx);
1778            pane::init(cx);
1779            app_state
1780        })
1781    }
1782
1783    fn rust_lang() -> Arc<language::Language> {
1784        Arc::new(language::Language::new(
1785            language::LanguageConfig {
1786                name: "Rust".into(),
1787                path_suffixes: vec!["rs".to_string()],
1788                ..Default::default()
1789            },
1790            Some(tree_sitter_rust::language()),
1791        ))
1792    }
1793}