use crate::{rpc::RECONNECT_TIMEOUT, tests::TestServer};use call::ActiveCall;use editor::{ DocumentColorsRenderMode, Editor, FETCH_COLORS_DEBOUNCE_TIMEOUT, MultiBufferOffset, RowInfo, SelectionEffects, actions::{ ConfirmCodeAction, ConfirmCompletion, ConfirmRename, ContextMenuFirst, CopyFileLocation, CopyFileName, CopyFileNameWithoutExtension, ExpandMacroRecursively, MoveToEnd, Redo, Rename, SelectAll, ToggleCodeActions, Undo, }, test::{ editor_test_context::{AssertionContextManager, EditorTestContext}, expand_macro_recursively, },};use fs::Fs;use futures::{SinkExt, StreamExt, channel::mpsc, lock::Mutex};use git::repository::repo_path;use gpui::{ App, Rgba, SharedString, TestAppContext, UpdateGlobal, VisualContext, VisualTestContext,};use indoc::indoc;use language::{FakeLspAdapter, rust_lang};use lsp::LSP_REQUEST_TIMEOUT;use pretty_assertions::assert_eq;use project::{ ProgressToken, ProjectPath, SERVER_PROGRESS_THROTTLE_TIMEOUT, lsp_store::lsp_ext_command::{ExpandedMacro, LspExtExpandMacro},};use recent_projects::disconnected_overlay::DisconnectedOverlay;use rpc::RECEIVE_TIMEOUT;use serde_json::json;use settings::{InlayHintSettingsContent, InlineBlameSettings, SettingsStore};use std::{ collections::BTreeSet, ops::{Deref as _, Range}, path::{Path, PathBuf}, sync::{ Arc, atomic::{self, AtomicBool, AtomicUsize}, }, time::Duration,};use text::Point;use util::{path, rel_path::rel_path, uri};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, rel_path("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(path!("/dir"), json!({ "a.txt": "Some text\n" })) .await; let (project_a, worktree_id) = client_a.build_local_project(path!("/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, rel_path("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, rel_path("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]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); let capabilities = lsp::ServerCapabilities { completion_provider: Some(lsp::CompletionOptions { trigger_characters: Some(vec![".".to_string()]), resolve_provider: Some(true), ..lsp::CompletionOptions::default() }), ..lsp::ServerCapabilities::default() }; client_a.language_registry().add(rust_lang()); let mut fake_language_servers = [ client_a.language_registry().register_fake_lsp( "Rust", FakeLspAdapter { capabilities: capabilities.clone(), initializer: Some(Box::new(|fake_server| { fake_server.set_request_handler::<lsp::request::Completion, _, _>( |params, _| async move { assert_eq!( params.text_document_position.text_document.uri, lsp::Uri::from_file_path(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() }, ]))) }, ); })), ..FakeLspAdapter::default() }, ), client_a.language_registry().register_fake_lsp( "Rust", FakeLspAdapter { name: "fake-analyzer", capabilities: capabilities.clone(), initializer: Some(Box::new(|fake_server| { fake_server.set_request_handler::<lsp::request::Completion, _, _>( |_, _| async move { Ok(None) }, ); })), ..FakeLspAdapter::default() }, ), ]; client_b.language_registry().add(rust_lang()); client_b.language_registry().register_fake_lsp_adapter( "Rust", FakeLspAdapter { capabilities: capabilities.clone(), ..FakeLspAdapter::default() }, ); client_b.language_registry().register_fake_lsp_adapter( "Rust", FakeLspAdapter { name: "fake-analyzer", capabilities, ..FakeLspAdapter::default() }, ); client_a .fs() .insert_tree( path!("/a"), json!({ "main.rs": "fn main() { a }", "other.rs": "", }), ) .await; let (project_a, worktree_id) = client_a.build_local_project(path!("/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, rel_path("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[0].next().await.unwrap(); let second_fake_language_server = fake_language_servers[1].next().await.unwrap(); cx_a.background_executor.run_until_parked(); cx_b.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(SelectionEffects::no_scroll(), window, cx, |s| { s.select_ranges([MultiBufferOffset(13)..MultiBufferOffset(13)]) }); editor.handle_input(".", window, cx); }); cx_b.focus(&editor_b); // Allow the completion request to propagate from guest to host to LSP. cx_b.background_executor.run_until_parked(); cx_a.background_executor.run_until_parked(); // Open the buffer on the host. let buffer_a = project_a .update(cx_a, |p, cx| { p.open_buffer((worktree_id, rel_path("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.set_request_handler::<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(); cx_b.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() }" );Lines 1–500 of 4643. Load more
use crate::{rpc::RECONNECT_TIMEOUT, tests::TestServer}; use call::ActiveCall; use editor::{ DocumentColorsRenderMode, Editor, FETCH_COLORS_DEBOUNCE_TIMEOUT, MultiBufferOffset, RowInfo, SelectionEffects, actions::{ ConfirmCodeAction, ConfirmCompletion, ConfirmRename, ContextMenuFirst, CopyFileLocation, CopyFileName, CopyFileNameWithoutExtension, ExpandMacroRecursively, MoveToEnd, Redo, Rename, SelectAll, ToggleCodeActions, Undo, }, test::{ editor_test_context::{AssertionContextManager, EditorTestContext}, expand_macro_recursively, }, }; use fs::Fs; use futures::{SinkExt, StreamExt, channel::mpsc, lock::Mutex}; use git::repository::repo_path; use gpui::{ App, Rgba, SharedString, TestAppContext, UpdateGlobal, VisualContext, VisualTestContext, }; use indoc::indoc; use language::{FakeLspAdapter, rust_lang}; use lsp::LSP_REQUEST_TIMEOUT; use pretty_assertions::assert_eq; use project::{ ProgressToken, ProjectPath, SERVER_PROGRESS_THROTTLE_TIMEOUT, lsp_store::lsp_ext_command::{ExpandedMacro, LspExtExpandMacro}, }; use recent_projects::disconnected_overlay::DisconnectedOverlay; use rpc::RECEIVE_TIMEOUT; use serde_json::json; use settings::{InlayHintSettingsContent, InlineBlameSettings, SettingsStore}; use std::{ collections::BTreeSet, ops::{Deref as _, Range}, path::{Path, PathBuf}, sync::{ Arc, atomic::{self, AtomicBool, AtomicUsize}, }, time::Duration, }; use text::Point; use util::{path, rel_path::rel_path, uri}; 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, rel_path("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(path!("/dir"), json!({ "a.txt": "Some text\n" })) .await; let (project_a, worktree_id) = client_a.build_local_project(path!("/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, rel_path("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, rel_path("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] 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); let capabilities = lsp::ServerCapabilities { completion_provider: Some(lsp::CompletionOptions { trigger_characters: Some(vec![".".to_string()]), resolve_provider: Some(true), ..lsp::CompletionOptions::default() }), ..lsp::ServerCapabilities::default() }; client_a.language_registry().add(rust_lang()); let mut fake_language_servers = [ client_a.language_registry().register_fake_lsp( "Rust", FakeLspAdapter { capabilities: capabilities.clone(), initializer: Some(Box::new(|fake_server| { fake_server.set_request_handler::<lsp::request::Completion, _, _>( |params, _| async move { assert_eq!( params.text_document_position.text_document.uri, lsp::Uri::from_file_path(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() }, ]))) }, ); })), ..FakeLspAdapter::default() }, ), client_a.language_registry().register_fake_lsp( "Rust", FakeLspAdapter { name: "fake-analyzer", capabilities: capabilities.clone(), initializer: Some(Box::new(|fake_server| { fake_server.set_request_handler::<lsp::request::Completion, _, _>( |_, _| async move { Ok(None) }, ); })), ..FakeLspAdapter::default() }, ), ]; client_b.language_registry().add(rust_lang()); client_b.language_registry().register_fake_lsp_adapter( "Rust", FakeLspAdapter { capabilities: capabilities.clone(), ..FakeLspAdapter::default() }, ); client_b.language_registry().register_fake_lsp_adapter( "Rust", FakeLspAdapter { name: "fake-analyzer", capabilities, ..FakeLspAdapter::default() }, ); client_a .fs() .insert_tree( path!("/a"), json!({ "main.rs": "fn main() { a }", "other.rs": "", }), ) .await; let (project_a, worktree_id) = client_a.build_local_project(path!("/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, rel_path("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[0].next().await.unwrap(); let second_fake_language_server = fake_language_servers[1].next().await.unwrap(); cx_a.background_executor.run_until_parked(); cx_b.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(SelectionEffects::no_scroll(), window, cx, |s| { s.select_ranges([MultiBufferOffset(13)..MultiBufferOffset(13)]) }); editor.handle_input(".", window, cx); }); cx_b.focus(&editor_b); // Allow the completion request to propagate from guest to host to LSP. cx_b.background_executor.run_until_parked(); cx_a.background_executor.run_until_parked(); // Open the buffer on the host. let buffer_a = project_a .update(cx_a, |p, cx| { p.open_buffer((worktree_id, rel_path("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.set_request_handler::<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(); cx_b.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() }" );