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 workspace_b =
  52        cx_b.add_window(|cx| Workspace::new(0, project_b.clone(), client_b.app_state.clone(), cx));
  53
  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
 125#[gpui::test]
 126async fn test_newline_above_or_below_does_not_move_guest_cursor(
 127    executor: BackgroundExecutor,
 128    cx_a: &mut TestAppContext,
 129    cx_b: &mut TestAppContext,
 130) {
 131    let mut server = TestServer::start(&executor).await;
 132    let client_a = server.create_client(cx_a, "user_a").await;
 133    let client_b = server.create_client(cx_b, "user_b").await;
 134    server
 135        .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
 136        .await;
 137    let active_call_a = cx_a.read(ActiveCall::global);
 138
 139    client_a
 140        .fs()
 141        .insert_tree("/dir", json!({ "a.txt": "Some text\n" }))
 142        .await;
 143    let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
 144    let project_id = active_call_a
 145        .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
 146        .await
 147        .unwrap();
 148
 149    let project_b = client_b.build_remote_project(project_id, cx_b).await;
 150
 151    // Open a buffer as client A
 152    let buffer_a = project_a
 153        .update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
 154        .await
 155        .unwrap();
 156    let window_a = cx_a.add_window(|_| EmptyView);
 157    let editor_a = window_a.add_view(cx_a, |cx| Editor::for_buffer(buffer_a, Some(project_a), cx));
 158    let mut editor_cx_a = EditorTestContext {
 159        cx: cx_a,
 160        window: window_a.into(),
 161        editor: editor_a,
 162    };
 163
 164    // Open a buffer as client B
 165    let buffer_b = project_b
 166        .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
 167        .await
 168        .unwrap();
 169    let window_b = cx_b.add_window(|_| EmptyView);
 170    let editor_b = window_b.add_view(cx_b, |cx| Editor::for_buffer(buffer_b, Some(project_b), cx));
 171    let mut editor_cx_b = EditorTestContext {
 172        cx: cx_b,
 173        window: window_b.into(),
 174        editor: editor_b,
 175    };
 176
 177    // Test newline above
 178    editor_cx_a.set_selections_state(indoc! {"
 179        Some textˇ
 180    "});
 181    editor_cx_b.set_selections_state(indoc! {"
 182        Some textˇ
 183    "});
 184    editor_cx_a.update_editor(|editor, cx| editor.newline_above(&editor::NewlineAbove, cx));
 185    executor.run_until_parked();
 186    editor_cx_a.assert_editor_state(indoc! {"
 187        ˇ
 188        Some text
 189    "});
 190    editor_cx_b.assert_editor_state(indoc! {"
 191
 192        Some textˇ
 193    "});
 194
 195    // Test newline below
 196    editor_cx_a.set_selections_state(indoc! {"
 197
 198        Some textˇ
 199    "});
 200    editor_cx_b.set_selections_state(indoc! {"
 201
 202        Some textˇ
 203    "});
 204    editor_cx_a.update_editor(|editor, cx| editor.newline_below(&editor::NewlineBelow, cx));
 205    executor.run_until_parked();
 206    editor_cx_a.assert_editor_state(indoc! {"
 207
 208        Some text
 209        ˇ
 210    "});
 211    editor_cx_b.assert_editor_state(indoc! {"
 212
 213        Some textˇ
 214
 215    "});
 216}
 217
 218#[gpui::test(iterations = 10)]
 219async fn test_collaborating_with_completion(
 220    executor: BackgroundExecutor,
 221    cx_a: &mut TestAppContext,
 222    cx_b: &mut TestAppContext,
 223) {
 224    let mut server = TestServer::start(&executor).await;
 225    let client_a = server.create_client(cx_a, "user_a").await;
 226    let client_b = server.create_client(cx_b, "user_b").await;
 227    server
 228        .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
 229        .await;
 230    let active_call_a = cx_a.read(ActiveCall::global);
 231
 232    // Set up a fake language server.
 233    let mut language = Language::new(
 234        LanguageConfig {
 235            name: "Rust".into(),
 236            path_suffixes: vec!["rs".to_string()],
 237            ..Default::default()
 238        },
 239        Some(tree_sitter_rust::language()),
 240    );
 241    let mut fake_language_servers = language
 242        .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
 243            capabilities: lsp::ServerCapabilities {
 244                completion_provider: Some(lsp::CompletionOptions {
 245                    trigger_characters: Some(vec![".".to_string()]),
 246                    resolve_provider: Some(true),
 247                    ..Default::default()
 248                }),
 249                ..Default::default()
 250            },
 251            ..Default::default()
 252        }))
 253        .await;
 254    client_a.language_registry().add(Arc::new(language));
 255
 256    client_a
 257        .fs()
 258        .insert_tree(
 259            "/a",
 260            json!({
 261                "main.rs": "fn main() { a }",
 262                "other.rs": "",
 263            }),
 264        )
 265        .await;
 266    let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
 267    let project_id = active_call_a
 268        .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
 269        .await
 270        .unwrap();
 271    let project_b = client_b.build_remote_project(project_id, cx_b).await;
 272
 273    // Open a file in an editor as the guest.
 274    let buffer_b = project_b
 275        .update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
 276        .await
 277        .unwrap();
 278    let window_b = cx_b.add_window(|_| EmptyView);
 279    let editor_b = window_b.add_view(cx_b, |cx| {
 280        Editor::for_buffer(buffer_b.clone(), Some(project_b.clone()), cx)
 281    });
 282
 283    let fake_language_server = fake_language_servers.next().await.unwrap();
 284    cx_a.foreground().run_until_parked();
 285
 286    buffer_b.read_with(cx_b, |buffer, _| {
 287        assert!(!buffer.completion_triggers().is_empty())
 288    });
 289
 290    // Type a completion trigger character as the guest.
 291    editor_b.update(cx_b, |editor, cx| {
 292        editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
 293        editor.handle_input(".", cx);
 294        cx.focus(&editor_b);
 295    });
 296
 297    // Receive a completion request as the host's language server.
 298    // Return some completions from the host's language server.
 299    cx_a.foreground().start_waiting();
 300    fake_language_server
 301        .handle_request::<lsp::request::Completion, _, _>(|params, _| async move {
 302            assert_eq!(
 303                params.text_document_position.text_document.uri,
 304                lsp::Url::from_file_path("/a/main.rs").unwrap(),
 305            );
 306            assert_eq!(
 307                params.text_document_position.position,
 308                lsp::Position::new(0, 14),
 309            );
 310
 311            Ok(Some(lsp::CompletionResponse::Array(vec![
 312                lsp::CompletionItem {
 313                    label: "first_method(…)".into(),
 314                    detail: Some("fn(&mut self, B) -> C".into()),
 315                    text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
 316                        new_text: "first_method($1)".to_string(),
 317                        range: lsp::Range::new(
 318                            lsp::Position::new(0, 14),
 319                            lsp::Position::new(0, 14),
 320                        ),
 321                    })),
 322                    insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
 323                    ..Default::default()
 324                },
 325                lsp::CompletionItem {
 326                    label: "second_method(…)".into(),
 327                    detail: Some("fn(&mut self, C) -> D<E>".into()),
 328                    text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
 329                        new_text: "second_method()".to_string(),
 330                        range: lsp::Range::new(
 331                            lsp::Position::new(0, 14),
 332                            lsp::Position::new(0, 14),
 333                        ),
 334                    })),
 335                    insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
 336                    ..Default::default()
 337                },
 338            ])))
 339        })
 340        .next()
 341        .await
 342        .unwrap();
 343    cx_a.foreground().finish_waiting();
 344
 345    // Open the buffer on the host.
 346    let buffer_a = project_a
 347        .update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
 348        .await
 349        .unwrap();
 350    cx_a.foreground().run_until_parked();
 351
 352    buffer_a.read_with(cx_a, |buffer, _| {
 353        assert_eq!(buffer.text(), "fn main() { a. }")
 354    });
 355
 356    // Confirm a completion on the guest.
 357
 358    editor_b.read_with(cx_b, |editor, _| assert!(editor.context_menu_visible()));
 359    editor_b.update(cx_b, |editor, cx| {
 360        editor.confirm_completion(&ConfirmCompletion { item_ix: Some(0) }, cx);
 361        assert_eq!(editor.text(cx), "fn main() { a.first_method() }");
 362    });
 363
 364    // Return a resolved completion from the host's language server.
 365    // The resolved completion has an additional text edit.
 366    fake_language_server.handle_request::<lsp::request::ResolveCompletionItem, _, _>(
 367        |params, _| async move {
 368            assert_eq!(params.label, "first_method(…)");
 369            Ok(lsp::CompletionItem {
 370                label: "first_method(…)".into(),
 371                detail: Some("fn(&mut self, B) -> C".into()),
 372                text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
 373                    new_text: "first_method($1)".to_string(),
 374                    range: lsp::Range::new(lsp::Position::new(0, 14), lsp::Position::new(0, 14)),
 375                })),
 376                additional_text_edits: Some(vec![lsp::TextEdit {
 377                    new_text: "use d::SomeTrait;\n".to_string(),
 378                    range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
 379                }]),
 380                insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
 381                ..Default::default()
 382            })
 383        },
 384    );
 385
 386    // The additional edit is applied.
 387    cx_a.foreground().run_until_parked();
 388
 389    buffer_a.read_with(cx_a, |buffer, _| {
 390        assert_eq!(
 391            buffer.text(),
 392            "use d::SomeTrait;\nfn main() { a.first_method() }"
 393        );
 394    });
 395
 396    buffer_b.read_with(cx_b, |buffer, _| {
 397        assert_eq!(
 398            buffer.text(),
 399            "use d::SomeTrait;\nfn main() { a.first_method() }"
 400        );
 401    });
 402}
 403
 404#[gpui::test(iterations = 10)]
 405async fn test_collaborating_with_code_actions(
 406    executor: BackgroundExecutor,
 407    cx_a: &mut TestAppContext,
 408    cx_b: &mut TestAppContext,
 409) {
 410    let mut server = TestServer::start(&executor).await;
 411    let client_a = server.create_client(cx_a, "user_a").await;
 412    //
 413    let client_b = server.create_client(cx_b, "user_b").await;
 414    server
 415        .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
 416        .await;
 417    let active_call_a = cx_a.read(ActiveCall::global);
 418
 419    cx_b.update(editor::init);
 420
 421    // Set up a fake language server.
 422    let mut language = Language::new(
 423        LanguageConfig {
 424            name: "Rust".into(),
 425            path_suffixes: vec!["rs".to_string()],
 426            ..Default::default()
 427        },
 428        Some(tree_sitter_rust::language()),
 429    );
 430    let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
 431    client_a.language_registry().add(Arc::new(language));
 432
 433    client_a
 434        .fs()
 435        .insert_tree(
 436            "/a",
 437            json!({
 438                "main.rs": "mod other;\nfn main() { let foo = other::foo(); }",
 439                "other.rs": "pub fn foo() -> usize { 4 }",
 440            }),
 441        )
 442        .await;
 443    let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
 444    let project_id = active_call_a
 445        .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
 446        .await
 447        .unwrap();
 448
 449    // Join the project as client B.
 450    let project_b = client_b.build_remote_project(project_id, cx_b).await;
 451    let window_b =
 452        cx_b.add_window(|cx| Workspace::new(0, project_b.clone(), client_b.app_state.clone(), cx));
 453    let workspace_b = window_b.root(cx_b);
 454    let editor_b = workspace_b
 455        .update(cx_b, |workspace, cx| {
 456            workspace.open_path((worktree_id, "main.rs"), None, true, cx)
 457        })
 458        .await
 459        .unwrap()
 460        .downcast::<Editor>()
 461        .unwrap();
 462
 463    let mut fake_language_server = fake_language_servers.next().await.unwrap();
 464    let mut requests = fake_language_server
 465        .handle_request::<lsp::request::CodeActionRequest, _, _>(|params, _| async move {
 466            assert_eq!(
 467                params.text_document.uri,
 468                lsp::Url::from_file_path("/a/main.rs").unwrap(),
 469            );
 470            assert_eq!(params.range.start, lsp::Position::new(0, 0));
 471            assert_eq!(params.range.end, lsp::Position::new(0, 0));
 472            Ok(None)
 473        });
 474    executor.advance_clock(editor::CODE_ACTIONS_DEBOUNCE_TIMEOUT * 2);
 475    requests.next().await;
 476
 477    // Move cursor to a location that contains code actions.
 478    editor_b.update(cx_b, |editor, cx| {
 479        editor.change_selections(None, cx, |s| {
 480            s.select_ranges([Point::new(1, 31)..Point::new(1, 31)])
 481        });
 482        cx.focus(&editor_b);
 483    });
 484
 485    let mut requests = fake_language_server
 486        .handle_request::<lsp::request::CodeActionRequest, _, _>(|params, _| async move {
 487            assert_eq!(
 488                params.text_document.uri,
 489                lsp::Url::from_file_path("/a/main.rs").unwrap(),
 490            );
 491            assert_eq!(params.range.start, lsp::Position::new(1, 31));
 492            assert_eq!(params.range.end, lsp::Position::new(1, 31));
 493
 494            Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
 495                lsp::CodeAction {
 496                    title: "Inline into all callers".to_string(),
 497                    edit: Some(lsp::WorkspaceEdit {
 498                        changes: Some(
 499                            [
 500                                (
 501                                    lsp::Url::from_file_path("/a/main.rs").unwrap(),
 502                                    vec![lsp::TextEdit::new(
 503                                        lsp::Range::new(
 504                                            lsp::Position::new(1, 22),
 505                                            lsp::Position::new(1, 34),
 506                                        ),
 507                                        "4".to_string(),
 508                                    )],
 509                                ),
 510                                (
 511                                    lsp::Url::from_file_path("/a/other.rs").unwrap(),
 512                                    vec![lsp::TextEdit::new(
 513                                        lsp::Range::new(
 514                                            lsp::Position::new(0, 0),
 515                                            lsp::Position::new(0, 27),
 516                                        ),
 517                                        "".to_string(),
 518                                    )],
 519                                ),
 520                            ]
 521                            .into_iter()
 522                            .collect(),
 523                        ),
 524                        ..Default::default()
 525                    }),
 526                    data: Some(json!({
 527                        "codeActionParams": {
 528                            "range": {
 529                                "start": {"line": 1, "column": 31},
 530                                "end": {"line": 1, "column": 31},
 531                            }
 532                        }
 533                    })),
 534                    ..Default::default()
 535                },
 536            )]))
 537        });
 538    executor.advance_clock(editor::CODE_ACTIONS_DEBOUNCE_TIMEOUT * 2);
 539    requests.next().await;
 540
 541    // Toggle code actions and wait for them to display.
 542    editor_b.update(cx_b, |editor, cx| {
 543        editor.toggle_code_actions(
 544            &ToggleCodeActions {
 545                deployed_from_indicator: false,
 546            },
 547            cx,
 548        );
 549    });
 550    cx_a.foreground().run_until_parked();
 551
 552    editor_b.read_with(cx_b, |editor, _| assert!(editor.context_menu_visible()));
 553
 554    fake_language_server.remove_request_handler::<lsp::request::CodeActionRequest>();
 555
 556    // Confirming the code action will trigger a resolve request.
 557    let confirm_action = workspace_b
 558        .update(cx_b, |workspace, cx| {
 559            Editor::confirm_code_action(workspace, &ConfirmCodeAction { item_ix: Some(0) }, cx)
 560        })
 561        .unwrap();
 562    fake_language_server.handle_request::<lsp::request::CodeActionResolveRequest, _, _>(
 563        |_, _| async move {
 564            Ok(lsp::CodeAction {
 565                title: "Inline into all callers".to_string(),
 566                edit: Some(lsp::WorkspaceEdit {
 567                    changes: Some(
 568                        [
 569                            (
 570                                lsp::Url::from_file_path("/a/main.rs").unwrap(),
 571                                vec![lsp::TextEdit::new(
 572                                    lsp::Range::new(
 573                                        lsp::Position::new(1, 22),
 574                                        lsp::Position::new(1, 34),
 575                                    ),
 576                                    "4".to_string(),
 577                                )],
 578                            ),
 579                            (
 580                                lsp::Url::from_file_path("/a/other.rs").unwrap(),
 581                                vec![lsp::TextEdit::new(
 582                                    lsp::Range::new(
 583                                        lsp::Position::new(0, 0),
 584                                        lsp::Position::new(0, 27),
 585                                    ),
 586                                    "".to_string(),
 587                                )],
 588                            ),
 589                        ]
 590                        .into_iter()
 591                        .collect(),
 592                    ),
 593                    ..Default::default()
 594                }),
 595                ..Default::default()
 596            })
 597        },
 598    );
 599
 600    // After the action is confirmed, an editor containing both modified files is opened.
 601    confirm_action.await.unwrap();
 602
 603    let code_action_editor = workspace_b.read_with(cx_b, |workspace, cx| {
 604        workspace
 605            .active_item(cx)
 606            .unwrap()
 607            .downcast::<Editor>()
 608            .unwrap()
 609    });
 610    code_action_editor.update(cx_b, |editor, cx| {
 611        assert_eq!(editor.text(cx), "mod other;\nfn main() { let foo = 4; }\n");
 612        editor.undo(&Undo, cx);
 613        assert_eq!(
 614            editor.text(cx),
 615            "mod other;\nfn main() { let foo = other::foo(); }\npub fn foo() -> usize { 4 }"
 616        );
 617        editor.redo(&Redo, cx);
 618        assert_eq!(editor.text(cx), "mod other;\nfn main() { let foo = 4; }\n");
 619    });
 620}
 621
 622#[gpui::test(iterations = 10)]
 623async fn test_collaborating_with_renames(
 624    executor: BackgroundExecutor,
 625    cx_a: &mut TestAppContext,
 626    cx_b: &mut TestAppContext,
 627) {
 628    let mut server = TestServer::start(&executor).await;
 629    let client_a = server.create_client(cx_a, "user_a").await;
 630    let client_b = server.create_client(cx_b, "user_b").await;
 631    server
 632        .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
 633        .await;
 634    let active_call_a = cx_a.read(ActiveCall::global);
 635
 636    cx_b.update(editor::init);
 637
 638    // Set up a fake language server.
 639    let mut language = Language::new(
 640        LanguageConfig {
 641            name: "Rust".into(),
 642            path_suffixes: vec!["rs".to_string()],
 643            ..Default::default()
 644        },
 645        Some(tree_sitter_rust::language()),
 646    );
 647    let mut fake_language_servers = language
 648        .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
 649            capabilities: lsp::ServerCapabilities {
 650                rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
 651                    prepare_provider: Some(true),
 652                    work_done_progress_options: Default::default(),
 653                })),
 654                ..Default::default()
 655            },
 656            ..Default::default()
 657        }))
 658        .await;
 659    client_a.language_registry().add(Arc::new(language));
 660
 661    client_a
 662        .fs()
 663        .insert_tree(
 664            "/dir",
 665            json!({
 666                "one.rs": "const ONE: usize = 1;",
 667                "two.rs": "const TWO: usize = one::ONE + one::ONE;"
 668            }),
 669        )
 670        .await;
 671    let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
 672    let project_id = active_call_a
 673        .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
 674        .await
 675        .unwrap();
 676    let project_b = client_b.build_remote_project(project_id, cx_b).await;
 677
 678    let window_b =
 679        cx_b.add_window(|cx| Workspace::new(0, project_b.clone(), client_b.app_state.clone(), cx));
 680    let workspace_b = window_b.root(cx_b);
 681    let editor_b = workspace_b
 682        .update(cx_b, |workspace, cx| {
 683            workspace.open_path((worktree_id, "one.rs"), None, true, cx)
 684        })
 685        .await
 686        .unwrap()
 687        .downcast::<Editor>()
 688        .unwrap();
 689    let fake_language_server = fake_language_servers.next().await.unwrap();
 690
 691    // Move cursor to a location that can be renamed.
 692    let prepare_rename = editor_b.update(cx_b, |editor, cx| {
 693        editor.change_selections(None, cx, |s| s.select_ranges([7..7]));
 694        editor.rename(&Rename, cx).unwrap()
 695    });
 696
 697    fake_language_server
 698        .handle_request::<lsp::request::PrepareRenameRequest, _, _>(|params, _| async move {
 699            assert_eq!(params.text_document.uri.as_str(), "file:///dir/one.rs");
 700            assert_eq!(params.position, lsp::Position::new(0, 7));
 701            Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range::new(
 702                lsp::Position::new(0, 6),
 703                lsp::Position::new(0, 9),
 704            ))))
 705        })
 706        .next()
 707        .await
 708        .unwrap();
 709    prepare_rename.await.unwrap();
 710    editor_b.update(cx_b, |editor, cx| {
 711        use editor::ToOffset;
 712        let rename = editor.pending_rename().unwrap();
 713        let buffer = editor.buffer().read(cx).snapshot(cx);
 714        assert_eq!(
 715            rename.range.start.to_offset(&buffer)..rename.range.end.to_offset(&buffer),
 716            6..9
 717        );
 718        rename.editor.update(cx, |rename_editor, cx| {
 719            rename_editor.buffer().update(cx, |rename_buffer, cx| {
 720                rename_buffer.edit([(0..3, "THREE")], None, cx);
 721            });
 722        });
 723    });
 724
 725    let confirm_rename = workspace_b.update(cx_b, |workspace, cx| {
 726        Editor::confirm_rename(workspace, &ConfirmRename, cx).unwrap()
 727    });
 728    fake_language_server
 729        .handle_request::<lsp::request::Rename, _, _>(|params, _| async move {
 730            assert_eq!(
 731                params.text_document_position.text_document.uri.as_str(),
 732                "file:///dir/one.rs"
 733            );
 734            assert_eq!(
 735                params.text_document_position.position,
 736                lsp::Position::new(0, 6)
 737            );
 738            assert_eq!(params.new_name, "THREE");
 739            Ok(Some(lsp::WorkspaceEdit {
 740                changes: Some(
 741                    [
 742                        (
 743                            lsp::Url::from_file_path("/dir/one.rs").unwrap(),
 744                            vec![lsp::TextEdit::new(
 745                                lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
 746                                "THREE".to_string(),
 747                            )],
 748                        ),
 749                        (
 750                            lsp::Url::from_file_path("/dir/two.rs").unwrap(),
 751                            vec![
 752                                lsp::TextEdit::new(
 753                                    lsp::Range::new(
 754                                        lsp::Position::new(0, 24),
 755                                        lsp::Position::new(0, 27),
 756                                    ),
 757                                    "THREE".to_string(),
 758                                ),
 759                                lsp::TextEdit::new(
 760                                    lsp::Range::new(
 761                                        lsp::Position::new(0, 35),
 762                                        lsp::Position::new(0, 38),
 763                                    ),
 764                                    "THREE".to_string(),
 765                                ),
 766                            ],
 767                        ),
 768                    ]
 769                    .into_iter()
 770                    .collect(),
 771                ),
 772                ..Default::default()
 773            }))
 774        })
 775        .next()
 776        .await
 777        .unwrap();
 778    confirm_rename.await.unwrap();
 779
 780    let rename_editor = workspace_b.read_with(cx_b, |workspace, cx| {
 781        workspace
 782            .active_item(cx)
 783            .unwrap()
 784            .downcast::<Editor>()
 785            .unwrap()
 786    });
 787    rename_editor.update(cx_b, |editor, cx| {
 788        assert_eq!(
 789            editor.text(cx),
 790            "const THREE: usize = 1;\nconst TWO: usize = one::THREE + one::THREE;"
 791        );
 792        editor.undo(&Undo, cx);
 793        assert_eq!(
 794            editor.text(cx),
 795            "const ONE: usize = 1;\nconst TWO: usize = one::ONE + one::ONE;"
 796        );
 797        editor.redo(&Redo, cx);
 798        assert_eq!(
 799            editor.text(cx),
 800            "const THREE: usize = 1;\nconst TWO: usize = one::THREE + one::THREE;"
 801        );
 802    });
 803
 804    // Ensure temporary rename edits cannot be undone/redone.
 805    editor_b.update(cx_b, |editor, cx| {
 806        editor.undo(&Undo, cx);
 807        assert_eq!(editor.text(cx), "const ONE: usize = 1;");
 808        editor.undo(&Undo, cx);
 809        assert_eq!(editor.text(cx), "const ONE: usize = 1;");
 810        editor.redo(&Redo, cx);
 811        assert_eq!(editor.text(cx), "const THREE: usize = 1;");
 812    })
 813}
 814
 815#[gpui::test(iterations = 10)]
 816async fn test_language_server_statuses(
 817    executor: BackgroundExecutor,
 818    cx_a: &mut TestAppContext,
 819    cx_b: &mut TestAppContext,
 820) {
 821    let mut server = TestServer::start(&executor).await;
 822    let client_a = server.create_client(cx_a, "user_a").await;
 823    let client_b = server.create_client(cx_b, "user_b").await;
 824    server
 825        .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
 826        .await;
 827    let active_call_a = cx_a.read(ActiveCall::global);
 828
 829    cx_b.update(editor::init);
 830
 831    // Set up a fake language server.
 832    let mut language = Language::new(
 833        LanguageConfig {
 834            name: "Rust".into(),
 835            path_suffixes: vec!["rs".to_string()],
 836            ..Default::default()
 837        },
 838        Some(tree_sitter_rust::language()),
 839    );
 840    let mut fake_language_servers = language
 841        .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
 842            name: "the-language-server",
 843            ..Default::default()
 844        }))
 845        .await;
 846    client_a.language_registry().add(Arc::new(language));
 847
 848    client_a
 849        .fs()
 850        .insert_tree(
 851            "/dir",
 852            json!({
 853                "main.rs": "const ONE: usize = 1;",
 854            }),
 855        )
 856        .await;
 857    let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
 858
 859    let _buffer_a = project_a
 860        .update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
 861        .await
 862        .unwrap();
 863
 864    let fake_language_server = fake_language_servers.next().await.unwrap();
 865    fake_language_server.start_progress("the-token").await;
 866    fake_language_server.notify::<lsp::notification::Progress>(lsp::ProgressParams {
 867        token: lsp::NumberOrString::String("the-token".to_string()),
 868        value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::Report(
 869            lsp::WorkDoneProgressReport {
 870                message: Some("the-message".to_string()),
 871                ..Default::default()
 872            },
 873        )),
 874    });
 875    executor.run_until_parked();
 876
 877    project_a.read_with(cx_a, |project, _| {
 878        let status = project.language_server_statuses().next().unwrap();
 879        assert_eq!(status.name, "the-language-server");
 880        assert_eq!(status.pending_work.len(), 1);
 881        assert_eq!(
 882            status.pending_work["the-token"].message.as_ref().unwrap(),
 883            "the-message"
 884        );
 885    });
 886
 887    let project_id = active_call_a
 888        .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
 889        .await
 890        .unwrap();
 891    executor.run_until_parked();
 892    let project_b = client_b.build_remote_project(project_id, cx_b).await;
 893
 894    project_b.read_with(cx_b, |project, _| {
 895        let status = project.language_server_statuses().next().unwrap();
 896        assert_eq!(status.name, "the-language-server");
 897    });
 898
 899    fake_language_server.notify::<lsp::notification::Progress>(lsp::ProgressParams {
 900        token: lsp::NumberOrString::String("the-token".to_string()),
 901        value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::Report(
 902            lsp::WorkDoneProgressReport {
 903                message: Some("the-message-2".to_string()),
 904                ..Default::default()
 905            },
 906        )),
 907    });
 908    executor.run_until_parked();
 909
 910    project_a.read_with(cx_a, |project, _| {
 911        let status = project.language_server_statuses().next().unwrap();
 912        assert_eq!(status.name, "the-language-server");
 913        assert_eq!(status.pending_work.len(), 1);
 914        assert_eq!(
 915            status.pending_work["the-token"].message.as_ref().unwrap(),
 916            "the-message-2"
 917        );
 918    });
 919
 920    project_b.read_with(cx_b, |project, _| {
 921        let status = project.language_server_statuses().next().unwrap();
 922        assert_eq!(status.name, "the-language-server");
 923        assert_eq!(status.pending_work.len(), 1);
 924        assert_eq!(
 925            status.pending_work["the-token"].message.as_ref().unwrap(),
 926            "the-message-2"
 927        );
 928    });
 929}
 930
 931#[gpui::test(iterations = 10)]
 932async fn test_share_project(
 933    executor: BackgroundExecutor,
 934    cx_a: &mut TestAppContext,
 935    cx_b: &mut TestAppContext,
 936    cx_c: &mut TestAppContext,
 937) {
 938    let window_b = cx_b.add_window(|_| EmptyView);
 939    let mut server = TestServer::start(&executor).await;
 940    let client_a = server.create_client(cx_a, "user_a").await;
 941    let client_b = server.create_client(cx_b, "user_b").await;
 942    let client_c = server.create_client(cx_c, "user_c").await;
 943    server
 944        .make_contacts(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)])
 945        .await;
 946    let active_call_a = cx_a.read(ActiveCall::global);
 947    let active_call_b = cx_b.read(ActiveCall::global);
 948    let active_call_c = cx_c.read(ActiveCall::global);
 949
 950    client_a
 951        .fs()
 952        .insert_tree(
 953            "/a",
 954            json!({
 955                ".gitignore": "ignored-dir",
 956                "a.txt": "a-contents",
 957                "b.txt": "b-contents",
 958                "ignored-dir": {
 959                    "c.txt": "",
 960                    "d.txt": "",
 961                }
 962            }),
 963        )
 964        .await;
 965
 966    // Invite client B to collaborate on a project
 967    let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
 968    active_call_a
 969        .update(cx_a, |call, cx| {
 970            call.invite(client_b.user_id().unwrap(), Some(project_a.clone()), cx)
 971        })
 972        .await
 973        .unwrap();
 974
 975    // Join that project as client B
 976
 977    let incoming_call_b = active_call_b.read_with(cx_b, |call, _| call.incoming());
 978    executor.run_until_parked();
 979    let call = incoming_call_b.borrow().clone().unwrap();
 980    assert_eq!(call.calling_user.github_login, "user_a");
 981    let initial_project = call.initial_project.unwrap();
 982    active_call_b
 983        .update(cx_b, |call, cx| call.accept_incoming(cx))
 984        .await
 985        .unwrap();
 986    let client_b_peer_id = client_b.peer_id().unwrap();
 987    let project_b = client_b
 988        .build_remote_project(initial_project.id, cx_b)
 989        .await;
 990
 991    let replica_id_b = project_b.read_with(cx_b, |project, _| project.replica_id());
 992
 993    executor.run_until_parked();
 994
 995    project_a.read_with(cx_a, |project, _| {
 996        let client_b_collaborator = project.collaborators().get(&client_b_peer_id).unwrap();
 997        assert_eq!(client_b_collaborator.replica_id, replica_id_b);
 998    });
 999
1000    project_b.read_with(cx_b, |project, cx| {
1001        let worktree = project.worktrees().next().unwrap().read(cx);
1002        assert_eq!(
1003            worktree.paths().map(AsRef::as_ref).collect::<Vec<_>>(),
1004            [
1005                Path::new(".gitignore"),
1006                Path::new("a.txt"),
1007                Path::new("b.txt"),
1008                Path::new("ignored-dir"),
1009            ]
1010        );
1011    });
1012
1013    project_b
1014        .update(cx_b, |project, cx| {
1015            let worktree = project.worktrees().next().unwrap();
1016            let entry = worktree.read(cx).entry_for_path("ignored-dir").unwrap();
1017            project.expand_entry(worktree_id, entry.id, cx).unwrap()
1018        })
1019        .await
1020        .unwrap();
1021
1022    project_b.read_with(cx_b, |project, cx| {
1023        let worktree = project.worktrees().next().unwrap().read(cx);
1024        assert_eq!(
1025            worktree.paths().map(AsRef::as_ref).collect::<Vec<_>>(),
1026            [
1027                Path::new(".gitignore"),
1028                Path::new("a.txt"),
1029                Path::new("b.txt"),
1030                Path::new("ignored-dir"),
1031                Path::new("ignored-dir/c.txt"),
1032                Path::new("ignored-dir/d.txt"),
1033            ]
1034        );
1035    });
1036
1037    // Open the same file as client B and client A.
1038    let buffer_b = project_b
1039        .update(cx_b, |p, cx| p.open_buffer((worktree_id, "b.txt"), cx))
1040        .await
1041        .unwrap();
1042
1043    buffer_b.read_with(cx_b, |buf, _| assert_eq!(buf.text(), "b-contents"));
1044
1045    project_a.read_with(cx_a, |project, cx| {
1046        assert!(project.has_open_buffer((worktree_id, "b.txt"), cx))
1047    });
1048    let buffer_a = project_a
1049        .update(cx_a, |p, cx| p.open_buffer((worktree_id, "b.txt"), cx))
1050        .await
1051        .unwrap();
1052
1053    let editor_b = window_b.add_view(cx_b, |cx| Editor::for_buffer(buffer_b, None, cx));
1054
1055    // Client A sees client B's selection
1056    executor.run_until_parked();
1057
1058    buffer_a.read_with(cx_a, |buffer, _| {
1059        buffer
1060            .snapshot()
1061            .remote_selections_in_range(Anchor::MIN..Anchor::MAX)
1062            .count()
1063            == 1
1064    });
1065
1066    // Edit the buffer as client B and see that edit as client A.
1067    editor_b.update(cx_b, |editor, cx| editor.handle_input("ok, ", cx));
1068    executor.run_until_parked();
1069
1070    buffer_a.read_with(cx_a, |buffer, _| {
1071        assert_eq!(buffer.text(), "ok, b-contents")
1072    });
1073
1074    // Client B can invite client C on a project shared by client A.
1075    active_call_b
1076        .update(cx_b, |call, cx| {
1077            call.invite(client_c.user_id().unwrap(), Some(project_b.clone()), cx)
1078        })
1079        .await
1080        .unwrap();
1081
1082    let incoming_call_c = active_call_c.read_with(cx_c, |call, _| call.incoming());
1083    executor.run_until_parked();
1084    let call = incoming_call_c.borrow().clone().unwrap();
1085    assert_eq!(call.calling_user.github_login, "user_b");
1086    let initial_project = call.initial_project.unwrap();
1087    active_call_c
1088        .update(cx_c, |call, cx| call.accept_incoming(cx))
1089        .await
1090        .unwrap();
1091    let _project_c = client_c
1092        .build_remote_project(initial_project.id, cx_c)
1093        .await;
1094
1095    // Client B closes the editor, and client A sees client B's selections removed.
1096    cx_b.update(move |_| drop(editor_b));
1097    executor.run_until_parked();
1098
1099    buffer_a.read_with(cx_a, |buffer, _| {
1100        buffer
1101            .snapshot()
1102            .remote_selections_in_range(Anchor::MIN..Anchor::MAX)
1103            .count()
1104            == 0
1105    });
1106}
1107
1108#[gpui::test(iterations = 10)]
1109async fn test_on_input_format_from_host_to_guest(
1110    executor: BackgroundExecutor,
1111    cx_a: &mut TestAppContext,
1112    cx_b: &mut TestAppContext,
1113) {
1114    let mut server = TestServer::start(&executor).await;
1115    let client_a = server.create_client(cx_a, "user_a").await;
1116    let client_b = server.create_client(cx_b, "user_b").await;
1117    server
1118        .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
1119        .await;
1120    let active_call_a = cx_a.read(ActiveCall::global);
1121
1122    // Set up a fake language server.
1123    let mut language = Language::new(
1124        LanguageConfig {
1125            name: "Rust".into(),
1126            path_suffixes: vec!["rs".to_string()],
1127            ..Default::default()
1128        },
1129        Some(tree_sitter_rust::language()),
1130    );
1131    let mut fake_language_servers = language
1132        .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
1133            capabilities: lsp::ServerCapabilities {
1134                document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
1135                    first_trigger_character: ":".to_string(),
1136                    more_trigger_character: Some(vec![">".to_string()]),
1137                }),
1138                ..Default::default()
1139            },
1140            ..Default::default()
1141        }))
1142        .await;
1143    client_a.language_registry().add(Arc::new(language));
1144
1145    client_a
1146        .fs()
1147        .insert_tree(
1148            "/a",
1149            json!({
1150                "main.rs": "fn main() { a }",
1151                "other.rs": "// Test file",
1152            }),
1153        )
1154        .await;
1155    let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
1156    let project_id = active_call_a
1157        .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
1158        .await
1159        .unwrap();
1160    let project_b = client_b.build_remote_project(project_id, cx_b).await;
1161
1162    // Open a file in an editor as the host.
1163    let buffer_a = project_a
1164        .update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
1165        .await
1166        .unwrap();
1167    let window_a = cx_a.add_window(|_| EmptyView);
1168    let editor_a = window_a.add_view(cx_a, |cx| {
1169        Editor::for_buffer(buffer_a, Some(project_a.clone()), cx)
1170    });
1171
1172    let fake_language_server = fake_language_servers.next().await.unwrap();
1173    executor.run_until_parked();
1174
1175    // Receive an OnTypeFormatting request as the host's language server.
1176    // Return some formattings from the host's language server.
1177    fake_language_server.handle_request::<lsp::request::OnTypeFormatting, _, _>(
1178        |params, _| async move {
1179            assert_eq!(
1180                params.text_document_position.text_document.uri,
1181                lsp::Url::from_file_path("/a/main.rs").unwrap(),
1182            );
1183            assert_eq!(
1184                params.text_document_position.position,
1185                lsp::Position::new(0, 14),
1186            );
1187
1188            Ok(Some(vec![lsp::TextEdit {
1189                new_text: "~<".to_string(),
1190                range: lsp::Range::new(lsp::Position::new(0, 14), lsp::Position::new(0, 14)),
1191            }]))
1192        },
1193    );
1194
1195    // Open the buffer on the guest and see that the formattings worked
1196    let buffer_b = project_b
1197        .update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
1198        .await
1199        .unwrap();
1200
1201    // Type a on type formatting trigger character as the guest.
1202    editor_a.update(cx_a, |editor, cx| {
1203        cx.focus(&editor_a);
1204        editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
1205        editor.handle_input(">", cx);
1206    });
1207
1208    executor.run_until_parked();
1209
1210    buffer_b.read_with(cx_b, |buffer, _| {
1211        assert_eq!(buffer.text(), "fn main() { a>~< }")
1212    });
1213
1214    // Undo should remove LSP edits first
1215    editor_a.update(cx_a, |editor, cx| {
1216        assert_eq!(editor.text(cx), "fn main() { a>~< }");
1217        editor.undo(&Undo, cx);
1218        assert_eq!(editor.text(cx), "fn main() { a> }");
1219    });
1220    executor.run_until_parked();
1221
1222    buffer_b.read_with(cx_b, |buffer, _| {
1223        assert_eq!(buffer.text(), "fn main() { a> }")
1224    });
1225
1226    editor_a.update(cx_a, |editor, cx| {
1227        assert_eq!(editor.text(cx), "fn main() { a> }");
1228        editor.undo(&Undo, cx);
1229        assert_eq!(editor.text(cx), "fn main() { a }");
1230    });
1231    executor.run_until_parked();
1232
1233    buffer_b.read_with(cx_b, |buffer, _| {
1234        assert_eq!(buffer.text(), "fn main() { a }")
1235    });
1236}
1237
1238#[gpui::test(iterations = 10)]
1239async fn test_on_input_format_from_guest_to_host(
1240    executor: BackgroundExecutor,
1241    cx_a: &mut TestAppContext,
1242    cx_b: &mut TestAppContext,
1243) {
1244    let mut server = TestServer::start(&executor).await;
1245    let client_a = server.create_client(cx_a, "user_a").await;
1246    let client_b = server.create_client(cx_b, "user_b").await;
1247    server
1248        .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
1249        .await;
1250    let active_call_a = cx_a.read(ActiveCall::global);
1251
1252    // Set up a fake language server.
1253    let mut language = Language::new(
1254        LanguageConfig {
1255            name: "Rust".into(),
1256            path_suffixes: vec!["rs".to_string()],
1257            ..Default::default()
1258        },
1259        Some(tree_sitter_rust::language()),
1260    );
1261    let mut fake_language_servers = language
1262        .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
1263            capabilities: lsp::ServerCapabilities {
1264                document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
1265                    first_trigger_character: ":".to_string(),
1266                    more_trigger_character: Some(vec![">".to_string()]),
1267                }),
1268                ..Default::default()
1269            },
1270            ..Default::default()
1271        }))
1272        .await;
1273    client_a.language_registry().add(Arc::new(language));
1274
1275    client_a
1276        .fs()
1277        .insert_tree(
1278            "/a",
1279            json!({
1280                "main.rs": "fn main() { a }",
1281                "other.rs": "// Test file",
1282            }),
1283        )
1284        .await;
1285    let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
1286    let project_id = active_call_a
1287        .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
1288        .await
1289        .unwrap();
1290    let project_b = client_b.build_remote_project(project_id, cx_b).await;
1291
1292    // Open a file in an editor as the guest.
1293    let buffer_b = project_b
1294        .update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
1295        .await
1296        .unwrap();
1297    let window_b = cx_b.add_window(|_| EmptyView);
1298    let editor_b = window_b.add_view(cx_b, |cx| {
1299        Editor::for_buffer(buffer_b, Some(project_b.clone()), cx)
1300    });
1301
1302    let fake_language_server = fake_language_servers.next().await.unwrap();
1303    executor.run_until_parked();
1304    // Type a on type formatting trigger character as the guest.
1305    editor_b.update(cx_b, |editor, cx| {
1306        editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
1307        editor.handle_input(":", cx);
1308        cx.focus(&editor_b);
1309    });
1310
1311    // Receive an OnTypeFormatting request as the host's language server.
1312    // Return some formattings from the host's language server.
1313    cx_a.foreground().start_waiting();
1314    fake_language_server
1315        .handle_request::<lsp::request::OnTypeFormatting, _, _>(|params, _| async move {
1316            assert_eq!(
1317                params.text_document_position.text_document.uri,
1318                lsp::Url::from_file_path("/a/main.rs").unwrap(),
1319            );
1320            assert_eq!(
1321                params.text_document_position.position,
1322                lsp::Position::new(0, 14),
1323            );
1324
1325            Ok(Some(vec![lsp::TextEdit {
1326                new_text: "~:".to_string(),
1327                range: lsp::Range::new(lsp::Position::new(0, 14), lsp::Position::new(0, 14)),
1328            }]))
1329        })
1330        .next()
1331        .await
1332        .unwrap();
1333    cx_a.foreground().finish_waiting();
1334
1335    // Open the buffer on the host and see that the formattings worked
1336    let buffer_a = project_a
1337        .update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
1338        .await
1339        .unwrap();
1340    executor.run_until_parked();
1341
1342    buffer_a.read_with(cx_a, |buffer, _| {
1343        assert_eq!(buffer.text(), "fn main() { a:~: }")
1344    });
1345
1346    // Undo should remove LSP edits first
1347    editor_b.update(cx_b, |editor, cx| {
1348        assert_eq!(editor.text(cx), "fn main() { a:~: }");
1349        editor.undo(&Undo, cx);
1350        assert_eq!(editor.text(cx), "fn main() { a: }");
1351    });
1352    executor.run_until_parked();
1353
1354    buffer_a.read_with(cx_a, |buffer, _| {
1355        assert_eq!(buffer.text(), "fn main() { a: }")
1356    });
1357
1358    editor_b.update(cx_b, |editor, cx| {
1359        assert_eq!(editor.text(cx), "fn main() { a: }");
1360        editor.undo(&Undo, cx);
1361        assert_eq!(editor.text(cx), "fn main() { a }");
1362    });
1363    executor.run_until_parked();
1364
1365    buffer_a.read_with(cx_a, |buffer, _| {
1366        assert_eq!(buffer.text(), "fn main() { a }")
1367    });
1368}
1369
1370#[gpui::test(iterations = 10)]
1371async fn test_mutual_editor_inlay_hint_cache_update(
1372    executor: BackgroundExecutor,
1373    cx_a: &mut TestAppContext,
1374    cx_b: &mut TestAppContext,
1375) {
1376    let mut server = TestServer::start(&executor).await;
1377    let client_a = server.create_client(cx_a, "user_a").await;
1378    let client_b = server.create_client(cx_b, "user_b").await;
1379    server
1380        .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
1381        .await;
1382    let active_call_a = cx_a.read(ActiveCall::global);
1383    let active_call_b = cx_b.read(ActiveCall::global);
1384
1385    cx_a.update(editor::init);
1386    cx_b.update(editor::init);
1387
1388    cx_a.update(|cx| {
1389        cx.update_global(|store: &mut SettingsStore, cx| {
1390            store.update_user_settings::<AllLanguageSettings>(cx, |settings| {
1391                settings.defaults.inlay_hints = Some(InlayHintSettings {
1392                    enabled: true,
1393                    show_type_hints: true,
1394                    show_parameter_hints: false,
1395                    show_other_hints: true,
1396                })
1397            });
1398        });
1399    });
1400    cx_b.update(|cx| {
1401        cx.update_global(|store: &mut SettingsStore, cx| {
1402            store.update_user_settings::<AllLanguageSettings>(cx, |settings| {
1403                settings.defaults.inlay_hints = Some(InlayHintSettings {
1404                    enabled: true,
1405                    show_type_hints: true,
1406                    show_parameter_hints: false,
1407                    show_other_hints: true,
1408                })
1409            });
1410        });
1411    });
1412
1413    let mut language = Language::new(
1414        LanguageConfig {
1415            name: "Rust".into(),
1416            path_suffixes: vec!["rs".to_string()],
1417            ..Default::default()
1418        },
1419        Some(tree_sitter_rust::language()),
1420    );
1421    let mut fake_language_servers = language
1422        .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
1423            capabilities: lsp::ServerCapabilities {
1424                inlay_hint_provider: Some(lsp::OneOf::Left(true)),
1425                ..Default::default()
1426            },
1427            ..Default::default()
1428        }))
1429        .await;
1430    let language = Arc::new(language);
1431    client_a.language_registry().add(Arc::clone(&language));
1432    client_b.language_registry().add(language);
1433
1434    // Client A opens a project.
1435    client_a
1436        .fs()
1437        .insert_tree(
1438            "/a",
1439            json!({
1440                "main.rs": "fn main() { a } // and some long comment to ensure inlay hints are not trimmed out",
1441                "other.rs": "// Test file",
1442            }),
1443        )
1444        .await;
1445    let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
1446    active_call_a
1447        .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
1448        .await
1449        .unwrap();
1450    let project_id = active_call_a
1451        .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
1452        .await
1453        .unwrap();
1454
1455    // Client B joins the project
1456    let project_b = client_b.build_remote_project(project_id, cx_b).await;
1457    active_call_b
1458        .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
1459        .await
1460        .unwrap();
1461
1462    let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a);
1463    cx_a.foreground().start_waiting();
1464
1465    // The host opens a rust file.
1466    let _buffer_a = project_a
1467        .update(cx_a, |project, cx| {
1468            project.open_local_buffer("/a/main.rs", cx)
1469        })
1470        .await
1471        .unwrap();
1472    let fake_language_server = fake_language_servers.next().await.unwrap();
1473    let editor_a = workspace_a
1474        .update(cx_a, |workspace, cx| {
1475            workspace.open_path((worktree_id, "main.rs"), None, true, cx)
1476        })
1477        .await
1478        .unwrap()
1479        .downcast::<Editor>()
1480        .unwrap();
1481
1482    // Set up the language server to return an additional inlay hint on each request.
1483    let edits_made = Arc::new(AtomicUsize::new(0));
1484    let closure_edits_made = Arc::clone(&edits_made);
1485    fake_language_server
1486        .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
1487            let task_edits_made = Arc::clone(&closure_edits_made);
1488            async move {
1489                assert_eq!(
1490                    params.text_document.uri,
1491                    lsp::Url::from_file_path("/a/main.rs").unwrap(),
1492                );
1493                let edits_made = task_edits_made.load(atomic::Ordering::Acquire);
1494                Ok(Some(vec![lsp::InlayHint {
1495                    position: lsp::Position::new(0, edits_made as u32),
1496                    label: lsp::InlayHintLabel::String(edits_made.to_string()),
1497                    kind: None,
1498                    text_edits: None,
1499                    tooltip: None,
1500                    padding_left: None,
1501                    padding_right: None,
1502                    data: None,
1503                }]))
1504            }
1505        })
1506        .next()
1507        .await
1508        .unwrap();
1509
1510    executor.run_until_parked();
1511
1512    let initial_edit = edits_made.load(atomic::Ordering::Acquire);
1513    editor_a.update(cx_a, |editor, _| {
1514        assert_eq!(
1515            vec![initial_edit.to_string()],
1516            extract_hint_labels(editor),
1517            "Host should get its first hints when opens an editor"
1518        );
1519        let inlay_cache = editor.inlay_hint_cache();
1520        assert_eq!(
1521            inlay_cache.version(),
1522            1,
1523            "Host editor update the cache version after every cache/view change",
1524        );
1525    });
1526    let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b);
1527    let editor_b = workspace_b
1528        .update(cx_b, |workspace, cx| {
1529            workspace.open_path((worktree_id, "main.rs"), None, true, cx)
1530        })
1531        .await
1532        .unwrap()
1533        .downcast::<Editor>()
1534        .unwrap();
1535
1536    executor.run_until_parked();
1537    editor_b.update(cx_b, |editor, _| {
1538        assert_eq!(
1539            vec![initial_edit.to_string()],
1540            extract_hint_labels(editor),
1541            "Client should get its first hints when opens an editor"
1542        );
1543        let inlay_cache = editor.inlay_hint_cache();
1544        assert_eq!(
1545            inlay_cache.version(),
1546            1,
1547            "Guest editor update the cache version after every cache/view change"
1548        );
1549    });
1550
1551    let after_client_edit = edits_made.fetch_add(1, atomic::Ordering::Release) + 1;
1552    editor_b.update(cx_b, |editor, cx| {
1553        editor.change_selections(None, cx, |s| s.select_ranges([13..13].clone()));
1554        editor.handle_input(":", cx);
1555        cx.focus(&editor_b);
1556    });
1557
1558    executor.run_until_parked();
1559    editor_a.update(cx_a, |editor, _| {
1560        assert_eq!(
1561            vec![after_client_edit.to_string()],
1562            extract_hint_labels(editor),
1563        );
1564        let inlay_cache = editor.inlay_hint_cache();
1565        assert_eq!(inlay_cache.version(), 2);
1566    });
1567    editor_b.update(cx_b, |editor, _| {
1568        assert_eq!(
1569            vec![after_client_edit.to_string()],
1570            extract_hint_labels(editor),
1571        );
1572        let inlay_cache = editor.inlay_hint_cache();
1573        assert_eq!(inlay_cache.version(), 2);
1574    });
1575
1576    let after_host_edit = edits_made.fetch_add(1, atomic::Ordering::Release) + 1;
1577    editor_a.update(cx_a, |editor, cx| {
1578        editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
1579        editor.handle_input("a change to increment both buffers' versions", cx);
1580        cx.focus(&editor_a);
1581    });
1582
1583    executor.run_until_parked();
1584    editor_a.update(cx_a, |editor, _| {
1585        assert_eq!(
1586            vec![after_host_edit.to_string()],
1587            extract_hint_labels(editor),
1588        );
1589        let inlay_cache = editor.inlay_hint_cache();
1590        assert_eq!(inlay_cache.version(), 3);
1591    });
1592    editor_b.update(cx_b, |editor, _| {
1593        assert_eq!(
1594            vec![after_host_edit.to_string()],
1595            extract_hint_labels(editor),
1596        );
1597        let inlay_cache = editor.inlay_hint_cache();
1598        assert_eq!(inlay_cache.version(), 3);
1599    });
1600
1601    let after_special_edit_for_refresh = edits_made.fetch_add(1, atomic::Ordering::Release) + 1;
1602    fake_language_server
1603        .request::<lsp::request::InlayHintRefreshRequest>(())
1604        .await
1605        .expect("inlay refresh request failed");
1606
1607    executor.run_until_parked();
1608    editor_a.update(cx_a, |editor, _| {
1609        assert_eq!(
1610            vec![after_special_edit_for_refresh.to_string()],
1611            extract_hint_labels(editor),
1612            "Host should react to /refresh LSP request"
1613        );
1614        let inlay_cache = editor.inlay_hint_cache();
1615        assert_eq!(
1616            inlay_cache.version(),
1617            4,
1618            "Host should accepted all edits and bump its cache version every time"
1619        );
1620    });
1621    editor_b.update(cx_b, |editor, _| {
1622        assert_eq!(
1623            vec![after_special_edit_for_refresh.to_string()],
1624            extract_hint_labels(editor),
1625            "Guest should get a /refresh LSP request propagated by host"
1626        );
1627        let inlay_cache = editor.inlay_hint_cache();
1628        assert_eq!(
1629            inlay_cache.version(),
1630            4,
1631            "Guest should accepted all edits and bump its cache version every time"
1632        );
1633    });
1634}
1635
1636#[gpui::test(iterations = 10)]
1637async fn test_inlay_hint_refresh_is_forwarded(
1638    executor: BackgroundExecutor,
1639    cx_a: &mut TestAppContext,
1640    cx_b: &mut TestAppContext,
1641) {
1642    let mut server = TestServer::start(&executor).await;
1643    let client_a = server.create_client(cx_a, "user_a").await;
1644    let client_b = server.create_client(cx_b, "user_b").await;
1645    server
1646        .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
1647        .await;
1648    let active_call_a = cx_a.read(ActiveCall::global);
1649    let active_call_b = cx_b.read(ActiveCall::global);
1650
1651    cx_a.update(editor::init);
1652    cx_b.update(editor::init);
1653
1654    cx_a.update(|cx| {
1655        cx.update_global(|store: &mut SettingsStore, cx| {
1656            store.update_user_settings::<AllLanguageSettings>(cx, |settings| {
1657                settings.defaults.inlay_hints = Some(InlayHintSettings {
1658                    enabled: false,
1659                    show_type_hints: false,
1660                    show_parameter_hints: false,
1661                    show_other_hints: false,
1662                })
1663            });
1664        });
1665    });
1666    cx_b.update(|cx| {
1667        cx.update_global(|store: &mut SettingsStore, cx| {
1668            store.update_user_settings::<AllLanguageSettings>(cx, |settings| {
1669                settings.defaults.inlay_hints = Some(InlayHintSettings {
1670                    enabled: true,
1671                    show_type_hints: true,
1672                    show_parameter_hints: true,
1673                    show_other_hints: true,
1674                })
1675            });
1676        });
1677    });
1678
1679    let mut language = Language::new(
1680        LanguageConfig {
1681            name: "Rust".into(),
1682            path_suffixes: vec!["rs".to_string()],
1683            ..Default::default()
1684        },
1685        Some(tree_sitter_rust::language()),
1686    );
1687    let mut fake_language_servers = language
1688        .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
1689            capabilities: lsp::ServerCapabilities {
1690                inlay_hint_provider: Some(lsp::OneOf::Left(true)),
1691                ..Default::default()
1692            },
1693            ..Default::default()
1694        }))
1695        .await;
1696    let language = Arc::new(language);
1697    client_a.language_registry().add(Arc::clone(&language));
1698    client_b.language_registry().add(language);
1699
1700    client_a
1701        .fs()
1702        .insert_tree(
1703            "/a",
1704            json!({
1705                "main.rs": "fn main() { a } // and some long comment to ensure inlay hints are not trimmed out",
1706                "other.rs": "// Test file",
1707            }),
1708        )
1709        .await;
1710    let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
1711    active_call_a
1712        .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
1713        .await
1714        .unwrap();
1715    let project_id = active_call_a
1716        .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
1717        .await
1718        .unwrap();
1719
1720    let project_b = client_b.build_remote_project(project_id, cx_b).await;
1721    active_call_b
1722        .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
1723        .await
1724        .unwrap();
1725
1726    let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a);
1727    let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b);
1728    cx_a.foreground().start_waiting();
1729    cx_b.foreground().start_waiting();
1730
1731    let editor_a = workspace_a
1732        .update(cx_a, |workspace, cx| {
1733            workspace.open_path((worktree_id, "main.rs"), None, true, cx)
1734        })
1735        .await
1736        .unwrap()
1737        .downcast::<Editor>()
1738        .unwrap();
1739
1740    let editor_b = workspace_b
1741        .update(cx_b, |workspace, cx| {
1742            workspace.open_path((worktree_id, "main.rs"), None, true, cx)
1743        })
1744        .await
1745        .unwrap()
1746        .downcast::<Editor>()
1747        .unwrap();
1748
1749    let other_hints = Arc::new(AtomicBool::new(false));
1750    let fake_language_server = fake_language_servers.next().await.unwrap();
1751    let closure_other_hints = Arc::clone(&other_hints);
1752    fake_language_server
1753        .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
1754            let task_other_hints = Arc::clone(&closure_other_hints);
1755            async move {
1756                assert_eq!(
1757                    params.text_document.uri,
1758                    lsp::Url::from_file_path("/a/main.rs").unwrap(),
1759                );
1760                let other_hints = task_other_hints.load(atomic::Ordering::Acquire);
1761                let character = if other_hints { 0 } else { 2 };
1762                let label = if other_hints {
1763                    "other hint"
1764                } else {
1765                    "initial hint"
1766                };
1767                Ok(Some(vec![lsp::InlayHint {
1768                    position: lsp::Position::new(0, character),
1769                    label: lsp::InlayHintLabel::String(label.to_string()),
1770                    kind: None,
1771                    text_edits: None,
1772                    tooltip: None,
1773                    padding_left: None,
1774                    padding_right: None,
1775                    data: None,
1776                }]))
1777            }
1778        })
1779        .next()
1780        .await
1781        .unwrap();
1782    cx_a.foreground().finish_waiting();
1783    cx_b.foreground().finish_waiting();
1784
1785    executor.run_until_parked();
1786    editor_a.update(cx_a, |editor, _| {
1787        assert!(
1788            extract_hint_labels(editor).is_empty(),
1789            "Host should get no hints due to them turned off"
1790        );
1791        let inlay_cache = editor.inlay_hint_cache();
1792        assert_eq!(
1793            inlay_cache.version(),
1794            0,
1795            "Turned off hints should not generate version updates"
1796        );
1797    });
1798
1799    executor.run_until_parked();
1800    editor_b.update(cx_b, |editor, _| {
1801        assert_eq!(
1802            vec!["initial hint".to_string()],
1803            extract_hint_labels(editor),
1804            "Client should get its first hints when opens an editor"
1805        );
1806        let inlay_cache = editor.inlay_hint_cache();
1807        assert_eq!(
1808            inlay_cache.version(),
1809            1,
1810            "Should update cache verison after first hints"
1811        );
1812    });
1813
1814    other_hints.fetch_or(true, atomic::Ordering::Release);
1815    fake_language_server
1816        .request::<lsp::request::InlayHintRefreshRequest>(())
1817        .await
1818        .expect("inlay refresh request failed");
1819    executor.run_until_parked();
1820    editor_a.update(cx_a, |editor, _| {
1821        assert!(
1822            extract_hint_labels(editor).is_empty(),
1823            "Host should get nop hints due to them turned off, even after the /refresh"
1824        );
1825        let inlay_cache = editor.inlay_hint_cache();
1826        assert_eq!(
1827            inlay_cache.version(),
1828            0,
1829            "Turned off hints should not generate version updates, again"
1830        );
1831    });
1832
1833    executor.run_until_parked();
1834    editor_b.update(cx_b, |editor, _| {
1835        assert_eq!(
1836            vec!["other hint".to_string()],
1837            extract_hint_labels(editor),
1838            "Guest should get a /refresh LSP request propagated by host despite host hints are off"
1839        );
1840        let inlay_cache = editor.inlay_hint_cache();
1841        assert_eq!(
1842            inlay_cache.version(),
1843            2,
1844            "Guest should accepted all edits and bump its cache version every time"
1845        );
1846    });
1847}
1848
1849fn extract_hint_labels(editor: &Editor) -> Vec<String> {
1850    let mut labels = Vec::new();
1851    for hint in editor.inlay_hint_cache().hints() {
1852        match hint.label {
1853            project::InlayHintLabel::String(s) => labels.push(s),
1854            _ => unreachable!(),
1855        }
1856    }
1857    labels
1858}