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.rs155
1 files changed, 102 insertions, 53 deletions
diff --git a/helix-term/src/ui/completion.rs b/helix-term/src/ui/completion.rs
index 3e86237d..be78dd08 100644
--- a/helix-term/src/ui/completion.rs
+++ b/helix-term/src/ui/completion.rs
@@ -1,53 +1,32 @@
+use crate::ui::{menu, Markdown, Menu, Popup, PromptEvent};
use crate::{
compositor::{Component, Context, Event, EventResult},
- handlers::{
- completion::{CompletionItem, LspCompletionItem, ResolveHandler},
- trigger_auto_completion,
+ handlers::completion::{
+ 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_view::{
- document::SavePoint,
editor::CompleteAction,
handlers::lsp::SignatureHelpInvoked,
theme::{Color, Modifier, Style},
ViewId,
};
-use tui::{
- buffer::Buffer as Surface,
- text::{Span, Spans},
-};
-
-use std::{borrow::Cow, sync::Arc};
-
-use helix_core::{
- self as core, chars,
- snippets::{ActiveSnippet, RenderedSnippet, Snippet},
- Change, Transaction,
-};
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 crate::ui::{menu, Markdown, Menu, Popup, PromptEvent};
-
-use helix_lsp::{lsp, util, OffsetEncoding};
+use std::cmp::Reverse;
impl menu::Item for CompletionItem {
type Data = Style;
- fn sort_text(&self, data: &Self::Data) -> Cow<str> {
- self.filter_text(data)
- }
-
- #[inline]
- fn filter_text(&self, _data: &Self::Data) -> Cow<str> {
- 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, dir_style: &Self::Data) -> menu::Row {
let deprecated = match self {
@@ -143,22 +122,16 @@ 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,
}
impl Completion {
pub const ID: &'static str = "completion";
- pub fn new(
- editor: &Editor,
- savepoint: Arc<SavePoint>,
- mut items: Vec<CompletionItem>,
- trigger_offset: usize,
- ) -> Self {
+ pub fn new(editor: &Editor, items: Vec<CompletionItem>, trigger_offset: usize) -> Self {
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.preselect());
let dir_style = editor.theme.get("ui.text.directory");
@@ -202,10 +175,11 @@ 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, &savepoint, false);
+ doc.restore(view, &context.savepoint, false);
// always present here
- let item = item.unwrap();
match item {
CompletionItem::Lsp(item) => {
@@ -232,13 +206,15 @@ 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, &savepoint, true);
+ doc.restore(view, &context.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.unwrap().clone() {
+ let (transaction, additional_edits, snippet) = match item.clone() {
CompletionItem::Lsp(mut item) => {
let language_server = language_server!(item);
@@ -302,7 +278,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.handlers.completions, editor, true);
+ trigger_auto_completion(editor, true);
}
};
@@ -339,14 +315,70 @@ impl Completion {
};
// need to recompute immediately in case start_offset != trigger_offset
- completion
- .popup
- .contents_mut()
- .score(&completion.filter, false);
+ completion.score(false);
completion
}
+ fn score(&mut self, incremental: bool) {
+ let pattern = &self.filter;
+ let mut matcher = MATCHER.lock();
+ matcher.config = Config::DEFAULT;
+ // slight preference towards prefix matches
+ matcher.config.prefer_prefix = true;
+ let pattern = Atom::new(
+ pattern,
+ CaseMatching::Ignore,
+ Normalization::Smart,
+ AtomKind::Fuzzy,
+ false,
+ );
+ let mut buf = Vec::new();
+ let (matches, options) = self.popup.contents_mut().update_options();
+ if incremental {
+ matches.retain_mut(|(index, score)| {
+ let option = &options[*index as usize];
+ let text = option.filter_text();
+ let new_score = pattern.score(Utf32Str::new(text, &mut buf), &mut matcher);
+ match new_score {
+ Some(new_score) => {
+ *score = new_score as u32 / 2;
+ true
+ }
+ None => false,
+ }
+ })
+ } else {
+ matches.clear();
+ matches.extend(options.iter().enumerate().filter_map(|(i, option)| {
+ let text = option.filter_text();
+ pattern
+ .score(Utf32Str::new(text, &mut buf), &mut matcher)
+ .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.
+ //
+ // 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.
+ 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];
+ (
+ score <= min_score,
+ Reverse(option.preselect()),
+ option.provider_priority(),
+ Reverse(score),
+ i,
+ )
+ });
+ }
+
/// Synchronously resolve the given completion item. This is used when
/// accepting a completion.
fn resolve_completion_item(
@@ -388,7 +420,24 @@ impl Completion {
}
}
}
- menu.score(&self.filter, c.is_some());
+ self.score(c.is_some());
+ self.popup.contents_mut().reset_cursor();
+ }
+
+ pub fn replace_provider_completions(
+ &mut self,
+ response: &mut CompletionResponse,
+ is_incomplete: bool,
+ ) {
+ let menu = self.popup.contents_mut();
+ let (_, options) = menu.update_options();
+ if is_incomplete {
+ options.retain(|item| item.provider() != response.provider)
+ }
+ response.take_items(options);
+ self.score(false);
+ let menu = self.popup.contents_mut();
+ menu.ensure_cursor_in_bounds();
}
pub fn is_empty(&self) -> bool {