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                })
1541            });
1542        });
1543    });
1544    cx_b.update(|cx| {
1545        SettingsStore::update_global(cx, |store, cx| {
1546            store.update_user_settings::<AllLanguageSettings>(cx, |settings| {
1547                settings.defaults.inlay_hints = Some(InlayHintSettings {
1548                    enabled: true,
1549                    edit_debounce_ms: 0,
1550                    scroll_debounce_ms: 0,
1551                    show_type_hints: true,
1552                    show_parameter_hints: false,
1553                    show_other_hints: true,
1554                    show_background: false,
1555                })
1556            });
1557        });
1558    });
1559
1560    client_a.language_registry().add(rust_lang());
1561    client_b.language_registry().add(rust_lang());
1562    let mut fake_language_servers = client_a.language_registry().register_fake_lsp(
1563        "Rust",
1564        FakeLspAdapter {
1565            capabilities: lsp::ServerCapabilities {
1566                inlay_hint_provider: Some(lsp::OneOf::Left(true)),
1567                ..Default::default()
1568            },
1569            ..Default::default()
1570        },
1571    );
1572
1573    // Client A opens a project.
1574    client_a
1575        .fs()
1576        .insert_tree(
1577            "/a",
1578            json!({
1579                "main.rs": "fn main() { a } // and some long comment to ensure inlay hints are not trimmed out",
1580                "other.rs": "// Test file",
1581            }),
1582        )
1583        .await;
1584    let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
1585    active_call_a
1586        .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
1587        .await
1588        .unwrap();
1589    let project_id = active_call_a
1590        .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
1591        .await
1592        .unwrap();
1593
1594    // Client B joins the project
1595    let project_b = client_b.join_remote_project(project_id, cx_b).await;
1596    active_call_b
1597        .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
1598        .await
1599        .unwrap();
1600
1601    let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
1602    executor.start_waiting();
1603
1604    // The host opens a rust file.
1605    let _buffer_a = project_a
1606        .update(cx_a, |project, cx| {
1607            project.open_local_buffer("/a/main.rs", cx)
1608        })
1609        .await
1610        .unwrap();
1611    let editor_a = workspace_a
1612        .update_in(cx_a, |workspace, window, cx| {
1613            workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
1614        })
1615        .await
1616        .unwrap()
1617        .downcast::<Editor>()
1618        .unwrap();
1619
1620    let fake_language_server = fake_language_servers.next().await.unwrap();
1621
1622    // Set up the language server to return an additional inlay hint on each request.
1623    let edits_made = Arc::new(AtomicUsize::new(0));
1624    let closure_edits_made = Arc::clone(&edits_made);
1625    fake_language_server
1626        .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
1627            let task_edits_made = Arc::clone(&closure_edits_made);
1628            async move {
1629                assert_eq!(
1630                    params.text_document.uri,
1631                    lsp::Url::from_file_path("/a/main.rs").unwrap(),
1632                );
1633                let edits_made = task_edits_made.load(atomic::Ordering::Acquire);
1634                Ok(Some(vec![lsp::InlayHint {
1635                    position: lsp::Position::new(0, edits_made as u32),
1636                    label: lsp::InlayHintLabel::String(edits_made.to_string()),
1637                    kind: None,
1638                    text_edits: None,
1639                    tooltip: None,
1640                    padding_left: None,
1641                    padding_right: None,
1642                    data: None,
1643                }]))
1644            }
1645        })
1646        .next()
1647        .await
1648        .unwrap();
1649
1650    executor.run_until_parked();
1651
1652    let initial_edit = edits_made.load(atomic::Ordering::Acquire);
1653    editor_a.update(cx_a, |editor, _| {
1654        assert_eq!(
1655            vec![initial_edit.to_string()],
1656            extract_hint_labels(editor),
1657            "Host should get its first hints when opens an editor"
1658        );
1659    });
1660    let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
1661    let editor_b = workspace_b
1662        .update_in(cx_b, |workspace, window, cx| {
1663            workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
1664        })
1665        .await
1666        .unwrap()
1667        .downcast::<Editor>()
1668        .unwrap();
1669
1670    executor.run_until_parked();
1671    editor_b.update(cx_b, |editor, _| {
1672        assert_eq!(
1673            vec![initial_edit.to_string()],
1674            extract_hint_labels(editor),
1675            "Client should get its first hints when opens an editor"
1676        );
1677    });
1678
1679    let after_client_edit = edits_made.fetch_add(1, atomic::Ordering::Release) + 1;
1680    editor_b.update_in(cx_b, |editor, window, cx| {
1681        editor.change_selections(None, window, cx, |s| s.select_ranges([13..13].clone()));
1682        editor.handle_input(":", window, cx);
1683    });
1684    cx_b.focus(&editor_b);
1685
1686    executor.run_until_parked();
1687    editor_a.update(cx_a, |editor, _| {
1688        assert_eq!(
1689            vec![after_client_edit.to_string()],
1690            extract_hint_labels(editor),
1691        );
1692    });
1693    editor_b.update(cx_b, |editor, _| {
1694        assert_eq!(
1695            vec![after_client_edit.to_string()],
1696            extract_hint_labels(editor),
1697        );
1698    });
1699
1700    let after_host_edit = edits_made.fetch_add(1, atomic::Ordering::Release) + 1;
1701    editor_a.update_in(cx_a, |editor, window, cx| {
1702        editor.change_selections(None, window, cx, |s| s.select_ranges([13..13]));
1703        editor.handle_input("a change to increment both buffers' versions", window, cx);
1704    });
1705    cx_a.focus(&editor_a);
1706
1707    executor.run_until_parked();
1708    editor_a.update(cx_a, |editor, _| {
1709        assert_eq!(
1710            vec![after_host_edit.to_string()],
1711            extract_hint_labels(editor),
1712        );
1713    });
1714    editor_b.update(cx_b, |editor, _| {
1715        assert_eq!(
1716            vec![after_host_edit.to_string()],
1717            extract_hint_labels(editor),
1718        );
1719    });
1720
1721    let after_special_edit_for_refresh = edits_made.fetch_add(1, atomic::Ordering::Release) + 1;
1722    fake_language_server
1723        .request::<lsp::request::InlayHintRefreshRequest>(())
1724        .await
1725        .expect("inlay refresh request failed");
1726
1727    executor.run_until_parked();
1728    editor_a.update(cx_a, |editor, _| {
1729        assert_eq!(
1730            vec![after_special_edit_for_refresh.to_string()],
1731            extract_hint_labels(editor),
1732            "Host should react to /refresh LSP request"
1733        );
1734    });
1735    editor_b.update(cx_b, |editor, _| {
1736        assert_eq!(
1737            vec![after_special_edit_for_refresh.to_string()],
1738            extract_hint_labels(editor),
1739            "Guest should get a /refresh LSP request propagated by host"
1740        );
1741    });
1742}
1743
1744#[gpui::test(iterations = 10)]
1745async fn test_inlay_hint_refresh_is_forwarded(
1746    cx_a: &mut TestAppContext,
1747    cx_b: &mut TestAppContext,
1748) {
1749    let mut server = TestServer::start(cx_a.executor()).await;
1750    let executor = cx_a.executor();
1751    let client_a = server.create_client(cx_a, "user_a").await;
1752    let client_b = server.create_client(cx_b, "user_b").await;
1753    server
1754        .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
1755        .await;
1756    let active_call_a = cx_a.read(ActiveCall::global);
1757    let active_call_b = cx_b.read(ActiveCall::global);
1758
1759    cx_a.update(editor::init);
1760    cx_b.update(editor::init);
1761
1762    cx_a.update(|cx| {
1763        SettingsStore::update_global(cx, |store, cx| {
1764            store.update_user_settings::<AllLanguageSettings>(cx, |settings| {
1765                settings.defaults.inlay_hints = Some(InlayHintSettings {
1766                    enabled: false,
1767                    edit_debounce_ms: 0,
1768                    scroll_debounce_ms: 0,
1769                    show_type_hints: false,
1770                    show_parameter_hints: false,
1771                    show_other_hints: false,
1772                    show_background: false,
1773                })
1774            });
1775        });
1776    });
1777    cx_b.update(|cx| {
1778        SettingsStore::update_global(cx, |store, cx| {
1779            store.update_user_settings::<AllLanguageSettings>(cx, |settings| {
1780                settings.defaults.inlay_hints = Some(InlayHintSettings {
1781                    enabled: true,
1782                    edit_debounce_ms: 0,
1783                    scroll_debounce_ms: 0,
1784                    show_type_hints: true,
1785                    show_parameter_hints: true,
1786                    show_other_hints: true,
1787                    show_background: false,
1788                })
1789            });
1790        });
1791    });
1792
1793    client_a.language_registry().add(rust_lang());
1794    client_b.language_registry().add(rust_lang());
1795    let mut fake_language_servers = client_a.language_registry().register_fake_lsp(
1796        "Rust",
1797        FakeLspAdapter {
1798            capabilities: lsp::ServerCapabilities {
1799                inlay_hint_provider: Some(lsp::OneOf::Left(true)),
1800                ..Default::default()
1801            },
1802            ..Default::default()
1803        },
1804    );
1805
1806    client_a
1807        .fs()
1808        .insert_tree(
1809            "/a",
1810            json!({
1811                "main.rs": "fn main() { a } // and some long comment to ensure inlay hints are not trimmed out",
1812                "other.rs": "// Test file",
1813            }),
1814        )
1815        .await;
1816    let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
1817    active_call_a
1818        .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
1819        .await
1820        .unwrap();
1821    let project_id = active_call_a
1822        .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
1823        .await
1824        .unwrap();
1825
1826    let project_b = client_b.join_remote_project(project_id, cx_b).await;
1827    active_call_b
1828        .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
1829        .await
1830        .unwrap();
1831
1832    let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
1833    let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
1834
1835    cx_a.background_executor.start_waiting();
1836
1837    let editor_a = workspace_a
1838        .update_in(cx_a, |workspace, window, cx| {
1839            workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
1840        })
1841        .await
1842        .unwrap()
1843        .downcast::<Editor>()
1844        .unwrap();
1845
1846    let editor_b = workspace_b
1847        .update_in(cx_b, |workspace, window, cx| {
1848            workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
1849        })
1850        .await
1851        .unwrap()
1852        .downcast::<Editor>()
1853        .unwrap();
1854
1855    let other_hints = Arc::new(AtomicBool::new(false));
1856    let fake_language_server = fake_language_servers.next().await.unwrap();
1857    let closure_other_hints = Arc::clone(&other_hints);
1858    fake_language_server
1859        .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
1860            let task_other_hints = Arc::clone(&closure_other_hints);
1861            async move {
1862                assert_eq!(
1863                    params.text_document.uri,
1864                    lsp::Url::from_file_path("/a/main.rs").unwrap(),
1865                );
1866                let other_hints = task_other_hints.load(atomic::Ordering::Acquire);
1867                let character = if other_hints { 0 } else { 2 };
1868                let label = if other_hints {
1869                    "other hint"
1870                } else {
1871                    "initial hint"
1872                };
1873                Ok(Some(vec![lsp::InlayHint {
1874                    position: lsp::Position::new(0, character),
1875                    label: lsp::InlayHintLabel::String(label.to_string()),
1876                    kind: None,
1877                    text_edits: None,
1878                    tooltip: None,
1879                    padding_left: None,
1880                    padding_right: None,
1881                    data: None,
1882                }]))
1883            }
1884        })
1885        .next()
1886        .await
1887        .unwrap();
1888    executor.finish_waiting();
1889
1890    executor.run_until_parked();
1891    editor_a.update(cx_a, |editor, _| {
1892        assert!(
1893            extract_hint_labels(editor).is_empty(),
1894            "Host should get no hints due to them turned off"
1895        );
1896    });
1897
1898    executor.run_until_parked();
1899    editor_b.update(cx_b, |editor, _| {
1900        assert_eq!(
1901            vec!["initial hint".to_string()],
1902            extract_hint_labels(editor),
1903            "Client should get its first hints when opens an editor"
1904        );
1905    });
1906
1907    other_hints.fetch_or(true, atomic::Ordering::Release);
1908    fake_language_server
1909        .request::<lsp::request::InlayHintRefreshRequest>(())
1910        .await
1911        .expect("inlay refresh request failed");
1912    executor.run_until_parked();
1913    editor_a.update(cx_a, |editor, _| {
1914        assert!(
1915            extract_hint_labels(editor).is_empty(),
1916            "Host should get no hints due to them turned off, even after the /refresh"
1917        );
1918    });
1919
1920    executor.run_until_parked();
1921    editor_b.update(cx_b, |editor, _| {
1922        assert_eq!(
1923            vec!["other hint".to_string()],
1924            extract_hint_labels(editor),
1925            "Guest should get a /refresh LSP request propagated by host despite host hints are off"
1926        );
1927    });
1928}
1929
1930#[gpui::test(iterations = 10)]
1931async fn test_git_blame_is_forwarded(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
1932    let mut server = TestServer::start(cx_a.executor()).await;
1933    let client_a = server.create_client(cx_a, "user_a").await;
1934    let client_b = server.create_client(cx_b, "user_b").await;
1935    server
1936        .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
1937        .await;
1938    let active_call_a = cx_a.read(ActiveCall::global);
1939
1940    cx_a.update(editor::init);
1941    cx_b.update(editor::init);
1942    // Turn inline-blame-off by default so no state is transferred without us explicitly doing so
1943    let inline_blame_off_settings = Some(InlineBlameSettings {
1944        enabled: false,
1945        delay_ms: None,
1946        min_column: None,
1947        show_commit_summary: false,
1948    });
1949    cx_a.update(|cx| {
1950        SettingsStore::update_global(cx, |store, cx| {
1951            store.update_user_settings::<ProjectSettings>(cx, |settings| {
1952                settings.git.inline_blame = inline_blame_off_settings;
1953            });
1954        });
1955    });
1956    cx_b.update(|cx| {
1957        SettingsStore::update_global(cx, |store, cx| {
1958            store.update_user_settings::<ProjectSettings>(cx, |settings| {
1959                settings.git.inline_blame = inline_blame_off_settings;
1960            });
1961        });
1962    });
1963
1964    client_a
1965        .fs()
1966        .insert_tree(
1967            "/my-repo",
1968            json!({
1969                ".git": {},
1970                "file.txt": "line1\nline2\nline3\nline\n",
1971            }),
1972        )
1973        .await;
1974
1975    let blame = git::blame::Blame {
1976        entries: vec![
1977            blame_entry("1b1b1b", 0..1),
1978            blame_entry("0d0d0d", 1..2),
1979            blame_entry("3a3a3a", 2..3),
1980            blame_entry("4c4c4c", 3..4),
1981        ],
1982        permalinks: HashMap::default(), // This field is deprecrated
1983        messages: [
1984            ("1b1b1b", "message for idx-0"),
1985            ("0d0d0d", "message for idx-1"),
1986            ("3a3a3a", "message for idx-2"),
1987            ("4c4c4c", "message for idx-3"),
1988        ]
1989        .into_iter()
1990        .map(|(sha, message)| (sha.parse().unwrap(), message.into()))
1991        .collect(),
1992        remote_url: Some("git@github.com:zed-industries/zed.git".to_string()),
1993    };
1994    client_a
1995        .fs()
1996        .set_blame_for_repo(Path::new("/my-repo/.git"), vec![("file.txt".into(), blame)]);
1997
1998    let (project_a, worktree_id) = client_a.build_local_project("/my-repo", cx_a).await;
1999    let project_id = active_call_a
2000        .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
2001        .await
2002        .unwrap();
2003
2004    // Create editor_a
2005    let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
2006    let editor_a = workspace_a
2007        .update_in(cx_a, |workspace, window, cx| {
2008            workspace.open_path((worktree_id, "file.txt"), None, true, window, cx)
2009        })
2010        .await
2011        .unwrap()
2012        .downcast::<Editor>()
2013        .unwrap();
2014
2015    // Join the project as client B.
2016    let project_b = client_b.join_remote_project(project_id, cx_b).await;
2017    let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
2018    let editor_b = workspace_b
2019        .update_in(cx_b, |workspace, window, cx| {
2020            workspace.open_path((worktree_id, "file.txt"), None, true, window, cx)
2021        })
2022        .await
2023        .unwrap()
2024        .downcast::<Editor>()
2025        .unwrap();
2026
2027    // client_b now requests git blame for the open buffer
2028    editor_b.update_in(cx_b, |editor_b, window, cx| {
2029        assert!(editor_b.blame().is_none());
2030        editor_b.toggle_git_blame(&editor::actions::ToggleGitBlame {}, window, cx);
2031    });
2032
2033    cx_a.executor().run_until_parked();
2034    cx_b.executor().run_until_parked();
2035
2036    editor_b.update(cx_b, |editor_b, cx| {
2037        let blame = editor_b.blame().expect("editor_b should have blame now");
2038        let entries = blame.update(cx, |blame, cx| {
2039            blame
2040                .blame_for_rows(
2041                    &(0..4)
2042                        .map(|row| RowInfo {
2043                            buffer_row: Some(row),
2044                            ..Default::default()
2045                        })
2046                        .collect::<Vec<_>>(),
2047                    cx,
2048                )
2049                .collect::<Vec<_>>()
2050        });
2051
2052        assert_eq!(
2053            entries,
2054            vec![
2055                Some(blame_entry("1b1b1b", 0..1)),
2056                Some(blame_entry("0d0d0d", 1..2)),
2057                Some(blame_entry("3a3a3a", 2..3)),
2058                Some(blame_entry("4c4c4c", 3..4)),
2059            ]
2060        );
2061
2062        blame.update(cx, |blame, _| {
2063            for (idx, entry) in entries.iter().flatten().enumerate() {
2064                let details = blame.details_for_entry(entry).unwrap();
2065                assert_eq!(details.message, format!("message for idx-{}", idx));
2066                assert_eq!(
2067                    details.permalink.unwrap().to_string(),
2068                    format!("https://github.com/zed-industries/zed/commit/{}", entry.sha)
2069                );
2070            }
2071        });
2072    });
2073
2074    // editor_b updates the file, which gets sent to client_a, which updates git blame,
2075    // which gets back to client_b.
2076    editor_b.update_in(cx_b, |editor_b, _, cx| {
2077        editor_b.edit([(Point::new(0, 3)..Point::new(0, 3), "FOO")], cx);
2078    });
2079
2080    cx_a.executor().run_until_parked();
2081    cx_b.executor().run_until_parked();
2082
2083    editor_b.update(cx_b, |editor_b, cx| {
2084        let blame = editor_b.blame().expect("editor_b should have blame now");
2085        let entries = blame.update(cx, |blame, cx| {
2086            blame
2087                .blame_for_rows(
2088                    &(0..4)
2089                        .map(|row| RowInfo {
2090                            buffer_row: Some(row),
2091                            ..Default::default()
2092                        })
2093                        .collect::<Vec<_>>(),
2094                    cx,
2095                )
2096                .collect::<Vec<_>>()
2097        });
2098
2099        assert_eq!(
2100            entries,
2101            vec![
2102                None,
2103                Some(blame_entry("0d0d0d", 1..2)),
2104                Some(blame_entry("3a3a3a", 2..3)),
2105                Some(blame_entry("4c4c4c", 3..4)),
2106            ]
2107        );
2108    });
2109
2110    // Now editor_a also updates the file
2111    editor_a.update_in(cx_a, |editor_a, _, cx| {
2112        editor_a.edit([(Point::new(1, 3)..Point::new(1, 3), "FOO")], cx);
2113    });
2114
2115    cx_a.executor().run_until_parked();
2116    cx_b.executor().run_until_parked();
2117
2118    editor_b.update(cx_b, |editor_b, cx| {
2119        let blame = editor_b.blame().expect("editor_b should have blame now");
2120        let entries = blame.update(cx, |blame, cx| {
2121            blame
2122                .blame_for_rows(
2123                    &(0..4)
2124                        .map(|row| RowInfo {
2125                            buffer_row: Some(row),
2126                            ..Default::default()
2127                        })
2128                        .collect::<Vec<_>>(),
2129                    cx,
2130                )
2131                .collect::<Vec<_>>()
2132        });
2133
2134        assert_eq!(
2135            entries,
2136            vec![
2137                None,
2138                None,
2139                Some(blame_entry("3a3a3a", 2..3)),
2140                Some(blame_entry("4c4c4c", 3..4)),
2141            ]
2142        );
2143    });
2144}
2145
2146#[gpui::test(iterations = 30)]
2147async fn test_collaborating_with_editorconfig(
2148    cx_a: &mut TestAppContext,
2149    cx_b: &mut TestAppContext,
2150) {
2151    let mut server = TestServer::start(cx_a.executor()).await;
2152    let client_a = server.create_client(cx_a, "user_a").await;
2153    let client_b = server.create_client(cx_b, "user_b").await;
2154    server
2155        .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
2156        .await;
2157    let active_call_a = cx_a.read(ActiveCall::global);
2158
2159    cx_b.update(editor::init);
2160
2161    // Set up a fake language server.
2162    client_a.language_registry().add(rust_lang());
2163    client_a
2164        .fs()
2165        .insert_tree(
2166            "/a",
2167            json!({
2168                "src": {
2169                    "main.rs": "mod other;\nfn main() { let foo = other::foo(); }",
2170                    "other_mod": {
2171                        "other.rs": "pub fn foo() -> usize {\n    4\n}",
2172                        ".editorconfig": "",
2173                    },
2174                },
2175                ".editorconfig": "[*]\ntab_width = 2\n",
2176            }),
2177        )
2178        .await;
2179    let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
2180    let project_id = active_call_a
2181        .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
2182        .await
2183        .unwrap();
2184    let main_buffer_a = project_a
2185        .update(cx_a, |p, cx| {
2186            p.open_buffer((worktree_id, "src/main.rs"), cx)
2187        })
2188        .await
2189        .unwrap();
2190    let other_buffer_a = project_a
2191        .update(cx_a, |p, cx| {
2192            p.open_buffer((worktree_id, "src/other_mod/other.rs"), cx)
2193        })
2194        .await
2195        .unwrap();
2196    let cx_a = cx_a.add_empty_window();
2197    let main_editor_a = cx_a.new_window_entity(|window, cx| {
2198        Editor::for_buffer(main_buffer_a, Some(project_a.clone()), window, cx)
2199    });
2200    let other_editor_a = cx_a.new_window_entity(|window, cx| {
2201        Editor::for_buffer(other_buffer_a, Some(project_a), window, cx)
2202    });
2203    let mut main_editor_cx_a = EditorTestContext {
2204        cx: cx_a.clone(),
2205        window: cx_a.window_handle(),
2206        editor: main_editor_a,
2207        assertion_cx: AssertionContextManager::new(),
2208    };
2209    let mut other_editor_cx_a = EditorTestContext {
2210        cx: cx_a.clone(),
2211        window: cx_a.window_handle(),
2212        editor: other_editor_a,
2213        assertion_cx: AssertionContextManager::new(),
2214    };
2215
2216    // Join the project as client B.
2217    let project_b = client_b.join_remote_project(project_id, cx_b).await;
2218    let main_buffer_b = project_b
2219        .update(cx_b, |p, cx| {
2220            p.open_buffer((worktree_id, "src/main.rs"), cx)
2221        })
2222        .await
2223        .unwrap();
2224    let other_buffer_b = project_b
2225        .update(cx_b, |p, cx| {
2226            p.open_buffer((worktree_id, "src/other_mod/other.rs"), cx)
2227        })
2228        .await
2229        .unwrap();
2230    let cx_b = cx_b.add_empty_window();
2231    let main_editor_b = cx_b.new_window_entity(|window, cx| {
2232        Editor::for_buffer(main_buffer_b, Some(project_b.clone()), window, cx)
2233    });
2234    let other_editor_b = cx_b.new_window_entity(|window, cx| {
2235        Editor::for_buffer(other_buffer_b, Some(project_b.clone()), window, cx)
2236    });
2237    let mut main_editor_cx_b = EditorTestContext {
2238        cx: cx_b.clone(),
2239        window: cx_b.window_handle(),
2240        editor: main_editor_b,
2241        assertion_cx: AssertionContextManager::new(),
2242    };
2243    let mut other_editor_cx_b = EditorTestContext {
2244        cx: cx_b.clone(),
2245        window: cx_b.window_handle(),
2246        editor: other_editor_b,
2247        assertion_cx: AssertionContextManager::new(),
2248    };
2249
2250    let initial_main = indoc! {"
2251ˇmod other;
2252fn main() { let foo = other::foo(); }"};
2253    let initial_other = indoc! {"
2254ˇpub fn foo() -> usize {
2255    4
2256}"};
2257
2258    let first_tabbed_main = indoc! {"
2259  ˇmod other;
2260fn main() { let foo = other::foo(); }"};
2261    tab_undo_assert(
2262        &mut main_editor_cx_a,
2263        &mut main_editor_cx_b,
2264        initial_main,
2265        first_tabbed_main,
2266        true,
2267    );
2268    tab_undo_assert(
2269        &mut main_editor_cx_a,
2270        &mut main_editor_cx_b,
2271        initial_main,
2272        first_tabbed_main,
2273        false,
2274    );
2275
2276    let first_tabbed_other = indoc! {"
2277  ˇpub fn foo() -> usize {
2278    4
2279}"};
2280    tab_undo_assert(
2281        &mut other_editor_cx_a,
2282        &mut other_editor_cx_b,
2283        initial_other,
2284        first_tabbed_other,
2285        true,
2286    );
2287    tab_undo_assert(
2288        &mut other_editor_cx_a,
2289        &mut other_editor_cx_b,
2290        initial_other,
2291        first_tabbed_other,
2292        false,
2293    );
2294
2295    client_a
2296        .fs()
2297        .atomic_write(
2298            PathBuf::from("/a/src/.editorconfig"),
2299            "[*]\ntab_width = 3\n".to_owned(),
2300        )
2301        .await
2302        .unwrap();
2303    cx_a.run_until_parked();
2304    cx_b.run_until_parked();
2305
2306    let second_tabbed_main = indoc! {"
2307   ˇmod other;
2308fn main() { let foo = other::foo(); }"};
2309    tab_undo_assert(
2310        &mut main_editor_cx_a,
2311        &mut main_editor_cx_b,
2312        initial_main,
2313        second_tabbed_main,
2314        true,
2315    );
2316    tab_undo_assert(
2317        &mut main_editor_cx_a,
2318        &mut main_editor_cx_b,
2319        initial_main,
2320        second_tabbed_main,
2321        false,
2322    );
2323
2324    let second_tabbed_other = indoc! {"
2325   ˇpub fn foo() -> usize {
2326    4
2327}"};
2328    tab_undo_assert(
2329        &mut other_editor_cx_a,
2330        &mut other_editor_cx_b,
2331        initial_other,
2332        second_tabbed_other,
2333        true,
2334    );
2335    tab_undo_assert(
2336        &mut other_editor_cx_a,
2337        &mut other_editor_cx_b,
2338        initial_other,
2339        second_tabbed_other,
2340        false,
2341    );
2342
2343    let editorconfig_buffer_b = project_b
2344        .update(cx_b, |p, cx| {
2345            p.open_buffer((worktree_id, "src/other_mod/.editorconfig"), cx)
2346        })
2347        .await
2348        .unwrap();
2349    editorconfig_buffer_b.update(cx_b, |buffer, cx| {
2350        buffer.set_text("[*.rs]\ntab_width = 6\n", cx);
2351    });
2352    project_b
2353        .update(cx_b, |project, cx| {
2354            project.save_buffer(editorconfig_buffer_b.clone(), cx)
2355        })
2356        .await
2357        .unwrap();
2358    cx_a.run_until_parked();
2359    cx_b.run_until_parked();
2360
2361    tab_undo_assert(
2362        &mut main_editor_cx_a,
2363        &mut main_editor_cx_b,
2364        initial_main,
2365        second_tabbed_main,
2366        true,
2367    );
2368    tab_undo_assert(
2369        &mut main_editor_cx_a,
2370        &mut main_editor_cx_b,
2371        initial_main,
2372        second_tabbed_main,
2373        false,
2374    );
2375
2376    let third_tabbed_other = indoc! {"
2377      ˇpub fn foo() -> usize {
2378    4
2379}"};
2380    tab_undo_assert(
2381        &mut other_editor_cx_a,
2382        &mut other_editor_cx_b,
2383        initial_other,
2384        third_tabbed_other,
2385        true,
2386    );
2387
2388    tab_undo_assert(
2389        &mut other_editor_cx_a,
2390        &mut other_editor_cx_b,
2391        initial_other,
2392        third_tabbed_other,
2393        false,
2394    );
2395}
2396
2397#[track_caller]
2398fn tab_undo_assert(
2399    cx_a: &mut EditorTestContext,
2400    cx_b: &mut EditorTestContext,
2401    expected_initial: &str,
2402    expected_tabbed: &str,
2403    a_tabs: bool,
2404) {
2405    cx_a.assert_editor_state(expected_initial);
2406    cx_b.assert_editor_state(expected_initial);
2407
2408    if a_tabs {
2409        cx_a.update_editor(|editor, window, cx| {
2410            editor.tab(&editor::actions::Tab, window, cx);
2411        });
2412    } else {
2413        cx_b.update_editor(|editor, window, cx| {
2414            editor.tab(&editor::actions::Tab, window, cx);
2415        });
2416    }
2417
2418    cx_a.run_until_parked();
2419    cx_b.run_until_parked();
2420
2421    cx_a.assert_editor_state(expected_tabbed);
2422    cx_b.assert_editor_state(expected_tabbed);
2423
2424    if a_tabs {
2425        cx_a.update_editor(|editor, window, cx| {
2426            editor.undo(&editor::actions::Undo, window, cx);
2427        });
2428    } else {
2429        cx_b.update_editor(|editor, window, cx| {
2430            editor.undo(&editor::actions::Undo, window, cx);
2431        });
2432    }
2433    cx_a.run_until_parked();
2434    cx_b.run_until_parked();
2435    cx_a.assert_editor_state(expected_initial);
2436    cx_b.assert_editor_state(expected_initial);
2437}
2438
2439fn extract_hint_labels(editor: &Editor) -> Vec<String> {
2440    let mut labels = Vec::new();
2441    for hint in editor.inlay_hint_cache().hints() {
2442        match hint.label {
2443            project::InlayHintLabel::String(s) => labels.push(s),
2444            _ => unreachable!(),
2445        }
2446    }
2447    labels
2448}
2449
2450fn blame_entry(sha: &str, range: Range<u32>) -> git::blame::BlameEntry {
2451    git::blame::BlameEntry {
2452        sha: sha.parse().unwrap(),
2453        range,
2454        ..Default::default()
2455    }
2456}