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