Do not resolve completions if extra edits are available

Kirill Bulatov created

Change summary

crates/editor/src/editor_tests.rs | 86 +++++++++++++++++++++++++++++++++
crates/project/src/lsp_command.rs | 12 ----
crates/project/src/project.rs     | 15 ++++-
3 files changed, 98 insertions(+), 15 deletions(-)

Detailed changes

crates/editor/src/editor_tests.rs 🔗

@@ -7294,6 +7294,92 @@ async fn test_completions_with_extra_edits(cx: &mut gpui::TestAppContext) {
 
     request.next().await;
 
+    cx.condition(|editor, _| editor.context_menu_visible())
+        .await;
+    let apply_additional_edits = cx.update_editor(|editor, cx| {
+        editor
+            .confirm_completion(&ConfirmCompletion::default(), cx)
+            .unwrap()
+    });
+    cx.assert_editor_state(indoc! {"fn main() { let a = 2.Some(2)ˇ; }"});
+    apply_additional_edits.await.unwrap();
+    cx.assert_editor_state(indoc! {"fn main() { let a = Some(2)ˇ; }"});
+}
+
+#[gpui::test]
+async fn test_completions_with_extra_resolved_edits(cx: &mut gpui::TestAppContext) {
+    init_test(cx, |_| {});
+
+    let mut cx = EditorLspTestContext::new_rust(
+        lsp::ServerCapabilities {
+            completion_provider: Some(lsp::CompletionOptions {
+                trigger_characters: Some(vec![".".to_string()]),
+                ..Default::default()
+            }),
+            ..Default::default()
+        },
+        cx,
+    )
+    .await;
+
+    cx.set_state(indoc! {"fn main() { let a = 2ˇ; }"});
+    cx.simulate_keystroke(".");
+    let completion_item = lsp::CompletionItem {
+        label: "some".into(),
+        kind: Some(lsp::CompletionItemKind::SNIPPET),
+        detail: Some("Wrap the expression in an `Option::Some`".to_string()),
+        documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
+            kind: lsp::MarkupKind::Markdown,
+            value: "```rust\nSome(2)\n```".to_string(),
+        })),
+        deprecated: Some(false),
+        sort_text: Some("fffffff2".to_string()),
+        filter_text: Some("some".to_string()),
+        insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
+        text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
+            range: lsp::Range {
+                start: lsp::Position {
+                    line: 0,
+                    character: 22,
+                },
+                end: lsp::Position {
+                    line: 0,
+                    character: 22,
+                },
+            },
+            new_text: "Some(2)".to_string(),
+        })),
+        additional_text_edits: Some(vec![lsp::TextEdit {
+            range: lsp::Range {
+                start: lsp::Position {
+                    line: 0,
+                    character: 20,
+                },
+                end: lsp::Position {
+                    line: 0,
+                    character: 22,
+                },
+            },
+            new_text: "".to_string(),
+        }]),
+        ..Default::default()
+    };
+
+    let closure_completion_item = completion_item.clone();
+    let mut request = cx.handle_request::<lsp::request::Completion, _, _>(move |_, _, _| {
+        let task_completion_item = closure_completion_item.clone();
+        async move {
+            Ok(Some(lsp::CompletionResponse::Array(vec![
+                lsp::CompletionItem {
+                    additional_text_edits: None,
+                    ..task_completion_item
+                },
+            ])))
+        }
+    });
+
+    request.next().await;
+
     cx.condition(|editor, _| editor.context_menu_visible())
         .await;
     let apply_additional_edits = cx.update_editor(|editor, cx| {

crates/project/src/lsp_command.rs 🔗

@@ -1349,6 +1349,7 @@ impl LspCommand for GetCompletions {
         } else {
             Default::default()
         };
+
         let completions = buffer.read_with(&cx, |buffer, _| {
             let language = buffer.language().cloned();
             let snapshot = buffer.snapshot();
@@ -1357,17 +1358,6 @@ impl LspCommand for GetCompletions {
             completions
                 .into_iter()
                 .filter_map(move |mut lsp_completion| {
-                    // TODO kb store these? at least, should only allow this when we have resolve
-                    // // For now, we can only handle additional edits if they are returned
-                    // // when resolving the completion, not if they are present initially.
-                    // if lsp_completion
-                    //     .additional_text_edits
-                    //     .as_ref()
-                    //     .map_or(false, |edits| !edits.is_empty())
-                    // {
-                    //     return None;
-                    // }
-
                     let (old_range, mut new_text) = match lsp_completion.text_edit.as_ref() {
                         // If the language server provides a range to overwrite, then
                         // check that the range is valid.

crates/project/src/project.rs 🔗

@@ -4446,11 +4446,18 @@ impl Project {
             };
 
             cx.spawn(|this, mut cx| async move {
-                let resolved_completion = lang_server
-                    .request::<lsp::request::ResolveCompletionItem>(completion.lsp_completion)
-                    .await?;
+                let additional_text_edits = if let Some(edits) =
+                    completion.lsp_completion.additional_text_edits.as_ref()
+                {
+                    Some(edits.clone())
+                } else {
+                    lang_server
+                        .request::<lsp::request::ResolveCompletionItem>(completion.lsp_completion)
+                        .await?
+                        .additional_text_edits
+                };
 
-                if let Some(edits) = resolved_completion.additional_text_edits {
+                if let Some(edits) = additional_text_edits {
                     let edits = this
                         .update(&mut cx, |this, cx| {
                             this.edits_from_lsp(