zed.rs

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