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