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