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

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.
This commit is contained in:
Michael Sloan 2025-06-02 16:19:09 -06:00 committed by GitHub
parent b7ec437b13
commit 17cf865d1e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 1221 additions and 720 deletions

View file

@ -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> {

View file

@ -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)]

View file

@ -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)

View file

@ -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");
}