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