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