Unnamed repository; edit this file 'description' to name the repository.
Diffstat (limited to 'helix-term/src/ui/completion.rs')
-rw-r--r--helix-term/src/ui/completion.rs218
1 files changed, 131 insertions, 87 deletions
diff --git a/helix-term/src/ui/completion.rs b/helix-term/src/ui/completion.rs
index 14397bb5..cb0af6fc 100644
--- a/helix-term/src/ui/completion.rs
+++ b/helix-term/src/ui/completion.rs
@@ -1,6 +1,9 @@
use crate::{
compositor::{Component, Context, Event, EventResult},
- handlers::{completion::ResolveHandler, trigger_auto_completion},
+ handlers::{
+ completion::{CompletionItem, LspCompletionItem, ResolveHandler},
+ trigger_auto_completion,
+ },
};
use helix_view::{
document::SavePoint,
@@ -13,12 +16,12 @@ use tui::{buffer::Buffer as Surface, text::Span};
use std::{borrow::Cow, sync::Arc};
-use helix_core::{chars, Change, Transaction};
+use helix_core::{self as core, chars, Change, Transaction};
use helix_view::{graphics::Rect, Document, Editor};
use crate::ui::{menu, Markdown, Menu, Popup, PromptEvent};
-use helix_lsp::{lsp, util, LanguageServerId, OffsetEncoding};
+use helix_lsp::{lsp, util, OffsetEncoding};
impl menu::Item for CompletionItem {
type Data = ();
@@ -28,30 +31,35 @@ impl menu::Item for CompletionItem {
#[inline]
fn filter_text(&self, _data: &Self::Data) -> Cow<str> {
- self.item
- .filter_text
- .as_ref()
- .unwrap_or(&self.item.label)
- .as_str()
- .into()
+ match self {
+ CompletionItem::Lsp(LspCompletionItem { item, .. }) => item
+ .filter_text
+ .as_ref()
+ .unwrap_or(&item.label)
+ .as_str()
+ .into(),
+ CompletionItem::Other(core::CompletionItem { label, .. }) => label.clone(),
+ }
}
fn format(&self, _data: &Self::Data) -> menu::Row {
- let deprecated = self.item.deprecated.unwrap_or_default()
- || self.item.tags.as_ref().map_or(false, |tags| {
- tags.contains(&lsp::CompletionItemTag::DEPRECATED)
- });
+ let deprecated = match self {
+ CompletionItem::Lsp(LspCompletionItem { item, .. }) => {
+ item.deprecated.unwrap_or_default()
+ || item.tags.as_ref().map_or(false, |tags| {
+ tags.contains(&lsp::CompletionItemTag::DEPRECATED)
+ })
+ }
+ CompletionItem::Other(_) => false,
+ };
- menu::Row::new(vec![
- menu::Cell::from(Span::styled(
- self.item.label.as_str(),
- if deprecated {
- Style::default().add_modifier(Modifier::CROSSED_OUT)
- } else {
- Style::default()
- },
- )),
- menu::Cell::from(match self.item.kind {
+ let label = match self {
+ CompletionItem::Lsp(LspCompletionItem { item, .. }) => item.label.as_str(),
+ CompletionItem::Other(core::CompletionItem { label, .. }) => label,
+ };
+
+ let kind = match self {
+ CompletionItem::Lsp(LspCompletionItem { item, .. }) => match item.kind {
Some(lsp::CompletionItemKind::TEXT) => "text",
Some(lsp::CompletionItemKind::METHOD) => "method",
Some(lsp::CompletionItemKind::FUNCTION) => "function",
@@ -82,18 +90,24 @@ impl menu::Item for CompletionItem {
""
}
None => "",
- }),
+ },
+ CompletionItem::Other(core::CompletionItem { kind, .. }) => kind,
+ };
+
+ menu::Row::new([
+ menu::Cell::from(Span::styled(
+ label,
+ if deprecated {
+ Style::default().add_modifier(Modifier::CROSSED_OUT)
+ } else {
+ Style::default()
+ },
+ )),
+ menu::Cell::from(kind),
])
}
}
-#[derive(Debug, PartialEq, Default, Clone)]
-pub struct CompletionItem {
- pub item: lsp::CompletionItem,
- pub provider: LanguageServerId,
- pub resolved: bool,
-}
-
/// Wraps a Menu.
pub struct Completion {
popup: Popup<Menu<CompletionItem>>,
@@ -115,11 +129,11 @@ impl Completion {
let preview_completion_insert = editor.config().preview_completion_insert;
let replace_mode = editor.config().completion_replace;
// Sort completion items according to their preselect status (given by the LSP server)
- items.sort_by_key(|item| !item.item.preselect.unwrap_or(false));
+ items.sort_by_key(|item| !item.preselect());
// Then create the menu
let menu = Menu::new(items, (), move |editor: &mut Editor, item, event| {
- fn item_to_transaction(
+ fn lsp_item_to_transaction(
doc: &Document,
view_id: ViewId,
item: &lsp::CompletionItem,
@@ -257,16 +271,23 @@ impl Completion {
// always present here
let item = item.unwrap();
- let transaction = item_to_transaction(
- doc,
- view.id,
- &item.item,
- language_server!(item).offset_encoding(),
- trigger_offset,
- true,
- replace_mode,
- );
- doc.apply_temporary(&transaction, view.id);
+ match item {
+ CompletionItem::Lsp(item) => doc.apply_temporary(
+ &lsp_item_to_transaction(
+ doc,
+ view.id,
+ &item.item,
+ language_server!(item).offset_encoding(),
+ trigger_offset,
+ true,
+ replace_mode,
+ ),
+ view.id,
+ ),
+ CompletionItem::Other(core::CompletionItem { transaction, .. }) => {
+ doc.apply_temporary(transaction, view.id)
+ }
+ };
}
PromptEvent::Update => {}
PromptEvent::Validate => {
@@ -275,32 +296,46 @@ impl Completion {
{
doc.restore(view, &savepoint, false);
}
- // always present here
- let mut item = item.unwrap().clone();
-
- let language_server = language_server!(item);
- let offset_encoding = language_server.offset_encoding();
- if !item.resolved {
- if let Some(resolved) =
- Self::resolve_completion_item(language_server, item.item.clone())
- {
- item.item = resolved;
- }
- };
// if more text was entered, remove it
doc.restore(view, &savepoint, true);
// save an undo checkpoint before the completion
doc.append_changes_to_history(view);
- let transaction = item_to_transaction(
- doc,
- view.id,
- &item.item,
- offset_encoding,
- trigger_offset,
- false,
- replace_mode,
- );
+
+ // item always present here
+ let (transaction, additional_edits) = match item.unwrap().clone() {
+ CompletionItem::Lsp(mut item) => {
+ let language_server = language_server!(item);
+
+ // resolve item if not yet resolved
+ if !item.resolved {
+ if let Some(resolved_item) = Self::resolve_completion_item(
+ language_server,
+ item.item.clone(),
+ ) {
+ item.item = resolved_item;
+ }
+ };
+
+ let encoding = language_server.offset_encoding();
+ let transaction = lsp_item_to_transaction(
+ doc,
+ view.id,
+ &item.item,
+ encoding,
+ trigger_offset,
+ false,
+ replace_mode,
+ );
+ let add_edits = item.item.additional_text_edits;
+
+ (transaction, add_edits.map(|edits| (edits, encoding)))
+ }
+ CompletionItem::Other(core::CompletionItem { transaction, .. }) => {
+ (transaction, None)
+ }
+ };
+
doc.apply(&transaction, view.id);
editor.last_completion = Some(CompleteAction::Applied {
@@ -309,7 +344,7 @@ impl Completion {
});
// TODO: add additional _edits to completion_changes?
- if let Some(additional_edits) = item.item.additional_text_edits {
+ if let Some((additional_edits, offset_encoding)) = additional_edits {
if !additional_edits.is_empty() {
let transaction = util::generate_transaction_from_edits(
doc.text(),
@@ -414,7 +449,11 @@ impl Completion {
self.popup.contents().is_empty()
}
- pub fn replace_item(&mut self, old_item: &CompletionItem, new_item: CompletionItem) {
+ pub fn replace_item(
+ &mut self,
+ old_item: &impl PartialEq<CompletionItem>,
+ new_item: CompletionItem,
+ ) {
self.popup.contents_mut().replace_option(old_item, new_item);
}
@@ -440,7 +479,7 @@ impl Component for Completion {
Some(option) => option,
None => return,
};
- if !option.resolved {
+ if let CompletionItem::Lsp(option) = option {
self.resolve_handler.ensure_item_resolved(cx.editor, option);
}
// need to render:
@@ -465,27 +504,32 @@ impl Component for Completion {
Markdown::new(md, cx.editor.syn_loader.clone())
};
- let mut markdown_doc = match &option.item.documentation {
- Some(lsp::Documentation::String(contents))
- | Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
- kind: lsp::MarkupKind::PlainText,
- value: contents,
- })) => {
- // TODO: convert to wrapped text
- markdowned(language, option.item.detail.as_deref(), Some(contents))
- }
- Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
- kind: lsp::MarkupKind::Markdown,
- value: contents,
- })) => {
- // TODO: set language based on doc scope
- markdowned(language, option.item.detail.as_deref(), Some(contents))
- }
- None if option.item.detail.is_some() => {
- // TODO: set language based on doc scope
- markdowned(language, option.item.detail.as_deref(), None)
+ let mut markdown_doc = match option {
+ CompletionItem::Lsp(option) => match &option.item.documentation {
+ Some(lsp::Documentation::String(contents))
+ | Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
+ kind: lsp::MarkupKind::PlainText,
+ value: contents,
+ })) => {
+ // TODO: convert to wrapped text
+ markdowned(language, option.item.detail.as_deref(), Some(contents))
+ }
+ Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
+ kind: lsp::MarkupKind::Markdown,
+ value: contents,
+ })) => {
+ // TODO: set language based on doc scope
+ markdowned(language, option.item.detail.as_deref(), Some(contents))
+ }
+ None if option.item.detail.is_some() => {
+ // TODO: set language based on doc scope
+ markdowned(language, option.item.detail.as_deref(), None)
+ }
+ None => return,
+ },
+ CompletionItem::Other(option) => {
+ markdowned(language, None, Some(&option.documentation))
}
- None => return,
};
let popup_area = self.popup.area(area, cx.editor);