diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index aae60d3c17..8c45d2e590 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -1677,7 +1677,7 @@ impl Editor { self.completion_state.take() } - fn confirm_completion( + pub fn confirm_completion( &mut self, completion_ix: Option, cx: &mut ViewContext, diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index 417e0bc69b..0554e1b8b5 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/crates/editor/src/multi_buffer.rs @@ -1339,7 +1339,7 @@ impl MultiBufferSnapshot { range: range.clone(), excerpts: self.excerpts.cursor(), excerpt_chunks: None, - language_aware: language_aware, + language_aware, }; chunks.seek(range.start); chunks diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index 4ebe6552cd..e4d22346bf 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -363,7 +363,15 @@ impl LanguageServerConfig { pub async fn fake( executor: Arc, ) -> (Self, lsp::FakeLanguageServer) { - let (server, fake) = lsp::LanguageServer::fake(executor).await; + Self::fake_with_capabilities(Default::default(), executor).await + } + + pub async fn fake_with_capabilities( + capabilites: lsp::ServerCapabilities, + executor: Arc, + ) -> (Self, lsp::FakeLanguageServer) { + let (server, fake) = + lsp::LanguageServer::fake_with_capabilities(capabilites, executor).await; fake.started .store(false, std::sync::atomic::Ordering::SeqCst); let started = fake.started.clone(); diff --git a/crates/server/src/rpc.rs b/crates/server/src/rpc.rs index 70e55c66f9..1d9f784f47 100644 --- a/crates/server/src/rpc.rs +++ b/crates/server/src/rpc.rs @@ -343,7 +343,7 @@ impl Server { self.peer.send( conn_id, proto::AddProjectCollaborator { - project_id: project_id, + project_id, collaborator: Some(proto::Collaborator { peer_id: request.sender_id.0, replica_id: response.replica_id, @@ -2297,6 +2297,231 @@ mod tests { }); } + #[gpui::test] + async fn test_collaborating_with_completion( + mut cx_a: TestAppContext, + mut cx_b: TestAppContext, + ) { + cx_a.foreground().forbid_parking(); + let mut lang_registry = Arc::new(LanguageRegistry::new()); + let fs = Arc::new(FakeFs::new(cx_a.background())); + + // Set up a fake language server. + let (language_server_config, mut fake_language_server) = + LanguageServerConfig::fake_with_capabilities( + lsp::ServerCapabilities { + completion_provider: Some(lsp::CompletionOptions { + trigger_characters: Some(vec![".".to_string()]), + ..Default::default() + }), + ..Default::default() + }, + cx_a.background(), + ) + .await; + Arc::get_mut(&mut lang_registry) + .unwrap() + .add(Arc::new(Language::new( + LanguageConfig { + name: "Rust".to_string(), + path_suffixes: vec!["rs".to_string()], + language_server: Some(language_server_config), + ..Default::default() + }, + Some(tree_sitter_rust::language()), + ))); + + // Connect to a server as 2 clients. + let mut server = TestServer::start(cx_a.foreground()).await; + let client_a = server.create_client(&mut cx_a, "user_a").await; + let client_b = server.create_client(&mut cx_b, "user_b").await; + + // Share a project as client A + fs.insert_tree( + "/a", + json!({ + ".zed.toml": r#"collaborators = ["user_b"]"#, + "main.rs": "fn main() { a }", + "other.rs": "", + }), + ) + .await; + let project_a = cx_a.update(|cx| { + Project::local( + client_a.clone(), + client_a.user_store.clone(), + lang_registry.clone(), + fs.clone(), + cx, + ) + }); + let (worktree_a, _) = project_a + .update(&mut cx_a, |p, cx| { + p.find_or_create_local_worktree("/a", false, cx) + }) + .await + .unwrap(); + worktree_a + .read_with(&cx_a, |tree, _| tree.as_local().unwrap().scan_complete()) + .await; + let project_id = project_a.update(&mut cx_a, |p, _| p.next_remote_id()).await; + let worktree_id = worktree_a.read_with(&cx_a, |tree, _| tree.id()); + project_a + .update(&mut cx_a, |p, cx| p.share(cx)) + .await + .unwrap(); + + // Join the worktree as client B. + let project_b = Project::remote( + project_id, + client_b.clone(), + client_b.user_store.clone(), + lang_registry.clone(), + fs.clone(), + &mut cx_b.to_async(), + ) + .await + .unwrap(); + + // Open a file in an editor as the guest. + let buffer_b = project_b + .update(&mut cx_b, |p, cx| { + p.open_buffer((worktree_id, "main.rs"), cx) + }) + .await + .unwrap(); + let (window_b, _) = cx_b.add_window(|_| EmptyView); + let editor_b = cx_b.add_view(window_b, |cx| { + Editor::for_buffer( + cx.add_model(|cx| MultiBuffer::singleton(buffer_b.clone(), cx)), + Arc::new(|cx| EditorSettings::test(cx)), + cx, + ) + }); + + // Type a completion trigger character as the guest. + editor_b.update(&mut cx_b, |editor, cx| { + editor.select_ranges([13..13], None, cx); + editor.handle_input(&Input(".".into()), cx); + cx.focus(&editor_b); + }); + + // Receive a completion request as the host's language server. + let (request_id, params) = fake_language_server + .receive_request::() + .await; + 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), + ); + + // Return some completions from the host's language server. + fake_language_server + .respond( + request_id, + 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".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() + }, + ])), + ) + .await; + + // Open the buffer on the host. + let buffer_a = project_a + .update(&mut cx_a, |p, cx| { + p.open_buffer((worktree_id, "main.rs"), cx) + }) + .await + .unwrap(); + buffer_a + .condition(&cx_a, |buffer, _| buffer.text() == "fn main() { a. }") + .await; + + // Confirm a completion on the guest. + editor_b.next_notification(&cx_b).await; + editor_b.update(&mut cx_b, |editor, cx| { + assert!(editor.has_completions()); + editor.confirm_completion(Some(0), cx); + assert_eq!(editor.text(cx), "fn main() { a.first_method() }"); + }); + + buffer_a + .condition(&cx_a, |buffer, _| { + buffer.text() == "fn main() { a.first_method() }" + }) + .await; + + // Receive a request resolve the selected completion on the host's language server. + let (request_id, params) = fake_language_server + .receive_request::() + .await; + assert_eq!(params.label, "first_method(…)"); + + // Return a resolved completion from the host's language server. + // The resolved completion has an additional text edit. + fake_language_server + .respond( + request_id, + 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() + }, + ) + .await; + + // The additional edit is applied. + buffer_b + .condition(&cx_b, |buffer, _| { + buffer.text() == "use d::SomeTrait;\nfn main() { a.first_method() }" + }) + .await; + assert_eq!( + buffer_a.read_with(&cx_a, |buffer, _| buffer.text()), + buffer_b.read_with(&cx_b, |buffer, _| buffer.text()), + ); + } + #[gpui::test] async fn test_formatting_buffer(mut cx_a: TestAppContext, mut cx_b: TestAppContext) { cx_a.foreground().forbid_parking();