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