diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 1604330ea10d67c6899a5814d968765bf83e8d35..47826797f2e615bf12016a3ad3cd00ffc040b430 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -4104,23 +4104,26 @@ impl Editor { match completion_settings.words { WordsCompletionMode::Enabled => { - completions.extend( - words - .await - .into_iter() - .filter(|(word, _)| word_to_exclude.as_ref() != Some(word)) - .map(|(word, word_range)| Completion { - old_range: old_range.clone(), - new_text: word.clone(), - label: CodeLabel::plain(word, None), - documentation: None, - source: CompletionSource::BufferWord { - word_range, - resolved: false, - }, - confirm: None, - }), - ); + let mut words = words.await; + if let Some(word_to_exclude) = &word_to_exclude { + words.remove(word_to_exclude); + } + for lsp_completion in &completions { + words.remove(&lsp_completion.new_text); + } + completions.extend(words.into_iter().map(|(word, word_range)| { + Completion { + old_range: old_range.clone(), + new_text: word.clone(), + label: CodeLabel::plain(word, None), + documentation: None, + source: CompletionSource::BufferWord { + word_range, + resolved: false, + }, + confirm: None, + } + })); } WordsCompletionMode::Fallback => { if completions.is_empty() { diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 5b86c74d7a4ff6e88152c3a475e13b5ade814122..318e6ede48cbb1c55b6de838d67a7fd8622a90c3 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -9236,11 +9236,11 @@ async fn test_words_completion(cx: &mut TestAppContext) { Ok(Some(lsp::CompletionResponse::Array(vec![ lsp::CompletionItem { label: "first".into(), - ..Default::default() + ..lsp::CompletionItem::default() }, lsp::CompletionItem { label: "last".into(), - ..Default::default() + ..lsp::CompletionItem::default() }, ]))) } @@ -9290,6 +9290,69 @@ async fn test_words_completion(cx: &mut TestAppContext) { }); } +#[gpui::test] +async fn test_word_completions_do_not_duplicate_lsp_ones(cx: &mut TestAppContext) { + init_test(cx, |language_settings| { + language_settings.defaults.completions = Some(CompletionSettings { + words: WordsCompletionMode::Enabled, + lsp: true, + lsp_fetch_timeout_ms: 0, + }); + }); + + let mut cx = EditorLspTestContext::new_rust( + lsp::ServerCapabilities { + completion_provider: Some(lsp::CompletionOptions { + trigger_characters: Some(vec![".".to_string(), ":".to_string()]), + ..lsp::CompletionOptions::default() + }), + signature_help_provider: Some(lsp::SignatureHelpOptions::default()), + ..lsp::ServerCapabilities::default() + }, + cx, + ) + .await; + + let _completion_requests_handler = + cx.lsp + .server + .on_request::(move |_, _| async move { + Ok(Some(lsp::CompletionResponse::Array(vec![ + lsp::CompletionItem { + label: "first".into(), + ..lsp::CompletionItem::default() + }, + lsp::CompletionItem { + label: "last".into(), + ..lsp::CompletionItem::default() + }, + ]))) + }); + + cx.set_state(indoc! {"ˇ + first + last + second + "}); + cx.simulate_keystroke("."); + cx.executor().run_until_parked(); + cx.condition(|editor, _| editor.context_menu_visible()) + .await; + cx.update_editor(|editor, window, cx| { + if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref() + { + assert_eq!( + completion_menu_entries(&menu), + &["first", "last", "second"], + "Word completions that has the same edit as the any of the LSP ones, should not be proposed" + ); + } else { + panic!("expected completion menu to be open"); + } + editor.cancel(&Cancel, window, cx); + }); +} + #[gpui::test] async fn test_multiline_completion(cx: &mut TestAppContext) { init_test(cx, |_| {});