aboutsummaryrefslogtreecommitdiff
path: root/helix-term/src/ui
diff options
context:
space:
mode:
Diffstat (limited to 'helix-term/src/ui')
-rw-r--r--helix-term/src/ui/completion.rs87
-rw-r--r--helix-term/src/ui/editor.rs59
-rw-r--r--helix-term/src/ui/menu.rs34
3 files changed, 82 insertions, 98 deletions
diff --git a/helix-term/src/ui/completion.rs b/helix-term/src/ui/completion.rs
index 7c6a0055..48d97fbd 100644
--- a/helix-term/src/ui/completion.rs
+++ b/helix-term/src/ui/completion.rs
@@ -1,8 +1,12 @@
-use crate::compositor::{Component, Context, Event, EventResult};
+use crate::{
+ compositor::{Component, Context, Event, EventResult},
+ handlers::trigger_auto_completion,
+};
use helix_view::{
document::SavePoint,
editor::CompleteAction,
graphics::Margin,
+ handlers::lsp::SignatureHelpInvoked,
theme::{Modifier, Style},
ViewId,
};
@@ -10,7 +14,7 @@ use tui::{buffer::Buffer as Surface, text::Span};
use std::{borrow::Cow, sync::Arc};
-use helix_core::{Change, Transaction};
+use helix_core::{chars, Change, Transaction};
use helix_view::{graphics::Rect, Document, Editor};
use crate::commands;
@@ -95,10 +99,9 @@ pub struct CompletionItem {
/// Wraps a Menu.
pub struct Completion {
popup: Popup<Menu<CompletionItem>>,
- start_offset: usize,
#[allow(dead_code)]
trigger_offset: usize,
- // TODO: maintain a completioncontext with trigger kind & trigger char
+ filter: String,
}
impl Completion {
@@ -108,7 +111,6 @@ impl Completion {
editor: &Editor,
savepoint: Arc<SavePoint>,
mut items: Vec<CompletionItem>,
- start_offset: usize,
trigger_offset: usize,
) -> Self {
let preview_completion_insert = editor.config().preview_completion_insert;
@@ -246,7 +248,7 @@ impl Completion {
// (also without sending the transaction to the LS) *before any further transaction is applied*.
// Otherwise incremental sync breaks (since the state of the LS doesn't match the state the transaction
// is applied to).
- if editor.last_completion.is_none() {
+ if matches!(editor.last_completion, Some(CompleteAction::Triggered)) {
editor.last_completion = Some(CompleteAction::Selected {
savepoint: doc.savepoint(view),
})
@@ -324,8 +326,18 @@ impl Completion {
doc.apply(&transaction, view.id);
}
}
+ // 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);
}
};
+
+ // In case the popup was deleted because of an intersection w/ the auto-complete menu.
+ if event != PromptEvent::Update {
+ editor
+ .handlers
+ .trigger_signature_help(SignatureHelpInvoked::Automatic, editor);
+ }
});
let margin = if editor.menu_border() {
@@ -339,14 +351,30 @@ impl Completion {
.ignore_escape_key(true)
.margin(margin);
+ let (view, doc) = current_ref!(editor);
+ let text = doc.text().slice(..);
+ let cursor = doc.selection(view.id).primary().cursor(text);
+ let offset = text
+ .chars_at(cursor)
+ .reversed()
+ .take_while(|ch| chars::char_is_word(*ch))
+ .count();
+ let start_offset = cursor.saturating_sub(offset);
+
+ let fragment = doc.text().slice(start_offset..cursor);
let mut completion = Self {
popup,
- start_offset,
trigger_offset,
+ // TODO: expand nucleo api to allow moving straight to a Utf32String here
+ // and avoid allocation during matching
+ filter: String::from(fragment),
};
// need to recompute immediately in case start_offset != trigger_offset
- completion.recompute_filter(editor);
+ completion
+ .popup
+ .contents_mut()
+ .score(&completion.filter, false);
completion
}
@@ -366,39 +394,22 @@ impl Completion {
}
}
- pub fn recompute_filter(&mut self, editor: &Editor) {
+ /// Appends (`c: Some(c)`) or removes (`c: None`) a character to/from the filter
+ /// this should be called whenever the user types or deletes a character in insert mode.
+ pub fn update_filter(&mut self, c: Option<char>) {
// recompute menu based on matches
let menu = self.popup.contents_mut();
- let (view, doc) = current_ref!(editor);
-
- // cx.hooks()
- // cx.add_hook(enum type, ||)
- // cx.trigger_hook(enum type, &str, ...) <-- there has to be enough to identify doc/view
- // callback with editor & compositor
- //
- // trigger_hook sends event into channel, that's consumed in the global loop and
- // triggers all registered callbacks
- // TODO: hooks should get processed immediately so maybe do it after select!(), before
- // looping?
-
- let cursor = doc
- .selection(view.id)
- .primary()
- .cursor(doc.text().slice(..));
- if self.trigger_offset <= cursor {
- let fragment = doc.text().slice(self.start_offset..cursor);
- let text = Cow::from(fragment);
- // TODO: logic is same as ui/picker
- menu.score(&text);
- } else {
- // we backspaced before the start offset, clear the menu
- // this will cause the editor to remove the completion popup
- menu.clear();
+ match c {
+ Some(c) => self.filter.push(c),
+ None => {
+ self.filter.pop();
+ if self.filter.is_empty() {
+ menu.clear();
+ return;
+ }
+ }
}
- }
-
- pub fn update(&mut self, cx: &mut commands::Context) {
- self.recompute_filter(cx.editor)
+ menu.score(&self.filter, c.is_some());
}
pub fn is_empty(&self) -> bool {
diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs
index 9f186d14..fef62a29 100644
--- a/helix-term/src/ui/editor.rs
+++ b/helix-term/src/ui/editor.rs
@@ -1,7 +1,6 @@
use crate::{
commands::{self, OnKeyCallback},
compositor::{Component, Context, Event, EventResult},
- job::{self, Callback},
events::{OnModeSwitch, PostCommand},
key,
keymap::{KeymapResult, Keymaps},
@@ -34,8 +33,8 @@ use std::{mem::take, num::NonZeroUsize, path::PathBuf, rc::Rc, sync::Arc};
use tui::{buffer::Buffer as Surface, text::Span};
+use super::document::LineDecoration;
use super::{completion::CompletionItem, statusline};
-use super::{document::LineDecoration, lsp::SignatureHelp};
pub struct EditorView {
pub keymaps: Keymaps,
@@ -837,11 +836,8 @@ impl EditorView {
let mut execute_command = |command: &commands::MappableCommand| {
command.execute(cxt);
helix_event::dispatch(PostCommand { command, cx: cxt });
+
let current_mode = cxt.editor.mode();
- match (last_mode, current_mode) {
- (Mode::Normal, Mode::Insert) => {
- // HAXX: if we just entered insert mode from normal, clear key buf
- // and record the command that got us into this mode.
if current_mode != last_mode {
helix_event::dispatch(OnModeSwitch {
old_mode: last_mode,
@@ -849,29 +845,16 @@ impl EditorView {
cx: cxt,
});
+ // HAXX: if we just entered insert mode from normal, clear key buf
+ // and record the command that got us into this mode.
+ if current_mode == Mode::Insert {
// how we entered insert mode is important, and we should track that so
// we can repeat the side effect.
self.last_insert.0 = command.clone();
self.last_insert.1.clear();
-
- commands::signature_help_impl(cxt, commands::SignatureHelpInvoked::Automatic);
- }
- (Mode::Insert, Mode::Normal) => {
- // if exiting insert mode, remove completion
- self.clear_completion(cxt.editor);
- cxt.editor.completion_request_handle = None;
-
- // TODO: Use an on_mode_change hook to remove signature help
- cxt.jobs.callback(async {
- let call: job::Callback =
- Callback::EditorCompositor(Box::new(|_editor, compositor| {
- compositor.remove(SignatureHelp::ID);
- }));
- Ok(call)
- });
}
- _ => (),
}
+
last_mode = current_mode;
};
@@ -999,12 +982,10 @@ impl EditorView {
editor: &mut Editor,
savepoint: Arc<SavePoint>,
items: Vec<CompletionItem>,
- start_offset: usize,
trigger_offset: usize,
size: Rect,
) -> Option<Rect> {
- let mut completion =
- Completion::new(editor, savepoint, items, start_offset, trigger_offset);
+ let mut completion = Completion::new(editor, savepoint, items, trigger_offset);
if completion.is_empty() {
// skip if we got no completion results
@@ -1025,6 +1006,7 @@ impl EditorView {
self.completion = None;
if let Some(last_completion) = editor.last_completion.take() {
match last_completion {
+ CompleteAction::Triggered => (),
CompleteAction::Applied {
trigger_offset,
changes,
@@ -1038,9 +1020,6 @@ impl EditorView {
}
}
}
-
- // Clear any savepoints
- editor.clear_idle_timer(); // don't retrigger
}
pub fn handle_idle_timeout(&mut self, cx: &mut commands::Context) -> EventResult {
@@ -1054,13 +1033,7 @@ impl EditorView {
};
}
- if cx.editor.mode != Mode::Insert || !cx.editor.config().auto_completion {
- return EventResult::Ignored(None);
- }
-
- crate::commands::insert::idle_completion(cx);
-
- EventResult::Consumed(None)
+ EventResult::Ignored(None)
}
}
@@ -1346,12 +1319,6 @@ impl Component for EditorView {
if callback.is_some() {
// assume close_fn
self.clear_completion(cx.editor);
-
- // In case the popup was deleted because of an intersection w/ the auto-complete menu.
- commands::signature_help_impl(
- &mut cx,
- commands::SignatureHelpInvoked::Automatic,
- );
}
}
}
@@ -1362,14 +1329,6 @@ impl Component for EditorView {
// record last_insert key
self.last_insert.1.push(InsertEvent::Key(key));
-
- // lastly we recalculate completion
- if let Some(completion) = &mut self.completion {
- completion.update(&mut cx);
- if completion.is_empty() {
- self.clear_completion(cx.editor);
- }
- }
}
}
mode => self.command_mode(mode, &mut cx, key),
diff --git a/helix-term/src/ui/menu.rs b/helix-term/src/ui/menu.rs
index 0ee64ce9..64127e3a 100644
--- a/helix-term/src/ui/menu.rs
+++ b/helix-term/src/ui/menu.rs
@@ -96,20 +96,34 @@ impl<T: Item> Menu<T> {
}
}
- pub fn score(&mut self, pattern: &str) {
- // reuse the matches allocation
- self.matches.clear();
+ pub fn score(&mut self, pattern: &str, incremental: bool) {
let mut matcher = MATCHER.lock();
matcher.config = Config::DEFAULT;
let pattern = Atom::new(pattern, CaseMatching::Ignore, AtomKind::Fuzzy, false);
let mut buf = Vec::new();
- let matches = self.options.iter().enumerate().filter_map(|(i, option)| {
- let text = option.filter_text(&self.editor_data);
- pattern
- .score(Utf32Str::new(&text, &mut buf), &mut matcher)
- .map(|score| (i as u32, score as u32))
- });
- self.matches.extend(matches);
+ if incremental {
+ self.matches.retain_mut(|(index, score)| {
+ let option = &self.options[*index as usize];
+ let text = option.filter_text(&self.editor_data);
+ let new_score = pattern.score(Utf32Str::new(&text, &mut buf), &mut matcher);
+ match new_score {
+ Some(new_score) => {
+ *score = new_score as u32;
+ true
+ }
+ None => false,
+ }
+ })
+ } else {
+ self.matches.clear();
+ let matches = self.options.iter().enumerate().filter_map(|(i, option)| {
+ let text = option.filter_text(&self.editor_data);
+ pattern
+ .score(Utf32Str::new(&text, &mut buf), &mut matcher)
+ .map(|score| (i as u32, score as u32))
+ });
+ self.matches.extend(matches);
+ }
self.matches
.sort_unstable_by_key(|&(i, score)| (Reverse(score), i));