zed.rs

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