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