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