Unnamed repository; edit this file 'description' to name the repository.
Diffstat (limited to 'helix-term/src/ui/menu.rs')
| -rw-r--r-- | helix-term/src/ui/menu.rs | 82 |
1 files changed, 60 insertions, 22 deletions
diff --git a/helix-term/src/ui/menu.rs b/helix-term/src/ui/menu.rs index 41cf96ac..c120d0b2 100644 --- a/helix-term/src/ui/menu.rs +++ b/helix-term/src/ui/menu.rs @@ -1,7 +1,12 @@ +use std::{borrow::Cow, cmp::Reverse}; + use crate::{ compositor::{Callback, Component, Compositor, Context, Event, EventResult}, ctrl, key, shift, }; +use helix_core::fuzzy::MATCHER; +use nucleo::pattern::{Atom, AtomKind, CaseMatching, Normalization}; +use nucleo::{Config, Utf32Str}; use tui::{buffer::Buffer as Surface, widgets::Table}; pub use tui::widgets::{Cell, Row}; @@ -13,7 +18,17 @@ pub trait Item: Sync + Send + 'static { /// Additional editor state that is used for label calculation. type Data: Sync + Send + 'static; - fn format(&self, data: &Self::Data) -> Row<'_>; + fn format(&self, data: &Self::Data) -> Row; + + fn sort_text(&self, data: &Self::Data) -> Cow<str> { + let label: String = self.format(data).cell_text().collect(); + label.into() + } + + fn filter_text(&self, data: &Self::Data) -> Cow<str> { + let label: String = self.format(data).cell_text().collect(); + label.into() + } } pub type MenuCallback<T> = Box<dyn Fn(&mut Editor, Option<&T>, MenuEvent)>; @@ -62,30 +77,49 @@ impl<T: Item> Menu<T> { } } - pub fn reset_cursor(&mut self) { + 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, + Normalization::Smart, + AtomKind::Fuzzy, + false, + ); + let mut buf = Vec::new(); + 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)); + + // reset cursor position self.cursor = None; self.scroll = 0; self.recalculate = true; } - pub fn update_options(&mut self) -> (&mut Vec<(u32, u32)>, &mut Vec<T>) { - self.recalculate = true; - (&mut self.matches, &mut self.options) - } - - pub fn ensure_cursor_in_bounds(&mut self) { - if self.matches.is_empty() { - self.cursor = None; - self.scroll = 0; - } else { - self.scroll = 0; - self.recalculate = true; - if let Some(cursor) = &mut self.cursor { - *cursor = (*cursor).min(self.matches.len() - 1) - } - } - } - pub fn clear(&mut self) { self.matches.clear(); @@ -194,7 +228,7 @@ impl<T: Item> Menu<T> { } impl<T: Item + PartialEq> Menu<T> { - pub fn replace_option(&mut self, old_option: &impl PartialEq<T>, new_option: T) { + pub fn replace_option(&mut self, old_option: &T, new_option: T) { for option in &mut self.options { if old_option == option { *option = new_option; @@ -312,6 +346,10 @@ impl<T: Item + 'static> Component for Menu<T> { let win_height = area.height as usize; + const fn div_ceil(a: usize, b: usize) -> usize { + (a + b - 1) / b + } + let rows = options .iter() .map(|option| option.format(&self.editor_data)); @@ -352,7 +390,7 @@ impl<T: Item + 'static> Component for Menu<T> { let scroll_style = theme.get("ui.menu.scroll"); if !fits { - let scroll_height = win_height.pow(2).div_ceil(len).min(win_height); + let scroll_height = div_ceil(win_height.pow(2), len).min(win_height); let scroll_line = (win_height - scroll_height) * scroll / std::cmp::max(1, len.saturating_sub(win_height)); |