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 | 160 |
1 files changed, 85 insertions, 75 deletions
diff --git a/helix-term/src/ui/menu.rs b/helix-term/src/ui/menu.rs index 41cf96ac..b9c1f9de 100644 --- a/helix-term/src/ui/menu.rs +++ b/helix-term/src/ui/menu.rs @@ -1,22 +1,51 @@ +use std::{borrow::Cow, path::PathBuf}; + use crate::{ compositor::{Callback, Component, Compositor, Context, Event, EventResult}, ctrl, key, shift, }; -use tui::{buffer::Buffer as Surface, widgets::Table}; +use tui::{buffer::Buffer as Surface, text::Spans, widgets::Table}; pub use tui::widgets::{Cell, Row}; -use helix_view::{editor::SmartTabConfig, graphics::Rect, Editor}; +use fuzzy_matcher::skim::SkimMatcherV2 as Matcher; +use fuzzy_matcher::FuzzyMatcher; + +use helix_view::{graphics::Rect, Editor}; use tui::layout::Constraint; -pub trait Item: Sync + Send + 'static { +pub trait Item { /// Additional editor state that is used for label calculation. - type Data: Sync + Send + 'static; + type Data; + + fn label(&self, data: &Self::Data) -> Spans; + + fn sort_text(&self, data: &Self::Data) -> Cow<str> { + let label: String = self.label(data).into(); + label.into() + } + + fn filter_text(&self, data: &Self::Data) -> Cow<str> { + let label: String = self.label(data).into(); + label.into() + } - fn format(&self, data: &Self::Data) -> Row<'_>; + fn row(&self, data: &Self::Data) -> Row { + Row::new(vec![Cell::from(self.label(data))]) + } } -pub type MenuCallback<T> = Box<dyn Fn(&mut Editor, Option<&T>, MenuEvent)>; +impl Item for PathBuf { + /// Root prefix to strip. + type Data = PathBuf; + + fn label(&self, root_path: &Self::Data) -> Spans { + self.strip_prefix(root_path) + .unwrap_or(self) + .to_string_lossy() + .into() + } +} pub struct Menu<T: Item> { options: Vec<T>, @@ -24,12 +53,13 @@ pub struct Menu<T: Item> { cursor: Option<usize>, + matcher: Box<Matcher>, /// (index, score) - matches: Vec<(u32, u32)>, + matches: Vec<(usize, i64)>, widths: Vec<Constraint>, - callback_fn: MenuCallback<T>, + callback_fn: Box<dyn Fn(&mut Editor, Option<&T>, MenuEvent)>, scroll: usize, size: (u16, u16), @@ -47,10 +77,11 @@ impl<T: Item> Menu<T> { editor_data: <T as Item>::Data, callback_fn: impl Fn(&mut Editor, Option<&T>, MenuEvent) + 'static, ) -> Self { - let matches = (0..options.len() as u32).map(|i| (i, 0)).collect(); + let matches = (0..options.len()).map(|i| (i, 0)).collect(); Self { options, editor_data, + matcher: Box::new(Matcher::default()), matches, cursor: None, widths: Vec::new(), @@ -62,30 +93,30 @@ 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(); + self.matches.extend( + self.options + .iter() + .enumerate() + .filter_map(|(index, option)| { + let text = option.filter_text(&self.editor_data); + // TODO: using fuzzy_indices could give us the char idx for match highlighting + self.matcher + .fuzzy_match(&text, pattern) + .map(|score| (index, score)) + }), + ); + // Order of equal elements needs to be preserved as LSP preselected items come in order of high to low priority + self.matches.sort_by_key(|(_, score)| -score); + + // 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(); @@ -113,10 +144,10 @@ impl<T: Item> Menu<T> { let n = self .options .first() - .map(|option| option.format(&self.editor_data).cells.len()) + .map(|option| option.row(&self.editor_data).cells.len()) .unwrap_or_default(); let max_lens = self.options.iter().fold(vec![0; n], |mut acc, option| { - let row = option.format(&self.editor_data); + let row = option.row(&self.editor_data); // maintain max for each column for (acc, cell) in acc.iter_mut().zip(row.cells.iter()) { let width = cell.content.width(); @@ -172,7 +203,7 @@ impl<T: Item> Menu<T> { self.cursor.and_then(|cursor| { self.matches .get(cursor) - .map(|(index, _score)| &self.options[*index as usize]) + .map(|(index, _score)| &self.options[*index]) }) } @@ -180,7 +211,7 @@ impl<T: Item> Menu<T> { self.cursor.and_then(|cursor| { self.matches .get(cursor) - .map(|(index, _score)| &mut self.options[*index as usize]) + .map(|(index, _score)| &mut self.options[*index]) }) } @@ -194,9 +225,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; } @@ -218,21 +249,6 @@ impl<T: Item + 'static> Component for Menu<T> { compositor.pop(); })); - // Ignore tab key when supertab is turned on in order not to interfere - // with it. (Is there a better way to do this?) - if (event == key!(Tab) || event == shift!(Tab)) - && cx.editor.config().auto_completion - && matches!( - cx.editor.config().smart_tab, - Some(SmartTabConfig { - enable: true, - supersede_menu: true, - }) - ) - { - return EventResult::Ignored(None); - } - match event { // esc or ctrl-c aborts the completion and closes the menu key!(Esc) | ctrl!('c') => { @@ -240,12 +256,12 @@ impl<T: Item + 'static> Component for Menu<T> { return EventResult::Consumed(close_fn); } // arrow up/ctrl-p/shift-tab prev completion choice (including updating the doc) - shift!(Tab) | key!(Up) | ctrl!('p') => { + shift!(Tab) | key!(Up) | ctrl!('p') | ctrl!('k') => { self.move_up(); (self.callback_fn)(cx.editor, self.selection(), MenuEvent::Update); return EventResult::Consumed(None); } - key!(Tab) | key!(Down) | ctrl!('n') => { + key!(Tab) | key!(Down) | ctrl!('n') | ctrl!('j') => { // arrow down/ctrl-n/tab advances completion choice (including updating the doc) self.move_down(); (self.callback_fn)(cx.editor, self.selection(), MenuEvent::Update); @@ -294,7 +310,6 @@ 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 scroll = self.scroll; @@ -304,7 +319,7 @@ impl<T: Item + 'static> Component for Menu<T> { .iter() .map(|(index, _score)| { // (index, self.options.get(*index).unwrap()) // get_unchecked - &self.options[*index as usize] // get_unchecked + &self.options[*index] // get_unchecked }) .collect(); @@ -312,9 +327,11 @@ impl<T: Item + 'static> Component for Menu<T> { let win_height = area.height as usize; - let rows = options - .iter() - .map(|option| option.format(&self.editor_data)); + const fn div_ceil(a: usize, b: usize) -> usize { + (a + b - 1) / b + } + + let rows = options.iter().map(|option| option.row(&self.editor_data)); let table = Table::new(rows) .style(style) .highlight_style(selected) @@ -330,29 +347,24 @@ impl<T: Item + 'static> Component for Menu<T> { offset: scroll, selected: self.cursor, }, - false, ); - let render_borders = cx.editor.menu_border(); - - if !render_borders { - if let Some(cursor) = self.cursor { - let offset_from_top = cursor - scroll; - let left = &mut surface[(area.left(), area.y + offset_from_top as u16)]; - left.set_style(selected); - let right = &mut surface[( - area.right().saturating_sub(1), - area.y + offset_from_top as u16, - )]; - right.set_style(selected); - } + if let Some(cursor) = self.cursor { + let offset_from_top = cursor - scroll; + let left = &mut surface[(area.left(), area.y + offset_from_top as u16)]; + left.set_style(selected); + let right = &mut surface[( + area.right().saturating_sub(1), + area.y + offset_from_top as u16, + )]; + right.set_style(selected); } let fits = len <= win_height; 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)); @@ -360,15 +372,13 @@ impl<T: Item + 'static> Component for Menu<T> { for i in 0..win_height { cell = &mut surface[(area.right() - 1, area.top() + i as u16)]; - let half_block = if render_borders { "▌" } else { "▐" }; + cell.set_symbol("▐"); // right half block if scroll_line <= i && i < scroll_line + scroll_height { // Draw scroll thumb - cell.set_symbol(half_block); cell.set_fg(scroll_style.fg.unwrap_or(helix_view::theme::Color::Reset)); - } else if !render_borders { + } else { // Draw scroll track - cell.set_symbol(half_block); cell.set_fg(scroll_style.bg.unwrap_or(helix_view::theme::Color::Reset)); } } |