editor_tests.rs

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