2472 lines · 83.8 KB · Rust
RawDownload
use crate::{    rpc::RECONNECT_TIMEOUT,    tests::{rust_lang, TestServer},};use call::ActiveCall;use collections::HashMap;use editor::{    actions::{        ConfirmCodeAction, ConfirmCompletion, ConfirmRename, ContextMenuFirst, Redo, Rename,        ToggleCodeActions, Undo,    },    test::editor_test_context::{AssertionContextManager, EditorTestContext},    Editor, RowInfo,};use fs::Fs;use futures::StreamExt;use gpui::{TestAppContext, UpdateGlobal, VisualContext, VisualTestContext};use indoc::indoc;use language::{    language_settings::{AllLanguageSettings, InlayHintSettings},    FakeLspAdapter,};use project::{    project_settings::{InlineBlameSettings, ProjectSettings},    SERVER_PROGRESS_THROTTLE_TIMEOUT,};use recent_projects::disconnected_overlay::DisconnectedOverlay;use rpc::RECEIVE_TIMEOUT;use serde_json::json;use settings::SettingsStore;use std::{    ops::Range,    path::{Path, PathBuf},    sync::{        atomic::{self, AtomicBool, AtomicUsize},        Arc,    },};use text::Point;use workspace::{CloseIntent, Workspace}; #[gpui::test(iterations = 10)]async fn test_host_disconnect(    cx_a: &mut TestAppContext,    cx_b: &mut TestAppContext,    cx_c: &mut TestAppContext,) {    let mut server = TestServer::start(cx_a.executor()).await;    let client_a = server.create_client(cx_a, "user_a").await;    let client_b = server.create_client(cx_b, "user_b").await;    let client_c = server.create_client(cx_c, "user_c").await;    server        .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)])        .await;     cx_b.update(editor::init);    cx_b.update(recent_projects::init);     client_a        .fs()        .insert_tree(            "/a",            json!({                "a.txt": "a-contents",                "b.txt": "b-contents",            }),        )        .await;     let active_call_a = cx_a.read(ActiveCall::global);    let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;     let worktree_a = project_a.read_with(cx_a, |project, cx| project.worktrees(cx).next().unwrap());    let project_id = active_call_a        .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))        .await        .unwrap();     let project_b = client_b.join_remote_project(project_id, cx_b).await;    cx_a.background_executor.run_until_parked();     assert!(worktree_a.read_with(cx_a, |tree, _| tree.has_update_observer()));     let workspace_b = cx_b.add_window(|window, cx| {        Workspace::new(            None,            project_b.clone(),            client_b.app_state.clone(),            window,            cx,        )    });    let cx_b = &mut VisualTestContext::from_window(*workspace_b, cx_b);    let workspace_b_view = workspace_b.root(cx_b).unwrap();     let editor_b = workspace_b        .update(cx_b, |workspace, window, cx| {            workspace.open_path((worktree_id, "b.txt"), None, true, window, cx)        })        .unwrap()        .await        .unwrap()        .downcast::<Editor>()        .unwrap();     //TODO: focus    assert!(cx_b.update_window_entity(&editor_b, |editor, window, _| editor.is_focused(window)));    editor_b.update_in(cx_b, |editor, window, cx| editor.insert("X", window, cx));     cx_b.update(|_, cx| {        assert!(workspace_b_view.read(cx).is_edited());    });     // Drop client A's connection. Collaborators should disappear and the project should not be shown as shared.    server.forbid_connections();    server.disconnect_client(client_a.peer_id().unwrap());    cx_a.background_executor        .advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);     project_a.read_with(cx_a, |project, _| project.collaborators().is_empty());     project_a.read_with(cx_a, |project, _| assert!(!project.is_shared()));     project_b.read_with(cx_b, |project, cx| project.is_read_only(cx));     assert!(worktree_a.read_with(cx_a, |tree, _| !tree.has_update_observer()));     // Ensure client B's edited state is reset and that the whole window is blurred.    workspace_b        .update(cx_b, |workspace, _, cx| {            assert!(workspace.active_modal::<DisconnectedOverlay>(cx).is_some());            assert!(!workspace.is_edited());        })        .unwrap();     // Ensure client B is not prompted to save edits when closing window after disconnecting.    let can_close = workspace_b        .update(cx_b, |workspace, window, cx| {            workspace.prepare_to_close(CloseIntent::Quit, window, cx)        })        .unwrap()        .await        .unwrap();    assert!(can_close);     // Allow client A to reconnect to the server.    server.allow_connections();    cx_a.background_executor.advance_clock(RECEIVE_TIMEOUT);     // Client B calls client A again after they reconnected.    let active_call_b = cx_b.read(ActiveCall::global);    active_call_b        .update(cx_b, |call, cx| {            call.invite(client_a.user_id().unwrap(), None, cx)        })        .await        .unwrap();    cx_a.background_executor.run_until_parked();    active_call_a        .update(cx_a, |call, cx| call.accept_incoming(cx))        .await        .unwrap();     active_call_a        .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))        .await        .unwrap();     // Drop client A's connection again. We should still unshare it successfully.    server.forbid_connections();    server.disconnect_client(client_a.peer_id().unwrap());    cx_a.background_executor        .advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);     project_a.read_with(cx_a, |project, _| assert!(!project.is_shared()));} #[gpui::test]async fn test_newline_above_or_below_does_not_move_guest_cursor(    cx_a: &mut TestAppContext,    cx_b: &mut TestAppContext,) {    let mut server = TestServer::start(cx_a.executor()).await;    let client_a = server.create_client(cx_a, "user_a").await;    let client_b = server.create_client(cx_b, "user_b").await;    let executor = cx_a.executor();    server        .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])        .await;    let active_call_a = cx_a.read(ActiveCall::global);     client_a        .fs()        .insert_tree("/dir", json!({ "a.txt": "Some text\n" }))        .await;    let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;    let project_id = active_call_a        .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))        .await        .unwrap();     let project_b = client_b.join_remote_project(project_id, cx_b).await;     // Open a buffer as client A    let buffer_a = project_a        .update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))        .await        .unwrap();    let cx_a = cx_a.add_empty_window();    let editor_a = cx_a        .new_window_entity(|window, cx| Editor::for_buffer(buffer_a, Some(project_a), window, cx));     let mut editor_cx_a = EditorTestContext {        cx: cx_a.clone(),        window: cx_a.window_handle(),        editor: editor_a,        assertion_cx: AssertionContextManager::new(),    };     let cx_b = cx_b.add_empty_window();    // Open a buffer as client B    let buffer_b = project_b        .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))        .await        .unwrap();    let editor_b = cx_b        .new_window_entity(|window, cx| Editor::for_buffer(buffer_b, Some(project_b), window, cx));     let mut editor_cx_b = EditorTestContext {        cx: cx_b.clone(),        window: cx_b.window_handle(),        editor: editor_b,        assertion_cx: AssertionContextManager::new(),    };     // Test newline above    editor_cx_a.set_selections_state(indoc! {"        Some textˇ    "});    editor_cx_b.set_selections_state(indoc! {"        Some textˇ    "});    editor_cx_a.update_editor(|editor, window, cx| {        editor.newline_above(&editor::actions::NewlineAbove, window, cx)    });    executor.run_until_parked();    editor_cx_a.assert_editor_state(indoc! {"        ˇ        Some text    "});    editor_cx_b.assert_editor_state(indoc! {"         Some textˇ    "});     // Test newline below    editor_cx_a.set_selections_state(indoc! {"         Some textˇ    "});    editor_cx_b.set_selections_state(indoc! {"         Some textˇ    "});    editor_cx_a.update_editor(|editor, window, cx| {        editor.newline_below(&editor::actions::NewlineBelow, window, cx)    });    executor.run_until_parked();    editor_cx_a.assert_editor_state(indoc! {"         Some text        ˇ    "});    editor_cx_b.assert_editor_state(indoc! {"         Some textˇ     "});} #[gpui::test(iterations = 10)]async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {    let mut server = TestServer::start(cx_a.executor()).await;    let client_a = server.create_client(cx_a, "user_a").await;    let client_b = server.create_client(cx_b, "user_b").await;    server        .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])        .await;    let active_call_a = cx_a.read(ActiveCall::global);     client_a.language_registry().add(rust_lang());    let mut fake_language_servers = client_a.language_registry().register_fake_lsp(        "Rust",        FakeLspAdapter {            capabilities: lsp::ServerCapabilities {                completion_provider: Some(lsp::CompletionOptions {                    trigger_characters: Some(vec![".".to_string()]),                    resolve_provider: Some(true),                    ..Default::default()                }),                ..Default::default()            },            ..Default::default()        },    );     client_a        .fs()        .insert_tree(            "/a",            json!({                "main.rs": "fn main() { a }",                "other.rs": "",            }),        )        .await;    let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;    let project_id = active_call_a        .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))        .await        .unwrap();    let project_b = client_b.join_remote_project(project_id, cx_b).await;     // Open a file in an editor as the guest.    let buffer_b = project_b        .update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))        .await        .unwrap();    let cx_b = cx_b.add_empty_window();    let editor_b = cx_b.new_window_entity(|window, cx| {        Editor::for_buffer(buffer_b.clone(), Some(project_b.clone()), window, cx)    });     let fake_language_server = fake_language_servers.next().await.unwrap();    cx_a.background_executor.run_until_parked();     buffer_b.read_with(cx_b, |buffer, _| {        assert!(!buffer.completion_triggers().is_empty())    });     // Type a completion trigger character as the guest.    editor_b.update_in(cx_b, |editor, window, cx| {        editor.change_selections(None, window, cx, |s| s.select_ranges([13..13]));        editor.handle_input(".", window, cx);    });    cx_b.focus(&editor_b);     // Receive a completion request as the host's language server.    // Return some completions from the host's language server.    cx_a.executor().start_waiting();    fake_language_server        .handle_request::<lsp::request::Completion, _, _>(|params, _| async move {            assert_eq!(                params.text_document_position.text_document.uri,                lsp::Url::from_file_path("/a/main.rs").unwrap(),            );            assert_eq!(                params.text_document_position.position,                lsp::Position::new(0, 14),            );             Ok(Some(lsp::CompletionResponse::Array(vec![                lsp::CompletionItem {                    label: "first_method(…)".into(),                    detail: Some("fn(&mut self, B) -> C".into()),                    text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {                        new_text: "first_method($1)".to_string(),                        range: lsp::Range::new(                            lsp::Position::new(0, 14),                            lsp::Position::new(0, 14),                        ),                    })),                    insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),                    ..Default::default()                },                lsp::CompletionItem {                    label: "second_method(…)".into(),                    detail: Some("fn(&mut self, C) -> D<E>".into()),                    text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {                        new_text: "second_method()".to_string(),                        range: lsp::Range::new(                            lsp::Position::new(0, 14),                            lsp::Position::new(0, 14),                        ),                    })),                    insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),                    ..Default::default()                },            ])))        })        .next()        .await        .unwrap();    cx_a.executor().finish_waiting();     // Open the buffer on the host.    let buffer_a = project_a        .update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))        .await        .unwrap();    cx_a.executor().run_until_parked();     buffer_a.read_with(cx_a, |buffer, _| {        assert_eq!(buffer.text(), "fn main() { a. }")    });     // Confirm a completion on the guest.    editor_b.update_in(cx_b, |editor, window, cx| {        assert!(editor.context_menu_visible());        editor.confirm_completion(&ConfirmCompletion { item_ix: Some(0) }, window, cx);        assert_eq!(editor.text(cx), "fn main() { a.first_method() }");    });     // Return a resolved completion from the host's language server.    // The resolved completion has an additional text edit.    fake_language_server.handle_request::<lsp::request::ResolveCompletionItem, _, _>(        |params, _| async move {            assert_eq!(params.label, "first_method(…)");            Ok(lsp::CompletionItem {                label: "first_method(…)".into(),                detail: Some("fn(&mut self, B) -> C".into()),                text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {                    new_text: "first_method($1)".to_string(),                    range: lsp::Range::new(lsp::Position::new(0, 14), lsp::Position::new(0, 14)),                })),                additional_text_edits: Some(vec![lsp::TextEdit {                    new_text: "use d::SomeTrait;\n".to_string(),                    range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),                }]),                insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),                ..Default::default()            })        },    );     // The additional edit is applied.    cx_a.executor().run_until_parked();     buffer_a.read_with(cx_a, |buffer, _| {        assert_eq!(            buffer.text(),            "use d::SomeTrait;\nfn main() { a.first_method() }"        );    });     buffer_b.read_with(cx_b, |buffer, _| {        assert_eq!(            buffer.text(),            "use d::SomeTrait;\nfn main() { a.first_method() }"        );    });     // Now we do a second completion, this time to ensure that documentation/snippets are    // resolved    editor_b.update_in(cx_b, |editor, window, cx| {        editor.change_selections(None, window, cx, |s| s.select_ranges([46..46]));        editor.handle_input("; a", window, cx);        editor.handle_input(".", window, cx);    });     buffer_b.read_with(cx_b, |buffer, _| {        assert_eq!(            buffer.text(),            "use d::SomeTrait;\nfn main() { a.first_method(); a. }"        );    });     let mut completion_response = fake_language_server        .handle_request::<lsp::request::Completion, _, _>(|params, _| async move {            assert_eq!(                params.text_document_position.text_document.uri,                lsp::Url::from_file_path("/a/main.rs").unwrap(),            );            assert_eq!(                params.text_document_position.position,                lsp::Position::new(1, 32),            );             Ok(Some(lsp::CompletionResponse::Array(vec![                lsp::CompletionItem {                    label: "third_method(…)".into(),                    detail: Some("fn(&mut self, B, C, D) -> E".into()),                    text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {                        // no snippet placehodlers                        new_text: "third_method".to_string(),                        range: lsp::Range::new(                            lsp::Position::new(1, 32),                            lsp::Position::new(1, 32),                        ),                    })),                    insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),                    documentation: None,                    ..Default::default()                },            ])))        });     // The completion now gets a new `text_edit.new_text` when resolving the completion item    let mut resolve_completion_response = fake_language_server        .handle_request::<lsp::request::ResolveCompletionItem, _, _>(|params, _| async move {

Lines 1–500 of 2472.