|
|
@ -16,7 +16,6 @@ use crate::commands;
|
|
|
|
use crate::ui::{menu, Markdown, Menu, Popup, PromptEvent};
|
|
|
|
use crate::ui::{menu, Markdown, Menu, Popup, PromptEvent};
|
|
|
|
|
|
|
|
|
|
|
|
use helix_lsp::{lsp, util};
|
|
|
|
use helix_lsp::{lsp, util};
|
|
|
|
use lsp::CompletionItem;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
impl menu::Item for CompletionItem {
|
|
|
|
impl menu::Item for CompletionItem {
|
|
|
|
type Data = ();
|
|
|
|
type Data = ();
|
|
|
@ -26,28 +25,29 @@ impl menu::Item for CompletionItem {
|
|
|
|
|
|
|
|
|
|
|
|
#[inline]
|
|
|
|
#[inline]
|
|
|
|
fn filter_text(&self, _data: &Self::Data) -> Cow<str> {
|
|
|
|
fn filter_text(&self, _data: &Self::Data) -> Cow<str> {
|
|
|
|
self.filter_text
|
|
|
|
self.item
|
|
|
|
|
|
|
|
.filter_text
|
|
|
|
.as_ref()
|
|
|
|
.as_ref()
|
|
|
|
.unwrap_or(&self.label)
|
|
|
|
.unwrap_or(&self.item.label)
|
|
|
|
.as_str()
|
|
|
|
.as_str()
|
|
|
|
.into()
|
|
|
|
.into()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn format(&self, _data: &Self::Data) -> menu::Row {
|
|
|
|
fn format(&self, _data: &Self::Data) -> menu::Row {
|
|
|
|
let deprecated = self.deprecated.unwrap_or_default()
|
|
|
|
let deprecated = self.item.deprecated.unwrap_or_default()
|
|
|
|
|| self.tags.as_ref().map_or(false, |tags| {
|
|
|
|
|| self.item.tags.as_ref().map_or(false, |tags| {
|
|
|
|
tags.contains(&lsp::CompletionItemTag::DEPRECATED)
|
|
|
|
tags.contains(&lsp::CompletionItemTag::DEPRECATED)
|
|
|
|
});
|
|
|
|
});
|
|
|
|
menu::Row::new(vec![
|
|
|
|
menu::Row::new(vec![
|
|
|
|
menu::Cell::from(Span::styled(
|
|
|
|
menu::Cell::from(Span::styled(
|
|
|
|
self.label.as_str(),
|
|
|
|
self.item.label.as_str(),
|
|
|
|
if deprecated {
|
|
|
|
if deprecated {
|
|
|
|
Style::default().add_modifier(Modifier::CROSSED_OUT)
|
|
|
|
Style::default().add_modifier(Modifier::CROSSED_OUT)
|
|
|
|
} else {
|
|
|
|
} else {
|
|
|
|
Style::default()
|
|
|
|
Style::default()
|
|
|
|
},
|
|
|
|
},
|
|
|
|
)),
|
|
|
|
)),
|
|
|
|
menu::Cell::from(match self.kind {
|
|
|
|
menu::Cell::from(match self.item.kind {
|
|
|
|
Some(lsp::CompletionItemKind::TEXT) => "text",
|
|
|
|
Some(lsp::CompletionItemKind::TEXT) => "text",
|
|
|
|
Some(lsp::CompletionItemKind::METHOD) => "method",
|
|
|
|
Some(lsp::CompletionItemKind::METHOD) => "method",
|
|
|
|
Some(lsp::CompletionItemKind::FUNCTION) => "function",
|
|
|
|
Some(lsp::CompletionItemKind::FUNCTION) => "function",
|
|
|
@ -88,6 +88,12 @@ impl menu::Item for CompletionItem {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, PartialEq, Default, Clone)]
|
|
|
|
|
|
|
|
struct CompletionItem {
|
|
|
|
|
|
|
|
item: lsp::CompletionItem,
|
|
|
|
|
|
|
|
resolved: bool,
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Wraps a Menu.
|
|
|
|
/// Wraps a Menu.
|
|
|
|
pub struct Completion {
|
|
|
|
pub struct Completion {
|
|
|
|
popup: Popup<Menu<CompletionItem>>,
|
|
|
|
popup: Popup<Menu<CompletionItem>>,
|
|
|
@ -103,7 +109,7 @@ impl Completion {
|
|
|
|
pub fn new(
|
|
|
|
pub fn new(
|
|
|
|
editor: &Editor,
|
|
|
|
editor: &Editor,
|
|
|
|
savepoint: Arc<SavePoint>,
|
|
|
|
savepoint: Arc<SavePoint>,
|
|
|
|
mut items: Vec<CompletionItem>,
|
|
|
|
mut items: Vec<lsp::CompletionItem>,
|
|
|
|
offset_encoding: helix_lsp::OffsetEncoding,
|
|
|
|
offset_encoding: helix_lsp::OffsetEncoding,
|
|
|
|
start_offset: usize,
|
|
|
|
start_offset: usize,
|
|
|
|
trigger_offset: usize,
|
|
|
|
trigger_offset: usize,
|
|
|
@ -111,6 +117,13 @@ impl Completion {
|
|
|
|
let replace_mode = editor.config().completion_replace;
|
|
|
|
let replace_mode = editor.config().completion_replace;
|
|
|
|
// Sort completion items according to their preselect status (given by the LSP server)
|
|
|
|
// Sort completion items according to their preselect status (given by the LSP server)
|
|
|
|
items.sort_by_key(|item| !item.preselect.unwrap_or(false));
|
|
|
|
items.sort_by_key(|item| !item.preselect.unwrap_or(false));
|
|
|
|
|
|
|
|
let items = items
|
|
|
|
|
|
|
|
.into_iter()
|
|
|
|
|
|
|
|
.map(|item| CompletionItem {
|
|
|
|
|
|
|
|
item,
|
|
|
|
|
|
|
|
resolved: false,
|
|
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
.collect();
|
|
|
|
|
|
|
|
|
|
|
|
// Then create the menu
|
|
|
|
// Then create the menu
|
|
|
|
let menu = Menu::new(items, (), move |editor: &mut Editor, item, event| {
|
|
|
|
let menu = Menu::new(items, (), move |editor: &mut Editor, item, event| {
|
|
|
@ -128,7 +141,7 @@ impl Completion {
|
|
|
|
let text = doc.text().slice(..);
|
|
|
|
let text = doc.text().slice(..);
|
|
|
|
let primary_cursor = selection.primary().cursor(text);
|
|
|
|
let primary_cursor = selection.primary().cursor(text);
|
|
|
|
|
|
|
|
|
|
|
|
let (edit_offset, new_text) = if let Some(edit) = &item.text_edit {
|
|
|
|
let (edit_offset, new_text) = if let Some(edit) = &item.item.text_edit {
|
|
|
|
let edit = match edit {
|
|
|
|
let edit = match edit {
|
|
|
|
lsp::CompletionTextEdit::Edit(edit) => edit.clone(),
|
|
|
|
lsp::CompletionTextEdit::Edit(edit) => edit.clone(),
|
|
|
|
lsp::CompletionTextEdit::InsertAndReplace(item) => {
|
|
|
|
lsp::CompletionTextEdit::InsertAndReplace(item) => {
|
|
|
@ -151,9 +164,10 @@ impl Completion {
|
|
|
|
(Some((start_offset, end_offset)), edit.new_text)
|
|
|
|
(Some((start_offset, end_offset)), edit.new_text)
|
|
|
|
} else {
|
|
|
|
} else {
|
|
|
|
let new_text = item
|
|
|
|
let new_text = item
|
|
|
|
|
|
|
|
.item
|
|
|
|
.insert_text
|
|
|
|
.insert_text
|
|
|
|
.clone()
|
|
|
|
.clone()
|
|
|
|
.unwrap_or_else(|| item.label.clone());
|
|
|
|
.unwrap_or_else(|| item.item.label.clone());
|
|
|
|
// check that we are still at the correct savepoint
|
|
|
|
// check that we are still at the correct savepoint
|
|
|
|
// we can still generate a transaction regardless but if the
|
|
|
|
// we can still generate a transaction regardless but if the
|
|
|
|
// document changed (and not just the selection) then we will
|
|
|
|
// document changed (and not just the selection) then we will
|
|
|
@ -162,9 +176,9 @@ impl Completion {
|
|
|
|
(None, new_text)
|
|
|
|
(None, new_text)
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
if matches!(item.kind, Some(lsp::CompletionItemKind::SNIPPET))
|
|
|
|
if matches!(item.item.kind, Some(lsp::CompletionItemKind::SNIPPET))
|
|
|
|
|| matches!(
|
|
|
|
|| matches!(
|
|
|
|
item.insert_text_format,
|
|
|
|
item.item.insert_text_format,
|
|
|
|
Some(lsp::InsertTextFormat::SNIPPET)
|
|
|
|
Some(lsp::InsertTextFormat::SNIPPET)
|
|
|
|
)
|
|
|
|
)
|
|
|
|
{
|
|
|
|
{
|
|
|
@ -251,26 +265,22 @@ impl Completion {
|
|
|
|
doc.restore(view, &savepoint, false);
|
|
|
|
doc.restore(view, &savepoint, false);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// always present here
|
|
|
|
// always present here
|
|
|
|
let item = item.unwrap();
|
|
|
|
let mut item = item.unwrap().clone();
|
|
|
|
|
|
|
|
|
|
|
|
// apply additional edits, mostly used to auto import unqualified types
|
|
|
|
// resolve item if not yet resolved
|
|
|
|
let resolved_item = if item
|
|
|
|
if !item.resolved {
|
|
|
|
.additional_text_edits
|
|
|
|
if let Some(resolved) =
|
|
|
|
.as_ref()
|
|
|
|
Self::resolve_completion_item(doc, item.item.clone())
|
|
|
|
.map(|edits| !edits.is_empty())
|
|
|
|
|
|
|
|
.unwrap_or(false)
|
|
|
|
|
|
|
|
{
|
|
|
|
{
|
|
|
|
None
|
|
|
|
item.item = resolved;
|
|
|
|
} else {
|
|
|
|
}
|
|
|
|
Self::resolve_completion_item(doc, item.clone())
|
|
|
|
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// if more text was entered, remove it
|
|
|
|
// if more text was entered, remove it
|
|
|
|
doc.restore(view, &savepoint, true);
|
|
|
|
doc.restore(view, &savepoint, true);
|
|
|
|
let transaction = item_to_transaction(
|
|
|
|
let transaction = item_to_transaction(
|
|
|
|
doc,
|
|
|
|
doc,
|
|
|
|
view.id,
|
|
|
|
view.id,
|
|
|
|
item,
|
|
|
|
&item,
|
|
|
|
offset_encoding,
|
|
|
|
offset_encoding,
|
|
|
|
trigger_offset,
|
|
|
|
trigger_offset,
|
|
|
|
false,
|
|
|
|
false,
|
|
|
@ -283,15 +293,12 @@ impl Completion {
|
|
|
|
changes: completion_changes(&transaction, trigger_offset),
|
|
|
|
changes: completion_changes(&transaction, trigger_offset),
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if let Some(additional_edits) = resolved_item
|
|
|
|
// TOOD: add additional _edits to completion_changes?
|
|
|
|
.as_ref()
|
|
|
|
if let Some(additional_edits) = item.item.additional_text_edits {
|
|
|
|
.and_then(|item| item.additional_text_edits.as_ref())
|
|
|
|
|
|
|
|
.or(item.additional_text_edits.as_ref())
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
if !additional_edits.is_empty() {
|
|
|
|
if !additional_edits.is_empty() {
|
|
|
|
let transaction = util::generate_transaction_from_edits(
|
|
|
|
let transaction = util::generate_transaction_from_edits(
|
|
|
|
doc.text(),
|
|
|
|
doc.text(),
|
|
|
|
additional_edits.clone(),
|
|
|
|
additional_edits,
|
|
|
|
offset_encoding, // TODO: should probably transcode in Client
|
|
|
|
offset_encoding, // TODO: should probably transcode in Client
|
|
|
|
);
|
|
|
|
);
|
|
|
|
doc.apply(&transaction, view.id);
|
|
|
|
doc.apply(&transaction, view.id);
|
|
|
@ -318,7 +325,7 @@ impl Completion {
|
|
|
|
fn resolve_completion_item(
|
|
|
|
fn resolve_completion_item(
|
|
|
|
doc: &Document,
|
|
|
|
doc: &Document,
|
|
|
|
completion_item: lsp::CompletionItem,
|
|
|
|
completion_item: lsp::CompletionItem,
|
|
|
|
) -> Option<CompletionItem> {
|
|
|
|
) -> Option<lsp::CompletionItem> {
|
|
|
|
let language_server = doc.language_server()?;
|
|
|
|
let language_server = doc.language_server()?;
|
|
|
|
|
|
|
|
|
|
|
|
let future = language_server.resolve_completion_item(completion_item)?;
|
|
|
|
let future = language_server.resolve_completion_item(completion_item)?;
|
|
|
@ -371,7 +378,7 @@ impl Completion {
|
|
|
|
self.popup.contents().is_empty()
|
|
|
|
self.popup.contents().is_empty()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn replace_item(&mut self, old_item: lsp::CompletionItem, new_item: lsp::CompletionItem) {
|
|
|
|
fn replace_item(&mut self, old_item: CompletionItem, new_item: CompletionItem) {
|
|
|
|
self.popup.contents_mut().replace_option(old_item, new_item);
|
|
|
|
self.popup.contents_mut().replace_option(old_item, new_item);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
@ -387,7 +394,7 @@ impl Completion {
|
|
|
|
// > The returned completion item should have the documentation property filled in.
|
|
|
|
// > The returned completion item should have the documentation property filled in.
|
|
|
|
// https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_completion
|
|
|
|
// https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_completion
|
|
|
|
let current_item = match self.popup.contents().selection() {
|
|
|
|
let current_item = match self.popup.contents().selection() {
|
|
|
|
Some(item) if item.documentation.is_none() => item.clone(),
|
|
|
|
Some(item) if !item.resolved => item.clone(),
|
|
|
|
_ => return false,
|
|
|
|
_ => return false,
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
@ -397,7 +404,7 @@ impl Completion {
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// This method should not block the compositor so we handle the response asynchronously.
|
|
|
|
// This method should not block the compositor so we handle the response asynchronously.
|
|
|
|
let future = match language_server.resolve_completion_item(current_item.clone()) {
|
|
|
|
let future = match language_server.resolve_completion_item(current_item.item.clone()) {
|
|
|
|
Some(future) => future,
|
|
|
|
Some(future) => future,
|
|
|
|
None => return false,
|
|
|
|
None => return false,
|
|
|
|
};
|
|
|
|
};
|
|
|
@ -415,7 +422,13 @@ impl Completion {
|
|
|
|
.unwrap()
|
|
|
|
.unwrap()
|
|
|
|
.completion
|
|
|
|
.completion
|
|
|
|
{
|
|
|
|
{
|
|
|
|
completion.replace_item(current_item, resolved_item);
|
|
|
|
completion.replace_item(
|
|
|
|
|
|
|
|
current_item,
|
|
|
|
|
|
|
|
CompletionItem {
|
|
|
|
|
|
|
|
item: resolved_item,
|
|
|
|
|
|
|
|
resolved: true,
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
|
|
|
},
|
|
|
|
);
|
|
|
|
);
|
|
|
@ -469,25 +482,25 @@ impl Component for Completion {
|
|
|
|
Markdown::new(md, cx.editor.syn_loader.clone())
|
|
|
|
Markdown::new(md, cx.editor.syn_loader.clone())
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
let mut markdown_doc = match &option.documentation {
|
|
|
|
let mut markdown_doc = match &option.item.documentation {
|
|
|
|
Some(lsp::Documentation::String(contents))
|
|
|
|
Some(lsp::Documentation::String(contents))
|
|
|
|
| Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
|
|
|
|
| Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
|
|
|
|
kind: lsp::MarkupKind::PlainText,
|
|
|
|
kind: lsp::MarkupKind::PlainText,
|
|
|
|
value: contents,
|
|
|
|
value: contents,
|
|
|
|
})) => {
|
|
|
|
})) => {
|
|
|
|
// TODO: convert to wrapped text
|
|
|
|
// TODO: convert to wrapped text
|
|
|
|
markdowned(language, option.detail.as_deref(), Some(contents))
|
|
|
|
markdowned(language, option.item.detail.as_deref(), Some(contents))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
|
|
|
|
Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
|
|
|
|
kind: lsp::MarkupKind::Markdown,
|
|
|
|
kind: lsp::MarkupKind::Markdown,
|
|
|
|
value: contents,
|
|
|
|
value: contents,
|
|
|
|
})) => {
|
|
|
|
})) => {
|
|
|
|
// TODO: set language based on doc scope
|
|
|
|
// TODO: set language based on doc scope
|
|
|
|
markdowned(language, option.detail.as_deref(), Some(contents))
|
|
|
|
markdowned(language, option.item.detail.as_deref(), Some(contents))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
None if option.detail.is_some() => {
|
|
|
|
None if option.item.detail.is_some() => {
|
|
|
|
// TODO: set language based on doc scope
|
|
|
|
// TODO: set language based on doc scope
|
|
|
|
markdowned(language, option.detail.as_deref(), None)
|
|
|
|
markdowned(language, option.item.detail.as_deref(), None)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
None => return,
|
|
|
|
None => return,
|
|
|
|
};
|
|
|
|
};
|
|
|
|