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