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