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