editor_tests.rs

   1use editor::{
   2    test::editor_test_context::EditorTestContext, ConfirmCodeAction, ConfirmCompletion,
   3    ConfirmRename, Editor, Redo, Rename, ToggleCodeActions, Undo,
   4};
   5use gpui::{BackgroundExecutor, TestAppContext};
   6
   7use crate::tests::TestServer;
   8
   9#[gpui::test(iterations = 10)]
  10async fn test_host_disconnect(
  11    executor: BackgroundExecutor,
  12    cx_a: &mut TestAppContext,
  13    cx_b: &mut TestAppContext,
  14    cx_c: &mut TestAppContext,
  15) {
  16    let mut server = TestServer::start(&executor).await;
  17    let client_a = server.create_client(cx_a, "user_a").await;
  18    let client_b = server.create_client(cx_b, "user_b").await;
  19    let client_c = server.create_client(cx_c, "user_c").await;
  20    server
  21        .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)])
  22        .await;
  23
  24    cx_b.update(editor::init);
  25
  26    client_a
  27        .fs()
  28        .insert_tree(
  29            "/a",
  30            json!({
  31                "a.txt": "a-contents",
  32                "b.txt": "b-contents",
  33            }),
  34        )
  35        .await;
  36
  37    let active_call_a = cx_a.read(ActiveCall::global);
  38    let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
  39
  40    let worktree_a = project_a.read_with(cx_a, |project, cx| project.worktrees(cx).next().unwrap());
  41    let project_id = active_call_a
  42        .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
  43        .await
  44        .unwrap();
  45
  46    let project_b = client_b.build_remote_project(project_id, cx_b).await;
  47    executor.run_until_parked();
  48
  49    assert!(worktree_a.read_with(cx_a, |tree, _| tree.as_local().unwrap().is_shared()));
  50
  51    let window_b =
  52        cx_b.add_window(|cx| Workspace::new(0, project_b.clone(), client_b.app_state.clone(), cx));
  53    let workspace_b = window_b.root(cx_b);
  54    let editor_b = workspace_b
  55        .update(cx_b, |workspace, cx| {
  56            workspace.open_path((worktree_id, "b.txt"), None, true, cx)
  57        })
  58        .await
  59        .unwrap()
  60        .downcast::<Editor>()
  61        .unwrap();
  62
  63    assert!(window_b.read_with(cx_b, |cx| editor_b.is_focused(cx)));
  64    editor_b.update(cx_b, |editor, cx| editor.insert("X", cx));
  65    assert!(window_b.is_edited(cx_b));
  66
  67    // Drop client A's connection. Collaborators should disappear and the project should not be shown as shared.
  68    server.forbid_connections();
  69    server.disconnect_client(client_a.peer_id().unwrap());
  70    executor.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
  71
  72    project_a.read_with(cx_a, |project, _| project.collaborators().is_empty());
  73
  74    project_a.read_with(cx_a, |project, _| assert!(!project.is_shared()));
  75
  76    project_b.read_with(cx_b, |project, _| project.is_read_only());
  77
  78    assert!(worktree_a.read_with(cx_a, |tree, _| !tree.as_local().unwrap().is_shared()));
  79
  80    // Ensure client B's edited state is reset and that the whole window is blurred.
  81
  82    window_b.read_with(cx_b, |cx| {
  83        assert_eq!(cx.focused_view_id(), None);
  84    });
  85    assert!(!window_b.is_edited(cx_b));
  86
  87    // Ensure client B is not prompted to save edits when closing window after disconnecting.
  88    let can_close = workspace_b
  89        .update(cx_b, |workspace, cx| workspace.prepare_to_close(true, cx))
  90        .await
  91        .unwrap();
  92    assert!(can_close);
  93
  94    // Allow client A to reconnect to the server.
  95    server.allow_connections();
  96    executor.advance_clock(RECEIVE_TIMEOUT);
  97
  98    // Client B calls client A again after they reconnected.
  99    let active_call_b = cx_b.read(ActiveCall::global);
 100    active_call_b
 101        .update(cx_b, |call, cx| {
 102            call.invite(client_a.user_id().unwrap(), None, cx)
 103        })
 104        .await
 105        .unwrap();
 106    executor.run_until_parked();
 107    active_call_a
 108        .update(cx_a, |call, cx| call.accept_incoming(cx))
 109        .await
 110        .unwrap();
 111
 112    active_call_a
 113        .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
 114        .await
 115        .unwrap();
 116
 117    // Drop client A's connection again. We should still unshare it successfully.
 118    server.forbid_connections();
 119    server.disconnect_client(client_a.peer_id().unwrap());
 120    executor.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
 121
 122    project_a.read_with(cx_a, |project, _| assert!(!project.is_shared()));
 123}
 124
 125todo!(editor)
 126#[gpui::test]
 127async fn test_newline_above_or_below_does_not_move_guest_cursor(
 128    executor: BackgroundExecutor,
 129    cx_a: &mut TestAppContext,
 130    cx_b: &mut TestAppContext,
 131) {
 132    let mut server = TestServer::start(&executor).await;
 133    let client_a = server.create_client(cx_a, "user_a").await;
 134    let client_b = server.create_client(cx_b, "user_b").await;
 135    server
 136        .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
 137        .await;
 138    let active_call_a = cx_a.read(ActiveCall::global);
 139
 140    client_a
 141        .fs()
 142        .insert_tree("/dir", json!({ "a.txt": "Some text\n" }))
 143        .await;
 144    let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
 145    let project_id = active_call_a
 146        .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
 147        .await
 148        .unwrap();
 149
 150    let project_b = client_b.build_remote_project(project_id, cx_b).await;
 151
 152    // Open a buffer as client A
 153    let buffer_a = project_a
 154        .update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
 155        .await
 156        .unwrap();
 157    let window_a = cx_a.add_window(|_| EmptyView);
 158    let editor_a = window_a.add_view(cx_a, |cx| Editor::for_buffer(buffer_a, Some(project_a), cx));
 159    let mut editor_cx_a = EditorTestContext {
 160        cx: cx_a,
 161        window: window_a.into(),
 162        editor: editor_a,
 163    };
 164
 165    // Open a buffer as client B
 166    let buffer_b = project_b
 167        .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
 168        .await
 169        .unwrap();
 170    let window_b = cx_b.add_window(|_| EmptyView);
 171    let editor_b = window_b.add_view(cx_b, |cx| Editor::for_buffer(buffer_b, Some(project_b), cx));
 172    let mut editor_cx_b = EditorTestContext {
 173        cx: cx_b,
 174        window: window_b.into(),
 175        editor: editor_b,
 176    };
 177
 178    // Test newline above
 179    editor_cx_a.set_selections_state(indoc! {"
 180        Some textˇ
 181    "});
 182    editor_cx_b.set_selections_state(indoc! {"
 183        Some textˇ
 184    "});
 185    editor_cx_a.update_editor(|editor, cx| editor.newline_above(&editor::NewlineAbove, cx));
 186    executor.run_until_parked();
 187    editor_cx_a.assert_editor_state(indoc! {"
 188        ˇ
 189        Some text
 190    "});
 191    editor_cx_b.assert_editor_state(indoc! {"
 192
 193        Some textˇ
 194    "});
 195
 196    // Test newline below
 197    editor_cx_a.set_selections_state(indoc! {"
 198
 199        Some textˇ
 200    "});
 201    editor_cx_b.set_selections_state(indoc! {"
 202
 203        Some textˇ
 204    "});
 205    editor_cx_a.update_editor(|editor, cx| editor.newline_below(&editor::NewlineBelow, cx));
 206    executor.run_until_parked();
 207    editor_cx_a.assert_editor_state(indoc! {"
 208
 209        Some text
 210        ˇ
 211    "});
 212    editor_cx_b.assert_editor_state(indoc! {"
 213
 214        Some textˇ
 215
 216    "});
 217}
 218
 219todo!(editor)
 220#[gpui::test(iterations = 10)]
 221async fn test_collaborating_with_completion(
 222    executor: BackgroundExecutor,
 223    cx_a: &mut TestAppContext,
 224    cx_b: &mut TestAppContext,
 225) {
 226    let mut server = TestServer::start(&executor).await;
 227    let client_a = server.create_client(cx_a, "user_a").await;
 228    let client_b = server.create_client(cx_b, "user_b").await;
 229    server
 230        .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
 231        .await;
 232    let active_call_a = cx_a.read(ActiveCall::global);
 233
 234    // Set up a fake language server.
 235    let mut language = Language::new(
 236        LanguageConfig {
 237            name: "Rust".into(),
 238            path_suffixes: vec!["rs".to_string()],
 239            ..Default::default()
 240        },
 241        Some(tree_sitter_rust::language()),
 242    );
 243    let mut fake_language_servers = language
 244        .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
 245            capabilities: lsp::ServerCapabilities {
 246                completion_provider: Some(lsp::CompletionOptions {
 247                    trigger_characters: Some(vec![".".to_string()]),
 248                    resolve_provider: Some(true),
 249                    ..Default::default()
 250                }),
 251                ..Default::default()
 252            },
 253            ..Default::default()
 254        }))
 255        .await;
 256    client_a.language_registry().add(Arc::new(language));
 257
 258    client_a
 259        .fs()
 260        .insert_tree(
 261            "/a",
 262            json!({
 263                "main.rs": "fn main() { a }",
 264                "other.rs": "",
 265            }),
 266        )
 267        .await;
 268    let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
 269    let project_id = active_call_a
 270        .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
 271        .await
 272        .unwrap();
 273    let project_b = client_b.build_remote_project(project_id, cx_b).await;
 274
 275    // Open a file in an editor as the guest.
 276    let buffer_b = project_b
 277        .update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
 278        .await
 279        .unwrap();
 280    let window_b = cx_b.add_window(|_| EmptyView);
 281    let editor_b = window_b.add_view(cx_b, |cx| {
 282        Editor::for_buffer(buffer_b.clone(), Some(project_b.clone()), cx)
 283    });
 284
 285    let fake_language_server = fake_language_servers.next().await.unwrap();
 286    cx_a.foreground().run_until_parked();
 287
 288    buffer_b.read_with(cx_b, |buffer, _| {
 289        assert!(!buffer.completion_triggers().is_empty())
 290    });
 291
 292    // Type a completion trigger character as the guest.
 293    editor_b.update(cx_b, |editor, cx| {
 294        editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
 295        editor.handle_input(".", cx);
 296        cx.focus(&editor_b);
 297    });
 298
 299    // Receive a completion request as the host's language server.
 300    // Return some completions from the host's language server.
 301    cx_a.foreground().start_waiting();
 302    fake_language_server
 303        .handle_request::<lsp::request::Completion, _, _>(|params, _| async move {
 304            assert_eq!(
 305                params.text_document_position.text_document.uri,
 306                lsp::Url::from_file_path("/a/main.rs").unwrap(),
 307            );
 308            assert_eq!(
 309                params.text_document_position.position,
 310                lsp::Position::new(0, 14),
 311            );
 312
 313            Ok(Some(lsp::CompletionResponse::Array(vec![
 314                lsp::CompletionItem {
 315                    label: "first_method(…)".into(),
 316                    detail: Some("fn(&mut self, B) -> C".into()),
 317                    text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
 318                        new_text: "first_method($1)".to_string(),
 319                        range: lsp::Range::new(
 320                            lsp::Position::new(0, 14),
 321                            lsp::Position::new(0, 14),
 322                        ),
 323                    })),
 324                    insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
 325                    ..Default::default()
 326                },
 327                lsp::CompletionItem {
 328                    label: "second_method(…)".into(),
 329                    detail: Some("fn(&mut self, C) -> D<E>".into()),
 330                    text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
 331                        new_text: "second_method()".to_string(),
 332                        range: lsp::Range::new(
 333                            lsp::Position::new(0, 14),
 334                            lsp::Position::new(0, 14),
 335                        ),
 336                    })),
 337                    insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
 338                    ..Default::default()
 339                },
 340            ])))
 341        })
 342        .next()
 343        .await
 344        .unwrap();
 345    cx_a.foreground().finish_waiting();
 346
 347    // Open the buffer on the host.
 348    let buffer_a = project_a
 349        .update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
 350        .await
 351        .unwrap();
 352    cx_a.foreground().run_until_parked();
 353
 354    buffer_a.read_with(cx_a, |buffer, _| {
 355        assert_eq!(buffer.text(), "fn main() { a. }")
 356    });
 357
 358    // Confirm a completion on the guest.
 359
 360    editor_b.read_with(cx_b, |editor, _| assert!(editor.context_menu_visible()));
 361    editor_b.update(cx_b, |editor, cx| {
 362        editor.confirm_completion(&ConfirmCompletion { item_ix: Some(0) }, cx);
 363        assert_eq!(editor.text(cx), "fn main() { a.first_method() }");
 364    });
 365
 366    // Return a resolved completion from the host's language server.
 367    // The resolved completion has an additional text edit.
 368    fake_language_server.handle_request::<lsp::request::ResolveCompletionItem, _, _>(
 369        |params, _| async move {
 370            assert_eq!(params.label, "first_method(…)");
 371            Ok(lsp::CompletionItem {
 372                label: "first_method(…)".into(),
 373                detail: Some("fn(&mut self, B) -> C".into()),
 374                text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
 375                    new_text: "first_method($1)".to_string(),
 376                    range: lsp::Range::new(lsp::Position::new(0, 14), lsp::Position::new(0, 14)),
 377                })),
 378                additional_text_edits: Some(vec![lsp::TextEdit {
 379                    new_text: "use d::SomeTrait;\n".to_string(),
 380                    range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
 381                }]),
 382                insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
 383                ..Default::default()
 384            })
 385        },
 386    );
 387
 388    // The additional edit is applied.
 389    cx_a.foreground().run_until_parked();
 390
 391    buffer_a.read_with(cx_a, |buffer, _| {
 392        assert_eq!(
 393            buffer.text(),
 394            "use d::SomeTrait;\nfn main() { a.first_method() }"
 395        );
 396    });
 397
 398    buffer_b.read_with(cx_b, |buffer, _| {
 399        assert_eq!(
 400            buffer.text(),
 401            "use d::SomeTrait;\nfn main() { a.first_method() }"
 402        );
 403    });
 404}
 405todo!(editor)
 406#[gpui::test(iterations = 10)]
 407async fn test_collaborating_with_code_actions(
 408    executor: BackgroundExecutor,
 409    cx_a: &mut TestAppContext,
 410    cx_b: &mut TestAppContext,
 411) {
 412    let mut server = TestServer::start(&executor).await;
 413    let client_a = server.create_client(cx_a, "user_a").await;
 414    //
 415    let client_b = server.create_client(cx_b, "user_b").await;
 416    server
 417        .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
 418        .await;
 419    let active_call_a = cx_a.read(ActiveCall::global);
 420
 421    cx_b.update(editor::init);
 422
 423    // Set up a fake language server.
 424    let mut language = Language::new(
 425        LanguageConfig {
 426            name: "Rust".into(),
 427            path_suffixes: vec!["rs".to_string()],
 428            ..Default::default()
 429        },
 430        Some(tree_sitter_rust::language()),
 431    );
 432    let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
 433    client_a.language_registry().add(Arc::new(language));
 434
 435    client_a
 436        .fs()
 437        .insert_tree(
 438            "/a",
 439            json!({
 440                "main.rs": "mod other;\nfn main() { let foo = other::foo(); }",
 441                "other.rs": "pub fn foo() -> usize { 4 }",
 442            }),
 443        )
 444        .await;
 445    let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
 446    let project_id = active_call_a
 447        .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
 448        .await
 449        .unwrap();
 450
 451    // Join the project as client B.
 452    let project_b = client_b.build_remote_project(project_id, cx_b).await;
 453    let window_b =
 454        cx_b.add_window(|cx| Workspace::new(0, project_b.clone(), client_b.app_state.clone(), cx));
 455    let workspace_b = window_b.root(cx_b);
 456    let editor_b = workspace_b
 457        .update(cx_b, |workspace, cx| {
 458            workspace.open_path((worktree_id, "main.rs"), None, true, cx)
 459        })
 460        .await
 461        .unwrap()
 462        .downcast::<Editor>()
 463        .unwrap();
 464
 465    let mut fake_language_server = fake_language_servers.next().await.unwrap();
 466    let mut requests = fake_language_server
 467        .handle_request::<lsp::request::CodeActionRequest, _, _>(|params, _| async move {
 468            assert_eq!(
 469                params.text_document.uri,
 470                lsp::Url::from_file_path("/a/main.rs").unwrap(),
 471            );
 472            assert_eq!(params.range.start, lsp::Position::new(0, 0));
 473            assert_eq!(params.range.end, lsp::Position::new(0, 0));
 474            Ok(None)
 475        });
 476    executor.advance_clock(editor::CODE_ACTIONS_DEBOUNCE_TIMEOUT * 2);
 477    requests.next().await;
 478
 479    // Move cursor to a location that contains code actions.
 480    editor_b.update(cx_b, |editor, cx| {
 481        editor.change_selections(None, cx, |s| {
 482            s.select_ranges([Point::new(1, 31)..Point::new(1, 31)])
 483        });
 484        cx.focus(&editor_b);
 485    });
 486
 487    let mut requests = fake_language_server
 488        .handle_request::<lsp::request::CodeActionRequest, _, _>(|params, _| async move {
 489            assert_eq!(
 490                params.text_document.uri,
 491                lsp::Url::from_file_path("/a/main.rs").unwrap(),
 492            );
 493            assert_eq!(params.range.start, lsp::Position::new(1, 31));
 494            assert_eq!(params.range.end, lsp::Position::new(1, 31));
 495
 496            Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
 497                lsp::CodeAction {
 498                    title: "Inline into all callers".to_string(),
 499                    edit: Some(lsp::WorkspaceEdit {
 500                        changes: Some(
 501                            [
 502                                (
 503                                    lsp::Url::from_file_path("/a/main.rs").unwrap(),
 504                                    vec![lsp::TextEdit::new(
 505                                        lsp::Range::new(
 506                                            lsp::Position::new(1, 22),
 507                                            lsp::Position::new(1, 34),
 508                                        ),
 509                                        "4".to_string(),
 510                                    )],
 511                                ),
 512                                (
 513                                    lsp::Url::from_file_path("/a/other.rs").unwrap(),
 514                                    vec![lsp::TextEdit::new(
 515                                        lsp::Range::new(
 516                                            lsp::Position::new(0, 0),
 517                                            lsp::Position::new(0, 27),
 518                                        ),
 519                                        "".to_string(),
 520                                    )],
 521                                ),
 522                            ]
 523                            .into_iter()
 524                            .collect(),
 525                        ),
 526                        ..Default::default()
 527                    }),
 528                    data: Some(json!({
 529                        "codeActionParams": {
 530                            "range": {
 531                                "start": {"line": 1, "column": 31},
 532                                "end": {"line": 1, "column": 31},
 533                            }
 534                        }
 535                    })),
 536                    ..Default::default()
 537                },
 538            )]))
 539        });
 540    executor.advance_clock(editor::CODE_ACTIONS_DEBOUNCE_TIMEOUT * 2);
 541    requests.next().await;
 542
 543    // Toggle code actions and wait for them to display.
 544    editor_b.update(cx_b, |editor, cx| {
 545        editor.toggle_code_actions(
 546            &ToggleCodeActions {
 547                deployed_from_indicator: false,
 548            },
 549            cx,
 550        );
 551    });
 552    cx_a.foreground().run_until_parked();
 553
 554    editor_b.read_with(cx_b, |editor, _| assert!(editor.context_menu_visible()));
 555
 556    fake_language_server.remove_request_handler::<lsp::request::CodeActionRequest>();
 557
 558    // Confirming the code action will trigger a resolve request.
 559    let confirm_action = workspace_b
 560        .update(cx_b, |workspace, cx| {
 561            Editor::confirm_code_action(workspace, &ConfirmCodeAction { item_ix: Some(0) }, cx)
 562        })
 563        .unwrap();
 564    fake_language_server.handle_request::<lsp::request::CodeActionResolveRequest, _, _>(
 565        |_, _| async move {
 566            Ok(lsp::CodeAction {
 567                title: "Inline into all callers".to_string(),
 568                edit: Some(lsp::WorkspaceEdit {
 569                    changes: Some(
 570                        [
 571                            (
 572                                lsp::Url::from_file_path("/a/main.rs").unwrap(),
 573                                vec![lsp::TextEdit::new(
 574                                    lsp::Range::new(
 575                                        lsp::Position::new(1, 22),
 576                                        lsp::Position::new(1, 34),
 577                                    ),
 578                                    "4".to_string(),
 579                                )],
 580                            ),
 581                            (
 582                                lsp::Url::from_file_path("/a/other.rs").unwrap(),
 583                                vec![lsp::TextEdit::new(
 584                                    lsp::Range::new(
 585                                        lsp::Position::new(0, 0),
 586                                        lsp::Position::new(0, 27),
 587                                    ),
 588                                    "".to_string(),
 589                                )],
 590                            ),
 591                        ]
 592                        .into_iter()
 593                        .collect(),
 594                    ),
 595                    ..Default::default()
 596                }),
 597                ..Default::default()
 598            })
 599        },
 600    );
 601
 602    // After the action is confirmed, an editor containing both modified files is opened.
 603    confirm_action.await.unwrap();
 604
 605    let code_action_editor = workspace_b.read_with(cx_b, |workspace, cx| {
 606        workspace
 607            .active_item(cx)
 608            .unwrap()
 609            .downcast::<Editor>()
 610            .unwrap()
 611    });
 612    code_action_editor.update(cx_b, |editor, cx| {
 613        assert_eq!(editor.text(cx), "mod other;\nfn main() { let foo = 4; }\n");
 614        editor.undo(&Undo, cx);
 615        assert_eq!(
 616            editor.text(cx),
 617            "mod other;\nfn main() { let foo = other::foo(); }\npub fn foo() -> usize { 4 }"
 618        );
 619        editor.redo(&Redo, cx);
 620        assert_eq!(editor.text(cx), "mod other;\nfn main() { let foo = 4; }\n");
 621    });
 622}
 623
 624todo!(editor)
 625#[gpui::test(iterations = 10)]
 626async fn test_collaborating_with_renames(
 627    executor: BackgroundExecutor,
 628    cx_a: &mut TestAppContext,
 629    cx_b: &mut TestAppContext,
 630) {
 631    let mut server = TestServer::start(&executor).await;
 632    let client_a = server.create_client(cx_a, "user_a").await;
 633    let client_b = server.create_client(cx_b, "user_b").await;
 634    server
 635        .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
 636        .await;
 637    let active_call_a = cx_a.read(ActiveCall::global);
 638
 639    cx_b.update(editor::init);
 640
 641    // Set up a fake language server.
 642    let mut language = Language::new(
 643        LanguageConfig {
 644            name: "Rust".into(),
 645            path_suffixes: vec!["rs".to_string()],
 646            ..Default::default()
 647        },
 648        Some(tree_sitter_rust::language()),
 649    );
 650    let mut fake_language_servers = language
 651        .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
 652            capabilities: lsp::ServerCapabilities {
 653                rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
 654                    prepare_provider: Some(true),
 655                    work_done_progress_options: Default::default(),
 656                })),
 657                ..Default::default()
 658            },
 659            ..Default::default()
 660        }))
 661        .await;
 662    client_a.language_registry().add(Arc::new(language));
 663
 664    client_a
 665        .fs()
 666        .insert_tree(
 667            "/dir",
 668            json!({
 669                "one.rs": "const ONE: usize = 1;",
 670                "two.rs": "const TWO: usize = one::ONE + one::ONE;"
 671            }),
 672        )
 673        .await;
 674    let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
 675    let project_id = active_call_a
 676        .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
 677        .await
 678        .unwrap();
 679    let project_b = client_b.build_remote_project(project_id, cx_b).await;
 680
 681    let window_b =
 682        cx_b.add_window(|cx| Workspace::new(0, project_b.clone(), client_b.app_state.clone(), cx));
 683    let workspace_b = window_b.root(cx_b);
 684    let editor_b = workspace_b
 685        .update(cx_b, |workspace, cx| {
 686            workspace.open_path((worktree_id, "one.rs"), None, true, cx)
 687        })
 688        .await
 689        .unwrap()
 690        .downcast::<Editor>()
 691        .unwrap();
 692    let fake_language_server = fake_language_servers.next().await.unwrap();
 693
 694    // Move cursor to a location that can be renamed.
 695    let prepare_rename = editor_b.update(cx_b, |editor, cx| {
 696        editor.change_selections(None, cx, |s| s.select_ranges([7..7]));
 697        editor.rename(&Rename, cx).unwrap()
 698    });
 699
 700    fake_language_server
 701        .handle_request::<lsp::request::PrepareRenameRequest, _, _>(|params, _| async move {
 702            assert_eq!(params.text_document.uri.as_str(), "file:///dir/one.rs");
 703            assert_eq!(params.position, lsp::Position::new(0, 7));
 704            Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range::new(
 705                lsp::Position::new(0, 6),
 706                lsp::Position::new(0, 9),
 707            ))))
 708        })
 709        .next()
 710        .await
 711        .unwrap();
 712    prepare_rename.await.unwrap();
 713    editor_b.update(cx_b, |editor, cx| {
 714        use editor::ToOffset;
 715        let rename = editor.pending_rename().unwrap();
 716        let buffer = editor.buffer().read(cx).snapshot(cx);
 717        assert_eq!(
 718            rename.range.start.to_offset(&buffer)..rename.range.end.to_offset(&buffer),
 719            6..9
 720        );
 721        rename.editor.update(cx, |rename_editor, cx| {
 722            rename_editor.buffer().update(cx, |rename_buffer, cx| {
 723                rename_buffer.edit([(0..3, "THREE")], None, cx);
 724            });
 725        });
 726    });
 727
 728    let confirm_rename = workspace_b.update(cx_b, |workspace, cx| {
 729        Editor::confirm_rename(workspace, &ConfirmRename, cx).unwrap()
 730    });
 731    fake_language_server
 732        .handle_request::<lsp::request::Rename, _, _>(|params, _| async move {
 733            assert_eq!(
 734                params.text_document_position.text_document.uri.as_str(),
 735                "file:///dir/one.rs"
 736            );
 737            assert_eq!(
 738                params.text_document_position.position,
 739                lsp::Position::new(0, 6)
 740            );
 741            assert_eq!(params.new_name, "THREE");
 742            Ok(Some(lsp::WorkspaceEdit {
 743                changes: Some(
 744                    [
 745                        (
 746                            lsp::Url::from_file_path("/dir/one.rs").unwrap(),
 747                            vec![lsp::TextEdit::new(
 748                                lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
 749                                "THREE".to_string(),
 750                            )],
 751                        ),
 752                        (
 753                            lsp::Url::from_file_path("/dir/two.rs").unwrap(),
 754                            vec![
 755                                lsp::TextEdit::new(
 756                                    lsp::Range::new(
 757                                        lsp::Position::new(0, 24),
 758                                        lsp::Position::new(0, 27),
 759                                    ),
 760                                    "THREE".to_string(),
 761                                ),
 762                                lsp::TextEdit::new(
 763                                    lsp::Range::new(
 764                                        lsp::Position::new(0, 35),
 765                                        lsp::Position::new(0, 38),
 766                                    ),
 767                                    "THREE".to_string(),
 768                                ),
 769                            ],
 770                        ),
 771                    ]
 772                    .into_iter()
 773                    .collect(),
 774                ),
 775                ..Default::default()
 776            }))
 777        })
 778        .next()
 779        .await
 780        .unwrap();
 781    confirm_rename.await.unwrap();
 782
 783    let rename_editor = workspace_b.read_with(cx_b, |workspace, cx| {
 784        workspace
 785            .active_item(cx)
 786            .unwrap()
 787            .downcast::<Editor>()
 788            .unwrap()
 789    });
 790    rename_editor.update(cx_b, |editor, cx| {
 791        assert_eq!(
 792            editor.text(cx),
 793            "const THREE: usize = 1;\nconst TWO: usize = one::THREE + one::THREE;"
 794        );
 795        editor.undo(&Undo, cx);
 796        assert_eq!(
 797            editor.text(cx),
 798            "const ONE: usize = 1;\nconst TWO: usize = one::ONE + one::ONE;"
 799        );
 800        editor.redo(&Redo, cx);
 801        assert_eq!(
 802            editor.text(cx),
 803            "const THREE: usize = 1;\nconst TWO: usize = one::THREE + one::THREE;"
 804        );
 805    });
 806
 807    // Ensure temporary rename edits cannot be undone/redone.
 808    editor_b.update(cx_b, |editor, cx| {
 809        editor.undo(&Undo, cx);
 810        assert_eq!(editor.text(cx), "const ONE: usize = 1;");
 811        editor.undo(&Undo, cx);
 812        assert_eq!(editor.text(cx), "const ONE: usize = 1;");
 813        editor.redo(&Redo, cx);
 814        assert_eq!(editor.text(cx), "const THREE: usize = 1;");
 815    })
 816}
 817
 818todo!(editor)
 819#[gpui::test(iterations = 10)]
 820async fn test_language_server_statuses(
 821    executor: BackgroundExecutor,
 822    cx_a: &mut TestAppContext,
 823    cx_b: &mut TestAppContext,
 824) {
 825    let mut server = TestServer::start(&executor).await;
 826    let client_a = server.create_client(cx_a, "user_a").await;
 827    let client_b = server.create_client(cx_b, "user_b").await;
 828    server
 829        .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
 830        .await;
 831    let active_call_a = cx_a.read(ActiveCall::global);
 832
 833    cx_b.update(editor::init);
 834
 835    // Set up a fake language server.
 836    let mut language = Language::new(
 837        LanguageConfig {
 838            name: "Rust".into(),
 839            path_suffixes: vec!["rs".to_string()],
 840            ..Default::default()
 841        },
 842        Some(tree_sitter_rust::language()),
 843    );
 844    let mut fake_language_servers = language
 845        .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
 846            name: "the-language-server",
 847            ..Default::default()
 848        }))
 849        .await;
 850    client_a.language_registry().add(Arc::new(language));
 851
 852    client_a
 853        .fs()
 854        .insert_tree(
 855            "/dir",
 856            json!({
 857                "main.rs": "const ONE: usize = 1;",
 858            }),
 859        )
 860        .await;
 861    let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
 862
 863    let _buffer_a = project_a
 864        .update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
 865        .await
 866        .unwrap();
 867
 868    let fake_language_server = fake_language_servers.next().await.unwrap();
 869    fake_language_server.start_progress("the-token").await;
 870    fake_language_server.notify::<lsp::notification::Progress>(lsp::ProgressParams {
 871        token: lsp::NumberOrString::String("the-token".to_string()),
 872        value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::Report(
 873            lsp::WorkDoneProgressReport {
 874                message: Some("the-message".to_string()),
 875                ..Default::default()
 876            },
 877        )),
 878    });
 879    executor.run_until_parked();
 880
 881    project_a.read_with(cx_a, |project, _| {
 882        let status = project.language_server_statuses().next().unwrap();
 883        assert_eq!(status.name, "the-language-server");
 884        assert_eq!(status.pending_work.len(), 1);
 885        assert_eq!(
 886            status.pending_work["the-token"].message.as_ref().unwrap(),
 887            "the-message"
 888        );
 889    });
 890
 891    let project_id = active_call_a
 892        .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
 893        .await
 894        .unwrap();
 895    executor.run_until_parked();
 896    let project_b = client_b.build_remote_project(project_id, cx_b).await;
 897
 898    project_b.read_with(cx_b, |project, _| {
 899        let status = project.language_server_statuses().next().unwrap();
 900        assert_eq!(status.name, "the-language-server");
 901    });
 902
 903    fake_language_server.notify::<lsp::notification::Progress>(lsp::ProgressParams {
 904        token: lsp::NumberOrString::String("the-token".to_string()),
 905        value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::Report(
 906            lsp::WorkDoneProgressReport {
 907                message: Some("the-message-2".to_string()),
 908                ..Default::default()
 909            },
 910        )),
 911    });
 912    executor.run_until_parked();
 913
 914    project_a.read_with(cx_a, |project, _| {
 915        let status = project.language_server_statuses().next().unwrap();
 916        assert_eq!(status.name, "the-language-server");
 917        assert_eq!(status.pending_work.len(), 1);
 918        assert_eq!(
 919            status.pending_work["the-token"].message.as_ref().unwrap(),
 920            "the-message-2"
 921        );
 922    });
 923
 924    project_b.read_with(cx_b, |project, _| {
 925        let status = project.language_server_statuses().next().unwrap();
 926        assert_eq!(status.name, "the-language-server");
 927        assert_eq!(status.pending_work.len(), 1);
 928        assert_eq!(
 929            status.pending_work["the-token"].message.as_ref().unwrap(),
 930            "the-message-2"
 931        );
 932    });
 933}
 934
 935#[gpui::test(iterations = 10)]
 936async fn test_share_project(
 937    executor: BackgroundExecutor,
 938    cx_a: &mut TestAppContext,
 939    cx_b: &mut TestAppContext,
 940    cx_c: &mut TestAppContext,
 941) {
 942    let window_b = cx_b.add_window(|_| EmptyView);
 943    let mut server = TestServer::start(&executor).await;
 944    let client_a = server.create_client(cx_a, "user_a").await;
 945    let client_b = server.create_client(cx_b, "user_b").await;
 946    let client_c = server.create_client(cx_c, "user_c").await;
 947    server
 948        .make_contacts(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)])
 949        .await;
 950    let active_call_a = cx_a.read(ActiveCall::global);
 951    let active_call_b = cx_b.read(ActiveCall::global);
 952    let active_call_c = cx_c.read(ActiveCall::global);
 953
 954    client_a
 955        .fs()
 956        .insert_tree(
 957            "/a",
 958            json!({
 959                ".gitignore": "ignored-dir",
 960                "a.txt": "a-contents",
 961                "b.txt": "b-contents",
 962                "ignored-dir": {
 963                    "c.txt": "",
 964                    "d.txt": "",
 965                }
 966            }),
 967        )
 968        .await;
 969
 970    // Invite client B to collaborate on a project
 971    let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
 972    active_call_a
 973        .update(cx_a, |call, cx| {
 974            call.invite(client_b.user_id().unwrap(), Some(project_a.clone()), cx)
 975        })
 976        .await
 977        .unwrap();
 978
 979    // Join that project as client B
 980
 981    let incoming_call_b = active_call_b.read_with(cx_b, |call, _| call.incoming());
 982    executor.run_until_parked();
 983    let call = incoming_call_b.borrow().clone().unwrap();
 984    assert_eq!(call.calling_user.github_login, "user_a");
 985    let initial_project = call.initial_project.unwrap();
 986    active_call_b
 987        .update(cx_b, |call, cx| call.accept_incoming(cx))
 988        .await
 989        .unwrap();
 990    let client_b_peer_id = client_b.peer_id().unwrap();
 991    let project_b = client_b
 992        .build_remote_project(initial_project.id, cx_b)
 993        .await;
 994
 995    let replica_id_b = project_b.read_with(cx_b, |project, _| project.replica_id());
 996
 997    executor.run_until_parked();
 998
 999    project_a.read_with(cx_a, |project, _| {
1000        let client_b_collaborator = project.collaborators().get(&client_b_peer_id).unwrap();
1001        assert_eq!(client_b_collaborator.replica_id, replica_id_b);
1002    });
1003
1004    project_b.read_with(cx_b, |project, cx| {
1005        let worktree = project.worktrees().next().unwrap().read(cx);
1006        assert_eq!(
1007            worktree.paths().map(AsRef::as_ref).collect::<Vec<_>>(),
1008            [
1009                Path::new(".gitignore"),
1010                Path::new("a.txt"),
1011                Path::new("b.txt"),
1012                Path::new("ignored-dir"),
1013            ]
1014        );
1015    });
1016
1017    project_b
1018        .update(cx_b, |project, cx| {
1019            let worktree = project.worktrees().next().unwrap();
1020            let entry = worktree.read(cx).entry_for_path("ignored-dir").unwrap();
1021            project.expand_entry(worktree_id, entry.id, cx).unwrap()
1022        })
1023        .await
1024        .unwrap();
1025
1026    project_b.read_with(cx_b, |project, cx| {
1027        let worktree = project.worktrees().next().unwrap().read(cx);
1028        assert_eq!(
1029            worktree.paths().map(AsRef::as_ref).collect::<Vec<_>>(),
1030            [
1031                Path::new(".gitignore"),
1032                Path::new("a.txt"),
1033                Path::new("b.txt"),
1034                Path::new("ignored-dir"),
1035                Path::new("ignored-dir/c.txt"),
1036                Path::new("ignored-dir/d.txt"),
1037            ]
1038        );
1039    });
1040
1041    // Open the same file as client B and client A.
1042    let buffer_b = project_b
1043        .update(cx_b, |p, cx| p.open_buffer((worktree_id, "b.txt"), cx))
1044        .await
1045        .unwrap();
1046
1047    buffer_b.read_with(cx_b, |buf, _| assert_eq!(buf.text(), "b-contents"));
1048
1049    project_a.read_with(cx_a, |project, cx| {
1050        assert!(project.has_open_buffer((worktree_id, "b.txt"), cx))
1051    });
1052    let buffer_a = project_a
1053        .update(cx_a, |p, cx| p.open_buffer((worktree_id, "b.txt"), cx))
1054        .await
1055        .unwrap();
1056
1057    let editor_b = window_b.add_view(cx_b, |cx| Editor::for_buffer(buffer_b, None, cx));
1058
1059    // Client A sees client B's selection
1060    executor.run_until_parked();
1061
1062    buffer_a.read_with(cx_a, |buffer, _| {
1063        buffer
1064            .snapshot()
1065            .remote_selections_in_range(Anchor::MIN..Anchor::MAX)
1066            .count()
1067            == 1
1068    });
1069
1070    // Edit the buffer as client B and see that edit as client A.
1071    editor_b.update(cx_b, |editor, cx| editor.handle_input("ok, ", cx));
1072    executor.run_until_parked();
1073
1074    buffer_a.read_with(cx_a, |buffer, _| {
1075        assert_eq!(buffer.text(), "ok, b-contents")
1076    });
1077
1078    // Client B can invite client C on a project shared by client A.
1079    active_call_b
1080        .update(cx_b, |call, cx| {
1081            call.invite(client_c.user_id().unwrap(), Some(project_b.clone()), cx)
1082        })
1083        .await
1084        .unwrap();
1085
1086    let incoming_call_c = active_call_c.read_with(cx_c, |call, _| call.incoming());
1087    executor.run_until_parked();
1088    let call = incoming_call_c.borrow().clone().unwrap();
1089    assert_eq!(call.calling_user.github_login, "user_b");
1090    let initial_project = call.initial_project.unwrap();
1091    active_call_c
1092        .update(cx_c, |call, cx| call.accept_incoming(cx))
1093        .await
1094        .unwrap();
1095    let _project_c = client_c
1096        .build_remote_project(initial_project.id, cx_c)
1097        .await;
1098
1099    // Client B closes the editor, and client A sees client B's selections removed.
1100    cx_b.update(move |_| drop(editor_b));
1101    executor.run_until_parked();
1102
1103    buffer_a.read_with(cx_a, |buffer, _| {
1104        buffer
1105            .snapshot()
1106            .remote_selections_in_range(Anchor::MIN..Anchor::MAX)
1107            .count()
1108            == 0
1109    });
1110}