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