editor_tests.rs

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