From 7c75ec04e8db825f24f311d0c26109310738183d Mon Sep 17 00:00:00 2001 From: Blaž Hrastnik Date: Thu, 17 Dec 2020 18:08:16 +0900 Subject: File picker mockup, reuses the line editor work done on Prompt. --- helix-term/Cargo.toml | 4 + helix-term/src/commands.rs | 8 +- helix-term/src/keymap.rs | 2 + helix-term/src/ui/helix.log | 0 helix-term/src/ui/mod.rs | 2 + helix-term/src/ui/picker.rs | 205 ++++++++++++++++++++++++++++++++++++++++++++ helix-term/src/ui/prompt.rs | 10 ++- 7 files changed, 228 insertions(+), 3 deletions(-) create mode 100644 helix-term/src/ui/helix.log create mode 100644 helix-term/src/ui/picker.rs (limited to 'helix-term') diff --git a/helix-term/Cargo.toml b/helix-term/Cargo.toml index b8eea7c2..645ed155 100644 --- a/helix-term/Cargo.toml +++ b/helix-term/Cargo.toml @@ -32,3 +32,7 @@ futures-util = "0.3" fern = "0.6" chrono = "0.4" log = "0.4" + +# File picker +fuzzy-matcher = "0.3" +ignore = "0.4" diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index cdd2ad34..4b246721 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -10,7 +10,7 @@ use helix_core::{ use once_cell::sync::Lazy; use crate::compositor::Compositor; -use crate::ui::{Prompt, PromptEvent}; +use crate::ui::{self, Prompt, PromptEvent}; use helix_view::{ document::Mode, @@ -456,6 +456,12 @@ pub fn command_mode(cx: &mut Context) { compositor.push(Box::new(prompt)); })); } +pub fn file_picker(cx: &mut Context) { + cx.callback = Some(Box::new(|compositor: &mut Compositor| { + let picker = ui::Picker::new(); + compositor.push(Box::new(picker)); + })); +} // calculate line numbers for each selection range fn selection_lines(state: &State) -> Vec { diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs index 884c04a1..a31676e4 100644 --- a/helix-term/src/keymap.rs +++ b/helix-term/src/keymap.rs @@ -183,6 +183,8 @@ pub fn default() -> Keymaps { }] => commands::page_down, vec![ctrl!('u')] => commands::half_page_up, vec![ctrl!('d')] => commands::half_page_down, + + vec![ctrl!('p')] => commands::file_picker, ), Mode::Insert => hashmap!( vec![Key { diff --git a/helix-term/src/ui/helix.log b/helix-term/src/ui/helix.log new file mode 100644 index 00000000..e69de29b diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index 9a70d1bd..cb79a1d1 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -1,7 +1,9 @@ mod editor; +mod picker; mod prompt; pub use editor::EditorView; +pub use picker::Picker; pub use prompt::{Prompt, PromptEvent}; pub use tui::layout::Rect; diff --git a/helix-term/src/ui/picker.rs b/helix-term/src/ui/picker.rs new file mode 100644 index 00000000..5046ef74 --- /dev/null +++ b/helix-term/src/ui/picker.rs @@ -0,0 +1,205 @@ +use crate::compositor::{Component, Compositor, Context, EventResult}; +use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers}; +use tui::buffer::Buffer as Surface; +use tui::{ + layout::Rect, + style::{Color, Style}, + widgets::{Block, Borders}, +}; + +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, + // filter: String, + matcher: Box, + + cursor: usize, + // pattern: String, + prompt: Prompt, +} + +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, + }); + + let prompt = Prompt::new( + "".to_string(), + |pattern: &str| Vec::new(), + |editor: &mut Editor, pattern: &str, event: PromptEvent| { + // + }, + ); + + const MAX: usize = 1024; + + Self { + files: files.take(MAX).collect(), + matcher: Box::new(Matcher::default()), + cursor: 0, + prompt, + } + } + + pub fn score(&mut self, pattern: &str) { + self.files.iter().filter_map(|path| match path.to_str() { + // TODO: using fuzzy_indices could give us the char idx for match highlighting + Some(path) => (self.matcher.fuzzy_match(path, pattern)), + None => None, + }); + } + + pub fn move_up(&mut self) { + self.cursor = self.cursor.saturating_sub(1); + } + + pub fn move_down(&mut self) { + // TODO: len - 1 + if self.cursor < self.files.len() { + self.cursor += 1; + } + } +} + +// process: +// - read all the files into a list, maxed out at a large value +// - on input change: +// - score all the names in relation to input + +impl Component for Picker { + fn handle_event(&mut self, event: Event, cx: &mut Context) -> EventResult { + let key_event = match event { + Event::Key(event) => event, + Event::Resize(..) => return EventResult::Consumed(None), + _ => return EventResult::Ignored, + }; + + let close_fn = EventResult::Consumed(Some(Box::new(|compositor: &mut Compositor| { + // remove the layer + compositor.pop(); + }))); + + match key_event { + // KeyEvent { + // code: KeyCode::Char(c), + // modifiers: KeyModifiers::NONE, + // } => { + // self.insert_char(c); + // (self.callback_fn)(cx.editor, &self.line, PromptEvent::Update); + // } + KeyEvent { + code: KeyCode::Up, .. + } + | KeyEvent { + code: KeyCode::Char('k'), + modifiers: KeyModifiers::CONTROL, + } => self.move_up(), + KeyEvent { + code: KeyCode::Down, + .. + } + | KeyEvent { + code: KeyCode::Char('j'), + modifiers: KeyModifiers::CONTROL, + } => self.move_down(), + KeyEvent { + code: KeyCode::Esc, .. + } => { + return close_fn; + } + _ => return self.prompt.handle_event(event, cx), + } + + EventResult::Consumed(None) + } + + fn render(&self, area: Rect, surface: &mut Surface, cx: &mut Context) { + let padding_vertical = area.height * 20 / 100; + let padding_horizontal = area.width * 20 / 100; + + let area = Rect::new( + area.x + padding_horizontal, + area.y + padding_vertical, + area.width - padding_horizontal * 2, + area.height - padding_vertical * 2, + ); + + // -- Render the frame: + + // clear area + for y in area.top()..area.bottom() { + for x in area.left()..area.right() { + surface.get_mut(x, y).reset() + } + } + + use tui::widgets::Widget; + // don't like this but the lifetime sucks + let block = Block::default().borders(Borders::ALL); + + // calculate the inner area inside the box + let inner = block.inner(area); + + block.render(area, surface); + // TODO: abstract into a clear(area) fn + // surface.set_style(inner, Style::default().bg(Color::Rgb(150, 50, 0))); + + // -- Render the input bar: + + let area = Rect::new(inner.x + 1, inner.y, inner.width - 1, 1); + self.prompt.render(area, surface, cx); + + // -- Separator + use tui::widgets::BorderType; + let style = Style::default().fg(Color::Rgb(90, 89, 119)); + let symbols = BorderType::line_symbols(BorderType::Plain); + for x in inner.left()..inner.right() { + surface + .get_mut(x, inner.y + 1) + .set_symbol(symbols.horizontal) + .set_style(style); + } + + // -- Render the contents: + + let style = Style::default().fg(Color::Rgb(164, 160, 232)); // lavender + let selected = Style::default().fg(Color::Rgb(255, 255, 255)); + + let rows = inner.height - 2; // -1 for search bar + for (i, file) in self.files.iter().take(rows as usize).enumerate() { + if i == self.cursor { + surface.set_string(inner.x + 1, inner.y + 2 + i as u16, ">", selected); + } + + surface.set_stringn( + inner.x + 3, + inner.y + 2 + i as u16, + file.strip_prefix("./").unwrap().to_str().unwrap(), // TODO: render paths without ./ + inner.width as usize - 1, + if i == self.cursor { selected } else { style }, + ); + } + } + + fn cursor_position(&self, area: Rect, ctx: &mut Context) -> Option { + self.prompt.cursor_position(area, ctx) + } +} diff --git a/helix-term/src/ui/prompt.rs b/helix-term/src/ui/prompt.rs index 07c0f917..58efd560 100644 --- a/helix-term/src/ui/prompt.rs +++ b/helix-term/src/ui/prompt.rs @@ -149,8 +149,13 @@ impl Prompt { } let line = area.height - 1; // render buffer text - surface.set_string(0, line, &self.prompt, text_color); - surface.set_string(self.prompt.len() as u16, line, &self.line, text_color); + surface.set_string(area.x, area.y + line, &self.prompt, text_color); + surface.set_string( + area.x + self.prompt.len() as u16, + area.y + line, + &self.line, + text_color, + ); } } @@ -158,6 +163,7 @@ impl Component for Prompt { fn handle_event(&mut self, event: Event, cx: &mut Context) -> EventResult { let event = match event { Event::Key(event) => event, + Event::Resize(..) => return EventResult::Consumed(None), _ => return EventResult::Ignored, }; -- cgit v1.2.3-70-g09d2