editor_tests.rs

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