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.rs112
1 files changed, 85 insertions, 27 deletions
diff --git a/helix-term/src/ui/menu.rs b/helix-term/src/ui/menu.rs
index 41cf96ac..64127e3a 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,43 @@ 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, 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,9 +241,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 +341,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 +367,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 +411,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 +427,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));
}
}