Resolve documentation for visible completions (#21705)

Release Notes:

- Improved LSP resolution of documentation for completions. It now
queries documentation for visible completions and avoids doing too many
redundant queries.

---

In #21286, documentation resolution was made more efficient by only
resolving the current completion. However, this meant that single line
documentation shown inline in the menu was missing until scrolled
to. This also meant that it would wait for navigation to resolve
completion docs, leading to lag for displaying documentation.

This change resolves this by attempting to fetch all the completions
that will be shown. It also mostly avoids re-resolving completions. It
intentionally re-resolves the current selection on navigation, as some
language servers will respond with more information later on.
This commit is contained in:
Michael Sloan 2024-12-10 12:25:30 -07:00 committed by GitHub
parent ab1e9bf270
commit ab595b0d55
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 286 additions and 184 deletions

View file

@ -1,4 +1,9 @@
use std::{cell::Cell, cmp::Reverse, ops::Range, sync::Arc};
use std::{
cell::Cell,
cmp::{min, Reverse},
ops::Range,
sync::Arc,
};
use fuzzy::{StringMatch, StringMatchCandidate};
use gpui::{
@ -11,8 +16,9 @@ use language::{CodeLabel, Documentation};
use lsp::LanguageServerId;
use multi_buffer::{Anchor, ExcerptId};
use ordered_float::OrderedFloat;
use parking_lot::RwLock;
use parking_lot::{Mutex, RwLock};
use project::{CodeAction, Completion, TaskSourceKind};
use std::iter;
use task::ResolvedTask;
use ui::{
h_flex, ActiveTheme as _, Color, FluentBuilder as _, InteractiveElement as _, IntoElement,
@ -145,6 +151,7 @@ pub struct CompletionsMenu {
resolve_completions: bool,
pub aside_was_displayed: Cell<bool>,
show_completion_documentation: bool,
last_rendered_range: Arc<Mutex<Option<Range<usize>>>>,
}
impl CompletionsMenu {
@ -173,7 +180,6 @@ impl CompletionsMenu {
sort_completions,
initial_position,
buffer,
show_completion_documentation,
completions: Arc::new(RwLock::new(completions)),
match_candidates,
matches: Vec::new().into(),
@ -181,6 +187,8 @@ impl CompletionsMenu {
scroll_handle: UniformListScrollHandle::new(),
resolve_completions: true,
aside_was_displayed: Cell::new(aside_was_displayed),
show_completion_documentation,
last_rendered_range: Arc::new(Mutex::new(None)),
}
}
@ -236,6 +244,7 @@ impl CompletionsMenu {
resolve_completions: false,
aside_was_displayed: Cell::new(false),
show_completion_documentation: false,
last_rendered_range: Arc::new(Mutex::new(None)),
}
}
@ -244,11 +253,7 @@ impl CompletionsMenu {
provider: Option<&dyn CompletionProvider>,
cx: &mut ViewContext<Editor>,
) {
self.selected_item = 0;
self.scroll_handle
.scroll_to_item(self.selected_item, ScrollStrategy::Top);
self.resolve_selected_completion(provider, cx);
cx.notify();
self.update_selection_index(0, provider, cx);
}
fn select_prev(
@ -256,15 +261,7 @@ impl CompletionsMenu {
provider: Option<&dyn CompletionProvider>,
cx: &mut ViewContext<Editor>,
) {
if self.selected_item > 0 {
self.selected_item -= 1;
} else {
self.selected_item = self.matches.len() - 1;
}
self.scroll_handle
.scroll_to_item(self.selected_item, ScrollStrategy::Top);
self.resolve_selected_completion(provider, cx);
cx.notify();
self.update_selection_index(self.prev_match_index(), provider, cx);
}
fn select_next(
@ -272,15 +269,7 @@ impl CompletionsMenu {
provider: Option<&dyn CompletionProvider>,
cx: &mut ViewContext<Editor>,
) {
if self.selected_item + 1 < self.matches.len() {
self.selected_item += 1;
} else {
self.selected_item = 0;
}
self.scroll_handle
.scroll_to_item(self.selected_item, ScrollStrategy::Top);
self.resolve_selected_completion(provider, cx);
cx.notify();
self.update_selection_index(self.next_match_index(), provider, cx);
}
fn select_last(
@ -288,14 +277,41 @@ impl CompletionsMenu {
provider: Option<&dyn CompletionProvider>,
cx: &mut ViewContext<Editor>,
) {
self.selected_item = self.matches.len() - 1;
self.scroll_handle
.scroll_to_item(self.selected_item, ScrollStrategy::Top);
self.resolve_selected_completion(provider, cx);
cx.notify();
self.update_selection_index(self.matches.len() - 1, provider, cx);
}
pub fn resolve_selected_completion(
fn update_selection_index(
&mut self,
match_index: usize,
provider: Option<&dyn CompletionProvider>,
cx: &mut ViewContext<Editor>,
) {
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);
cx.notify();
}
}
fn prev_match_index(&self) -> usize {
if self.selected_item > 0 {
self.selected_item - 1
} else {
self.matches.len() - 1
}
}
fn next_match_index(&self) -> usize {
if self.selected_item + 1 < self.matches.len() {
self.selected_item + 1
} else {
0
}
}
pub fn resolve_visible_completions(
&mut self,
provider: Option<&dyn CompletionProvider>,
cx: &mut ViewContext<Editor>,
@ -307,10 +323,59 @@ impl CompletionsMenu {
return;
};
let completion_index = self.matches[self.selected_item].candidate_id;
// Attempt to resolve completions for every item that will be displayed. This matters
// because single line documentation may be displayed inline with the completion.
//
// When navigating to the very beginning or end of completions, `last_rendered_range` may
// have no overlap with the completions that will be displayed, so instead use a range based
// on the last rendered count.
const APPROXIMATE_VISIBLE_COUNT: usize = 12;
let last_rendered_range = self.last_rendered_range.lock().clone();
let visible_count = last_rendered_range
.clone()
.map_or(APPROXIMATE_VISIBLE_COUNT, |range| range.count());
let matches_range = if self.selected_item == 0 {
0..min(visible_count, self.matches.len())
} else if self.selected_item == self.matches.len() - 1 {
self.matches.len().saturating_sub(visible_count)..self.matches.len()
} else {
last_rendered_range.unwrap_or_else(|| self.selected_item..self.selected_item + 1)
};
// Expand the range to resolve more completions than are predicted to be visible, to reduce
// jank on navigation.
const EXTRA_TO_RESOLVE: usize = 4;
let matches_indices = util::iterate_expanded_and_wrapped_usize_range(
matches_range.clone(),
EXTRA_TO_RESOLVE,
EXTRA_TO_RESOLVE,
self.matches.len(),
);
// Avoid work by sometimes filtering out completions that already have documentation.
// This filtering doesn't happen if the completions are currently being updated.
let candidate_ids = matches_indices.map(|i| self.matches[i].candidate_id);
let candidate_ids = match self.completions.try_read() {
None => candidate_ids.collect::<Vec<usize>>(),
Some(completions) => candidate_ids
.filter(|i| completions[*i].documentation.is_none())
.collect::<Vec<usize>>(),
};
// Current selection is always resolved even if it already has documentation, to handle
// out-of-spec language servers that return more results later.
let selected_candidate_id = self.matches[self.selected_item].candidate_id;
let candidate_ids = iter::once(selected_candidate_id)
.chain(
candidate_ids
.into_iter()
.filter(|id| *id != selected_candidate_id),
)
.collect::<Vec<usize>>();
let resolve_task = provider.resolve_completions(
self.buffer.clone(),
vec![completion_index],
candidate_ids,
self.completions.clone(),
cx,
);
@ -406,11 +471,14 @@ impl CompletionsMenu {
.occlude()
});
let last_rendered_range = self.last_rendered_range.clone();
let list = uniform_list(
cx.view().clone(),
"completions",
matches.len(),
move |_editor, range, cx| {
last_rendered_range.lock().replace(range.clone());
let start_ix = range.start;
let completions_guard = completions.read();