zed.rs

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