aboutsummaryrefslogtreecommitdiff
path: root/helix-term/src/ui
diff options
context:
space:
mode:
authorGokul Soumya2022-07-02 11:21:27 +0000
committerGitHub2022-07-02 11:21:27 +0000
commit6e2aaed5c2cbcedc9ee4e225510cae4f357888aa (patch)
treebddbecd95c9b3df5cb207736f1d0cbdeb56c1c3c /helix-term/src/ui
parent290b3ebbbe0c365eee436b9de9d6d6fc2b4339e9 (diff)
Reuse menu::Item trait in picker (#2814)
* Refactor menu::Item to accomodate external state Will be useful for storing editor state when reused by pickers. * Add some type aliases for readability * Reuse menu::Item trait in picker This opens the way for merging the menu and picker code in the future, since a picker is essentially a menu + prompt. More excitingly, this change will also allow aligning items in the picker, which would be useful (for example) in the command palette for aligning the descriptions to the left and the keybinds to the right in two separate columns. The item formatting of each picker has been kept as is, even though there is room for improvement now that we can format the data into columns, since that is better tackled in a separate PR. * Rename menu::Item::EditorData to Data * Call and inline filter_text() in sort_text() completion * Rename diagnostic picker's Item::Data
Diffstat (limited to 'helix-term/src/ui')
-rw-r--r--helix-term/src/ui/completion.rs23
-rw-r--r--helix-term/src/ui/menu.rs54
-rw-r--r--helix-term/src/ui/mod.rs7
-rw-r--r--helix-term/src/ui/picker.rs38
4 files changed, 74 insertions, 48 deletions
diff --git a/helix-term/src/ui/completion.rs b/helix-term/src/ui/completion.rs
index 38005aad..a3637415 100644
--- a/helix-term/src/ui/completion.rs
+++ b/helix-term/src/ui/completion.rs
@@ -2,6 +2,7 @@ use crate::compositor::{Component, Context, EventResult};
use crossterm::event::{Event, KeyCode, KeyEvent};
use helix_view::editor::CompleteAction;
use tui::buffer::Buffer as Surface;
+use tui::text::Spans;
use std::borrow::Cow;
@@ -15,19 +16,25 @@ use helix_lsp::{lsp, util};
use lsp::CompletionItem;
impl menu::Item for CompletionItem {
- fn sort_text(&self) -> &str {
- self.filter_text.as_ref().unwrap_or(&self.label).as_str()
+ type Data = ();
+ fn sort_text(&self, data: &Self::Data) -> Cow<str> {
+ self.filter_text(data)
}
- fn filter_text(&self) -> &str {
- self.filter_text.as_ref().unwrap_or(&self.label).as_str()
+ #[inline]
+ fn filter_text(&self, _data: &Self::Data) -> Cow<str> {
+ self.filter_text
+ .as_ref()
+ .unwrap_or(&self.label)
+ .as_str()
+ .into()
}
- fn label(&self) -> &str {
- self.label.as_str()
+ fn label(&self, _data: &Self::Data) -> Spans {
+ self.label.as_str().into()
}
- fn row(&self) -> menu::Row {
+ fn row(&self, _data: &Self::Data) -> menu::Row {
menu::Row::new(vec![
menu::Cell::from(self.label.as_str()),
menu::Cell::from(match self.kind {
@@ -85,7 +92,7 @@ impl Completion {
start_offset: usize,
trigger_offset: usize,
) -> Self {
- let menu = Menu::new(items, move |editor: &mut Editor, item, event| {
+ let menu = Menu::new(items, (), move |editor: &mut Editor, item, event| {
fn item_to_transaction(
doc: &Document,
item: &CompletionItem,
diff --git a/helix-term/src/ui/menu.rs b/helix-term/src/ui/menu.rs
index 0519374a..6bb64139 100644
--- a/helix-term/src/ui/menu.rs
+++ b/helix-term/src/ui/menu.rs
@@ -1,9 +1,11 @@
+use std::{borrow::Cow, path::PathBuf};
+
use crate::{
compositor::{Callback, Component, Compositor, Context, EventResult},
ctrl, key, shift,
};
use crossterm::event::Event;
-use tui::{buffer::Buffer as Surface, widgets::Table};
+use tui::{buffer::Buffer as Surface, text::Spans, widgets::Table};
pub use tui::widgets::{Cell, Row};
@@ -14,22 +16,41 @@ use helix_view::{graphics::Rect, Editor};
use tui::layout::Constraint;
pub trait Item {
- fn label(&self) -> &str;
+ /// Additional editor state that is used for label calculation.
+ 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 sort_text(&self) -> &str {
- self.label()
+ fn filter_text(&self, data: &Self::Data) -> Cow<str> {
+ let label: String = self.label(data).into();
+ label.into()
}
- fn filter_text(&self) -> &str {
- self.label()
+
+ fn row(&self, data: &Self::Data) -> Row {
+ Row::new(vec![Cell::from(self.label(data))])
}
+}
- fn row(&self) -> Row {
- Row::new(vec![Cell::from(self.label())])
+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>,
+ editor_data: T::Data,
cursor: Option<usize>,
@@ -54,10 +75,12 @@ impl<T: Item> Menu<T> {
// rendering)
pub fn new(
options: Vec<T>,
+ editor_data: <T as Item>::Data,
callback_fn: impl Fn(&mut Editor, Option<&T>, MenuEvent) + 'static,
) -> Self {
let mut menu = Self {
options,
+ editor_data,
matcher: Box::new(Matcher::default()),
matches: Vec::new(),
cursor: None,
@@ -83,16 +106,17 @@ impl<T: Item> Menu<T> {
.iter()
.enumerate()
.filter_map(|(index, option)| {
- let text = option.filter_text();
+ let text: String = option.filter_text(&self.editor_data).into();
// TODO: using fuzzy_indices could give us the char idx for match highlighting
self.matcher
- .fuzzy_match(text, pattern)
+ .fuzzy_match(&text, pattern)
.map(|score| (index, score))
}),
);
// matches.sort_unstable_by_key(|(_, score)| -score);
- self.matches
- .sort_unstable_by_key(|(index, _score)| self.options[*index].sort_text());
+ self.matches.sort_unstable_by_key(|(index, _score)| {
+ self.options[*index].sort_text(&self.editor_data)
+ });
// reset cursor position
self.cursor = None;
@@ -127,10 +151,10 @@ impl<T: Item> Menu<T> {
let n = self
.options
.first()
- .map(|option| option.row().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.row();
+ 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();
@@ -300,7 +324,7 @@ impl<T: Item + 'static> Component for Menu<T> {
let scroll_line = (win_height - scroll_height) * scroll
/ std::cmp::max(1, len.saturating_sub(win_height));
- let rows = options.iter().map(|option| option.row());
+ let rows = options.iter().map(|option| option.row(&self.editor_data));
let table = Table::new(rows)
.style(style)
.highlight_style(selected)
diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs
index 948a5f2b..8d2bd325 100644
--- a/helix-term/src/ui/mod.rs
+++ b/helix-term/src/ui/mod.rs
@@ -23,8 +23,6 @@ pub use text::Text;
use helix_core::regex::Regex;
use helix_core::regex::RegexBuilder;
use helix_view::{Document, Editor, View};
-use tui;
-use tui::text::Spans;
use std::path::PathBuf;
@@ -172,10 +170,7 @@ pub fn file_picker(root: PathBuf, config: &helix_view::editor::Config) -> FilePi
FilePicker::new(
files,
- move |path: &PathBuf| {
- // format_fn
- Spans::from(path.strip_prefix(&root).unwrap_or(path).to_string_lossy())
- },
+ root,
move |cx, path: &PathBuf, action| {
if let Err(e) = cx.editor.open(path, action) {
let err = if let Some(err) = e.source() {
diff --git a/helix-term/src/ui/picker.rs b/helix-term/src/ui/picker.rs
index 1581b0a1..01fea718 100644
--- a/helix-term/src/ui/picker.rs
+++ b/helix-term/src/ui/picker.rs
@@ -6,7 +6,6 @@ use crate::{
use crossterm::event::Event;
use tui::{
buffer::Buffer as Surface,
- text::Spans,
widgets::{Block, BorderType, Borders},
};
@@ -30,6 +29,8 @@ use helix_view::{
Document, Editor,
};
+use super::menu::Item;
+
pub const MIN_AREA_WIDTH_FOR_PREVIEW: u16 = 72;
/// Biggest file size to preview in bytes
pub const MAX_FILE_SIZE_FOR_PREVIEW: u64 = 10 * 1024 * 1024;
@@ -37,7 +38,7 @@ pub const MAX_FILE_SIZE_FOR_PREVIEW: u64 = 10 * 1024 * 1024;
/// File path and range of lines (used to align and highlight lines)
pub type FileLocation = (PathBuf, Option<(usize, usize)>);
-pub struct FilePicker<T> {
+pub struct FilePicker<T: Item> {
picker: Picker<T>,
pub truncate_start: bool,
/// Caches paths to documents
@@ -84,15 +85,15 @@ impl Preview<'_, '_> {
}
}
-impl<T> FilePicker<T> {
+impl<T: Item> FilePicker<T> {
pub fn new(
options: Vec<T>,
- format_fn: impl Fn(&T) -> Spans + 'static,
+ editor_data: T::Data,
callback_fn: impl Fn(&mut Context, &T, Action) + 'static,
preview_fn: impl Fn(&Editor, &T) -> Option<FileLocation> + 'static,
) -> Self {
let truncate_start = true;
- let mut picker = Picker::new(options, format_fn, callback_fn);
+ let mut picker = Picker::new(options, editor_data, callback_fn);
picker.truncate_start = truncate_start;
Self {
@@ -163,7 +164,7 @@ impl<T> FilePicker<T> {
}
}
-impl<T: 'static> Component for FilePicker<T> {
+impl<T: Item + 'static> Component for FilePicker<T> {
fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) {
// +---------+ +---------+
// |prompt | |preview |
@@ -280,8 +281,9 @@ impl<T: 'static> Component for FilePicker<T> {
}
}
-pub struct Picker<T> {
+pub struct Picker<T: Item> {
options: Vec<T>,
+ editor_data: T::Data,
// filter: String,
matcher: Box<Matcher>,
/// (index, score)
@@ -299,14 +301,13 @@ pub struct Picker<T> {
/// Whether to truncate the start (default true)
pub truncate_start: bool,
- format_fn: Box<dyn Fn(&T) -> Spans>,
callback_fn: Box<dyn Fn(&mut Context, &T, Action)>,
}
-impl<T> Picker<T> {
+impl<T: Item> Picker<T> {
pub fn new(
options: Vec<T>,
- format_fn: impl Fn(&T) -> Spans + 'static,
+ editor_data: T::Data,
callback_fn: impl Fn(&mut Context, &T, Action) + 'static,
) -> Self {
let prompt = Prompt::new(
@@ -318,6 +319,7 @@ impl<T> Picker<T> {
let mut picker = Self {
options,
+ editor_data,
matcher: Box::new(Matcher::default()),
matches: Vec::new(),
filters: Vec::new(),
@@ -325,7 +327,6 @@ impl<T> Picker<T> {
prompt,
previous_pattern: String::new(),
truncate_start: true,
- format_fn: Box::new(format_fn),
callback_fn: Box::new(callback_fn),
completion_height: 0,
};
@@ -371,9 +372,9 @@ impl<T> Picker<T> {
#[allow(unstable_name_collisions)]
self.matches.retain_mut(|(index, score)| {
let option = &self.options[*index];
- // TODO: maybe using format_fn isn't the best idea here
- let line: String = (self.format_fn)(option).into();
- match self.matcher.fuzzy_match(&line, pattern) {
+ let text = option.sort_text(&self.editor_data);
+
+ match self.matcher.fuzzy_match(&text, pattern) {
Some(s) => {
// Update the score
*score = s;
@@ -399,11 +400,10 @@ impl<T> Picker<T> {
self.filters.binary_search(&index).ok()?;
}
- // TODO: maybe using format_fn isn't the best idea here
- let line: String = (self.format_fn)(option).into();
+ let text = option.filter_text(&self.editor_data);
self.matcher
- .fuzzy_match(&line, pattern)
+ .fuzzy_match(&text, pattern)
.map(|score| (index, score))
}),
);
@@ -477,7 +477,7 @@ impl<T> Picker<T> {
// - on input change:
// - score all the names in relation to input
-impl<T: 'static> Component for Picker<T> {
+impl<T: Item + 'static> Component for Picker<T> {
fn required_size(&mut self, viewport: (u16, u16)) -> Option<(u16, u16)> {
self.completion_height = viewport.1.saturating_sub(4);
Some(viewport)
@@ -610,7 +610,7 @@ impl<T: 'static> Component for Picker<T> {
surface.set_string(inner.x.saturating_sub(2), inner.y + i as u16, ">", selected);
}
- let spans = (self.format_fn)(option);
+ let spans = option.label(&self.editor_data);
let (_score, highlights) = self
.matcher
.fuzzy_indices(&String::from(&spans), self.prompt.line())