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.rs306
1 files changed, 198 insertions, 108 deletions
diff --git a/helix-term/src/ui/completion.rs b/helix-term/src/ui/completion.rs
index c0d3294f..93b6a753 100644
--- a/helix-term/src/ui/completion.rs
+++ b/helix-term/src/ui/completion.rs
@@ -1,41 +1,49 @@
-use crate::handlers::completion::LspCompletionItem;
-use crate::ui::{menu, Markdown, Menu, Popup, PromptEvent};
use crate::{
compositor::{Component, Context, Event, EventResult},
handlers::completion::{
- trigger_auto_completion, CompletionItem, CompletionResponse, ResolveHandler,
+ trigger_auto_completion, CompletionItem, CompletionResponse, LspCompletionItem,
+ ResolveHandler,
},
};
-use helix_core::snippets::{ActiveSnippet, RenderedSnippet, Snippet};
-use helix_core::{self as core, chars, fuzzy::MATCHER, Change, Transaction};
-use helix_lsp::{lsp, util, OffsetEncoding};
+use helix_event::TaskController;
use helix_view::{
+ document::SavePoint,
editor::CompleteAction,
handlers::lsp::SignatureHelpInvoked,
- theme::{Color, Modifier, Style},
+ theme::{Modifier, Style},
ViewId,
};
-use helix_view::{graphics::Rect, Document, Editor};
use nucleo::{
pattern::{Atom, AtomKind, CaseMatching, Normalization},
Config, Utf32Str,
};
-use tui::text::Spans;
use tui::{buffer::Buffer as Surface, text::Span};
-use std::cmp::Reverse;
+use std::{cmp::Reverse, collections::HashMap, sync::Arc};
+
+use helix_core::{
+ self as core, chars,
+ completion::CompletionProvider,
+ fuzzy::MATCHER,
+ snippets::{ActiveSnippet, RenderedSnippet, Snippet},
+ Change, Transaction,
+};
+use helix_view::{graphics::Rect, Document, Editor};
+
+use crate::ui::{menu, Markdown, Menu, Popup, PromptEvent};
+
+use helix_lsp::{lsp, util, OffsetEncoding};
impl menu::Item for CompletionItem {
- type Data = Style;
+ type Data = ();
- fn format(&self, dir_style: &Self::Data) -> menu::Row<'_> {
+ fn format(&self, _data: &Self::Data) -> menu::Row {
let deprecated = match self {
CompletionItem::Lsp(LspCompletionItem { item, .. }) => {
item.deprecated.unwrap_or_default()
- || item
- .tags
- .as_ref()
- .is_some_and(|tags| tags.contains(&lsp::CompletionItemTag::DEPRECATED))
+ || item.tags.as_ref().map_or(false, |tags| {
+ tags.contains(&lsp::CompletionItemTag::DEPRECATED)
+ })
}
CompletionItem::Other(_) => false,
};
@@ -47,72 +55,51 @@ impl menu::Item for CompletionItem {
let kind = match self {
CompletionItem::Lsp(LspCompletionItem { item, .. }) => match item.kind {
- Some(lsp::CompletionItemKind::TEXT) => "text".into(),
- Some(lsp::CompletionItemKind::METHOD) => "method".into(),
- Some(lsp::CompletionItemKind::FUNCTION) => "function".into(),
- Some(lsp::CompletionItemKind::CONSTRUCTOR) => "constructor".into(),
- Some(lsp::CompletionItemKind::FIELD) => "field".into(),
- Some(lsp::CompletionItemKind::VARIABLE) => "variable".into(),
- Some(lsp::CompletionItemKind::CLASS) => "class".into(),
- Some(lsp::CompletionItemKind::INTERFACE) => "interface".into(),
- Some(lsp::CompletionItemKind::MODULE) => "module".into(),
- Some(lsp::CompletionItemKind::PROPERTY) => "property".into(),
- Some(lsp::CompletionItemKind::UNIT) => "unit".into(),
- Some(lsp::CompletionItemKind::VALUE) => "value".into(),
- Some(lsp::CompletionItemKind::ENUM) => "enum".into(),
- Some(lsp::CompletionItemKind::KEYWORD) => "keyword".into(),
- Some(lsp::CompletionItemKind::SNIPPET) => "snippet".into(),
- Some(lsp::CompletionItemKind::COLOR) => item
- .documentation
- .as_ref()
- .and_then(|docs| {
- let text = match docs {
- lsp::Documentation::String(text) => text,
- lsp::Documentation::MarkupContent(lsp::MarkupContent {
- value, ..
- }) => value,
- };
- // Language servers which send Color completion items tend to include a 6
- // digit hex code at the end for the color. The extra 1 digit is for the '#'
- text.get(text.len().checked_sub(7)?..)
- })
- .and_then(Color::from_hex)
- .map_or("color".into(), |color| {
- Spans::from(vec![
- Span::raw("color "),
- Span::styled("■", Style::default().fg(color)),
- ])
- }),
- Some(lsp::CompletionItemKind::FILE) => "file".into(),
- Some(lsp::CompletionItemKind::REFERENCE) => "reference".into(),
- Some(lsp::CompletionItemKind::FOLDER) => "folder".into(),
- Some(lsp::CompletionItemKind::ENUM_MEMBER) => "enum_member".into(),
- Some(lsp::CompletionItemKind::CONSTANT) => "constant".into(),
- Some(lsp::CompletionItemKind::STRUCT) => "struct".into(),
- Some(lsp::CompletionItemKind::EVENT) => "event".into(),
- Some(lsp::CompletionItemKind::OPERATOR) => "operator".into(),
- Some(lsp::CompletionItemKind::TYPE_PARAMETER) => "type_param".into(),
+ Some(lsp::CompletionItemKind::TEXT) => "text",
+ Some(lsp::CompletionItemKind::METHOD) => "method",
+ Some(lsp::CompletionItemKind::FUNCTION) => "function",
+ Some(lsp::CompletionItemKind::CONSTRUCTOR) => "constructor",
+ Some(lsp::CompletionItemKind::FIELD) => "field",
+ Some(lsp::CompletionItemKind::VARIABLE) => "variable",
+ Some(lsp::CompletionItemKind::CLASS) => "class",
+ Some(lsp::CompletionItemKind::INTERFACE) => "interface",
+ Some(lsp::CompletionItemKind::MODULE) => "module",
+ Some(lsp::CompletionItemKind::PROPERTY) => "property",
+ Some(lsp::CompletionItemKind::UNIT) => "unit",
+ Some(lsp::CompletionItemKind::VALUE) => "value",
+ Some(lsp::CompletionItemKind::ENUM) => "enum",
+ Some(lsp::CompletionItemKind::KEYWORD) => "keyword",
+ Some(lsp::CompletionItemKind::SNIPPET) => "snippet",
+ Some(lsp::CompletionItemKind::COLOR) => "color",
+ Some(lsp::CompletionItemKind::FILE) => "file",
+ Some(lsp::CompletionItemKind::REFERENCE) => "reference",
+ Some(lsp::CompletionItemKind::FOLDER) => "folder",
+ Some(lsp::CompletionItemKind::ENUM_MEMBER) => "enum_member",
+ Some(lsp::CompletionItemKind::CONSTANT) => "constant",
+ Some(lsp::CompletionItemKind::STRUCT) => "struct",
+ Some(lsp::CompletionItemKind::EVENT) => "event",
+ Some(lsp::CompletionItemKind::OPERATOR) => "operator",
+ Some(lsp::CompletionItemKind::TYPE_PARAMETER) => "type_param",
Some(kind) => {
log::error!("Received unknown completion item kind: {:?}", kind);
- "".into()
+ ""
}
- None => "".into(),
+ None => "",
},
- CompletionItem::Other(core::CompletionItem { kind, .. }) => kind.as_ref().into(),
+ CompletionItem::Other(core::CompletionItem { kind, .. }) => kind,
};
- let label = Span::styled(
- label,
- if deprecated {
- Style::default().add_modifier(Modifier::CROSSED_OUT)
- } else if kind.0[0].content == "folder" {
- *dir_style
- } else {
- Style::default()
- },
- );
-
- menu::Row::new([menu::Cell::from(label), menu::Cell::from(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),
+ ])
}
}
@@ -122,21 +109,27 @@ pub struct Completion {
#[allow(dead_code)]
trigger_offset: usize,
filter: String,
- // TODO: move to helix-view/central handler struct in the future
resolve_handler: ResolveHandler,
+ pub incomplete_completion_lists: HashMap<CompletionProvider, i8>,
+ // controller for requesting updates for incomplete completion lists
+ pub incomplete_list_controller: TaskController,
}
impl Completion {
pub const ID: &'static str = "completion";
- pub fn new(editor: &Editor, items: Vec<CompletionItem>, trigger_offset: usize) -> Self {
+ pub fn new(
+ editor: &Editor,
+ savepoint: Arc<SavePoint>,
+ items: Vec<CompletionItem>,
+ incomplete_completion_lists: HashMap<CompletionProvider, i8>,
+ trigger_offset: usize,
+ ) -> Self {
let preview_completion_insert = editor.config().preview_completion_insert;
let replace_mode = editor.config().completion_replace;
- let dir_style = editor.theme.get("ui.text.directory");
-
// Then create the menu
- let menu = Menu::new(items, dir_style, move |editor: &mut Editor, item, event| {
+ let menu = Menu::new(items, (), move |editor: &mut Editor, item, event| {
let (view, doc) = current!(editor);
macro_rules! language_server {
@@ -175,11 +168,10 @@ impl Completion {
savepoint: doc.savepoint(view),
})
}
- let item = item.unwrap();
- let context = &editor.handlers.completions.active_completions[&item.provider()];
// if more text was entered, remove it
- doc.restore(view, &context.savepoint, false);
+ doc.restore(view, &savepoint, false);
// always present here
+ let item = item.unwrap();
match item {
CompletionItem::Lsp(item) => {
@@ -206,15 +198,13 @@ impl Completion {
doc.restore(view, &savepoint, false);
}
- let item = item.unwrap();
- let context = &editor.handlers.completions.active_completions[&item.provider()];
// if more text was entered, remove it
- doc.restore(view, &context.savepoint, true);
+ doc.restore(view, &savepoint, true);
// save an undo checkpoint before the completion
doc.append_changes_to_history(view);
// item always present here
- let (transaction, additional_edits, snippet) = match item.clone() {
+ let (transaction, additional_edits, snippet) = match item.unwrap().clone() {
CompletionItem::Lsp(mut item) => {
let language_server = language_server!(item);
@@ -278,7 +268,7 @@ impl Completion {
}
// we could have just inserted a trigger char (like a `crate::` completion for rust
// so we want to retrigger immediately when accepting a completion.
- trigger_auto_completion(editor, true);
+ trigger_auto_completion(&editor.handlers.completions, editor, true);
}
};
@@ -312,6 +302,8 @@ impl Completion {
// and avoid allocation during matching
filter: String::from(fragment),
resolve_handler: ResolveHandler::new(),
+ incomplete_completion_lists,
+ incomplete_list_controller: TaskController::new(),
};
// need to recompute immediately in case start_offset != trigger_offset
@@ -357,15 +349,17 @@ impl Completion {
.map(|score| (i as u32, score as u32 / 3))
}));
}
- // Nucleo is meant as an FZF-like fuzzy matcher and only hides matches that are truly
- // impossible - as in the sequence of characters just doesn't appear. That doesn't work
- // well for completions with multiple language servers where all completions of the next
- // server are below the current one (so you would get good suggestions from the second
- // server below those of the first). Setting a reasonable cutoff below which to move bad
- // completions out of the way helps with that.
+ // nuclueo is meant as an fzf-like fuzzy matcher and only hides
+ // matches that are truely impossible (as in the sequence of char
+ // just doens't appeart) that doesn't work well for completions
+ // with multi lsps where all completions of the next lsp are below
+ // the current one (so you would good suggestions from the second lsp below those
+ // of the first). Setting a reasonable cutoff below which to move
+ // bad completions out of the way helps with that.
//
- // The score computation is a heuristic derived from Nucleo internal constants that may
- // move upstream in the future. I want to test this out here to settle on a good number.
+ // The score computation is a heuristic dervied from nucleo internal
+ // constants and may move upstream in the future. I want to test this out
+ // here to settle on a good number
let min_score = (7 + pattern.needle_text().len() as u32 * 14) / 3;
matches.sort_unstable_by_key(|&(i, score)| {
let option = &options[i as usize];
@@ -424,17 +418,21 @@ impl Completion {
self.popup.contents_mut().reset_cursor();
}
- pub fn replace_provider_completions(
- &mut self,
- response: &mut CompletionResponse,
- is_incomplete: bool,
- ) {
+ pub fn replace_provider_completions(&mut self, response: CompletionResponse) {
let menu = self.popup.contents_mut();
let (_, options) = menu.update_options();
- if is_incomplete {
+ if self
+ .incomplete_completion_lists
+ .remove(&response.provider)
+ .is_some()
+ {
options.retain(|item| item.provider() != response.provider)
}
- response.take_items(options);
+ if response.incomplete {
+ self.incomplete_completion_lists
+ .insert(response.provider, response.priority);
+ }
+ response.into_items(options);
self.score(false);
let menu = self.popup.contents_mut();
menu.ensure_cursor_in_bounds();
@@ -523,10 +521,7 @@ impl Component for Completion {
None => return,
},
CompletionItem::Other(option) => {
- let Some(doc) = option.documentation.as_deref() else {
- return;
- };
- markdowned(language, None, Some(doc))
+ markdowned(language, None, Some(&option.documentation))
}
};
@@ -662,3 +657,98 @@ fn completion_changes(transaction: &Transaction, trigger_offset: usize) -> Vec<C
.filter(|(start, end, _)| (*start..=*end).contains(&trigger_offset))
.collect()
}
+
+// fn lsp_item_to_transaction(
+// doc: &Document,
+// view_id: ViewId,
+// item: &lsp::CompletionItem,
+// offset_encoding: OffsetEncoding,
+// trigger_offset: usize,
+// include_placeholder: bool,
+// replace_mode: bool,
+// ) -> Transaction {
+// use helix_lsp::snippet;
+// let selection = doc.selection(view_id);
+// let text = doc.text().slice(..);
+// let primary_cursor = selection.primary().cursor(text);
+
+// let (edit_offset, new_text) = if let Some(edit) = &item.text_edit {
+// let edit = match edit {
+// lsp::CompletionTextEdit::Edit(edit) => edit.clone(),
+// lsp::CompletionTextEdit::InsertAndReplace(item) => {
+// let range = if replace_mode {
+// item.replace
+// } else {
+// item.insert
+// };
+// lsp::TextEdit::new(range, item.new_text.clone())
+// }
+// };
+
+// let Some(range) =
+// util::lsp_range_to_range(doc.text(), edit.range, offset_encoding)
+// else {
+// return Transaction::new(doc.text());
+// };
+
+// let start_offset = range.anchor as i128 - primary_cursor as i128;
+// let end_offset = range.head as i128 - primary_cursor as i128;
+
+// (Some((start_offset, end_offset)), edit.new_text)
+// } else {
+// let new_text = item
+// .insert_text
+// .clone()
+// .unwrap_or_else(|| item.label.clone());
+// // check that we are still at the correct savepoint
+// // we can still generate a transaction regardless but if the
+// // document changed (and not just the selection) then we will
+// // likely delete the wrong text (same if we applied an edit sent by the LS)
+// debug_assert!(primary_cursor == trigger_offset);
+// (None, new_text)
+// };
+
+// if matches!(item.kind, Some(lsp::CompletionItemKind::SNIPPET))
+// || matches!(
+// item.insert_text_format,
+// Some(lsp::InsertTextFormat::SNIPPET)
+// )
+// {
+// match snippet::parse(&new_text) {
+// Ok(snippet) => util::generate_transaction_from_snippet(
+// doc.text(),
+// selection,
+// edit_offset,
+// replace_mode,
+// snippet,
+// doc.line_ending.as_str(),
+// include_placeholder,
+// doc.tab_width(),
+// doc.indent_width(),
+// ),
+// Err(err) => {
+// log::error!(
+// "Failed to parse snippet: {:?}, remaining output: {}",
+// &new_text,
+// err
+// );
+// Transaction::new(doc.text())
+// }
+// }
+// } else {
+// util::generate_transaction_from_completion_edit(
+// doc.text(),
+// selection,
+// edit_offset,
+// replace_mode,
+// new_text,
+// )
+// }
+// }
+
+// fn completion_changes(transaction: &Transaction, trigger_offset: usize) -> Vec<Change> {
+// transaction
+// .changes_iter()
+// .filter(|(start, end, _)| (*start..=*end).contains(&trigger_offset))
+// .collect()
+// }