zed.rs

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