ui: Rework command mode, implement file path completion.

This commit is contained in:
Blaž Hrastnik 2021-03-01 18:02:31 +09:00
parent a16c6e2585
commit 857bce0e30
3 changed files with 127 additions and 18 deletions

View File

@ -519,22 +519,37 @@ pub fn append_mode(cx: &mut Context) {
doc.set_selection(selection);
}
const COMMAND_LIST: &[&str] = &["write", "open", "quit"];
// TODO: I, A, o and O can share a lot of the primitives.
pub fn command_mode(cx: &mut Context) {
let executor = cx.executor;
let prompt = Prompt::new(
":".to_owned(),
|_input: &str| {
let command_list = vec![
"q".to_string(),
"o".to_string(),
"w".to_string(),
// String::from("q"),
];
command_list
.into_iter()
.filter(|command| command.contains(_input))
.collect()
|input: &str| {
// we use .this over split_ascii_whitespace() because we care about empty segments
let parts = input.split(' ').collect::<Vec<&str>>();
// simple heuristic: if there's no space, complete command.
// if there's a space, file completion kicks in. We should specialize by command later.
if parts.len() <= 1 {
COMMAND_LIST
.iter()
.filter(|command| command.contains(input))
.map(|command| std::borrow::Cow::Borrowed(*command))
.collect()
} else {
let part = parts.last().unwrap();
ui::completers::filename(part)
// TODO
// completion needs to be more advanced: need to return starting index for replace
// for example, "src/" completion application.rs needs to insert after /, but "hx"
// completion helix-core needs to replace the text.
//
// additionally, completion items could have a info section that would get
// displayed in a popup above the prompt when items are tabbed over
}
}, // completion
move |editor: &mut Editor, input: &str, event: PromptEvent| {
if event != PromptEvent::Validate {
@ -544,14 +559,14 @@ pub fn command_mode(cx: &mut Context) {
let parts = input.split_ascii_whitespace().collect::<Vec<&str>>();
match *parts.as_slice() {
["q"] => {
["q"] | ["quit"] => {
editor.tree.remove(editor.view().id);
// editor.should_close = true,
}
["o", path] => {
["o", path] | ["open", path] => {
editor.open(path.into(), executor);
}
["w"] => {
["w"] | ["write"] => {
// TODO: non-blocking via save() command
smol::block_on(editor.view_mut().doc.save());
}

View File

@ -124,3 +124,79 @@ pub fn buffer_picker(views: &[View], current: usize) -> Picker<(Option<PathBuf>,
// },
// )
}
pub mod completers {
use std::borrow::Cow;
// TODO: we could return an iter/lazy thing so it can fetch as many as it needs.
pub fn filename(input: &str) -> Vec<Cow<'static, str>> {
// Rust's filename handling is really annoying.
use ignore::WalkBuilder;
use std::path::{Path, PathBuf};
let path = Path::new(input);
let (dir, file_name) = if input.ends_with('/') {
(path.into(), None)
} else {
let file_name = path
.file_name()
.map(|file| file.to_str().unwrap().to_owned());
let path = match path.parent() {
Some(path) if !path.as_os_str().is_empty() => path.to_path_buf(),
// Path::new("h")'s parent is Some("")...
_ => std::env::current_dir().expect("couldn't determine current directory"),
};
(path, file_name)
};
let mut files: Vec<_> = WalkBuilder::new(dir.clone())
.max_depth(Some(1))
.build()
.filter_map(|file| {
file.ok().map(|entry| {
let is_dir = entry
.file_type()
.map(|entry| entry.is_dir())
.unwrap_or(false);
let mut path = entry.path().strip_prefix(&dir).unwrap().to_path_buf();
if is_dir {
path.push("");
}
Cow::from(path.to_str().unwrap().to_string())
})
}) // TODO: unwrap or skip
.filter(|path| !path.is_empty()) // TODO
.collect();
// if empty, return a list of dirs and files in current dir
if let Some(file_name) = file_name {
use fuzzy_matcher::skim::SkimMatcherV2 as Matcher;
use fuzzy_matcher::FuzzyMatcher;
use std::cmp::Reverse;
let matcher = Matcher::default();
// inefficient, but we need to calculate the scores, filter out None, then sort.
let mut matches: Vec<_> = files
.into_iter()
.filter_map(|file| {
matcher
.fuzzy_match(&file, &file_name)
.map(|score| (file, score))
})
.collect();
matches.sort_unstable_by_key(|(_file, score)| Reverse(*score));
files = matches.into_iter().map(|(file, _)| file.into()).collect();
// TODO: complete to longest common match
}
files
}
}

View File

@ -3,15 +3,16 @@ use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers};
use helix_core::Position;
use helix_view::Editor;
use helix_view::Theme;
use std::borrow::Cow;
use std::string::String;
pub struct Prompt {
pub prompt: String,
pub line: String,
pub cursor: usize,
pub completion: Vec<String>,
pub completion: Vec<Cow<'static, str>>,
pub completion_selection_index: Option<usize>,
completion_fn: Box<dyn FnMut(&str) -> Vec<String>>,
completion_fn: Box<dyn FnMut(&str) -> Vec<Cow<'static, str>>>,
callback_fn: Box<dyn FnMut(&mut Editor, &str, PromptEvent)>,
}
@ -28,7 +29,7 @@ pub enum PromptEvent {
impl Prompt {
pub fn new(
prompt: String,
mut completion_fn: impl FnMut(&str) -> Vec<String> + 'static,
mut completion_fn: impl FnMut(&str) -> Vec<Cow<'static, str>> + 'static,
callback_fn: impl FnMut(&mut Editor, &str, PromptEvent) + 'static,
) -> Prompt {
Prompt {
@ -83,7 +84,19 @@ impl Prompt {
let index =
self.completion_selection_index.map(|i| i + 1).unwrap_or(0) % self.completion.len();
self.completion_selection_index = Some(index);
self.line = self.completion[index].clone();
let item = &self.completion[index];
// replace the last arg
if let Some(pos) = self.line.rfind(' ') {
self.line.replace_range(pos + 1.., item);
} else {
// need toowned_clone_into nightly feature to reuse allocation
self.line = item.to_string();
}
self.move_end();
// TODO: recalculate completion when completion item is accepted, (Enter)
}
pub fn exit_selection(&mut self) {
self.completion_selection_index = None;
@ -175,9 +188,14 @@ impl Component for Prompt {
)));
match event {
// char or shift char
KeyEvent {
code: KeyCode::Char(c),
modifiers: KeyModifiers::NONE,
}
| KeyEvent {
code: KeyCode::Char(c),
modifiers: KeyModifiers::SHIFT,
} => {
self.insert_char(c);
(self.callback_fn)(cx.editor, &self.line, PromptEvent::Update);