picker: Factor out file picker, we want to reuse code for other pickers.

This commit is contained in:
Blaž Hrastnik 2020-12-18 19:19:50 +09:00
parent 0b63e838e0
commit 25aa45e76c
3 changed files with 71 additions and 43 deletions

View File

@ -458,7 +458,7 @@ pub fn command_mode(cx: &mut Context) {
}
pub fn file_picker(cx: &mut Context) {
cx.callback = Some(Box::new(|compositor: &mut Compositor| {
let picker = ui::Picker::new();
let picker = ui::file_picker("./");
compositor.push(Box::new(picker));
}));
}

View File

@ -14,3 +14,35 @@ pub use tui::style::{Color, Modifier, Style};
pub fn text_color() -> Style {
Style::default().fg(Color::Rgb(219, 191, 239)) // lilac
}
use std::path::PathBuf;
pub fn file_picker(root: &str) -> Picker<PathBuf> {
use ignore::Walk;
// TODO: determine root based on git root
let files = Walk::new(root).filter_map(|entry| match entry {
Ok(entry) => {
// filter dirs, but we might need special handling for symlinks!
if !entry.file_type().unwrap().is_dir() {
Some(entry.into_path())
} else {
None
}
}
Err(_err) => None,
});
const MAX: usize = 1024;
use helix_view::Editor;
Picker::new(
files.take(MAX).collect(),
|path: &PathBuf| {
// format_fn
path.strip_prefix("./").unwrap().to_str().unwrap() // TODO: render paths without ./
},
|editor: &mut Editor, path: &PathBuf| {
let size = editor.view().unwrap().size;
editor.open(path.into(), size);
},
)
}

View File

@ -9,16 +9,13 @@ use tui::{
use fuzzy_matcher::skim::SkimMatcherV2 as Matcher;
use fuzzy_matcher::FuzzyMatcher;
use ignore::Walk;
use std::path::PathBuf;
use crate::ui::{Prompt, PromptEvent};
use helix_core::Position;
use helix_view::Editor;
pub struct Picker {
files: Vec<PathBuf>,
pub struct Picker<T> {
options: Vec<T>,
// filter: String,
matcher: Box<Matcher>,
/// (index, score)
@ -27,22 +24,17 @@ pub struct Picker {
cursor: usize,
// pattern: String,
prompt: Prompt,
format_fn: Box<dyn Fn(&T) -> &str>,
callback_fn: Box<dyn Fn(&mut Editor, &T)>,
}
impl Picker {
pub fn new() -> Self {
let files = Walk::new("./").filter_map(|entry| match entry {
Ok(entry) => {
// filter dirs, but we might need special handling for symlinks!
if !entry.file_type().unwrap().is_dir() {
Some(entry.into_path())
} else {
None
}
}
Err(_err) => None,
});
impl<T> Picker<T> {
pub fn new(
options: Vec<T>,
format_fn: impl Fn(&T) -> &str + 'static,
callback_fn: impl Fn(&mut Editor, &T) + 'static,
) -> Self {
let prompt = Prompt::new(
"".to_string(),
|pattern: &str| Vec::new(),
@ -51,14 +43,14 @@ impl Picker {
},
);
const MAX: usize = 1024;
let mut picker = Self {
files: files.take(MAX).collect(),
options,
matcher: Box::new(Matcher::default()),
matches: Vec::new(),
cursor: 0,
prompt,
format_fn: Box::new(format_fn),
callback_fn: Box::new(callback_fn),
};
// TODO: scoring on empty input should just use a fastpath
@ -70,9 +62,10 @@ impl Picker {
pub fn score(&mut self) {
// need to borrow via pattern match otherwise it complains about simultaneous borrow
let Self {
ref mut files,
ref mut options,
ref mut matcher,
ref mut matches,
ref format_fn,
..
} = *self;
@ -80,15 +73,19 @@ impl Picker {
// reuse the matches allocation
matches.clear();
matches.extend(self.files.iter().enumerate().filter_map(|(index, path)| {
match path.to_str() {
// TODO: using fuzzy_indices could give us the char idx for match highlighting
Some(path) => matcher
.fuzzy_match(path, pattern)
.map(|score| (index, score)),
None => None,
}
}));
matches.extend(
self.options
.iter()
.enumerate()
.filter_map(|(index, option)| {
// TODO: maybe using format_fn isn't the best idea here
let text = (format_fn)(option);
// TODO: using fuzzy_indices could give us the char idx for match highlighting
matcher
.fuzzy_match(text, pattern)
.map(|score| (index, score))
}),
);
matches.sort_unstable_by_key(|(_, score)| -score);
// reset cursor position
@ -101,15 +98,15 @@ impl Picker {
pub fn move_down(&mut self) {
// TODO: len - 1
if self.cursor < self.files.len() {
if self.cursor < self.options.len() {
self.cursor += 1;
}
}
pub fn selection(&self) -> Option<&PathBuf> {
pub fn selection(&self) -> Option<&T> {
self.matches
.get(self.cursor)
.map(|(index, _score)| &self.files[*index])
.map(|(index, _score)| &self.options[*index])
}
}
@ -118,7 +115,7 @@ impl Picker {
// - on input change:
// - score all the names in relation to input
impl Component for Picker {
impl<T> Component for Picker<T> {
fn handle_event(&mut self, event: Event, cx: &mut Context) -> EventResult {
let key_event = match event {
Event::Key(event) => event,
@ -163,9 +160,8 @@ impl Component for Picker {
code: KeyCode::Enter,
..
} => {
let size = cx.editor.view().unwrap().size;
if let Some(path) = self.selection() {
cx.editor.open(path.into(), size);
if let Some(option) = self.selection() {
(self.callback_fn)(&mut cx.editor, option);
}
return close_fn;
}
@ -238,10 +234,10 @@ impl Component for Picker {
let rows = inner.height - 2; // -1 for search bar
let files = self.matches.iter().map(|(index, _score)| {
(index, self.files.get(*index).unwrap()) // get_unchecked
(index, self.options.get(*index).unwrap()) // get_unchecked
});
for (i, (_index, file)) in files.take(rows as usize).enumerate() {
for (i, (_index, option)) in files.take(rows as usize).enumerate() {
if i == self.cursor {
surface.set_string(inner.x + 1, inner.y + 2 + i as u16, ">", selected);
}
@ -249,7 +245,7 @@ impl Component for Picker {
surface.set_stringn(
inner.x + 3,
inner.y + 2 + i as u16,
file.strip_prefix("./").unwrap().to_str().unwrap(), // TODO: render paths without ./
(self.format_fn)(option),
inner.width as usize - 1,
if i == self.cursor { selected } else { style },
);