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