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