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