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