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