editor_tests.rs

   1use std::{
   2    path::Path,
   3    sync::{
   4        atomic::{self, AtomicBool, AtomicUsize},
   5        Arc,
   6    },
   7};
   8
   9use call::ActiveCall;
  10use editor::{
  11    actions::{
  12        ConfirmCodeAction, ConfirmCompletion, ConfirmRename, Redo, Rename, ToggleCodeActions, Undo,
  13    },
  14    test::editor_test_context::{AssertionContextManager, EditorTestContext},
  15    Editor,
  16};
  17use futures::StreamExt;
  18use gpui::{TestAppContext, VisualContext, VisualTestContext};
  19use indoc::indoc;
  20use language::{
  21    language_settings::{AllLanguageSettings, InlayHintSettings},
  22    tree_sitter_rust, FakeLspAdapter, Language, LanguageConfig,
  23};
  24use rpc::RECEIVE_TIMEOUT;
  25use serde_json::json;
  26use settings::SettingsStore;
  27use text::Point;
  28use workspace::Workspace;
  29
  30use crate::{rpc::RECONNECT_TIMEOUT, tests::TestServer};
  31
  32#[gpui::test(iterations = 10)]
  33async fn test_host_disconnect(
  34    cx_a: &mut TestAppContext,
  35    cx_b: &mut TestAppContext,
  36    cx_c: &mut TestAppContext,
  37) {
  38    let mut server = TestServer::start(cx_a.executor()).await;
  39    let client_a = server.create_client(cx_a, "user_a").await;
  40    let client_b = server.create_client(cx_b, "user_b").await;
  41    let client_c = server.create_client(cx_c, "user_c").await;
  42    server
  43        .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)])
  44        .await;
  45
  46    cx_b.update(editor::init);
  47
  48    client_a
  49        .fs()
  50        .insert_tree(
  51            "/a",
  52            serde_json::json!({
  53                "a.txt": "a-contents",
  54                "b.txt": "b-contents",
  55            }),
  56        )
  57        .await;
  58
  59    let active_call_a = cx_a.read(ActiveCall::global);
  60    let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
  61
  62    let worktree_a = project_a.read_with(cx_a, |project, _| project.worktrees().next().unwrap());
  63    let project_id = active_call_a
  64        .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
  65        .await
  66        .unwrap();
  67
  68    let project_b = client_b.build_remote_project(project_id, cx_b).await;
  69    cx_a.background_executor.run_until_parked();
  70
  71    assert!(worktree_a.read_with(cx_a, |tree, _| tree.as_local().unwrap().is_shared()));
  72
  73    let workspace_b =
  74        cx_b.add_window(|cx| Workspace::new(0, project_b.clone(), client_b.app_state.clone(), cx));
  75    let cx_b = &mut VisualTestContext::from_window(*workspace_b, cx_b);
  76    let workspace_b_view = workspace_b.root_view(cx_b).unwrap();
  77
  78    let editor_b = workspace_b
  79        .update(cx_b, |workspace, cx| {
  80            workspace.open_path((worktree_id, "b.txt"), None, true, cx)
  81        })
  82        .unwrap()
  83        .await
  84        .unwrap()
  85        .downcast::<Editor>()
  86        .unwrap();
  87
  88    //TODO: focus
  89    assert!(cx_b.update_view(&editor_b, |editor, cx| editor.is_focused(cx)));
  90    editor_b.update(cx_b, |editor, cx| editor.insert("X", cx));
  91
  92    cx_b.update(|cx| {
  93        assert!(workspace_b_view.read(cx).is_edited());
  94    });
  95
  96    // Drop client A's connection. Collaborators should disappear and the project should not be shown as shared.
  97    server.forbid_connections();
  98    server.disconnect_client(client_a.peer_id().unwrap());
  99    cx_a.background_executor
 100        .advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
 101
 102    project_a.read_with(cx_a, |project, _| project.collaborators().is_empty());
 103
 104    project_a.read_with(cx_a, |project, _| assert!(!project.is_shared()));
 105
 106    project_b.read_with(cx_b, |project, _| project.is_read_only());
 107
 108    assert!(worktree_a.read_with(cx_a, |tree, _| !tree.as_local().unwrap().is_shared()));
 109
 110    // Ensure client B's edited state is reset and that the whole window is blurred.
 111
 112    workspace_b
 113        .update(cx_b, |workspace, cx| {
 114            assert_eq!(cx.focused(), None);
 115            assert!(!workspace.is_edited())
 116        })
 117        .unwrap();
 118
 119    // Ensure client B is not prompted to save edits when closing window after disconnecting.
 120    let can_close = workspace_b
 121        .update(cx_b, |workspace, cx| workspace.prepare_to_close(true, cx))
 122        .unwrap()
 123        .await
 124        .unwrap();
 125    assert!(can_close);
 126
 127    // Allow client A to reconnect to the server.
 128    server.allow_connections();
 129    cx_a.background_executor.advance_clock(RECEIVE_TIMEOUT);
 130
 131    // Client B calls client A again after they reconnected.
 132    let active_call_b = cx_b.read(ActiveCall::global);
 133    active_call_b
 134        .update(cx_b, |call, cx| {
 135            call.invite(client_a.user_id().unwrap(), None, cx)
 136        })
 137        .await
 138        .unwrap();
 139    cx_a.background_executor.run_until_parked();
 140    active_call_a
 141        .update(cx_a, |call, cx| call.accept_incoming(cx))
 142        .await
 143        .unwrap();
 144
 145    active_call_a
 146        .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
 147        .await
 148        .unwrap();
 149
 150    // Drop client A's connection again. We should still unshare it successfully.
 151    server.forbid_connections();
 152    server.disconnect_client(client_a.peer_id().unwrap());
 153    cx_a.background_executor
 154        .advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
 155
 156    project_a.read_with(cx_a, |project, _| assert!(!project.is_shared()));
 157}
 158
 159#[gpui::test]
 160async fn test_newline_above_or_below_does_not_move_guest_cursor(
 161    cx_a: &mut TestAppContext,
 162    cx_b: &mut TestAppContext,
 163) {
 164    let mut server = TestServer::start(cx_a.executor()).await;
 165    let client_a = server.create_client(cx_a, "user_a").await;
 166    let client_b = server.create_client(cx_b, "user_b").await;
 167    let executor = cx_a.executor();
 168    server
 169        .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
 170        .await;
 171    let active_call_a = cx_a.read(ActiveCall::global);
 172
 173    client_a
 174        .fs()
 175        .insert_tree("/dir", json!({ "a.txt": "Some text\n" }))
 176        .await;
 177    let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
 178    let project_id = active_call_a
 179        .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
 180        .await
 181        .unwrap();
 182
 183    let project_b = client_b.build_remote_project(project_id, cx_b).await;
 184
 185    // Open a buffer as client A
 186    let buffer_a = project_a
 187        .update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
 188        .await
 189        .unwrap();
 190    let cx_a = cx_a.add_empty_window();
 191    let editor_a = cx_a.new_view(|cx| Editor::for_buffer(buffer_a, Some(project_a), cx));
 192
 193    let mut editor_cx_a = EditorTestContext {
 194        cx: cx_a.clone(),
 195        window: cx_a.handle(),
 196        editor: editor_a,
 197        assertion_cx: AssertionContextManager::new(),
 198    };
 199
 200    let cx_b = cx_b.add_empty_window();
 201    // Open a buffer as client B
 202    let buffer_b = project_b
 203        .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
 204        .await
 205        .unwrap();
 206    let editor_b = cx_b.new_view(|cx| Editor::for_buffer(buffer_b, Some(project_b), cx));
 207
 208    let mut editor_cx_b = EditorTestContext {
 209        cx: cx_b.clone(),
 210        window: cx_b.handle(),
 211        editor: editor_b,
 212        assertion_cx: AssertionContextManager::new(),
 213    };
 214
 215    // Test newline above
 216    editor_cx_a.set_selections_state(indoc! {"
 217        Some textˇ
 218    "});
 219    editor_cx_b.set_selections_state(indoc! {"
 220        Some textˇ
 221    "});
 222    editor_cx_a
 223        .update_editor(|editor, cx| editor.newline_above(&editor::actions::NewlineAbove, cx));
 224    executor.run_until_parked();
 225    editor_cx_a.assert_editor_state(indoc! {"
 226        ˇ
 227        Some text
 228    "});
 229    editor_cx_b.assert_editor_state(indoc! {"
 230
 231        Some textˇ
 232    "});
 233
 234    // Test newline below
 235    editor_cx_a.set_selections_state(indoc! {"
 236
 237        Some textˇ
 238    "});
 239    editor_cx_b.set_selections_state(indoc! {"
 240
 241        Some textˇ
 242    "});
 243    editor_cx_a
 244        .update_editor(|editor, cx| editor.newline_below(&editor::actions::NewlineBelow, cx));
 245    executor.run_until_parked();
 246    editor_cx_a.assert_editor_state(indoc! {"
 247
 248        Some text
 249        ˇ
 250    "});
 251    editor_cx_b.assert_editor_state(indoc! {"
 252
 253        Some textˇ
 254
 255    "});
 256}
 257
 258#[gpui::test(iterations = 10)]
 259async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
 260    let mut server = TestServer::start(cx_a.executor()).await;
 261    let client_a = server.create_client(cx_a, "user_a").await;
 262    let client_b = server.create_client(cx_b, "user_b").await;
 263    server
 264        .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
 265        .await;
 266    let active_call_a = cx_a.read(ActiveCall::global);
 267
 268    // Set up a fake language server.
 269    let mut language = Language::new(
 270        LanguageConfig {
 271            name: "Rust".into(),
 272            path_suffixes: vec!["rs".to_string()],
 273            ..Default::default()
 274        },
 275        Some(tree_sitter_rust::language()),
 276    );
 277    let mut fake_language_servers = language
 278        .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
 279            capabilities: lsp::ServerCapabilities {
 280                completion_provider: Some(lsp::CompletionOptions {
 281                    trigger_characters: Some(vec![".".to_string()]),
 282                    resolve_provider: Some(true),
 283                    ..Default::default()
 284                }),
 285                ..Default::default()
 286            },
 287            ..Default::default()
 288        }))
 289        .await;
 290    client_a.language_registry().add(Arc::new(language));
 291
 292    client_a
 293        .fs()
 294        .insert_tree(
 295            "/a",
 296            json!({
 297                "main.rs": "fn main() { a }",
 298                "other.rs": "",
 299            }),
 300        )
 301        .await;
 302    let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
 303    let project_id = active_call_a
 304        .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
 305        .await
 306        .unwrap();
 307    let project_b = client_b.build_remote_project(project_id, cx_b).await;
 308
 309    // Open a file in an editor as the guest.
 310    let buffer_b = project_b
 311        .update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
 312        .await
 313        .unwrap();
 314    let cx_b = cx_b.add_empty_window();
 315    let editor_b =
 316        cx_b.new_view(|cx| Editor::for_buffer(buffer_b.clone(), Some(project_b.clone()), cx));
 317
 318    let fake_language_server = fake_language_servers.next().await.unwrap();
 319    cx_a.background_executor.run_until_parked();
 320
 321    buffer_b.read_with(cx_b, |buffer, _| {
 322        assert!(!buffer.completion_triggers().is_empty())
 323    });
 324
 325    // Type a completion trigger character as the guest.
 326    editor_b.update(cx_b, |editor, cx| {
 327        editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
 328        editor.handle_input(".", cx);
 329    });
 330    cx_b.focus_view(&editor_b);
 331
 332    // Receive a completion request as the host's language server.
 333    // Return some completions from the host's language server.
 334    cx_a.executor().start_waiting();
 335    fake_language_server
 336        .handle_request::<lsp::request::Completion, _, _>(|params, _| async move {
 337            assert_eq!(
 338                params.text_document_position.text_document.uri,
 339                lsp::Url::from_file_path("/a/main.rs").unwrap(),
 340            );
 341            assert_eq!(
 342                params.text_document_position.position,
 343                lsp::Position::new(0, 14),
 344            );
 345
 346            Ok(Some(lsp::CompletionResponse::Array(vec![
 347                lsp::CompletionItem {
 348                    label: "first_method(…)".into(),
 349                    detail: Some("fn(&mut self, B) -> C".into()),
 350                    text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
 351                        new_text: "first_method($1)".to_string(),
 352                        range: lsp::Range::new(
 353                            lsp::Position::new(0, 14),
 354                            lsp::Position::new(0, 14),
 355                        ),
 356                    })),
 357                    insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
 358                    ..Default::default()
 359                },
 360                lsp::CompletionItem {
 361                    label: "second_method(…)".into(),
 362                    detail: Some("fn(&mut self, C) -> D<E>".into()),
 363                    text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
 364                        new_text: "second_method()".to_string(),
 365                        range: lsp::Range::new(
 366                            lsp::Position::new(0, 14),
 367                            lsp::Position::new(0, 14),
 368                        ),
 369                    })),
 370                    insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
 371                    ..Default::default()
 372                },
 373            ])))
 374        })
 375        .next()
 376        .await
 377        .unwrap();
 378    cx_a.executor().finish_waiting();
 379
 380    // Open the buffer on the host.
 381    let buffer_a = project_a
 382        .update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
 383        .await
 384        .unwrap();
 385    cx_a.executor().run_until_parked();
 386
 387    buffer_a.read_with(cx_a, |buffer, _| {
 388        assert_eq!(buffer.text(), "fn main() { a. }")
 389    });
 390
 391    // Confirm a completion on the guest.
 392    editor_b.update(cx_b, |editor, cx| {
 393        assert!(editor.context_menu_visible());
 394        editor.confirm_completion(&ConfirmCompletion { item_ix: Some(0) }, cx);
 395        assert_eq!(editor.text(cx), "fn main() { a.first_method() }");
 396    });
 397
 398    // Return a resolved completion from the host's language server.
 399    // The resolved completion has an additional text edit.
 400    fake_language_server.handle_request::<lsp::request::ResolveCompletionItem, _, _>(
 401        |params, _| async move {
 402            assert_eq!(params.label, "first_method(…)");
 403            Ok(lsp::CompletionItem {
 404                label: "first_method(…)".into(),
 405                detail: Some("fn(&mut self, B) -> C".into()),
 406                text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
 407                    new_text: "first_method($1)".to_string(),
 408                    range: lsp::Range::new(lsp::Position::new(0, 14), lsp::Position::new(0, 14)),
 409                })),
 410                additional_text_edits: Some(vec![lsp::TextEdit {
 411                    new_text: "use d::SomeTrait;\n".to_string(),
 412                    range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
 413                }]),
 414                insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
 415                ..Default::default()
 416            })
 417        },
 418    );
 419
 420    // The additional edit is applied.
 421    cx_a.executor().run_until_parked();
 422
 423    buffer_a.read_with(cx_a, |buffer, _| {
 424        assert_eq!(
 425            buffer.text(),
 426            "use d::SomeTrait;\nfn main() { a.first_method() }"
 427        );
 428    });
 429
 430    buffer_b.read_with(cx_b, |buffer, _| {
 431        assert_eq!(
 432            buffer.text(),
 433            "use d::SomeTrait;\nfn main() { a.first_method() }"
 434        );
 435    });
 436}
 437
 438#[gpui::test(iterations = 10)]
 439async fn test_collaborating_with_code_actions(
 440    cx_a: &mut TestAppContext,
 441    cx_b: &mut TestAppContext,
 442) {
 443    let mut server = TestServer::start(cx_a.executor()).await;
 444    let client_a = server.create_client(cx_a, "user_a").await;
 445    //
 446    let client_b = server.create_client(cx_b, "user_b").await;
 447    server
 448        .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
 449        .await;
 450    let active_call_a = cx_a.read(ActiveCall::global);
 451
 452    cx_b.update(editor::init);
 453
 454    // Set up a fake language server.
 455    let mut language = Language::new(
 456        LanguageConfig {
 457            name: "Rust".into(),
 458            path_suffixes: vec!["rs".to_string()],
 459            ..Default::default()
 460        },
 461        Some(tree_sitter_rust::language()),
 462    );
 463    let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
 464    client_a.language_registry().add(Arc::new(language));
 465
 466    client_a
 467        .fs()
 468        .insert_tree(
 469            "/a",
 470            json!({
 471                "main.rs": "mod other;\nfn main() { let foo = other::foo(); }",
 472                "other.rs": "pub fn foo() -> usize { 4 }",
 473            }),
 474        )
 475        .await;
 476    let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
 477    let project_id = active_call_a
 478        .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
 479        .await
 480        .unwrap();
 481
 482    // Join the project as client B.
 483    let project_b = client_b.build_remote_project(project_id, cx_b).await;
 484    let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
 485    let editor_b = workspace_b
 486        .update(cx_b, |workspace, cx| {
 487            workspace.open_path((worktree_id, "main.rs"), None, true, cx)
 488        })
 489        .await
 490        .unwrap()
 491        .downcast::<Editor>()
 492        .unwrap();
 493
 494    let mut fake_language_server = fake_language_servers.next().await.unwrap();
 495    let mut requests = fake_language_server
 496        .handle_request::<lsp::request::CodeActionRequest, _, _>(|params, _| async move {
 497            assert_eq!(
 498                params.text_document.uri,
 499                lsp::Url::from_file_path("/a/main.rs").unwrap(),
 500            );
 501            assert_eq!(params.range.start, lsp::Position::new(0, 0));
 502            assert_eq!(params.range.end, lsp::Position::new(0, 0));
 503            Ok(None)
 504        });
 505    cx_a.background_executor
 506        .advance_clock(editor::CODE_ACTIONS_DEBOUNCE_TIMEOUT * 2);
 507    requests.next().await;
 508
 509    // Move cursor to a location that contains code actions.
 510    editor_b.update(cx_b, |editor, cx| {
 511        editor.change_selections(None, cx, |s| {
 512            s.select_ranges([Point::new(1, 31)..Point::new(1, 31)])
 513        });
 514    });
 515    cx_b.focus_view(&editor_b);
 516
 517    let mut requests = fake_language_server
 518        .handle_request::<lsp::request::CodeActionRequest, _, _>(|params, _| async move {
 519            assert_eq!(
 520                params.text_document.uri,
 521                lsp::Url::from_file_path("/a/main.rs").unwrap(),
 522            );
 523            assert_eq!(params.range.start, lsp::Position::new(1, 31));
 524            assert_eq!(params.range.end, lsp::Position::new(1, 31));
 525
 526            Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
 527                lsp::CodeAction {
 528                    title: "Inline into all callers".to_string(),
 529                    edit: Some(lsp::WorkspaceEdit {
 530                        changes: Some(
 531                            [
 532                                (
 533                                    lsp::Url::from_file_path("/a/main.rs").unwrap(),
 534                                    vec![lsp::TextEdit::new(
 535                                        lsp::Range::new(
 536                                            lsp::Position::new(1, 22),
 537                                            lsp::Position::new(1, 34),
 538                                        ),
 539                                        "4".to_string(),
 540                                    )],
 541                                ),
 542                                (
 543                                    lsp::Url::from_file_path("/a/other.rs").unwrap(),
 544                                    vec![lsp::TextEdit::new(
 545                                        lsp::Range::new(
 546                                            lsp::Position::new(0, 0),
 547                                            lsp::Position::new(0, 27),
 548                                        ),
 549                                        "".to_string(),
 550                                    )],
 551                                ),
 552                            ]
 553                            .into_iter()
 554                            .collect(),
 555                        ),
 556                        ..Default::default()
 557                    }),
 558                    data: Some(json!({
 559                        "codeActionParams": {
 560                            "range": {
 561                                "start": {"line": 1, "column": 31},
 562                                "end": {"line": 1, "column": 31},
 563                            }
 564                        }
 565                    })),
 566                    ..Default::default()
 567                },
 568            )]))
 569        });
 570    cx_a.background_executor
 571        .advance_clock(editor::CODE_ACTIONS_DEBOUNCE_TIMEOUT * 2);
 572    requests.next().await;
 573
 574    // Toggle code actions and wait for them to display.
 575    editor_b.update(cx_b, |editor, cx| {
 576        editor.toggle_code_actions(
 577            &ToggleCodeActions {
 578                deployed_from_indicator: false,
 579            },
 580            cx,
 581        );
 582    });
 583    cx_a.background_executor.run_until_parked();
 584
 585    editor_b.update(cx_b, |editor, _| assert!(editor.context_menu_visible()));
 586
 587    fake_language_server.remove_request_handler::<lsp::request::CodeActionRequest>();
 588
 589    // Confirming the code action will trigger a resolve request.
 590    let confirm_action = editor_b
 591        .update(cx_b, |editor, cx| {
 592            Editor::confirm_code_action(editor, &ConfirmCodeAction { item_ix: Some(0) }, cx)
 593        })
 594        .unwrap();
 595    fake_language_server.handle_request::<lsp::request::CodeActionResolveRequest, _, _>(
 596        |_, _| async move {
 597            Ok(lsp::CodeAction {
 598                title: "Inline into all callers".to_string(),
 599                edit: Some(lsp::WorkspaceEdit {
 600                    changes: Some(
 601                        [
 602                            (
 603                                lsp::Url::from_file_path("/a/main.rs").unwrap(),
 604                                vec![lsp::TextEdit::new(
 605                                    lsp::Range::new(
 606                                        lsp::Position::new(1, 22),
 607                                        lsp::Position::new(1, 34),
 608                                    ),
 609                                    "4".to_string(),
 610                                )],
 611                            ),
 612                            (
 613                                lsp::Url::from_file_path("/a/other.rs").unwrap(),
 614                                vec![lsp::TextEdit::new(
 615                                    lsp::Range::new(
 616                                        lsp::Position::new(0, 0),
 617                                        lsp::Position::new(0, 27),
 618                                    ),
 619                                    "".to_string(),
 620                                )],
 621                            ),
 622                        ]
 623                        .into_iter()
 624                        .collect(),
 625                    ),
 626                    ..Default::default()
 627                }),
 628                ..Default::default()
 629            })
 630        },
 631    );
 632
 633    // After the action is confirmed, an editor containing both modified files is opened.
 634    confirm_action.await.unwrap();
 635
 636    let code_action_editor = workspace_b.update(cx_b, |workspace, cx| {
 637        workspace
 638            .active_item(cx)
 639            .unwrap()
 640            .downcast::<Editor>()
 641            .unwrap()
 642    });
 643    code_action_editor.update(cx_b, |editor, cx| {
 644        assert_eq!(editor.text(cx), "mod other;\nfn main() { let foo = 4; }\n");
 645        editor.undo(&Undo, cx);
 646        assert_eq!(
 647            editor.text(cx),
 648            "mod other;\nfn main() { let foo = other::foo(); }\npub fn foo() -> usize { 4 }"
 649        );
 650        editor.redo(&Redo, cx);
 651        assert_eq!(editor.text(cx), "mod other;\nfn main() { let foo = 4; }\n");
 652    });
 653}
 654
 655#[gpui::test(iterations = 10)]
 656async fn test_collaborating_with_renames(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
 657    let mut server = TestServer::start(cx_a.executor()).await;
 658    let client_a = server.create_client(cx_a, "user_a").await;
 659    let client_b = server.create_client(cx_b, "user_b").await;
 660    server
 661        .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
 662        .await;
 663    let active_call_a = cx_a.read(ActiveCall::global);
 664
 665    cx_b.update(editor::init);
 666
 667    // Set up a fake language server.
 668    let mut language = Language::new(
 669        LanguageConfig {
 670            name: "Rust".into(),
 671            path_suffixes: vec!["rs".to_string()],
 672            ..Default::default()
 673        },
 674        Some(tree_sitter_rust::language()),
 675    );
 676    let mut fake_language_servers = language
 677        .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
 678            capabilities: lsp::ServerCapabilities {
 679                rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
 680                    prepare_provider: Some(true),
 681                    work_done_progress_options: Default::default(),
 682                })),
 683                ..Default::default()
 684            },
 685            ..Default::default()
 686        }))
 687        .await;
 688    client_a.language_registry().add(Arc::new(language));
 689
 690    client_a
 691        .fs()
 692        .insert_tree(
 693            "/dir",
 694            json!({
 695                "one.rs": "const ONE: usize = 1;",
 696                "two.rs": "const TWO: usize = one::ONE + one::ONE;"
 697            }),
 698        )
 699        .await;
 700    let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
 701    let project_id = active_call_a
 702        .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
 703        .await
 704        .unwrap();
 705    let project_b = client_b.build_remote_project(project_id, cx_b).await;
 706
 707    let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
 708    let editor_b = workspace_b
 709        .update(cx_b, |workspace, cx| {
 710            workspace.open_path((worktree_id, "one.rs"), None, true, cx)
 711        })
 712        .await
 713        .unwrap()
 714        .downcast::<Editor>()
 715        .unwrap();
 716    let fake_language_server = fake_language_servers.next().await.unwrap();
 717
 718    // Move cursor to a location that can be renamed.
 719    let prepare_rename = editor_b.update(cx_b, |editor, cx| {
 720        editor.change_selections(None, cx, |s| s.select_ranges([7..7]));
 721        editor.rename(&Rename, cx).unwrap()
 722    });
 723
 724    fake_language_server
 725        .handle_request::<lsp::request::PrepareRenameRequest, _, _>(|params, _| async move {
 726            assert_eq!(params.text_document.uri.as_str(), "file:///dir/one.rs");
 727            assert_eq!(params.position, lsp::Position::new(0, 7));
 728            Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range::new(
 729                lsp::Position::new(0, 6),
 730                lsp::Position::new(0, 9),
 731            ))))
 732        })
 733        .next()
 734        .await
 735        .unwrap();
 736    prepare_rename.await.unwrap();
 737    editor_b.update(cx_b, |editor, cx| {
 738        use editor::ToOffset;
 739        let rename = editor.pending_rename().unwrap();
 740        let buffer = editor.buffer().read(cx).snapshot(cx);
 741        assert_eq!(
 742            rename.range.start.to_offset(&buffer)..rename.range.end.to_offset(&buffer),
 743            6..9
 744        );
 745        rename.editor.update(cx, |rename_editor, cx| {
 746            rename_editor.buffer().update(cx, |rename_buffer, cx| {
 747                rename_buffer.edit([(0..3, "THREE")], None, cx);
 748            });
 749        });
 750    });
 751
 752    let confirm_rename = editor_b.update(cx_b, |editor, cx| {
 753        Editor::confirm_rename(editor, &ConfirmRename, cx).unwrap()
 754    });
 755    fake_language_server
 756        .handle_request::<lsp::request::Rename, _, _>(|params, _| async move {
 757            assert_eq!(
 758                params.text_document_position.text_document.uri.as_str(),
 759                "file:///dir/one.rs"
 760            );
 761            assert_eq!(
 762                params.text_document_position.position,
 763                lsp::Position::new(0, 6)
 764            );
 765            assert_eq!(params.new_name, "THREE");
 766            Ok(Some(lsp::WorkspaceEdit {
 767                changes: Some(
 768                    [
 769                        (
 770                            lsp::Url::from_file_path("/dir/one.rs").unwrap(),
 771                            vec![lsp::TextEdit::new(
 772                                lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
 773                                "THREE".to_string(),
 774                            )],
 775                        ),
 776                        (
 777                            lsp::Url::from_file_path("/dir/two.rs").unwrap(),
 778                            vec![
 779                                lsp::TextEdit::new(
 780                                    lsp::Range::new(
 781                                        lsp::Position::new(0, 24),
 782                                        lsp::Position::new(0, 27),
 783                                    ),
 784                                    "THREE".to_string(),
 785                                ),
 786                                lsp::TextEdit::new(
 787                                    lsp::Range::new(
 788                                        lsp::Position::new(0, 35),
 789                                        lsp::Position::new(0, 38),
 790                                    ),
 791                                    "THREE".to_string(),
 792                                ),
 793                            ],
 794                        ),
 795                    ]
 796                    .into_iter()
 797                    .collect(),
 798                ),
 799                ..Default::default()
 800            }))
 801        })
 802        .next()
 803        .await
 804        .unwrap();
 805    confirm_rename.await.unwrap();
 806
 807    let rename_editor = workspace_b.update(cx_b, |workspace, cx| {
 808        workspace.active_item_as::<Editor>(cx).unwrap()
 809    });
 810
 811    rename_editor.update(cx_b, |editor, cx| {
 812        assert_eq!(
 813            editor.text(cx),
 814            "const THREE: usize = 1;\nconst TWO: usize = one::THREE + one::THREE;"
 815        );
 816        editor.undo(&Undo, cx);
 817        assert_eq!(
 818            editor.text(cx),
 819            "const ONE: usize = 1;\nconst TWO: usize = one::ONE + one::ONE;"
 820        );
 821        editor.redo(&Redo, cx);
 822        assert_eq!(
 823            editor.text(cx),
 824            "const THREE: usize = 1;\nconst TWO: usize = one::THREE + one::THREE;"
 825        );
 826    });
 827
 828    // Ensure temporary rename edits cannot be undone/redone.
 829    editor_b.update(cx_b, |editor, cx| {
 830        editor.undo(&Undo, cx);
 831        assert_eq!(editor.text(cx), "const ONE: usize = 1;");
 832        editor.undo(&Undo, cx);
 833        assert_eq!(editor.text(cx), "const ONE: usize = 1;");
 834        editor.redo(&Redo, cx);
 835        assert_eq!(editor.text(cx), "const THREE: usize = 1;");
 836    })
 837}
 838
 839#[gpui::test(iterations = 10)]
 840async fn test_language_server_statuses(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
 841    let mut server = TestServer::start(cx_a.executor()).await;
 842    let executor = cx_a.executor();
 843    let client_a = server.create_client(cx_a, "user_a").await;
 844    let client_b = server.create_client(cx_b, "user_b").await;
 845    server
 846        .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
 847        .await;
 848    let active_call_a = cx_a.read(ActiveCall::global);
 849
 850    cx_b.update(editor::init);
 851
 852    // Set up a fake language server.
 853    let mut language = Language::new(
 854        LanguageConfig {
 855            name: "Rust".into(),
 856            path_suffixes: vec!["rs".to_string()],
 857            ..Default::default()
 858        },
 859        Some(tree_sitter_rust::language()),
 860    );
 861    let mut fake_language_servers = language
 862        .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
 863            name: "the-language-server",
 864            ..Default::default()
 865        }))
 866        .await;
 867    client_a.language_registry().add(Arc::new(language));
 868
 869    client_a
 870        .fs()
 871        .insert_tree(
 872            "/dir",
 873            json!({
 874                "main.rs": "const ONE: usize = 1;",
 875            }),
 876        )
 877        .await;
 878    let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
 879
 880    let _buffer_a = project_a
 881        .update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
 882        .await
 883        .unwrap();
 884
 885    let fake_language_server = fake_language_servers.next().await.unwrap();
 886    fake_language_server.start_progress("the-token").await;
 887    fake_language_server.notify::<lsp::notification::Progress>(lsp::ProgressParams {
 888        token: lsp::NumberOrString::String("the-token".to_string()),
 889        value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::Report(
 890            lsp::WorkDoneProgressReport {
 891                message: Some("the-message".to_string()),
 892                ..Default::default()
 893            },
 894        )),
 895    });
 896    executor.run_until_parked();
 897
 898    project_a.read_with(cx_a, |project, _| {
 899        let status = project.language_server_statuses().next().unwrap();
 900        assert_eq!(status.name, "the-language-server");
 901        assert_eq!(status.pending_work.len(), 1);
 902        assert_eq!(
 903            status.pending_work["the-token"].message.as_ref().unwrap(),
 904            "the-message"
 905        );
 906    });
 907
 908    let project_id = active_call_a
 909        .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
 910        .await
 911        .unwrap();
 912    executor.run_until_parked();
 913    let project_b = client_b.build_remote_project(project_id, cx_b).await;
 914
 915    project_b.read_with(cx_b, |project, _| {
 916        let status = project.language_server_statuses().next().unwrap();
 917        assert_eq!(status.name, "the-language-server");
 918    });
 919
 920    fake_language_server.notify::<lsp::notification::Progress>(lsp::ProgressParams {
 921        token: lsp::NumberOrString::String("the-token".to_string()),
 922        value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::Report(
 923            lsp::WorkDoneProgressReport {
 924                message: Some("the-message-2".to_string()),
 925                ..Default::default()
 926            },
 927        )),
 928    });
 929    executor.run_until_parked();
 930
 931    project_a.read_with(cx_a, |project, _| {
 932        let status = project.language_server_statuses().next().unwrap();
 933        assert_eq!(status.name, "the-language-server");
 934        assert_eq!(status.pending_work.len(), 1);
 935        assert_eq!(
 936            status.pending_work["the-token"].message.as_ref().unwrap(),
 937            "the-message-2"
 938        );
 939    });
 940
 941    project_b.read_with(cx_b, |project, _| {
 942        let status = project.language_server_statuses().next().unwrap();
 943        assert_eq!(status.name, "the-language-server");
 944        assert_eq!(status.pending_work.len(), 1);
 945        assert_eq!(
 946            status.pending_work["the-token"].message.as_ref().unwrap(),
 947            "the-message-2"
 948        );
 949    });
 950}
 951
 952#[gpui::test(iterations = 10)]
 953async fn test_share_project(
 954    cx_a: &mut TestAppContext,
 955    cx_b: &mut TestAppContext,
 956    cx_c: &mut TestAppContext,
 957) {
 958    let executor = cx_a.executor();
 959    let cx_b = cx_b.add_empty_window();
 960    let mut server = TestServer::start(executor.clone()).await;
 961    let client_a = server.create_client(cx_a, "user_a").await;
 962    let client_b = server.create_client(cx_b, "user_b").await;
 963    let client_c = server.create_client(cx_c, "user_c").await;
 964    server
 965        .make_contacts(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)])
 966        .await;
 967    let active_call_a = cx_a.read(ActiveCall::global);
 968    let active_call_b = cx_b.read(ActiveCall::global);
 969    let active_call_c = cx_c.read(ActiveCall::global);
 970
 971    client_a
 972        .fs()
 973        .insert_tree(
 974            "/a",
 975            json!({
 976                ".gitignore": "ignored-dir",
 977                "a.txt": "a-contents",
 978                "b.txt": "b-contents",
 979                "ignored-dir": {
 980                    "c.txt": "",
 981                    "d.txt": "",
 982                }
 983            }),
 984        )
 985        .await;
 986
 987    // Invite client B to collaborate on a project
 988    let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
 989    active_call_a
 990        .update(cx_a, |call, cx| {
 991            call.invite(client_b.user_id().unwrap(), Some(project_a.clone()), cx)
 992        })
 993        .await
 994        .unwrap();
 995
 996    // Join that project as client B
 997
 998    let incoming_call_b = active_call_b.read_with(cx_b, |call, _| call.incoming());
 999    executor.run_until_parked();
1000    let call = incoming_call_b.borrow().clone().unwrap();
1001    assert_eq!(call.calling_user.github_login, "user_a");
1002    let initial_project = call.initial_project.unwrap();
1003    active_call_b
1004        .update(cx_b, |call, cx| call.accept_incoming(cx))
1005        .await
1006        .unwrap();
1007    let client_b_peer_id = client_b.peer_id().unwrap();
1008    let project_b = client_b
1009        .build_remote_project(initial_project.id, cx_b)
1010        .await;
1011
1012    let replica_id_b = project_b.read_with(cx_b, |project, _| project.replica_id());
1013
1014    executor.run_until_parked();
1015
1016    project_a.read_with(cx_a, |project, _| {
1017        let client_b_collaborator = project.collaborators().get(&client_b_peer_id).unwrap();
1018        assert_eq!(client_b_collaborator.replica_id, replica_id_b);
1019    });
1020
1021    project_b.read_with(cx_b, |project, cx| {
1022        let worktree = project.worktrees().next().unwrap().read(cx);
1023        assert_eq!(
1024            worktree.paths().map(AsRef::as_ref).collect::<Vec<_>>(),
1025            [
1026                Path::new(".gitignore"),
1027                Path::new("a.txt"),
1028                Path::new("b.txt"),
1029                Path::new("ignored-dir"),
1030            ]
1031        );
1032    });
1033
1034    project_b
1035        .update(cx_b, |project, cx| {
1036            let worktree = project.worktrees().next().unwrap();
1037            let entry = worktree.read(cx).entry_for_path("ignored-dir").unwrap();
1038            project.expand_entry(worktree_id, entry.id, cx).unwrap()
1039        })
1040        .await
1041        .unwrap();
1042
1043    project_b.read_with(cx_b, |project, cx| {
1044        let worktree = project.worktrees().next().unwrap().read(cx);
1045        assert_eq!(
1046            worktree.paths().map(AsRef::as_ref).collect::<Vec<_>>(),
1047            [
1048                Path::new(".gitignore"),
1049                Path::new("a.txt"),
1050                Path::new("b.txt"),
1051                Path::new("ignored-dir"),
1052                Path::new("ignored-dir/c.txt"),
1053                Path::new("ignored-dir/d.txt"),
1054            ]
1055        );
1056    });
1057
1058    // Open the same file as client B and client A.
1059    let buffer_b = project_b
1060        .update(cx_b, |p, cx| p.open_buffer((worktree_id, "b.txt"), cx))
1061        .await
1062        .unwrap();
1063
1064    buffer_b.read_with(cx_b, |buf, _| assert_eq!(buf.text(), "b-contents"));
1065
1066    project_a.read_with(cx_a, |project, cx| {
1067        assert!(project.has_open_buffer((worktree_id, "b.txt"), cx))
1068    });
1069    let buffer_a = project_a
1070        .update(cx_a, |p, cx| p.open_buffer((worktree_id, "b.txt"), cx))
1071        .await
1072        .unwrap();
1073
1074    let editor_b = cx_b.new_view(|cx| Editor::for_buffer(buffer_b, None, cx));
1075
1076    // Client A sees client B's selection
1077    executor.run_until_parked();
1078
1079    buffer_a.read_with(cx_a, |buffer, _| {
1080        buffer
1081            .snapshot()
1082            .remote_selections_in_range(text::Anchor::MIN..text::Anchor::MAX)
1083            .count()
1084            == 1
1085    });
1086
1087    // Edit the buffer as client B and see that edit as client A.
1088    editor_b.update(cx_b, |editor, cx| editor.handle_input("ok, ", cx));
1089    executor.run_until_parked();
1090
1091    buffer_a.read_with(cx_a, |buffer, _| {
1092        assert_eq!(buffer.text(), "ok, b-contents")
1093    });
1094
1095    // Client B can invite client C on a project shared by client A.
1096    active_call_b
1097        .update(cx_b, |call, cx| {
1098            call.invite(client_c.user_id().unwrap(), Some(project_b.clone()), cx)
1099        })
1100        .await
1101        .unwrap();
1102
1103    let incoming_call_c = active_call_c.read_with(cx_c, |call, _| call.incoming());
1104    executor.run_until_parked();
1105    let call = incoming_call_c.borrow().clone().unwrap();
1106    assert_eq!(call.calling_user.github_login, "user_b");
1107    let initial_project = call.initial_project.unwrap();
1108    active_call_c
1109        .update(cx_c, |call, cx| call.accept_incoming(cx))
1110        .await
1111        .unwrap();
1112    let _project_c = client_c
1113        .build_remote_project(initial_project.id, cx_c)
1114        .await;
1115
1116    // Client B closes the editor, and client A sees client B's selections removed.
1117    cx_b.update(move |_| drop(editor_b));
1118    executor.run_until_parked();
1119
1120    buffer_a.read_with(cx_a, |buffer, _| {
1121        buffer
1122            .snapshot()
1123            .remote_selections_in_range(text::Anchor::MIN..text::Anchor::MAX)
1124            .count()
1125            == 0
1126    });
1127}
1128
1129#[gpui::test(iterations = 10)]
1130async fn test_on_input_format_from_host_to_guest(
1131    cx_a: &mut TestAppContext,
1132    cx_b: &mut TestAppContext,
1133) {
1134    let mut server = TestServer::start(cx_a.executor()).await;
1135    let executor = cx_a.executor();
1136    let client_a = server.create_client(cx_a, "user_a").await;
1137    let client_b = server.create_client(cx_b, "user_b").await;
1138    server
1139        .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
1140        .await;
1141    let active_call_a = cx_a.read(ActiveCall::global);
1142
1143    // Set up a fake language server.
1144    let mut language = Language::new(
1145        LanguageConfig {
1146            name: "Rust".into(),
1147            path_suffixes: vec!["rs".to_string()],
1148            ..Default::default()
1149        },
1150        Some(tree_sitter_rust::language()),
1151    );
1152    let mut fake_language_servers = language
1153        .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
1154            capabilities: lsp::ServerCapabilities {
1155                document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
1156                    first_trigger_character: ":".to_string(),
1157                    more_trigger_character: Some(vec![">".to_string()]),
1158                }),
1159                ..Default::default()
1160            },
1161            ..Default::default()
1162        }))
1163        .await;
1164    client_a.language_registry().add(Arc::new(language));
1165
1166    client_a
1167        .fs()
1168        .insert_tree(
1169            "/a",
1170            json!({
1171                "main.rs": "fn main() { a }",
1172                "other.rs": "// Test file",
1173            }),
1174        )
1175        .await;
1176    let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
1177    let project_id = active_call_a
1178        .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
1179        .await
1180        .unwrap();
1181    let project_b = client_b.build_remote_project(project_id, cx_b).await;
1182
1183    // Open a file in an editor as the host.
1184    let buffer_a = project_a
1185        .update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
1186        .await
1187        .unwrap();
1188    let cx_a = cx_a.add_empty_window();
1189    let editor_a = cx_a.new_view(|cx| Editor::for_buffer(buffer_a, Some(project_a.clone()), cx));
1190
1191    let fake_language_server = fake_language_servers.next().await.unwrap();
1192    executor.run_until_parked();
1193
1194    // Receive an OnTypeFormatting request as the host's language server.
1195    // Return some formatting from the host's language server.
1196    fake_language_server.handle_request::<lsp::request::OnTypeFormatting, _, _>(
1197        |params, _| async move {
1198            assert_eq!(
1199                params.text_document_position.text_document.uri,
1200                lsp::Url::from_file_path("/a/main.rs").unwrap(),
1201            );
1202            assert_eq!(
1203                params.text_document_position.position,
1204                lsp::Position::new(0, 14),
1205            );
1206
1207            Ok(Some(vec![lsp::TextEdit {
1208                new_text: "~<".to_string(),
1209                range: lsp::Range::new(lsp::Position::new(0, 14), lsp::Position::new(0, 14)),
1210            }]))
1211        },
1212    );
1213
1214    // Open the buffer on the guest and see that the formatting worked
1215    let buffer_b = project_b
1216        .update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
1217        .await
1218        .unwrap();
1219
1220    // Type a on type formatting trigger character as the guest.
1221    cx_a.focus_view(&editor_a);
1222    editor_a.update(cx_a, |editor, cx| {
1223        editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
1224        editor.handle_input(">", cx);
1225    });
1226
1227    executor.run_until_parked();
1228
1229    buffer_b.read_with(cx_b, |buffer, _| {
1230        assert_eq!(buffer.text(), "fn main() { a>~< }")
1231    });
1232
1233    // Undo should remove LSP edits first
1234    editor_a.update(cx_a, |editor, cx| {
1235        assert_eq!(editor.text(cx), "fn main() { a>~< }");
1236        editor.undo(&Undo, cx);
1237        assert_eq!(editor.text(cx), "fn main() { a> }");
1238    });
1239    executor.run_until_parked();
1240
1241    buffer_b.read_with(cx_b, |buffer, _| {
1242        assert_eq!(buffer.text(), "fn main() { a> }")
1243    });
1244
1245    editor_a.update(cx_a, |editor, cx| {
1246        assert_eq!(editor.text(cx), "fn main() { a> }");
1247        editor.undo(&Undo, cx);
1248        assert_eq!(editor.text(cx), "fn main() { a }");
1249    });
1250    executor.run_until_parked();
1251
1252    buffer_b.read_with(cx_b, |buffer, _| {
1253        assert_eq!(buffer.text(), "fn main() { a }")
1254    });
1255}
1256
1257#[gpui::test(iterations = 10)]
1258async fn test_on_input_format_from_guest_to_host(
1259    cx_a: &mut TestAppContext,
1260    cx_b: &mut TestAppContext,
1261) {
1262    let mut server = TestServer::start(cx_a.executor()).await;
1263    let executor = cx_a.executor();
1264    let client_a = server.create_client(cx_a, "user_a").await;
1265    let client_b = server.create_client(cx_b, "user_b").await;
1266    server
1267        .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
1268        .await;
1269    let active_call_a = cx_a.read(ActiveCall::global);
1270
1271    // Set up a fake language server.
1272    let mut language = Language::new(
1273        LanguageConfig {
1274            name: "Rust".into(),
1275            path_suffixes: vec!["rs".to_string()],
1276            ..Default::default()
1277        },
1278        Some(tree_sitter_rust::language()),
1279    );
1280    let mut fake_language_servers = language
1281        .set_fake_lsp_adapter(Arc::new(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        .await;
1292    client_a.language_registry().add(Arc::new(language));
1293
1294    client_a
1295        .fs()
1296        .insert_tree(
1297            "/a",
1298            json!({
1299                "main.rs": "fn main() { a }",
1300                "other.rs": "// Test file",
1301            }),
1302        )
1303        .await;
1304    let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
1305    let project_id = active_call_a
1306        .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
1307        .await
1308        .unwrap();
1309    let project_b = client_b.build_remote_project(project_id, cx_b).await;
1310
1311    // Open a file in an editor as the guest.
1312    let buffer_b = project_b
1313        .update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
1314        .await
1315        .unwrap();
1316    let cx_b = cx_b.add_empty_window();
1317    let editor_b = cx_b.new_view(|cx| Editor::for_buffer(buffer_b, Some(project_b.clone()), cx));
1318
1319    let fake_language_server = fake_language_servers.next().await.unwrap();
1320    executor.run_until_parked();
1321
1322    // Type a on type formatting trigger character as the guest.
1323    cx_b.focus_view(&editor_b);
1324    editor_b.update(cx_b, |editor, cx| {
1325        editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
1326        editor.handle_input(":", cx);
1327    });
1328
1329    // Receive an OnTypeFormatting request as the host's language server.
1330    // Return some formatting from the host's language server.
1331    executor.start_waiting();
1332    fake_language_server
1333        .handle_request::<lsp::request::OnTypeFormatting, _, _>(|params, _| async move {
1334            assert_eq!(
1335                params.text_document_position.text_document.uri,
1336                lsp::Url::from_file_path("/a/main.rs").unwrap(),
1337            );
1338            assert_eq!(
1339                params.text_document_position.position,
1340                lsp::Position::new(0, 14),
1341            );
1342
1343            Ok(Some(vec![lsp::TextEdit {
1344                new_text: "~:".to_string(),
1345                range: lsp::Range::new(lsp::Position::new(0, 14), lsp::Position::new(0, 14)),
1346            }]))
1347        })
1348        .next()
1349        .await
1350        .unwrap();
1351    executor.finish_waiting();
1352
1353    // Open the buffer on the host and see that the formatting worked
1354    let buffer_a = project_a
1355        .update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
1356        .await
1357        .unwrap();
1358    executor.run_until_parked();
1359
1360    buffer_a.read_with(cx_a, |buffer, _| {
1361        assert_eq!(buffer.text(), "fn main() { a:~: }")
1362    });
1363
1364    // Undo should remove LSP edits first
1365    editor_b.update(cx_b, |editor, cx| {
1366        assert_eq!(editor.text(cx), "fn main() { a:~: }");
1367        editor.undo(&Undo, cx);
1368        assert_eq!(editor.text(cx), "fn main() { a: }");
1369    });
1370    executor.run_until_parked();
1371
1372    buffer_a.read_with(cx_a, |buffer, _| {
1373        assert_eq!(buffer.text(), "fn main() { a: }")
1374    });
1375
1376    editor_b.update(cx_b, |editor, cx| {
1377        assert_eq!(editor.text(cx), "fn main() { a: }");
1378        editor.undo(&Undo, cx);
1379        assert_eq!(editor.text(cx), "fn main() { a }");
1380    });
1381    executor.run_until_parked();
1382
1383    buffer_a.read_with(cx_a, |buffer, _| {
1384        assert_eq!(buffer.text(), "fn main() { a }")
1385    });
1386}
1387
1388#[gpui::test(iterations = 10)]
1389async fn test_mutual_editor_inlay_hint_cache_update(
1390    cx_a: &mut TestAppContext,
1391    cx_b: &mut TestAppContext,
1392) {
1393    let mut server = TestServer::start(cx_a.executor()).await;
1394    let executor = cx_a.executor();
1395    let client_a = server.create_client(cx_a, "user_a").await;
1396    let client_b = server.create_client(cx_b, "user_b").await;
1397    server
1398        .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
1399        .await;
1400    let active_call_a = cx_a.read(ActiveCall::global);
1401    let active_call_b = cx_b.read(ActiveCall::global);
1402
1403    cx_a.update(editor::init);
1404    cx_b.update(editor::init);
1405
1406    cx_a.update(|cx| {
1407        cx.update_global(|store: &mut SettingsStore, cx| {
1408            store.update_user_settings::<AllLanguageSettings>(cx, |settings| {
1409                settings.defaults.inlay_hints = Some(InlayHintSettings {
1410                    enabled: true,
1411                    show_type_hints: true,
1412                    show_parameter_hints: false,
1413                    show_other_hints: true,
1414                })
1415            });
1416        });
1417    });
1418    cx_b.update(|cx| {
1419        cx.update_global(|store: &mut SettingsStore, cx| {
1420            store.update_user_settings::<AllLanguageSettings>(cx, |settings| {
1421                settings.defaults.inlay_hints = Some(InlayHintSettings {
1422                    enabled: true,
1423                    show_type_hints: true,
1424                    show_parameter_hints: false,
1425                    show_other_hints: true,
1426                })
1427            });
1428        });
1429    });
1430
1431    let mut language = Language::new(
1432        LanguageConfig {
1433            name: "Rust".into(),
1434            path_suffixes: vec!["rs".to_string()],
1435            ..Default::default()
1436        },
1437        Some(tree_sitter_rust::language()),
1438    );
1439    let mut fake_language_servers = language
1440        .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
1441            capabilities: lsp::ServerCapabilities {
1442                inlay_hint_provider: Some(lsp::OneOf::Left(true)),
1443                ..Default::default()
1444            },
1445            ..Default::default()
1446        }))
1447        .await;
1448    let language = Arc::new(language);
1449    client_a.language_registry().add(Arc::clone(&language));
1450    client_b.language_registry().add(language);
1451
1452    // Client A opens a project.
1453    client_a
1454        .fs()
1455        .insert_tree(
1456            "/a",
1457            json!({
1458                "main.rs": "fn main() { a } // and some long comment to ensure inlay hints are not trimmed out",
1459                "other.rs": "// Test file",
1460            }),
1461        )
1462        .await;
1463    let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
1464    active_call_a
1465        .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
1466        .await
1467        .unwrap();
1468    let project_id = active_call_a
1469        .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
1470        .await
1471        .unwrap();
1472
1473    // Client B joins the project
1474    let project_b = client_b.build_remote_project(project_id, cx_b).await;
1475    active_call_b
1476        .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
1477        .await
1478        .unwrap();
1479
1480    let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
1481    executor.start_waiting();
1482
1483    // The host opens a rust file.
1484    let _buffer_a = project_a
1485        .update(cx_a, |project, cx| {
1486            project.open_local_buffer("/a/main.rs", cx)
1487        })
1488        .await
1489        .unwrap();
1490    let fake_language_server = fake_language_servers.next().await.unwrap();
1491    let editor_a = workspace_a
1492        .update(cx_a, |workspace, cx| {
1493            workspace.open_path((worktree_id, "main.rs"), None, true, cx)
1494        })
1495        .await
1496        .unwrap()
1497        .downcast::<Editor>()
1498        .unwrap();
1499
1500    // Set up the language server to return an additional inlay hint on each request.
1501    let edits_made = Arc::new(AtomicUsize::new(0));
1502    let closure_edits_made = Arc::clone(&edits_made);
1503    fake_language_server
1504        .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
1505            let task_edits_made = Arc::clone(&closure_edits_made);
1506            async move {
1507                assert_eq!(
1508                    params.text_document.uri,
1509                    lsp::Url::from_file_path("/a/main.rs").unwrap(),
1510                );
1511                let edits_made = task_edits_made.load(atomic::Ordering::Acquire);
1512                Ok(Some(vec![lsp::InlayHint {
1513                    position: lsp::Position::new(0, edits_made as u32),
1514                    label: lsp::InlayHintLabel::String(edits_made.to_string()),
1515                    kind: None,
1516                    text_edits: None,
1517                    tooltip: None,
1518                    padding_left: None,
1519                    padding_right: None,
1520                    data: None,
1521                }]))
1522            }
1523        })
1524        .next()
1525        .await
1526        .unwrap();
1527
1528    executor.run_until_parked();
1529
1530    let initial_edit = edits_made.load(atomic::Ordering::Acquire);
1531    editor_a.update(cx_a, |editor, _| {
1532        assert_eq!(
1533            vec![initial_edit.to_string()],
1534            extract_hint_labels(editor),
1535            "Host should get its first hints when opens an editor"
1536        );
1537        let inlay_cache = editor.inlay_hint_cache();
1538        assert_eq!(
1539            inlay_cache.version(),
1540            1,
1541            "Host editor update the cache version after every cache/view change",
1542        );
1543    });
1544    let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
1545    let editor_b = workspace_b
1546        .update(cx_b, |workspace, cx| {
1547            workspace.open_path((worktree_id, "main.rs"), None, true, cx)
1548        })
1549        .await
1550        .unwrap()
1551        .downcast::<Editor>()
1552        .unwrap();
1553
1554    executor.run_until_parked();
1555    editor_b.update(cx_b, |editor, _| {
1556        assert_eq!(
1557            vec![initial_edit.to_string()],
1558            extract_hint_labels(editor),
1559            "Client should get its first hints when opens an editor"
1560        );
1561        let inlay_cache = editor.inlay_hint_cache();
1562        assert_eq!(
1563            inlay_cache.version(),
1564            1,
1565            "Guest editor update the cache version after every cache/view change"
1566        );
1567    });
1568
1569    let after_client_edit = edits_made.fetch_add(1, atomic::Ordering::Release) + 1;
1570    editor_b.update(cx_b, |editor, cx| {
1571        editor.change_selections(None, cx, |s| s.select_ranges([13..13].clone()));
1572        editor.handle_input(":", cx);
1573    });
1574    cx_b.focus_view(&editor_b);
1575
1576    executor.run_until_parked();
1577    editor_a.update(cx_a, |editor, _| {
1578        assert_eq!(
1579            vec![after_client_edit.to_string()],
1580            extract_hint_labels(editor),
1581        );
1582        let inlay_cache = editor.inlay_hint_cache();
1583        assert_eq!(inlay_cache.version(), 2);
1584    });
1585    editor_b.update(cx_b, |editor, _| {
1586        assert_eq!(
1587            vec![after_client_edit.to_string()],
1588            extract_hint_labels(editor),
1589        );
1590        let inlay_cache = editor.inlay_hint_cache();
1591        assert_eq!(inlay_cache.version(), 2);
1592    });
1593
1594    let after_host_edit = edits_made.fetch_add(1, atomic::Ordering::Release) + 1;
1595    editor_a.update(cx_a, |editor, cx| {
1596        editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
1597        editor.handle_input("a change to increment both buffers' versions", cx);
1598    });
1599    cx_a.focus_view(&editor_a);
1600
1601    executor.run_until_parked();
1602    editor_a.update(cx_a, |editor, _| {
1603        assert_eq!(
1604            vec![after_host_edit.to_string()],
1605            extract_hint_labels(editor),
1606        );
1607        let inlay_cache = editor.inlay_hint_cache();
1608        assert_eq!(inlay_cache.version(), 3);
1609    });
1610    editor_b.update(cx_b, |editor, _| {
1611        assert_eq!(
1612            vec![after_host_edit.to_string()],
1613            extract_hint_labels(editor),
1614        );
1615        let inlay_cache = editor.inlay_hint_cache();
1616        assert_eq!(inlay_cache.version(), 3);
1617    });
1618
1619    let after_special_edit_for_refresh = edits_made.fetch_add(1, atomic::Ordering::Release) + 1;
1620    fake_language_server
1621        .request::<lsp::request::InlayHintRefreshRequest>(())
1622        .await
1623        .expect("inlay refresh request failed");
1624
1625    executor.run_until_parked();
1626    editor_a.update(cx_a, |editor, _| {
1627        assert_eq!(
1628            vec![after_special_edit_for_refresh.to_string()],
1629            extract_hint_labels(editor),
1630            "Host should react to /refresh LSP request"
1631        );
1632        let inlay_cache = editor.inlay_hint_cache();
1633        assert_eq!(
1634            inlay_cache.version(),
1635            4,
1636            "Host should accepted all edits and bump its cache version every time"
1637        );
1638    });
1639    editor_b.update(cx_b, |editor, _| {
1640        assert_eq!(
1641            vec![after_special_edit_for_refresh.to_string()],
1642            extract_hint_labels(editor),
1643            "Guest should get a /refresh LSP request propagated by host"
1644        );
1645        let inlay_cache = editor.inlay_hint_cache();
1646        assert_eq!(
1647            inlay_cache.version(),
1648            4,
1649            "Guest should accepted all edits and bump its cache version every time"
1650        );
1651    });
1652}
1653
1654#[gpui::test(iterations = 10)]
1655async fn test_inlay_hint_refresh_is_forwarded(
1656    cx_a: &mut TestAppContext,
1657    cx_b: &mut TestAppContext,
1658) {
1659    let mut server = TestServer::start(cx_a.executor()).await;
1660    let executor = cx_a.executor();
1661    let client_a = server.create_client(cx_a, "user_a").await;
1662    let client_b = server.create_client(cx_b, "user_b").await;
1663    server
1664        .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
1665        .await;
1666    let active_call_a = cx_a.read(ActiveCall::global);
1667    let active_call_b = cx_b.read(ActiveCall::global);
1668
1669    cx_a.update(editor::init);
1670    cx_b.update(editor::init);
1671
1672    cx_a.update(|cx| {
1673        cx.update_global(|store: &mut SettingsStore, cx| {
1674            store.update_user_settings::<AllLanguageSettings>(cx, |settings| {
1675                settings.defaults.inlay_hints = Some(InlayHintSettings {
1676                    enabled: false,
1677                    show_type_hints: false,
1678                    show_parameter_hints: false,
1679                    show_other_hints: false,
1680                })
1681            });
1682        });
1683    });
1684    cx_b.update(|cx| {
1685        cx.update_global(|store: &mut SettingsStore, cx| {
1686            store.update_user_settings::<AllLanguageSettings>(cx, |settings| {
1687                settings.defaults.inlay_hints = Some(InlayHintSettings {
1688                    enabled: true,
1689                    show_type_hints: true,
1690                    show_parameter_hints: true,
1691                    show_other_hints: true,
1692                })
1693            });
1694        });
1695    });
1696
1697    let mut language = Language::new(
1698        LanguageConfig {
1699            name: "Rust".into(),
1700            path_suffixes: vec!["rs".to_string()],
1701            ..Default::default()
1702        },
1703        Some(tree_sitter_rust::language()),
1704    );
1705    let mut fake_language_servers = language
1706        .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
1707            capabilities: lsp::ServerCapabilities {
1708                inlay_hint_provider: Some(lsp::OneOf::Left(true)),
1709                ..Default::default()
1710            },
1711            ..Default::default()
1712        }))
1713        .await;
1714    let language = Arc::new(language);
1715    client_a.language_registry().add(Arc::clone(&language));
1716    client_b.language_registry().add(language);
1717
1718    client_a
1719        .fs()
1720        .insert_tree(
1721            "/a",
1722            json!({
1723                "main.rs": "fn main() { a } // and some long comment to ensure inlay hints are not trimmed out",
1724                "other.rs": "// Test file",
1725            }),
1726        )
1727        .await;
1728    let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
1729    active_call_a
1730        .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
1731        .await
1732        .unwrap();
1733    let project_id = active_call_a
1734        .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
1735        .await
1736        .unwrap();
1737
1738    let project_b = client_b.build_remote_project(project_id, cx_b).await;
1739    active_call_b
1740        .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
1741        .await
1742        .unwrap();
1743
1744    let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
1745    let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
1746
1747    cx_a.background_executor.start_waiting();
1748
1749    let editor_a = workspace_a
1750        .update(cx_a, |workspace, cx| {
1751            workspace.open_path((worktree_id, "main.rs"), None, true, cx)
1752        })
1753        .await
1754        .unwrap()
1755        .downcast::<Editor>()
1756        .unwrap();
1757
1758    let editor_b = workspace_b
1759        .update(cx_b, |workspace, cx| {
1760            workspace.open_path((worktree_id, "main.rs"), None, true, cx)
1761        })
1762        .await
1763        .unwrap()
1764        .downcast::<Editor>()
1765        .unwrap();
1766
1767    let other_hints = Arc::new(AtomicBool::new(false));
1768    let fake_language_server = fake_language_servers.next().await.unwrap();
1769    let closure_other_hints = Arc::clone(&other_hints);
1770    fake_language_server
1771        .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
1772            let task_other_hints = Arc::clone(&closure_other_hints);
1773            async move {
1774                assert_eq!(
1775                    params.text_document.uri,
1776                    lsp::Url::from_file_path("/a/main.rs").unwrap(),
1777                );
1778                let other_hints = task_other_hints.load(atomic::Ordering::Acquire);
1779                let character = if other_hints { 0 } else { 2 };
1780                let label = if other_hints {
1781                    "other hint"
1782                } else {
1783                    "initial hint"
1784                };
1785                Ok(Some(vec![lsp::InlayHint {
1786                    position: lsp::Position::new(0, character),
1787                    label: lsp::InlayHintLabel::String(label.to_string()),
1788                    kind: None,
1789                    text_edits: None,
1790                    tooltip: None,
1791                    padding_left: None,
1792                    padding_right: None,
1793                    data: None,
1794                }]))
1795            }
1796        })
1797        .next()
1798        .await
1799        .unwrap();
1800    executor.finish_waiting();
1801
1802    executor.run_until_parked();
1803    editor_a.update(cx_a, |editor, _| {
1804        assert!(
1805            extract_hint_labels(editor).is_empty(),
1806            "Host should get no hints due to them turned off"
1807        );
1808        let inlay_cache = editor.inlay_hint_cache();
1809        assert_eq!(
1810            inlay_cache.version(),
1811            0,
1812            "Turned off hints should not generate version updates"
1813        );
1814    });
1815
1816    executor.run_until_parked();
1817    editor_b.update(cx_b, |editor, _| {
1818        assert_eq!(
1819            vec!["initial hint".to_string()],
1820            extract_hint_labels(editor),
1821            "Client should get its first hints when opens an editor"
1822        );
1823        let inlay_cache = editor.inlay_hint_cache();
1824        assert_eq!(
1825            inlay_cache.version(),
1826            1,
1827            "Should update cache version after first hints"
1828        );
1829    });
1830
1831    other_hints.fetch_or(true, atomic::Ordering::Release);
1832    fake_language_server
1833        .request::<lsp::request::InlayHintRefreshRequest>(())
1834        .await
1835        .expect("inlay refresh request failed");
1836    executor.run_until_parked();
1837    editor_a.update(cx_a, |editor, _| {
1838        assert!(
1839            extract_hint_labels(editor).is_empty(),
1840            "Host should get nop hints due to them turned off, even after the /refresh"
1841        );
1842        let inlay_cache = editor.inlay_hint_cache();
1843        assert_eq!(
1844            inlay_cache.version(),
1845            0,
1846            "Turned off hints should not generate version updates, again"
1847        );
1848    });
1849
1850    executor.run_until_parked();
1851    editor_b.update(cx_b, |editor, _| {
1852        assert_eq!(
1853            vec!["other hint".to_string()],
1854            extract_hint_labels(editor),
1855            "Guest should get a /refresh LSP request propagated by host despite host hints are off"
1856        );
1857        let inlay_cache = editor.inlay_hint_cache();
1858        assert_eq!(
1859            inlay_cache.version(),
1860            2,
1861            "Guest should accepted all edits and bump its cache version every time"
1862        );
1863    });
1864}
1865
1866fn extract_hint_labels(editor: &Editor) -> Vec<String> {
1867    let mut labels = Vec::new();
1868    for hint in editor.inlay_hint_cache().hints() {
1869        match hint.label {
1870            project::InlayHintLabel::String(s) => labels.push(s),
1871            _ => unreachable!(),
1872        }
1873    }
1874    labels
1875}