editor_tests.rs

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