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.rs139
1 files changed, 75 insertions, 64 deletions
diff --git a/helix-term/src/ui/menu.rs b/helix-term/src/ui/menu.rs
index 41cf96ac..bdad2e40 100644
--- a/helix-term/src/ui/menu.rs
+++ b/helix-term/src/ui/menu.rs
@@ -1,3 +1,5 @@
+use std::{borrow::Cow, path::PathBuf};
+
use crate::{
compositor::{Callback, Component, Compositor, Context, Event, EventResult},
ctrl, key, shift,
@@ -6,14 +8,39 @@ use tui::{buffer::Buffer as Surface, 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 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 format(&self, data: &Self::Data) -> Row<'_>;
+ 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)>;
@@ -24,8 +51,9 @@ pub struct Menu<T: Item> {
cursor: Option<usize>,
+ matcher: Box<Matcher>,
/// (index, score)
- matches: Vec<(u32, u32)>,
+ matches: Vec<(usize, i64)>,
widths: Vec<Constraint>,
@@ -47,10 +75,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().ignore_case()),
matches,
cursor: None,
widths: Vec::new(),
@@ -62,30 +91,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();
@@ -172,7 +201,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 +209,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 +223,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 +247,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') => {
@@ -294,7 +308,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 +317,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,6 +325,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));
@@ -333,26 +350,22 @@ impl<T: Item + 'static> Component for Menu<T> {
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 +373,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));
}
}