Compare commits

...

6 Commits

Author SHA1 Message Date
mgsloan@gmail.com
06f7eb84be Merge branch 'completion-docs-prefetch' into fix-completion-fuzzy-match 2024-12-13 20:26:11 -07:00
mgsloan@gmail.com
653484ee56 Progress! 2024-12-13 19:28:12 -07:00
mgsloan@gmail.com
ce367ef305 WIP
Co-authored-by: Conrad <conrad@zed.dev>
2024-12-13 18:43:36 -07:00
mgsloan@gmail.com
b76237b230 Remove use of mutation for completion resolution in lsp_store
Co-authored-by: Conrad <conrad@zed.dev>
2024-12-13 18:43:36 -07:00
mgsloan@gmail.com
8bdf90787a Change editor context_menu from RwLock to RefCell 2024-12-13 18:41:57 -07:00
mgsloan@gmail.com
bc9dfc6385 Resolve documentation for visible completions
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.
2024-12-10 11:52:32 -07:00
10 changed files with 502 additions and 485 deletions

1
Cargo.lock generated
View File

@@ -13974,6 +13974,7 @@ dependencies = [
"futures-lite 1.13.0",
"git2",
"globset",
"itertools 0.13.0",
"log",
"rand 0.8.5",
"regex",

View File

@@ -322,14 +322,13 @@ impl CompletionProvider for SlashCommandCompletionProvider {
}
}
fn resolve_completions(
fn resolve_completion(
&self,
_: Model<Buffer>,
_: Vec<usize>,
_: Arc<RwLock<Box<[project::Completion]>>>,
_: Completion,
_: &mut ViewContext<Editor>,
) -> Task<Result<bool>> {
Task::ready(Ok(true))
) -> Task<Result<Option<Completion>>> {
Task::ready(Ok(None))
}
fn apply_additional_edits_for_completion(

View File

@@ -1,4 +1,10 @@
use std::{cell::Cell, cmp::Reverse, ops::Range, sync::Arc};
use std::{
cell::{Cell, RefCell}
cmp::{min, Reverse},
ops::Range,
sync::Arc,
rc::Rc,
};
use fuzzy::{StringMatch, StringMatchCandidate};
use gpui::{
@@ -11,8 +17,10 @@ use language::{CodeLabel, Documentation};
use lsp::LanguageServerId;
use multi_buffer::{Anchor, ExcerptId};
use ordered_float::OrderedFloat;
use parking_lot::RwLock;
// FIXME
use parking_lot::Mutex;
use project::{CodeAction, Completion, TaskSourceKind};
use std::iter;
use task::ResolvedTask;
use ui::{
h_flex, ActiveTheme as _, Color, FluentBuilder as _, InteractiveElement as _, IntoElement,
@@ -137,7 +145,7 @@ pub struct CompletionsMenu {
sort_completions: bool,
pub initial_position: Anchor,
pub buffer: Model<Buffer>,
pub completions: Arc<RwLock<Box<[Completion]>>>,
pub completions: Rc<RefCell<Vec<Completion>>>,
match_candidates: Arc<[StringMatchCandidate]>,
pub matches: Arc<[StringMatch]>,
pub selected_item: usize,
@@ -145,6 +153,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 {
@@ -154,7 +163,7 @@ impl CompletionsMenu {
show_completion_documentation: bool,
initial_position: Anchor,
buffer: Model<Buffer>,
completions: Box<[Completion]>,
completions: Vec<Completion>,
aside_was_displayed: bool,
) -> Self {
let match_candidates = completions
@@ -173,14 +182,15 @@ impl CompletionsMenu {
sort_completions,
initial_position,
buffer,
show_completion_documentation,
completions: Arc::new(RwLock::new(completions)),
completions: Rc::new(RefCell::new(completions)),
match_candidates,
matches: Vec::new().into(),
selected_item: 0,
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)),
}
}
@@ -191,7 +201,7 @@ impl CompletionsMenu {
selection: Range<Anchor>,
buffer: Model<Buffer>,
) -> Self {
let completions = choices
let completions: Vec<_> = choices
.iter()
.map(|choice| Completion {
old_range: selection.start.text_anchor..selection.end.text_anchor,
@@ -228,7 +238,7 @@ impl CompletionsMenu {
sort_completions,
initial_position: selection.start,
buffer,
completions: Arc::new(RwLock::new(completions)),
completions: Rc::new(RefCell::new(completions)),
match_candidates,
matches,
selected_item: 0,
@@ -236,6 +246,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 +255,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 +263,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 +271,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 +279,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,17 +325,113 @@ 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.map_or(0..0, |range| {
min(range.start, self.matches.len())..min(range.end, self.matches.len())
})
};
// 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 filtering out completions that already have documentation.
let candidate_ids = matches_indices
.map(|i| self.matches[i].candidate_id)
// FIXME: need borrow
// .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,
);
cx.spawn(move |editor, mut cx| async move {
if let Some(true) = resolve_task.await.log_err() {
editor.update(&mut cx, |_, cx| cx.notify()).ok();
if let Some(new_completion) = resolve_task.await.log_err().flatten() {
editor
.update(&mut cx, |editor, cx| {
let mut menu = editor.context_menu.borrow_mut();
match menu.as_mut() {
Some(CodeContextMenu::Completions(menu)) if menu.id == menu_id => {
let completions = menu.completions.borrow_mut();
assert!(completion_index < completions.len());
/*
let new_completions = completions
.iter()
.enumerate()
.map(|(i, c)| {
if i == completion_index {
new_completion.clone()
} else {
c.clone()
}
})
.collect();
let new_match_candidates = menu.match_candidates
.iter()
.enumerate()
.map(|(i, c)| {
if i == completion_index {
new_completion.clone()
} else {
}
})
.collect();
let new_matches = menu
menu.completions = new_completions.into();
menu.match_candidates = new_match_candidates.into();
for mat in menu.matches.iter() {
if mat.completion_id == completion_index {
mat
mat.completion = new_completion.clone();
}
}
self.completions[completion_index] = new_completion;
self.match_candidates[completion_index] = todo!();
*/
// we need to update self.matches
}
_ => {}
}
cx.notify()
})
.ok();
}
})
.detach();
@@ -340,12 +454,10 @@ impl CompletionsMenu {
.iter()
.enumerate()
.max_by_key(|(_, mat)| {
let completions = self.completions.read();
let completion = &completions[mat.candidate_id];
let documentation = &completion.documentation;
let completion = &self.completions.borrow()[mat.candidate_id];
let mut len = completion.label.text.chars().count();
if let Some(Documentation::SingleLine(text)) = documentation {
if let Some(Documentation::SingleLine(text)) = &completion.documentation {
if show_completion_documentation {
len += text.chars().count();
}
@@ -355,14 +467,13 @@ impl CompletionsMenu {
})
.map(|(ix, _)| ix);
let completions = self.completions.clone();
let matches = self.matches.clone();
let selected_item = self.selected_item;
let style = style.clone();
let multiline_docs = if show_completion_documentation {
let mat = &self.matches[selected_item];
match &self.completions.read()[mat.candidate_id].documentation {
match &self.completions.borrow()[mat.candidate_id].documentation {
Some(Documentation::MultiLinePlainText(text)) => {
Some(div().child(SharedString::from(text.clone())))
}
@@ -406,13 +517,15 @@ impl CompletionsMenu {
.occlude()
});
let completions_ref = self.completions.clone();
let last_rendered_range_ref = 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();
matches[range]
.iter()
@@ -420,7 +533,7 @@ impl CompletionsMenu {
.map(|(ix, mat)| {
let item_ix = start_ix + ix;
let candidate_id = mat.candidate_id;
let completion = &completions_guard[candidate_id];
let completion = &completions_ref.borrow()[candidate_id];
let documentation = if show_completion_documentation {
&completion.documentation
@@ -547,7 +660,7 @@ impl CompletionsMenu {
}
}
let completions = self.completions.read();
let completions = self.completions.borrow();
if self.sort_completions {
matches.sort_unstable_by_key(|mat| {
// We do want to strike a balance here between what the language server tells us
@@ -607,7 +720,6 @@ impl CompletionsMenu {
*position += completion.label.filter_range.start;
}
}
drop(completions);
self.matches = matches.into();
self.selected_item = 0;

View File

@@ -127,7 +127,6 @@ pub use multi_buffer::{
use multi_buffer::{
ExpandExcerptDirection, MultiBufferDiffHunk, MultiBufferPoint, MultiBufferRow, ToOffsetUtf16,
};
use parking_lot::RwLock;
use project::{
lsp_store::{FormatTarget, FormatTrigger, OpenLspBufferHandle},
project_settings::{GitGutterSetting, ProjectSettings},
@@ -606,7 +605,7 @@ pub struct Editor {
scrollbar_marker_state: ScrollbarMarkerState,
active_indent_guides_state: ActiveIndentGuidesState,
nav_history: Option<ItemNavHistory>,
context_menu: RwLock<Option<CodeContextMenu>>,
context_menu: RefCell<Option<CodeContextMenu>>,
mouse_context_menu: Option<MouseContextMenu>,
hunk_controls_menu_handle: PopoverMenuHandle<ui::ContextMenu>,
completion_tasks: Vec<(CompletionId, Task<Option<()>>)>,
@@ -1237,7 +1236,7 @@ impl Editor {
scrollbar_marker_state: ScrollbarMarkerState::default(),
active_indent_guides_state: ActiveIndentGuidesState::default(),
nav_history: None,
context_menu: RwLock::new(None),
context_menu: RefCell::new(None),
mouse_context_menu: None,
hunk_controls_menu_handle: PopoverMenuHandle::default(),
completion_tasks: Default::default(),
@@ -1382,7 +1381,7 @@ impl Editor {
key_context.add("renaming");
}
if self.context_menu_visible() {
match self.context_menu.read().as_ref() {
match self.context_menu.borrow().as_ref() {
Some(CodeContextMenu::Completions(_)) => {
key_context.add("menu");
key_context.add("showing_completions")
@@ -1884,10 +1883,9 @@ impl Editor {
if local {
let new_cursor_position = self.selections.newest_anchor().head();
let mut context_menu = self.context_menu.write();
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
@@ -1911,7 +1909,7 @@ impl Editor {
.await;
this.update(&mut cx, |this, cx| {
let mut context_menu = this.context_menu.write();
let mut context_menu = this.context_menu.borrow_mut();
let Some(CodeContextMenu::Completions(menu)) = context_menu.as_ref()
else {
return;
@@ -3649,7 +3647,7 @@ impl Editor {
return;
};
if !self.snippet_stack.is_empty() && self.context_menu.read().as_ref().is_some() {
if !self.snippet_stack.is_empty() && self.context_menu.borrow().as_ref().is_some() {
return;
}
@@ -3668,7 +3666,7 @@ impl Editor {
let query = Self::completion_query(&self.buffer.read(cx).read(cx), position);
let aside_was_displayed = match self.context_menu.read().deref() {
let aside_was_displayed = match self.context_menu.borrow().deref() {
Some(CodeContextMenu::Completions(menu)) => menu.aside_was_displayed.get(),
_ => false,
};
@@ -3705,7 +3703,7 @@ impl Editor {
show_completion_documentation,
position,
buffer.clone(),
completions.into(),
completions,
aside_was_displayed,
);
menu.filter(query.as_deref(), cx.background_executor().clone())
@@ -3721,22 +3719,20 @@ impl Editor {
};
editor.update(&mut cx, |editor, cx| {
let mut context_menu = editor.context_menu.write();
let mut context_menu = editor.context_menu.borrow_mut();
match context_menu.as_ref() {
None => {}
Some(CodeContextMenu::Completions(prev_menu)) => {
if prev_menu.id > id {
return;
}
}
_ => return,
}
if editor.focus_handle.is_focused(cx) && menu.is_some() {
let mut menu = menu.unwrap();
menu.resolve_selected_completion(editor.completion_provider.as_deref(), cx);
menu.resolve_visible_completions(editor.completion_provider.as_deref(), cx);
*context_menu = Some(CodeContextMenu::Completions(menu));
drop(context_menu);
cx.notify();
@@ -3793,8 +3789,7 @@ impl Editor {
.matches
.get(item_ix.unwrap_or(completions_menu.selected_item))?;
let buffer_handle = completions_menu.buffer;
let completions = completions_menu.completions.read();
let completion = completions.get(mat.candidate_id)?;
let completion = &completions_menu.completions.borrow()[mat.candidate_id];
cx.stop_propagation();
let snippet;
@@ -3938,12 +3933,8 @@ impl Editor {
}
let provider = self.completion_provider.as_ref()?;
let apply_edits = provider.apply_additional_edits_for_completion(
buffer_handle,
completion.clone(),
true,
cx,
);
let apply_edits =
provider.apply_additional_edits_for_completion(buffer_handle, completion, true, cx);
let editor_settings = EditorSettings::get_global(cx);
if editor_settings.show_signature_help_after_edits || editor_settings.auto_signature_help {
@@ -3959,7 +3950,7 @@ impl Editor {
}
pub fn toggle_code_actions(&mut self, action: &ToggleCodeActions, cx: &mut ViewContext<Self>) {
let mut context_menu = self.context_menu.write();
let mut context_menu = self.context_menu.borrow_mut();
if let Some(CodeContextMenu::CodeActions(code_actions)) = context_menu.as_ref() {
if code_actions.deployed_from_indicator == action.deployed_from_indicator {
// Toggle if we're selecting the same one
@@ -4054,7 +4045,7 @@ impl Editor {
.as_ref()
.map_or(true, |actions| actions.is_empty());
if let Ok(task) = editor.update(&mut cx, |editor, cx| {
*editor.context_menu.write() =
*editor.context_menu.borrow_mut() =
Some(CodeContextMenu::CodeActions(CodeActionsMenu {
buffer,
actions: CodeActionContents {
@@ -5002,7 +4993,7 @@ impl Editor {
pub fn context_menu_visible(&self) -> bool {
self.context_menu
.read()
.borrow()
.as_ref()
.map_or(false, |menu| menu.visible())
}
@@ -5014,7 +5005,7 @@ impl Editor {
max_height: Pixels,
cx: &mut ViewContext<Editor>,
) -> Option<(ContextMenuOrigin, AnyElement)> {
self.context_menu.read().as_ref().map(|menu| {
self.context_menu.borrow().as_ref().map(|menu| {
menu.render(
cursor_position,
style,
@@ -5028,7 +5019,7 @@ impl Editor {
fn hide_context_menu(&mut self, cx: &mut ViewContext<Self>) -> Option<CodeContextMenu> {
cx.notify();
self.completion_tasks.clear();
self.context_menu.write().take()
self.context_menu.borrow_mut().take()
}
fn show_snippet_choices(
@@ -5045,7 +5036,7 @@ impl Editor {
let id = post_inc(&mut self.next_completion_id);
if let Some(buffer) = buffer {
*self.context_menu.write() = Some(CodeContextMenu::Completions(
*self.context_menu.borrow_mut() = Some(CodeContextMenu::Completions(
CompletionsMenu::new_snippet_choices(id, true, choices, selection, buffer),
));
}
@@ -7109,7 +7100,7 @@ impl Editor {
if self
.context_menu
.write()
.borrow_mut()
.as_mut()
.map(|menu| menu.select_first(self.completion_provider.as_deref(), cx))
.unwrap_or(false)
@@ -7218,7 +7209,7 @@ impl Editor {
if self
.context_menu
.write()
.borrow_mut()
.as_mut()
.map(|menu| menu.select_last(self.completion_provider.as_deref(), cx))
.unwrap_or(false)
@@ -7271,25 +7262,25 @@ impl Editor {
}
pub fn context_menu_first(&mut self, _: &ContextMenuFirst, cx: &mut ViewContext<Self>) {
if let Some(context_menu) = self.context_menu.write().as_mut() {
if let Some(context_menu) = self.context_menu.borrow_mut().as_mut() {
context_menu.select_first(self.completion_provider.as_deref(), cx);
}
}
pub fn context_menu_prev(&mut self, _: &ContextMenuPrev, cx: &mut ViewContext<Self>) {
if let Some(context_menu) = self.context_menu.write().as_mut() {
if let Some(context_menu) = self.context_menu.borrow_mut().as_mut() {
context_menu.select_prev(self.completion_provider.as_deref(), cx);
}
}
pub fn context_menu_next(&mut self, _: &ContextMenuNext, cx: &mut ViewContext<Self>) {
if let Some(context_menu) = self.context_menu.write().as_mut() {
if let Some(context_menu) = self.context_menu.borrow_mut().as_mut() {
context_menu.select_next(self.completion_provider.as_deref(), cx);
}
}
pub fn context_menu_last(&mut self, _: &ContextMenuLast, cx: &mut ViewContext<Self>) {
if let Some(context_menu) = self.context_menu.write().as_mut() {
if let Some(context_menu) = self.context_menu.borrow_mut().as_mut() {
context_menu.select_last(self.completion_provider.as_deref(), cx);
}
}
@@ -12689,7 +12680,7 @@ impl Editor {
}
pub fn has_active_completions_menu(&self) -> bool {
self.context_menu.read().as_ref().map_or(false, |menu| {
self.context_menu.borrow().as_ref().map_or(false, |menu| {
menu.visible() && matches!(menu, CodeContextMenu::Completions(_))
})
}
@@ -13181,18 +13172,17 @@ pub trait CompletionProvider {
cx: &mut ViewContext<Editor>,
) -> Task<Result<Vec<Completion>>>;
fn resolve_completions(
fn resolve_completion(
&self,
buffer: Model<Buffer>,
completion_indices: Vec<usize>,
completions: Arc<RwLock<Box<[Completion]>>>,
completion: &Completion,
cx: &mut ViewContext<Editor>,
) -> Task<Result<bool>>;
) -> Task<Result<Option<Completion>>>;
fn apply_additional_edits_for_completion(
&self,
buffer: Model<Buffer>,
completion: Completion,
completion: &Completion,
push_to_history: bool,
cx: &mut ViewContext<Editor>,
) -> Task<Result<Option<language::Transaction>>>;
@@ -13411,22 +13401,21 @@ impl CompletionProvider for Model<Project> {
})
}
fn resolve_completions(
fn resolve_completion(
&self,
buffer: Model<Buffer>,
completion_indices: Vec<usize>,
completions: Arc<RwLock<Box<[Completion]>>>,
completion: &Completion,
cx: &mut ViewContext<Editor>,
) -> Task<Result<bool>> {
) -> Task<Result<Option<Completion>>> {
self.update(cx, |project, cx| {
project.resolve_completions(buffer, completion_indices, completions, cx)
project.resolve_completion(buffer, completion, cx)
})
}
fn apply_additional_edits_for_completion(
&self,
buffer: Model<Buffer>,
completion: Completion,
completion: &Completion,
push_to_history: bool,
cx: &mut ViewContext<Editor>,
) -> Task<Result<Option<language::Transaction>>> {

View File

@@ -25,6 +25,7 @@ use language::{
use language_settings::{Formatter, FormatterList, IndentGuideSettings};
use multi_buffer::MultiBufferIndentGuide;
use parking_lot::Mutex;
use pretty_assertions::{assert_eq, assert_ne};
use project::{buffer_store::BufferChangeSet, FakeFs};
use project::{
lsp_command::SIGNATURE_HELP_HIGHLIGHT_CURRENT,
@@ -10742,6 +10743,62 @@ async fn test_completions_resolve_updates_labels(cx: &mut gpui::TestAppContext)
async fn test_completions_default_resolve_data_handling(cx: &mut gpui::TestAppContext) {
init_test(cx, |_| {});
let item_0 = lsp::CompletionItem {
label: "abs".into(),
insert_text: Some("abs".into()),
data: Some(json!({ "very": "special"})),
insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
lsp::InsertReplaceEdit {
new_text: "abs".to_string(),
insert: lsp::Range::default(),
replace: lsp::Range::default(),
},
)),
..lsp::CompletionItem::default()
};
let items = iter::once(item_0.clone())
.chain((11..51).map(|i| lsp::CompletionItem {
label: format!("item_{}", i),
insert_text: Some(format!("item_{}", i)),
insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
..lsp::CompletionItem::default()
}))
.collect::<Vec<_>>();
let default_commit_characters = vec!["?".to_string()];
let default_data = json!({ "default": "data"});
let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
let default_edit_range = lsp::Range {
start: lsp::Position {
line: 0,
character: 5,
},
end: lsp::Position {
line: 0,
character: 5,
},
};
let item_0_out = lsp::CompletionItem {
commit_characters: Some(default_commit_characters.clone()),
insert_text_format: Some(default_insert_text_format),
..item_0
};
let items_out = iter::once(item_0_out)
.chain(items[1..].iter().map(|item| lsp::CompletionItem {
commit_characters: Some(default_commit_characters.clone()),
data: Some(default_data.clone()),
insert_text_mode: Some(default_insert_text_mode),
text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
range: default_edit_range,
new_text: item.label.clone(),
})),
..item.clone()
}))
.collect::<Vec<lsp::CompletionItem>>();
let mut cx = EditorLspTestContext::new_rust(
lsp::ServerCapabilities {
completion_provider: Some(lsp::CompletionOptions {
@@ -10758,138 +10815,15 @@ async fn test_completions_default_resolve_data_handling(cx: &mut gpui::TestAppCo
cx.set_state(indoc! {"fn main() { let a = 2ˇ; }"});
cx.simulate_keystroke(".");
let default_commit_characters = vec!["?".to_string()];
let default_data = json!({ "very": "special"});
let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
let default_edit_range = lsp::Range {
start: lsp::Position {
line: 0,
character: 5,
},
end: lsp::Position {
line: 0,
character: 5,
},
};
let resolve_requests_number = Arc::new(AtomicUsize::new(0));
let expect_first_item = Arc::new(AtomicBool::new(true));
cx.lsp
.server
.on_request::<lsp::request::ResolveCompletionItem, _, _>({
let closure_default_data = default_data.clone();
let closure_resolve_requests_number = resolve_requests_number.clone();
let closure_expect_first_item = expect_first_item.clone();
let closure_default_commit_characters = default_commit_characters.clone();
move |item_to_resolve, _| {
closure_resolve_requests_number.fetch_add(1, atomic::Ordering::Release);
let default_data = closure_default_data.clone();
let default_commit_characters = closure_default_commit_characters.clone();
let expect_first_item = closure_expect_first_item.clone();
async move {
if expect_first_item.load(atomic::Ordering::Acquire) {
assert_eq!(
item_to_resolve.label, "Some(2)",
"Should have selected the first item"
);
assert_eq!(
item_to_resolve.data,
Some(json!({ "very": "special"})),
"First item should bring its own data for resolving"
);
assert_eq!(
item_to_resolve.commit_characters,
Some(default_commit_characters),
"First item had no own commit characters and should inherit the default ones"
);
assert!(
matches!(
item_to_resolve.text_edit,
Some(lsp::CompletionTextEdit::InsertAndReplace { .. })
),
"First item should bring its own edit range for resolving"
);
assert_eq!(
item_to_resolve.insert_text_format,
Some(default_insert_text_format),
"First item had no own insert text format and should inherit the default one"
);
assert_eq!(
item_to_resolve.insert_text_mode,
Some(lsp::InsertTextMode::ADJUST_INDENTATION),
"First item should bring its own insert text mode for resolving"
);
Ok(item_to_resolve)
} else {
assert_eq!(
item_to_resolve.label, "vec![2]",
"Should have selected the last item"
);
assert_eq!(
item_to_resolve.data,
Some(default_data),
"Last item has no own resolve data and should inherit the default one"
);
assert_eq!(
item_to_resolve.commit_characters,
Some(default_commit_characters),
"Last item had no own commit characters and should inherit the default ones"
);
assert_eq!(
item_to_resolve.text_edit,
Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
range: default_edit_range,
new_text: "vec![2]".to_string()
})),
"Last item had no own edit range and should inherit the default one"
);
assert_eq!(
item_to_resolve.insert_text_format,
Some(lsp::InsertTextFormat::PLAIN_TEXT),
"Last item should bring its own insert text format for resolving"
);
assert_eq!(
item_to_resolve.insert_text_mode,
Some(default_insert_text_mode),
"Last item had no own insert text mode and should inherit the default one"
);
Ok(item_to_resolve)
}
}
}
}).detach();
let completion_data = default_data.clone();
let completion_characters = default_commit_characters.clone();
cx.handle_request::<lsp::request::Completion, _, _>(move |_, _, _| {
let default_data = completion_data.clone();
let default_commit_characters = completion_characters.clone();
let items = items.clone();
async move {
Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
items: vec![
lsp::CompletionItem {
label: "Some(2)".into(),
insert_text: Some("Some(2)".into()),
data: Some(json!({ "very": "special"})),
insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
lsp::InsertReplaceEdit {
new_text: "Some(2)".to_string(),
insert: lsp::Range::default(),
replace: lsp::Range::default(),
},
)),
..lsp::CompletionItem::default()
},
lsp::CompletionItem {
label: "vec![2]".into(),
insert_text: Some("vec![2]".into()),
insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
..lsp::CompletionItem::default()
},
],
items,
item_defaults: Some(lsp::CompletionListItemDefaults {
data: Some(default_data.clone()),
commit_characters: Some(default_commit_characters.clone()),
@@ -10906,6 +10840,21 @@ async fn test_completions_default_resolve_data_handling(cx: &mut gpui::TestAppCo
.next()
.await;
let resolved_items = Arc::new(Mutex::new(Vec::new()));
cx.lsp
.server
.on_request::<lsp::request::ResolveCompletionItem, _, _>({
let closure_resolved_items = resolved_items.clone();
move |item_to_resolve, _| {
let closure_resolved_items = closure_resolved_items.clone();
async move {
closure_resolved_items.lock().push(item_to_resolve.clone());
Ok(item_to_resolve)
}
}
})
.detach();
cx.condition(|editor, _| editor.context_menu_visible())
.await;
cx.run_until_parked();
@@ -10917,40 +10866,50 @@ async fn test_completions_default_resolve_data_handling(cx: &mut gpui::TestAppCo
completions_menu
.matches
.iter()
.map(|c| c.string.as_str())
.collect::<Vec<_>>(),
vec!["Some(2)", "vec![2]"]
.map(|c| c.string.clone())
.collect::<Vec<String>>(),
items_out
.iter()
.map(|completion| completion.label.clone())
.collect::<Vec<String>>()
);
}
CodeContextMenu::CodeActions(_) => panic!("Expected to have the completions menu"),
}
});
// Approximate initial displayed interval is 0..12. With extra item padding of 4 this is 0..16
// with 4 from the end.
assert_eq!(
resolve_requests_number.load(atomic::Ordering::Acquire),
1,
"While there are 2 items in the completion list, only 1 resolve request should have been sent, for the selected item"
*resolved_items.lock(),
[
&items_out[0..16],
&items_out[items_out.len() - 4..items_out.len()]
]
.concat()
.iter()
.cloned()
.collect::<Vec<lsp::CompletionItem>>()
);
resolved_items.lock().clear();
cx.update_editor(|editor, cx| {
editor.context_menu_first(&ContextMenuFirst, cx);
editor.context_menu_prev(&ContextMenuPrev, cx);
});
cx.run_until_parked();
// Completions that have already been resolved are skipped.
assert_eq!(
resolve_requests_number.load(atomic::Ordering::Acquire),
2,
"After re-selecting the first item, another resolve request should have been sent"
);
expect_first_item.store(false, atomic::Ordering::Release);
cx.update_editor(|editor, cx| {
editor.context_menu_last(&ContextMenuLast, cx);
});
cx.run_until_parked();
assert_eq!(
resolve_requests_number.load(atomic::Ordering::Acquire),
3,
"After selecting the other item, another resolve request should have been sent"
*resolved_items.lock(),
[
// Selected item is always resolved even if it was resolved before.
&items_out[items_out.len() - 1..items_out.len()],
&items_out[items_out.len() - 16..items_out.len() - 4]
]
.concat()
.iter()
.cloned()
.collect::<Vec<lsp::CompletionItem>>()
);
resolved_items.lock().clear();
}
#[gpui::test]

View File

@@ -1686,7 +1686,7 @@ impl EditorElement {
deployed_from_indicator,
actions,
..
})) = editor.context_menu.read().as_ref()
})) = editor.context_menu.borrow().as_ref()
{
actions
.tasks
@@ -1768,7 +1768,7 @@ impl EditorElement {
if let Some(crate::CodeContextMenu::CodeActions(CodeActionsMenu {
deployed_from_indicator,
..
})) = editor.context_menu.read().as_ref()
})) = editor.context_menu.borrow().as_ref()
{
active = deployed_from_indicator.map_or(true, |indicator_row| indicator_row == row);
};

View File

@@ -34,25 +34,24 @@ use language::{
language_settings::{
language_settings, FormatOnSave, Formatter, LanguageSettings, SelectedFormatter,
},
markdown, point_to_lsp, prepare_completion_documentation,
point_to_lsp, prepare_completion_documentation,
proto::{deserialize_anchor, deserialize_version, serialize_anchor, serialize_version},
range_from_lsp, Bias, Buffer, BufferSnapshot, CachedLspAdapter, CodeLabel, Diagnostic,
DiagnosticEntry, DiagnosticSet, Diff, Documentation, File as _, Language, LanguageName,
LanguageRegistry, LanguageServerBinaryStatus, LanguageToolchainStore, LocalFile, LspAdapter,
LspAdapterDelegate, Patch, PointUtf16, TextBufferSnapshot, ToOffset, ToPointUtf16, Transaction,
Unclipped,
range_from_lsp, Bias, Buffer, CachedLspAdapter, CodeLabel, Diagnostic, DiagnosticEntry,
DiagnosticSet, Diff, Documentation, File as _, Language, LanguageName, LanguageRegistry,
LanguageServerBinaryStatus, LanguageToolchainStore, LocalFile, LspAdapter, LspAdapterDelegate,
Patch, PointUtf16, TextBufferSnapshot, ToOffset, ToPointUtf16, Transaction, Unclipped,
};
use lsp::{
notification::DidRenameFiles, CodeActionKind, CompletionContext, DiagnosticSeverity,
DiagnosticTag, DidChangeWatchedFilesRegistrationOptions, Edit, FileOperationFilter,
FileOperationPatternKind, FileOperationRegistrationOptions, FileRename, FileSystemWatcher,
InsertTextFormat, LanguageServer, LanguageServerBinary, LanguageServerBinaryOptions,
LanguageServerId, LanguageServerName, LspRequestFuture, MessageActionItem, MessageType, OneOf,
RenameFilesParams, ServerHealthStatus, ServerStatus, SymbolKind, TextEdit, Url,
WillRenameFiles, WorkDoneProgressCancelParams, WorkspaceFolder,
LanguageServer, LanguageServerBinary, LanguageServerBinaryOptions, LanguageServerId,
LanguageServerName, LspRequestFuture, MessageActionItem, MessageType, OneOf, RenameFilesParams,
ServerHealthStatus, ServerStatus, SymbolKind, TextEdit, Url, WillRenameFiles,
WorkDoneProgressCancelParams, WorkspaceFolder,
};
use node_runtime::read_package_installed_version;
use parking_lot::{Mutex, RwLock};
use parking_lot::Mutex;
use postage::watch;
use rand::prelude::*;
@@ -4133,243 +4132,125 @@ impl LspStore {
}
}
pub fn resolve_completions(
pub fn resolve_completion(
&self,
buffer: Model<Buffer>,
completion_indices: Vec<usize>,
completions: Arc<RwLock<Box<[Completion]>>>,
completion: &Completion,
cx: &mut ModelContext<Self>,
) -> Task<Result<bool>> {
) -> Task<Result<Option<Completion>>> {
let client = self.upstream_client();
let language_registry = self.languages.clone();
let buffer_id = buffer.read(cx).remote_id();
let buffer_snapshot = buffer.read(cx).snapshot();
let snapshot = buffer.read(cx).snapshot();
let server_id = completion.server_id;
let lsp_completion = completion.lsp_completion.clone();
cx.spawn(move |this, cx| async move {
let mut did_resolve = false;
if let Some((client, project_id)) = client {
for completion_index in completion_indices {
let (server_id, completion) = {
let completions_guard = completions.read();
let completion = &completions_guard[completion_index];
did_resolve = true;
let server_id = completion.server_id;
let completion = completion.lsp_completion.clone();
let (completion_item, new_label) = if let Some((client, project_id)) = client {
let request = proto::ResolveCompletionDocumentation {
project_id,
language_server_id: server_id.0 as u64,
lsp_completion: serde_json::to_string(&lsp_completion).unwrap().into_bytes(),
buffer_id: buffer_id.into(),
};
(server_id, completion)
};
let response = client
.request(request)
.await
.context("completion documentation resolve proto request")?;
let completion_item = serde_json::from_slice(&response.lsp_completion)?;
Self::resolve_completion_remote(
project_id,
server_id,
buffer_id,
completions.clone(),
completion_index,
completion,
client.clone(),
language_registry.clone(),
)
.await;
}
// todo! support for new_label?
(completion_item, None)
} else {
for completion_index in completion_indices {
let (server_id, completion) = {
let completions_guard = completions.read();
let completion = &completions_guard[completion_index];
let server_id = completion.server_id;
let completion = completion.lsp_completion.clone();
let server_and_adapter = this
.read_with(&cx, |lsp_store, _| {
let server = lsp_store.language_server_for_id(server_id)?;
let adapter =
lsp_store.language_server_adapter_for_id(server.server_id())?;
Some((server, adapter))
})
.ok()
.flatten();
let Some((server, adapter)) = server_and_adapter else {
anyhow::bail!("Language server not found for ID {}", server_id);
};
(server_id, completion)
};
let can_resolve = server
.capabilities()
.completion_provider
.as_ref()
.and_then(|options| options.resolve_provider)
.unwrap_or(false);
if !can_resolve {
return Ok(None);
}
let server_and_adapter = this
.read_with(&cx, |lsp_store, _| {
let server = lsp_store.language_server_for_id(server_id)?;
let adapter =
lsp_store.language_server_adapter_for_id(server.server_id())?;
Some((server, adapter))
})
.ok()
.flatten();
let Some((server, adapter)) = server_and_adapter else {
continue;
};
let request = server.request::<lsp::request::ResolveCompletionItem>(lsp_completion);
let completion_item = request.await?;
did_resolve = true;
Self::resolve_completion_local(
server,
adapter,
&buffer_snapshot,
completions.clone(),
completion_index,
completion,
language_registry.clone(),
)
.await;
// NB: Zed does not have `details` inside the completion resolve capabilities, but
// certain language servers violate the spec and do not return `details`
// immediately, e.g. https://github.com/yioneko/vtsls/issues/213 So we have to
// update the label here anyway...
let new_label = match snapshot.language() {
Some(language) => adapter
.labels_for_completions(&[completion_item.clone()], language)
.await
.log_err()
.unwrap_or_default(),
None => Vec::new(),
}
.pop()
.flatten();
(completion_item, new_label)
};
/*
if let Some(lsp_documentation) = completion_item.documentation.as_ref() {
let documentation = language::prepare_completion_documentation(
lsp_documentation,
&language_registry,
snapshot.language().cloned(),
)
.await;
completion.documentation = Some(documentation);
} else {
completion.documentation = Some(Documentation::Undocumented);
}
if let Some(text_edit) = completion_item.text_edit.as_ref() {
// Technically we don't have to parse the whole `text_edit`, since the only
// language server we currently use that does update `text_edit` in `completionItem/resolve`
// is `typescript-language-server` and they only update `text_edit.new_text`.
// But we should not rely on that.
let edit = parse_completion_text_edit(text_edit, &snapshot);
if let Some((old_range, mut new_text)) = edit {
LineEnding::normalize(&mut new_text);
completion.new_text = new_text;
completion.old_range = old_range;
}
}
Ok(did_resolve)
completion.lsp_completion = completion_item;
if let Some(new_label) = new_label {
completion.label = new_label;
};
*/
Ok(Some(todo!()))
})
}
async fn resolve_completion_local(
server: Arc<lsp::LanguageServer>,
adapter: Arc<CachedLspAdapter>,
snapshot: &BufferSnapshot,
completions: Arc<RwLock<Box<[Completion]>>>,
completion_index: usize,
completion: lsp::CompletionItem,
language_registry: Arc<LanguageRegistry>,
) {
let can_resolve = server
.capabilities()
.completion_provider
.as_ref()
.and_then(|options| options.resolve_provider)
.unwrap_or(false);
if !can_resolve {
return;
}
let request = server.request::<lsp::request::ResolveCompletionItem>(completion);
let Some(completion_item) = request.await.log_err() else {
return;
};
if let Some(lsp_documentation) = completion_item.documentation.as_ref() {
let documentation = language::prepare_completion_documentation(
lsp_documentation,
&language_registry,
snapshot.language().cloned(),
)
.await;
let mut completions = completions.write();
let completion = &mut completions[completion_index];
completion.documentation = Some(documentation);
} else {
let mut completions = completions.write();
let completion = &mut completions[completion_index];
completion.documentation = Some(Documentation::Undocumented);
}
if let Some(text_edit) = completion_item.text_edit.as_ref() {
// Technically we don't have to parse the whole `text_edit`, since the only
// language server we currently use that does update `text_edit` in `completionItem/resolve`
// is `typescript-language-server` and they only update `text_edit.new_text`.
// But we should not rely on that.
let edit = parse_completion_text_edit(text_edit, snapshot);
if let Some((old_range, mut new_text)) = edit {
LineEnding::normalize(&mut new_text);
let mut completions = completions.write();
let completion = &mut completions[completion_index];
completion.new_text = new_text;
completion.old_range = old_range;
}
}
if completion_item.insert_text_format == Some(InsertTextFormat::SNIPPET) {
// vtsls might change the type of completion after resolution.
let mut completions = completions.write();
let completion = &mut completions[completion_index];
if completion_item.insert_text_format != completion.lsp_completion.insert_text_format {
completion.lsp_completion.insert_text_format = completion_item.insert_text_format;
}
}
// NB: Zed does not have `details` inside the completion resolve capabilities, but certain language servers violate the spec and do not return `details` immediately, e.g. https://github.com/yioneko/vtsls/issues/213
// So we have to update the label here anyway...
let new_label = match snapshot.language() {
Some(language) => adapter
.labels_for_completions(&[completion_item.clone()], language)
.await
.log_err()
.unwrap_or_default(),
None => Vec::new(),
}
.pop()
.flatten()
.unwrap_or_else(|| {
CodeLabel::plain(
completion_item.label.clone(),
completion_item.filter_text.as_deref(),
)
});
let mut completions = completions.write();
let completion = &mut completions[completion_index];
completion.lsp_completion = completion_item;
completion.label = new_label;
}
#[allow(clippy::too_many_arguments)]
async fn resolve_completion_remote(
project_id: u64,
server_id: LanguageServerId,
buffer_id: BufferId,
completions: Arc<RwLock<Box<[Completion]>>>,
completion_index: usize,
completion: lsp::CompletionItem,
client: AnyProtoClient,
language_registry: Arc<LanguageRegistry>,
) {
let request = proto::ResolveCompletionDocumentation {
project_id,
language_server_id: server_id.0 as u64,
lsp_completion: serde_json::to_string(&completion).unwrap().into_bytes(),
buffer_id: buffer_id.into(),
};
let Some(response) = client
.request(request)
.await
.context("completion documentation resolve proto request")
.log_err()
else {
return;
};
let Some(lsp_completion) = serde_json::from_slice(&response.lsp_completion).log_err()
else {
return;
};
let documentation = if response.documentation.is_empty() {
Documentation::Undocumented
} else if response.documentation_is_markdown {
Documentation::MultiLineMarkdown(
markdown::parse_markdown(&response.documentation, &language_registry, None).await,
)
} else if response.documentation.lines().count() <= 1 {
Documentation::SingleLine(response.documentation)
} else {
Documentation::MultiLinePlainText(response.documentation)
};
let mut completions = completions.write();
let completion = &mut completions[completion_index];
completion.documentation = Some(documentation);
completion.lsp_completion = lsp_completion;
let old_range = response
.old_start
.and_then(deserialize_anchor)
.zip(response.old_end.and_then(deserialize_anchor));
if let Some((old_start, old_end)) = old_range {
if !response.new_text.is_empty() {
completion.new_text = response.new_text;
completion.old_range = old_start..old_end;
}
}
}
pub fn apply_additional_edits_for_completion(
&self,
buffer_handle: Model<Buffer>,
completion: Completion,
completion: &Completion,
push_to_history: bool,
cx: &mut ModelContext<Self>,
) -> Task<Result<Option<Transaction>>> {
@@ -4377,6 +4258,7 @@ impl LspStore {
let buffer_id = buffer.remote_id();
if let Some((client, project_id)) = self.upstream_client() {
let completion = completion.clone();
cx.spawn(move |_, mut cx| async move {
let response = client
.request(proto::ApplyCompletionAdditionalEdits {
@@ -4415,6 +4297,8 @@ impl LspStore {
_ => return Task::ready(Ok(Default::default())),
};
let lsp_completion = completion.lsp_completion.clone();
let primary = completion.old_range.clone();
cx.spawn(move |this, mut cx| async move {
let can_resolve = lang_server
.capabilities()
@@ -4424,11 +4308,11 @@ impl LspStore {
.unwrap_or(false);
let additional_text_edits = if can_resolve {
lang_server
.request::<lsp::request::ResolveCompletionItem>(completion.lsp_completion)
.request::<lsp::request::ResolveCompletionItem>(lsp_completion)
.await?
.additional_text_edits
} else {
completion.lsp_completion.additional_text_edits
lsp_completion.additional_text_edits
};
if let Some(edits) = additional_text_edits {
let edits = this
@@ -4448,7 +4332,6 @@ impl LspStore {
buffer.start_transaction();
for (range, text) in edits {
let primary = &completion.old_range;
let start_within = primary.start.cmp(&range.start, buffer).is_le()
&& primary.end.cmp(&range.start, buffer).is_ge();
let end_within = range.start.cmp(&primary.end, buffer).is_le()
@@ -6790,7 +6673,7 @@ impl LspStore {
let apply_additional_edits = this.update(&mut cx, |this, cx| {
this.apply_additional_edits_for_completion(
buffer,
Completion {
&Completion {
old_range: completion.old_range,
new_text: completion.new_text,
lsp_completion: completion.lsp_completion,

View File

@@ -57,7 +57,7 @@ use lsp::{
};
use lsp_command::*;
use node_runtime::NodeRuntime;
use parking_lot::{Mutex, RwLock};
use parking_lot::Mutex;
pub use prettier_store::PrettierStore;
use project_settings::{ProjectSettings, SettingsObserver, SettingsObserverEvent};
use remote::{SshConnectionOptions, SshRemoteClient};
@@ -2868,22 +2868,21 @@ impl Project {
})
}
pub fn resolve_completions(
pub fn resolve_completion(
&self,
buffer: Model<Buffer>,
completion_indices: Vec<usize>,
completions: Arc<RwLock<Box<[Completion]>>>,
completion: &Completion,
cx: &mut ModelContext<Self>,
) -> Task<Result<bool>> {
) -> Task<Result<Option<Completion>>> {
self.lsp_store.update(cx, |lsp_store, cx| {
lsp_store.resolve_completions(buffer, completion_indices, completions, cx)
lsp_store.resolve_completion(buffer, completion, cx)
})
}
pub fn apply_additional_edits_for_completion(
&self,
buffer_handle: Model<Buffer>,
completion: Completion,
completion: &Completion,
push_to_history: bool,
cx: &mut ModelContext<Self>,
) -> Task<Result<Option<Transaction>>> {

View File

@@ -24,6 +24,7 @@ futures-lite.workspace = true
futures.workspace = true
git2 = { workspace = true, optional = true }
globset.workspace = true
itertools.workspace = true
log.workspace = true
rand = { workspace = true, optional = true }
regex.workspace = true

View File

@@ -8,6 +8,7 @@ pub mod test;
use futures::Future;
use itertools::Either;
use regex::Regex;
use std::sync::OnceLock;
use std::{
@@ -199,6 +200,35 @@ pub fn measure<R>(label: &str, f: impl FnOnce() -> R) -> R {
}
}
pub fn iterate_expanded_and_wrapped_usize_range(
range: Range<usize>,
additional_before: usize,
additional_after: usize,
wrap_length: usize,
) -> impl Iterator<Item = usize> {
let start_wraps = range.start < additional_before;
let end_wraps = wrap_length < range.end + additional_after;
if start_wraps && end_wraps {
Either::Left(0..wrap_length)
} else if start_wraps {
let wrapped_start = (range.start + wrap_length).saturating_sub(additional_before);
if wrapped_start <= range.end {
Either::Left(0..wrap_length)
} else {
Either::Right((0..range.end + additional_after).chain(wrapped_start..wrap_length))
}
} else if end_wraps {
let wrapped_end = range.end + additional_after - wrap_length;
if range.start <= wrapped_end {
Either::Left(0..wrap_length)
} else {
Either::Right((0..wrapped_end).chain(range.start - additional_before..wrap_length))
}
} else {
Either::Left((range.start - additional_before)..(range.end + additional_after))
}
}
pub trait ResultExt<E> {
type Ok;
@@ -733,4 +763,48 @@ Line 2
Line 3"#
);
}
#[test]
fn test_iterate_expanded_and_wrapped_usize_range() {
// Neither wrap
assert_eq!(
iterate_expanded_and_wrapped_usize_range(2..4, 1, 1, 8).collect::<Vec<usize>>(),
(1..5).collect::<Vec<usize>>()
);
// Start wraps
assert_eq!(
iterate_expanded_and_wrapped_usize_range(2..4, 3, 1, 8).collect::<Vec<usize>>(),
((0..5).chain(7..8)).collect::<Vec<usize>>()
);
// Start wraps all the way around
assert_eq!(
iterate_expanded_and_wrapped_usize_range(2..4, 5, 1, 8).collect::<Vec<usize>>(),
(0..8).collect::<Vec<usize>>()
);
// Start wraps all the way around and past 0
assert_eq!(
iterate_expanded_and_wrapped_usize_range(2..4, 10, 1, 8).collect::<Vec<usize>>(),
(0..8).collect::<Vec<usize>>()
);
// End wraps
assert_eq!(
iterate_expanded_and_wrapped_usize_range(3..5, 1, 4, 8).collect::<Vec<usize>>(),
(0..1).chain(2..8).collect::<Vec<usize>>()
);
// End wraps all the way around
assert_eq!(
iterate_expanded_and_wrapped_usize_range(3..5, 1, 5, 8).collect::<Vec<usize>>(),
(0..8).collect::<Vec<usize>>()
);
// End wraps all the way around and past the end
assert_eq!(
iterate_expanded_and_wrapped_usize_range(3..5, 1, 10, 8).collect::<Vec<usize>>(),
(0..8).collect::<Vec<usize>>()
);
// Both start and end wrap
assert_eq!(
iterate_expanded_and_wrapped_usize_range(3..5, 4, 4, 8).collect::<Vec<usize>>(),
(0..8).collect::<Vec<usize>>()
);
}
}