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