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