Properly use lsp::CompletionList defaults (#21202)

Kirill Bulatov created

- Closes https://github.com/zed-industries/zed/issues/21185

Release Notes:

- Fixed incorrect handling of the completion list defaults

Change summary

crates/editor/src/editor_tests.rs | 194 +++++++++++++++++++++++++++++++++
crates/lsp/src/lsp.rs             |   1 
crates/project/src/lsp_command.rs |  45 ++++++-
3 files changed, 234 insertions(+), 6 deletions(-)

Detailed changes

crates/editor/src/editor_tests.rs 🔗

@@ -10541,6 +10541,200 @@ async fn test_completions_with_additional_edits(cx: &mut gpui::TestAppContext) {
     cx.assert_editor_state(indoc! {"fn main() { let a = Some(2)ˇ; }"});
 }
 
+#[gpui::test]
+async fn test_completions_default_resolve_data_handling(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()]),
+                resolve_provider: Some(true),
+                ..Default::default()
+            }),
+            ..Default::default()
+        },
+        cx,
+    )
+    .await;
+
+    cx.set_state(indoc! {"fn main() { let a = 2ˇ; }"});
+    cx.simulate_keystroke(".");
+
+    let default_commit_characters = vec!["?".to_string()];
+    let default_data = json!({ "very": "special"});
+    let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
+    let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
+    let default_edit_range = lsp::Range {
+        start: lsp::Position {
+            line: 0,
+            character: 5,
+        },
+        end: lsp::Position {
+            line: 0,
+            character: 5,
+        },
+    };
+
+    let completion_data = default_data.clone();
+    let completion_characters = default_commit_characters.clone();
+    cx.handle_request::<lsp::request::Completion, _, _>(move |_, _, _| {
+        let default_data = completion_data.clone();
+        let default_commit_characters = completion_characters.clone();
+        async move {
+            Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
+                items: vec![
+                    lsp::CompletionItem {
+                        label: "Some(2)".into(),
+                        insert_text: Some("Some(2)".into()),
+                        data: Some(json!({ "very": "special"})),
+                        insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
+                        text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
+                            lsp::InsertReplaceEdit {
+                                new_text: "Some(2)".to_string(),
+                                insert: lsp::Range::default(),
+                                replace: lsp::Range::default(),
+                            },
+                        )),
+                        ..lsp::CompletionItem::default()
+                    },
+                    lsp::CompletionItem {
+                        label: "vec![2]".into(),
+                        insert_text: Some("vec![2]".into()),
+                        insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
+                        ..lsp::CompletionItem::default()
+                    },
+                ],
+                item_defaults: Some(lsp::CompletionListItemDefaults {
+                    data: Some(default_data.clone()),
+                    commit_characters: Some(default_commit_characters.clone()),
+                    edit_range: Some(lsp::CompletionListItemDefaultsEditRange::Range(
+                        default_edit_range,
+                    )),
+                    insert_text_format: Some(default_insert_text_format),
+                    insert_text_mode: Some(default_insert_text_mode),
+                }),
+                ..lsp::CompletionList::default()
+            })))
+        }
+    })
+    .next()
+    .await;
+
+    cx.condition(|editor, _| editor.context_menu_visible())
+        .await;
+
+    cx.update_editor(|editor, _| {
+        let menu = editor.context_menu.read();
+        match menu.as_ref().expect("should have the completions menu") {
+            ContextMenu::Completions(completions_menu) => {
+                assert_eq!(
+                    completions_menu
+                        .matches
+                        .iter()
+                        .map(|c| c.string.as_str())
+                        .collect::<Vec<_>>(),
+                    vec!["Some(2)", "vec![2]"]
+                );
+            }
+            ContextMenu::CodeActions(_) => panic!("Expected to have the completions menu"),
+        }
+    });
+
+    cx.update_editor(|editor, cx| {
+        editor.context_menu_first(&ContextMenuFirst, cx);
+    });
+    let first_item_resolve_characters = default_commit_characters.clone();
+    cx.handle_request::<lsp::request::ResolveCompletionItem, _, _>(move |_, item_to_resolve, _| {
+        let default_commit_characters = first_item_resolve_characters.clone();
+
+        async move {
+            assert_eq!(
+                item_to_resolve.label, "Some(2)",
+                "Should have selected the first item"
+            );
+            assert_eq!(
+                item_to_resolve.data,
+                Some(json!({ "very": "special"})),
+                "First item should bring its own data for resolving"
+            );
+            assert_eq!(
+                item_to_resolve.commit_characters,
+                Some(default_commit_characters),
+                "First item had no own commit characters and should inherit the default ones"
+            );
+            assert!(
+                matches!(
+                    item_to_resolve.text_edit,
+                    Some(lsp::CompletionTextEdit::InsertAndReplace { .. })
+                ),
+                "First item should bring its own edit range for resolving"
+            );
+            assert_eq!(
+                item_to_resolve.insert_text_format,
+                Some(default_insert_text_format),
+                "First item had no own insert text format and should inherit the default one"
+            );
+            assert_eq!(
+                item_to_resolve.insert_text_mode,
+                Some(lsp::InsertTextMode::ADJUST_INDENTATION),
+                "First item should bring its own insert text mode for resolving"
+            );
+            Ok(item_to_resolve)
+        }
+    })
+    .next()
+    .await
+    .unwrap();
+
+    cx.update_editor(|editor, cx| {
+        editor.context_menu_last(&ContextMenuLast, cx);
+    });
+    cx.handle_request::<lsp::request::ResolveCompletionItem, _, _>(move |_, item_to_resolve, _| {
+        let default_data = default_data.clone();
+        let default_commit_characters = default_commit_characters.clone();
+        async move {
+            assert_eq!(
+                item_to_resolve.label, "vec![2]",
+                "Should have selected the last item"
+            );
+            assert_eq!(
+                item_to_resolve.data,
+                Some(default_data),
+                "Last item has no own resolve data and should inherit the default one"
+            );
+            assert_eq!(
+                item_to_resolve.commit_characters,
+                Some(default_commit_characters),
+                "Last item had no own commit characters and should inherit the default ones"
+            );
+            assert_eq!(
+                item_to_resolve.text_edit,
+                Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
+                    range: default_edit_range,
+                    new_text: "vec![2]".to_string()
+                })),
+                "Last item had no own edit range and should inherit the default one"
+            );
+            assert_eq!(
+                item_to_resolve.insert_text_format,
+                Some(lsp::InsertTextFormat::PLAIN_TEXT),
+                "Last item should bring its own insert text format for resolving"
+            );
+            assert_eq!(
+                item_to_resolve.insert_text_mode,
+                Some(default_insert_text_mode),
+                "Last item had no own insert text mode and should inherit the default one"
+            );
+
+            Ok(item_to_resolve)
+        }
+    })
+    .next()
+    .await
+    .unwrap();
+}
+
 #[gpui::test]
 async fn test_completions_in_languages_with_extra_word_characters(cx: &mut gpui::TestAppContext) {
     init_test(cx, |_| {});

crates/lsp/src/lsp.rs 🔗

@@ -697,6 +697,7 @@ impl LanguageServer {
                                 "commitCharacters".to_owned(),
                                 "editRange".to_owned(),
                                 "insertTextMode".to_owned(),
+                                "insertTextFormat".to_owned(),
                                 "data".to_owned(),
                             ]),
                         }),

crates/project/src/lsp_command.rs 🔗

@@ -1775,21 +1775,54 @@ impl LspCommand for GetCompletions {
         if let Some(item_defaults) = item_defaults {
             let default_data = item_defaults.data.as_ref();
             let default_commit_characters = item_defaults.commit_characters.as_ref();
+            let default_edit_range = item_defaults.edit_range.as_ref();
+            let default_insert_text_format = item_defaults.insert_text_format.as_ref();
             let default_insert_text_mode = item_defaults.insert_text_mode.as_ref();
 
             if default_data.is_some()
                 || default_commit_characters.is_some()
+                || default_edit_range.is_some()
+                || default_insert_text_format.is_some()
                 || default_insert_text_mode.is_some()
             {
                 for item in completions.iter_mut() {
-                    if let Some(data) = default_data {
-                        item.data = Some(data.clone())
+                    if item.data.is_none() && default_data.is_some() {
+                        item.data = default_data.cloned()
                     }
-                    if let Some(characters) = default_commit_characters {
-                        item.commit_characters = Some(characters.clone())
+                    if item.commit_characters.is_none() && default_commit_characters.is_some() {
+                        item.commit_characters = default_commit_characters.cloned()
                     }
-                    if let Some(text_mode) = default_insert_text_mode {
-                        item.insert_text_mode = Some(*text_mode)
+                    if item.text_edit.is_none() {
+                        if let Some(default_edit_range) = default_edit_range {
+                            match default_edit_range {
+                                CompletionListItemDefaultsEditRange::Range(range) => {
+                                    item.text_edit =
+                                        Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
+                                            range: *range,
+                                            new_text: item.label.clone(),
+                                        }))
+                                }
+                                CompletionListItemDefaultsEditRange::InsertAndReplace {
+                                    insert,
+                                    replace,
+                                } => {
+                                    item.text_edit =
+                                        Some(lsp::CompletionTextEdit::InsertAndReplace(
+                                            lsp::InsertReplaceEdit {
+                                                new_text: item.label.clone(),
+                                                insert: *insert,
+                                                replace: *replace,
+                                            },
+                                        ))
+                                }
+                            }
+                        }
+                    }
+                    if item.insert_text_format.is_none() && default_insert_text_format.is_some() {
+                        item.insert_text_format = default_insert_text_format.cloned()
+                    }
+                    if item.insert_text_mode.is_none() && default_insert_text_mode.is_some() {
+                        item.insert_text_mode = default_insert_text_mode.cloned()
                     }
                 }
             }