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