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

@ -14,7 +14,7 @@ use http_client::HttpClientWithUrl;
use itertools::Itertools; use itertools::Itertools;
use language::{Buffer, CodeLabel, HighlightId}; use language::{Buffer, CodeLabel, HighlightId};
use lsp::CompletionContext; use lsp::CompletionContext;
use project::{Completion, CompletionIntent, ProjectPath, Symbol, WorktreeId}; use project::{Completion, CompletionIntent, CompletionResponse, ProjectPath, Symbol, WorktreeId};
use prompt_store::PromptStore; use prompt_store::PromptStore;
use rope::Point; use rope::Point;
use text::{Anchor, OffsetRangeExt, ToPoint}; use text::{Anchor, OffsetRangeExt, ToPoint};
@ -746,7 +746,7 @@ impl CompletionProvider for ContextPickerCompletionProvider {
_trigger: CompletionContext, _trigger: CompletionContext,
_window: &mut Window, _window: &mut Window,
cx: &mut Context<Editor>, cx: &mut Context<Editor>,
) -> Task<Result<Option<Vec<Completion>>>> { ) -> Task<Result<Vec<CompletionResponse>>> {
let state = buffer.update(cx, |buffer, _cx| { let state = buffer.update(cx, |buffer, _cx| {
let position = buffer_position.to_point(buffer); let position = buffer_position.to_point(buffer);
let line_start = Point::new(position.row, 0); let line_start = Point::new(position.row, 0);
@ -756,13 +756,13 @@ impl CompletionProvider for ContextPickerCompletionProvider {
MentionCompletion::try_parse(line, offset_to_line) MentionCompletion::try_parse(line, offset_to_line)
}); });
let Some(state) = state else { let Some(state) = state else {
return Task::ready(Ok(None)); return Task::ready(Ok(Vec::new()));
}; };
let Some((workspace, context_store)) = let Some((workspace, context_store)) =
self.workspace.upgrade().zip(self.context_store.upgrade()) self.workspace.upgrade().zip(self.context_store.upgrade())
else { else {
return Task::ready(Ok(None)); return Task::ready(Ok(Vec::new()));
}; };
let snapshot = buffer.read(cx).snapshot(); let snapshot = buffer.read(cx).snapshot();
@ -815,10 +815,10 @@ impl CompletionProvider for ContextPickerCompletionProvider {
cx.spawn(async move |_, cx| { cx.spawn(async move |_, cx| {
let matches = search_task.await; let matches = search_task.await;
let Some(editor) = editor.upgrade() else { let Some(editor) = editor.upgrade() else {
return Ok(None); return Ok(Vec::new());
}; };
Ok(Some(cx.update(|cx| { let completions = cx.update(|cx| {
matches matches
.into_iter() .into_iter()
.filter_map(|mat| match mat { .filter_map(|mat| match mat {
@ -901,7 +901,14 @@ impl CompletionProvider for ContextPickerCompletionProvider {
), ),
}) })
.collect() .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,
}])
}) })
} }

View file

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

View file

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

View file

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

View file

@ -1,9 +1,8 @@
use fuzzy::{StringMatch, StringMatchCandidate}; use fuzzy::{StringMatch, StringMatchCandidate};
use gpui::{ use gpui::{
AnyElement, Entity, Focusable, FontWeight, ListSizingBehavior, ScrollStrategy, SharedString, 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 itertools::Itertools;
use language::CodeLabel; use language::CodeLabel;
use language::{Buffer, LanguageName, LanguageRegistry}; use language::{Buffer, LanguageName, LanguageRegistry};
@ -18,6 +17,7 @@ use task::TaskContext;
use std::collections::VecDeque; use std::collections::VecDeque;
use std::sync::Arc; use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::{ use std::{
cell::RefCell, cell::RefCell,
cmp::{Reverse, min}, 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 // Constants for the markdown cache. The purpose of this cache is to reduce flickering due to
// documentation not yet being parsed. // documentation not yet being parsed.
// //
// The size of the cache is set to the number of items fetched around the current selection plus one // The size of the cache is set to 16, which is roughly 3 times more than the number of items
// for the current selection and another to avoid cases where and adjacent selection exits the // fetched around the current selection. This way documentation is more often ready for render when
// cache. The only current benefit of a larger cache would be doing less markdown parsing when the // revisiting previous entries, such as when pressing backspace.
// selection revisits items. const MARKDOWN_CACHE_MAX_SIZE: usize = 16;
//
// 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;
const MARKDOWN_CACHE_BEFORE_ITEMS: usize = 2; const MARKDOWN_CACHE_BEFORE_ITEMS: usize = 2;
const MARKDOWN_CACHE_AFTER_ITEMS: usize = 2; const MARKDOWN_CACHE_AFTER_ITEMS: usize = 2;
@ -197,27 +192,48 @@ pub enum ContextMenuOrigin {
QuickActionBar, QuickActionBar,
} }
#[derive(Clone)]
pub struct CompletionsMenu { pub struct CompletionsMenu {
pub id: CompletionId, pub id: CompletionId,
sort_completions: bool, sort_completions: bool,
pub initial_position: Anchor, pub initial_position: Anchor,
pub initial_query: Option<Arc<String>>,
pub is_incomplete: bool,
pub buffer: Entity<Buffer>, pub buffer: Entity<Buffer>,
pub completions: Rc<RefCell<Box<[Completion]>>>, pub completions: Rc<RefCell<Box<[Completion]>>>,
match_candidates: Rc<[StringMatchCandidate]>, match_candidates: Arc<[StringMatchCandidate]>,
pub entries: Rc<RefCell<Vec<StringMatch>>>, pub entries: Rc<RefCell<Box<[StringMatch]>>>,
pub selected_item: usize, pub selected_item: usize,
filter_task: Task<()>,
cancel_filter: Arc<AtomicBool>,
scroll_handle: UniformListScrollHandle, scroll_handle: UniformListScrollHandle,
resolve_completions: bool, resolve_completions: bool,
show_completion_documentation: bool, show_completion_documentation: bool,
pub(super) ignore_completion_provider: bool, pub(super) ignore_completion_provider: bool,
last_rendered_range: Rc<RefCell<Option<Range<usize>>>>, 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_registry: Option<Arc<LanguageRegistry>>,
language: Option<LanguageName>, language: Option<LanguageName>,
snippet_sort_order: SnippetSortOrder, 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 { impl CompletionsMenu {
pub fn new( pub fn new(
id: CompletionId, id: CompletionId,
@ -225,6 +241,8 @@ impl CompletionsMenu {
show_completion_documentation: bool, show_completion_documentation: bool,
ignore_completion_provider: bool, ignore_completion_provider: bool,
initial_position: Anchor, initial_position: Anchor,
initial_query: Option<Arc<String>>,
is_incomplete: bool,
buffer: Entity<Buffer>, buffer: Entity<Buffer>,
completions: Box<[Completion]>, completions: Box<[Completion]>,
snippet_sort_order: SnippetSortOrder, snippet_sort_order: SnippetSortOrder,
@ -242,17 +260,21 @@ impl CompletionsMenu {
id, id,
sort_completions, sort_completions,
initial_position, initial_position,
initial_query,
is_incomplete,
buffer, buffer,
show_completion_documentation, show_completion_documentation,
ignore_completion_provider, ignore_completion_provider,
completions: RefCell::new(completions).into(), completions: RefCell::new(completions).into(),
match_candidates, match_candidates,
entries: RefCell::new(Vec::new()).into(), entries: Rc::new(RefCell::new(Box::new([]))),
selected_item: 0, selected_item: 0,
filter_task: Task::ready(()),
cancel_filter: Arc::new(AtomicBool::new(false)),
scroll_handle: UniformListScrollHandle::new(), scroll_handle: UniformListScrollHandle::new(),
resolve_completions: true, resolve_completions: true,
last_rendered_range: RefCell::new(None).into(), 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_registry,
language, language,
snippet_sort_order, snippet_sort_order,
@ -303,16 +325,20 @@ impl CompletionsMenu {
positions: vec![], positions: vec![],
string: completion.clone(), string: completion.clone(),
}) })
.collect::<Vec<_>>(); .collect();
Self { Self {
id, id,
sort_completions, sort_completions,
initial_position: selection.start, initial_position: selection.start,
initial_query: None,
is_incomplete: false,
buffer, buffer,
completions: RefCell::new(completions).into(), completions: RefCell::new(completions).into(),
match_candidates, match_candidates,
entries: RefCell::new(entries).into(), entries: RefCell::new(entries).into(),
selected_item: 0, selected_item: 0,
filter_task: Task::ready(()),
cancel_filter: Arc::new(AtomicBool::new(false)),
scroll_handle: UniformListScrollHandle::new(), scroll_handle: UniformListScrollHandle::new(),
resolve_completions: false, resolve_completions: false,
show_completion_documentation: false, show_completion_documentation: false,
@ -390,14 +416,7 @@ impl CompletionsMenu {
) { ) {
if self.selected_item != match_index { if self.selected_item != match_index {
self.selected_item = match_index; self.selected_item = match_index;
self.scroll_handle self.handle_selection_changed(provider, window, cx);
.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();
} }
} }
@ -418,18 +437,25 @@ impl CompletionsMenu {
} }
fn handle_selection_changed( fn handle_selection_changed(
&self, &mut self,
provider: &dyn CompletionProvider, provider: Option<&dyn CompletionProvider>,
window: &mut Window, window: &mut Window,
cx: &mut App, cx: &mut Context<Editor>,
) { ) {
let entries = self.entries.borrow(); self.scroll_handle
let entry = if self.selected_item < entries.len() { .scroll_to_item(self.selected_item, ScrollStrategy::Top);
Some(&entries[self.selected_item]) if let Some(provider) = provider {
} else { let entries = self.entries.borrow();
None let entry = if self.selected_item < entries.len() {
}; Some(&entries[self.selected_item])
provider.selection_changed(entry, window, cx); } 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( pub fn resolve_visible_completions(
@ -444,6 +470,19 @@ impl CompletionsMenu {
return; 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 // Attempt to resolve completions for every item that will be displayed. This matters
// because single line documentation may be displayed inline with the completion. // because single line documentation may be displayed inline with the completion.
// //
@ -455,7 +494,6 @@ impl CompletionsMenu {
let visible_count = last_rendered_range let visible_count = last_rendered_range
.clone() .clone()
.map_or(APPROXIMATE_VISIBLE_COUNT, |range| range.count()); .map_or(APPROXIMATE_VISIBLE_COUNT, |range| range.count());
let entries = self.entries.borrow();
let entry_range = if self.selected_item == 0 { let entry_range = if self.selected_item == 0 {
0..min(visible_count, entries.len()) 0..min(visible_count, entries.len())
} else if self.selected_item == entries.len() - 1 { } else if self.selected_item == entries.len() - 1 {
@ -508,11 +546,11 @@ impl CompletionsMenu {
.update(cx, |editor, cx| { .update(cx, |editor, cx| {
// `resolve_completions` modified state affecting display. // `resolve_completions` modified state affecting display.
cx.notify(); cx.notify();
editor.with_completions_menu_matching_id( editor.with_completions_menu_matching_id(completion_id, |menu| {
completion_id, if let Some(menu) = menu {
|| (), menu.start_markdown_parse_for_nearby_entries(cx)
|this| this.start_markdown_parse_for_nearby_entries(cx), }
); });
}) })
.ok(); .ok();
} }
@ -548,11 +586,11 @@ impl CompletionsMenu {
return None; return None;
} }
let candidate_id = entries[index].candidate_id; let candidate_id = entries[index].candidate_id;
match &self.completions.borrow()[candidate_id].documentation { let completions = self.completions.borrow();
Some(CompletionDocumentation::MultiLineMarkdown(source)) if !source.is_empty() => Some( match &completions[candidate_id].documentation {
self.get_or_create_markdown(candidate_id, source.clone(), false, cx) Some(CompletionDocumentation::MultiLineMarkdown(source)) if !source.is_empty() => self
.1, .get_or_create_markdown(candidate_id, Some(source), false, &completions, cx)
), .map(|(_, markdown)| markdown),
Some(_) => None, Some(_) => None,
_ => None, _ => None,
} }
@ -561,38 +599,75 @@ impl CompletionsMenu {
fn get_or_create_markdown( fn get_or_create_markdown(
&self, &self,
candidate_id: usize, candidate_id: usize,
source: SharedString, source: Option<&SharedString>,
is_render: bool, is_render: bool,
completions: &[Completion],
cx: &mut Context<Editor>, cx: &mut Context<Editor>,
) -> (bool, Entity<Markdown>) { ) -> Option<(bool, Entity<Markdown>)> {
let mut markdown_cache = self.markdown_cache.borrow_mut(); let mut markdown_cache = self.markdown_cache.borrow_mut();
if let Some((cache_index, (_, markdown))) = markdown_cache
.iter() let mut has_completion_match_cache_entry = false;
.find_position(|(id, _)| *id == candidate_id) let mut matching_entry = markdown_cache.iter().find_position(|(key, _)| match key {
{ MarkdownCacheKey::ForCandidate { candidate_id: id } => *id == candidate_id,
let markdown = if is_render && cache_index != 0 { 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. // Move the current selection's cache entry to the front.
markdown_cache.rotate_right(1); markdown_cache.rotate_right(1);
let cache_len = markdown_cache.len(); let cache_len = markdown_cache.len();
markdown_cache.swap(0, (cache_index + 1) % 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| { let is_parsing = markdown.update(cx, |markdown, cx| {
// `reset` is called as it's possible for documentation to change due to resolve if let Some(source) = source {
// requests. It does nothing if `source` is unchanged. // `reset` is called as it's possible for documentation to change due to resolve
markdown.reset(source, cx); // requests. It does nothing if `source` is unchanged.
markdown.reset(source.clone(), cx);
}
markdown.is_parsing() 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 { if markdown_cache.len() < MARKDOWN_CACHE_MAX_SIZE {
let markdown = cx.new(|cx| { let markdown = cx.new(|cx| {
Markdown::new( Markdown::new(
source, source.clone(),
self.language_registry.clone(), self.language_registry.clone(),
self.language.clone(), self.language.clone(),
cx, cx,
@ -601,17 +676,20 @@ impl CompletionsMenu {
// Handles redraw when the markdown is done parsing. The current render is for a // 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. // deferred draw, and so without this did not redraw when `markdown` notified.
cx.observe(&markdown, |_, _, cx| cx.notify()).detach(); cx.observe(&markdown, |_, _, cx| cx.notify()).detach();
markdown_cache.push_front((candidate_id, markdown.clone())); markdown_cache.push_front((
(true, markdown) MarkdownCacheKey::ForCandidate { candidate_id },
markdown.clone(),
));
Some((true, markdown))
} else { } else {
debug_assert_eq!(markdown_cache.capacity(), MARKDOWN_CACHE_MAX_SIZE); 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 // Moves the last cache entry to the start. The ring buffer is full, so this does no
// copying and just shifts indexes. // copying and just shifts indexes.
markdown_cache.rotate_right(1); 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; let markdown = &markdown_cache[0].1;
markdown.update(cx, |markdown, cx| markdown.reset(source, cx)); markdown.update(cx, |markdown, cx| markdown.reset(source.clone(), cx));
(true, markdown.clone()) Some((true, markdown.clone()))
} }
} }
@ -774,37 +852,46 @@ impl CompletionsMenu {
} }
let mat = &self.entries.borrow()[self.selected_item]; let mat = &self.entries.borrow()[self.selected_item];
let multiline_docs = match self.completions.borrow_mut()[mat.candidate_id] let completions = self.completions.borrow_mut();
.documentation let multiline_docs = match completions[mat.candidate_id].documentation.as_ref() {
.as_ref()? Some(CompletionDocumentation::MultiLinePlainText(text)) => div().child(text.clone()),
{ Some(CompletionDocumentation::SingleLineAndMultiLinePlainText {
CompletionDocumentation::MultiLinePlainText(text) => div().child(text.clone()),
CompletionDocumentation::SingleLineAndMultiLinePlainText {
plain_text: Some(text), plain_text: Some(text),
.. ..
} => div().child(text.clone()), }) => div().child(text.clone()),
CompletionDocumentation::MultiLineMarkdown(source) if !source.is_empty() => { Some(CompletionDocumentation::MultiLineMarkdown(source)) if !source.is_empty() => {
let (is_parsing, markdown) = let Some((false, markdown)) = self.get_or_create_markdown(
self.get_or_create_markdown(mat.candidate_id, source.clone(), true, cx); mat.candidate_id,
if is_parsing { Some(source),
true,
&completions,
cx,
) else {
return None; return None;
} };
div().child( Self::render_markdown(markdown, window, cx)
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),
)
} }
CompletionDocumentation::MultiLineMarkdown(_) => return None, None => {
CompletionDocumentation::SingleLine(_) => return None, // Handle the case where documentation hasn't yet been resolved but there's a
CompletionDocumentation::Undocumented => return None, // `new_text` match in the cache.
CompletionDocumentation::SingleLineAndMultiLinePlainText { //
plain_text: None, .. // 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)
}
Some(CompletionDocumentation::MultiLineMarkdown(_)) => return None,
Some(CompletionDocumentation::SingleLine(_)) => return None,
Some(CompletionDocumentation::Undocumented) => return None,
Some(CompletionDocumentation::SingleLineAndMultiLinePlainText {
plain_text: None,
..
}) => {
return 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( pub fn sort_matches(
matches: &mut Vec<SortableMatch<'_>>, matches: &mut Vec<SortableMatch<'_>>,
query: Option<&str>, query: Option<&str>,
@ -857,6 +1115,7 @@ impl CompletionsMenu {
let fuzzy_bracket_threshold = max_score * (3.0 / 5.0); let fuzzy_bracket_threshold = max_score * (3.0 / 5.0);
let query_start_lower = query let query_start_lower = query
.as_ref()
.and_then(|q| q.chars().next()) .and_then(|q| q.chars().next())
.and_then(|c| c.to_lowercase().next()); .and_then(|c| c.to_lowercase().next());
@ -890,6 +1149,7 @@ impl CompletionsMenu {
}; };
let sort_mixed_case_prefix_length = Reverse( let sort_mixed_case_prefix_length = Reverse(
query query
.as_ref()
.map(|q| { .map(|q| {
q.chars() q.chars()
.zip(mat.string_match.string.chars()) .zip(mat.string_match.string.chars())
@ -920,97 +1180,32 @@ impl CompletionsMenu {
}); });
} }
pub async fn filter( pub fn preserve_markdown_cache(&mut self, prev_menu: CompletionsMenu) {
&mut self, self.markdown_cache = prev_menu.markdown_cache.clone();
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 { // Convert ForCandidate cache keys to ForCompletionMatch keys.
let completions = self.completions.borrow(); let prev_completions = prev_menu.completions.borrow();
self.markdown_cache
let mut sortable_items: Vec<SortableMatch<'_>> = matches .borrow_mut()
.into_iter() .retain_mut(|(key, _markdown)| match key {
.map(|string_match| { MarkdownCacheKey::ForCompletionMatch { .. } => true,
let completion = &completions[string_match.candidate_id]; MarkdownCacheKey::ForCandidate { candidate_id } => {
if let Some(completion) = prev_completions.get(*candidate_id) {
let is_snippet = matches!( match &completion.documentation {
&completion.source, Some(CompletionDocumentation::MultiLineMarkdown(source)) => {
CompletionSource::Lsp { lsp_completion, .. } *key = MarkdownCacheKey::ForCompletionMatch {
if lsp_completion.kind == Some(CompletionItemKind::SNIPPET) new_text: completion.new_text.clone(),
); markdown_source: source.clone(),
};
let sort_text = true
if let CompletionSource::Lsp { lsp_completion, .. } = &completion.source { }
lsp_completion.sort_text.as_deref() _ => false,
} else { }
None } else {
}; false
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, 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();
}
} }
} }

View file

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

View file

@ -1,6 +1,7 @@
use super::*; use super::*;
use crate::{ use crate::{
JoinLines, JoinLines,
code_context_menus::CodeContextMenu,
inline_completion_tests::FakeInlineCompletionProvider, inline_completion_tests::FakeInlineCompletionProvider,
linked_editing_ranges::LinkedEditingRanges, linked_editing_ranges::LinkedEditingRanges,
scroll::scroll_amount::ScrollAmount, scroll::scroll_amount::ScrollAmount,
@ -11184,14 +11185,15 @@ async fn test_completion(cx: &mut TestAppContext) {
"}); "});
cx.simulate_keystroke("."); cx.simulate_keystroke(".");
handle_completion_request( handle_completion_request(
&mut cx,
indoc! {" indoc! {"
one.|<> one.|<>
two two
three three
"}, "},
vec!["first_completion", "second_completion"], vec!["first_completion", "second_completion"],
true,
counter.clone(), counter.clone(),
&mut cx,
) )
.await; .await;
cx.condition(|editor, _| editor.context_menu_visible()) cx.condition(|editor, _| editor.context_menu_visible())
@ -11291,7 +11293,6 @@ async fn test_completion(cx: &mut TestAppContext) {
additional edit additional edit
"}); "});
handle_completion_request( handle_completion_request(
&mut cx,
indoc! {" indoc! {"
one.second_completion one.second_completion
two s two s
@ -11299,7 +11300,9 @@ async fn test_completion(cx: &mut TestAppContext) {
additional edit additional edit
"}, "},
vec!["fourth_completion", "fifth_completion", "sixth_completion"], vec!["fourth_completion", "fifth_completion", "sixth_completion"],
true,
counter.clone(), counter.clone(),
&mut cx,
) )
.await; .await;
cx.condition(|editor, _| editor.context_menu_visible()) cx.condition(|editor, _| editor.context_menu_visible())
@ -11309,7 +11312,6 @@ async fn test_completion(cx: &mut TestAppContext) {
cx.simulate_keystroke("i"); cx.simulate_keystroke("i");
handle_completion_request( handle_completion_request(
&mut cx,
indoc! {" indoc! {"
one.second_completion one.second_completion
two si two si
@ -11317,7 +11319,9 @@ async fn test_completion(cx: &mut TestAppContext) {
additional edit additional edit
"}, "},
vec!["fourth_completion", "fifth_completion", "sixth_completion"], vec!["fourth_completion", "fifth_completion", "sixth_completion"],
true,
counter.clone(), counter.clone(),
&mut cx,
) )
.await; .await;
cx.condition(|editor, _| editor.context_menu_visible()) 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); editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
}); });
handle_completion_request( handle_completion_request(
&mut cx,
"editor.<clo|>", "editor.<clo|>",
vec!["close", "clobber"], vec!["close", "clobber"],
true,
counter.clone(), counter.clone(),
&mut cx,
) )
.await; .await;
cx.condition(|editor, _| editor.context_menu_visible()) cx.condition(|editor, _| editor.context_menu_visible())
@ -11371,6 +11376,128 @@ async fn test_completion(cx: &mut TestAppContext) {
apply_additional_edits.await.unwrap(); 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] #[gpui::test]
async fn test_word_completion(cx: &mut TestAppContext) { async fn test_word_completion(cx: &mut TestAppContext) {
let lsp_fetch_timeout_ms = 10; 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(); let task_completion_item = closure_completion_item.clone();
counter_clone.fetch_add(1, atomic::Ordering::Release); counter_clone.fetch_add(1, atomic::Ordering::Release);
async move { async move {
Ok(Some(lsp::CompletionResponse::Array(vec![ Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
task_completion_item, 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 /// 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 triggered from using '|' character, what range should be replaced, and what completions
/// should be returned using '<' and '>' to delimit the range. /// 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`. /// Also see `handle_completion_request_with_insert_and_replace`.
#[track_caller] #[track_caller]
pub fn handle_completion_request( pub fn handle_completion_request(
cx: &mut EditorLspTestContext,
marked_string: &str, marked_string: &str,
completions: Vec<&'static str>, completions: Vec<&'static str>,
is_incomplete: bool,
counter: Arc<AtomicUsize>, counter: Arc<AtomicUsize>,
cx: &mut EditorLspTestContext,
) -> impl Future<Output = ()> { ) -> impl Future<Output = ()> {
let complete_from_marker: TextRangeMarker = '|'.into(); let complete_from_marker: TextRangeMarker = '|'.into();
let replace_range_marker: TextRangeMarker = ('<', '>').into(); let replace_range_marker: TextRangeMarker = ('<', '>').into();
@ -21143,8 +21289,10 @@ pub fn handle_completion_request(
params.text_document_position.position, params.text_document_position.position,
complete_from_position complete_from_position
); );
Ok(Some(lsp::CompletionResponse::Array( Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
completions is_incomplete: is_incomplete,
item_defaults: None,
items: completions
.iter() .iter()
.map(|completion_text| lsp::CompletionItem { .map(|completion_text| lsp::CompletionItem {
label: completion_text.to_string(), label: completion_text.to_string(),
@ -21155,7 +21303,7 @@ pub fn handle_completion_request(
..Default::default() ..Default::default()
}) })
.collect(), .collect(),
))) })))
} }
}); });

View file

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

View file

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

View file

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

View file

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

View file

@ -1,10 +1,10 @@
mod signature_help; mod signature_help;
use crate::{ use crate::{
CodeAction, CompletionSource, CoreCompletion, DocumentHighlight, DocumentSymbol, Hover, CodeAction, CompletionSource, CoreCompletion, CoreCompletionResponse, DocumentHighlight,
HoverBlock, HoverBlockKind, InlayHint, InlayHintLabel, InlayHintLabelPart, DocumentSymbol, Hover, HoverBlock, HoverBlockKind, InlayHint, InlayHintLabel,
InlayHintLabelPartTooltip, InlayHintTooltip, Location, LocationLink, LspAction, MarkupContent, InlayHintLabelPart, InlayHintLabelPartTooltip, InlayHintTooltip, Location, LocationLink,
PrepareRenameResponse, ProjectTransaction, ResolveState, LspAction, MarkupContent, PrepareRenameResponse, ProjectTransaction, ResolveState,
lsp_store::{LocalLspStore, LspStore}, lsp_store::{LocalLspStore, LspStore},
}; };
use anyhow::{Context as _, Result}; use anyhow::{Context as _, Result};
@ -2095,7 +2095,7 @@ impl LspCommand for GetHover {
#[async_trait(?Send)] #[async_trait(?Send)]
impl LspCommand for GetCompletions { impl LspCommand for GetCompletions {
type Response = Vec<CoreCompletion>; type Response = CoreCompletionResponse;
type LspRequest = lsp::request::Completion; type LspRequest = lsp::request::Completion;
type ProtoRequest = proto::GetCompletions; type ProtoRequest = proto::GetCompletions;
@ -2127,19 +2127,22 @@ impl LspCommand for GetCompletions {
mut cx: AsyncApp, mut cx: AsyncApp,
) -> Result<Self::Response> { ) -> Result<Self::Response> {
let mut response_list = None; 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 { match completions {
lsp::CompletionResponse::Array(completions) => completions, lsp::CompletionResponse::Array(completions) => (completions, false),
lsp::CompletionResponse::List(mut list) => { lsp::CompletionResponse::List(mut list) => {
let is_incomplete = list.is_incomplete;
let items = std::mem::take(&mut list.items); let items = std::mem::take(&mut list.items);
response_list = Some(list); response_list = Some(list);
items (items, is_incomplete)
} }
} }
} else { } else {
Vec::new() (Vec::new(), false)
}; };
let unfiltered_completions_count = completions.len();
let language_server_adapter = lsp_store let language_server_adapter = lsp_store
.read_with(&mut cx, |lsp_store, _| { .read_with(&mut cx, |lsp_store, _| {
lsp_store.language_server_adapter_for_id(server_id) 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 language_server_adapter
.process_completions(&mut completions) .process_completions(&mut completions)
.await; .await;
Ok(completions let completions = completions
.into_iter() .into_iter()
.zip(completion_edits) .zip(completion_edits)
.map(|(mut lsp_completion, mut edit)| { .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 { fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::GetCompletions {
@ -2332,18 +2346,20 @@ impl LspCommand for GetCompletions {
} }
fn response_to_proto( fn response_to_proto(
completions: Vec<CoreCompletion>, response: CoreCompletionResponse,
_: &mut LspStore, _: &mut LspStore,
_: PeerId, _: PeerId,
buffer_version: &clock::Global, buffer_version: &clock::Global,
_: &mut App, _: &mut App,
) -> proto::GetCompletionsResponse { ) -> proto::GetCompletionsResponse {
proto::GetCompletionsResponse { proto::GetCompletionsResponse {
completions: completions completions: response
.completions
.iter() .iter()
.map(LspStore::serialize_completion) .map(LspStore::serialize_completion)
.collect(), .collect(),
version: serialize_version(buffer_version), version: serialize_version(buffer_version),
can_reuse: !response.is_incomplete,
} }
} }
@ -2360,11 +2376,16 @@ impl LspCommand for GetCompletions {
})? })?
.await?; .await?;
message let completions = message
.completions .completions
.into_iter() .into_iter()
.map(LspStore::deserialize_completion) .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> { 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; pub mod rust_analyzer_ext;
use crate::{ use crate::{
CodeAction, Completion, CompletionSource, CoreCompletion, Hover, InlayHint, LspAction, CodeAction, Completion, CompletionResponse, CompletionSource, CoreCompletion, Hover, InlayHint,
ProjectItem, ProjectPath, ProjectTransaction, ResolveState, Symbol, ToolchainStore, LspAction, ProjectItem, ProjectPath, ProjectTransaction, ResolveState, Symbol, ToolchainStore,
buffer_store::{BufferStore, BufferStoreEvent}, buffer_store::{BufferStore, BufferStoreEvent},
environment::ProjectEnvironment, environment::ProjectEnvironment,
lsp_command::{self, *}, lsp_command::{self, *},
@ -998,7 +998,7 @@ impl LocalLspStore {
.collect::<Vec<_>>(); .collect::<Vec<_>>();
async move { async move {
futures::future::join_all(shutdown_futures).await; join_all(shutdown_futures).await;
} }
} }
@ -5081,7 +5081,7 @@ impl LspStore {
position: PointUtf16, position: PointUtf16,
context: CompletionContext, context: CompletionContext,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) -> Task<Result<Option<Vec<Completion>>>> { ) -> Task<Result<Vec<CompletionResponse>>> {
let language_registry = self.languages.clone(); let language_registry = self.languages.clone();
if let Some((upstream_client, project_id)) = self.upstream_client() { if let Some((upstream_client, project_id)) = self.upstream_client() {
@ -5105,11 +5105,17 @@ impl LspStore {
}); });
cx.foreground_executor().spawn(async move { cx.foreground_executor().spawn(async move {
let completions = task.await?; let completion_response = task.await?;
let mut result = Vec::new(); let completions = populate_labels_for_completions(
populate_labels_for_completions(completions, language, lsp_adapter, &mut result) completion_response.completions,
.await; language,
Ok(Some(result)) lsp_adapter,
)
.await;
Ok(vec![CompletionResponse {
completions,
is_incomplete: completion_response.is_incomplete,
}])
}) })
} else if let Some(local) = self.as_local() { } else if let Some(local) = self.as_local() {
let snapshot = buffer.read(cx).snapshot(); let snapshot = buffer.read(cx).snapshot();
@ -5123,7 +5129,7 @@ impl LspStore {
) )
.completions; .completions;
if !completion_settings.lsp { if !completion_settings.lsp {
return Task::ready(Ok(None)); return Task::ready(Ok(Vec::new()));
} }
let server_ids: Vec<_> = buffer.update(cx, |buffer, cx| { let server_ids: Vec<_> = buffer.update(cx, |buffer, cx| {
@ -5190,25 +5196,23 @@ impl LspStore {
} }
})?; })?;
let mut has_completions_returned = false; let futures = tasks.into_iter().map(async |(lsp_adapter, task)| {
let mut completions = Vec::new(); let completion_response = task.await.ok()??;
for (lsp_adapter, task) in tasks { let completions = populate_labels_for_completions(
if let Ok(Some(new_completions)) = task.await { completion_response.completions,
has_completions_returned = true;
populate_labels_for_completions(
new_completions,
language.clone(), language.clone(),
lsp_adapter, lsp_adapter,
&mut completions,
) )
.await; .await;
} Some(CompletionResponse {
} completions,
if has_completions_returned { is_incomplete: completion_response.is_incomplete,
Ok(Some(completions)) })
} else { });
Ok(None)
} let responses: Vec<Option<CompletionResponse>> = join_all(futures).await;
Ok(responses.into_iter().flatten().collect())
}) })
} else { } else {
Task::ready(Err(anyhow!("No upstream client or local language server"))) 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>, new_completions: Vec<CoreCompletion>,
language: Option<Arc<Language>>, language: Option<Arc<Language>>,
lsp_adapter: Option<Arc<CachedLspAdapter>>, lsp_adapter: Option<Arc<CachedLspAdapter>>,
completions: &mut Vec<Completion>, ) -> Vec<Completion> {
) {
let lsp_completions = new_completions let lsp_completions = new_completions
.iter() .iter()
.filter_map(|new_completion| { .filter_map(|new_completion| {
@ -9572,6 +9575,7 @@ async fn populate_labels_for_completions(
.into_iter() .into_iter()
.fuse(); .fuse();
let mut completions = Vec::new();
for completion in new_completions { for completion in new_completions {
match completion.source.lsp_completion(true) { match completion.source.lsp_completion(true) {
Some(lsp_completion) => { Some(lsp_completion) => {
@ -9612,6 +9616,7 @@ async fn populate_labels_for_completions(
} }
} }
} }
completions
} }
#[derive(Debug)] #[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. /// A generic completion that can come from different sources.
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub(crate) struct CoreCompletion { pub(crate) struct CoreCompletion {
@ -3430,7 +3447,7 @@ impl Project {
position: T, position: T,
context: CompletionContext, context: CompletionContext,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) -> Task<Result<Option<Vec<Completion>>>> { ) -> Task<Result<Vec<CompletionResponse>>> {
let position = position.to_point_utf16(buffer.read(cx)); let position = position.to_point_utf16(buffer.read(cx));
self.lsp_store.update(cx, |lsp_store, cx| { self.lsp_store.update(cx, |lsp_store, cx| {
lsp_store.completions(buffer, position, context, 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() .next()
.await; .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()); let snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
assert_eq!(completions.len(), 1); assert_eq!(completions.len(), 1);
@ -3097,7 +3102,12 @@ async fn test_completions_with_edit_ranges(cx: &mut gpui::TestAppContext) {
.next() .next()
.await; .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()); let snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
assert_eq!(completions.len(), 1); assert_eq!(completions.len(), 1);
@ -3139,7 +3149,12 @@ async fn test_completions_with_edit_ranges(cx: &mut gpui::TestAppContext) {
.next() .next()
.await; .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()); let snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
assert_eq!(completions.len(), 1); assert_eq!(completions.len(), 1);
@ -3210,7 +3225,12 @@ async fn test_completions_without_edit_ranges(cx: &mut gpui::TestAppContext) {
}) })
.next() .next()
.await; .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()); let snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
assert_eq!(completions.len(), 1); assert_eq!(completions.len(), 1);
assert_eq!(completions[0].new_text, "fullyQualifiedName"); assert_eq!(completions[0].new_text, "fullyQualifiedName");
@ -3237,7 +3257,12 @@ async fn test_completions_without_edit_ranges(cx: &mut gpui::TestAppContext) {
}) })
.next() .next()
.await; .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()); let snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
assert_eq!(completions.len(), 1); assert_eq!(completions.len(), 1);
assert_eq!(completions[0].new_text, "component"); assert_eq!(completions[0].new_text, "component");
@ -3305,7 +3330,12 @@ async fn test_completions_with_carriage_returns(cx: &mut gpui::TestAppContext) {
}) })
.next() .next()
.await; .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.len(), 1);
assert_eq!(completions[0].new_text, "fully\nQualified\nName"); assert_eq!(completions[0].new_text, "fully\nQualified\nName");
} }

View file

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

View file

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