on master help sign in
only resolve completion items once
28b7303Pascal Kuthe
helix-term/src/ui/completion.rs+55−42
@@ -16,7 +16,6 @@ use crate::commands;
16use crate::ui::{menu, Markdown, Menu, Popup, PromptEvent};
16use crate::ui::{menu, Markdown, Menu, Popup, PromptEvent};
17
17
18use helix_lsp::{lsp, util};
18use helix_lsp::{lsp, util};
19−use lsp::CompletionItem;
20
19
21impl menu::Item for CompletionItem {
20impl menu::Item for CompletionItem {
22 type Data = ();
21 type Data = ();
@@ -26,28 +25,29 @@ impl menu::Item for CompletionItem {
26
25
27 #[inline]
26 #[inline]
28 fn filter_text(&self, _data: &Self::Data) -> Cow<str> {
27 fn filter_text(&self, _data: &Self::Data) -> Cow<str> {
28+ self.item
29− self.filter_text
29+ .filter_text
30 .as_ref()
30 .as_ref()
31− .unwrap_or(&self.label)
31+ .unwrap_or(&self.item.label)
32 .as_str()
32 .as_str()
33 .into()
33 .into()
34 }
34 }
35
35
36 fn format(&self, _data: &Self::Data) -> menu::Row {
36 fn format(&self, _data: &Self::Data) -> menu::Row {
37− let deprecated = self.deprecated.unwrap_or_default()
37+ let deprecated = self.item.deprecated.unwrap_or_default()
38− || self.tags.as_ref().map_or(false, |tags| {
38+ || self.item.tags.as_ref().map_or(false, |tags| {
39 tags.contains(&lsp::CompletionItemTag::DEPRECATED)
39 tags.contains(&lsp::CompletionItemTag::DEPRECATED)
40 });
40 });
41 menu::Row::new(vec![
41 menu::Row::new(vec![
42 menu::Cell::from(Span::styled(
42 menu::Cell::from(Span::styled(
43− self.label.as_str(),
43+ self.item.label.as_str(),
44 if deprecated {
44 if deprecated {
45 Style::default().add_modifier(Modifier::CROSSED_OUT)
45 Style::default().add_modifier(Modifier::CROSSED_OUT)
46 } else {
46 } else {
47 Style::default()
47 Style::default()
48 },
48 },
49 )),
49 )),
50− menu::Cell::from(match self.kind {
50+ menu::Cell::from(match self.item.kind {
51 Some(lsp::CompletionItemKind::TEXT) => "text",
51 Some(lsp::CompletionItemKind::TEXT) => "text",
52 Some(lsp::CompletionItemKind::METHOD) => "method",
52 Some(lsp::CompletionItemKind::METHOD) => "method",
53 Some(lsp::CompletionItemKind::FUNCTION) => "function",
53 Some(lsp::CompletionItemKind::FUNCTION) => "function",
@@ -88,6 +88,12 @@ impl menu::Item for CompletionItem {
88 }
88 }
89}
89}
90
90
91+#[derive(Debug, PartialEq, Default, Clone)]
92+struct CompletionItem {
93+ item: lsp::CompletionItem,
94+ resolved: bool,
95+}
96+
91/// Wraps a Menu.
97/// Wraps a Menu.
92pub struct Completion {
98pub struct Completion {
93 popup: Popup<Menu<CompletionItem>>,
99 popup: Popup<Menu<CompletionItem>>,
@@ -103,7 +109,7 @@ impl Completion {
103 pub fn new(
109 pub fn new(
104 editor: &Editor,
110 editor: &Editor,
105 savepoint: Arc<SavePoint>,
111 savepoint: Arc<SavePoint>,
106− mut items: Vec<CompletionItem>,
112+ mut items: Vec<lsp::CompletionItem>,
107 offset_encoding: helix_lsp::OffsetEncoding,
113 offset_encoding: helix_lsp::OffsetEncoding,
108 start_offset: usize,
114 start_offset: usize,
109 trigger_offset: usize,
115 trigger_offset: usize,
@@ -111,6 +117,13 @@ impl Completion {
111 let replace_mode = editor.config().completion_replace;
117 let replace_mode = editor.config().completion_replace;
112 // Sort completion items according to their preselect status (given by the LSP server)
118 // Sort completion items according to their preselect status (given by the LSP server)
113 items.sort_by_key(|item| !item.preselect.unwrap_or(false));
119 items.sort_by_key(|item| !item.preselect.unwrap_or(false));
120+ let items = items
121+ .into_iter()
122+ .map(|item| CompletionItem {
123+ item,
124+ resolved: false,
125+ })
126+ .collect();
114
127
115 // Then create the menu
128 // Then create the menu
116 let menu = Menu::new(items, (), move |editor: &mut Editor, item, event| {
129 let menu = Menu::new(items, (), move |editor: &mut Editor, item, event| {
@@ -128,7 +141,7 @@ impl Completion {
128 let text = doc.text().slice(..);
141 let text = doc.text().slice(..);
129 let primary_cursor = selection.primary().cursor(text);
142 let primary_cursor = selection.primary().cursor(text);
130
143
131− let (edit_offset, new_text) = if let Some(edit) = &item.text_edit {
144+ let (edit_offset, new_text) = if let Some(edit) = &item.item.text_edit {
132 let edit = match edit {
145 let edit = match edit {
133 lsp::CompletionTextEdit::Edit(edit) => edit.clone(),
146 lsp::CompletionTextEdit::Edit(edit) => edit.clone(),
134 lsp::CompletionTextEdit::InsertAndReplace(item) => {
147 lsp::CompletionTextEdit::InsertAndReplace(item) => {
@@ -151,9 +164,10 @@ impl Completion {
151 (Some((start_offset, end_offset)), edit.new_text)
164 (Some((start_offset, end_offset)), edit.new_text)
152 } else {
165 } else {
153 let new_text = item
166 let new_text = item
167+ .item
154 .insert_text
168 .insert_text
155 .clone()
169 .clone()
156− .unwrap_or_else(|| item.label.clone());
170+ .unwrap_or_else(|| item.item.label.clone());
157 // check that we are still at the correct savepoint
171 // check that we are still at the correct savepoint
158 // we can still generate a transaction regardless but if the
172 // we can still generate a transaction regardless but if the
159 // document changed (and not just the selection) then we will
173 // document changed (and not just the selection) then we will
@@ -162,9 +176,9 @@ impl Completion {
162 (None, new_text)
176 (None, new_text)
163 };
177 };
164
178
165− if matches!(item.kind, Some(lsp::CompletionItemKind::SNIPPET))
179+ if matches!(item.item.kind, Some(lsp::CompletionItemKind::SNIPPET))
166 || matches!(
180 || matches!(
167− item.insert_text_format,
181+ item.item.insert_text_format,
168 Some(lsp::InsertTextFormat::SNIPPET)
182 Some(lsp::InsertTextFormat::SNIPPET)
169 )
183 )
170 {
184 {
@@ -251,26 +265,22 @@ impl Completion {
251 doc.restore(view, &savepoint, false);
265 doc.restore(view, &savepoint, false);
252 }
266 }
253 // always present here
267 // always present here
254− let item = item.unwrap();
268+ let mut item = item.unwrap().clone();
255−
269+
256− // apply additional edits, mostly used to auto import unqualified types
270+ // resolve item if not yet resolved
257− let resolved_item = if item
271+ if !item.resolved {
258− .additional_text_edits
272+ if let Some(resolved) =
259− .as_ref()
260− .map(|edits| !edits.is_empty())
261− .unwrap_or(false)
262− {
263− None
264− } else {
265− Self::resolve_completion_item(doc, item.clone())
273+ Self::resolve_completion_item(doc, item.item.clone())
274+ {
275+ item.item = resolved;
276+ }
266 };
277 };
267−
268 // if more text was entered, remove it
278 // if more text was entered, remove it
269 doc.restore(view, &savepoint, true);
279 doc.restore(view, &savepoint, true);
270 let transaction = item_to_transaction(
280 let transaction = item_to_transaction(
271 doc,
281 doc,
272 view.id,
282 view.id,
273− item,
283+ &item,
274 offset_encoding,
284 offset_encoding,
275 trigger_offset,
285 trigger_offset,
276 false,
286 false,
@@ -283,15 +293,12 @@ impl Completion {
283 changes: completion_changes(&transaction, trigger_offset),
293 changes: completion_changes(&transaction, trigger_offset),
284 });
294 });
285
295
286− if let Some(additional_edits) = resolved_item
296+ // TOOD: add additional _edits to completion_changes?
287− .as_ref()
288− .and_then(|item| item.additional_text_edits.as_ref())
289− .or(item.additional_text_edits.as_ref())
297+ if let Some(additional_edits) = item.item.additional_text_edits {
290− {
291 if !additional_edits.is_empty() {
298 if !additional_edits.is_empty() {
292 let transaction = util::generate_transaction_from_edits(
299 let transaction = util::generate_transaction_from_edits(
293 doc.text(),
300 doc.text(),
294− additional_edits.clone(),
301+ additional_edits,
295 offset_encoding, // TODO: should probably transcode in Client
302 offset_encoding, // TODO: should probably transcode in Client
296 );
303 );
297 doc.apply(&transaction, view.id);
304 doc.apply(&transaction, view.id);
@@ -318,7 +325,7 @@ impl Completion {
318 fn resolve_completion_item(
325 fn resolve_completion_item(
319 doc: &Document,
326 doc: &Document,
320 completion_item: lsp::CompletionItem,
327 completion_item: lsp::CompletionItem,
321− ) -> Option<CompletionItem> {
328+ ) -> Option<lsp::CompletionItem> {
322 let language_server = doc.language_server()?;
329 let language_server = doc.language_server()?;
323
330
324 let future = language_server.resolve_completion_item(completion_item)?;
331 let future = language_server.resolve_completion_item(completion_item)?;
@@ -371,7 +378,7 @@ impl Completion {
371 self.popup.contents().is_empty()
378 self.popup.contents().is_empty()
372 }
379 }
373
380
374− fn replace_item(&mut self, old_item: lsp::CompletionItem, new_item: lsp::CompletionItem) {
381+ fn replace_item(&mut self, old_item: CompletionItem, new_item: CompletionItem) {
375 self.popup.contents_mut().replace_option(old_item, new_item);
382 self.popup.contents_mut().replace_option(old_item, new_item);
376 }
383 }
377
384
@@ -387,7 +394,7 @@ impl Completion {
387 // > The returned completion item should have the documentation property filled in.
394 // > The returned completion item should have the documentation property filled in.
388 // https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_completion
395 // https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_completion
389 let current_item = match self.popup.contents().selection() {
396 let current_item = match self.popup.contents().selection() {
390− Some(item) if item.documentation.is_none() => item.clone(),
397+ Some(item) if !item.resolved => item.clone(),
391 _ => return false,
398 _ => return false,
392 };
399 };
393
400
@@ -397,7 +404,7 @@ impl Completion {
397 };
404 };
398
405
399 // This method should not block the compositor so we handle the response asynchronously.
406 // This method should not block the compositor so we handle the response asynchronously.
400− let future = match language_server.resolve_completion_item(current_item.clone()) {
407+ let future = match language_server.resolve_completion_item(current_item.item.clone()) {
401 Some(future) => future,
408 Some(future) => future,
402 None => return false,
409 None => return false,
403 };
410 };
@@ -415,7 +422,13 @@ impl Completion {
415 .unwrap()
422 .unwrap()
416 .completion
423 .completion
417 {
424 {
418− completion.replace_item(current_item, resolved_item);
425+ completion.replace_item(
426+ current_item,
427+ CompletionItem {
428+ item: resolved_item,
429+ resolved: true,
430+ },
431+ );
419 }
432 }
420 },
433 },
421 );
434 );
@@ -469,25 +482,25 @@ impl Component for Completion {
469 Markdown::new(md, cx.editor.syn_loader.clone())
482 Markdown::new(md, cx.editor.syn_loader.clone())
470 };
483 };
471
484
472− let mut markdown_doc = match &option.documentation {
485+ let mut markdown_doc = match &option.item.documentation {
473 Some(lsp::Documentation::String(contents))
486 Some(lsp::Documentation::String(contents))
474 | Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
487 | Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
475 kind: lsp::MarkupKind::PlainText,
488 kind: lsp::MarkupKind::PlainText,
476 value: contents,
489 value: contents,
477 })) => {
490 })) => {
478 // TODO: convert to wrapped text
491 // TODO: convert to wrapped text
479− markdowned(language, option.detail.as_deref(), Some(contents))
492+ markdowned(language, option.item.detail.as_deref(), Some(contents))
480 }
493 }
481 Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
494 Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
482 kind: lsp::MarkupKind::Markdown,
495 kind: lsp::MarkupKind::Markdown,
483 value: contents,
496 value: contents,
484 })) => {
497 })) => {
485 // TODO: set language based on doc scope
498 // TODO: set language based on doc scope
486− markdowned(language, option.detail.as_deref(), Some(contents))
499+ markdowned(language, option.item.detail.as_deref(), Some(contents))
487 }
500 }
488− None if option.detail.is_some() => {
501+ None if option.item.detail.is_some() => {
489 // TODO: set language based on doc scope
502 // TODO: set language based on doc scope
490− markdowned(language, option.detail.as_deref(), None)
503+ markdowned(language, option.item.detail.as_deref(), None)
491 }
504 }
492 None => return,
505 None => return,
493 };
506 };