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        cx.read_window(window_id, |cx| {
 751            let workspace = cx.root_view().clone().downcast::<Workspace>().unwrap();
 752            let workspace = workspace.read(cx);
 753            assert_eq!(
 754                workspace
 755                    .worktrees(cx)
 756                    .map(|w| w.read(cx).abs_path())
 757                    .collect::<Vec<_>>(),
 758                &[Path::new("/root/c").into(), Path::new("/root/d").into()]
 759            );
 760            assert!(workspace.left_sidebar().read(cx).is_open());
 761            assert!(workspace.active_pane().is_focused(cx));
 762        });
 763    }
 764
 765    #[gpui::test]
 766    async fn test_window_edit_state(executor: Arc<Deterministic>, cx: &mut TestAppContext) {
 767        let app_state = init(cx);
 768        app_state
 769            .fs
 770            .as_fake()
 771            .insert_tree("/root", json!({"a": "hey"}))
 772            .await;
 773
 774        cx.update(|cx| open_paths(&[PathBuf::from("/root/a")], &app_state, None, cx))
 775            .await;
 776        assert_eq!(cx.window_ids().len(), 1);
 777
 778        // When opening the workspace, the window is not in a edited state.
 779        let workspace = cx
 780            .read_window(cx.window_ids()[0], |cx| cx.root_view().clone())
 781            .unwrap()
 782            .downcast::<Workspace>()
 783            .unwrap();
 784        let editor = workspace.read_with(cx, |workspace, cx| {
 785            workspace
 786                .active_item(cx)
 787                .unwrap()
 788                .downcast::<Editor>()
 789                .unwrap()
 790        });
 791        assert!(!cx.is_window_edited(workspace.window_id()));
 792
 793        // Editing a buffer marks the window as edited.
 794        editor.update(cx, |editor, cx| editor.insert("EDIT", cx));
 795        assert!(cx.is_window_edited(workspace.window_id()));
 796
 797        // Undoing the edit restores the window's edited state.
 798        editor.update(cx, |editor, cx| editor.undo(&Default::default(), cx));
 799        assert!(!cx.is_window_edited(workspace.window_id()));
 800
 801        // Redoing the edit marks the window as edited again.
 802        editor.update(cx, |editor, cx| editor.redo(&Default::default(), cx));
 803        assert!(cx.is_window_edited(workspace.window_id()));
 804
 805        // Closing the item restores the window's edited state.
 806        let close = workspace.update(cx, |workspace, cx| {
 807            drop(editor);
 808            Pane::close_active_item(workspace, &Default::default(), cx).unwrap()
 809        });
 810        executor.run_until_parked();
 811        cx.simulate_prompt_answer(workspace.window_id(), 1);
 812        close.await.unwrap();
 813        assert!(!cx.is_window_edited(workspace.window_id()));
 814
 815        // Opening the buffer again doesn't impact the window's edited state.
 816        cx.update(|cx| open_paths(&[PathBuf::from("/root/a")], &app_state, None, cx))
 817            .await;
 818        let editor = workspace.read_with(cx, |workspace, cx| {
 819            workspace
 820                .active_item(cx)
 821                .unwrap()
 822                .downcast::<Editor>()
 823                .unwrap()
 824        });
 825        assert!(!cx.is_window_edited(workspace.window_id()));
 826
 827        // Editing the buffer marks the window as edited.
 828        editor.update(cx, |editor, cx| editor.insert("EDIT", cx));
 829        assert!(cx.is_window_edited(workspace.window_id()));
 830
 831        // Ensure closing the window via the mouse gets preempted due to the
 832        // buffer having unsaved changes.
 833        assert!(!cx.simulate_window_close(workspace.window_id()));
 834        executor.run_until_parked();
 835        assert_eq!(cx.window_ids().len(), 1);
 836
 837        // The window is successfully closed after the user dismisses the prompt.
 838        cx.simulate_prompt_answer(workspace.window_id(), 1);
 839        executor.run_until_parked();
 840        assert_eq!(cx.window_ids().len(), 0);
 841    }
 842
 843    #[gpui::test]
 844    async fn test_new_empty_workspace(cx: &mut TestAppContext) {
 845        let app_state = init(cx);
 846        cx.update(|cx| open_new(&app_state, cx, |_, cx| cx.dispatch_action(NewFile)))
 847            .await;
 848
 849        let window_id = *cx.window_ids().first().unwrap();
 850        let workspace = cx
 851            .read_window(window_id, |cx| cx.root_view().clone())
 852            .unwrap()
 853            .downcast::<Workspace>()
 854            .unwrap();
 855
 856        let editor = workspace.update(cx, |workspace, cx| {
 857            workspace
 858                .active_item(cx)
 859                .unwrap()
 860                .downcast::<editor::Editor>()
 861                .unwrap()
 862        });
 863
 864        editor.update(cx, |editor, cx| {
 865            assert!(editor.text(cx).is_empty());
 866        });
 867
 868        let save_task = workspace.update(cx, |workspace, cx| workspace.save_active_item(false, cx));
 869        app_state.fs.create_dir(Path::new("/root")).await.unwrap();
 870        cx.simulate_new_path_selection(|_| Some(PathBuf::from("/root/the-new-name")));
 871        save_task.await.unwrap();
 872        editor.read_with(cx, |editor, cx| {
 873            assert!(!editor.is_dirty(cx));
 874            assert_eq!(editor.title(cx), "the-new-name");
 875        });
 876    }
 877
 878    #[gpui::test]
 879    async fn test_open_entry(cx: &mut TestAppContext) {
 880        let app_state = init(cx);
 881        app_state
 882            .fs
 883            .as_fake()
 884            .insert_tree(
 885                "/root",
 886                json!({
 887                    "a": {
 888                        "file1": "contents 1",
 889                        "file2": "contents 2",
 890                        "file3": "contents 3",
 891                    },
 892                }),
 893            )
 894            .await;
 895
 896        let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
 897        let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
 898
 899        let entries = cx.read(|cx| workspace.file_project_paths(cx));
 900        let file1 = entries[0].clone();
 901        let file2 = entries[1].clone();
 902        let file3 = entries[2].clone();
 903
 904        // Open the first entry
 905        let entry_1 = workspace
 906            .update(cx, |w, cx| w.open_path(file1.clone(), None, true, cx))
 907            .await
 908            .unwrap();
 909        cx.read(|cx| {
 910            let pane = workspace.read(cx).active_pane().read(cx);
 911            assert_eq!(
 912                pane.active_item().unwrap().project_path(cx),
 913                Some(file1.clone())
 914            );
 915            assert_eq!(pane.items_len(), 1);
 916        });
 917
 918        // Open the second entry
 919        workspace
 920            .update(cx, |w, cx| w.open_path(file2.clone(), None, true, cx))
 921            .await
 922            .unwrap();
 923        cx.read(|cx| {
 924            let pane = workspace.read(cx).active_pane().read(cx);
 925            assert_eq!(
 926                pane.active_item().unwrap().project_path(cx),
 927                Some(file2.clone())
 928            );
 929            assert_eq!(pane.items_len(), 2);
 930        });
 931
 932        // Open the first entry again. The existing pane item is activated.
 933        let entry_1b = workspace
 934            .update(cx, |w, cx| w.open_path(file1.clone(), None, true, cx))
 935            .await
 936            .unwrap();
 937        assert_eq!(entry_1.id(), entry_1b.id());
 938
 939        cx.read(|cx| {
 940            let pane = workspace.read(cx).active_pane().read(cx);
 941            assert_eq!(
 942                pane.active_item().unwrap().project_path(cx),
 943                Some(file1.clone())
 944            );
 945            assert_eq!(pane.items_len(), 2);
 946        });
 947
 948        // Split the pane with the first entry, then open the second entry again.
 949        workspace
 950            .update(cx, |w, cx| {
 951                w.split_pane(w.active_pane().clone(), SplitDirection::Right, cx);
 952                w.open_path(file2.clone(), None, true, cx)
 953            })
 954            .await
 955            .unwrap();
 956
 957        workspace.read_with(cx, |w, cx| {
 958            assert_eq!(
 959                w.active_pane()
 960                    .read(cx)
 961                    .active_item()
 962                    .unwrap()
 963                    .project_path(cx),
 964                Some(file2.clone())
 965            );
 966        });
 967
 968        // Open the third entry twice concurrently. Only one pane item is added.
 969        let (t1, t2) = workspace.update(cx, |w, cx| {
 970            (
 971                w.open_path(file3.clone(), None, true, cx),
 972                w.open_path(file3.clone(), None, true, cx),
 973            )
 974        });
 975        t1.await.unwrap();
 976        t2.await.unwrap();
 977        cx.read(|cx| {
 978            let pane = workspace.read(cx).active_pane().read(cx);
 979            assert_eq!(
 980                pane.active_item().unwrap().project_path(cx),
 981                Some(file3.clone())
 982            );
 983            let pane_entries = pane
 984                .items()
 985                .map(|i| i.project_path(cx).unwrap())
 986                .collect::<Vec<_>>();
 987            assert_eq!(pane_entries, &[file1, file2, file3]);
 988        });
 989    }
 990
 991    #[gpui::test]
 992    async fn test_open_paths(cx: &mut TestAppContext) {
 993        let app_state = init(cx);
 994
 995        app_state
 996            .fs
 997            .as_fake()
 998            .insert_tree(
 999                "/",
1000                json!({
1001                    "dir1": {
1002                        "a.txt": ""
1003                    },
1004                    "dir2": {
1005                        "b.txt": ""
1006                    },
1007                    "dir3": {
1008                        "c.txt": ""
1009                    },
1010                    "d.txt": ""
1011                }),
1012            )
1013            .await;
1014
1015        let project = Project::test(app_state.fs.clone(), ["/dir1".as_ref()], cx).await;
1016        let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
1017
1018        // Open a file within an existing worktree.
1019        cx.update(|cx| {
1020            workspace.update(cx, |view, cx| {
1021                view.open_paths(vec!["/dir1/a.txt".into()], true, cx)
1022            })
1023        })
1024        .await;
1025        cx.read(|cx| {
1026            assert_eq!(
1027                workspace
1028                    .read(cx)
1029                    .active_pane()
1030                    .read(cx)
1031                    .active_item()
1032                    .unwrap()
1033                    .as_any()
1034                    .downcast_ref::<Editor>()
1035                    .unwrap()
1036                    .read(cx)
1037                    .title(cx),
1038                "a.txt"
1039            );
1040        });
1041
1042        // Open a file outside of any existing worktree.
1043        cx.update(|cx| {
1044            workspace.update(cx, |view, cx| {
1045                view.open_paths(vec!["/dir2/b.txt".into()], true, cx)
1046            })
1047        })
1048        .await;
1049        cx.read(|cx| {
1050            let worktree_roots = workspace
1051                .read(cx)
1052                .worktrees(cx)
1053                .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref())
1054                .collect::<HashSet<_>>();
1055            assert_eq!(
1056                worktree_roots,
1057                vec!["/dir1", "/dir2/b.txt"]
1058                    .into_iter()
1059                    .map(Path::new)
1060                    .collect(),
1061            );
1062            assert_eq!(
1063                workspace
1064                    .read(cx)
1065                    .active_pane()
1066                    .read(cx)
1067                    .active_item()
1068                    .unwrap()
1069                    .as_any()
1070                    .downcast_ref::<Editor>()
1071                    .unwrap()
1072                    .read(cx)
1073                    .title(cx),
1074                "b.txt"
1075            );
1076        });
1077
1078        // Ensure opening a directory and one of its children only adds one worktree.
1079        cx.update(|cx| {
1080            workspace.update(cx, |view, cx| {
1081                view.open_paths(vec!["/dir3".into(), "/dir3/c.txt".into()], true, cx)
1082            })
1083        })
1084        .await;
1085        cx.read(|cx| {
1086            let worktree_roots = workspace
1087                .read(cx)
1088                .worktrees(cx)
1089                .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref())
1090                .collect::<HashSet<_>>();
1091            assert_eq!(
1092                worktree_roots,
1093                vec!["/dir1", "/dir2/b.txt", "/dir3"]
1094                    .into_iter()
1095                    .map(Path::new)
1096                    .collect(),
1097            );
1098            assert_eq!(
1099                workspace
1100                    .read(cx)
1101                    .active_pane()
1102                    .read(cx)
1103                    .active_item()
1104                    .unwrap()
1105                    .as_any()
1106                    .downcast_ref::<Editor>()
1107                    .unwrap()
1108                    .read(cx)
1109                    .title(cx),
1110                "c.txt"
1111            );
1112        });
1113
1114        // Ensure opening invisibly a file outside an existing worktree adds a new, invisible worktree.
1115        cx.update(|cx| {
1116            workspace.update(cx, |view, cx| {
1117                view.open_paths(vec!["/d.txt".into()], false, cx)
1118            })
1119        })
1120        .await;
1121        cx.read(|cx| {
1122            let worktree_roots = workspace
1123                .read(cx)
1124                .worktrees(cx)
1125                .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref())
1126                .collect::<HashSet<_>>();
1127            assert_eq!(
1128                worktree_roots,
1129                vec!["/dir1", "/dir2/b.txt", "/dir3", "/d.txt"]
1130                    .into_iter()
1131                    .map(Path::new)
1132                    .collect(),
1133            );
1134
1135            let visible_worktree_roots = workspace
1136                .read(cx)
1137                .visible_worktrees(cx)
1138                .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref())
1139                .collect::<HashSet<_>>();
1140            assert_eq!(
1141                visible_worktree_roots,
1142                vec!["/dir1", "/dir2/b.txt", "/dir3"]
1143                    .into_iter()
1144                    .map(Path::new)
1145                    .collect(),
1146            );
1147
1148            assert_eq!(
1149                workspace
1150                    .read(cx)
1151                    .active_pane()
1152                    .read(cx)
1153                    .active_item()
1154                    .unwrap()
1155                    .as_any()
1156                    .downcast_ref::<Editor>()
1157                    .unwrap()
1158                    .read(cx)
1159                    .title(cx),
1160                "d.txt"
1161            );
1162        });
1163    }
1164
1165    #[gpui::test]
1166    async fn test_save_conflicting_item(cx: &mut TestAppContext) {
1167        let app_state = init(cx);
1168        app_state
1169            .fs
1170            .as_fake()
1171            .insert_tree("/root", json!({ "a.txt": "" }))
1172            .await;
1173
1174        let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
1175        let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
1176
1177        // Open a file within an existing worktree.
1178        cx.update(|cx| {
1179            workspace.update(cx, |view, cx| {
1180                view.open_paths(vec![PathBuf::from("/root/a.txt")], true, cx)
1181            })
1182        })
1183        .await;
1184        let editor = cx.read(|cx| {
1185            let pane = workspace.read(cx).active_pane().read(cx);
1186            let item = pane.active_item().unwrap();
1187            item.downcast::<Editor>().unwrap()
1188        });
1189
1190        cx.update(|cx| editor.update(cx, |editor, cx| editor.handle_input("x", cx)));
1191        app_state
1192            .fs
1193            .as_fake()
1194            .insert_file("/root/a.txt", "changed".to_string())
1195            .await;
1196        editor
1197            .condition(cx, |editor, cx| editor.has_conflict(cx))
1198            .await;
1199        cx.read(|cx| assert!(editor.is_dirty(cx)));
1200
1201        let save_task = workspace.update(cx, |workspace, cx| workspace.save_active_item(false, cx));
1202        cx.simulate_prompt_answer(window_id, 0);
1203        save_task.await.unwrap();
1204        editor.read_with(cx, |editor, cx| {
1205            assert!(!editor.is_dirty(cx));
1206            assert!(!editor.has_conflict(cx));
1207        });
1208    }
1209
1210    #[gpui::test]
1211    async fn test_open_and_save_new_file(cx: &mut TestAppContext) {
1212        let app_state = init(cx);
1213        app_state.fs.create_dir(Path::new("/root")).await.unwrap();
1214
1215        let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
1216        project.update(cx, |project, _| project.languages().add(rust_lang()));
1217        let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
1218        let worktree = cx.read(|cx| workspace.read(cx).worktrees(cx).next().unwrap());
1219
1220        // Create a new untitled buffer
1221        cx.dispatch_action(window_id, NewFile);
1222        let editor = workspace.read_with(cx, |workspace, cx| {
1223            workspace
1224                .active_item(cx)
1225                .unwrap()
1226                .downcast::<Editor>()
1227                .unwrap()
1228        });
1229
1230        editor.update(cx, |editor, cx| {
1231            assert!(!editor.is_dirty(cx));
1232            assert_eq!(editor.title(cx), "untitled");
1233            assert!(Arc::ptr_eq(
1234                &editor.language_at(0, cx).unwrap(),
1235                &languages::PLAIN_TEXT
1236            ));
1237            editor.handle_input("hi", cx);
1238            assert!(editor.is_dirty(cx));
1239        });
1240
1241        // Save the buffer. This prompts for a filename.
1242        let save_task = workspace.update(cx, |workspace, cx| workspace.save_active_item(false, cx));
1243        cx.simulate_new_path_selection(|parent_dir| {
1244            assert_eq!(parent_dir, Path::new("/root"));
1245            Some(parent_dir.join("the-new-name.rs"))
1246        });
1247        cx.read(|cx| {
1248            assert!(editor.is_dirty(cx));
1249            assert_eq!(editor.read(cx).title(cx), "untitled");
1250        });
1251
1252        // When the save completes, the buffer's title is updated and the language is assigned based
1253        // on the path.
1254        save_task.await.unwrap();
1255        editor.read_with(cx, |editor, cx| {
1256            assert!(!editor.is_dirty(cx));
1257            assert_eq!(editor.title(cx), "the-new-name.rs");
1258            assert_eq!(editor.language_at(0, cx).unwrap().name().as_ref(), "Rust");
1259        });
1260
1261        // Edit the file and save it again. This time, there is no filename prompt.
1262        editor.update(cx, |editor, cx| {
1263            editor.handle_input(" there", cx);
1264            assert!(editor.is_dirty(cx));
1265        });
1266        let save_task = workspace.update(cx, |workspace, cx| workspace.save_active_item(false, cx));
1267        save_task.await.unwrap();
1268        assert!(!cx.did_prompt_for_new_path());
1269        editor.read_with(cx, |editor, cx| {
1270            assert!(!editor.is_dirty(cx));
1271            assert_eq!(editor.title(cx), "the-new-name.rs")
1272        });
1273
1274        // Open the same newly-created file in another pane item. The new editor should reuse
1275        // the same buffer.
1276        cx.dispatch_action(window_id, NewFile);
1277        workspace
1278            .update(cx, |workspace, cx| {
1279                workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Right, cx);
1280                workspace.open_path((worktree.read(cx).id(), "the-new-name.rs"), None, true, cx)
1281            })
1282            .await
1283            .unwrap();
1284        let editor2 = workspace.update(cx, |workspace, cx| {
1285            workspace
1286                .active_item(cx)
1287                .unwrap()
1288                .downcast::<Editor>()
1289                .unwrap()
1290        });
1291        cx.read(|cx| {
1292            assert_eq!(
1293                editor2.read(cx).buffer().read(cx).as_singleton().unwrap(),
1294                editor.read(cx).buffer().read(cx).as_singleton().unwrap()
1295            );
1296        })
1297    }
1298
1299    #[gpui::test]
1300    async fn test_setting_language_when_saving_as_single_file_worktree(cx: &mut TestAppContext) {
1301        let app_state = init(cx);
1302        app_state.fs.create_dir(Path::new("/root")).await.unwrap();
1303
1304        let project = Project::test(app_state.fs.clone(), [], cx).await;
1305        project.update(cx, |project, _| project.languages().add(rust_lang()));
1306        let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
1307
1308        // Create a new untitled buffer
1309        cx.dispatch_action(window_id, NewFile);
1310        let editor = workspace.read_with(cx, |workspace, cx| {
1311            workspace
1312                .active_item(cx)
1313                .unwrap()
1314                .downcast::<Editor>()
1315                .unwrap()
1316        });
1317
1318        editor.update(cx, |editor, cx| {
1319            assert!(Arc::ptr_eq(
1320                &editor.language_at(0, cx).unwrap(),
1321                &languages::PLAIN_TEXT
1322            ));
1323            editor.handle_input("hi", cx);
1324            assert!(editor.is_dirty(cx));
1325        });
1326
1327        // Save the buffer. This prompts for a filename.
1328        let save_task = workspace.update(cx, |workspace, cx| workspace.save_active_item(false, cx));
1329        cx.simulate_new_path_selection(|_| Some(PathBuf::from("/root/the-new-name.rs")));
1330        save_task.await.unwrap();
1331        // The buffer is not dirty anymore and the language is assigned based on the path.
1332        editor.read_with(cx, |editor, cx| {
1333            assert!(!editor.is_dirty(cx));
1334            assert_eq!(editor.language_at(0, cx).unwrap().name().as_ref(), "Rust")
1335        });
1336    }
1337
1338    #[gpui::test]
1339    async fn test_pane_actions(cx: &mut TestAppContext) {
1340        init(cx);
1341
1342        let app_state = cx.update(AppState::test);
1343        app_state
1344            .fs
1345            .as_fake()
1346            .insert_tree(
1347                "/root",
1348                json!({
1349                    "a": {
1350                        "file1": "contents 1",
1351                        "file2": "contents 2",
1352                        "file3": "contents 3",
1353                    },
1354                }),
1355            )
1356            .await;
1357
1358        let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
1359        let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
1360
1361        let entries = cx.read(|cx| workspace.file_project_paths(cx));
1362        let file1 = entries[0].clone();
1363
1364        let pane_1 = cx.read(|cx| workspace.read(cx).active_pane().clone());
1365
1366        workspace
1367            .update(cx, |w, cx| w.open_path(file1.clone(), None, true, cx))
1368            .await
1369            .unwrap();
1370
1371        let (editor_1, buffer) = pane_1.update(cx, |pane_1, cx| {
1372            let editor = pane_1.active_item().unwrap().downcast::<Editor>().unwrap();
1373            assert_eq!(editor.project_path(cx), Some(file1.clone()));
1374            let buffer = editor.update(cx, |editor, cx| {
1375                editor.insert("dirt", cx);
1376                editor.buffer().downgrade()
1377            });
1378            (editor.downgrade(), buffer)
1379        });
1380
1381        cx.dispatch_action(window_id, pane::SplitRight);
1382        let editor_2 = cx.update(|cx| {
1383            let pane_2 = workspace.read(cx).active_pane().clone();
1384            assert_ne!(pane_1, pane_2);
1385
1386            let pane2_item = pane_2.read(cx).active_item().unwrap();
1387            assert_eq!(pane2_item.project_path(cx), Some(file1.clone()));
1388
1389            pane2_item.downcast::<Editor>().unwrap().downgrade()
1390        });
1391        cx.dispatch_action(window_id, workspace::CloseActiveItem);
1392
1393        cx.foreground().run_until_parked();
1394        workspace.read_with(cx, |workspace, _| {
1395            assert_eq!(workspace.panes().len(), 2); //Center pane + Dock pane
1396            assert_eq!(workspace.active_pane(), &pane_1);
1397        });
1398
1399        cx.dispatch_action(window_id, workspace::CloseActiveItem);
1400        cx.foreground().run_until_parked();
1401        cx.simulate_prompt_answer(window_id, 1);
1402        cx.foreground().run_until_parked();
1403
1404        workspace.read_with(cx, |workspace, cx| {
1405            assert_eq!(workspace.panes().len(), 2);
1406            assert!(workspace.active_item(cx).is_none());
1407        });
1408
1409        cx.assert_dropped(editor_1);
1410        cx.assert_dropped(editor_2);
1411        cx.assert_dropped(buffer);
1412    }
1413
1414    #[gpui::test]
1415    async fn test_navigation(cx: &mut TestAppContext) {
1416        let app_state = init(cx);
1417        app_state
1418            .fs
1419            .as_fake()
1420            .insert_tree(
1421                "/root",
1422                json!({
1423                    "a": {
1424                        "file1": "contents 1\n".repeat(20),
1425                        "file2": "contents 2\n".repeat(20),
1426                        "file3": "contents 3\n".repeat(20),
1427                    },
1428                }),
1429            )
1430            .await;
1431
1432        let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
1433        let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
1434
1435        let entries = cx.read(|cx| workspace.file_project_paths(cx));
1436        let file1 = entries[0].clone();
1437        let file2 = entries[1].clone();
1438        let file3 = entries[2].clone();
1439
1440        let editor1 = workspace
1441            .update(cx, |w, cx| w.open_path(file1.clone(), None, true, cx))
1442            .await
1443            .unwrap()
1444            .downcast::<Editor>()
1445            .unwrap();
1446        editor1.update(cx, |editor, cx| {
1447            editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
1448                s.select_display_ranges([DisplayPoint::new(10, 0)..DisplayPoint::new(10, 0)])
1449            });
1450        });
1451        let editor2 = workspace
1452            .update(cx, |w, cx| w.open_path(file2.clone(), None, true, cx))
1453            .await
1454            .unwrap()
1455            .downcast::<Editor>()
1456            .unwrap();
1457        let editor3 = workspace
1458            .update(cx, |w, cx| w.open_path(file3.clone(), None, true, cx))
1459            .await
1460            .unwrap()
1461            .downcast::<Editor>()
1462            .unwrap();
1463
1464        editor3
1465            .update(cx, |editor, cx| {
1466                editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
1467                    s.select_display_ranges([DisplayPoint::new(12, 0)..DisplayPoint::new(12, 0)])
1468                });
1469                editor.newline(&Default::default(), cx);
1470                editor.newline(&Default::default(), cx);
1471                editor.move_down(&Default::default(), cx);
1472                editor.move_down(&Default::default(), cx);
1473                editor.save(project.clone(), cx)
1474            })
1475            .await
1476            .unwrap();
1477        editor3.update(cx, |editor, cx| {
1478            editor.set_scroll_position(vec2f(0., 12.5), cx)
1479        });
1480        assert_eq!(
1481            active_location(&workspace, cx),
1482            (file3.clone(), DisplayPoint::new(16, 0), 12.5)
1483        );
1484
1485        workspace
1486            .update(cx, |w, cx| Pane::go_back(w, None, cx))
1487            .await;
1488        assert_eq!(
1489            active_location(&workspace, cx),
1490            (file3.clone(), DisplayPoint::new(0, 0), 0.)
1491        );
1492
1493        workspace
1494            .update(cx, |w, cx| Pane::go_back(w, None, cx))
1495            .await;
1496        assert_eq!(
1497            active_location(&workspace, cx),
1498            (file2.clone(), DisplayPoint::new(0, 0), 0.)
1499        );
1500
1501        workspace
1502            .update(cx, |w, cx| Pane::go_back(w, None, cx))
1503            .await;
1504        assert_eq!(
1505            active_location(&workspace, cx),
1506            (file1.clone(), DisplayPoint::new(10, 0), 0.)
1507        );
1508
1509        workspace
1510            .update(cx, |w, cx| Pane::go_back(w, None, cx))
1511            .await;
1512        assert_eq!(
1513            active_location(&workspace, cx),
1514            (file1.clone(), DisplayPoint::new(0, 0), 0.)
1515        );
1516
1517        // Go back one more time and ensure we don't navigate past the first item in the history.
1518        workspace
1519            .update(cx, |w, cx| Pane::go_back(w, None, cx))
1520            .await;
1521        assert_eq!(
1522            active_location(&workspace, cx),
1523            (file1.clone(), DisplayPoint::new(0, 0), 0.)
1524        );
1525
1526        workspace
1527            .update(cx, |w, cx| Pane::go_forward(w, None, cx))
1528            .await;
1529        assert_eq!(
1530            active_location(&workspace, cx),
1531            (file1.clone(), DisplayPoint::new(10, 0), 0.)
1532        );
1533
1534        workspace
1535            .update(cx, |w, cx| Pane::go_forward(w, None, cx))
1536            .await;
1537        assert_eq!(
1538            active_location(&workspace, cx),
1539            (file2.clone(), DisplayPoint::new(0, 0), 0.)
1540        );
1541
1542        // Go forward to an item that has been closed, ensuring it gets re-opened at the same
1543        // location.
1544        workspace
1545            .update(cx, |workspace, cx| {
1546                let editor3_id = editor3.id();
1547                drop(editor3);
1548                Pane::close_item(workspace, workspace.active_pane().clone(), editor3_id, cx)
1549            })
1550            .await
1551            .unwrap();
1552        workspace
1553            .update(cx, |w, cx| Pane::go_forward(w, None, cx))
1554            .await;
1555        assert_eq!(
1556            active_location(&workspace, cx),
1557            (file3.clone(), DisplayPoint::new(0, 0), 0.)
1558        );
1559
1560        workspace
1561            .update(cx, |w, cx| Pane::go_forward(w, None, cx))
1562            .await;
1563        assert_eq!(
1564            active_location(&workspace, cx),
1565            (file3.clone(), DisplayPoint::new(16, 0), 12.5)
1566        );
1567
1568        workspace
1569            .update(cx, |w, cx| Pane::go_back(w, None, cx))
1570            .await;
1571        assert_eq!(
1572            active_location(&workspace, cx),
1573            (file3.clone(), DisplayPoint::new(0, 0), 0.)
1574        );
1575
1576        // Go back to an item that has been closed and removed from disk, ensuring it gets skipped.
1577        workspace
1578            .update(cx, |workspace, cx| {
1579                let editor2_id = editor2.id();
1580                drop(editor2);
1581                Pane::close_item(workspace, workspace.active_pane().clone(), editor2_id, cx)
1582            })
1583            .await
1584            .unwrap();
1585        app_state
1586            .fs
1587            .remove_file(Path::new("/root/a/file2"), Default::default())
1588            .await
1589            .unwrap();
1590        workspace
1591            .update(cx, |w, cx| Pane::go_back(w, None, cx))
1592            .await;
1593        assert_eq!(
1594            active_location(&workspace, cx),
1595            (file1.clone(), DisplayPoint::new(10, 0), 0.)
1596        );
1597        workspace
1598            .update(cx, |w, cx| Pane::go_forward(w, None, cx))
1599            .await;
1600        assert_eq!(
1601            active_location(&workspace, cx),
1602            (file3.clone(), DisplayPoint::new(0, 0), 0.)
1603        );
1604
1605        // Modify file to collapse multiple nav history entries into the same location.
1606        // Ensure we don't visit the same location twice when navigating.
1607        editor1.update(cx, |editor, cx| {
1608            editor.change_selections(None, cx, |s| {
1609                s.select_display_ranges([DisplayPoint::new(15, 0)..DisplayPoint::new(15, 0)])
1610            })
1611        });
1612
1613        for _ in 0..5 {
1614            editor1.update(cx, |editor, cx| {
1615                editor.change_selections(None, cx, |s| {
1616                    s.select_display_ranges([DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0)])
1617                });
1618            });
1619            editor1.update(cx, |editor, cx| {
1620                editor.change_selections(None, cx, |s| {
1621                    s.select_display_ranges([DisplayPoint::new(13, 0)..DisplayPoint::new(13, 0)])
1622                })
1623            });
1624        }
1625
1626        editor1.update(cx, |editor, cx| {
1627            editor.transact(cx, |editor, cx| {
1628                editor.change_selections(None, cx, |s| {
1629                    s.select_display_ranges([DisplayPoint::new(2, 0)..DisplayPoint::new(14, 0)])
1630                });
1631                editor.insert("", cx);
1632            })
1633        });
1634
1635        editor1.update(cx, |editor, cx| {
1636            editor.change_selections(None, cx, |s| {
1637                s.select_display_ranges([DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)])
1638            })
1639        });
1640        workspace
1641            .update(cx, |w, cx| Pane::go_back(w, None, cx))
1642            .await;
1643        assert_eq!(
1644            active_location(&workspace, cx),
1645            (file1.clone(), DisplayPoint::new(2, 0), 0.)
1646        );
1647        workspace
1648            .update(cx, |w, cx| Pane::go_back(w, None, cx))
1649            .await;
1650        assert_eq!(
1651            active_location(&workspace, cx),
1652            (file1.clone(), DisplayPoint::new(3, 0), 0.)
1653        );
1654
1655        fn active_location(
1656            workspace: &ViewHandle<Workspace>,
1657            cx: &mut TestAppContext,
1658        ) -> (ProjectPath, DisplayPoint, f32) {
1659            workspace.update(cx, |workspace, cx| {
1660                let item = workspace.active_item(cx).unwrap();
1661                let editor = item.downcast::<Editor>().unwrap();
1662                let (selections, scroll_position) = editor.update(cx, |editor, cx| {
1663                    (
1664                        editor.selections.display_ranges(cx),
1665                        editor.scroll_position(cx),
1666                    )
1667                });
1668                (
1669                    item.project_path(cx).unwrap(),
1670                    selections[0].start,
1671                    scroll_position.y(),
1672                )
1673            })
1674        }
1675    }
1676
1677    #[gpui::test]
1678    async fn test_reopening_closed_items(cx: &mut TestAppContext) {
1679        let app_state = init(cx);
1680        app_state
1681            .fs
1682            .as_fake()
1683            .insert_tree(
1684                "/root",
1685                json!({
1686                    "a": {
1687                        "file1": "",
1688                        "file2": "",
1689                        "file3": "",
1690                        "file4": "",
1691                    },
1692                }),
1693            )
1694            .await;
1695
1696        let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
1697        let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
1698        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
1699
1700        let entries = cx.read(|cx| workspace.file_project_paths(cx));
1701        let file1 = entries[0].clone();
1702        let file2 = entries[1].clone();
1703        let file3 = entries[2].clone();
1704        let file4 = entries[3].clone();
1705
1706        let file1_item_id = workspace
1707            .update(cx, |w, cx| w.open_path(file1.clone(), None, true, cx))
1708            .await
1709            .unwrap()
1710            .id();
1711        let file2_item_id = workspace
1712            .update(cx, |w, cx| w.open_path(file2.clone(), None, true, cx))
1713            .await
1714            .unwrap()
1715            .id();
1716        let file3_item_id = workspace
1717            .update(cx, |w, cx| w.open_path(file3.clone(), None, true, cx))
1718            .await
1719            .unwrap()
1720            .id();
1721        let file4_item_id = workspace
1722            .update(cx, |w, cx| w.open_path(file4.clone(), None, true, cx))
1723            .await
1724            .unwrap()
1725            .id();
1726        assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
1727
1728        // Close all the pane items in some arbitrary order.
1729        workspace
1730            .update(cx, |workspace, cx| {
1731                Pane::close_item(workspace, pane.clone(), file1_item_id, cx)
1732            })
1733            .await
1734            .unwrap();
1735        assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
1736
1737        workspace
1738            .update(cx, |workspace, cx| {
1739                Pane::close_item(workspace, pane.clone(), file4_item_id, cx)
1740            })
1741            .await
1742            .unwrap();
1743        assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
1744
1745        workspace
1746            .update(cx, |workspace, cx| {
1747                Pane::close_item(workspace, pane.clone(), file2_item_id, cx)
1748            })
1749            .await
1750            .unwrap();
1751        assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
1752
1753        workspace
1754            .update(cx, |workspace, cx| {
1755                Pane::close_item(workspace, pane.clone(), file3_item_id, cx)
1756            })
1757            .await
1758            .unwrap();
1759        assert_eq!(active_path(&workspace, cx), None);
1760
1761        // Reopen all the closed items, ensuring they are reopened in the same order
1762        // in which they were closed.
1763        workspace.update(cx, Pane::reopen_closed_item).await;
1764        assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
1765
1766        workspace.update(cx, Pane::reopen_closed_item).await;
1767        assert_eq!(active_path(&workspace, cx), Some(file2.clone()));
1768
1769        workspace.update(cx, Pane::reopen_closed_item).await;
1770        assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
1771
1772        workspace.update(cx, Pane::reopen_closed_item).await;
1773        assert_eq!(active_path(&workspace, cx), Some(file1.clone()));
1774
1775        // Reopening past the last closed item is a no-op.
1776        workspace.update(cx, Pane::reopen_closed_item).await;
1777        assert_eq!(active_path(&workspace, cx), Some(file1.clone()));
1778
1779        // Reopening closed items doesn't interfere with navigation history.
1780        workspace
1781            .update(cx, |workspace, cx| Pane::go_back(workspace, None, cx))
1782            .await;
1783        assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
1784
1785        workspace
1786            .update(cx, |workspace, cx| Pane::go_back(workspace, None, cx))
1787            .await;
1788        assert_eq!(active_path(&workspace, cx), Some(file2.clone()));
1789
1790        workspace
1791            .update(cx, |workspace, cx| Pane::go_back(workspace, None, cx))
1792            .await;
1793        assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
1794
1795        workspace
1796            .update(cx, |workspace, cx| Pane::go_back(workspace, None, cx))
1797            .await;
1798        assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
1799
1800        workspace
1801            .update(cx, |workspace, cx| Pane::go_back(workspace, None, cx))
1802            .await;
1803        assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
1804
1805        workspace
1806            .update(cx, |workspace, cx| Pane::go_back(workspace, None, cx))
1807            .await;
1808        assert_eq!(active_path(&workspace, cx), Some(file2.clone()));
1809
1810        workspace
1811            .update(cx, |workspace, cx| Pane::go_back(workspace, None, cx))
1812            .await;
1813        assert_eq!(active_path(&workspace, cx), Some(file1.clone()));
1814
1815        workspace
1816            .update(cx, |workspace, cx| Pane::go_back(workspace, None, cx))
1817            .await;
1818        assert_eq!(active_path(&workspace, cx), Some(file1.clone()));
1819
1820        fn active_path(
1821            workspace: &ViewHandle<Workspace>,
1822            cx: &TestAppContext,
1823        ) -> Option<ProjectPath> {
1824            workspace.read_with(cx, |workspace, cx| {
1825                let item = workspace.active_item(cx)?;
1826                item.project_path(cx)
1827            })
1828        }
1829    }
1830
1831    #[gpui::test]
1832    fn test_bundled_settings_and_themes(cx: &mut AppContext) {
1833        cx.platform()
1834            .fonts()
1835            .add_fonts(&[
1836                Assets
1837                    .load("fonts/zed-sans/zed-sans-extended.ttf")
1838                    .unwrap()
1839                    .to_vec()
1840                    .into(),
1841                Assets
1842                    .load("fonts/zed-mono/zed-mono-extended.ttf")
1843                    .unwrap()
1844                    .to_vec()
1845                    .into(),
1846            ])
1847            .unwrap();
1848        let themes = ThemeRegistry::new(Assets, cx.font_cache().clone());
1849        let settings = Settings::defaults(Assets, cx.font_cache(), &themes);
1850
1851        let mut has_default_theme = false;
1852        for theme_name in themes.list(false).map(|meta| meta.name) {
1853            let theme = themes.get(&theme_name).unwrap();
1854            if theme.meta.name == settings.theme.meta.name {
1855                has_default_theme = true;
1856            }
1857            assert_eq!(theme.meta.name, theme_name);
1858        }
1859        assert!(has_default_theme);
1860    }
1861
1862    #[gpui::test]
1863    fn test_bundled_languages(cx: &mut AppContext) {
1864        let mut languages = LanguageRegistry::test();
1865        languages.set_executor(cx.background().clone());
1866        let languages = Arc::new(languages);
1867        let themes = ThemeRegistry::new((), cx.font_cache().clone());
1868        let http = FakeHttpClient::with_404_response();
1869        let node_runtime = NodeRuntime::new(http, cx.background().to_owned());
1870        languages::init(languages.clone(), themes, node_runtime);
1871        for name in languages.language_names() {
1872            languages.language_for_name(&name);
1873        }
1874        cx.foreground().run_until_parked();
1875    }
1876
1877    fn init(cx: &mut TestAppContext) -> Arc<AppState> {
1878        cx.foreground().forbid_parking();
1879        cx.update(|cx| {
1880            let mut app_state = AppState::test(cx);
1881            let state = Arc::get_mut(&mut app_state).unwrap();
1882            state.initialize_workspace = initialize_workspace;
1883            state.build_window_options = build_window_options;
1884            call::init(app_state.client.clone(), app_state.user_store.clone(), cx);
1885            workspace::init(app_state.clone(), cx);
1886            editor::init(cx);
1887            pane::init(cx);
1888            app_state
1889        })
1890    }
1891
1892    fn rust_lang() -> Arc<language::Language> {
1893        Arc::new(language::Language::new(
1894            language::LanguageConfig {
1895                name: "Rust".into(),
1896                path_suffixes: vec!["rs".to_string()],
1897                ..Default::default()
1898            },
1899            Some(tree_sitter_rust::language()),
1900        ))
1901    }
1902}