From ed6a4c4bd240630355d1c61a1c05c4e22bf62a86 Mon Sep 17 00:00:00 2001 From: Blaž Hrastnik Date: Mon, 14 Dec 2020 14:12:54 +0900 Subject: wip: Use prompt for interactive commands. --- helix-term/src/commands.rs | 33 +++++++++++++++++++++++++++++++++ helix-term/src/keymap.rs | 1 + helix-term/src/ui/prompt.rs | 6 +----- 3 files changed, 35 insertions(+), 5 deletions(-) (limited to 'helix-term/src') diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index b345d2e8..7001aa11 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -248,6 +248,39 @@ pub fn extend_line_down(cx: &mut Context) { cx.view.doc.set_selection(selection); } +pub fn split_selection(cx: &mut Context) { + // TODO: this needs to store initial selection state, revert on esc, confirm on enter + // needs to also call the callback function per input change, not just final time. + // could cheat and put it into completion_fn + // + // kakoune does it like this: + // # save state to register + // { + // # restore state from register + // # if event == abort, return early + // # add to history if enabled + // # update state + // } + + let prompt = Prompt::new( + "split:".to_string(), + |input: &str| Vec::new(), // TODO: use Option here? + |editor: &mut Editor, input: &str| { + match Regex::new(input) { + Ok(regex) => { + let view = editor.view_mut().unwrap(); + let text = &view.doc.text().slice(..); + let selection = selection::split_on_matches(text, view.doc.selection(), ®ex); + view.doc.set_selection(selection); + } + Err(_) => (), // TODO: mark command line as error + } + }, + ); + + unimplemented!() +} + pub fn split_selection_on_newline(cx: &mut Context) { let text = &cx.view.doc.text().slice(..); // only compile the regex once diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs index af46f7a4..304a173c 100644 --- a/helix-term/src/keymap.rs +++ b/helix-term/src/keymap.rs @@ -156,6 +156,7 @@ pub fn default() -> Keymaps { vec![key!('o')] => commands::open_below, vec![key!('d')] => commands::delete_selection, vec![key!('c')] => commands::change_selection, + vec![shift!('S')] => commands::split_selection, vec![key!('s')] => commands::split_selection_on_newline, vec![key!(';')] => commands::collapse_selection, // TODO should be alt(;) diff --git a/helix-term/src/ui/prompt.rs b/helix-term/src/ui/prompt.rs index ce00a129..d1d92975 100644 --- a/helix-term/src/ui/prompt.rs +++ b/helix-term/src/ui/prompt.rs @@ -10,7 +10,6 @@ pub struct Prompt { pub line: String, pub cursor: usize, pub completion: Vec, - pub should_close: bool, pub completion_selection_index: Option, completion_fn: Box Vec>, callback_fn: Box, @@ -27,7 +26,6 @@ impl Prompt { line: String::new(), cursor: 0, completion: completion_fn(""), - should_close: false, completion_selection_index: None, completion_fn: Box::new(completion_fn), callback_fn: Box::new(callback_fn), @@ -42,9 +40,7 @@ impl Prompt { } pub fn move_char_left(&mut self) { - if self.cursor > 0 { - self.cursor -= 1; - } + self.cursor = self.cursor.saturating_sub(1) } pub fn move_char_right(&mut self) { -- cgit v1.2.3-70-g09d2 From ca809617b2fb38e70910d30feefabbb21ec0ad7f Mon Sep 17 00:00:00 2001 From: Blaž Hrastnik Date: Mon, 14 Dec 2020 15:58:03 +0900 Subject: Take prompt length into account. --- helix-term/src/ui/prompt.rs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) (limited to 'helix-term/src') diff --git a/helix-term/src/ui/prompt.rs b/helix-term/src/ui/prompt.rs index d1d92975..3ca1a941 100644 --- a/helix-term/src/ui/prompt.rs +++ b/helix-term/src/ui/prompt.rs @@ -137,9 +137,10 @@ impl Prompt { } } } + let line = area.height - 1; // render buffer text - surface.set_string(1, area.height - 1, &self.prompt, text_color); - surface.set_string(2, area.height - 1, &self.line, text_color); + surface.set_string(0, line, &self.prompt, text_color); + surface.set_string(self.prompt.len() as u16, line, &self.line, text_color); } } @@ -206,8 +207,8 @@ impl Component for Prompt { fn cursor_position(&self, area: Rect, ctx: &mut Context) -> Option { Some(Position::new( - area.height as usize - 1, - area.x as usize + 2 + self.cursor, + area.height as usize, + area.x as usize + self.prompt.len() + self.cursor, )) } } -- cgit v1.2.3-70-g09d2 From f92998a14b3017371c6a55756c60c03e126ae23f Mon Sep 17 00:00:00 2001 From: Blaž Hrastnik Date: Mon, 14 Dec 2020 15:59:48 +0900 Subject: Prompt needs to close on enter. --- helix-term/src/ui/prompt.rs | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) (limited to 'helix-term/src') diff --git a/helix-term/src/ui/prompt.rs b/helix-term/src/ui/prompt.rs index 3ca1a941..f5ef9477 100644 --- a/helix-term/src/ui/prompt.rs +++ b/helix-term/src/ui/prompt.rs @@ -151,6 +151,11 @@ impl Component for Prompt { _ => return EventResult::Ignored, }; + let close_fn = EventResult::Consumed(Some(Box::new(|compositor: &mut Compositor| { + // remove the layer + compositor.pop(); + }))); + match event { KeyEvent { code: KeyCode::Char(c), @@ -159,10 +164,7 @@ impl Component for Prompt { KeyEvent { code: KeyCode::Esc, .. } => { - return EventResult::Consumed(Some(Box::new(|compositor: &mut Compositor| { - // remove the layer - compositor.pop(); - }))); + return close_fn; } KeyEvent { code: KeyCode::Right, @@ -187,7 +189,10 @@ impl Component for Prompt { KeyEvent { code: KeyCode::Enter, .. - } => (self.callback_fn)(cx.editor, &self.line), + } => { + (self.callback_fn)(cx.editor, &self.line); + return close_fn; + } KeyEvent { code: KeyCode::Tab, .. } => self.change_completion_selection(), -- cgit v1.2.3-70-g09d2 From 2bfdcede32586af01e1e7cda686a48525e0154bd Mon Sep 17 00:00:00 2001 From: Blaž Hrastnik Date: Mon, 14 Dec 2020 16:02:07 +0900 Subject: split_selection --- helix-term/src/commands.rs | 33 ++++++++++++++++++--------------- helix-term/src/keymap.rs | 2 +- 2 files changed, 19 insertions(+), 16 deletions(-) (limited to 'helix-term/src') diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 7001aa11..3db112d3 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -262,23 +262,26 @@ pub fn split_selection(cx: &mut Context) { // # update state // } - let prompt = Prompt::new( - "split:".to_string(), - |input: &str| Vec::new(), // TODO: use Option here? - |editor: &mut Editor, input: &str| { - match Regex::new(input) { - Ok(regex) => { - let view = editor.view_mut().unwrap(); - let text = &view.doc.text().slice(..); - let selection = selection::split_on_matches(text, view.doc.selection(), ®ex); - view.doc.set_selection(selection); + cx.callback = Some(Box::new(|compositor: &mut Compositor| { + let prompt = Prompt::new( + "split:".to_string(), + |input: &str| Vec::new(), // this is fine because Vec::new() doesn't allocate + |editor: &mut Editor, input: &str| { + match Regex::new(input) { + Ok(regex) => { + let view = editor.view_mut().unwrap(); + let text = &view.doc.text().slice(..); + let selection = + selection::split_on_matches(text, view.doc.selection(), ®ex); + view.doc.set_selection(selection); + } + Err(_) => (), // TODO: mark command line as error } - Err(_) => (), // TODO: mark command line as error - } - }, - ); + }, + ); - unimplemented!() + compositor.push(Box::new(prompt)); + })); } pub fn split_selection_on_newline(cx: &mut Context) { diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs index 304a173c..884c04a1 100644 --- a/helix-term/src/keymap.rs +++ b/helix-term/src/keymap.rs @@ -156,8 +156,8 @@ pub fn default() -> Keymaps { vec![key!('o')] => commands::open_below, vec![key!('d')] => commands::delete_selection, vec![key!('c')] => commands::change_selection, - vec![shift!('S')] => commands::split_selection, vec![key!('s')] => commands::split_selection_on_newline, + vec![shift!('S')] => commands::split_selection, vec![key!(';')] => commands::collapse_selection, // TODO should be alt(;) vec![key!('%')] => commands::flip_selections, -- cgit v1.2.3-70-g09d2 From 1a843b6c06b5d4d75feb0a424bbfdcfb33ab7651 Mon Sep 17 00:00:00 2001 From: Blaž Hrastnik Date: Tue, 15 Dec 2020 19:07:25 +0900 Subject: prompt: make the callback a FnOnce. --- helix-term/src/commands.rs | 31 +++++++++++++++---------------- helix-term/src/compositor.rs | 2 +- 2 files changed, 16 insertions(+), 17 deletions(-) (limited to 'helix-term/src') diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 3db112d3..e49c780a 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -262,24 +262,23 @@ pub fn split_selection(cx: &mut Context) { // # update state // } - cx.callback = Some(Box::new(|compositor: &mut Compositor| { - let prompt = Prompt::new( - "split:".to_string(), - |input: &str| Vec::new(), // this is fine because Vec::new() doesn't allocate - |editor: &mut Editor, input: &str| { - match Regex::new(input) { - Ok(regex) => { - let view = editor.view_mut().unwrap(); - let text = &view.doc.text().slice(..); - let selection = - selection::split_on_matches(text, view.doc.selection(), ®ex); - view.doc.set_selection(selection); - } - Err(_) => (), // TODO: mark command line as error + let prompt = Prompt::new( + "split:".to_string(), + |input: &str| Vec::new(), // this is fine because Vec::new() doesn't allocate + |editor: &mut Editor, input: &str| { + match Regex::new(input) { + Ok(regex) => { + let view = editor.view_mut().unwrap(); + let text = &view.doc.text().slice(..); + let selection = selection::split_on_matches(text, view.doc.selection(), ®ex); + view.doc.set_selection(selection); } - }, - ); + Err(_) => (), // TODO: mark command line as error + } + }, + ); + cx.callback = Some(Box::new(move |compositor: &mut Compositor| { compositor.push(Box::new(prompt)); })); } diff --git a/helix-term/src/compositor.rs b/helix-term/src/compositor.rs index 2e65f02a..f0d94dbc 100644 --- a/helix-term/src/compositor.rs +++ b/helix-term/src/compositor.rs @@ -19,7 +19,7 @@ use smol::Executor; use tui::buffer::Buffer as Surface; use tui::layout::Rect; -pub type Callback = Box; +pub type Callback = Box; // --> EventResult should have a callback that takes a context with methods like .popup(), // .prompt() etc. That way we can abstract it from the renderer. -- cgit v1.2.3-70-g09d2 From 8f0b28aeb872797e4be3f07575e628f5f93e74e0 Mon Sep 17 00:00:00 2001 From: Blaž Hrastnik Date: Tue, 15 Dec 2020 19:29:56 +0900 Subject: Make the select prompt interactive. --- helix-term/src/commands.rs | 47 ++++++++++++++++++++++++++++++++++----------- helix-term/src/ui/mod.rs | 2 +- helix-term/src/ui/prompt.rs | 27 +++++++++++++++++++++----- 3 files changed, 59 insertions(+), 17 deletions(-) (limited to 'helix-term/src') diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index e49c780a..462d2a20 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; +use crate::ui::{Prompt, PromptEvent}; use helix_view::{ document::Mode, @@ -262,18 +262,37 @@ pub fn split_selection(cx: &mut Context) { // # update state // } + let snapshot = cx.view.doc.state.clone(); + let prompt = Prompt::new( "split:".to_string(), |input: &str| Vec::new(), // this is fine because Vec::new() doesn't allocate - |editor: &mut Editor, input: &str| { - match Regex::new(input) { - Ok(regex) => { + move |editor: &mut Editor, input: &str, event: PromptEvent| { + match event { + PromptEvent::Abort => { + // revert state let view = editor.view_mut().unwrap(); - let text = &view.doc.text().slice(..); - let selection = selection::split_on_matches(text, view.doc.selection(), ®ex); - view.doc.set_selection(selection); + view.doc.state = snapshot.clone(); + } + PromptEvent::Validate => { + // + } + PromptEvent::Update => { + match Regex::new(input) { + Ok(regex) => { + let view = editor.view_mut().unwrap(); + + // revert state to what it was before the last update + view.doc.state = snapshot.clone(); + + let text = &view.doc.text().slice(..); + let selection = + selection::split_on_matches(text, view.doc.selection(), ®ex); + view.doc.set_selection(selection); + } + Err(_) => (), // TODO: mark command line as error + } } - Err(_) => (), // TODO: mark command line as error } }, ); @@ -416,9 +435,15 @@ pub fn command_mode(cx: &mut Context) { .filter(|command| command.contains(_input)) .collect() }, // completion - |editor: &mut Editor, input: &str| match input { - "q" => editor.should_close = true, - _ => (), + |editor: &mut Editor, input: &str, event: PromptEvent| { + if event != PromptEvent::Validate { + return; + } + + match input { + "q" => editor.should_close = true, + _ => (), + } }, ); compositor.push(Box::new(prompt)); diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index bc79e09c..9a70d1bd 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -2,7 +2,7 @@ mod editor; mod prompt; pub use editor::EditorView; -pub use prompt::Prompt; +pub use prompt::{Prompt, PromptEvent}; pub use tui::layout::Rect; pub use tui::style::{Color, Modifier, Style}; diff --git a/helix-term/src/ui/prompt.rs b/helix-term/src/ui/prompt.rs index f5ef9477..07c0f917 100644 --- a/helix-term/src/ui/prompt.rs +++ b/helix-term/src/ui/prompt.rs @@ -12,14 +12,24 @@ pub struct Prompt { pub completion: Vec, pub completion_selection_index: Option, completion_fn: Box Vec>, - callback_fn: Box, + callback_fn: Box, +} + +#[derive(PartialEq)] +pub enum PromptEvent { + /// The prompt input has been updated. + Update, + /// Validate and finalize the change. + Validate, + /// Abort the change, reverting to the initial state. + Abort, } impl Prompt { pub fn new( prompt: String, mut completion_fn: impl FnMut(&str) -> Vec + 'static, - callback_fn: impl FnMut(&mut Editor, &str) + 'static, + callback_fn: impl FnMut(&mut Editor, &str, PromptEvent) + 'static, ) -> Prompt { Prompt { prompt, @@ -160,10 +170,14 @@ impl Component for Prompt { KeyEvent { code: KeyCode::Char(c), modifiers: KeyModifiers::NONE, - } => self.insert_char(c), + } => { + self.insert_char(c); + (self.callback_fn)(cx.editor, &self.line, PromptEvent::Update); + } KeyEvent { code: KeyCode::Esc, .. } => { + (self.callback_fn)(cx.editor, &self.line, PromptEvent::Abort); return close_fn; } KeyEvent { @@ -185,12 +199,15 @@ impl Component for Prompt { KeyEvent { code: KeyCode::Backspace, modifiers: KeyModifiers::NONE, - } => self.delete_char_backwards(), + } => { + self.delete_char_backwards(); + (self.callback_fn)(cx.editor, &self.line, PromptEvent::Update); + } KeyEvent { code: KeyCode::Enter, .. } => { - (self.callback_fn)(cx.editor, &self.line); + (self.callback_fn)(cx.editor, &self.line, PromptEvent::Validate); return close_fn; } KeyEvent { -- cgit v1.2.3-70-g09d2 From ed60866c5485c8f7e8d2e12fd34dd8243d079b2a Mon Sep 17 00:00:00 2001 From: Blaž Hrastnik Date: Wed, 16 Dec 2020 16:34:12 +0900 Subject: Add an :o open command. --- helix-term/src/commands.rs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) (limited to 'helix-term/src') diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 462d2a20..cdd2ad34 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -440,8 +440,15 @@ pub fn command_mode(cx: &mut Context) { return; } - match input { - "q" => editor.should_close = true, + let parts = input.split_ascii_whitespace().collect::>(); + + match parts.as_slice() { + &["q"] => editor.should_close = true, + &["o", path] => { + // TODO: make view()/view_mut() always contain a view. + let size = editor.view().unwrap().size; + editor.open(path.into(), size); + } _ => (), } }, -- cgit v1.2.3-70-g09d2 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. --- Cargo.lock | 86 +++++++++++++++++++ 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 ++- 8 files changed, 314 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/src') diff --git a/Cargo.lock b/Cargo.lock index 1c8c86c7..3f64bcdf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -181,6 +181,15 @@ dependencies = [ "once_cell", ] +[[package]] +name = "bstr" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "473fc6b38233f9af7baa94fb5852dca389e3d95b8e21c8e3719301462c5d9faf" +dependencies = [ + "memchr", +] + [[package]] name = "cache-padded" version = "1.1.1" @@ -339,6 +348,12 @@ dependencies = [ "log", ] +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + [[package]] name = "form_urlencoded" version = "1.0.0" @@ -429,6 +444,15 @@ dependencies = [ "slab", ] +[[package]] +name = "fuzzy-matcher" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54614a3312934d066701a80f20f15fa3b56d67ac7722b39eea5b4c9dd1d66c94" +dependencies = [ + "thread_local", +] + [[package]] name = "getrandom" version = "0.1.15" @@ -446,6 +470,19 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" +[[package]] +name = "globset" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c152169ef1e421390738366d2f796655fec62621dabbd0fd476f905934061e4a" +dependencies = [ + "aho-corasick", + "bstr", + "fnv", + "log", + "regex", +] + [[package]] name = "hashbrown" version = "0.9.1" @@ -508,9 +545,11 @@ dependencies = [ "crossterm", "fern", "futures-util", + "fuzzy-matcher", "helix-core", "helix-lsp", "helix-view", + "ignore", "log", "num_cpus", "once_cell", @@ -551,6 +590,24 @@ dependencies = [ "unicode-normalization", ] +[[package]] +name = "ignore" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b287fb45c60bb826a0dc68ff08742b9d88a2fea13d6e0c286b3172065aaf878c" +dependencies = [ + "crossbeam-utils", + "globset", + "lazy_static", + "log", + "memchr", + "regex", + "same-file", + "thread_local", + "walkdir", + "winapi-util", +] + [[package]] name = "indexmap" version = "1.6.0" @@ -929,6 +986,15 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + [[package]] name = "scopeguard" version = "1.1.0" @@ -1228,6 +1294,17 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d5b2c62b4012a3e1eca5a7e077d13b3bf498c4073e33ccd58626607748ceeca" +[[package]] +name = "walkdir" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "777182bc735b6424e1a57516d35ed72cb8019d85c8c9bf536dccb3445c1a2f7d" +dependencies = [ + "same-file", + "winapi", + "winapi-util", +] + [[package]] name = "wasi" version = "0.9.0+wasi-snapshot-preview1" @@ -1265,6 +1342,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" 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 From edfd3933dbdfe30e05533beff625e0ddd446f41f Mon Sep 17 00:00:00 2001 From: Blaž Hrastnik Date: Fri, 18 Dec 2020 16:43:15 +0900 Subject: picker: Implement fuzzy search. --- helix-term/src/ui/picker.rs | 67 ++++++++++++++++++++++++++++++++++++++------- 1 file changed, 57 insertions(+), 10 deletions(-) (limited to 'helix-term/src') diff --git a/helix-term/src/ui/picker.rs b/helix-term/src/ui/picker.rs index 5046ef74..82dbdd2e 100644 --- a/helix-term/src/ui/picker.rs +++ b/helix-term/src/ui/picker.rs @@ -21,6 +21,8 @@ pub struct Picker { files: Vec, // filter: String, matcher: Box, + /// (index, score) + matches: Vec<(usize, i64)>, cursor: usize, // pattern: String, @@ -51,20 +53,46 @@ impl Picker { const MAX: usize = 1024; - Self { + let mut picker = Self { files: files.take(MAX).collect(), matcher: Box::new(Matcher::default()), + matches: Vec::new(), cursor: 0, prompt, - } + }; + + // TODO: scoring on empty input should just use a fastpath + picker.score(); + + picker } - 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 score(&mut self) { + // need to borrow via pattern match otherwise it complains about simultaneous borrow + let Self { + ref mut files, + ref mut matcher, + ref mut matches, + .. + } = *self; + + let pattern = &self.prompt.line; + + // 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.sort_unstable_by_key(|(_, score)| -score); + + // reset cursor position + self.cursor = 0; } pub fn move_up(&mut self) { @@ -77,6 +105,12 @@ impl Picker { self.cursor += 1; } } + + pub fn selection(&self) -> Option<&PathBuf> { + self.matches + .get(self.cursor) + .map(|(index, _score)| &self.files[*index]) + } } // process: @@ -125,7 +159,15 @@ impl Component for Picker { } => { return close_fn; } - _ => return self.prompt.handle_event(event, cx), + _ => { + match self.prompt.handle_event(event, cx) { + EventResult::Consumed(_) => { + // TODO: recalculate only if pattern changed + self.score(); + } + _ => (), + } + } } EventResult::Consumed(None) @@ -184,7 +226,12 @@ impl Component for Picker { 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() { + + let files = self.matches.iter().map(|(index, _score)| { + (index, self.files.get(*index).unwrap()) // get_unchecked + }); + + for (i, (_index, file)) in files.take(rows as usize).enumerate() { if i == self.cursor { surface.set_string(inner.x + 1, inner.y + 2 + i as u16, ">", selected); } -- cgit v1.2.3-70-g09d2 From 8b263ef04b8a1399727c41b4e2f4017fc0be9316 Mon Sep 17 00:00:00 2001 From: Blaž Hrastnik Date: Fri, 18 Dec 2020 17:16:04 +0900 Subject: picker: open file on Enter. --- helix-term/src/ui/picker.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) (limited to 'helix-term/src') diff --git a/helix-term/src/ui/picker.rs b/helix-term/src/ui/picker.rs index 82dbdd2e..3a3c648f 100644 --- a/helix-term/src/ui/picker.rs +++ b/helix-term/src/ui/picker.rs @@ -159,6 +159,16 @@ impl Component for Picker { } => { return close_fn; } + KeyEvent { + code: KeyCode::Enter, + .. + } => { + let size = cx.editor.view().unwrap().size; + if let Some(path) = self.selection() { + cx.editor.open(path.into(), size); + } + return close_fn; + } _ => { match self.prompt.handle_event(event, cx) { EventResult::Consumed(_) => { -- cgit v1.2.3-70-g09d2 From 0b63e838e0ffe9a36a2cb51790f464d634b843af Mon Sep 17 00:00:00 2001 From: Blaž Hrastnik Date: Fri, 18 Dec 2020 17:18:11 +0900 Subject: Port over Doc::relative_path. --- helix-term/src/ui/editor.rs | 2 +- helix-view/src/document.rs | 9 ++++++++- 2 files changed, 9 insertions(+), 2 deletions(-) (limited to 'helix-term/src') diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index ceb5a442..996e182f 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -226,7 +226,7 @@ impl EditorView { ); surface.set_string(1, viewport.y, mode, text_color); - if let Some(path) = view.doc.path() { + if let Some(path) = view.doc.relative_path() { surface.set_string(6, viewport.y, path.to_string_lossy(), text_color); } diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs index 7c4596ad..323c7bff 100644 --- a/helix-view/src/document.rs +++ b/helix-view/src/document.rs @@ -1,6 +1,6 @@ use anyhow::Error; use std::future::Future; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use helix_core::{ syntax::LOADER, ChangeSet, Diagnostic, History, Position, Range, Rope, RopeSlice, Selection, @@ -201,6 +201,13 @@ impl Document { &self.state.selection } + pub fn relative_path(&self) -> Option<&Path> { + self.path.as_ref().map(|path| { + path.strip_prefix(std::env::current_dir().unwrap()) + .unwrap_or(path) + }) + } + // pub fn slice(&self, range: R) -> RopeSlice where R: RangeBounds { // self.state.doc.slice // } -- cgit v1.2.3-70-g09d2 From 25aa45e76c9bec62f36a59768298e1f2ea2678bf Mon Sep 17 00:00:00 2001 From: Blaž Hrastnik Date: Fri, 18 Dec 2020 19:19:50 +0900 Subject: picker: Factor out file picker, we want to reuse code for other pickers. --- helix-term/src/commands.rs | 2 +- helix-term/src/ui/mod.rs | 32 ++++++++++++++++++ helix-term/src/ui/picker.rs | 80 +++++++++++++++++++++------------------------ 3 files changed, 71 insertions(+), 43 deletions(-) (limited to 'helix-term/src') diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 4b246721..5f8f63f1 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -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)); })); } diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index cb79a1d1..b778f531 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -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 { + 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); + }, + ) +} diff --git a/helix-term/src/ui/picker.rs b/helix-term/src/ui/picker.rs index 3a3c648f..0a12cff9 100644 --- a/helix-term/src/ui/picker.rs +++ b/helix-term/src/ui/picker.rs @@ -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, +pub struct Picker { + options: Vec, // filter: String, matcher: Box, /// (index, score) @@ -27,22 +24,17 @@ pub struct Picker { 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, - }); + format_fn: Box &str>, + callback_fn: Box, +} +impl Picker { + pub fn new( + options: Vec, + 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 Component for Picker { 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 }, ); -- cgit v1.2.3-70-g09d2