diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 01507c4e31f53facbbe44a4ea489f7789217d1d9..669134ef1028d7e1807e18c755d03f67e9217c1f 100644 --- a/crates/editor/src/editor_tests.rs +++ b/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::(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!["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::(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::(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, |_| {}); diff --git a/crates/lsp/src/lsp.rs b/crates/lsp/src/lsp.rs index 87c04030bd5e144921da5405fc2bd4abfb931ad7..98755583e3ce574eb5a5b7e4a76944aab785c6de 100644 --- a/crates/lsp/src/lsp.rs +++ b/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(), ]), }), diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index 6de4902746cb2955eda02b9dc84bfcd76106a950..d317f5a4d496cbab78901db8e2990d7f3da92c1e 100644 --- a/crates/project/src/lsp_command.rs +++ b/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() } } }