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