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