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

@ -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)?;
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,
})
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 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 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;
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(),
))
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,
}])
})
}
}