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            let rename_selection = rename_editor.selections.newest::<usize>(cx);
 740            assert_eq!(
 741                rename_selection.range(),
 742                0..3,
 743                "Rename that was triggered from zero selection caret, should propose the whole word."
 744            );
 745            rename_editor.buffer().update(cx, |rename_buffer, cx| {
 746                rename_buffer.edit([(0..3, "THREE")], None, cx);
 747            });
 748        });
 749    });
 750
 751    // Cancel the rename, and repeat the same, but use selections instead of cursor movement
 752    editor_b.update(cx_b, |editor, cx| {
 753        editor.cancel(&editor::actions::Cancel, cx);
 754    });
 755    let prepare_rename = editor_b.update(cx_b, |editor, cx| {
 756        editor.change_selections(None, cx, |s| s.select_ranges([7..8]));
 757        editor.rename(&Rename, cx).unwrap()
 758    });
 759
 760    fake_language_server
 761        .handle_request::<lsp::request::PrepareRenameRequest, _, _>(|params, _| async move {
 762            assert_eq!(params.text_document.uri.as_str(), "file:///dir/one.rs");
 763            assert_eq!(params.position, lsp::Position::new(0, 8));
 764            Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range::new(
 765                lsp::Position::new(0, 6),
 766                lsp::Position::new(0, 9),
 767            ))))
 768        })
 769        .next()
 770        .await
 771        .unwrap();
 772    prepare_rename.await.unwrap();
 773    editor_b.update(cx_b, |editor, cx| {
 774        use editor::ToOffset;
 775        let rename = editor.pending_rename().unwrap();
 776        let buffer = editor.buffer().read(cx).snapshot(cx);
 777        let lsp_rename_start = rename.range.start.to_offset(&buffer);
 778        let lsp_rename_end = rename.range.end.to_offset(&buffer);
 779        assert_eq!(lsp_rename_start..lsp_rename_end, 6..9);
 780        rename.editor.update(cx, |rename_editor, cx| {
 781            let rename_selection = rename_editor.selections.newest::<usize>(cx);
 782            assert_eq!(
 783                rename_selection.range(),
 784                1..2,
 785                "Rename that was triggered from a selection, should have the same selection range in the rename proposal"
 786            );
 787            rename_editor.buffer().update(cx, |rename_buffer, cx| {
 788                rename_buffer.edit([(0..lsp_rename_end - lsp_rename_start, "THREE")], None, cx);
 789            });
 790        });
 791    });
 792
 793    let confirm_rename = editor_b.update(cx_b, |editor, cx| {
 794        Editor::confirm_rename(editor, &ConfirmRename, cx).unwrap()
 795    });
 796    fake_language_server
 797        .handle_request::<lsp::request::Rename, _, _>(|params, _| async move {
 798            assert_eq!(
 799                params.text_document_position.text_document.uri.as_str(),
 800                "file:///dir/one.rs"
 801            );
 802            assert_eq!(
 803                params.text_document_position.position,
 804                lsp::Position::new(0, 6)
 805            );
 806            assert_eq!(params.new_name, "THREE");
 807            Ok(Some(lsp::WorkspaceEdit {
 808                changes: Some(
 809                    [
 810                        (
 811                            lsp::Url::from_file_path("/dir/one.rs").unwrap(),
 812                            vec![lsp::TextEdit::new(
 813                                lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
 814                                "THREE".to_string(),
 815                            )],
 816                        ),
 817                        (
 818                            lsp::Url::from_file_path("/dir/two.rs").unwrap(),
 819                            vec![
 820                                lsp::TextEdit::new(
 821                                    lsp::Range::new(
 822                                        lsp::Position::new(0, 24),
 823                                        lsp::Position::new(0, 27),
 824                                    ),
 825                                    "THREE".to_string(),
 826                                ),
 827                                lsp::TextEdit::new(
 828                                    lsp::Range::new(
 829                                        lsp::Position::new(0, 35),
 830                                        lsp::Position::new(0, 38),
 831                                    ),
 832                                    "THREE".to_string(),
 833                                ),
 834                            ],
 835                        ),
 836                    ]
 837                    .into_iter()
 838                    .collect(),
 839                ),
 840                ..Default::default()
 841            }))
 842        })
 843        .next()
 844        .await
 845        .unwrap();
 846    confirm_rename.await.unwrap();
 847
 848    let rename_editor = workspace_b.update(cx_b, |workspace, cx| {
 849        workspace.active_item_as::<Editor>(cx).unwrap()
 850    });
 851
 852    rename_editor.update(cx_b, |editor, cx| {
 853        assert_eq!(
 854            editor.text(cx),
 855            "const THREE: usize = 1;\nconst TWO: usize = one::THREE + one::THREE;"
 856        );
 857        editor.undo(&Undo, cx);
 858        assert_eq!(
 859            editor.text(cx),
 860            "const ONE: usize = 1;\nconst TWO: usize = one::ONE + one::ONE;"
 861        );
 862        editor.redo(&Redo, cx);
 863        assert_eq!(
 864            editor.text(cx),
 865            "const THREE: usize = 1;\nconst TWO: usize = one::THREE + one::THREE;"
 866        );
 867    });
 868
 869    // Ensure temporary rename edits cannot be undone/redone.
 870    editor_b.update(cx_b, |editor, cx| {
 871        editor.undo(&Undo, cx);
 872        assert_eq!(editor.text(cx), "const ONE: usize = 1;");
 873        editor.undo(&Undo, cx);
 874        assert_eq!(editor.text(cx), "const ONE: usize = 1;");
 875        editor.redo(&Redo, cx);
 876        assert_eq!(editor.text(cx), "const THREE: usize = 1;");
 877    })
 878}
 879
 880#[gpui::test(iterations = 10)]
 881async fn test_language_server_statuses(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
 882    let mut server = TestServer::start(cx_a.executor()).await;
 883    let executor = cx_a.executor();
 884    let client_a = server.create_client(cx_a, "user_a").await;
 885    let client_b = server.create_client(cx_b, "user_b").await;
 886    server
 887        .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
 888        .await;
 889    let active_call_a = cx_a.read(ActiveCall::global);
 890
 891    cx_b.update(editor::init);
 892
 893    client_a.language_registry().add(rust_lang());
 894    let mut fake_language_servers = client_a.language_registry().register_fake_lsp_adapter(
 895        "Rust",
 896        FakeLspAdapter {
 897            name: "the-language-server",
 898            ..Default::default()
 899        },
 900    );
 901
 902    client_a
 903        .fs()
 904        .insert_tree(
 905            "/dir",
 906            json!({
 907                "main.rs": "const ONE: usize = 1;",
 908            }),
 909        )
 910        .await;
 911    let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
 912
 913    let _buffer_a = project_a
 914        .update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
 915        .await
 916        .unwrap();
 917
 918    let fake_language_server = fake_language_servers.next().await.unwrap();
 919    fake_language_server.start_progress("the-token").await;
 920    fake_language_server.notify::<lsp::notification::Progress>(lsp::ProgressParams {
 921        token: lsp::NumberOrString::String("the-token".to_string()),
 922        value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::Report(
 923            lsp::WorkDoneProgressReport {
 924                message: Some("the-message".to_string()),
 925                ..Default::default()
 926            },
 927        )),
 928    });
 929    executor.advance_clock(SERVER_PROGRESS_DEBOUNCE_TIMEOUT);
 930    executor.run_until_parked();
 931
 932    project_a.read_with(cx_a, |project, _| {
 933        let status = project.language_server_statuses().next().unwrap();
 934        assert_eq!(status.name, "the-language-server");
 935        assert_eq!(status.pending_work.len(), 1);
 936        assert_eq!(
 937            status.pending_work["the-token"].message.as_ref().unwrap(),
 938            "the-message"
 939        );
 940    });
 941
 942    let project_id = active_call_a
 943        .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
 944        .await
 945        .unwrap();
 946    executor.run_until_parked();
 947    let project_b = client_b.build_remote_project(project_id, cx_b).await;
 948
 949    project_b.read_with(cx_b, |project, _| {
 950        let status = project.language_server_statuses().next().unwrap();
 951        assert_eq!(status.name, "the-language-server");
 952    });
 953
 954    fake_language_server.notify::<lsp::notification::Progress>(lsp::ProgressParams {
 955        token: lsp::NumberOrString::String("the-token".to_string()),
 956        value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::Report(
 957            lsp::WorkDoneProgressReport {
 958                message: Some("the-message-2".to_string()),
 959                ..Default::default()
 960            },
 961        )),
 962    });
 963    executor.advance_clock(SERVER_PROGRESS_DEBOUNCE_TIMEOUT);
 964    executor.run_until_parked();
 965
 966    project_a.read_with(cx_a, |project, _| {
 967        let status = project.language_server_statuses().next().unwrap();
 968        assert_eq!(status.name, "the-language-server");
 969        assert_eq!(status.pending_work.len(), 1);
 970        assert_eq!(
 971            status.pending_work["the-token"].message.as_ref().unwrap(),
 972            "the-message-2"
 973        );
 974    });
 975
 976    project_b.read_with(cx_b, |project, _| {
 977        let status = project.language_server_statuses().next().unwrap();
 978        assert_eq!(status.name, "the-language-server");
 979        assert_eq!(status.pending_work.len(), 1);
 980        assert_eq!(
 981            status.pending_work["the-token"].message.as_ref().unwrap(),
 982            "the-message-2"
 983        );
 984    });
 985}
 986
 987#[gpui::test(iterations = 10)]
 988async fn test_share_project(
 989    cx_a: &mut TestAppContext,
 990    cx_b: &mut TestAppContext,
 991    cx_c: &mut TestAppContext,
 992) {
 993    let executor = cx_a.executor();
 994    let cx_b = cx_b.add_empty_window();
 995    let mut server = TestServer::start(executor.clone()).await;
 996    let client_a = server.create_client(cx_a, "user_a").await;
 997    let client_b = server.create_client(cx_b, "user_b").await;
 998    let client_c = server.create_client(cx_c, "user_c").await;
 999    server
1000        .make_contacts(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)])
1001        .await;
1002    let active_call_a = cx_a.read(ActiveCall::global);
1003    let active_call_b = cx_b.read(ActiveCall::global);
1004    let active_call_c = cx_c.read(ActiveCall::global);
1005
1006    client_a
1007        .fs()
1008        .insert_tree(
1009            "/a",
1010            json!({
1011                ".gitignore": "ignored-dir",
1012                "a.txt": "a-contents",
1013                "b.txt": "b-contents",
1014                "ignored-dir": {
1015                    "c.txt": "",
1016                    "d.txt": "",
1017                }
1018            }),
1019        )
1020        .await;
1021
1022    // Invite client B to collaborate on a project
1023    let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
1024    active_call_a
1025        .update(cx_a, |call, cx| {
1026            call.invite(client_b.user_id().unwrap(), Some(project_a.clone()), cx)
1027        })
1028        .await
1029        .unwrap();
1030
1031    // Join that project as client B
1032
1033    let incoming_call_b = active_call_b.read_with(cx_b, |call, _| call.incoming());
1034    executor.run_until_parked();
1035    let call = incoming_call_b.borrow().clone().unwrap();
1036    assert_eq!(call.calling_user.github_login, "user_a");
1037    let initial_project = call.initial_project.unwrap();
1038    active_call_b
1039        .update(cx_b, |call, cx| call.accept_incoming(cx))
1040        .await
1041        .unwrap();
1042    let client_b_peer_id = client_b.peer_id().unwrap();
1043    let project_b = client_b
1044        .build_remote_project(initial_project.id, cx_b)
1045        .await;
1046
1047    let replica_id_b = project_b.read_with(cx_b, |project, _| project.replica_id());
1048
1049    executor.run_until_parked();
1050
1051    project_a.read_with(cx_a, |project, _| {
1052        let client_b_collaborator = project.collaborators().get(&client_b_peer_id).unwrap();
1053        assert_eq!(client_b_collaborator.replica_id, replica_id_b);
1054    });
1055
1056    project_b.read_with(cx_b, |project, cx| {
1057        let worktree = project.worktrees().next().unwrap().read(cx);
1058        assert_eq!(
1059            worktree.paths().map(AsRef::as_ref).collect::<Vec<_>>(),
1060            [
1061                Path::new(".gitignore"),
1062                Path::new("a.txt"),
1063                Path::new("b.txt"),
1064                Path::new("ignored-dir"),
1065            ]
1066        );
1067    });
1068
1069    project_b
1070        .update(cx_b, |project, cx| {
1071            let worktree = project.worktrees().next().unwrap();
1072            let entry = worktree.read(cx).entry_for_path("ignored-dir").unwrap();
1073            project.expand_entry(worktree_id, entry.id, cx).unwrap()
1074        })
1075        .await
1076        .unwrap();
1077
1078    project_b.read_with(cx_b, |project, cx| {
1079        let worktree = project.worktrees().next().unwrap().read(cx);
1080        assert_eq!(
1081            worktree.paths().map(AsRef::as_ref).collect::<Vec<_>>(),
1082            [
1083                Path::new(".gitignore"),
1084                Path::new("a.txt"),
1085                Path::new("b.txt"),
1086                Path::new("ignored-dir"),
1087                Path::new("ignored-dir/c.txt"),
1088                Path::new("ignored-dir/d.txt"),
1089            ]
1090        );
1091    });
1092
1093    // Open the same file as client B and client A.
1094    let buffer_b = project_b
1095        .update(cx_b, |p, cx| p.open_buffer((worktree_id, "b.txt"), cx))
1096        .await
1097        .unwrap();
1098
1099    buffer_b.read_with(cx_b, |buf, _| assert_eq!(buf.text(), "b-contents"));
1100
1101    project_a.read_with(cx_a, |project, cx| {
1102        assert!(project.has_open_buffer((worktree_id, "b.txt"), cx))
1103    });
1104    let buffer_a = project_a
1105        .update(cx_a, |p, cx| p.open_buffer((worktree_id, "b.txt"), cx))
1106        .await
1107        .unwrap();
1108
1109    let editor_b = cx_b.new_view(|cx| Editor::for_buffer(buffer_b, None, cx));
1110
1111    // Client A sees client B's selection
1112    executor.run_until_parked();
1113
1114    buffer_a.read_with(cx_a, |buffer, _| {
1115        buffer
1116            .snapshot()
1117            .remote_selections_in_range(text::Anchor::MIN..text::Anchor::MAX)
1118            .count()
1119            == 1
1120    });
1121
1122    // Edit the buffer as client B and see that edit as client A.
1123    editor_b.update(cx_b, |editor, cx| editor.handle_input("ok, ", cx));
1124    executor.run_until_parked();
1125
1126    buffer_a.read_with(cx_a, |buffer, _| {
1127        assert_eq!(buffer.text(), "ok, b-contents")
1128    });
1129
1130    // Client B can invite client C on a project shared by client A.
1131    active_call_b
1132        .update(cx_b, |call, cx| {
1133            call.invite(client_c.user_id().unwrap(), Some(project_b.clone()), cx)
1134        })
1135        .await
1136        .unwrap();
1137
1138    let incoming_call_c = active_call_c.read_with(cx_c, |call, _| call.incoming());
1139    executor.run_until_parked();
1140    let call = incoming_call_c.borrow().clone().unwrap();
1141    assert_eq!(call.calling_user.github_login, "user_b");
1142    let initial_project = call.initial_project.unwrap();
1143    active_call_c
1144        .update(cx_c, |call, cx| call.accept_incoming(cx))
1145        .await
1146        .unwrap();
1147    let _project_c = client_c
1148        .build_remote_project(initial_project.id, cx_c)
1149        .await;
1150
1151    // Client B closes the editor, and client A sees client B's selections removed.
1152    cx_b.update(move |_| drop(editor_b));
1153    executor.run_until_parked();
1154
1155    buffer_a.read_with(cx_a, |buffer, _| {
1156        buffer
1157            .snapshot()
1158            .remote_selections_in_range(text::Anchor::MIN..text::Anchor::MAX)
1159            .count()
1160            == 0
1161    });
1162}
1163
1164#[gpui::test(iterations = 10)]
1165async fn test_on_input_format_from_host_to_guest(
1166    cx_a: &mut TestAppContext,
1167    cx_b: &mut TestAppContext,
1168) {
1169    let mut server = TestServer::start(cx_a.executor()).await;
1170    let executor = cx_a.executor();
1171    let client_a = server.create_client(cx_a, "user_a").await;
1172    let client_b = server.create_client(cx_b, "user_b").await;
1173    server
1174        .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
1175        .await;
1176    let active_call_a = cx_a.read(ActiveCall::global);
1177
1178    client_a.language_registry().add(rust_lang());
1179    let mut fake_language_servers = client_a.language_registry().register_fake_lsp_adapter(
1180        "Rust",
1181        FakeLspAdapter {
1182            capabilities: lsp::ServerCapabilities {
1183                document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
1184                    first_trigger_character: ":".to_string(),
1185                    more_trigger_character: Some(vec![">".to_string()]),
1186                }),
1187                ..Default::default()
1188            },
1189            ..Default::default()
1190        },
1191    );
1192
1193    client_a
1194        .fs()
1195        .insert_tree(
1196            "/a",
1197            json!({
1198                "main.rs": "fn main() { a }",
1199                "other.rs": "// Test file",
1200            }),
1201        )
1202        .await;
1203    let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
1204    let project_id = active_call_a
1205        .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
1206        .await
1207        .unwrap();
1208    let project_b = client_b.build_remote_project(project_id, cx_b).await;
1209
1210    // Open a file in an editor as the host.
1211    let buffer_a = project_a
1212        .update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
1213        .await
1214        .unwrap();
1215    let cx_a = cx_a.add_empty_window();
1216    let editor_a = cx_a.new_view(|cx| Editor::for_buffer(buffer_a, Some(project_a.clone()), cx));
1217
1218    let fake_language_server = fake_language_servers.next().await.unwrap();
1219    executor.run_until_parked();
1220
1221    // Receive an OnTypeFormatting request as the host's language server.
1222    // Return some formatting from the host's language server.
1223    fake_language_server.handle_request::<lsp::request::OnTypeFormatting, _, _>(
1224        |params, _| async move {
1225            assert_eq!(
1226                params.text_document_position.text_document.uri,
1227                lsp::Url::from_file_path("/a/main.rs").unwrap(),
1228            );
1229            assert_eq!(
1230                params.text_document_position.position,
1231                lsp::Position::new(0, 14),
1232            );
1233
1234            Ok(Some(vec![lsp::TextEdit {
1235                new_text: "~<".to_string(),
1236                range: lsp::Range::new(lsp::Position::new(0, 14), lsp::Position::new(0, 14)),
1237            }]))
1238        },
1239    );
1240
1241    // Open the buffer on the guest and see that the formatting worked
1242    let buffer_b = project_b
1243        .update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
1244        .await
1245        .unwrap();
1246
1247    // Type a on type formatting trigger character as the guest.
1248    cx_a.focus_view(&editor_a);
1249    editor_a.update(cx_a, |editor, cx| {
1250        editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
1251        editor.handle_input(">", cx);
1252    });
1253
1254    executor.run_until_parked();
1255
1256    buffer_b.read_with(cx_b, |buffer, _| {
1257        assert_eq!(buffer.text(), "fn main() { a>~< }")
1258    });
1259
1260    // Undo should remove LSP edits first
1261    editor_a.update(cx_a, |editor, cx| {
1262        assert_eq!(editor.text(cx), "fn main() { a>~< }");
1263        editor.undo(&Undo, cx);
1264        assert_eq!(editor.text(cx), "fn main() { a> }");
1265    });
1266    executor.run_until_parked();
1267
1268    buffer_b.read_with(cx_b, |buffer, _| {
1269        assert_eq!(buffer.text(), "fn main() { a> }")
1270    });
1271
1272    editor_a.update(cx_a, |editor, cx| {
1273        assert_eq!(editor.text(cx), "fn main() { a> }");
1274        editor.undo(&Undo, cx);
1275        assert_eq!(editor.text(cx), "fn main() { a }");
1276    });
1277    executor.run_until_parked();
1278
1279    buffer_b.read_with(cx_b, |buffer, _| {
1280        assert_eq!(buffer.text(), "fn main() { a }")
1281    });
1282}
1283
1284#[gpui::test(iterations = 10)]
1285async fn test_on_input_format_from_guest_to_host(
1286    cx_a: &mut TestAppContext,
1287    cx_b: &mut TestAppContext,
1288) {
1289    let mut server = TestServer::start(cx_a.executor()).await;
1290    let executor = cx_a.executor();
1291    let client_a = server.create_client(cx_a, "user_a").await;
1292    let client_b = server.create_client(cx_b, "user_b").await;
1293    server
1294        .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
1295        .await;
1296    let active_call_a = cx_a.read(ActiveCall::global);
1297
1298    client_a.language_registry().add(rust_lang());
1299    let mut fake_language_servers = client_a.language_registry().register_fake_lsp_adapter(
1300        "Rust",
1301        FakeLspAdapter {
1302            capabilities: lsp::ServerCapabilities {
1303                document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
1304                    first_trigger_character: ":".to_string(),
1305                    more_trigger_character: Some(vec![">".to_string()]),
1306                }),
1307                ..Default::default()
1308            },
1309            ..Default::default()
1310        },
1311    );
1312
1313    client_a
1314        .fs()
1315        .insert_tree(
1316            "/a",
1317            json!({
1318                "main.rs": "fn main() { a }",
1319                "other.rs": "// Test file",
1320            }),
1321        )
1322        .await;
1323    let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
1324    let project_id = active_call_a
1325        .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
1326        .await
1327        .unwrap();
1328    let project_b = client_b.build_remote_project(project_id, cx_b).await;
1329
1330    // Open a file in an editor as the guest.
1331    let buffer_b = project_b
1332        .update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
1333        .await
1334        .unwrap();
1335    let cx_b = cx_b.add_empty_window();
1336    let editor_b = cx_b.new_view(|cx| Editor::for_buffer(buffer_b, Some(project_b.clone()), cx));
1337
1338    let fake_language_server = fake_language_servers.next().await.unwrap();
1339    executor.run_until_parked();
1340
1341    // Type a on type formatting trigger character as the guest.
1342    cx_b.focus_view(&editor_b);
1343    editor_b.update(cx_b, |editor, cx| {
1344        editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
1345        editor.handle_input(":", cx);
1346    });
1347
1348    // Receive an OnTypeFormatting request as the host's language server.
1349    // Return some formatting from the host's language server.
1350    executor.start_waiting();
1351    fake_language_server
1352        .handle_request::<lsp::request::OnTypeFormatting, _, _>(|params, _| async move {
1353            assert_eq!(
1354                params.text_document_position.text_document.uri,
1355                lsp::Url::from_file_path("/a/main.rs").unwrap(),
1356            );
1357            assert_eq!(
1358                params.text_document_position.position,
1359                lsp::Position::new(0, 14),
1360            );
1361
1362            Ok(Some(vec![lsp::TextEdit {
1363                new_text: "~:".to_string(),
1364                range: lsp::Range::new(lsp::Position::new(0, 14), lsp::Position::new(0, 14)),
1365            }]))
1366        })
1367        .next()
1368        .await
1369        .unwrap();
1370    executor.finish_waiting();
1371
1372    // Open the buffer on the host and see that the formatting worked
1373    let buffer_a = project_a
1374        .update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
1375        .await
1376        .unwrap();
1377    executor.run_until_parked();
1378
1379    buffer_a.read_with(cx_a, |buffer, _| {
1380        assert_eq!(buffer.text(), "fn main() { a:~: }")
1381    });
1382
1383    // Undo should remove LSP edits first
1384    editor_b.update(cx_b, |editor, cx| {
1385        assert_eq!(editor.text(cx), "fn main() { a:~: }");
1386        editor.undo(&Undo, cx);
1387        assert_eq!(editor.text(cx), "fn main() { a: }");
1388    });
1389    executor.run_until_parked();
1390
1391    buffer_a.read_with(cx_a, |buffer, _| {
1392        assert_eq!(buffer.text(), "fn main() { a: }")
1393    });
1394
1395    editor_b.update(cx_b, |editor, cx| {
1396        assert_eq!(editor.text(cx), "fn main() { a: }");
1397        editor.undo(&Undo, cx);
1398        assert_eq!(editor.text(cx), "fn main() { a }");
1399    });
1400    executor.run_until_parked();
1401
1402    buffer_a.read_with(cx_a, |buffer, _| {
1403        assert_eq!(buffer.text(), "fn main() { a }")
1404    });
1405}
1406
1407#[gpui::test(iterations = 10)]
1408async fn test_mutual_editor_inlay_hint_cache_update(
1409    cx_a: &mut TestAppContext,
1410    cx_b: &mut TestAppContext,
1411) {
1412    let mut server = TestServer::start(cx_a.executor()).await;
1413    let executor = cx_a.executor();
1414    let client_a = server.create_client(cx_a, "user_a").await;
1415    let client_b = server.create_client(cx_b, "user_b").await;
1416    server
1417        .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
1418        .await;
1419    let active_call_a = cx_a.read(ActiveCall::global);
1420    let active_call_b = cx_b.read(ActiveCall::global);
1421
1422    cx_a.update(editor::init);
1423    cx_b.update(editor::init);
1424
1425    cx_a.update(|cx| {
1426        cx.update_global(|store: &mut SettingsStore, cx| {
1427            store.update_user_settings::<AllLanguageSettings>(cx, |settings| {
1428                settings.defaults.inlay_hints = Some(InlayHintSettings {
1429                    enabled: true,
1430                    edit_debounce_ms: 0,
1431                    scroll_debounce_ms: 0,
1432                    show_type_hints: true,
1433                    show_parameter_hints: false,
1434                    show_other_hints: true,
1435                })
1436            });
1437        });
1438    });
1439    cx_b.update(|cx| {
1440        cx.update_global(|store: &mut SettingsStore, cx| {
1441            store.update_user_settings::<AllLanguageSettings>(cx, |settings| {
1442                settings.defaults.inlay_hints = Some(InlayHintSettings {
1443                    enabled: true,
1444                    edit_debounce_ms: 0,
1445                    scroll_debounce_ms: 0,
1446                    show_type_hints: true,
1447                    show_parameter_hints: false,
1448                    show_other_hints: true,
1449                })
1450            });
1451        });
1452    });
1453
1454    client_a.language_registry().add(rust_lang());
1455    client_b.language_registry().add(rust_lang());
1456    let mut fake_language_servers = client_a.language_registry().register_fake_lsp_adapter(
1457        "Rust",
1458        FakeLspAdapter {
1459            capabilities: lsp::ServerCapabilities {
1460                inlay_hint_provider: Some(lsp::OneOf::Left(true)),
1461                ..Default::default()
1462            },
1463            ..Default::default()
1464        },
1465    );
1466
1467    // Client A opens a project.
1468    client_a
1469        .fs()
1470        .insert_tree(
1471            "/a",
1472            json!({
1473                "main.rs": "fn main() { a } // and some long comment to ensure inlay hints are not trimmed out",
1474                "other.rs": "// Test file",
1475            }),
1476        )
1477        .await;
1478    let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
1479    active_call_a
1480        .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
1481        .await
1482        .unwrap();
1483    let project_id = active_call_a
1484        .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
1485        .await
1486        .unwrap();
1487
1488    // Client B joins the project
1489    let project_b = client_b.build_remote_project(project_id, cx_b).await;
1490    active_call_b
1491        .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
1492        .await
1493        .unwrap();
1494
1495    let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
1496    executor.start_waiting();
1497
1498    // The host opens a rust file.
1499    let _buffer_a = project_a
1500        .update(cx_a, |project, cx| {
1501            project.open_local_buffer("/a/main.rs", cx)
1502        })
1503        .await
1504        .unwrap();
1505    let fake_language_server = fake_language_servers.next().await.unwrap();
1506    let editor_a = workspace_a
1507        .update(cx_a, |workspace, cx| {
1508            workspace.open_path((worktree_id, "main.rs"), None, true, cx)
1509        })
1510        .await
1511        .unwrap()
1512        .downcast::<Editor>()
1513        .unwrap();
1514
1515    // Set up the language server to return an additional inlay hint on each request.
1516    let edits_made = Arc::new(AtomicUsize::new(0));
1517    let closure_edits_made = Arc::clone(&edits_made);
1518    fake_language_server
1519        .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
1520            let task_edits_made = Arc::clone(&closure_edits_made);
1521            async move {
1522                assert_eq!(
1523                    params.text_document.uri,
1524                    lsp::Url::from_file_path("/a/main.rs").unwrap(),
1525                );
1526                let edits_made = task_edits_made.load(atomic::Ordering::Acquire);
1527                Ok(Some(vec![lsp::InlayHint {
1528                    position: lsp::Position::new(0, edits_made as u32),
1529                    label: lsp::InlayHintLabel::String(edits_made.to_string()),
1530                    kind: None,
1531                    text_edits: None,
1532                    tooltip: None,
1533                    padding_left: None,
1534                    padding_right: None,
1535                    data: None,
1536                }]))
1537            }
1538        })
1539        .next()
1540        .await
1541        .unwrap();
1542
1543    executor.run_until_parked();
1544
1545    let initial_edit = edits_made.load(atomic::Ordering::Acquire);
1546    editor_a.update(cx_a, |editor, _| {
1547        assert_eq!(
1548            vec![initial_edit.to_string()],
1549            extract_hint_labels(editor),
1550            "Host should get its first hints when opens an editor"
1551        );
1552        let inlay_cache = editor.inlay_hint_cache();
1553        assert_eq!(
1554            inlay_cache.version(),
1555            1,
1556            "Host editor update the cache version after every cache/view change",
1557        );
1558    });
1559    let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
1560    let editor_b = workspace_b
1561        .update(cx_b, |workspace, cx| {
1562            workspace.open_path((worktree_id, "main.rs"), None, true, cx)
1563        })
1564        .await
1565        .unwrap()
1566        .downcast::<Editor>()
1567        .unwrap();
1568
1569    executor.run_until_parked();
1570    editor_b.update(cx_b, |editor, _| {
1571        assert_eq!(
1572            vec![initial_edit.to_string()],
1573            extract_hint_labels(editor),
1574            "Client should get its first hints when opens an editor"
1575        );
1576        let inlay_cache = editor.inlay_hint_cache();
1577        assert_eq!(
1578            inlay_cache.version(),
1579            1,
1580            "Guest editor update the cache version after every cache/view change"
1581        );
1582    });
1583
1584    let after_client_edit = edits_made.fetch_add(1, atomic::Ordering::Release) + 1;
1585    editor_b.update(cx_b, |editor, cx| {
1586        editor.change_selections(None, cx, |s| s.select_ranges([13..13].clone()));
1587        editor.handle_input(":", cx);
1588    });
1589    cx_b.focus_view(&editor_b);
1590
1591    executor.run_until_parked();
1592    editor_a.update(cx_a, |editor, _| {
1593        assert_eq!(
1594            vec![after_client_edit.to_string()],
1595            extract_hint_labels(editor),
1596        );
1597        let inlay_cache = editor.inlay_hint_cache();
1598        assert_eq!(inlay_cache.version(), 2);
1599    });
1600    editor_b.update(cx_b, |editor, _| {
1601        assert_eq!(
1602            vec![after_client_edit.to_string()],
1603            extract_hint_labels(editor),
1604        );
1605        let inlay_cache = editor.inlay_hint_cache();
1606        assert_eq!(inlay_cache.version(), 2);
1607    });
1608
1609    let after_host_edit = edits_made.fetch_add(1, atomic::Ordering::Release) + 1;
1610    editor_a.update(cx_a, |editor, cx| {
1611        editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
1612        editor.handle_input("a change to increment both buffers' versions", cx);
1613    });
1614    cx_a.focus_view(&editor_a);
1615
1616    executor.run_until_parked();
1617    editor_a.update(cx_a, |editor, _| {
1618        assert_eq!(
1619            vec![after_host_edit.to_string()],
1620            extract_hint_labels(editor),
1621        );
1622        let inlay_cache = editor.inlay_hint_cache();
1623        assert_eq!(inlay_cache.version(), 3);
1624    });
1625    editor_b.update(cx_b, |editor, _| {
1626        assert_eq!(
1627            vec![after_host_edit.to_string()],
1628            extract_hint_labels(editor),
1629        );
1630        let inlay_cache = editor.inlay_hint_cache();
1631        assert_eq!(inlay_cache.version(), 3);
1632    });
1633
1634    let after_special_edit_for_refresh = edits_made.fetch_add(1, atomic::Ordering::Release) + 1;
1635    fake_language_server
1636        .request::<lsp::request::InlayHintRefreshRequest>(())
1637        .await
1638        .expect("inlay refresh request failed");
1639
1640    executor.run_until_parked();
1641    editor_a.update(cx_a, |editor, _| {
1642        assert_eq!(
1643            vec![after_special_edit_for_refresh.to_string()],
1644            extract_hint_labels(editor),
1645            "Host should react to /refresh LSP request"
1646        );
1647        let inlay_cache = editor.inlay_hint_cache();
1648        assert_eq!(
1649            inlay_cache.version(),
1650            4,
1651            "Host should accepted all edits and bump its cache version every time"
1652        );
1653    });
1654    editor_b.update(cx_b, |editor, _| {
1655        assert_eq!(
1656            vec![after_special_edit_for_refresh.to_string()],
1657            extract_hint_labels(editor),
1658            "Guest should get a /refresh LSP request propagated by host"
1659        );
1660        let inlay_cache = editor.inlay_hint_cache();
1661        assert_eq!(
1662            inlay_cache.version(),
1663            4,
1664            "Guest should accepted all edits and bump its cache version every time"
1665        );
1666    });
1667}
1668
1669#[gpui::test(iterations = 10)]
1670async fn test_inlay_hint_refresh_is_forwarded(
1671    cx_a: &mut TestAppContext,
1672    cx_b: &mut TestAppContext,
1673) {
1674    let mut server = TestServer::start(cx_a.executor()).await;
1675    let executor = cx_a.executor();
1676    let client_a = server.create_client(cx_a, "user_a").await;
1677    let client_b = server.create_client(cx_b, "user_b").await;
1678    server
1679        .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
1680        .await;
1681    let active_call_a = cx_a.read(ActiveCall::global);
1682    let active_call_b = cx_b.read(ActiveCall::global);
1683
1684    cx_a.update(editor::init);
1685    cx_b.update(editor::init);
1686
1687    cx_a.update(|cx| {
1688        cx.update_global(|store: &mut SettingsStore, cx| {
1689            store.update_user_settings::<AllLanguageSettings>(cx, |settings| {
1690                settings.defaults.inlay_hints = Some(InlayHintSettings {
1691                    enabled: false,
1692                    edit_debounce_ms: 0,
1693                    scroll_debounce_ms: 0,
1694                    show_type_hints: false,
1695                    show_parameter_hints: false,
1696                    show_other_hints: false,
1697                })
1698            });
1699        });
1700    });
1701    cx_b.update(|cx| {
1702        cx.update_global(|store: &mut SettingsStore, cx| {
1703            store.update_user_settings::<AllLanguageSettings>(cx, |settings| {
1704                settings.defaults.inlay_hints = Some(InlayHintSettings {
1705                    enabled: true,
1706                    edit_debounce_ms: 0,
1707                    scroll_debounce_ms: 0,
1708                    show_type_hints: true,
1709                    show_parameter_hints: true,
1710                    show_other_hints: true,
1711                })
1712            });
1713        });
1714    });
1715
1716    client_a.language_registry().add(rust_lang());
1717    client_b.language_registry().add(rust_lang());
1718    let mut fake_language_servers = client_a.language_registry().register_fake_lsp_adapter(
1719        "Rust",
1720        FakeLspAdapter {
1721            capabilities: lsp::ServerCapabilities {
1722                inlay_hint_provider: Some(lsp::OneOf::Left(true)),
1723                ..Default::default()
1724            },
1725            ..Default::default()
1726        },
1727    );
1728
1729    client_a
1730        .fs()
1731        .insert_tree(
1732            "/a",
1733            json!({
1734                "main.rs": "fn main() { a } // and some long comment to ensure inlay hints are not trimmed out",
1735                "other.rs": "// Test file",
1736            }),
1737        )
1738        .await;
1739    let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
1740    active_call_a
1741        .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
1742        .await
1743        .unwrap();
1744    let project_id = active_call_a
1745        .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
1746        .await
1747        .unwrap();
1748
1749    let project_b = client_b.build_remote_project(project_id, cx_b).await;
1750    active_call_b
1751        .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
1752        .await
1753        .unwrap();
1754
1755    let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
1756    let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
1757
1758    cx_a.background_executor.start_waiting();
1759
1760    let editor_a = workspace_a
1761        .update(cx_a, |workspace, cx| {
1762            workspace.open_path((worktree_id, "main.rs"), None, true, cx)
1763        })
1764        .await
1765        .unwrap()
1766        .downcast::<Editor>()
1767        .unwrap();
1768
1769    let editor_b = workspace_b
1770        .update(cx_b, |workspace, cx| {
1771            workspace.open_path((worktree_id, "main.rs"), None, true, cx)
1772        })
1773        .await
1774        .unwrap()
1775        .downcast::<Editor>()
1776        .unwrap();
1777
1778    let other_hints = Arc::new(AtomicBool::new(false));
1779    let fake_language_server = fake_language_servers.next().await.unwrap();
1780    let closure_other_hints = Arc::clone(&other_hints);
1781    fake_language_server
1782        .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
1783            let task_other_hints = Arc::clone(&closure_other_hints);
1784            async move {
1785                assert_eq!(
1786                    params.text_document.uri,
1787                    lsp::Url::from_file_path("/a/main.rs").unwrap(),
1788                );
1789                let other_hints = task_other_hints.load(atomic::Ordering::Acquire);
1790                let character = if other_hints { 0 } else { 2 };
1791                let label = if other_hints {
1792                    "other hint"
1793                } else {
1794                    "initial hint"
1795                };
1796                Ok(Some(vec![lsp::InlayHint {
1797                    position: lsp::Position::new(0, character),
1798                    label: lsp::InlayHintLabel::String(label.to_string()),
1799                    kind: None,
1800                    text_edits: None,
1801                    tooltip: None,
1802                    padding_left: None,
1803                    padding_right: None,
1804                    data: None,
1805                }]))
1806            }
1807        })
1808        .next()
1809        .await
1810        .unwrap();
1811    executor.finish_waiting();
1812
1813    executor.run_until_parked();
1814    editor_a.update(cx_a, |editor, _| {
1815        assert!(
1816            extract_hint_labels(editor).is_empty(),
1817            "Host should get no hints due to them turned off"
1818        );
1819        let inlay_cache = editor.inlay_hint_cache();
1820        assert_eq!(
1821            inlay_cache.version(),
1822            0,
1823            "Turned off hints should not generate version updates"
1824        );
1825    });
1826
1827    executor.run_until_parked();
1828    editor_b.update(cx_b, |editor, _| {
1829        assert_eq!(
1830            vec!["initial hint".to_string()],
1831            extract_hint_labels(editor),
1832            "Client should get its first hints when opens an editor"
1833        );
1834        let inlay_cache = editor.inlay_hint_cache();
1835        assert_eq!(
1836            inlay_cache.version(),
1837            1,
1838            "Should update cache version after first hints"
1839        );
1840    });
1841
1842    other_hints.fetch_or(true, atomic::Ordering::Release);
1843    fake_language_server
1844        .request::<lsp::request::InlayHintRefreshRequest>(())
1845        .await
1846        .expect("inlay refresh request failed");
1847    executor.run_until_parked();
1848    editor_a.update(cx_a, |editor, _| {
1849        assert!(
1850            extract_hint_labels(editor).is_empty(),
1851            "Host should get nop hints due to them turned off, even after the /refresh"
1852        );
1853        let inlay_cache = editor.inlay_hint_cache();
1854        assert_eq!(
1855            inlay_cache.version(),
1856            0,
1857            "Turned off hints should not generate version updates, again"
1858        );
1859    });
1860
1861    executor.run_until_parked();
1862    editor_b.update(cx_b, |editor, _| {
1863        assert_eq!(
1864            vec!["other hint".to_string()],
1865            extract_hint_labels(editor),
1866            "Guest should get a /refresh LSP request propagated by host despite host hints are off"
1867        );
1868        let inlay_cache = editor.inlay_hint_cache();
1869        assert_eq!(
1870            inlay_cache.version(),
1871            2,
1872            "Guest should accepted all edits and bump its cache version every time"
1873        );
1874    });
1875}
1876
1877#[gpui::test]
1878async fn test_multiple_types_reverts(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
1879    let mut server = TestServer::start(cx_a.executor()).await;
1880    let client_a = server.create_client(cx_a, "user_a").await;
1881    let client_b = server.create_client(cx_b, "user_b").await;
1882    server
1883        .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
1884        .await;
1885    let active_call_a = cx_a.read(ActiveCall::global);
1886    let active_call_b = cx_b.read(ActiveCall::global);
1887
1888    cx_a.update(editor::init);
1889    cx_b.update(editor::init);
1890
1891    client_a.language_registry().add(rust_lang());
1892    client_b.language_registry().add(rust_lang());
1893
1894    let base_text = indoc! {r#"struct Row;
1895struct Row1;
1896struct Row2;
1897
1898struct Row4;
1899struct Row5;
1900struct Row6;
1901
1902struct Row8;
1903struct Row9;
1904struct Row10;"#};
1905
1906    client_a
1907        .fs()
1908        .insert_tree(
1909            "/a",
1910            json!({
1911                "main.rs": base_text,
1912            }),
1913        )
1914        .await;
1915    let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
1916    active_call_a
1917        .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
1918        .await
1919        .unwrap();
1920    let project_id = active_call_a
1921        .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
1922        .await
1923        .unwrap();
1924
1925    let project_b = client_b.build_remote_project(project_id, cx_b).await;
1926    active_call_b
1927        .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
1928        .await
1929        .unwrap();
1930
1931    let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
1932    let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
1933
1934    let editor_a = workspace_a
1935        .update(cx_a, |workspace, cx| {
1936            workspace.open_path((worktree_id, "main.rs"), None, true, cx)
1937        })
1938        .await
1939        .unwrap()
1940        .downcast::<Editor>()
1941        .unwrap();
1942
1943    let editor_b = workspace_b
1944        .update(cx_b, |workspace, cx| {
1945            workspace.open_path((worktree_id, "main.rs"), None, true, cx)
1946        })
1947        .await
1948        .unwrap()
1949        .downcast::<Editor>()
1950        .unwrap();
1951
1952    let mut editor_cx_a = EditorTestContext {
1953        cx: cx_a.clone(),
1954        window: cx_a.handle(),
1955        editor: editor_a,
1956        assertion_cx: AssertionContextManager::new(),
1957    };
1958    let mut editor_cx_b = EditorTestContext {
1959        cx: cx_b.clone(),
1960        window: cx_b.handle(),
1961        editor: editor_b,
1962        assertion_cx: AssertionContextManager::new(),
1963    };
1964
1965    // host edits the file, that differs from the base text, producing diff hunks
1966    editor_cx_a.set_state(indoc! {r#"struct Row;
1967        struct Row0.1;
1968        struct Row0.2;
1969        struct Row1;
1970
1971        struct Row4;
1972        struct Row5444;
1973        struct Row6;
1974
1975        struct Row9;
1976        struct Row1220;ˇ"#});
1977    editor_cx_a.update_editor(|editor, cx| {
1978        editor
1979            .buffer()
1980            .read(cx)
1981            .as_singleton()
1982            .unwrap()
1983            .update(cx, |buffer, cx| {
1984                buffer.set_diff_base(Some(base_text.to_string()), cx);
1985            });
1986    });
1987    editor_cx_b.update_editor(|editor, cx| {
1988        editor
1989            .buffer()
1990            .read(cx)
1991            .as_singleton()
1992            .unwrap()
1993            .update(cx, |buffer, cx| {
1994                buffer.set_diff_base(Some(base_text.to_string()), cx);
1995            });
1996    });
1997    cx_a.executor().run_until_parked();
1998    cx_b.executor().run_until_parked();
1999
2000    // client, selects a range in the updated buffer, and reverts it
2001    // both host and the client observe the reverted state (with one hunk left, not covered by client's selection)
2002    editor_cx_b.set_selections_state(indoc! {r#"«ˇstruct Row;
2003        struct Row0.1;
2004        struct Row0.2;
2005        struct Row1;
2006
2007        struct Row4;
2008        struct Row5444;
2009        struct Row6;
2010
2011        struct R»ow9;
2012        struct Row1220;"#});
2013    editor_cx_b.update_editor(|editor, cx| {
2014        editor.revert_selected_hunks(&RevertSelectedHunks, cx);
2015    });
2016    cx_a.executor().run_until_parked();
2017    cx_b.executor().run_until_parked();
2018    editor_cx_a.assert_editor_state(indoc! {r#"struct Row;
2019        struct Row1;
2020        struct Row2;
2021
2022        struct Row4;
2023        struct Row5;
2024        struct Row6;
2025
2026        struct Row8;
2027        struct Row9;
2028        struct Row1220;ˇ"#});
2029    editor_cx_b.assert_editor_state(indoc! {r#"«ˇstruct Row;
2030        struct Row1;
2031        struct Row2;
2032
2033        struct Row4;
2034        struct Row5;
2035        struct Row6;
2036
2037        struct Row8;
2038        struct R»ow9;
2039        struct Row1220;"#});
2040}
2041
2042#[gpui::test(iterations = 10)]
2043async fn test_git_blame_is_forwarded(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
2044    let mut server = TestServer::start(cx_a.executor()).await;
2045    let client_a = server.create_client(cx_a, "user_a").await;
2046    let client_b = server.create_client(cx_b, "user_b").await;
2047    server
2048        .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
2049        .await;
2050    let active_call_a = cx_a.read(ActiveCall::global);
2051
2052    cx_a.update(editor::init);
2053    cx_b.update(editor::init);
2054    // Turn inline-blame-off by default so no state is transferred without us explicitly doing so
2055    let inline_blame_off_settings = Some(InlineBlameSettings {
2056        enabled: false,
2057        delay_ms: None,
2058        min_column: None,
2059    });
2060    cx_a.update(|cx| {
2061        cx.update_global(|store: &mut SettingsStore, cx| {
2062            store.update_user_settings::<ProjectSettings>(cx, |settings| {
2063                settings.git.inline_blame = inline_blame_off_settings;
2064            });
2065        });
2066    });
2067    cx_b.update(|cx| {
2068        cx.update_global(|store: &mut SettingsStore, cx| {
2069            store.update_user_settings::<ProjectSettings>(cx, |settings| {
2070                settings.git.inline_blame = inline_blame_off_settings;
2071            });
2072        });
2073    });
2074
2075    client_a
2076        .fs()
2077        .insert_tree(
2078            "/my-repo",
2079            json!({
2080                ".git": {},
2081                "file.txt": "line1\nline2\nline3\nline\n",
2082            }),
2083        )
2084        .await;
2085
2086    let blame = git::blame::Blame {
2087        entries: vec![
2088            blame_entry("1b1b1b", 0..1),
2089            blame_entry("0d0d0d", 1..2),
2090            blame_entry("3a3a3a", 2..3),
2091            blame_entry("4c4c4c", 3..4),
2092        ],
2093        permalinks: HashMap::default(), // This field is deprecrated
2094        messages: [
2095            ("1b1b1b", "message for idx-0"),
2096            ("0d0d0d", "message for idx-1"),
2097            ("3a3a3a", "message for idx-2"),
2098            ("4c4c4c", "message for idx-3"),
2099        ]
2100        .into_iter()
2101        .map(|(sha, message)| (sha.parse().unwrap(), message.into()))
2102        .collect(),
2103        remote_url: Some("git@github.com:zed-industries/zed.git".to_string()),
2104    };
2105    client_a.fs().set_blame_for_repo(
2106        Path::new("/my-repo/.git"),
2107        vec![(Path::new("file.txt"), blame)],
2108    );
2109
2110    let (project_a, worktree_id) = client_a.build_local_project("/my-repo", cx_a).await;
2111    let project_id = active_call_a
2112        .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
2113        .await
2114        .unwrap();
2115
2116    // Create editor_a
2117    let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
2118    let editor_a = workspace_a
2119        .update(cx_a, |workspace, cx| {
2120            workspace.open_path((worktree_id, "file.txt"), None, true, cx)
2121        })
2122        .await
2123        .unwrap()
2124        .downcast::<Editor>()
2125        .unwrap();
2126
2127    // Join the project as client B.
2128    let project_b = client_b.build_remote_project(project_id, cx_b).await;
2129    let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
2130    let editor_b = workspace_b
2131        .update(cx_b, |workspace, cx| {
2132            workspace.open_path((worktree_id, "file.txt"), None, true, cx)
2133        })
2134        .await
2135        .unwrap()
2136        .downcast::<Editor>()
2137        .unwrap();
2138
2139    // client_b now requests git blame for the open buffer
2140    editor_b.update(cx_b, |editor_b, cx| {
2141        assert!(editor_b.blame().is_none());
2142        editor_b.toggle_git_blame(&editor::actions::ToggleGitBlame {}, cx);
2143    });
2144
2145    cx_a.executor().run_until_parked();
2146    cx_b.executor().run_until_parked();
2147
2148    editor_b.update(cx_b, |editor_b, cx| {
2149        let blame = editor_b.blame().expect("editor_b should have blame now");
2150        let entries = blame.update(cx, |blame, cx| {
2151            blame
2152                .blame_for_rows((0..4).map(Some), cx)
2153                .collect::<Vec<_>>()
2154        });
2155
2156        assert_eq!(
2157            entries,
2158            vec![
2159                Some(blame_entry("1b1b1b", 0..1)),
2160                Some(blame_entry("0d0d0d", 1..2)),
2161                Some(blame_entry("3a3a3a", 2..3)),
2162                Some(blame_entry("4c4c4c", 3..4)),
2163            ]
2164        );
2165
2166        blame.update(cx, |blame, _| {
2167            for (idx, entry) in entries.iter().flatten().enumerate() {
2168                let details = blame.details_for_entry(entry).unwrap();
2169                assert_eq!(details.message, format!("message for idx-{}", idx));
2170                assert_eq!(
2171                    details.permalink.unwrap().to_string(),
2172                    format!("https://github.com/zed-industries/zed/commit/{}", entry.sha)
2173                );
2174            }
2175        });
2176    });
2177
2178    // editor_b updates the file, which gets sent to client_a, which updates git blame,
2179    // which gets back to client_b.
2180    editor_b.update(cx_b, |editor_b, cx| {
2181        editor_b.edit([(Point::new(0, 3)..Point::new(0, 3), "FOO")], cx);
2182    });
2183
2184    cx_a.executor().run_until_parked();
2185    cx_b.executor().run_until_parked();
2186
2187    editor_b.update(cx_b, |editor_b, cx| {
2188        let blame = editor_b.blame().expect("editor_b should have blame now");
2189        let entries = blame.update(cx, |blame, cx| {
2190            blame
2191                .blame_for_rows((0..4).map(Some), cx)
2192                .collect::<Vec<_>>()
2193        });
2194
2195        assert_eq!(
2196            entries,
2197            vec![
2198                None,
2199                Some(blame_entry("0d0d0d", 1..2)),
2200                Some(blame_entry("3a3a3a", 2..3)),
2201                Some(blame_entry("4c4c4c", 3..4)),
2202            ]
2203        );
2204    });
2205
2206    // Now editor_a also updates the file
2207    editor_a.update(cx_a, |editor_a, cx| {
2208        editor_a.edit([(Point::new(1, 3)..Point::new(1, 3), "FOO")], cx);
2209    });
2210
2211    cx_a.executor().run_until_parked();
2212    cx_b.executor().run_until_parked();
2213
2214    editor_b.update(cx_b, |editor_b, cx| {
2215        let blame = editor_b.blame().expect("editor_b should have blame now");
2216        let entries = blame.update(cx, |blame, cx| {
2217            blame
2218                .blame_for_rows((0..4).map(Some), cx)
2219                .collect::<Vec<_>>()
2220        });
2221
2222        assert_eq!(
2223            entries,
2224            vec![
2225                None,
2226                None,
2227                Some(blame_entry("3a3a3a", 2..3)),
2228                Some(blame_entry("4c4c4c", 3..4)),
2229            ]
2230        );
2231    });
2232}
2233
2234fn extract_hint_labels(editor: &Editor) -> Vec<String> {
2235    let mut labels = Vec::new();
2236    for hint in editor.inlay_hint_cache().hints() {
2237        match hint.label {
2238            project::InlayHintLabel::String(s) => labels.push(s),
2239            _ => unreachable!(),
2240        }
2241    }
2242    labels
2243}
2244
2245fn blame_entry(sha: &str, range: Range<u32>) -> git::blame::BlameEntry {
2246    git::blame::BlameEntry {
2247        sha: sha.parse().unwrap(),
2248        range,
2249        ..Default::default()
2250    }
2251}