Merge pull request #7 from helix-editor/interactive-split-select
File picker/interactive split prompt
This commit is contained in:
commit
3f0dbfcac8
12 changed files with 573 additions and 43 deletions
151
Cargo.lock
generated
151
Cargo.lock
generated
|
@ -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"
|
||||
|
@ -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"
|
||||
|
@ -371,6 +405,17 @@ version = "0.3.8"
|
|||
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"
|
||||
|
@ -404,6 +449,12 @@ dependencies = [
|
|||
"syn",
|
||||
]
|
||||
|
||||
[[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"
|
||||
|
@ -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",
|
||||
|
@ -429,6 +484,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 +510,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 +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 = "indexmap"
|
||||
version = "1.6.0"
|
||||
name = "ignore"
|
||||
version = "0.4.17"
|
||||
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 = [
|
||||
"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",
|
||||
|
@ -929,6 +1026,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"
|
||||
|
@ -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",
|
||||
|
@ -1228,6 +1333,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 +1381,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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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> {
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
0
helix-term/src/ui/helix.log
Normal file
0
helix-term/src/ui/helix.log
Normal file
|
@ -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);
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
258
helix-term/src/ui/picker.rs
Normal file
258
helix-term/src/ui/picker.rs
Normal 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)
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
// }
|
||||
|
|
Loading…
Add table
Reference in a new issue