Add integration test for getting and resolving completions

Max Brunsfeld created

Change summary

crates/editor/src/editor.rs       |   2 
crates/editor/src/multi_buffer.rs |   2 
crates/language/src/language.rs   |  10 +
crates/server/src/rpc.rs          | 227 ++++++++++++++++++++++++++++++++
4 files changed, 237 insertions(+), 4 deletions(-)

Detailed changes

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<usize>,
         cx: &mut ViewContext<Self>,

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

crates/language/src/language.rs 🔗

@@ -363,7 +363,15 @@ impl LanguageServerConfig {
     pub async fn fake(
         executor: Arc<gpui::executor::Background>,
     ) -> (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<gpui::executor::Background>,
+    ) -> (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();

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::<lsp::request::Completion>()
+            .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<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()
+                    },
+                ])),
+            )
+            .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::<lsp::request::ResolveCompletionItem>()
+            .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();