diff options
author | Blaž Hrastnik | 2020-12-18 10:24:50 +0000 |
---|---|---|
committer | GitHub | 2020-12-18 10:24:50 +0000 |
commit | 3f0dbfcac878131167953b6f57c923a5bc889e80 (patch) | |
tree | 41b876f9bb067e5f199fe53ecb78eeb57d09719b | |
parent | b12a6dc8303bbc1b4b08a9abb4668741d154adbd (diff) | |
parent | 25aa45e76c9bec62f36a59768298e1f2ea2678bf (diff) |
Merge pull request #7 from helix-editor/interactive-split-select
File picker/interactive split prompt
-rw-r--r-- | Cargo.lock | 149 | ||||
-rw-r--r-- | helix-lsp/Cargo.toml | 9 | ||||
-rw-r--r-- | helix-term/Cargo.toml | 4 | ||||
-rw-r--r-- | helix-term/src/commands.rs | 81 | ||||
-rw-r--r-- | helix-term/src/compositor.rs | 2 | ||||
-rw-r--r-- | helix-term/src/keymap.rs | 3 | ||||
-rw-r--r-- | helix-term/src/ui/editor.rs | 2 | ||||
-rw-r--r-- | helix-term/src/ui/helix.log | 0 | ||||
-rw-r--r-- | helix-term/src/ui/mod.rs | 36 | ||||
-rw-r--r-- | helix-term/src/ui/picker.rs | 258 | ||||
-rw-r--r-- | helix-term/src/ui/prompt.rs | 61 | ||||
-rw-r--r-- | helix-view/src/document.rs | 9 |
12 files changed, 572 insertions, 42 deletions
@@ -182,6 +182,15 @@ dependencies = [ ] [[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" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -340,6 +349,12 @@ dependencies = [ ] [[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" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -361,9 +376,28 @@ dependencies = [ [[package]] name = "futures" -version = "0.1.30" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c7e4c2612746b0df8fed4ce0c69156021b704c9aefa360311c04e6e9e002eed" +checksum = "9b3b0c040a1fe6529d30b3c5944b280c7f0dcb2930d2c3062bca967b602583d0" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b7109687aa4e177ef6fe84553af6280ef2778bdb7783ba44c9dc3399110fe64" +dependencies = [ + "futures-core", + "futures-sink", +] [[package]] name = "futures-core" @@ -372,6 +406,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "847ce131b72ffb13b6109a221da9ad97a64cbe48feb1028356b836b47b8f1748" [[package]] +name = "futures-executor" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4caa2b2b68b880003057c1dd49f1ed937e38f22fcf6c212188a121f08cf40a65" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] name = "futures-io" version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -405,6 +450,12 @@ dependencies = [ ] [[package]] +name = "futures-sink" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f878195a49cee50e006b02b93cf7e0a95a38ac7b776b4c4d9cc1207cd20fcb3d" + +[[package]] name = "futures-task" version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -419,9 +470,13 @@ version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d304cff4a7b99cfb7986f7d43fbe93d175e72e704a8860787cc95e9ffd85cbd2" dependencies = [ + "futures-channel", "futures-core", + "futures-io", "futures-macro", + "futures-sink", "futures-task", + "memchr", "pin-project", "pin-utils", "proc-macro-hack", @@ -430,6 +485,15 @@ dependencies = [ ] [[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" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -447,6 +511,19 @@ 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" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -508,9 +585,11 @@ dependencies = [ "crossterm", "fern", "futures-util", + "fuzzy-matcher", "helix-core", "helix-lsp", "helix-view", + "ignore", "log", "num_cpus", "once_cell", @@ -552,10 +631,28 @@ dependencies = [ ] [[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" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55e2e4c765aa53a0424761bf9f41aa7a6ac1efa87238f59560640e27fca028f2" +checksum = "4fb1fa934250de4de8aef298d81c729a7d33d8c239daa3a7575e6b92bfc7313b" dependencies = [ "autocfg", "hashbrown", @@ -587,9 +684,9 @@ dependencies = [ [[package]] name = "jsonrpc-core" -version = "15.1.0" +version = "16.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0745a6379e3edc893c84ec203589790774e4247420033e71a76d3ab4687991fa" +checksum = "6a47c4c3ac843f9a4238943f97620619033dadef4b378cd1e8addd170de396b3" dependencies = [ "futures", "log", @@ -630,9 +727,9 @@ dependencies = [ [[package]] name = "lsp-types" -version = "0.84.0" +version = "0.85.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b95be71fe205e44de754185bcf86447b65813ce1ceb298f8d3793ade5fff08d" +checksum = "857650f3e83fb62f89d15410414e0ed7d0735445020da398d37f65d20a5423b9" dependencies = [ "base64 0.12.3", "bitflags", @@ -930,6 +1027,15 @@ 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" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1038,13 +1144,12 @@ dependencies = [ [[package]] name = "socket2" -version = "0.3.17" +version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c29947abdee2a218277abeca306f25789c938e500ea5a9d4b12a5a504466902" +checksum = "97e0e9fd577458a4f61fb91fcb559ea2afecc54c934119421f9f5d3d5b1a1057" dependencies = [ "cfg-if 1.0.0", "libc", - "redox_syscall", "winapi", ] @@ -1146,7 +1251,7 @@ dependencies = [ [[package]] name = "tui" version = "0.13.0" -source = "git+https://github.com/fdehau/tui-rs#74243394d90ea1316b6bedac6c9e4f26971c76b6" +source = "git+https://github.com/fdehau/tui-rs#eb1e3be7228509e42cbcbaef610e6bd5c5f64ba6" dependencies = [ "bitflags", "cassowary", @@ -1229,6 +1334,17 @@ 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" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1266,6 +1382,15 @@ 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" source = "registry+https://github.com/rust-lang/crates.io-index" diff --git a/helix-lsp/Cargo.toml b/helix-lsp/Cargo.toml index 2ecd0cc1..d22d8636 100644 --- a/helix-lsp/Cargo.toml +++ b/helix-lsp/Cargo.toml @@ -9,9 +9,10 @@ edition = "2018" [dependencies] helix-core = { path = "../helix-core" } helix-view = { path = "../helix-view" } + once_cell = "1.4" -lsp-types = { version = "0.84", features = ["proposed"] } +lsp-types = { version = "0.85", features = ["proposed"] } smol = "1.2" url = "2" pathdiff = "0.2" @@ -20,7 +21,7 @@ glob = "0.3" anyhow = "1" serde_json = "1.0" serde = { version = "1.0", features = ["derive"] } -jsonrpc-core = "15.1" +jsonrpc-core = "16.0" futures-util = "0.3" -thiserror = "1" -log = "0.4" +thiserror = "1.0" +log = "~0.4" 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 b345d2e8..5f8f63f1 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::{self, Prompt, PromptEvent}; use helix_view::{ document::Mode, @@ -248,6 +248,60 @@ 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 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 + move |editor: &mut Editor, input: &str, event: PromptEvent| { + match event { + PromptEvent::Abort => { + // revert state + let view = editor.view_mut().unwrap(); + 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 + } + } + } + }, + ); + + cx.callback = Some(Box::new(move |compositor: &mut Compositor| { + compositor.push(Box::new(prompt)); + })); +} + pub fn split_selection_on_newline(cx: &mut Context) { let text = &cx.view.doc.text().slice(..); // only compile the regex once @@ -381,14 +435,33 @@ 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; + } + + let parts = input.split_ascii_whitespace().collect::<Vec<&str>>(); + + 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); + } + _ => (), + } }, ); compositor.push(Box::new(prompt)); })); } +pub fn file_picker(cx: &mut Context) { + cx.callback = Some(Box::new(|compositor: &mut Compositor| { + let picker = ui::file_picker("./"); + compositor.push(Box::new(picker)); + })); +} // calculate line numbers for each selection range fn selection_lines(state: &State) -> Vec<usize> { 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<dyn Fn(&mut Compositor)>; +pub type Callback = Box<dyn FnOnce(&mut Compositor)>; // --> EventResult should have a callback that takes a context with methods like .popup(), // .prompt() etc. That way we can abstract it from the renderer. diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs index af46f7a4..a31676e4 100644 --- a/helix-term/src/keymap.rs +++ b/helix-term/src/keymap.rs @@ -157,6 +157,7 @@ pub fn default() -> Keymaps { vec![key!('d')] => commands::delete_selection, vec![key!('c')] => commands::change_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, @@ -182,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/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-term/src/ui/helix.log b/helix-term/src/ui/helix.log new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/helix-term/src/ui/helix.log diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index bc79e09c..b778f531 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -1,8 +1,10 @@ mod editor; +mod picker; mod prompt; pub use editor::EditorView; -pub use prompt::Prompt; +pub use picker::Picker; +pub use prompt::{Prompt, PromptEvent}; pub use tui::layout::Rect; pub use tui::style::{Color, Modifier, Style}; @@ -12,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); + }, + ) +} diff --git a/helix-term/src/ui/picker.rs b/helix-term/src/ui/picker.rs new file mode 100644 index 00000000..0a12cff9 --- /dev/null +++ b/helix-term/src/ui/picker.rs @@ -0,0 +1,258 @@ +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 crate::ui::{Prompt, PromptEvent}; +use helix_core::Position; +use helix_view::Editor; + +pub struct Picker<T> { + options: Vec<T>, + // filter: String, + matcher: Box<Matcher>, + /// (index, score) + matches: Vec<(usize, i64)>, + + cursor: usize, + // pattern: String, + prompt: Prompt, + + format_fn: Box<dyn Fn(&T) -> &str>, + callback_fn: Box<dyn Fn(&mut Editor, &T)>, +} + +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(), + |editor: &mut Editor, pattern: &str, event: PromptEvent| { + // + }, + ); + + let mut picker = Self { + 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 + picker.score(); + + picker + } + + pub fn score(&mut self) { + // need to borrow via pattern match otherwise it complains about simultaneous borrow + let Self { + ref mut options, + ref mut matcher, + ref mut matches, + ref format_fn, + .. + } = *self; + + let pattern = &self.prompt.line; + + // reuse the matches allocation + matches.clear(); + 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 + self.cursor = 0; + } + + 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.options.len() { + self.cursor += 1; + } + } + + pub fn selection(&self) -> Option<&T> { + self.matches + .get(self.cursor) + .map(|(index, _score)| &self.options[*index]) + } +} + +// 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<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, + 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; + } + KeyEvent { + code: KeyCode::Enter, + .. + } => { + if let Some(option) = self.selection() { + (self.callback_fn)(&mut cx.editor, option); + } + return close_fn; + } + _ => { + match self.prompt.handle_event(event, cx) { + EventResult::Consumed(_) => { + // TODO: recalculate only if pattern changed + self.score(); + } + _ => (), + } + } + } + + 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 + + let files = self.matches.iter().map(|(index, _score)| { + (index, self.options.get(*index).unwrap()) // get_unchecked + }); + + 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); + } + + surface.set_stringn( + inner.x + 3, + inner.y + 2 + i as u16, + (self.format_fn)(option), + inner.width as usize - 1, + if i == self.cursor { selected } else { style }, + ); + } + } + + fn cursor_position(&self, area: Rect, ctx: &mut Context) -> Option<Position> { + self.prompt.cursor_position(area, ctx) + } +} diff --git a/helix-term/src/ui/prompt.rs b/helix-term/src/ui/prompt.rs index ce00a129..58efd560 100644 --- a/helix-term/src/ui/prompt.rs +++ b/helix-term/src/ui/prompt.rs @@ -10,24 +10,32 @@ pub struct Prompt { pub line: String, pub cursor: usize, pub completion: Vec<String>, - pub should_close: bool, pub completion_selection_index: Option<usize>, completion_fn: Box<dyn FnMut(&str) -> Vec<String>>, - callback_fn: Box<dyn FnMut(&mut Editor, &str)>, + callback_fn: Box<dyn FnMut(&mut Editor, &str, PromptEvent)>, +} + +#[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<String> + 'static, - callback_fn: impl FnMut(&mut Editor, &str) + 'static, + callback_fn: impl FnMut(&mut Editor, &str, PromptEvent) + 'static, ) -> Prompt { Prompt { 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 +50,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) { @@ -141,9 +147,15 @@ 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(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, + ); } } @@ -151,21 +163,28 @@ 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, }; + let close_fn = EventResult::Consumed(Some(Box::new(|compositor: &mut Compositor| { + // remove the layer + compositor.pop(); + }))); + match event { 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, .. } => { - return EventResult::Consumed(Some(Box::new(|compositor: &mut Compositor| { - // remove the layer - compositor.pop(); - }))); + (self.callback_fn)(cx.editor, &self.line, PromptEvent::Abort); + return close_fn; } KeyEvent { code: KeyCode::Right, @@ -186,11 +205,17 @@ 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 { code: KeyCode::Tab, .. } => self.change_completion_selection(), @@ -210,8 +235,8 @@ impl Component for Prompt { fn cursor_position(&self, area: Rect, ctx: &mut Context) -> Option<Position> { 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, )) } } 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<R>(&self, range: R) -> RopeSlice where R: RangeBounds { // self.state.doc.slice // } |