Avoid re-querying language server completions when possible (#31872)

Michael Sloan created

Also adds reuse of the markdown documentation cache even when
completions are re-queried, so that markdown documentation doesn't
flicker when `is_incomplete: true` (completions provided by rust
analyzer always set this)

Release Notes:

- Added support for filtering language server completions instead of
re-querying.

Change summary

crates/agent/src/context_picker/completion_provider.rs |  21 
crates/assistant_context_editor/src/slash_command.rs   | 183 +-
crates/collab_ui/src/chat_panel/message_editor.rs      |  65 
crates/debugger_ui/src/session/running/console.rs      | 153 +-
crates/editor/src/code_context_menus.rs                | 563 +++++++---
crates/editor/src/editor.rs                            | 589 ++++++-----
crates/editor/src/editor_tests.rs                      | 170 +++
crates/editor/src/hover_popover.rs                     |   3 
crates/editor/src/jsx_tag_auto_close.rs                |   2 
crates/extension_host/src/extension_store_test.rs      |   2 
crates/inspector_ui/src/div_inspector.rs               |  13 
crates/project/src/lsp_command.rs                      |  51 
crates/project/src/lsp_store.rs                        |  59 
crates/project/src/project.rs                          |  19 
crates/project/src/project_tests.rs                    |  42 
crates/proto/proto/lsp.proto                           |   2 
crates/remote_server/src/remote_editing_tests.rs       |   2 
17 files changed, 1,220 insertions(+), 719 deletions(-)

Detailed changes

crates/agent/src/context_picker/completion_provider.rs 🔗

@@ -14,7 +14,7 @@ use http_client::HttpClientWithUrl;
 use itertools::Itertools;
 use language::{Buffer, CodeLabel, HighlightId};
 use lsp::CompletionContext;
-use project::{Completion, CompletionIntent, ProjectPath, Symbol, WorktreeId};
+use project::{Completion, CompletionIntent, CompletionResponse, ProjectPath, Symbol, WorktreeId};
 use prompt_store::PromptStore;
 use rope::Point;
 use text::{Anchor, OffsetRangeExt, ToPoint};
@@ -746,7 +746,7 @@ impl CompletionProvider for ContextPickerCompletionProvider {
         _trigger: CompletionContext,
         _window: &mut Window,
         cx: &mut Context<Editor>,
-    ) -> Task<Result<Option<Vec<Completion>>>> {
+    ) -> Task<Result<Vec<CompletionResponse>>> {
         let state = buffer.update(cx, |buffer, _cx| {
             let position = buffer_position.to_point(buffer);
             let line_start = Point::new(position.row, 0);
@@ -756,13 +756,13 @@ impl CompletionProvider for ContextPickerCompletionProvider {
             MentionCompletion::try_parse(line, offset_to_line)
         });
         let Some(state) = state else {
-            return Task::ready(Ok(None));
+            return Task::ready(Ok(Vec::new()));
         };
 
         let Some((workspace, context_store)) =
             self.workspace.upgrade().zip(self.context_store.upgrade())
         else {
-            return Task::ready(Ok(None));
+            return Task::ready(Ok(Vec::new()));
         };
 
         let snapshot = buffer.read(cx).snapshot();
@@ -815,10 +815,10 @@ impl CompletionProvider for ContextPickerCompletionProvider {
         cx.spawn(async move |_, cx| {
             let matches = search_task.await;
             let Some(editor) = editor.upgrade() else {
-                return Ok(None);
+                return Ok(Vec::new());
             };
 
-            Ok(Some(cx.update(|cx| {
+            let completions = cx.update(|cx| {
                 matches
                     .into_iter()
                     .filter_map(|mat| match mat {
@@ -901,7 +901,14 @@ impl CompletionProvider for ContextPickerCompletionProvider {
                         ),
                     })
                     .collect()
-            })?))
+            })?;
+
+            Ok(vec![CompletionResponse {
+                completions,
+                // Since this does its own filtering (see `filter_completions()` returns false),
+                // there is no benefit to computing whether this set of completions is incomplete.
+                is_incomplete: true,
+            }])
         })
     }
 

crates/assistant_context_editor/src/slash_command.rs 🔗

@@ -48,7 +48,7 @@ impl SlashCommandCompletionProvider {
         name_range: Range<Anchor>,
         window: &mut Window,
         cx: &mut App,
-    ) -> Task<Result<Option<Vec<project::Completion>>>> {
+    ) -> Task<Result<Vec<project::CompletionResponse>>> {
         let slash_commands = self.slash_commands.clone();
         let candidates = slash_commands
             .command_names(cx)
@@ -71,28 +71,27 @@ impl SlashCommandCompletionProvider {
             .await;
 
             cx.update(|_, cx| {
-                Some(
-                    matches
-                        .into_iter()
-                        .filter_map(|mat| {
-                            let command = slash_commands.command(&mat.string, cx)?;
-                            let mut new_text = mat.string.clone();
-                            let requires_argument = command.requires_argument();
-                            let accepts_arguments = command.accepts_arguments();
-                            if requires_argument || accepts_arguments {
-                                new_text.push(' ');
-                            }
+                let completions = matches
+                    .into_iter()
+                    .filter_map(|mat| {
+                        let command = slash_commands.command(&mat.string, cx)?;
+                        let mut new_text = mat.string.clone();
+                        let requires_argument = command.requires_argument();
+                        let accepts_arguments = command.accepts_arguments();
+                        if requires_argument || accepts_arguments {
+                            new_text.push(' ');
+                        }
 
-                            let confirm =
-                                editor
-                                    .clone()
-                                    .zip(workspace.clone())
-                                    .map(|(editor, workspace)| {
-                                        let command_name = mat.string.clone();
-                                        let command_range = command_range.clone();
-                                        let editor = editor.clone();
-                                        let workspace = workspace.clone();
-                                        Arc::new(
+                        let confirm =
+                            editor
+                                .clone()
+                                .zip(workspace.clone())
+                                .map(|(editor, workspace)| {
+                                    let command_name = mat.string.clone();
+                                    let command_range = command_range.clone();
+                                    let editor = editor.clone();
+                                    let workspace = workspace.clone();
+                                    Arc::new(
                                             move |intent: CompletionIntent,
                                             window: &mut Window,
                                             cx: &mut App| {
@@ -118,22 +117,27 @@ impl SlashCommandCompletionProvider {
                                                 }
                                             },
                                         ) as Arc<_>
-                                    });
-                            Some(project::Completion {
-                                replace_range: name_range.clone(),
-                                documentation: Some(CompletionDocumentation::SingleLine(
-                                    command.description().into(),
-                                )),
-                                new_text,
-                                label: command.label(cx),
-                                icon_path: None,
-                                insert_text_mode: None,
-                                confirm,
-                                source: CompletionSource::Custom,
-                            })
+                                });
+
+                        Some(project::Completion {
+                            replace_range: name_range.clone(),
+                            documentation: Some(CompletionDocumentation::SingleLine(
+                                command.description().into(),
+                            )),
+                            new_text,
+                            label: command.label(cx),
+                            icon_path: None,
+                            insert_text_mode: None,
+                            confirm,
+                            source: CompletionSource::Custom,
                         })
-                        .collect(),
-                )
+                    })
+                    .collect();
+
+                vec![project::CompletionResponse {
+                    completions,
+                    is_incomplete: false,
+                }]
             })
         })
     }
@@ -147,7 +151,7 @@ impl SlashCommandCompletionProvider {
         last_argument_range: Range<Anchor>,
         window: &mut Window,
         cx: &mut App,
-    ) -> Task<Result<Option<Vec<project::Completion>>>> {
+    ) -> Task<Result<Vec<project::CompletionResponse>>> {
         let new_cancel_flag = Arc::new(AtomicBool::new(false));
         let mut flag = self.cancel_flag.lock();
         flag.store(true, SeqCst);
@@ -165,28 +169,27 @@ impl SlashCommandCompletionProvider {
             let workspace = self.workspace.clone();
             let arguments = arguments.to_vec();
             cx.background_spawn(async move {
-                Ok(Some(
-                    completions
-                        .await?
-                        .into_iter()
-                        .map(|new_argument| {
-                            let confirm =
-                                editor
-                                    .clone()
-                                    .zip(workspace.clone())
-                                    .map(|(editor, workspace)| {
-                                        Arc::new({
-                                            let mut completed_arguments = arguments.clone();
-                                            if new_argument.replace_previous_arguments {
-                                                completed_arguments.clear();
-                                            } else {
-                                                completed_arguments.pop();
-                                            }
-                                            completed_arguments.push(new_argument.new_text.clone());
+                let completions = completions
+                    .await?
+                    .into_iter()
+                    .map(|new_argument| {
+                        let confirm =
+                            editor
+                                .clone()
+                                .zip(workspace.clone())
+                                .map(|(editor, workspace)| {
+                                    Arc::new({
+                                        let mut completed_arguments = arguments.clone();
+                                        if new_argument.replace_previous_arguments {
+                                            completed_arguments.clear();
+                                        } else {
+                                            completed_arguments.pop();
+                                        }
+                                        completed_arguments.push(new_argument.new_text.clone());
 
-                                            let command_range = command_range.clone();
-                                            let command_name = command_name.clone();
-                                            move |intent: CompletionIntent,
+                                        let command_range = command_range.clone();
+                                        let command_name = command_name.clone();
+                                        move |intent: CompletionIntent,
                                               window: &mut Window,
                                               cx: &mut App| {
                                             if new_argument.after_completion.run()
@@ -210,34 +213,41 @@ impl SlashCommandCompletionProvider {
                                                 !new_argument.after_completion.run()
                                             }
                                         }
-                                        }) as Arc<_>
-                                    });
+                                    }) as Arc<_>
+                                });
 
-                            let mut new_text = new_argument.new_text.clone();
-                            if new_argument.after_completion == AfterCompletion::Continue {
-                                new_text.push(' ');
-                            }
+                        let mut new_text = new_argument.new_text.clone();
+                        if new_argument.after_completion == AfterCompletion::Continue {
+                            new_text.push(' ');
+                        }
 
-                            project::Completion {
-                                replace_range: if new_argument.replace_previous_arguments {
-                                    argument_range.clone()
-                                } else {
-                                    last_argument_range.clone()
-                                },
-                                label: new_argument.label,
-                                icon_path: None,
-                                new_text,
-                                documentation: None,
-                                confirm,
-                                insert_text_mode: None,
-                                source: CompletionSource::Custom,
-                            }
-                        })
-                        .collect(),
-                ))
+                        project::Completion {
+                            replace_range: if new_argument.replace_previous_arguments {
+                                argument_range.clone()
+                            } else {
+                                last_argument_range.clone()
+                            },
+                            label: new_argument.label,
+                            icon_path: None,
+                            new_text,
+                            documentation: None,
+                            confirm,
+                            insert_text_mode: None,
+                            source: CompletionSource::Custom,
+                        }
+                    })
+                    .collect();
+
+                Ok(vec![project::CompletionResponse {
+                    completions,
+                    is_incomplete: false,
+                }])
             })
         } else {
-            Task::ready(Ok(Some(Vec::new())))
+            Task::ready(Ok(vec![project::CompletionResponse {
+                completions: Vec::new(),
+                is_incomplete: false,
+            }]))
         }
     }
 }
@@ -251,7 +261,7 @@ impl CompletionProvider for SlashCommandCompletionProvider {
         _: editor::CompletionContext,
         window: &mut Window,
         cx: &mut Context<Editor>,
-    ) -> Task<Result<Option<Vec<project::Completion>>>> {
+    ) -> Task<Result<Vec<project::CompletionResponse>>> {
         let Some((name, arguments, command_range, last_argument_range)) =
             buffer.update(cx, |buffer, _cx| {
                 let position = buffer_position.to_point(buffer);
@@ -295,7 +305,10 @@ impl CompletionProvider for SlashCommandCompletionProvider {
                 Some((name, arguments, command_range, last_argument_range))
             })
         else {
-            return Task::ready(Ok(Some(Vec::new())));
+            return Task::ready(Ok(vec![project::CompletionResponse {
+                completions: Vec::new(),
+                is_incomplete: false,
+            }]));
         };
 
         if let Some((arguments, argument_range)) = arguments {

crates/collab_ui/src/chat_panel/message_editor.rs 🔗

@@ -12,7 +12,7 @@ use language::{
     Anchor, Buffer, BufferSnapshot, CodeLabel, LanguageRegistry, ToOffset,
     language_settings::SoftWrap,
 };
-use project::{Completion, CompletionSource, search::SearchQuery};
+use project::{Completion, CompletionResponse, CompletionSource, search::SearchQuery};
 use settings::Settings;
 use std::{
     cell::RefCell,
@@ -64,9 +64,9 @@ impl CompletionProvider for MessageEditorCompletionProvider {
         _: editor::CompletionContext,
         _window: &mut Window,
         cx: &mut Context<Editor>,
-    ) -> Task<Result<Option<Vec<Completion>>>> {
+    ) -> Task<Result<Vec<CompletionResponse>>> {
         let Some(handle) = self.0.upgrade() else {
-            return Task::ready(Ok(None));
+            return Task::ready(Ok(Vec::new()));
         };
         handle.update(cx, |message_editor, cx| {
             message_editor.completions(buffer, buffer_position, cx)
@@ -248,22 +248,21 @@ impl MessageEditor {
         buffer: &Entity<Buffer>,
         end_anchor: Anchor,
         cx: &mut Context<Self>,
-    ) -> Task<Result<Option<Vec<Completion>>>> {
+    ) -> Task<Result<Vec<CompletionResponse>>> {
         if let Some((start_anchor, query, candidates)) =
             self.collect_mention_candidates(buffer, end_anchor, cx)
         {
             if !candidates.is_empty() {
                 return cx.spawn(async move |_, cx| {
-                    Ok(Some(
-                        Self::resolve_completions_for_candidates(
-                            &cx,
-                            query.as_str(),
-                            &candidates,
-                            start_anchor..end_anchor,
-                            Self::completion_for_mention,
-                        )
-                        .await,
-                    ))
+                    let completion_response = Self::resolve_completions_for_candidates(
+                        &cx,
+                        query.as_str(),
+                        &candidates,
+                        start_anchor..end_anchor,
+                        Self::completion_for_mention,
+                    )
+                    .await;
+                    Ok(vec![completion_response])
                 });
             }
         }
@@ -273,21 +272,23 @@ impl MessageEditor {
         {
             if !candidates.is_empty() {
                 return cx.spawn(async move |_, cx| {
-                    Ok(Some(
-                        Self::resolve_completions_for_candidates(
-                            &cx,
-                            query.as_str(),
-                            candidates,
-                            start_anchor..end_anchor,
-                            Self::completion_for_emoji,
-                        )
-                        .await,
-                    ))
+                    let completion_response = Self::resolve_completions_for_candidates(
+                        &cx,
+                        query.as_str(),
+                        candidates,
+                        start_anchor..end_anchor,
+                        Self::completion_for_emoji,
+                    )
+                    .await;
+                    Ok(vec![completion_response])
                 });
             }
         }
 
-        Task::ready(Ok(Some(Vec::new())))
+        Task::ready(Ok(vec![CompletionResponse {
+            completions: Vec::new(),
+            is_incomplete: false,
+        }]))
     }
 
     async fn resolve_completions_for_candidates(
@@ -296,18 +297,19 @@ impl MessageEditor {
         candidates: &[StringMatchCandidate],
         range: Range<Anchor>,
         completion_fn: impl Fn(&StringMatch) -> (String, CodeLabel),
-    ) -> Vec<Completion> {
+    ) -> CompletionResponse {
+        const LIMIT: usize = 10;
         let matches = fuzzy::match_strings(
             candidates,
             query,
             true,
-            10,
+            LIMIT,
             &Default::default(),
             cx.background_executor().clone(),
         )
         .await;
 
-        matches
+        let completions = matches
             .into_iter()
             .map(|mat| {
                 let (new_text, label) = completion_fn(&mat);
@@ -322,7 +324,12 @@ impl MessageEditor {
                     source: CompletionSource::Custom,
                 }
             })
-            .collect()
+            .collect::<Vec<_>>();
+
+        CompletionResponse {
+            is_incomplete: completions.len() >= LIMIT,
+            completions,
+        }
     }
 
     fn completion_for_mention(mat: &StringMatch) -> (String, CodeLabel) {

crates/debugger_ui/src/session/running/console.rs 🔗

@@ -13,7 +13,7 @@ use gpui::{
 use language::{Buffer, CodeLabel, ToOffset};
 use menu::Confirm;
 use project::{
-    Completion,
+    Completion, CompletionResponse,
     debugger::session::{CompletionsQuery, OutputToken, Session, SessionEvent},
 };
 use settings::Settings;
@@ -262,9 +262,9 @@ impl CompletionProvider for ConsoleQueryBarCompletionProvider {
         _trigger: editor::CompletionContext,
         _window: &mut Window,
         cx: &mut Context<Editor>,
-    ) -> Task<Result<Option<Vec<Completion>>>> {
+    ) -> Task<Result<Vec<CompletionResponse>>> {
         let Some(console) = self.0.upgrade() else {
-            return Task::ready(Ok(None));
+            return Task::ready(Ok(Vec::new()));
         };
 
         let support_completions = console
@@ -322,7 +322,7 @@ impl ConsoleQueryBarCompletionProvider {
         buffer: &Entity<Buffer>,
         buffer_position: language::Anchor,
         cx: &mut Context<Editor>,
-    ) -> Task<Result<Option<Vec<Completion>>>> {
+    ) -> Task<Result<Vec<CompletionResponse>>> {
         let (variables, string_matches) = console.update(cx, |console, cx| {
             let mut variables = HashMap::default();
             let mut string_matches = Vec::default();
@@ -354,39 +354,43 @@ impl ConsoleQueryBarCompletionProvider {
         let query = buffer.read(cx).text();
 
         cx.spawn(async move |_, cx| {
+            const LIMIT: usize = 10;
             let matches = fuzzy::match_strings(
                 &string_matches,
                 &query,
                 true,
-                10,
+                LIMIT,
                 &Default::default(),
                 cx.background_executor().clone(),
             )
             .await;
 
-            Ok(Some(
-                matches
-                    .iter()
-                    .filter_map(|string_match| {
-                        let variable_value = variables.get(&string_match.string)?;
-
-                        Some(project::Completion {
-                            replace_range: buffer_position..buffer_position,
-                            new_text: string_match.string.clone(),
-                            label: CodeLabel {
-                                filter_range: 0..string_match.string.len(),
-                                text: format!("{} {}", string_match.string, variable_value),
-                                runs: Vec::new(),
-                            },
-                            icon_path: None,
-                            documentation: None,
-                            confirm: None,
-                            source: project::CompletionSource::Custom,
-                            insert_text_mode: None,
-                        })
+            let completions = matches
+                .iter()
+                .filter_map(|string_match| {
+                    let variable_value = variables.get(&string_match.string)?;
+
+                    Some(project::Completion {
+                        replace_range: buffer_position..buffer_position,
+                        new_text: string_match.string.clone(),
+                        label: CodeLabel {
+                            filter_range: 0..string_match.string.len(),
+                            text: format!("{} {}", string_match.string, variable_value),
+                            runs: Vec::new(),
+                        },
+                        icon_path: None,
+                        documentation: None,
+                        confirm: None,
+                        source: project::CompletionSource::Custom,
+                        insert_text_mode: None,
                     })
-                    .collect(),
-            ))
+                })
+                .collect::<Vec<_>>();
+
+            Ok(vec![project::CompletionResponse {
+                is_incomplete: completions.len() >= LIMIT,
+                completions,
+            }])
         })
     }
 
@@ -396,7 +400,7 @@ impl ConsoleQueryBarCompletionProvider {
         buffer: &Entity<Buffer>,
         buffer_position: language::Anchor,
         cx: &mut Context<Editor>,
-    ) -> Task<Result<Option<Vec<Completion>>>> {
+    ) -> Task<Result<Vec<CompletionResponse>>> {
         let completion_task = console.update(cx, |console, cx| {
             console.session.update(cx, |state, cx| {
                 let frame_id = console.stack_frame_list.read(cx).opened_stack_frame_id();
@@ -411,53 +415,56 @@ impl ConsoleQueryBarCompletionProvider {
         cx.background_executor().spawn(async move {
             let completions = completion_task.await?;
 
-            Ok(Some(
-                completions
-                    .into_iter()
-                    .map(|completion| {
-                        let new_text = completion
-                            .text
-                            .as_ref()
-                            .unwrap_or(&completion.label)
-                            .to_owned();
-                        let buffer_text = snapshot.text();
-                        let buffer_bytes = buffer_text.as_bytes();
-                        let new_bytes = new_text.as_bytes();
-
-                        let mut prefix_len = 0;
-                        for i in (0..new_bytes.len()).rev() {
-                            if buffer_bytes.ends_with(&new_bytes[0..i]) {
-                                prefix_len = i;
-                                break;
-                            }
+            let completions = completions
+                .into_iter()
+                .map(|completion| {
+                    let new_text = completion
+                        .text
+                        .as_ref()
+                        .unwrap_or(&completion.label)
+                        .to_owned();
+                    let buffer_text = snapshot.text();
+                    let buffer_bytes = buffer_text.as_bytes();
+                    let new_bytes = new_text.as_bytes();
+
+                    let mut prefix_len = 0;
+                    for i in (0..new_bytes.len()).rev() {
+                        if buffer_bytes.ends_with(&new_bytes[0..i]) {
+                            prefix_len = i;
+                            break;
                         }
+                    }
 
-                        let buffer_offset = buffer_position.to_offset(&snapshot);
-                        let start = buffer_offset - prefix_len;
-                        let start = snapshot.clip_offset(start, Bias::Left);
-                        let start = snapshot.anchor_before(start);
-                        let replace_range = start..buffer_position;
-
-                        project::Completion {
-                            replace_range,
-                            new_text,
-                            label: CodeLabel {
-                                filter_range: 0..completion.label.len(),
-                                text: completion.label,
-                                runs: Vec::new(),
-                            },
-                            icon_path: None,
-                            documentation: None,
-                            confirm: None,
-                            source: project::CompletionSource::BufferWord {
-                                word_range: buffer_position..language::Anchor::MAX,
-                                resolved: false,
-                            },
-                            insert_text_mode: None,
-                        }
-                    })
-                    .collect(),
-            ))
+                    let buffer_offset = buffer_position.to_offset(&snapshot);
+                    let start = buffer_offset - prefix_len;
+                    let start = snapshot.clip_offset(start, Bias::Left);
+                    let start = snapshot.anchor_before(start);
+                    let replace_range = start..buffer_position;
+
+                    project::Completion {
+                        replace_range,
+                        new_text,
+                        label: CodeLabel {
+                            filter_range: 0..completion.label.len(),
+                            text: completion.label,
+                            runs: Vec::new(),
+                        },
+                        icon_path: None,
+                        documentation: None,
+                        confirm: None,
+                        source: project::CompletionSource::BufferWord {
+                            word_range: buffer_position..language::Anchor::MAX,
+                            resolved: false,
+                        },
+                        insert_text_mode: None,
+                    }
+                })
+                .collect();
+
+            Ok(vec![project::CompletionResponse {
+                completions,
+                is_incomplete: false,
+            }])
         })
     }
 }

crates/editor/src/code_context_menus.rs 🔗

@@ -1,9 +1,8 @@
 use fuzzy::{StringMatch, StringMatchCandidate};
 use gpui::{
     AnyElement, Entity, Focusable, FontWeight, ListSizingBehavior, ScrollStrategy, SharedString,
-    Size, StrikethroughStyle, StyledText, UniformListScrollHandle, div, px, uniform_list,
+    Size, StrikethroughStyle, StyledText, Task, UniformListScrollHandle, div, px, uniform_list,
 };
-use gpui::{AsyncWindowContext, WeakEntity};
 use itertools::Itertools;
 use language::CodeLabel;
 use language::{Buffer, LanguageName, LanguageRegistry};
@@ -18,6 +17,7 @@ use task::TaskContext;
 
 use std::collections::VecDeque;
 use std::sync::Arc;
+use std::sync::atomic::{AtomicBool, Ordering};
 use std::{
     cell::RefCell,
     cmp::{Reverse, min},
@@ -47,15 +47,10 @@ pub const MENU_ASIDE_MAX_WIDTH: Pixels = px(500.);
 // Constants for the markdown cache. The purpose of this cache is to reduce flickering due to
 // documentation not yet being parsed.
 //
-// The size of the cache is set to the number of items fetched around the current selection plus one
-// for the current selection and another to avoid cases where and adjacent selection exits the
-// cache. The only current benefit of a larger cache would be doing less markdown parsing when the
-// selection revisits items.
-//
-// One future benefit of a larger cache would be reducing flicker on backspace. This would require
-// not recreating the menu on every change, by not re-querying the language server when
-// `is_incomplete = false`.
-const MARKDOWN_CACHE_MAX_SIZE: usize = MARKDOWN_CACHE_BEFORE_ITEMS + MARKDOWN_CACHE_AFTER_ITEMS + 2;
+// The size of the cache is set to 16, which is roughly 3 times more than the number of items
+// fetched around the current selection. This way documentation is more often ready for render when
+// revisiting previous entries, such as when pressing backspace.
+const MARKDOWN_CACHE_MAX_SIZE: usize = 16;
 const MARKDOWN_CACHE_BEFORE_ITEMS: usize = 2;
 const MARKDOWN_CACHE_AFTER_ITEMS: usize = 2;
 
@@ -197,27 +192,48 @@ pub enum ContextMenuOrigin {
     QuickActionBar,
 }
 
-#[derive(Clone)]
 pub struct CompletionsMenu {
     pub id: CompletionId,
     sort_completions: bool,
     pub initial_position: Anchor,
+    pub initial_query: Option<Arc<String>>,
+    pub is_incomplete: bool,
     pub buffer: Entity<Buffer>,
     pub completions: Rc<RefCell<Box<[Completion]>>>,
-    match_candidates: Rc<[StringMatchCandidate]>,
-    pub entries: Rc<RefCell<Vec<StringMatch>>>,
+    match_candidates: Arc<[StringMatchCandidate]>,
+    pub entries: Rc<RefCell<Box<[StringMatch]>>>,
     pub selected_item: usize,
+    filter_task: Task<()>,
+    cancel_filter: Arc<AtomicBool>,
     scroll_handle: UniformListScrollHandle,
     resolve_completions: bool,
     show_completion_documentation: bool,
     pub(super) ignore_completion_provider: bool,
     last_rendered_range: Rc<RefCell<Option<Range<usize>>>>,
-    markdown_cache: Rc<RefCell<VecDeque<(usize, Entity<Markdown>)>>>,
+    markdown_cache: Rc<RefCell<VecDeque<(MarkdownCacheKey, Entity<Markdown>)>>>,
     language_registry: Option<Arc<LanguageRegistry>>,
     language: Option<LanguageName>,
     snippet_sort_order: SnippetSortOrder,
 }
 
+#[derive(Clone, Debug, PartialEq)]
+enum MarkdownCacheKey {
+    ForCandidate {
+        candidate_id: usize,
+    },
+    ForCompletionMatch {
+        new_text: String,
+        markdown_source: SharedString,
+    },
+}
+
+// TODO: There should really be a wrapper around fuzzy match tasks that does this.
+impl Drop for CompletionsMenu {
+    fn drop(&mut self) {
+        self.cancel_filter.store(true, Ordering::Relaxed);
+    }
+}
+
 impl CompletionsMenu {
     pub fn new(
         id: CompletionId,
@@ -225,6 +241,8 @@ impl CompletionsMenu {
         show_completion_documentation: bool,
         ignore_completion_provider: bool,
         initial_position: Anchor,
+        initial_query: Option<Arc<String>>,
+        is_incomplete: bool,
         buffer: Entity<Buffer>,
         completions: Box<[Completion]>,
         snippet_sort_order: SnippetSortOrder,
@@ -242,17 +260,21 @@ impl CompletionsMenu {
             id,
             sort_completions,
             initial_position,
+            initial_query,
+            is_incomplete,
             buffer,
             show_completion_documentation,
             ignore_completion_provider,
             completions: RefCell::new(completions).into(),
             match_candidates,
-            entries: RefCell::new(Vec::new()).into(),
+            entries: Rc::new(RefCell::new(Box::new([]))),
             selected_item: 0,
+            filter_task: Task::ready(()),
+            cancel_filter: Arc::new(AtomicBool::new(false)),
             scroll_handle: UniformListScrollHandle::new(),
             resolve_completions: true,
             last_rendered_range: RefCell::new(None).into(),
-            markdown_cache: RefCell::new(VecDeque::with_capacity(MARKDOWN_CACHE_MAX_SIZE)).into(),
+            markdown_cache: RefCell::new(VecDeque::new()).into(),
             language_registry,
             language,
             snippet_sort_order,
@@ -303,16 +325,20 @@ impl CompletionsMenu {
                 positions: vec![],
                 string: completion.clone(),
             })
-            .collect::<Vec<_>>();
+            .collect();
         Self {
             id,
             sort_completions,
             initial_position: selection.start,
+            initial_query: None,
+            is_incomplete: false,
             buffer,
             completions: RefCell::new(completions).into(),
             match_candidates,
             entries: RefCell::new(entries).into(),
             selected_item: 0,
+            filter_task: Task::ready(()),
+            cancel_filter: Arc::new(AtomicBool::new(false)),
             scroll_handle: UniformListScrollHandle::new(),
             resolve_completions: false,
             show_completion_documentation: false,
@@ -390,14 +416,7 @@ impl CompletionsMenu {
     ) {
         if self.selected_item != match_index {
             self.selected_item = match_index;
-            self.scroll_handle
-                .scroll_to_item(self.selected_item, ScrollStrategy::Top);
-            self.resolve_visible_completions(provider, cx);
-            self.start_markdown_parse_for_nearby_entries(cx);
-            if let Some(provider) = provider {
-                self.handle_selection_changed(provider, window, cx);
-            }
-            cx.notify();
+            self.handle_selection_changed(provider, window, cx);
         }
     }
 
@@ -418,18 +437,25 @@ impl CompletionsMenu {
     }
 
     fn handle_selection_changed(
-        &self,
-        provider: &dyn CompletionProvider,
+        &mut self,
+        provider: Option<&dyn CompletionProvider>,
         window: &mut Window,
-        cx: &mut App,
+        cx: &mut Context<Editor>,
     ) {
-        let entries = self.entries.borrow();
-        let entry = if self.selected_item < entries.len() {
-            Some(&entries[self.selected_item])
-        } else {
-            None
-        };
-        provider.selection_changed(entry, window, cx);
+        self.scroll_handle
+            .scroll_to_item(self.selected_item, ScrollStrategy::Top);
+        if let Some(provider) = provider {
+            let entries = self.entries.borrow();
+            let entry = if self.selected_item < entries.len() {
+                Some(&entries[self.selected_item])
+            } else {
+                None
+            };
+            provider.selection_changed(entry, window, cx);
+        }
+        self.resolve_visible_completions(provider, cx);
+        self.start_markdown_parse_for_nearby_entries(cx);
+        cx.notify();
     }
 
     pub fn resolve_visible_completions(
@@ -444,6 +470,19 @@ impl CompletionsMenu {
             return;
         };
 
+        let entries = self.entries.borrow();
+        if entries.is_empty() {
+            return;
+        }
+        if self.selected_item >= entries.len() {
+            log::error!(
+                "bug: completion selected_item >= entries.len(): {} >= {}",
+                self.selected_item,
+                entries.len()
+            );
+            self.selected_item = entries.len() - 1;
+        }
+
         // Attempt to resolve completions for every item that will be displayed. This matters
         // because single line documentation may be displayed inline with the completion.
         //
@@ -455,7 +494,6 @@ impl CompletionsMenu {
         let visible_count = last_rendered_range
             .clone()
             .map_or(APPROXIMATE_VISIBLE_COUNT, |range| range.count());
-        let entries = self.entries.borrow();
         let entry_range = if self.selected_item == 0 {
             0..min(visible_count, entries.len())
         } else if self.selected_item == entries.len() - 1 {
@@ -508,11 +546,11 @@ impl CompletionsMenu {
                     .update(cx, |editor, cx| {
                         // `resolve_completions` modified state affecting display.
                         cx.notify();
-                        editor.with_completions_menu_matching_id(
-                            completion_id,
-                            || (),
-                            |this| this.start_markdown_parse_for_nearby_entries(cx),
-                        );
+                        editor.with_completions_menu_matching_id(completion_id, |menu| {
+                            if let Some(menu) = menu {
+                                menu.start_markdown_parse_for_nearby_entries(cx)
+                            }
+                        });
                     })
                     .ok();
             }
@@ -548,11 +586,11 @@ impl CompletionsMenu {
             return None;
         }
         let candidate_id = entries[index].candidate_id;
-        match &self.completions.borrow()[candidate_id].documentation {
-            Some(CompletionDocumentation::MultiLineMarkdown(source)) if !source.is_empty() => Some(
-                self.get_or_create_markdown(candidate_id, source.clone(), false, cx)
-                    .1,
-            ),
+        let completions = self.completions.borrow();
+        match &completions[candidate_id].documentation {
+            Some(CompletionDocumentation::MultiLineMarkdown(source)) if !source.is_empty() => self
+                .get_or_create_markdown(candidate_id, Some(source), false, &completions, cx)
+                .map(|(_, markdown)| markdown),
             Some(_) => None,
             _ => None,
         }
@@ -561,38 +599,75 @@ impl CompletionsMenu {
     fn get_or_create_markdown(
         &self,
         candidate_id: usize,
-        source: SharedString,
+        source: Option<&SharedString>,
         is_render: bool,
+        completions: &[Completion],
         cx: &mut Context<Editor>,
-    ) -> (bool, Entity<Markdown>) {
+    ) -> Option<(bool, Entity<Markdown>)> {
         let mut markdown_cache = self.markdown_cache.borrow_mut();
-        if let Some((cache_index, (_, markdown))) = markdown_cache
-            .iter()
-            .find_position(|(id, _)| *id == candidate_id)
-        {
-            let markdown = if is_render && cache_index != 0 {
+
+        let mut has_completion_match_cache_entry = false;
+        let mut matching_entry = markdown_cache.iter().find_position(|(key, _)| match key {
+            MarkdownCacheKey::ForCandidate { candidate_id: id } => *id == candidate_id,
+            MarkdownCacheKey::ForCompletionMatch { .. } => {
+                has_completion_match_cache_entry = true;
+                false
+            }
+        });
+
+        if has_completion_match_cache_entry && matching_entry.is_none() {
+            if let Some(source) = source {
+                matching_entry = markdown_cache.iter().find_position(|(key, _)| {
+                    matches!(key, MarkdownCacheKey::ForCompletionMatch { markdown_source, .. }
+                                if markdown_source == source)
+                });
+            } else {
+                // Heuristic guess that documentation can be reused when new_text matches. This is
+                // to mitigate documentation flicker while typing. If this is wrong, then resolution
+                // should cause the correct documentation to be displayed soon.
+                let completion = &completions[candidate_id];
+                matching_entry = markdown_cache.iter().find_position(|(key, _)| {
+                    matches!(key, MarkdownCacheKey::ForCompletionMatch { new_text, .. }
+                                if new_text == &completion.new_text)
+                });
+            }
+        }
+
+        if let Some((cache_index, (key, markdown))) = matching_entry {
+            let markdown = markdown.clone();
+
+            // Since the markdown source matches, the key can now be ForCandidate.
+            if source.is_some() && matches!(key, MarkdownCacheKey::ForCompletionMatch { .. }) {
+                markdown_cache[cache_index].0 = MarkdownCacheKey::ForCandidate { candidate_id };
+            }
+
+            if is_render && cache_index != 0 {
                 // Move the current selection's cache entry to the front.
                 markdown_cache.rotate_right(1);
                 let cache_len = markdown_cache.len();
                 markdown_cache.swap(0, (cache_index + 1) % cache_len);
-                &markdown_cache[0].1
-            } else {
-                markdown
-            };
+            }
 
             let is_parsing = markdown.update(cx, |markdown, cx| {
-                // `reset` is called as it's possible for documentation to change due to resolve
-                // requests. It does nothing if `source` is unchanged.
-                markdown.reset(source, cx);
+                if let Some(source) = source {
+                    // `reset` is called as it's possible for documentation to change due to resolve
+                    // requests. It does nothing if `source` is unchanged.
+                    markdown.reset(source.clone(), cx);
+                }
                 markdown.is_parsing()
             });
-            return (is_parsing, markdown.clone());
+            return Some((is_parsing, markdown));
         }
 
+        let Some(source) = source else {
+            // Can't create markdown as there is no source.
+            return None;
+        };
+
         if markdown_cache.len() < MARKDOWN_CACHE_MAX_SIZE {
             let markdown = cx.new(|cx| {
                 Markdown::new(
-                    source,
+                    source.clone(),
                     self.language_registry.clone(),
                     self.language.clone(),
                     cx,
@@ -601,17 +676,20 @@ impl CompletionsMenu {
             // Handles redraw when the markdown is done parsing. The current render is for a
             // deferred draw, and so without this did not redraw when `markdown` notified.
             cx.observe(&markdown, |_, _, cx| cx.notify()).detach();
-            markdown_cache.push_front((candidate_id, markdown.clone()));
-            (true, markdown)
+            markdown_cache.push_front((
+                MarkdownCacheKey::ForCandidate { candidate_id },
+                markdown.clone(),
+            ));
+            Some((true, markdown))
         } else {
             debug_assert_eq!(markdown_cache.capacity(), MARKDOWN_CACHE_MAX_SIZE);
             // Moves the last cache entry to the start. The ring buffer is full, so this does no
             // copying and just shifts indexes.
             markdown_cache.rotate_right(1);
-            markdown_cache[0].0 = candidate_id;
+            markdown_cache[0].0 = MarkdownCacheKey::ForCandidate { candidate_id };
             let markdown = &markdown_cache[0].1;
-            markdown.update(cx, |markdown, cx| markdown.reset(source, cx));
-            (true, markdown.clone())
+            markdown.update(cx, |markdown, cx| markdown.reset(source.clone(), cx));
+            Some((true, markdown.clone()))
         }
     }
 
@@ -774,37 +852,46 @@ impl CompletionsMenu {
         }
 
         let mat = &self.entries.borrow()[self.selected_item];
-        let multiline_docs = match self.completions.borrow_mut()[mat.candidate_id]
-            .documentation
-            .as_ref()?
-        {
-            CompletionDocumentation::MultiLinePlainText(text) => div().child(text.clone()),
-            CompletionDocumentation::SingleLineAndMultiLinePlainText {
+        let completions = self.completions.borrow_mut();
+        let multiline_docs = match completions[mat.candidate_id].documentation.as_ref() {
+            Some(CompletionDocumentation::MultiLinePlainText(text)) => div().child(text.clone()),
+            Some(CompletionDocumentation::SingleLineAndMultiLinePlainText {
                 plain_text: Some(text),
                 ..
-            } => div().child(text.clone()),
-            CompletionDocumentation::MultiLineMarkdown(source) if !source.is_empty() => {
-                let (is_parsing, markdown) =
-                    self.get_or_create_markdown(mat.candidate_id, source.clone(), true, cx);
-                if is_parsing {
+            }) => div().child(text.clone()),
+            Some(CompletionDocumentation::MultiLineMarkdown(source)) if !source.is_empty() => {
+                let Some((false, markdown)) = self.get_or_create_markdown(
+                    mat.candidate_id,
+                    Some(source),
+                    true,
+                    &completions,
+                    cx,
+                ) else {
                     return None;
-                }
-                div().child(
-                    MarkdownElement::new(markdown, hover_markdown_style(window, cx))
-                        .code_block_renderer(markdown::CodeBlockRenderer::Default {
-                            copy_button: false,
-                            copy_button_on_hover: false,
-                            border: false,
-                        })
-                        .on_url_click(open_markdown_url),
-                )
+                };
+                Self::render_markdown(markdown, window, cx)
+            }
+            None => {
+                // Handle the case where documentation hasn't yet been resolved but there's a
+                // `new_text` match in the cache.
+                //
+                // TODO: It's inconsistent that documentation caching based on matching `new_text`
+                // only works for markdown. Consider generally caching the results of resolving
+                // completions.
+                let Some((false, markdown)) =
+                    self.get_or_create_markdown(mat.candidate_id, None, true, &completions, cx)
+                else {
+                    return None;
+                };
+                Self::render_markdown(markdown, window, cx)
             }
-            CompletionDocumentation::MultiLineMarkdown(_) => return None,
-            CompletionDocumentation::SingleLine(_) => return None,
-            CompletionDocumentation::Undocumented => return None,
-            CompletionDocumentation::SingleLineAndMultiLinePlainText {
-                plain_text: None, ..
-            } => {
+            Some(CompletionDocumentation::MultiLineMarkdown(_)) => return None,
+            Some(CompletionDocumentation::SingleLine(_)) => return None,
+            Some(CompletionDocumentation::Undocumented) => return None,
+            Some(CompletionDocumentation::SingleLineAndMultiLinePlainText {
+                plain_text: None,
+                ..
+            }) => {
                 return None;
             }
         };
@@ -824,6 +911,177 @@ impl CompletionsMenu {
         )
     }
 
+    fn render_markdown(
+        markdown: Entity<Markdown>,
+        window: &mut Window,
+        cx: &mut Context<Editor>,
+    ) -> Div {
+        div().child(
+            MarkdownElement::new(markdown, hover_markdown_style(window, cx))
+                .code_block_renderer(markdown::CodeBlockRenderer::Default {
+                    copy_button: false,
+                    copy_button_on_hover: false,
+                    border: false,
+                })
+                .on_url_click(open_markdown_url),
+        )
+    }
+
+    pub fn filter(
+        &mut self,
+        query: Option<Arc<String>>,
+        provider: Option<Rc<dyn CompletionProvider>>,
+        window: &mut Window,
+        cx: &mut Context<Editor>,
+    ) {
+        self.cancel_filter.store(true, Ordering::Relaxed);
+        if let Some(query) = query {
+            self.cancel_filter = Arc::new(AtomicBool::new(false));
+            let matches = self.do_async_filtering(query, cx);
+            let id = self.id;
+            self.filter_task = cx.spawn_in(window, async move |editor, cx| {
+                let matches = matches.await;
+                editor
+                    .update_in(cx, |editor, window, cx| {
+                        editor.with_completions_menu_matching_id(id, |this| {
+                            if let Some(this) = this {
+                                this.set_filter_results(matches, provider, window, cx);
+                            }
+                        });
+                    })
+                    .ok();
+            });
+        } else {
+            self.filter_task = Task::ready(());
+            let matches = self.unfiltered_matches();
+            self.set_filter_results(matches, provider, window, cx);
+        }
+    }
+
+    pub fn do_async_filtering(
+        &self,
+        query: Arc<String>,
+        cx: &Context<Editor>,
+    ) -> Task<Vec<StringMatch>> {
+        let matches_task = cx.background_spawn({
+            let query = query.clone();
+            let match_candidates = self.match_candidates.clone();
+            let cancel_filter = self.cancel_filter.clone();
+            let background_executor = cx.background_executor().clone();
+            async move {
+                fuzzy::match_strings(
+                    &match_candidates,
+                    &query,
+                    query.chars().any(|c| c.is_uppercase()),
+                    100,
+                    &cancel_filter,
+                    background_executor,
+                )
+                .await
+            }
+        });
+
+        let completions = self.completions.clone();
+        let sort_completions = self.sort_completions;
+        let snippet_sort_order = self.snippet_sort_order;
+        cx.foreground_executor().spawn(async move {
+            let mut matches = matches_task.await;
+
+            if sort_completions {
+                matches = Self::sort_string_matches(
+                    matches,
+                    Some(&query),
+                    snippet_sort_order,
+                    completions.borrow().as_ref(),
+                );
+            }
+
+            matches
+        })
+    }
+
+    /// Like `do_async_filtering` but there is no filter query, so no need to spawn tasks.
+    pub fn unfiltered_matches(&self) -> Vec<StringMatch> {
+        let mut matches = self
+            .match_candidates
+            .iter()
+            .enumerate()
+            .map(|(candidate_id, candidate)| StringMatch {
+                candidate_id,
+                score: Default::default(),
+                positions: Default::default(),
+                string: candidate.string.clone(),
+            })
+            .collect();
+
+        if self.sort_completions {
+            matches = Self::sort_string_matches(
+                matches,
+                None,
+                self.snippet_sort_order,
+                self.completions.borrow().as_ref(),
+            );
+        }
+
+        matches
+    }
+
+    pub fn set_filter_results(
+        &mut self,
+        matches: Vec<StringMatch>,
+        provider: Option<Rc<dyn CompletionProvider>>,
+        window: &mut Window,
+        cx: &mut Context<Editor>,
+    ) {
+        *self.entries.borrow_mut() = matches.into_boxed_slice();
+        self.selected_item = 0;
+        self.handle_selection_changed(provider.as_deref(), window, cx);
+    }
+
+    fn sort_string_matches(
+        matches: Vec<StringMatch>,
+        query: Option<&str>,
+        snippet_sort_order: SnippetSortOrder,
+        completions: &[Completion],
+    ) -> Vec<StringMatch> {
+        let mut sortable_items: Vec<SortableMatch<'_>> = matches
+            .into_iter()
+            .map(|string_match| {
+                let completion = &completions[string_match.candidate_id];
+
+                let is_snippet = matches!(
+                    &completion.source,
+                    CompletionSource::Lsp { lsp_completion, .. }
+                    if lsp_completion.kind == Some(CompletionItemKind::SNIPPET)
+                );
+
+                let sort_text =
+                    if let CompletionSource::Lsp { lsp_completion, .. } = &completion.source {
+                        lsp_completion.sort_text.as_deref()
+                    } else {
+                        None
+                    };
+
+                let (sort_kind, sort_label) = completion.sort_key();
+
+                SortableMatch {
+                    string_match,
+                    is_snippet,
+                    sort_text,
+                    sort_kind,
+                    sort_label,
+                }
+            })
+            .collect();
+
+        Self::sort_matches(&mut sortable_items, query, snippet_sort_order);
+
+        sortable_items
+            .into_iter()
+            .map(|sortable| sortable.string_match)
+            .collect()
+    }
+
     pub fn sort_matches(
         matches: &mut Vec<SortableMatch<'_>>,
         query: Option<&str>,
@@ -857,6 +1115,7 @@ impl CompletionsMenu {
         let fuzzy_bracket_threshold = max_score * (3.0 / 5.0);
 
         let query_start_lower = query
+            .as_ref()
             .and_then(|q| q.chars().next())
             .and_then(|c| c.to_lowercase().next());
 
@@ -890,6 +1149,7 @@ impl CompletionsMenu {
                 };
                 let sort_mixed_case_prefix_length = Reverse(
                     query
+                        .as_ref()
                         .map(|q| {
                             q.chars()
                                 .zip(mat.string_match.string.chars())
@@ -920,97 +1180,32 @@ impl CompletionsMenu {
         });
     }
 
-    pub async fn filter(
-        &mut self,
-        query: Option<&str>,
-        provider: Option<Rc<dyn CompletionProvider>>,
-        editor: WeakEntity<Editor>,
-        cx: &mut AsyncWindowContext,
-    ) {
-        let mut matches = if let Some(query) = query {
-            fuzzy::match_strings(
-                &self.match_candidates,
-                query,
-                query.chars().any(|c| c.is_uppercase()),
-                100,
-                &Default::default(),
-                cx.background_executor().clone(),
-            )
-            .await
-        } else {
-            self.match_candidates
-                .iter()
-                .enumerate()
-                .map(|(candidate_id, candidate)| StringMatch {
-                    candidate_id,
-                    score: Default::default(),
-                    positions: Default::default(),
-                    string: candidate.string.clone(),
-                })
-                .collect()
-        };
-
-        if self.sort_completions {
-            let completions = self.completions.borrow();
-
-            let mut sortable_items: Vec<SortableMatch<'_>> = matches
-                .into_iter()
-                .map(|string_match| {
-                    let completion = &completions[string_match.candidate_id];
-
-                    let is_snippet = matches!(
-                        &completion.source,
-                        CompletionSource::Lsp { lsp_completion, .. }
-                        if lsp_completion.kind == Some(CompletionItemKind::SNIPPET)
-                    );
-
-                    let sort_text =
-                        if let CompletionSource::Lsp { lsp_completion, .. } = &completion.source {
-                            lsp_completion.sort_text.as_deref()
-                        } else {
-                            None
-                        };
-
-                    let (sort_kind, sort_label) = completion.sort_key();
-
-                    SortableMatch {
-                        string_match,
-                        is_snippet,
-                        sort_text,
-                        sort_kind,
-                        sort_label,
+    pub fn preserve_markdown_cache(&mut self, prev_menu: CompletionsMenu) {
+        self.markdown_cache = prev_menu.markdown_cache.clone();
+
+        // Convert ForCandidate cache keys to ForCompletionMatch keys.
+        let prev_completions = prev_menu.completions.borrow();
+        self.markdown_cache
+            .borrow_mut()
+            .retain_mut(|(key, _markdown)| match key {
+                MarkdownCacheKey::ForCompletionMatch { .. } => true,
+                MarkdownCacheKey::ForCandidate { candidate_id } => {
+                    if let Some(completion) = prev_completions.get(*candidate_id) {
+                        match &completion.documentation {
+                            Some(CompletionDocumentation::MultiLineMarkdown(source)) => {
+                                *key = MarkdownCacheKey::ForCompletionMatch {
+                                    new_text: completion.new_text.clone(),
+                                    markdown_source: source.clone(),
+                                };
+                                true
+                            }
+                            _ => false,
+                        }
+                    } else {
+                        false
                     }
-                })
-                .collect();
-
-            Self::sort_matches(&mut sortable_items, query, self.snippet_sort_order);
-
-            matches = sortable_items
-                .into_iter()
-                .map(|sortable| sortable.string_match)
-                .collect();
-        }
-
-        *self.entries.borrow_mut() = matches;
-        self.selected_item = 0;
-        // This keeps the display consistent when y_flipped.
-        self.scroll_handle.scroll_to_item(0, ScrollStrategy::Top);
-
-        if let Some(provider) = provider {
-            cx.update(|window, cx| {
-                // Since this is async, it's possible the menu has been closed and possibly even
-                // another opened. `provider.selection_changed` should not be called in this case.
-                let this_menu_still_active = editor
-                    .read_with(cx, |editor, _cx| {
-                        editor.with_completions_menu_matching_id(self.id, || false, |_| true)
-                    })
-                    .unwrap_or(false);
-                if this_menu_still_active {
-                    self.handle_selection_changed(&*provider, window, cx);
                 }
-            })
-            .ok();
-        }
+            });
     }
 }
 

crates/editor/src/editor.rs 🔗

@@ -123,7 +123,7 @@ use markdown::Markdown;
 use mouse_context_menu::MouseContextMenu;
 use persistence::DB;
 use project::{
-    BreakpointWithPosition, ProjectPath,
+    BreakpointWithPosition, CompletionResponse, ProjectPath,
     debugger::{
         breakpoint_store::{
             BreakpointEditAction, BreakpointSessionState, BreakpointState, BreakpointStore,
@@ -987,7 +987,7 @@ pub struct Editor {
     context_menu: RefCell<Option<CodeContextMenu>>,
     context_menu_options: Option<ContextMenuOptions>,
     mouse_context_menu: Option<MouseContextMenu>,
-    completion_tasks: Vec<(CompletionId, Task<Option<()>>)>,
+    completion_tasks: Vec<(CompletionId, Task<()>)>,
     inline_blame_popover: Option<InlineBlamePopover>,
     signature_help_state: SignatureHelpState,
     auto_signature_help: Option<bool>,
@@ -1200,7 +1200,7 @@ impl Default for SelectionHistoryMode {
 
 struct DeferredSelectionEffectsState {
     changed: bool,
-    show_completions: bool,
+    should_update_completions: bool,
     autoscroll: Option<Autoscroll>,
     old_cursor_position: Anchor,
     history_entry: SelectionHistoryEntry,
@@ -2657,7 +2657,7 @@ impl Editor {
         &mut self,
         local: bool,
         old_cursor_position: &Anchor,
-        show_completions: bool,
+        should_update_completions: bool,
         window: &mut Window,
         cx: &mut Context<Self>,
     ) {
@@ -2720,14 +2720,7 @@ impl Editor {
 
         if local {
             let new_cursor_position = self.selections.newest_anchor().head();
-            let mut context_menu = self.context_menu.borrow_mut();
-            let completion_menu = match context_menu.as_ref() {
-                Some(CodeContextMenu::Completions(menu)) => Some(menu),
-                _ => {
-                    *context_menu = None;
-                    None
-                }
-            };
+
             if let Some(buffer_id) = new_cursor_position.buffer_id {
                 if !self.registered_buffers.contains_key(&buffer_id) {
                     if let Some(project) = self.project.as_ref() {
@@ -2744,50 +2737,40 @@ impl Editor {
                 }
             }
 
-            if let Some(completion_menu) = completion_menu {
-                let cursor_position = new_cursor_position.to_offset(buffer);
-                let (word_range, kind) =
-                    buffer.surrounding_word(completion_menu.initial_position, true);
-                if kind == Some(CharKind::Word)
-                    && word_range.to_inclusive().contains(&cursor_position)
-                {
-                    let mut completion_menu = completion_menu.clone();
-                    drop(context_menu);
-
-                    let query = Self::completion_query(buffer, cursor_position);
-                    let completion_provider = self.completion_provider.clone();
-                    cx.spawn_in(window, async move |this, cx| {
-                        completion_menu
-                            .filter(query.as_deref(), completion_provider, this.clone(), cx)
-                            .await;
-
-                        this.update(cx, |this, cx| {
-                            let mut context_menu = this.context_menu.borrow_mut();
-                            let Some(CodeContextMenu::Completions(menu)) = context_menu.as_ref()
-                            else {
-                                return;
-                            };
-
-                            if menu.id > completion_menu.id {
-                                return;
-                            }
-
-                            *context_menu = Some(CodeContextMenu::Completions(completion_menu));
-                            drop(context_menu);
-                            cx.notify();
-                        })
-                    })
-                    .detach();
+            let mut context_menu = self.context_menu.borrow_mut();
+            let completion_menu = match context_menu.as_ref() {
+                Some(CodeContextMenu::Completions(menu)) => Some(menu),
+                Some(CodeContextMenu::CodeActions(_)) => {
+                    *context_menu = None;
+                    None
+                }
+                None => None,
+            };
+            let completion_position = completion_menu.map(|menu| menu.initial_position);
+            drop(context_menu);
+
+            if should_update_completions {
+                if let Some(completion_position) = completion_position {
+                    let new_cursor_offset = new_cursor_position.to_offset(buffer);
+                    let position_matches =
+                        new_cursor_offset == completion_position.to_offset(buffer);
+                    let continue_showing = if position_matches {
+                        let (word_range, kind) = buffer.surrounding_word(new_cursor_offset, true);
+                        if let Some(CharKind::Word) = kind {
+                            word_range.start < new_cursor_offset
+                        } else {
+                            false
+                        }
+                    } else {
+                        false
+                    };
 
-                    if show_completions {
+                    if continue_showing {
                         self.show_completions(&ShowCompletions { trigger: None }, window, cx);
+                    } else {
+                        self.hide_context_menu(window, cx);
                     }
-                } else {
-                    drop(context_menu);
-                    self.hide_context_menu(window, cx);
                 }
-            } else {
-                drop(context_menu);
             }
 
             hide_hover(self, cx);
@@ -2981,7 +2964,7 @@ impl Editor {
         self.change_selections_inner(true, autoscroll, window, cx, change)
     }
 
-    pub(crate) fn change_selections_without_showing_completions<R>(
+    pub(crate) fn change_selections_without_updating_completions<R>(
         &mut self,
         autoscroll: Option<Autoscroll>,
         window: &mut Window,
@@ -2993,7 +2976,7 @@ impl Editor {
 
     fn change_selections_inner<R>(
         &mut self,
-        show_completions: bool,
+        should_update_completions: bool,
         autoscroll: Option<Autoscroll>,
         window: &mut Window,
         cx: &mut Context<Self>,
@@ -3001,14 +2984,14 @@ impl Editor {
     ) -> R {
         if let Some(state) = &mut self.deferred_selection_effects_state {
             state.autoscroll = autoscroll.or(state.autoscroll);
-            state.show_completions = show_completions;
+            state.should_update_completions = should_update_completions;
             let (changed, result) = self.selections.change_with(cx, change);
             state.changed |= changed;
             return result;
         }
         let mut state = DeferredSelectionEffectsState {
             changed: false,
-            show_completions,
+            should_update_completions,
             autoscroll,
             old_cursor_position: self.selections.newest_anchor().head(),
             history_entry: SelectionHistoryEntry {
@@ -3068,7 +3051,7 @@ impl Editor {
             self.selections_did_change(
                 true,
                 &old_cursor_position,
-                state.show_completions,
+                state.should_update_completions,
                 window,
                 cx,
             );
@@ -3979,7 +3962,7 @@ impl Editor {
             }
 
             let had_active_inline_completion = this.has_active_inline_completion();
-            this.change_selections_without_showing_completions(
+            this.change_selections_without_updating_completions(
                 Some(Autoscroll::fit()),
                 window,
                 cx,
@@ -5025,7 +5008,7 @@ impl Editor {
         window: &mut Window,
         cx: &mut Context<Self>,
     ) {
-        self.open_completions_menu(true, None, window, cx);
+        self.open_or_update_completions_menu(true, None, window, cx);
     }
 
     pub fn show_completions(
@@ -5034,10 +5017,10 @@ impl Editor {
         window: &mut Window,
         cx: &mut Context<Self>,
     ) {
-        self.open_completions_menu(false, options.trigger.as_deref(), window, cx);
+        self.open_or_update_completions_menu(false, options.trigger.as_deref(), window, cx);
     }
 
-    fn open_completions_menu(
+    fn open_or_update_completions_menu(
         &mut self,
         ignore_completion_provider: bool,
         trigger: Option<&str>,
@@ -5047,9 +5030,6 @@ impl Editor {
         if self.pending_rename.is_some() {
             return;
         }
-        if !self.snippet_stack.is_empty() && self.context_menu.borrow().as_ref().is_some() {
-            return;
-        }
 
         let position = self.selections.newest_anchor().head();
         if position.diff_base_anchor.is_some() {
@@ -5062,11 +5042,52 @@ impl Editor {
                 return;
             };
         let buffer_snapshot = buffer.read(cx).snapshot();
-        let show_completion_documentation = buffer_snapshot
-            .settings_at(buffer_position, cx)
-            .show_completion_documentation;
 
-        let query = Self::completion_query(&self.buffer.read(cx).read(cx), position);
+        let query: Option<Arc<String>> =
+            Self::completion_query(&self.buffer.read(cx).read(cx), position)
+                .map(|query| query.into());
+
+        let provider = if ignore_completion_provider {
+            None
+        } else {
+            self.completion_provider.clone()
+        };
+
+        let sort_completions = provider
+            .as_ref()
+            .map_or(false, |provider| provider.sort_completions());
+
+        let filter_completions = provider
+            .as_ref()
+            .map_or(true, |provider| provider.filter_completions());
+
+        // When `is_incomplete` is false, can filter completions instead of re-querying when the
+        // current query is a suffix of the initial query.
+        if let Some(CodeContextMenu::Completions(menu)) = self.context_menu.borrow_mut().as_mut() {
+            if !menu.is_incomplete && filter_completions {
+                // If the new query is a suffix of the old query (typing more characters) and
+                // the previous result was complete, the existing completions can be filtered.
+                //
+                // Note that this is always true for snippet completions.
+                let query_matches = match (&menu.initial_query, &query) {
+                    (Some(initial_query), Some(query)) => query.starts_with(initial_query.as_ref()),
+                    (None, _) => true,
+                    _ => false,
+                };
+                if query_matches {
+                    let position_matches = if menu.initial_position == position {
+                        true
+                    } else {
+                        let snapshot = self.buffer.read(cx).read(cx);
+                        menu.initial_position.to_offset(&snapshot) == position.to_offset(&snapshot)
+                    };
+                    if position_matches {
+                        menu.filter(query.clone(), provider.clone(), window, cx);
+                        return;
+                    }
+                }
+            }
+        };
 
         let trigger_kind = match trigger {
             Some(trigger) if buffer.read(cx).completion_triggers().contains(trigger) => {
@@ -5085,14 +5106,14 @@ impl Editor {
             trigger_kind,
         };
 
-        let (old_range, word_kind) = buffer_snapshot.surrounding_word(buffer_position);
-        let (old_range, word_to_exclude) = if word_kind == Some(CharKind::Word) {
+        let (replace_range, word_kind) = buffer_snapshot.surrounding_word(buffer_position);
+        let (replace_range, word_to_exclude) = if word_kind == Some(CharKind::Word) {
             let word_to_exclude = buffer_snapshot
-                .text_for_range(old_range.clone())
+                .text_for_range(replace_range.clone())
                 .collect::<String>();
             (
-                buffer_snapshot.anchor_before(old_range.start)
-                    ..buffer_snapshot.anchor_after(old_range.end),
+                buffer_snapshot.anchor_before(replace_range.start)
+                    ..buffer_snapshot.anchor_after(replace_range.end),
                 Some(word_to_exclude),
             )
         } else {
@@ -5106,6 +5127,10 @@ impl Editor {
         let completion_settings =
             language_settings(language.clone(), buffer_snapshot.file(), cx).completions;
 
+        let show_completion_documentation = buffer_snapshot
+            .settings_at(buffer_position, cx)
+            .show_completion_documentation;
+
         // The document can be large, so stay in reasonable bounds when searching for words,
         // otherwise completion pop-up might be slow to appear.
         const WORD_LOOKUP_ROWS: u32 = 5_000;
@@ -5121,18 +5146,13 @@ impl Editor {
         let word_search_range = buffer_snapshot.point_to_offset(min_word_search)
             ..buffer_snapshot.point_to_offset(max_word_search);
 
-        let provider = if ignore_completion_provider {
-            None
-        } else {
-            self.completion_provider.clone()
-        };
         let skip_digits = query
             .as_ref()
             .map_or(true, |query| !query.chars().any(|c| c.is_digit(10)));
 
-        let (mut words, provided_completions) = match &provider {
+        let (mut words, provider_responses) = match &provider {
             Some(provider) => {
-                let completions = provider.completions(
+                let provider_responses = provider.completions(
                     position.excerpt_id,
                     &buffer,
                     buffer_position,
@@ -5153,7 +5173,7 @@ impl Editor {
                         }),
                 };
 
-                (words, completions)
+                (words, provider_responses)
             }
             None => (
                 cx.background_spawn(async move {
@@ -5163,137 +5183,165 @@ impl Editor {
                         skip_digits,
                     })
                 }),
-                Task::ready(Ok(None)),
+                Task::ready(Ok(Vec::new())),
             ),
         };
 
-        let sort_completions = provider
-            .as_ref()
-            .map_or(false, |provider| provider.sort_completions());
-
-        let filter_completions = provider
-            .as_ref()
-            .map_or(true, |provider| provider.filter_completions());
-
         let snippet_sort_order = EditorSettings::get_global(cx).snippet_sort_order;
 
         let id = post_inc(&mut self.next_completion_id);
         let task = cx.spawn_in(window, async move |editor, cx| {
-            async move {
-                editor.update(cx, |this, _| {
-                    this.completion_tasks.retain(|(task_id, _)| *task_id >= id);
-                })?;
+            let Ok(()) = editor.update(cx, |this, _| {
+                this.completion_tasks.retain(|(task_id, _)| *task_id >= id);
+            }) else {
+                return;
+            };
 
-                let mut completions = Vec::new();
-                if let Some(provided_completions) = provided_completions.await.log_err().flatten() {
-                    completions.extend(provided_completions);
+            // TODO: Ideally completions from different sources would be selectively re-queried, so
+            // that having one source with `is_incomplete: true` doesn't cause all to be re-queried.
+            let mut completions = Vec::new();
+            let mut is_incomplete = false;
+            if let Some(provider_responses) = provider_responses.await.log_err() {
+                if !provider_responses.is_empty() {
+                    for response in provider_responses {
+                        completions.extend(response.completions);
+                        is_incomplete = is_incomplete || response.is_incomplete;
+                    }
                     if completion_settings.words == WordsCompletionMode::Fallback {
                         words = Task::ready(BTreeMap::default());
                     }
                 }
+            }
 
-                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 {
-                    replace_range: old_range.clone(),
-                    new_text: word.clone(),
-                    label: CodeLabel::plain(word, None),
-                    icon_path: None,
-                    documentation: None,
-                    source: CompletionSource::BufferWord {
-                        word_range,
-                        resolved: false,
-                    },
-                    insert_text_mode: Some(InsertTextMode::AS_IS),
-                    confirm: None,
-                }));
-
-                let menu = if completions.is_empty() {
-                    None
-                } else {
-                    let mut menu = editor.update(cx, |editor, cx| {
-                        let languages = editor
-                            .workspace
-                            .as_ref()
-                            .and_then(|(workspace, _)| workspace.upgrade())
-                            .map(|workspace| workspace.read(cx).app_state().languages.clone());
-                        CompletionsMenu::new(
-                            id,
-                            sort_completions,
-                            show_completion_documentation,
-                            ignore_completion_provider,
-                            position,
-                            buffer.clone(),
-                            completions.into(),
-                            snippet_sort_order,
-                            languages,
-                            language,
-                            cx,
-                        )
-                    })?;
+            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 {
+                replace_range: replace_range.clone(),
+                new_text: word.clone(),
+                label: CodeLabel::plain(word, None),
+                icon_path: None,
+                documentation: None,
+                source: CompletionSource::BufferWord {
+                    word_range,
+                    resolved: false,
+                },
+                insert_text_mode: Some(InsertTextMode::AS_IS),
+                confirm: None,
+            }));
 
-                    menu.filter(
-                        if filter_completions {
-                            query.as_deref()
-                        } else {
-                            None
-                        },
-                        provider,
-                        editor.clone(),
+            let menu = if completions.is_empty() {
+                None
+            } else {
+                let Ok((mut menu, matches_task)) = editor.update(cx, |editor, cx| {
+                    let languages = editor
+                        .workspace
+                        .as_ref()
+                        .and_then(|(workspace, _)| workspace.upgrade())
+                        .map(|workspace| workspace.read(cx).app_state().languages.clone());
+                    let menu = CompletionsMenu::new(
+                        id,
+                        sort_completions,
+                        show_completion_documentation,
+                        ignore_completion_provider,
+                        position,
+                        query.clone(),
+                        is_incomplete,
+                        buffer.clone(),
+                        completions.into(),
+                        snippet_sort_order,
+                        languages,
+                        language,
                         cx,
-                    )
-                    .await;
+                    );
 
-                    menu.visible().then_some(menu)
+                    let query = if filter_completions { query } else { None };
+                    let matches_task = if let Some(query) = query {
+                        menu.do_async_filtering(query, cx)
+                    } else {
+                        Task::ready(menu.unfiltered_matches())
+                    };
+                    (menu, matches_task)
+                }) else {
+                    return;
                 };
 
-                editor.update_in(cx, |editor, window, cx| {
+                let matches = matches_task.await;
+
+                let Ok(()) = editor.update_in(cx, |editor, window, cx| {
+                    // Newer menu already set, so exit.
                     match editor.context_menu.borrow().as_ref() {
-                        None => {}
                         Some(CodeContextMenu::Completions(prev_menu)) => {
                             if prev_menu.id > id {
                                 return;
                             }
                         }
-                        _ => return,
-                    }
+                        _ => {}
+                    };
 
-                    if editor.focus_handle.is_focused(window) && menu.is_some() {
-                        let mut menu = menu.unwrap();
-                        menu.resolve_visible_completions(editor.completion_provider.as_deref(), cx);
-                        crate::hover_popover::hide_hover(editor, cx);
-                        *editor.context_menu.borrow_mut() =
-                            Some(CodeContextMenu::Completions(menu));
+                    // Only valid to take prev_menu because it the new menu is immediately set
+                    // below, or the menu is hidden.
+                    match editor.context_menu.borrow_mut().take() {
+                        Some(CodeContextMenu::Completions(prev_menu)) => {
+                            let position_matches =
+                                if prev_menu.initial_position == menu.initial_position {
+                                    true
+                                } else {
+                                    let snapshot = editor.buffer.read(cx).read(cx);
+                                    prev_menu.initial_position.to_offset(&snapshot)
+                                        == menu.initial_position.to_offset(&snapshot)
+                                };
+                            if position_matches {
+                                // Preserve markdown cache before `set_filter_results` because it will
+                                // try to populate the documentation cache.
+                                menu.preserve_markdown_cache(prev_menu);
+                            }
+                        }
+                        _ => {}
+                    };
 
-                        if editor.show_edit_predictions_in_menu() {
-                            editor.update_visible_inline_completion(window, cx);
-                        } else {
-                            editor.discard_inline_completion(false, cx);
+                    menu.set_filter_results(matches, provider, window, cx);
+                }) else {
+                    return;
+                };
+
+                menu.visible().then_some(menu)
+            };
+
+            editor
+                .update_in(cx, |editor, window, cx| {
+                    if editor.focus_handle.is_focused(window) {
+                        if let Some(menu) = menu {
+                            *editor.context_menu.borrow_mut() =
+                                Some(CodeContextMenu::Completions(menu));
+
+                            crate::hover_popover::hide_hover(editor, cx);
+                            if editor.show_edit_predictions_in_menu() {
+                                editor.update_visible_inline_completion(window, cx);
+                            } else {
+                                editor.discard_inline_completion(false, cx);
+                            }
+
+                            cx.notify();
+                            return;
                         }
+                    }
 
-                        cx.notify();
-                    } else if editor.completion_tasks.len() <= 1 {
-                        // If there are no more completion tasks and the last menu was
-                        // empty, we should hide it.
+                    if editor.completion_tasks.len() <= 1 {
+                        // If there are no more completion tasks and the last menu was empty, we should hide it.
                         let was_hidden = editor.hide_context_menu(window, cx).is_none();
-                        // If it was already hidden and we don't show inline
-                        // completions in the menu, we should also show the
-                        // inline-completion when available.
+                        // If it was already hidden and we don't show inline completions in the menu, we should
+                        // also show the inline-completion when available.
                         if was_hidden && editor.show_edit_predictions_in_menu() {
                             editor.update_visible_inline_completion(window, cx);
                         }
                     }
-                })?;
-
-                anyhow::Ok(())
-            }
-            .log_err()
-            .await
+                })
+                .ok();
         });
 
         self.completion_tasks.push((id, task));
@@ -5313,17 +5361,16 @@ impl Editor {
     pub fn with_completions_menu_matching_id<R>(
         &self,
         id: CompletionId,
-        on_absent: impl FnOnce() -> R,
-        on_match: impl FnOnce(&mut CompletionsMenu) -> R,
+        f: impl FnOnce(Option<&mut CompletionsMenu>) -> R,
     ) -> R {
         let mut context_menu = self.context_menu.borrow_mut();
         let Some(CodeContextMenu::Completions(completions_menu)) = &mut *context_menu else {
-            return on_absent();
+            return f(None);
         };
         if completions_menu.id != id {
-            return on_absent();
+            return f(None);
         }
-        on_match(completions_menu)
+        f(Some(completions_menu))
     }
 
     pub fn confirm_completion(
@@ -5396,7 +5443,7 @@ impl Editor {
             .clone();
         cx.stop_propagation();
 
-        let buffer_handle = completions_menu.buffer;
+        let buffer_handle = completions_menu.buffer.clone();
 
         let CompletionEdit {
             new_text,
@@ -20206,7 +20253,7 @@ pub trait CompletionProvider {
         trigger: CompletionContext,
         window: &mut Window,
         cx: &mut Context<Editor>,
-    ) -> Task<Result<Option<Vec<Completion>>>>;
+    ) -> Task<Result<Vec<CompletionResponse>>>;
 
     fn resolve_completions(
         &self,
@@ -20315,7 +20362,7 @@ fn snippet_completions(
     buffer: &Entity<Buffer>,
     buffer_position: text::Anchor,
     cx: &mut App,
-) -> Task<Result<Vec<Completion>>> {
+) -> Task<Result<CompletionResponse>> {
     let languages = buffer.read(cx).languages_at(buffer_position);
     let snippet_store = project.snippets().read(cx);
 
@@ -20334,7 +20381,10 @@ fn snippet_completions(
         .collect();
 
     if scopes.is_empty() {
-        return Task::ready(Ok(vec![]));
+        return Task::ready(Ok(CompletionResponse {
+            completions: vec![],
+            is_incomplete: false,
+        }));
     }
 
     let snapshot = buffer.read(cx).text_snapshot();
@@ -20344,7 +20394,8 @@ fn snippet_completions(
     let executor = cx.background_executor().clone();
 
     cx.background_spawn(async move {
-        let mut all_results: Vec<Completion> = Vec::new();
+        let mut is_incomplete = false;
+        let mut completions: Vec<Completion> = Vec::new();
         for (scope, snippets) in scopes.into_iter() {
             let classifier = CharClassifier::new(Some(scope)).for_completion(true);
             let mut last_word = chars
@@ -20354,7 +20405,10 @@ fn snippet_completions(
             last_word = last_word.chars().rev().collect();
 
             if last_word.is_empty() {
-                return Ok(vec![]);
+                return Ok(CompletionResponse {
+                    completions: vec![],
+                    is_incomplete: true,
+                });
             }
 
             let as_offset = text::ToOffset::to_offset(&buffer_position, &snapshot);
@@ -20375,16 +20429,21 @@ fn snippet_completions(
                 })
                 .collect::<Vec<StringMatchCandidate>>();
 
+            const MAX_RESULTS: usize = 100;
             let mut matches = fuzzy::match_strings(
                 &candidates,
                 &last_word,
                 last_word.chars().any(|c| c.is_uppercase()),
-                100,
+                MAX_RESULTS,
                 &Default::default(),
                 executor.clone(),
             )
             .await;
 
+            if matches.len() >= MAX_RESULTS {
+                is_incomplete = true;
+            }
+
             // Remove all candidates where the query's start does not match the start of any word in the candidate
             if let Some(query_start) = last_word.chars().next() {
                 matches.retain(|string_match| {
@@ -20404,76 +20463,72 @@ fn snippet_completions(
                 .map(|m| m.string)
                 .collect::<HashSet<_>>();
 
-            let mut result: Vec<Completion> = snippets
-                .iter()
-                .filter_map(|snippet| {
-                    let matching_prefix = snippet
-                        .prefix
-                        .iter()
-                        .find(|prefix| matched_strings.contains(*prefix))?;
-                    let start = as_offset - last_word.len();
-                    let start = snapshot.anchor_before(start);
-                    let range = start..buffer_position;
-                    let lsp_start = to_lsp(&start);
-                    let lsp_range = lsp::Range {
-                        start: lsp_start,
-                        end: lsp_end,
-                    };
-                    Some(Completion {
-                        replace_range: range,
-                        new_text: snippet.body.clone(),
-                        source: CompletionSource::Lsp {
-                            insert_range: None,
-                            server_id: LanguageServerId(usize::MAX),
-                            resolved: true,
-                            lsp_completion: Box::new(lsp::CompletionItem {
-                                label: snippet.prefix.first().unwrap().clone(),
-                                kind: Some(CompletionItemKind::SNIPPET),
-                                label_details: snippet.description.as_ref().map(|description| {
-                                    lsp::CompletionItemLabelDetails {
-                                        detail: Some(description.clone()),
-                                        description: None,
-                                    }
-                                }),
-                                insert_text_format: Some(InsertTextFormat::SNIPPET),
-                                text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
-                                    lsp::InsertReplaceEdit {
-                                        new_text: snippet.body.clone(),
-                                        insert: lsp_range,
-                                        replace: lsp_range,
-                                    },
-                                )),
-                                filter_text: Some(snippet.body.clone()),
-                                sort_text: Some(char::MAX.to_string()),
-                                ..lsp::CompletionItem::default()
+            completions.extend(snippets.iter().filter_map(|snippet| {
+                let matching_prefix = snippet
+                    .prefix
+                    .iter()
+                    .find(|prefix| matched_strings.contains(*prefix))?;
+                let start = as_offset - last_word.len();
+                let start = snapshot.anchor_before(start);
+                let range = start..buffer_position;
+                let lsp_start = to_lsp(&start);
+                let lsp_range = lsp::Range {
+                    start: lsp_start,
+                    end: lsp_end,
+                };
+                Some(Completion {
+                    replace_range: range,
+                    new_text: snippet.body.clone(),
+                    source: CompletionSource::Lsp {
+                        insert_range: None,
+                        server_id: LanguageServerId(usize::MAX),
+                        resolved: true,
+                        lsp_completion: Box::new(lsp::CompletionItem {
+                            label: snippet.prefix.first().unwrap().clone(),
+                            kind: Some(CompletionItemKind::SNIPPET),
+                            label_details: snippet.description.as_ref().map(|description| {
+                                lsp::CompletionItemLabelDetails {
+                                    detail: Some(description.clone()),
+                                    description: None,
+                                }
                             }),
-                            lsp_defaults: None,
-                        },
-                        label: CodeLabel {
-                            text: matching_prefix.clone(),
-                            runs: Vec::new(),
-                            filter_range: 0..matching_prefix.len(),
-                        },
-                        icon_path: None,
-                        documentation: Some(
-                            CompletionDocumentation::SingleLineAndMultiLinePlainText {
-                                single_line: snippet.name.clone().into(),
-                                plain_text: snippet
-                                    .description
-                                    .clone()
-                                    .map(|description| description.into()),
-                            },
-                        ),
-                        insert_text_mode: None,
-                        confirm: None,
-                    })
+                            insert_text_format: Some(InsertTextFormat::SNIPPET),
+                            text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
+                                lsp::InsertReplaceEdit {
+                                    new_text: snippet.body.clone(),
+                                    insert: lsp_range,
+                                    replace: lsp_range,
+                                },
+                            )),
+                            filter_text: Some(snippet.body.clone()),
+                            sort_text: Some(char::MAX.to_string()),
+                            ..lsp::CompletionItem::default()
+                        }),
+                        lsp_defaults: None,
+                    },
+                    label: CodeLabel {
+                        text: matching_prefix.clone(),
+                        runs: Vec::new(),
+                        filter_range: 0..matching_prefix.len(),
+                    },
+                    icon_path: None,
+                    documentation: Some(CompletionDocumentation::SingleLineAndMultiLinePlainText {
+                        single_line: snippet.name.clone().into(),
+                        plain_text: snippet
+                            .description
+                            .clone()
+                            .map(|description| description.into()),
+                    }),
+                    insert_text_mode: None,
+                    confirm: None,
                 })
-                .collect();
-
-            all_results.append(&mut result);
+            }))
         }
 
-        Ok(all_results)
+        Ok(CompletionResponse {
+            completions,
+            is_incomplete,
+        })
     })
 }
 
@@ -20486,25 +20541,17 @@ impl CompletionProvider for Entity<Project> {
         options: CompletionContext,
         _window: &mut Window,
         cx: &mut Context<Editor>,
-    ) -> Task<Result<Option<Vec<Completion>>>> {
+    ) -> Task<Result<Vec<CompletionResponse>>> {
         self.update(cx, |project, cx| {
             let snippets = snippet_completions(project, buffer, buffer_position, cx);
             let project_completions = project.completions(buffer, buffer_position, options, cx);
             cx.background_spawn(async move {
-                let snippets_completions = snippets.await?;
-                match project_completions.await? {
-                    Some(mut completions) => {
-                        completions.extend(snippets_completions);
-                        Ok(Some(completions))
-                    }
-                    None => {
-                        if snippets_completions.is_empty() {
-                            Ok(None)
-                        } else {
-                            Ok(Some(snippets_completions))
-                        }
-                    }
+                let mut responses = project_completions.await?;
+                let snippets = snippets.await?;
+                if !snippets.completions.is_empty() {
+                    responses.push(snippets);
                 }
+                Ok(responses)
             })
         })
     }

crates/editor/src/editor_tests.rs 🔗

@@ -1,6 +1,7 @@
 use super::*;
 use crate::{
     JoinLines,
+    code_context_menus::CodeContextMenu,
     inline_completion_tests::FakeInlineCompletionProvider,
     linked_editing_ranges::LinkedEditingRanges,
     scroll::scroll_amount::ScrollAmount,
@@ -11184,14 +11185,15 @@ async fn test_completion(cx: &mut TestAppContext) {
     "});
     cx.simulate_keystroke(".");
     handle_completion_request(
-        &mut cx,
         indoc! {"
             one.|<>
             two
             three
         "},
         vec!["first_completion", "second_completion"],
+        true,
         counter.clone(),
+        &mut cx,
     )
     .await;
     cx.condition(|editor, _| editor.context_menu_visible())
@@ -11291,7 +11293,6 @@ async fn test_completion(cx: &mut TestAppContext) {
         additional edit
     "});
     handle_completion_request(
-        &mut cx,
         indoc! {"
             one.second_completion
             two s
@@ -11299,7 +11300,9 @@ async fn test_completion(cx: &mut TestAppContext) {
             additional edit
         "},
         vec!["fourth_completion", "fifth_completion", "sixth_completion"],
+        true,
         counter.clone(),
+        &mut cx,
     )
     .await;
     cx.condition(|editor, _| editor.context_menu_visible())
@@ -11309,7 +11312,6 @@ async fn test_completion(cx: &mut TestAppContext) {
     cx.simulate_keystroke("i");
 
     handle_completion_request(
-        &mut cx,
         indoc! {"
             one.second_completion
             two si
@@ -11317,7 +11319,9 @@ async fn test_completion(cx: &mut TestAppContext) {
             additional edit
         "},
         vec!["fourth_completion", "fifth_completion", "sixth_completion"],
+        true,
         counter.clone(),
+        &mut cx,
     )
     .await;
     cx.condition(|editor, _| editor.context_menu_visible())
@@ -11351,10 +11355,11 @@ async fn test_completion(cx: &mut TestAppContext) {
         editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
     });
     handle_completion_request(
-        &mut cx,
         "editor.<clo|>",
         vec!["close", "clobber"],
+        true,
         counter.clone(),
+        &mut cx,
     )
     .await;
     cx.condition(|editor, _| editor.context_menu_visible())
@@ -11371,6 +11376,128 @@ async fn test_completion(cx: &mut TestAppContext) {
     apply_additional_edits.await.unwrap();
 }
 
+#[gpui::test]
+async fn test_completion_reuse(cx: &mut 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;
+
+    let counter = Arc::new(AtomicUsize::new(0));
+    cx.set_state("objˇ");
+    cx.simulate_keystroke(".");
+
+    // Initial completion request returns complete results
+    let is_incomplete = false;
+    handle_completion_request(
+        "obj.|<>",
+        vec!["a", "ab", "abc"],
+        is_incomplete,
+        counter.clone(),
+        &mut cx,
+    )
+    .await;
+    cx.run_until_parked();
+    assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
+    cx.assert_editor_state("obj.ˇ");
+    check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
+
+    // Type "a" - filters existing completions
+    cx.simulate_keystroke("a");
+    cx.run_until_parked();
+    assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
+    cx.assert_editor_state("obj.aˇ");
+    check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
+
+    // Type "b" - filters existing completions
+    cx.simulate_keystroke("b");
+    cx.run_until_parked();
+    assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
+    cx.assert_editor_state("obj.abˇ");
+    check_displayed_completions(vec!["ab", "abc"], &mut cx);
+
+    // Type "c" - filters existing completions
+    cx.simulate_keystroke("c");
+    cx.run_until_parked();
+    assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
+    cx.assert_editor_state("obj.abcˇ");
+    check_displayed_completions(vec!["abc"], &mut cx);
+
+    // Backspace to delete "c" - filters existing completions
+    cx.update_editor(|editor, window, cx| {
+        editor.backspace(&Backspace, window, cx);
+    });
+    cx.run_until_parked();
+    assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
+    cx.assert_editor_state("obj.abˇ");
+    check_displayed_completions(vec!["ab", "abc"], &mut cx);
+
+    // Moving cursor to the left dismisses menu.
+    cx.update_editor(|editor, window, cx| {
+        editor.move_left(&MoveLeft, window, cx);
+    });
+    cx.run_until_parked();
+    assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
+    cx.assert_editor_state("obj.aˇb");
+    cx.update_editor(|editor, _, _| {
+        assert_eq!(editor.context_menu_visible(), false);
+    });
+
+    // Type "b" - new request
+    cx.simulate_keystroke("b");
+    let is_incomplete = false;
+    handle_completion_request(
+        "obj.<ab|>a",
+        vec!["ab", "abc"],
+        is_incomplete,
+        counter.clone(),
+        &mut cx,
+    )
+    .await;
+    cx.run_until_parked();
+    assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
+    cx.assert_editor_state("obj.abˇb");
+    check_displayed_completions(vec!["ab", "abc"], &mut cx);
+
+    // Backspace to delete "b" - since query was "ab" and is now "a", new request is made.
+    cx.update_editor(|editor, window, cx| {
+        editor.backspace(&Backspace, window, cx);
+    });
+    let is_incomplete = false;
+    handle_completion_request(
+        "obj.<a|>b",
+        vec!["a", "ab", "abc"],
+        is_incomplete,
+        counter.clone(),
+        &mut cx,
+    )
+    .await;
+    cx.run_until_parked();
+    assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
+    cx.assert_editor_state("obj.aˇb");
+    check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
+
+    // Backspace to delete "a" - dismisses menu.
+    cx.update_editor(|editor, window, cx| {
+        editor.backspace(&Backspace, window, cx);
+    });
+    cx.run_until_parked();
+    assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
+    cx.assert_editor_state("obj.ˇb");
+    cx.update_editor(|editor, _, _| {
+        assert_eq!(editor.context_menu_visible(), false);
+    });
+}
+
 #[gpui::test]
 async fn test_word_completion(cx: &mut TestAppContext) {
     let lsp_fetch_timeout_ms = 10;
@@ -12051,9 +12178,11 @@ async fn test_no_duplicated_completion_requests(cx: &mut TestAppContext) {
         let task_completion_item = closure_completion_item.clone();
         counter_clone.fetch_add(1, atomic::Ordering::Release);
         async move {
-            Ok(Some(lsp::CompletionResponse::Array(vec![
-                task_completion_item,
-            ])))
+            Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
+                is_incomplete: true,
+                item_defaults: None,
+                items: vec![task_completion_item],
+            })))
         }
     });
 
@@ -21109,6 +21238,22 @@ pub fn handle_signature_help_request(
     }
 }
 
+#[track_caller]
+pub fn check_displayed_completions(expected: Vec<&'static str>, cx: &mut EditorLspTestContext) {
+    cx.update_editor(|editor, _, _| {
+        if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow().as_ref() {
+            let entries = menu.entries.borrow();
+            let entries = entries
+                .iter()
+                .map(|entry| entry.string.as_str())
+                .collect::<Vec<_>>();
+            assert_eq!(entries, expected);
+        } else {
+            panic!("Expected completions menu");
+        }
+    });
+}
+
 /// Handle completion request passing a marked string specifying where the completion
 /// should be triggered from using '|' character, what range should be replaced, and what completions
 /// should be returned using '<' and '>' to delimit the range.
@@ -21116,10 +21261,11 @@ pub fn handle_signature_help_request(
 /// Also see `handle_completion_request_with_insert_and_replace`.
 #[track_caller]
 pub fn handle_completion_request(
-    cx: &mut EditorLspTestContext,
     marked_string: &str,
     completions: Vec<&'static str>,
+    is_incomplete: bool,
     counter: Arc<AtomicUsize>,
+    cx: &mut EditorLspTestContext,
 ) -> impl Future<Output = ()> {
     let complete_from_marker: TextRangeMarker = '|'.into();
     let replace_range_marker: TextRangeMarker = ('<', '>').into();
@@ -21143,8 +21289,10 @@ pub fn handle_completion_request(
                     params.text_document_position.position,
                     complete_from_position
                 );
-                Ok(Some(lsp::CompletionResponse::Array(
-                    completions
+                Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
+                    is_incomplete: is_incomplete,
+                    item_defaults: None,
+                    items: completions
                         .iter()
                         .map(|completion_text| lsp::CompletionItem {
                             label: completion_text.to_string(),
@@ -21155,7 +21303,7 @@ pub fn handle_completion_request(
                             ..Default::default()
                         })
                         .collect(),
-                )))
+                })))
             }
         });
 

crates/editor/src/hover_popover.rs 🔗

@@ -1095,14 +1095,15 @@ mod tests {
         //prompt autocompletion menu
         cx.simulate_keystroke(".");
         handle_completion_request(
-            &mut cx,
             indoc! {"
                         one.|<>
                         two
                         three
                     "},
             vec!["first_completion", "second_completion"],
+            true,
             counter.clone(),
+            &mut cx,
         )
         .await;
         cx.condition(|editor, _| editor.context_menu_visible()) // wait until completion menu is visible

crates/editor/src/jsx_tag_auto_close.rs 🔗

@@ -600,7 +600,7 @@ pub(crate) fn handle_from(
                     })
                     .collect::<Vec<_>>();
                 this.update_in(cx, |this, window, cx| {
-                    this.change_selections_without_showing_completions(None, window, cx, |s| {
+                    this.change_selections_without_updating_completions(None, window, cx, |s| {
                         s.select(base_selections);
                     });
                 })

crates/extension_host/src/extension_store_test.rs 🔗

@@ -759,8 +759,8 @@ async fn test_extension_store_with_test_extension(cx: &mut TestAppContext) {
         })
         .await
         .unwrap()
-        .unwrap()
         .into_iter()
+        .flat_map(|response| response.completions)
         .map(|c| c.label.text)
         .collect::<Vec<_>>();
     assert_eq!(

crates/inspector_ui/src/div_inspector.rs 🔗

@@ -11,7 +11,7 @@ use language::{
     DiagnosticSeverity, LanguageServerId, Point, ToOffset as _, ToPoint as _,
 };
 use project::lsp_store::CompletionDocumentation;
-use project::{Completion, CompletionSource, Project, ProjectPath};
+use project::{Completion, CompletionResponse, CompletionSource, Project, ProjectPath};
 use std::cell::RefCell;
 use std::fmt::Write as _;
 use std::ops::Range;
@@ -641,18 +641,18 @@ impl CompletionProvider for RustStyleCompletionProvider {
         _: editor::CompletionContext,
         _window: &mut Window,
         cx: &mut Context<Editor>,
-    ) -> Task<Result<Option<Vec<project::Completion>>>> {
+    ) -> Task<Result<Vec<CompletionResponse>>> {
         let Some(replace_range) = completion_replace_range(&buffer.read(cx).snapshot(), &position)
         else {
-            return Task::ready(Ok(Some(Vec::new())));
+            return Task::ready(Ok(Vec::new()));
         };
 
         self.div_inspector.update(cx, |div_inspector, _cx| {
             div_inspector.rust_completion_replace_range = Some(replace_range.clone());
         });
 
-        Task::ready(Ok(Some(
-            STYLE_METHODS
+        Task::ready(Ok(vec![CompletionResponse {
+            completions: STYLE_METHODS
                 .iter()
                 .map(|(_, method)| Completion {
                     replace_range: replace_range.clone(),
@@ -667,7 +667,8 @@ impl CompletionProvider for RustStyleCompletionProvider {
                     confirm: None,
                 })
                 .collect(),
-        )))
+            is_incomplete: false,
+        }]))
     }
 
     fn resolve_completions(

crates/project/src/lsp_command.rs 🔗

@@ -1,10 +1,10 @@
 mod signature_help;
 
 use crate::{
-    CodeAction, CompletionSource, CoreCompletion, DocumentHighlight, DocumentSymbol, Hover,
-    HoverBlock, HoverBlockKind, InlayHint, InlayHintLabel, InlayHintLabelPart,
-    InlayHintLabelPartTooltip, InlayHintTooltip, Location, LocationLink, LspAction, MarkupContent,
-    PrepareRenameResponse, ProjectTransaction, ResolveState,
+    CodeAction, CompletionSource, CoreCompletion, CoreCompletionResponse, DocumentHighlight,
+    DocumentSymbol, Hover, HoverBlock, HoverBlockKind, InlayHint, InlayHintLabel,
+    InlayHintLabelPart, InlayHintLabelPartTooltip, InlayHintTooltip, Location, LocationLink,
+    LspAction, MarkupContent, PrepareRenameResponse, ProjectTransaction, ResolveState,
     lsp_store::{LocalLspStore, LspStore},
 };
 use anyhow::{Context as _, Result};
@@ -2095,7 +2095,7 @@ impl LspCommand for GetHover {
 
 #[async_trait(?Send)]
 impl LspCommand for GetCompletions {
-    type Response = Vec<CoreCompletion>;
+    type Response = CoreCompletionResponse;
     type LspRequest = lsp::request::Completion;
     type ProtoRequest = proto::GetCompletions;
 
@@ -2127,19 +2127,22 @@ impl LspCommand for GetCompletions {
         mut cx: AsyncApp,
     ) -> Result<Self::Response> {
         let mut response_list = None;
-        let mut completions = if let Some(completions) = completions {
+        let (mut completions, mut is_incomplete) = if let Some(completions) = completions {
             match completions {
-                lsp::CompletionResponse::Array(completions) => completions,
+                lsp::CompletionResponse::Array(completions) => (completions, false),
                 lsp::CompletionResponse::List(mut list) => {
+                    let is_incomplete = list.is_incomplete;
                     let items = std::mem::take(&mut list.items);
                     response_list = Some(list);
-                    items
+                    (items, is_incomplete)
                 }
             }
         } else {
-            Vec::new()
+            (Vec::new(), false)
         };
 
+        let unfiltered_completions_count = completions.len();
+
         let language_server_adapter = lsp_store
             .read_with(&mut cx, |lsp_store, _| {
                 lsp_store.language_server_adapter_for_id(server_id)
@@ -2259,11 +2262,17 @@ impl LspCommand for GetCompletions {
             });
         })?;
 
+        // If completions were filtered out due to errors that may be transient, mark the result
+        // incomplete so that it is re-queried.
+        if unfiltered_completions_count != completions.len() {
+            is_incomplete = true;
+        }
+
         language_server_adapter
             .process_completions(&mut completions)
             .await;
 
-        Ok(completions
+        let completions = completions
             .into_iter()
             .zip(completion_edits)
             .map(|(mut lsp_completion, mut edit)| {
@@ -2290,7 +2299,12 @@ impl LspCommand for GetCompletions {
                     },
                 }
             })
-            .collect())
+            .collect();
+
+        Ok(CoreCompletionResponse {
+            completions,
+            is_incomplete,
+        })
     }
 
     fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::GetCompletions {
@@ -2332,18 +2346,20 @@ impl LspCommand for GetCompletions {
     }
 
     fn response_to_proto(
-        completions: Vec<CoreCompletion>,
+        response: CoreCompletionResponse,
         _: &mut LspStore,
         _: PeerId,
         buffer_version: &clock::Global,
         _: &mut App,
     ) -> proto::GetCompletionsResponse {
         proto::GetCompletionsResponse {
-            completions: completions
+            completions: response
+                .completions
                 .iter()
                 .map(LspStore::serialize_completion)
                 .collect(),
             version: serialize_version(buffer_version),
+            can_reuse: !response.is_incomplete,
         }
     }
 
@@ -2360,11 +2376,16 @@ impl LspCommand for GetCompletions {
             })?
             .await?;
 
-        message
+        let completions = message
             .completions
             .into_iter()
             .map(LspStore::deserialize_completion)
-            .collect()
+            .collect::<Result<Vec<_>>>()?;
+
+        Ok(CoreCompletionResponse {
+            completions,
+            is_incomplete: !message.can_reuse,
+        })
     }
 
     fn buffer_id_from_proto(message: &proto::GetCompletions) -> Result<BufferId> {

crates/project/src/lsp_store.rs 🔗

@@ -3,8 +3,8 @@ pub mod lsp_ext_command;
 pub mod rust_analyzer_ext;
 
 use crate::{
-    CodeAction, Completion, CompletionSource, CoreCompletion, Hover, InlayHint, LspAction,
-    ProjectItem, ProjectPath, ProjectTransaction, ResolveState, Symbol, ToolchainStore,
+    CodeAction, Completion, CompletionResponse, CompletionSource, CoreCompletion, Hover, InlayHint,
+    LspAction, ProjectItem, ProjectPath, ProjectTransaction, ResolveState, Symbol, ToolchainStore,
     buffer_store::{BufferStore, BufferStoreEvent},
     environment::ProjectEnvironment,
     lsp_command::{self, *},
@@ -998,7 +998,7 @@ impl LocalLspStore {
             .collect::<Vec<_>>();
 
         async move {
-            futures::future::join_all(shutdown_futures).await;
+            join_all(shutdown_futures).await;
         }
     }
 
@@ -5081,7 +5081,7 @@ impl LspStore {
         position: PointUtf16,
         context: CompletionContext,
         cx: &mut Context<Self>,
-    ) -> Task<Result<Option<Vec<Completion>>>> {
+    ) -> Task<Result<Vec<CompletionResponse>>> {
         let language_registry = self.languages.clone();
 
         if let Some((upstream_client, project_id)) = self.upstream_client() {
@@ -5105,11 +5105,17 @@ impl LspStore {
             });
 
             cx.foreground_executor().spawn(async move {
-                let completions = task.await?;
-                let mut result = Vec::new();
-                populate_labels_for_completions(completions, language, lsp_adapter, &mut result)
-                    .await;
-                Ok(Some(result))
+                let completion_response = task.await?;
+                let completions = populate_labels_for_completions(
+                    completion_response.completions,
+                    language,
+                    lsp_adapter,
+                )
+                .await;
+                Ok(vec![CompletionResponse {
+                    completions,
+                    is_incomplete: completion_response.is_incomplete,
+                }])
             })
         } else if let Some(local) = self.as_local() {
             let snapshot = buffer.read(cx).snapshot();
@@ -5123,7 +5129,7 @@ impl LspStore {
             )
             .completions;
             if !completion_settings.lsp {
-                return Task::ready(Ok(None));
+                return Task::ready(Ok(Vec::new()));
             }
 
             let server_ids: Vec<_> = buffer.update(cx, |buffer, cx| {
@@ -5190,25 +5196,23 @@ impl LspStore {
                     }
                 })?;
 
-                let mut has_completions_returned = false;
-                let mut completions = Vec::new();
-                for (lsp_adapter, task) in tasks {
-                    if let Ok(Some(new_completions)) = task.await {
-                        has_completions_returned = true;
-                        populate_labels_for_completions(
-                            new_completions,
+                let futures = tasks.into_iter().map(async |(lsp_adapter, task)| {
+                    let completion_response = task.await.ok()??;
+                    let completions = populate_labels_for_completions(
+                            completion_response.completions,
                             language.clone(),
                             lsp_adapter,
-                            &mut completions,
                         )
                         .await;
-                    }
-                }
-                if has_completions_returned {
-                    Ok(Some(completions))
-                } else {
-                    Ok(None)
-                }
+                    Some(CompletionResponse {
+                        completions,
+                        is_incomplete: completion_response.is_incomplete,
+                    })
+                });
+
+                let responses: Vec<Option<CompletionResponse>> = join_all(futures).await;
+
+                Ok(responses.into_iter().flatten().collect())
             })
         } else {
             Task::ready(Err(anyhow!("No upstream client or local language server")))
@@ -9547,8 +9551,7 @@ async fn populate_labels_for_completions(
     new_completions: Vec<CoreCompletion>,
     language: Option<Arc<Language>>,
     lsp_adapter: Option<Arc<CachedLspAdapter>>,
-    completions: &mut Vec<Completion>,
-) {
+) -> Vec<Completion> {
     let lsp_completions = new_completions
         .iter()
         .filter_map(|new_completion| {
@@ -9572,6 +9575,7 @@ async fn populate_labels_for_completions(
     .into_iter()
     .fuse();
 
+    let mut completions = Vec::new();
     for completion in new_completions {
         match completion.source.lsp_completion(true) {
             Some(lsp_completion) => {
@@ -9612,6 +9616,7 @@ async fn populate_labels_for_completions(
             }
         }
     }
+    completions
 }
 
 #[derive(Debug)]

crates/project/src/project.rs 🔗

@@ -555,6 +555,23 @@ impl std::fmt::Debug for Completion {
     }
 }
 
+/// Response from a source of completions.
+pub struct CompletionResponse {
+    pub completions: Vec<Completion>,
+    /// When false, indicates that the list is complete and so does not need to be re-queried if it
+    /// can be filtered instead.
+    pub is_incomplete: bool,
+}
+
+/// Response from language server completion request.
+#[derive(Clone, Debug, Default)]
+pub(crate) struct CoreCompletionResponse {
+    pub completions: Vec<CoreCompletion>,
+    /// When false, indicates that the list is complete and so does not need to be re-queried if it
+    /// can be filtered instead.
+    pub is_incomplete: bool,
+}
+
 /// A generic completion that can come from different sources.
 #[derive(Clone, Debug)]
 pub(crate) struct CoreCompletion {
@@ -3430,7 +3447,7 @@ impl Project {
         position: T,
         context: CompletionContext,
         cx: &mut Context<Self>,
-    ) -> Task<Result<Option<Vec<Completion>>>> {
+    ) -> Task<Result<Vec<CompletionResponse>>> {
         let position = position.to_point_utf16(buffer.read(cx));
         self.lsp_store.update(cx, |lsp_store, cx| {
             lsp_store.completions(buffer, position, context, cx)

crates/project/src/project_tests.rs 🔗

@@ -3014,7 +3014,12 @@ async fn test_completions_with_text_edit(cx: &mut gpui::TestAppContext) {
         .next()
         .await;
 
-    let completions = completions.await.unwrap().unwrap();
+    let completions = completions
+        .await
+        .unwrap()
+        .into_iter()
+        .flat_map(|response| response.completions)
+        .collect::<Vec<_>>();
     let snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
 
     assert_eq!(completions.len(), 1);
@@ -3097,7 +3102,12 @@ async fn test_completions_with_edit_ranges(cx: &mut gpui::TestAppContext) {
             .next()
             .await;
 
-        let completions = completions.await.unwrap().unwrap();
+        let completions = completions
+            .await
+            .unwrap()
+            .into_iter()
+            .flat_map(|response| response.completions)
+            .collect::<Vec<_>>();
         let snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
 
         assert_eq!(completions.len(), 1);
@@ -3139,7 +3149,12 @@ async fn test_completions_with_edit_ranges(cx: &mut gpui::TestAppContext) {
             .next()
             .await;
 
-        let completions = completions.await.unwrap().unwrap();
+        let completions = completions
+            .await
+            .unwrap()
+            .into_iter()
+            .flat_map(|response| response.completions)
+            .collect::<Vec<_>>();
         let snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
 
         assert_eq!(completions.len(), 1);
@@ -3210,7 +3225,12 @@ async fn test_completions_without_edit_ranges(cx: &mut gpui::TestAppContext) {
         })
         .next()
         .await;
-    let completions = completions.await.unwrap().unwrap();
+    let completions = completions
+        .await
+        .unwrap()
+        .into_iter()
+        .flat_map(|response| response.completions)
+        .collect::<Vec<_>>();
     let snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
     assert_eq!(completions.len(), 1);
     assert_eq!(completions[0].new_text, "fullyQualifiedName");
@@ -3237,7 +3257,12 @@ async fn test_completions_without_edit_ranges(cx: &mut gpui::TestAppContext) {
         })
         .next()
         .await;
-    let completions = completions.await.unwrap().unwrap();
+    let completions = completions
+        .await
+        .unwrap()
+        .into_iter()
+        .flat_map(|response| response.completions)
+        .collect::<Vec<_>>();
     let snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
     assert_eq!(completions.len(), 1);
     assert_eq!(completions[0].new_text, "component");
@@ -3305,7 +3330,12 @@ async fn test_completions_with_carriage_returns(cx: &mut gpui::TestAppContext) {
         })
         .next()
         .await;
-    let completions = completions.await.unwrap().unwrap();
+    let completions = completions
+        .await
+        .unwrap()
+        .into_iter()
+        .flat_map(|response| response.completions)
+        .collect::<Vec<_>>();
     assert_eq!(completions.len(), 1);
     assert_eq!(completions[0].new_text, "fully\nQualified\nName");
 }

crates/proto/proto/lsp.proto 🔗

@@ -195,6 +195,8 @@ message LspExtGoToParentModuleResponse {
 message GetCompletionsResponse {
     repeated Completion completions = 1;
     repeated VectorClockEntry version = 2;
+    // `!is_complete`, inverted for a default of `is_complete = true`
+    bool can_reuse = 3;
 }
 
 message ApplyCompletionAdditionalEdits {

crates/remote_server/src/remote_editing_tests.rs 🔗

@@ -513,8 +513,8 @@ async fn test_remote_lsp(cx: &mut TestAppContext, server_cx: &mut TestAppContext
 
     assert_eq!(
         result
-            .unwrap()
             .into_iter()
+            .flat_map(|response| response.completions)
             .map(|c| c.label.text)
             .collect::<Vec<_>>(),
         vec!["boop".to_string()]