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