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