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.join_remote_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.join_remote_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.join_remote_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.join_remote_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.join_remote_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.join_remote_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.join_remote_project(initial_project.id, cx_b).await;
1130
1131    let replica_id_b = project_b.read_with(cx_b, |project, _| project.replica_id());
1132
1133    executor.run_until_parked();
1134
1135    project_a.read_with(cx_a, |project, _| {
1136        let client_b_collaborator = project.collaborators().get(&client_b_peer_id).unwrap();
1137        assert_eq!(client_b_collaborator.replica_id, replica_id_b);
1138    });
1139
1140    project_b.read_with(cx_b, |project, cx| {
1141        let worktree = project.worktrees(cx).next().unwrap().read(cx);
1142        assert_eq!(
1143            worktree.paths().map(AsRef::as_ref).collect::<Vec<_>>(),
1144            [
1145                Path::new(".gitignore"),
1146                Path::new("a.txt"),
1147                Path::new("b.txt"),
1148                Path::new("ignored-dir"),
1149            ]
1150        );
1151    });
1152
1153    project_b
1154        .update(cx_b, |project, cx| {
1155            let worktree = project.worktrees(cx).next().unwrap();
1156            let entry = worktree.read(cx).entry_for_path("ignored-dir").unwrap();
1157            project.expand_entry(worktree_id, entry.id, cx).unwrap()
1158        })
1159        .await
1160        .unwrap();
1161
1162    project_b.read_with(cx_b, |project, cx| {
1163        let worktree = project.worktrees(cx).next().unwrap().read(cx);
1164        assert_eq!(
1165            worktree.paths().map(AsRef::as_ref).collect::<Vec<_>>(),
1166            [
1167                Path::new(".gitignore"),
1168                Path::new("a.txt"),
1169                Path::new("b.txt"),
1170                Path::new("ignored-dir"),
1171                Path::new("ignored-dir/c.txt"),
1172                Path::new("ignored-dir/d.txt"),
1173            ]
1174        );
1175    });
1176
1177    // Open the same file as client B and client A.
1178    let buffer_b = project_b
1179        .update(cx_b, |p, cx| p.open_buffer((worktree_id, "b.txt"), cx))
1180        .await
1181        .unwrap();
1182
1183    buffer_b.read_with(cx_b, |buf, _| assert_eq!(buf.text(), "b-contents"));
1184
1185    project_a.read_with(cx_a, |project, cx| {
1186        assert!(project.has_open_buffer((worktree_id, "b.txt"), cx))
1187    });
1188    let buffer_a = project_a
1189        .update(cx_a, |p, cx| p.open_buffer((worktree_id, "b.txt"), cx))
1190        .await
1191        .unwrap();
1192
1193    let editor_b = cx_b.new_view(|cx| Editor::for_buffer(buffer_b, None, cx));
1194
1195    // Client A sees client B's selection
1196    executor.run_until_parked();
1197
1198    buffer_a.read_with(cx_a, |buffer, _| {
1199        buffer
1200            .snapshot()
1201            .selections_in_range(text::Anchor::MIN..text::Anchor::MAX, false)
1202            .count()
1203            == 1
1204    });
1205
1206    // Edit the buffer as client B and see that edit as client A.
1207    editor_b.update(cx_b, |editor, cx| editor.handle_input("ok, ", cx));
1208    executor.run_until_parked();
1209
1210    buffer_a.read_with(cx_a, |buffer, _| {
1211        assert_eq!(buffer.text(), "ok, b-contents")
1212    });
1213
1214    // Client B can invite client C on a project shared by client A.
1215    active_call_b
1216        .update(cx_b, |call, cx| {
1217            call.invite(client_c.user_id().unwrap(), Some(project_b.clone()), cx)
1218        })
1219        .await
1220        .unwrap();
1221
1222    let incoming_call_c = active_call_c.read_with(cx_c, |call, _| call.incoming());
1223    executor.run_until_parked();
1224    let call = incoming_call_c.borrow().clone().unwrap();
1225    assert_eq!(call.calling_user.github_login, "user_b");
1226    let initial_project = call.initial_project.unwrap();
1227    active_call_c
1228        .update(cx_c, |call, cx| call.accept_incoming(cx))
1229        .await
1230        .unwrap();
1231    let _project_c = client_c.join_remote_project(initial_project.id, cx_c).await;
1232
1233    // Client B closes the editor, and client A sees client B's selections removed.
1234    cx_b.update(move |_| drop(editor_b));
1235    executor.run_until_parked();
1236
1237    buffer_a.read_with(cx_a, |buffer, _| {
1238        buffer
1239            .snapshot()
1240            .selections_in_range(text::Anchor::MIN..text::Anchor::MAX, false)
1241            .count()
1242            == 0
1243    });
1244}
1245
1246#[gpui::test(iterations = 10)]
1247async fn test_on_input_format_from_host_to_guest(
1248    cx_a: &mut TestAppContext,
1249    cx_b: &mut TestAppContext,
1250) {
1251    let mut server = TestServer::start(cx_a.executor()).await;
1252    let executor = cx_a.executor();
1253    let client_a = server.create_client(cx_a, "user_a").await;
1254    let client_b = server.create_client(cx_b, "user_b").await;
1255    server
1256        .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
1257        .await;
1258    let active_call_a = cx_a.read(ActiveCall::global);
1259
1260    client_a.language_registry().add(rust_lang());
1261    let mut fake_language_servers = client_a.language_registry().register_fake_lsp(
1262        "Rust",
1263        FakeLspAdapter {
1264            capabilities: lsp::ServerCapabilities {
1265                document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
1266                    first_trigger_character: ":".to_string(),
1267                    more_trigger_character: Some(vec![">".to_string()]),
1268                }),
1269                ..Default::default()
1270            },
1271            ..Default::default()
1272        },
1273    );
1274
1275    client_a
1276        .fs()
1277        .insert_tree(
1278            "/a",
1279            json!({
1280                "main.rs": "fn main() { a }",
1281                "other.rs": "// Test file",
1282            }),
1283        )
1284        .await;
1285    let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
1286    let project_id = active_call_a
1287        .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
1288        .await
1289        .unwrap();
1290    let project_b = client_b.join_remote_project(project_id, cx_b).await;
1291
1292    // Open a file in an editor as the host.
1293    let buffer_a = project_a
1294        .update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
1295        .await
1296        .unwrap();
1297    let cx_a = cx_a.add_empty_window();
1298    let editor_a = cx_a.new_view(|cx| Editor::for_buffer(buffer_a, Some(project_a.clone()), cx));
1299
1300    let fake_language_server = fake_language_servers.next().await.unwrap();
1301    executor.run_until_parked();
1302
1303    // Receive an OnTypeFormatting request as the host's language server.
1304    // Return some formatting from the host's language server.
1305    fake_language_server.handle_request::<lsp::request::OnTypeFormatting, _, _>(
1306        |params, _| async move {
1307            assert_eq!(
1308                params.text_document_position.text_document.uri,
1309                lsp::Url::from_file_path("/a/main.rs").unwrap(),
1310            );
1311            assert_eq!(
1312                params.text_document_position.position,
1313                lsp::Position::new(0, 14),
1314            );
1315
1316            Ok(Some(vec![lsp::TextEdit {
1317                new_text: "~<".to_string(),
1318                range: lsp::Range::new(lsp::Position::new(0, 14), lsp::Position::new(0, 14)),
1319            }]))
1320        },
1321    );
1322
1323    // Open the buffer on the guest and see that the formatting worked
1324    let buffer_b = project_b
1325        .update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
1326        .await
1327        .unwrap();
1328
1329    // Type a on type formatting trigger character as the guest.
1330    cx_a.focus_view(&editor_a);
1331    editor_a.update(cx_a, |editor, cx| {
1332        editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
1333        editor.handle_input(">", cx);
1334    });
1335
1336    executor.run_until_parked();
1337
1338    buffer_b.read_with(cx_b, |buffer, _| {
1339        assert_eq!(buffer.text(), "fn main() { a>~< }")
1340    });
1341
1342    // Undo should remove LSP edits first
1343    editor_a.update(cx_a, |editor, cx| {
1344        assert_eq!(editor.text(cx), "fn main() { a>~< }");
1345        editor.undo(&Undo, cx);
1346        assert_eq!(editor.text(cx), "fn main() { a> }");
1347    });
1348    executor.run_until_parked();
1349
1350    buffer_b.read_with(cx_b, |buffer, _| {
1351        assert_eq!(buffer.text(), "fn main() { a> }")
1352    });
1353
1354    editor_a.update(cx_a, |editor, cx| {
1355        assert_eq!(editor.text(cx), "fn main() { a> }");
1356        editor.undo(&Undo, cx);
1357        assert_eq!(editor.text(cx), "fn main() { a }");
1358    });
1359    executor.run_until_parked();
1360
1361    buffer_b.read_with(cx_b, |buffer, _| {
1362        assert_eq!(buffer.text(), "fn main() { a }")
1363    });
1364}
1365
1366#[gpui::test(iterations = 10)]
1367async fn test_on_input_format_from_guest_to_host(
1368    cx_a: &mut TestAppContext,
1369    cx_b: &mut TestAppContext,
1370) {
1371    let mut server = TestServer::start(cx_a.executor()).await;
1372    let executor = cx_a.executor();
1373    let client_a = server.create_client(cx_a, "user_a").await;
1374    let client_b = server.create_client(cx_b, "user_b").await;
1375    server
1376        .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
1377        .await;
1378    let active_call_a = cx_a.read(ActiveCall::global);
1379
1380    client_a.language_registry().add(rust_lang());
1381    let mut fake_language_servers = client_a.language_registry().register_fake_lsp(
1382        "Rust",
1383        FakeLspAdapter {
1384            capabilities: lsp::ServerCapabilities {
1385                document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
1386                    first_trigger_character: ":".to_string(),
1387                    more_trigger_character: Some(vec![">".to_string()]),
1388                }),
1389                ..Default::default()
1390            },
1391            ..Default::default()
1392        },
1393    );
1394
1395    client_a
1396        .fs()
1397        .insert_tree(
1398            "/a",
1399            json!({
1400                "main.rs": "fn main() { a }",
1401                "other.rs": "// Test file",
1402            }),
1403        )
1404        .await;
1405    let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
1406    let project_id = active_call_a
1407        .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
1408        .await
1409        .unwrap();
1410    let project_b = client_b.join_remote_project(project_id, cx_b).await;
1411
1412    // Open a file in an editor as the guest.
1413    let buffer_b = project_b
1414        .update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
1415        .await
1416        .unwrap();
1417    let cx_b = cx_b.add_empty_window();
1418    let editor_b = cx_b.new_view(|cx| Editor::for_buffer(buffer_b, Some(project_b.clone()), cx));
1419
1420    let fake_language_server = fake_language_servers.next().await.unwrap();
1421    executor.run_until_parked();
1422
1423    // Type a on type formatting trigger character as the guest.
1424    cx_b.focus_view(&editor_b);
1425    editor_b.update(cx_b, |editor, cx| {
1426        editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
1427        editor.handle_input(":", cx);
1428    });
1429
1430    // Receive an OnTypeFormatting request as the host's language server.
1431    // Return some formatting from the host's language server.
1432    executor.start_waiting();
1433    fake_language_server
1434        .handle_request::<lsp::request::OnTypeFormatting, _, _>(|params, _| async move {
1435            assert_eq!(
1436                params.text_document_position.text_document.uri,
1437                lsp::Url::from_file_path("/a/main.rs").unwrap(),
1438            );
1439            assert_eq!(
1440                params.text_document_position.position,
1441                lsp::Position::new(0, 14),
1442            );
1443
1444            Ok(Some(vec![lsp::TextEdit {
1445                new_text: "~:".to_string(),
1446                range: lsp::Range::new(lsp::Position::new(0, 14), lsp::Position::new(0, 14)),
1447            }]))
1448        })
1449        .next()
1450        .await
1451        .unwrap();
1452    executor.finish_waiting();
1453
1454    // Open the buffer on the host and see that the formatting worked
1455    let buffer_a = project_a
1456        .update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
1457        .await
1458        .unwrap();
1459    executor.run_until_parked();
1460
1461    buffer_a.read_with(cx_a, |buffer, _| {
1462        assert_eq!(buffer.text(), "fn main() { a:~: }")
1463    });
1464
1465    // Undo should remove LSP edits first
1466    editor_b.update(cx_b, |editor, cx| {
1467        assert_eq!(editor.text(cx), "fn main() { a:~: }");
1468        editor.undo(&Undo, cx);
1469        assert_eq!(editor.text(cx), "fn main() { a: }");
1470    });
1471    executor.run_until_parked();
1472
1473    buffer_a.read_with(cx_a, |buffer, _| {
1474        assert_eq!(buffer.text(), "fn main() { a: }")
1475    });
1476
1477    editor_b.update(cx_b, |editor, cx| {
1478        assert_eq!(editor.text(cx), "fn main() { a: }");
1479        editor.undo(&Undo, cx);
1480        assert_eq!(editor.text(cx), "fn main() { a }");
1481    });
1482    executor.run_until_parked();
1483
1484    buffer_a.read_with(cx_a, |buffer, _| {
1485        assert_eq!(buffer.text(), "fn main() { a }")
1486    });
1487}
1488
1489#[gpui::test(iterations = 10)]
1490async fn test_mutual_editor_inlay_hint_cache_update(
1491    cx_a: &mut TestAppContext,
1492    cx_b: &mut TestAppContext,
1493) {
1494    let mut server = TestServer::start(cx_a.executor()).await;
1495    let executor = cx_a.executor();
1496    let client_a = server.create_client(cx_a, "user_a").await;
1497    let client_b = server.create_client(cx_b, "user_b").await;
1498    server
1499        .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
1500        .await;
1501    let active_call_a = cx_a.read(ActiveCall::global);
1502    let active_call_b = cx_b.read(ActiveCall::global);
1503
1504    cx_a.update(editor::init);
1505    cx_b.update(editor::init);
1506
1507    cx_a.update(|cx| {
1508        SettingsStore::update_global(cx, |store, cx| {
1509            store.update_user_settings::<AllLanguageSettings>(cx, |settings| {
1510                settings.defaults.inlay_hints = Some(InlayHintSettings {
1511                    enabled: true,
1512                    edit_debounce_ms: 0,
1513                    scroll_debounce_ms: 0,
1514                    show_type_hints: true,
1515                    show_parameter_hints: false,
1516                    show_other_hints: true,
1517                    show_background: false,
1518                })
1519            });
1520        });
1521    });
1522    cx_b.update(|cx| {
1523        SettingsStore::update_global(cx, |store, cx| {
1524            store.update_user_settings::<AllLanguageSettings>(cx, |settings| {
1525                settings.defaults.inlay_hints = Some(InlayHintSettings {
1526                    enabled: true,
1527                    edit_debounce_ms: 0,
1528                    scroll_debounce_ms: 0,
1529                    show_type_hints: true,
1530                    show_parameter_hints: false,
1531                    show_other_hints: true,
1532                    show_background: false,
1533                })
1534            });
1535        });
1536    });
1537
1538    client_a.language_registry().add(rust_lang());
1539    client_b.language_registry().add(rust_lang());
1540    let mut fake_language_servers = client_a.language_registry().register_fake_lsp(
1541        "Rust",
1542        FakeLspAdapter {
1543            capabilities: lsp::ServerCapabilities {
1544                inlay_hint_provider: Some(lsp::OneOf::Left(true)),
1545                ..Default::default()
1546            },
1547            ..Default::default()
1548        },
1549    );
1550
1551    // Client A opens a project.
1552    client_a
1553        .fs()
1554        .insert_tree(
1555            "/a",
1556            json!({
1557                "main.rs": "fn main() { a } // and some long comment to ensure inlay hints are not trimmed out",
1558                "other.rs": "// Test file",
1559            }),
1560        )
1561        .await;
1562    let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
1563    active_call_a
1564        .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
1565        .await
1566        .unwrap();
1567    let project_id = active_call_a
1568        .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
1569        .await
1570        .unwrap();
1571
1572    // Client B joins the project
1573    let project_b = client_b.join_remote_project(project_id, cx_b).await;
1574    active_call_b
1575        .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
1576        .await
1577        .unwrap();
1578
1579    let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
1580    executor.start_waiting();
1581
1582    // The host opens a rust file.
1583    let _buffer_a = project_a
1584        .update(cx_a, |project, cx| {
1585            project.open_local_buffer("/a/main.rs", cx)
1586        })
1587        .await
1588        .unwrap();
1589    let fake_language_server = fake_language_servers.next().await.unwrap();
1590    let editor_a = workspace_a
1591        .update(cx_a, |workspace, cx| {
1592            workspace.open_path((worktree_id, "main.rs"), None, true, cx)
1593        })
1594        .await
1595        .unwrap()
1596        .downcast::<Editor>()
1597        .unwrap();
1598
1599    // Set up the language server to return an additional inlay hint on each request.
1600    let edits_made = Arc::new(AtomicUsize::new(0));
1601    let closure_edits_made = Arc::clone(&edits_made);
1602    fake_language_server
1603        .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
1604            let task_edits_made = Arc::clone(&closure_edits_made);
1605            async move {
1606                assert_eq!(
1607                    params.text_document.uri,
1608                    lsp::Url::from_file_path("/a/main.rs").unwrap(),
1609                );
1610                let edits_made = task_edits_made.load(atomic::Ordering::Acquire);
1611                Ok(Some(vec![lsp::InlayHint {
1612                    position: lsp::Position::new(0, edits_made as u32),
1613                    label: lsp::InlayHintLabel::String(edits_made.to_string()),
1614                    kind: None,
1615                    text_edits: None,
1616                    tooltip: None,
1617                    padding_left: None,
1618                    padding_right: None,
1619                    data: None,
1620                }]))
1621            }
1622        })
1623        .next()
1624        .await
1625        .unwrap();
1626
1627    executor.run_until_parked();
1628
1629    let initial_edit = edits_made.load(atomic::Ordering::Acquire);
1630    editor_a.update(cx_a, |editor, _| {
1631        assert_eq!(
1632            vec![initial_edit.to_string()],
1633            extract_hint_labels(editor),
1634            "Host should get its first hints when opens an editor"
1635        );
1636        let inlay_cache = editor.inlay_hint_cache();
1637        assert_eq!(
1638            inlay_cache.version(),
1639            1,
1640            "Host editor update the cache version after every cache/view change",
1641        );
1642    });
1643    let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
1644    let editor_b = workspace_b
1645        .update(cx_b, |workspace, cx| {
1646            workspace.open_path((worktree_id, "main.rs"), None, true, cx)
1647        })
1648        .await
1649        .unwrap()
1650        .downcast::<Editor>()
1651        .unwrap();
1652
1653    executor.run_until_parked();
1654    editor_b.update(cx_b, |editor, _| {
1655        assert_eq!(
1656            vec![initial_edit.to_string()],
1657            extract_hint_labels(editor),
1658            "Client should get its first hints when opens an editor"
1659        );
1660        let inlay_cache = editor.inlay_hint_cache();
1661        assert_eq!(
1662            inlay_cache.version(),
1663            1,
1664            "Guest editor update the cache version after every cache/view change"
1665        );
1666    });
1667
1668    let after_client_edit = edits_made.fetch_add(1, atomic::Ordering::Release) + 1;
1669    editor_b.update(cx_b, |editor, cx| {
1670        editor.change_selections(None, cx, |s| s.select_ranges([13..13].clone()));
1671        editor.handle_input(":", cx);
1672    });
1673    cx_b.focus_view(&editor_b);
1674
1675    executor.run_until_parked();
1676    editor_a.update(cx_a, |editor, _| {
1677        assert_eq!(
1678            vec![after_client_edit.to_string()],
1679            extract_hint_labels(editor),
1680        );
1681        let inlay_cache = editor.inlay_hint_cache();
1682        assert_eq!(inlay_cache.version(), 2);
1683    });
1684    editor_b.update(cx_b, |editor, _| {
1685        assert_eq!(
1686            vec![after_client_edit.to_string()],
1687            extract_hint_labels(editor),
1688        );
1689        let inlay_cache = editor.inlay_hint_cache();
1690        assert_eq!(inlay_cache.version(), 2);
1691    });
1692
1693    let after_host_edit = edits_made.fetch_add(1, atomic::Ordering::Release) + 1;
1694    editor_a.update(cx_a, |editor, cx| {
1695        editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
1696        editor.handle_input("a change to increment both buffers' versions", cx);
1697    });
1698    cx_a.focus_view(&editor_a);
1699
1700    executor.run_until_parked();
1701    editor_a.update(cx_a, |editor, _| {
1702        assert_eq!(
1703            vec![after_host_edit.to_string()],
1704            extract_hint_labels(editor),
1705        );
1706        let inlay_cache = editor.inlay_hint_cache();
1707        assert_eq!(inlay_cache.version(), 3);
1708    });
1709    editor_b.update(cx_b, |editor, _| {
1710        assert_eq!(
1711            vec![after_host_edit.to_string()],
1712            extract_hint_labels(editor),
1713        );
1714        let inlay_cache = editor.inlay_hint_cache();
1715        assert_eq!(inlay_cache.version(), 3);
1716    });
1717
1718    let after_special_edit_for_refresh = edits_made.fetch_add(1, atomic::Ordering::Release) + 1;
1719    fake_language_server
1720        .request::<lsp::request::InlayHintRefreshRequest>(())
1721        .await
1722        .expect("inlay refresh request failed");
1723
1724    executor.run_until_parked();
1725    editor_a.update(cx_a, |editor, _| {
1726        assert_eq!(
1727            vec![after_special_edit_for_refresh.to_string()],
1728            extract_hint_labels(editor),
1729            "Host should react to /refresh LSP request"
1730        );
1731        let inlay_cache = editor.inlay_hint_cache();
1732        assert_eq!(
1733            inlay_cache.version(),
1734            4,
1735            "Host should accepted all edits and bump its cache version every time"
1736        );
1737    });
1738    editor_b.update(cx_b, |editor, _| {
1739        assert_eq!(
1740            vec![after_special_edit_for_refresh.to_string()],
1741            extract_hint_labels(editor),
1742            "Guest should get a /refresh LSP request propagated by host"
1743        );
1744        let inlay_cache = editor.inlay_hint_cache();
1745        assert_eq!(
1746            inlay_cache.version(),
1747            4,
1748            "Guest should accepted all edits and bump its cache version every time"
1749        );
1750    });
1751}
1752
1753#[gpui::test(iterations = 10)]
1754async fn test_inlay_hint_refresh_is_forwarded(
1755    cx_a: &mut TestAppContext,
1756    cx_b: &mut TestAppContext,
1757) {
1758    let mut server = TestServer::start(cx_a.executor()).await;
1759    let executor = cx_a.executor();
1760    let client_a = server.create_client(cx_a, "user_a").await;
1761    let client_b = server.create_client(cx_b, "user_b").await;
1762    server
1763        .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
1764        .await;
1765    let active_call_a = cx_a.read(ActiveCall::global);
1766    let active_call_b = cx_b.read(ActiveCall::global);
1767
1768    cx_a.update(editor::init);
1769    cx_b.update(editor::init);
1770
1771    cx_a.update(|cx| {
1772        SettingsStore::update_global(cx, |store, cx| {
1773            store.update_user_settings::<AllLanguageSettings>(cx, |settings| {
1774                settings.defaults.inlay_hints = Some(InlayHintSettings {
1775                    enabled: false,
1776                    edit_debounce_ms: 0,
1777                    scroll_debounce_ms: 0,
1778                    show_type_hints: false,
1779                    show_parameter_hints: false,
1780                    show_other_hints: false,
1781                    show_background: false,
1782                })
1783            });
1784        });
1785    });
1786    cx_b.update(|cx| {
1787        SettingsStore::update_global(cx, |store, cx| {
1788            store.update_user_settings::<AllLanguageSettings>(cx, |settings| {
1789                settings.defaults.inlay_hints = Some(InlayHintSettings {
1790                    enabled: true,
1791                    edit_debounce_ms: 0,
1792                    scroll_debounce_ms: 0,
1793                    show_type_hints: true,
1794                    show_parameter_hints: true,
1795                    show_other_hints: true,
1796                    show_background: false,
1797                })
1798            });
1799        });
1800    });
1801
1802    client_a.language_registry().add(rust_lang());
1803    client_b.language_registry().add(rust_lang());
1804    let mut fake_language_servers = client_a.language_registry().register_fake_lsp(
1805        "Rust",
1806        FakeLspAdapter {
1807            capabilities: lsp::ServerCapabilities {
1808                inlay_hint_provider: Some(lsp::OneOf::Left(true)),
1809                ..Default::default()
1810            },
1811            ..Default::default()
1812        },
1813    );
1814
1815    client_a
1816        .fs()
1817        .insert_tree(
1818            "/a",
1819            json!({
1820                "main.rs": "fn main() { a } // and some long comment to ensure inlay hints are not trimmed out",
1821                "other.rs": "// Test file",
1822            }),
1823        )
1824        .await;
1825    let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
1826    active_call_a
1827        .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
1828        .await
1829        .unwrap();
1830    let project_id = active_call_a
1831        .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
1832        .await
1833        .unwrap();
1834
1835    let project_b = client_b.join_remote_project(project_id, cx_b).await;
1836    active_call_b
1837        .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
1838        .await
1839        .unwrap();
1840
1841    let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
1842    let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
1843
1844    cx_a.background_executor.start_waiting();
1845
1846    let editor_a = workspace_a
1847        .update(cx_a, |workspace, cx| {
1848            workspace.open_path((worktree_id, "main.rs"), None, true, cx)
1849        })
1850        .await
1851        .unwrap()
1852        .downcast::<Editor>()
1853        .unwrap();
1854
1855    let editor_b = workspace_b
1856        .update(cx_b, |workspace, cx| {
1857            workspace.open_path((worktree_id, "main.rs"), None, true, cx)
1858        })
1859        .await
1860        .unwrap()
1861        .downcast::<Editor>()
1862        .unwrap();
1863
1864    let other_hints = Arc::new(AtomicBool::new(false));
1865    let fake_language_server = fake_language_servers.next().await.unwrap();
1866    let closure_other_hints = Arc::clone(&other_hints);
1867    fake_language_server
1868        .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
1869            let task_other_hints = Arc::clone(&closure_other_hints);
1870            async move {
1871                assert_eq!(
1872                    params.text_document.uri,
1873                    lsp::Url::from_file_path("/a/main.rs").unwrap(),
1874                );
1875                let other_hints = task_other_hints.load(atomic::Ordering::Acquire);
1876                let character = if other_hints { 0 } else { 2 };
1877                let label = if other_hints {
1878                    "other hint"
1879                } else {
1880                    "initial hint"
1881                };
1882                Ok(Some(vec![lsp::InlayHint {
1883                    position: lsp::Position::new(0, character),
1884                    label: lsp::InlayHintLabel::String(label.to_string()),
1885                    kind: None,
1886                    text_edits: None,
1887                    tooltip: None,
1888                    padding_left: None,
1889                    padding_right: None,
1890                    data: None,
1891                }]))
1892            }
1893        })
1894        .next()
1895        .await
1896        .unwrap();
1897    executor.finish_waiting();
1898
1899    executor.run_until_parked();
1900    editor_a.update(cx_a, |editor, _| {
1901        assert!(
1902            extract_hint_labels(editor).is_empty(),
1903            "Host should get no hints due to them turned off"
1904        );
1905        let inlay_cache = editor.inlay_hint_cache();
1906        assert_eq!(
1907            inlay_cache.version(),
1908            0,
1909            "Turned off hints should not generate version updates"
1910        );
1911    });
1912
1913    executor.run_until_parked();
1914    editor_b.update(cx_b, |editor, _| {
1915        assert_eq!(
1916            vec!["initial hint".to_string()],
1917            extract_hint_labels(editor),
1918            "Client should get its first hints when opens an editor"
1919        );
1920        let inlay_cache = editor.inlay_hint_cache();
1921        assert_eq!(
1922            inlay_cache.version(),
1923            1,
1924            "Should update cache version after first hints"
1925        );
1926    });
1927
1928    other_hints.fetch_or(true, atomic::Ordering::Release);
1929    fake_language_server
1930        .request::<lsp::request::InlayHintRefreshRequest>(())
1931        .await
1932        .expect("inlay refresh request failed");
1933    executor.run_until_parked();
1934    editor_a.update(cx_a, |editor, _| {
1935        assert!(
1936            extract_hint_labels(editor).is_empty(),
1937            "Host should get nop hints due to them turned off, even after the /refresh"
1938        );
1939        let inlay_cache = editor.inlay_hint_cache();
1940        assert_eq!(
1941            inlay_cache.version(),
1942            0,
1943            "Turned off hints should not generate version updates, again"
1944        );
1945    });
1946
1947    executor.run_until_parked();
1948    editor_b.update(cx_b, |editor, _| {
1949        assert_eq!(
1950            vec!["other hint".to_string()],
1951            extract_hint_labels(editor),
1952            "Guest should get a /refresh LSP request propagated by host despite host hints are off"
1953        );
1954        let inlay_cache = editor.inlay_hint_cache();
1955        assert_eq!(
1956            inlay_cache.version(),
1957            2,
1958            "Guest should accepted all edits and bump its cache version every time"
1959        );
1960    });
1961}
1962
1963#[gpui::test(iterations = 10)]
1964async fn test_git_blame_is_forwarded(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
1965    let mut server = TestServer::start(cx_a.executor()).await;
1966    let client_a = server.create_client(cx_a, "user_a").await;
1967    let client_b = server.create_client(cx_b, "user_b").await;
1968    server
1969        .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
1970        .await;
1971    let active_call_a = cx_a.read(ActiveCall::global);
1972
1973    cx_a.update(editor::init);
1974    cx_b.update(editor::init);
1975    // Turn inline-blame-off by default so no state is transferred without us explicitly doing so
1976    let inline_blame_off_settings = Some(InlineBlameSettings {
1977        enabled: false,
1978        delay_ms: None,
1979        min_column: None,
1980    });
1981    cx_a.update(|cx| {
1982        SettingsStore::update_global(cx, |store, cx| {
1983            store.update_user_settings::<ProjectSettings>(cx, |settings| {
1984                settings.git.inline_blame = inline_blame_off_settings;
1985            });
1986        });
1987    });
1988    cx_b.update(|cx| {
1989        SettingsStore::update_global(cx, |store, cx| {
1990            store.update_user_settings::<ProjectSettings>(cx, |settings| {
1991                settings.git.inline_blame = inline_blame_off_settings;
1992            });
1993        });
1994    });
1995
1996    client_a
1997        .fs()
1998        .insert_tree(
1999            "/my-repo",
2000            json!({
2001                ".git": {},
2002                "file.txt": "line1\nline2\nline3\nline\n",
2003            }),
2004        )
2005        .await;
2006
2007    let blame = git::blame::Blame {
2008        entries: vec![
2009            blame_entry("1b1b1b", 0..1),
2010            blame_entry("0d0d0d", 1..2),
2011            blame_entry("3a3a3a", 2..3),
2012            blame_entry("4c4c4c", 3..4),
2013        ],
2014        permalinks: HashMap::default(), // This field is deprecrated
2015        messages: [
2016            ("1b1b1b", "message for idx-0"),
2017            ("0d0d0d", "message for idx-1"),
2018            ("3a3a3a", "message for idx-2"),
2019            ("4c4c4c", "message for idx-3"),
2020        ]
2021        .into_iter()
2022        .map(|(sha, message)| (sha.parse().unwrap(), message.into()))
2023        .collect(),
2024        remote_url: Some("git@github.com:zed-industries/zed.git".to_string()),
2025    };
2026    client_a.fs().set_blame_for_repo(
2027        Path::new("/my-repo/.git"),
2028        vec![(Path::new("file.txt"), blame)],
2029    );
2030
2031    let (project_a, worktree_id) = client_a.build_local_project("/my-repo", cx_a).await;
2032    let project_id = active_call_a
2033        .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
2034        .await
2035        .unwrap();
2036
2037    // Create editor_a
2038    let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
2039    let editor_a = workspace_a
2040        .update(cx_a, |workspace, cx| {
2041            workspace.open_path((worktree_id, "file.txt"), None, true, cx)
2042        })
2043        .await
2044        .unwrap()
2045        .downcast::<Editor>()
2046        .unwrap();
2047
2048    // Join the project as client B.
2049    let project_b = client_b.join_remote_project(project_id, cx_b).await;
2050    let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
2051    let editor_b = workspace_b
2052        .update(cx_b, |workspace, cx| {
2053            workspace.open_path((worktree_id, "file.txt"), None, true, cx)
2054        })
2055        .await
2056        .unwrap()
2057        .downcast::<Editor>()
2058        .unwrap();
2059
2060    // client_b now requests git blame for the open buffer
2061    editor_b.update(cx_b, |editor_b, cx| {
2062        assert!(editor_b.blame().is_none());
2063        editor_b.toggle_git_blame(&editor::actions::ToggleGitBlame {}, cx);
2064    });
2065
2066    cx_a.executor().run_until_parked();
2067    cx_b.executor().run_until_parked();
2068
2069    editor_b.update(cx_b, |editor_b, cx| {
2070        let blame = editor_b.blame().expect("editor_b should have blame now");
2071        let entries = blame.update(cx, |blame, cx| {
2072            blame
2073                .blame_for_rows((0..4).map(MultiBufferRow).map(Some), cx)
2074                .collect::<Vec<_>>()
2075        });
2076
2077        assert_eq!(
2078            entries,
2079            vec![
2080                Some(blame_entry("1b1b1b", 0..1)),
2081                Some(blame_entry("0d0d0d", 1..2)),
2082                Some(blame_entry("3a3a3a", 2..3)),
2083                Some(blame_entry("4c4c4c", 3..4)),
2084            ]
2085        );
2086
2087        blame.update(cx, |blame, _| {
2088            for (idx, entry) in entries.iter().flatten().enumerate() {
2089                let details = blame.details_for_entry(entry).unwrap();
2090                assert_eq!(details.message, format!("message for idx-{}", idx));
2091                assert_eq!(
2092                    details.permalink.unwrap().to_string(),
2093                    format!("https://github.com/zed-industries/zed/commit/{}", entry.sha)
2094                );
2095            }
2096        });
2097    });
2098
2099    // editor_b updates the file, which gets sent to client_a, which updates git blame,
2100    // which gets back to client_b.
2101    editor_b.update(cx_b, |editor_b, cx| {
2102        editor_b.edit([(Point::new(0, 3)..Point::new(0, 3), "FOO")], cx);
2103    });
2104
2105    cx_a.executor().run_until_parked();
2106    cx_b.executor().run_until_parked();
2107
2108    editor_b.update(cx_b, |editor_b, cx| {
2109        let blame = editor_b.blame().expect("editor_b should have blame now");
2110        let entries = blame.update(cx, |blame, cx| {
2111            blame
2112                .blame_for_rows((0..4).map(MultiBufferRow).map(Some), cx)
2113                .collect::<Vec<_>>()
2114        });
2115
2116        assert_eq!(
2117            entries,
2118            vec![
2119                None,
2120                Some(blame_entry("0d0d0d", 1..2)),
2121                Some(blame_entry("3a3a3a", 2..3)),
2122                Some(blame_entry("4c4c4c", 3..4)),
2123            ]
2124        );
2125    });
2126
2127    // Now editor_a also updates the file
2128    editor_a.update(cx_a, |editor_a, cx| {
2129        editor_a.edit([(Point::new(1, 3)..Point::new(1, 3), "FOO")], cx);
2130    });
2131
2132    cx_a.executor().run_until_parked();
2133    cx_b.executor().run_until_parked();
2134
2135    editor_b.update(cx_b, |editor_b, cx| {
2136        let blame = editor_b.blame().expect("editor_b should have blame now");
2137        let entries = blame.update(cx, |blame, cx| {
2138            blame
2139                .blame_for_rows((0..4).map(MultiBufferRow).map(Some), cx)
2140                .collect::<Vec<_>>()
2141        });
2142
2143        assert_eq!(
2144            entries,
2145            vec![
2146                None,
2147                None,
2148                Some(blame_entry("3a3a3a", 2..3)),
2149                Some(blame_entry("4c4c4c", 3..4)),
2150            ]
2151        );
2152    });
2153}
2154
2155fn extract_hint_labels(editor: &Editor) -> Vec<String> {
2156    let mut labels = Vec::new();
2157    for hint in editor.inlay_hint_cache().hints() {
2158        match hint.label {
2159            project::InlayHintLabel::String(s) => labels.push(s),
2160            _ => unreachable!(),
2161        }
2162    }
2163    labels
2164}
2165
2166fn blame_entry(sha: &str, range: Range<u32>) -> git::blame::BlameEntry {
2167    git::blame::BlameEntry {
2168        sha: sha.parse().unwrap(),
2169        range,
2170        ..Default::default()
2171    }
2172}