zed.rs

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