Merge pull request #7 from helix-editor/interactive-split-select

File picker/interactive split prompt
This commit is contained in:
Blaž Hrastnik 2020-12-18 19:24:50 +09:00 committed by GitHub
commit 3f0dbfcac8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 573 additions and 43 deletions

151
Cargo.lock generated
View file

@ -181,6 +181,15 @@ dependencies = [
"once_cell", "once_cell",
] ]
[[package]]
name = "bstr"
version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "473fc6b38233f9af7baa94fb5852dca389e3d95b8e21c8e3719301462c5d9faf"
dependencies = [
"memchr",
]
[[package]] [[package]]
name = "cache-padded" name = "cache-padded"
version = "1.1.1" version = "1.1.1"
@ -339,6 +348,12 @@ dependencies = [
"log", "log",
] ]
[[package]]
name = "fnv"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]] [[package]]
name = "form_urlencoded" name = "form_urlencoded"
version = "1.0.0" version = "1.0.0"
@ -361,9 +376,28 @@ dependencies = [
[[package]] [[package]]
name = "futures" name = "futures"
version = "0.1.30" version = "0.3.8"
source = "registry+https://github.com/rust-lang/crates.io-index" 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]] [[package]]
name = "futures-core" name = "futures-core"
@ -371,6 +405,17 @@ version = "0.3.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "847ce131b72ffb13b6109a221da9ad97a64cbe48feb1028356b836b47b8f1748" 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]] [[package]]
name = "futures-io" name = "futures-io"
version = "0.3.8" version = "0.3.8"
@ -404,6 +449,12 @@ dependencies = [
"syn", "syn",
] ]
[[package]]
name = "futures-sink"
version = "0.3.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f878195a49cee50e006b02b93cf7e0a95a38ac7b776b4c4d9cc1207cd20fcb3d"
[[package]] [[package]]
name = "futures-task" name = "futures-task"
version = "0.3.8" version = "0.3.8"
@ -419,9 +470,13 @@ version = "0.3.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d304cff4a7b99cfb7986f7d43fbe93d175e72e704a8860787cc95e9ffd85cbd2" checksum = "d304cff4a7b99cfb7986f7d43fbe93d175e72e704a8860787cc95e9ffd85cbd2"
dependencies = [ dependencies = [
"futures-channel",
"futures-core", "futures-core",
"futures-io",
"futures-macro", "futures-macro",
"futures-sink",
"futures-task", "futures-task",
"memchr",
"pin-project", "pin-project",
"pin-utils", "pin-utils",
"proc-macro-hack", "proc-macro-hack",
@ -429,6 +484,15 @@ dependencies = [
"slab", "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]] [[package]]
name = "getrandom" name = "getrandom"
version = "0.1.15" version = "0.1.15"
@ -446,6 +510,19 @@ version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" 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]] [[package]]
name = "hashbrown" name = "hashbrown"
version = "0.9.1" version = "0.9.1"
@ -508,9 +585,11 @@ dependencies = [
"crossterm", "crossterm",
"fern", "fern",
"futures-util", "futures-util",
"fuzzy-matcher",
"helix-core", "helix-core",
"helix-lsp", "helix-lsp",
"helix-view", "helix-view",
"ignore",
"log", "log",
"num_cpus", "num_cpus",
"once_cell", "once_cell",
@ -552,10 +631,28 @@ dependencies = [
] ]
[[package]] [[package]]
name = "indexmap" name = "ignore"
version = "1.6.0" version = "0.4.17"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55e2e4c765aa53a0424761bf9f41aa7a6ac1efa87238f59560640e27fca028f2" checksum = "b287fb45c60bb826a0dc68ff08742b9d88a2fea13d6e0c286b3172065aaf878c"
dependencies = [
"crossbeam-utils",
"globset",
"lazy_static",
"log",
"memchr",
"regex",
"same-file",
"thread_local",
"walkdir",
"winapi-util",
]
[[package]]
name = "indexmap"
version = "1.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4fb1fa934250de4de8aef298d81c729a7d33d8c239daa3a7575e6b92bfc7313b"
dependencies = [ dependencies = [
"autocfg", "autocfg",
"hashbrown", "hashbrown",
@ -587,9 +684,9 @@ dependencies = [
[[package]] [[package]]
name = "jsonrpc-core" name = "jsonrpc-core"
version = "15.1.0" version = "16.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0745a6379e3edc893c84ec203589790774e4247420033e71a76d3ab4687991fa" checksum = "6a47c4c3ac843f9a4238943f97620619033dadef4b378cd1e8addd170de396b3"
dependencies = [ dependencies = [
"futures", "futures",
"log", "log",
@ -630,9 +727,9 @@ dependencies = [
[[package]] [[package]]
name = "lsp-types" name = "lsp-types"
version = "0.84.0" version = "0.85.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b95be71fe205e44de754185bcf86447b65813ce1ceb298f8d3793ade5fff08d" checksum = "857650f3e83fb62f89d15410414e0ed7d0735445020da398d37f65d20a5423b9"
dependencies = [ dependencies = [
"base64 0.12.3", "base64 0.12.3",
"bitflags", "bitflags",
@ -929,6 +1026,15 @@ version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" 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]] [[package]]
name = "scopeguard" name = "scopeguard"
version = "1.1.0" version = "1.1.0"
@ -1038,13 +1144,12 @@ dependencies = [
[[package]] [[package]]
name = "socket2" name = "socket2"
version = "0.3.17" version = "0.3.18"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2c29947abdee2a218277abeca306f25789c938e500ea5a9d4b12a5a504466902" checksum = "97e0e9fd577458a4f61fb91fcb559ea2afecc54c934119421f9f5d3d5b1a1057"
dependencies = [ dependencies = [
"cfg-if 1.0.0", "cfg-if 1.0.0",
"libc", "libc",
"redox_syscall",
"winapi", "winapi",
] ]
@ -1146,7 +1251,7 @@ dependencies = [
[[package]] [[package]]
name = "tui" name = "tui"
version = "0.13.0" version = "0.13.0"
source = "git+https://github.com/fdehau/tui-rs#74243394d90ea1316b6bedac6c9e4f26971c76b6" source = "git+https://github.com/fdehau/tui-rs#eb1e3be7228509e42cbcbaef610e6bd5c5f64ba6"
dependencies = [ dependencies = [
"bitflags", "bitflags",
"cassowary", "cassowary",
@ -1228,6 +1333,17 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d5b2c62b4012a3e1eca5a7e077d13b3bf498c4073e33ccd58626607748ceeca" 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]] [[package]]
name = "wasi" name = "wasi"
version = "0.9.0+wasi-snapshot-preview1" version = "0.9.0+wasi-snapshot-preview1"
@ -1265,6 +1381,15 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 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]] [[package]]
name = "winapi-x86_64-pc-windows-gnu" name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0" version = "0.4.0"

View file

@ -9,9 +9,10 @@ edition = "2018"
[dependencies] [dependencies]
helix-core = { path = "../helix-core" } helix-core = { path = "../helix-core" }
helix-view = { path = "../helix-view" } helix-view = { path = "../helix-view" }
once_cell = "1.4" once_cell = "1.4"
lsp-types = { version = "0.84", features = ["proposed"] } lsp-types = { version = "0.85", features = ["proposed"] }
smol = "1.2" smol = "1.2"
url = "2" url = "2"
pathdiff = "0.2" pathdiff = "0.2"
@ -20,7 +21,7 @@ glob = "0.3"
anyhow = "1" anyhow = "1"
serde_json = "1.0" serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
jsonrpc-core = "15.1" jsonrpc-core = "16.0"
futures-util = "0.3" futures-util = "0.3"
thiserror = "1" thiserror = "1.0"
log = "0.4" log = "~0.4"

View file

@ -32,3 +32,7 @@ futures-util = "0.3"
fern = "0.6" fern = "0.6"
chrono = "0.4" chrono = "0.4"
log = "0.4" log = "0.4"
# File picker
fuzzy-matcher = "0.3"
ignore = "0.4"

View file

@ -10,7 +10,7 @@ use helix_core::{
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use crate::compositor::Compositor; use crate::compositor::Compositor;
use crate::ui::Prompt; use crate::ui::{self, Prompt, PromptEvent};
use helix_view::{ use helix_view::{
document::Mode, document::Mode,
@ -248,6 +248,60 @@ pub fn extend_line_down(cx: &mut Context) {
cx.view.doc.set_selection(selection); 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(), &regex);
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) { pub fn split_selection_on_newline(cx: &mut Context) {
let text = &cx.view.doc.text().slice(..); let text = &cx.view.doc.text().slice(..);
// only compile the regex once // only compile the regex once
@ -381,14 +435,33 @@ pub fn command_mode(cx: &mut Context) {
.filter(|command| command.contains(_input)) .filter(|command| command.contains(_input))
.collect() .collect()
}, // completion }, // completion
|editor: &mut Editor, input: &str| match input { |editor: &mut Editor, input: &str, event: PromptEvent| {
"q" => editor.should_close = true, 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)); 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 // calculate line numbers for each selection range
fn selection_lines(state: &State) -> Vec<usize> { fn selection_lines(state: &State) -> Vec<usize> {

View file

@ -19,7 +19,7 @@ use smol::Executor;
use tui::buffer::Buffer as Surface; use tui::buffer::Buffer as Surface;
use tui::layout::Rect; 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(), // --> EventResult should have a callback that takes a context with methods like .popup(),
// .prompt() etc. That way we can abstract it from the renderer. // .prompt() etc. That way we can abstract it from the renderer.

View file

@ -157,6 +157,7 @@ pub fn default() -> Keymaps {
vec![key!('d')] => commands::delete_selection, vec![key!('d')] => commands::delete_selection,
vec![key!('c')] => commands::change_selection, vec![key!('c')] => commands::change_selection,
vec![key!('s')] => commands::split_selection_on_newline, vec![key!('s')] => commands::split_selection_on_newline,
vec![shift!('S')] => commands::split_selection,
vec![key!(';')] => commands::collapse_selection, vec![key!(';')] => commands::collapse_selection,
// TODO should be alt(;) // TODO should be alt(;)
vec![key!('%')] => commands::flip_selections, vec![key!('%')] => commands::flip_selections,
@ -182,6 +183,8 @@ pub fn default() -> Keymaps {
}] => commands::page_down, }] => commands::page_down,
vec![ctrl!('u')] => commands::half_page_up, vec![ctrl!('u')] => commands::half_page_up,
vec![ctrl!('d')] => commands::half_page_down, vec![ctrl!('d')] => commands::half_page_down,
vec![ctrl!('p')] => commands::file_picker,
), ),
Mode::Insert => hashmap!( Mode::Insert => hashmap!(
vec![Key { vec![Key {

View file

@ -226,7 +226,7 @@ impl EditorView {
); );
surface.set_string(1, viewport.y, mode, text_color); 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); surface.set_string(6, viewport.y, path.to_string_lossy(), text_color);
} }

View file

View file

@ -1,8 +1,10 @@
mod editor; mod editor;
mod picker;
mod prompt; mod prompt;
pub use editor::EditorView; 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::layout::Rect;
pub use tui::style::{Color, Modifier, Style}; pub use tui::style::{Color, Modifier, Style};
@ -12,3 +14,35 @@ pub use tui::style::{Color, Modifier, Style};
pub fn text_color() -> Style { pub fn text_color() -> Style {
Style::default().fg(Color::Rgb(219, 191, 239)) // lilac 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);
},
)
}

258
helix-term/src/ui/picker.rs Normal file
View file

@ -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)
}
}

View file

@ -10,24 +10,32 @@ pub struct Prompt {
pub line: String, pub line: String,
pub cursor: usize, pub cursor: usize,
pub completion: Vec<String>, pub completion: Vec<String>,
pub should_close: bool,
pub completion_selection_index: Option<usize>, pub completion_selection_index: Option<usize>,
completion_fn: Box<dyn FnMut(&str) -> Vec<String>>, 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 { impl Prompt {
pub fn new( pub fn new(
prompt: String, prompt: String,
mut completion_fn: impl FnMut(&str) -> Vec<String> + 'static, 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 { Prompt {
prompt, prompt,
line: String::new(), line: String::new(),
cursor: 0, cursor: 0,
completion: completion_fn(""), completion: completion_fn(""),
should_close: false,
completion_selection_index: None, completion_selection_index: None,
completion_fn: Box::new(completion_fn), completion_fn: Box::new(completion_fn),
callback_fn: Box::new(callback_fn), callback_fn: Box::new(callback_fn),
@ -42,9 +50,7 @@ impl Prompt {
} }
pub fn move_char_left(&mut self) { pub fn move_char_left(&mut self) {
if self.cursor > 0 { self.cursor = self.cursor.saturating_sub(1)
self.cursor -= 1;
}
} }
pub fn move_char_right(&mut self) { pub fn move_char_right(&mut self) {
@ -141,9 +147,15 @@ impl Prompt {
} }
} }
} }
let line = area.height - 1;
// render buffer text // render buffer text
surface.set_string(1, area.height - 1, &self.prompt, text_color); surface.set_string(area.x, area.y + line, &self.prompt, text_color);
surface.set_string(2, area.height - 1, &self.line, 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 { fn handle_event(&mut self, event: Event, cx: &mut Context) -> EventResult {
let event = match event { let event = match event {
Event::Key(event) => event, Event::Key(event) => event,
Event::Resize(..) => return EventResult::Consumed(None),
_ => return EventResult::Ignored, _ => return EventResult::Ignored,
}; };
let close_fn = EventResult::Consumed(Some(Box::new(|compositor: &mut Compositor| {
// remove the layer
compositor.pop();
})));
match event { match event {
KeyEvent { KeyEvent {
code: KeyCode::Char(c), code: KeyCode::Char(c),
modifiers: KeyModifiers::NONE, modifiers: KeyModifiers::NONE,
} => self.insert_char(c), } => {
self.insert_char(c);
(self.callback_fn)(cx.editor, &self.line, PromptEvent::Update);
}
KeyEvent { KeyEvent {
code: KeyCode::Esc, .. code: KeyCode::Esc, ..
} => { } => {
return EventResult::Consumed(Some(Box::new(|compositor: &mut Compositor| { (self.callback_fn)(cx.editor, &self.line, PromptEvent::Abort);
// remove the layer return close_fn;
compositor.pop();
})));
} }
KeyEvent { KeyEvent {
code: KeyCode::Right, code: KeyCode::Right,
@ -186,11 +205,17 @@ impl Component for Prompt {
KeyEvent { KeyEvent {
code: KeyCode::Backspace, code: KeyCode::Backspace,
modifiers: KeyModifiers::NONE, modifiers: KeyModifiers::NONE,
} => self.delete_char_backwards(), } => {
self.delete_char_backwards();
(self.callback_fn)(cx.editor, &self.line, PromptEvent::Update);
}
KeyEvent { KeyEvent {
code: KeyCode::Enter, code: KeyCode::Enter,
.. ..
} => (self.callback_fn)(cx.editor, &self.line), } => {
(self.callback_fn)(cx.editor, &self.line, PromptEvent::Validate);
return close_fn;
}
KeyEvent { KeyEvent {
code: KeyCode::Tab, .. code: KeyCode::Tab, ..
} => self.change_completion_selection(), } => self.change_completion_selection(),
@ -210,8 +235,8 @@ impl Component for Prompt {
fn cursor_position(&self, area: Rect, ctx: &mut Context) -> Option<Position> { fn cursor_position(&self, area: Rect, ctx: &mut Context) -> Option<Position> {
Some(Position::new( Some(Position::new(
area.height as usize - 1, area.height as usize,
area.x as usize + 2 + self.cursor, area.x as usize + self.prompt.len() + self.cursor,
)) ))
} }
} }

View file

@ -1,6 +1,6 @@
use anyhow::Error; use anyhow::Error;
use std::future::Future; use std::future::Future;
use std::path::PathBuf; use std::path::{Path, PathBuf};
use helix_core::{ use helix_core::{
syntax::LOADER, ChangeSet, Diagnostic, History, Position, Range, Rope, RopeSlice, Selection, syntax::LOADER, ChangeSet, Diagnostic, History, Position, Range, Rope, RopeSlice, Selection,
@ -201,6 +201,13 @@ impl Document {
&self.state.selection &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 { // pub fn slice<R>(&self, range: R) -> RopeSlice where R: RangeBounds {
// self.state.doc.slice // self.state.doc.slice
// } // }