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 | 98 |
1 files changed, 71 insertions, 27 deletions
diff --git a/helix-term/src/ui/menu.rs b/helix-term/src/ui/menu.rs index 41cf96ac..0ee64ce9 100644 --- a/helix-term/src/ui/menu.rs +++ b/helix-term/src/ui/menu.rs @@ -1,19 +1,53 @@ +use std::{borrow::Cow, cmp::Reverse, path::PathBuf}; + use crate::{ compositor::{Callback, Component, Compositor, Context, Event, EventResult}, ctrl, key, shift, }; -use tui::{buffer::Buffer as Surface, widgets::Table}; +use helix_core::fuzzy::MATCHER; +use nucleo::pattern::{Atom, AtomKind, CaseMatching}; +use nucleo::{Config, Utf32Str}; +use tui::{ + buffer::Buffer as Surface, + widgets::{Block, Borders, Table, Widget}, +}; pub use tui::widgets::{Cell, Row}; -use helix_view::{editor::SmartTabConfig, graphics::Rect, Editor}; +use helix_view::{ + editor::SmartTabConfig, + graphics::{Margin, Rect}, + Editor, +}; use tui::layout::Constraint; 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() + } +} + +impl Item for PathBuf { + /// Root prefix to strip. + type Data = PathBuf; + + fn format(&self, root_path: &Self::Data) -> Row { + self.strip_prefix(root_path) + .unwrap_or(self) + .to_string_lossy() + .into() + } } pub type MenuCallback<T> = Box<dyn Fn(&mut Editor, Option<&T>, MenuEvent)>; @@ -62,30 +96,29 @@ impl<T: Item> Menu<T> { } } - pub fn reset_cursor(&mut self) { + pub fn score(&mut self, pattern: &str) { + // reuse the matches allocation + self.matches.clear(); + 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); + 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,9 +227,9 @@ 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 { + if old_option == *option { *option = new_option; break; } @@ -294,9 +327,17 @@ impl<T: Item + 'static> Component for Menu<T> { .try_get("ui.menu") .unwrap_or_else(|| theme.get("ui.text")); let selected = theme.get("ui.menu.selected"); - surface.clear_with(area, style); + let render_borders = cx.editor.menu_border(); + + let area = if render_borders { + Widget::render(Block::default().borders(Borders::ALL), area, surface); + area.inner(&Margin::vertical(1)) + } else { + area + }; + let scroll = self.scroll; let options: Vec<_> = self @@ -312,6 +353,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 +397,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)); @@ -368,7 +413,6 @@ impl<T: Item + 'static> Component for Menu<T> { cell.set_fg(scroll_style.fg.unwrap_or(helix_view::theme::Color::Reset)); } else if !render_borders { // Draw scroll track - cell.set_symbol(half_block); cell.set_fg(scroll_style.bg.unwrap_or(helix_view::theme::Color::Reset)); } } |