zed.rs

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