ui: Rework command mode, implement file path completion.
This commit is contained in:
parent
a16c6e2585
commit
857bce0e30
3 changed files with 127 additions and 18 deletions
|
@ -519,22 +519,37 @@ pub fn append_mode(cx: &mut Context) {
|
||||||
doc.set_selection(selection);
|
doc.set_selection(selection);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const COMMAND_LIST: &[&str] = &["write", "open", "quit"];
|
||||||
|
|
||||||
// TODO: I, A, o and O can share a lot of the primitives.
|
// TODO: I, A, o and O can share a lot of the primitives.
|
||||||
pub fn command_mode(cx: &mut Context) {
|
pub fn command_mode(cx: &mut Context) {
|
||||||
let executor = cx.executor;
|
let executor = cx.executor;
|
||||||
let prompt = Prompt::new(
|
let prompt = Prompt::new(
|
||||||
":".to_owned(),
|
":".to_owned(),
|
||||||
|_input: &str| {
|
|input: &str| {
|
||||||
let command_list = vec![
|
// we use .this over split_ascii_whitespace() because we care about empty segments
|
||||||
"q".to_string(),
|
let parts = input.split(' ').collect::<Vec<&str>>();
|
||||||
"o".to_string(),
|
|
||||||
"w".to_string(),
|
// simple heuristic: if there's no space, complete command.
|
||||||
// String::from("q"),
|
// if there's a space, file completion kicks in. We should specialize by command later.
|
||||||
];
|
if parts.len() <= 1 {
|
||||||
command_list
|
COMMAND_LIST
|
||||||
.into_iter()
|
.iter()
|
||||||
.filter(|command| command.contains(_input))
|
.filter(|command| command.contains(input))
|
||||||
.collect()
|
.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
|
}, // completion
|
||||||
move |editor: &mut Editor, input: &str, event: PromptEvent| {
|
move |editor: &mut Editor, input: &str, event: PromptEvent| {
|
||||||
if event != PromptEvent::Validate {
|
if event != PromptEvent::Validate {
|
||||||
|
@ -544,14 +559,14 @@ pub fn command_mode(cx: &mut Context) {
|
||||||
let parts = input.split_ascii_whitespace().collect::<Vec<&str>>();
|
let parts = input.split_ascii_whitespace().collect::<Vec<&str>>();
|
||||||
|
|
||||||
match *parts.as_slice() {
|
match *parts.as_slice() {
|
||||||
["q"] => {
|
["q"] | ["quit"] => {
|
||||||
editor.tree.remove(editor.view().id);
|
editor.tree.remove(editor.view().id);
|
||||||
// editor.should_close = true,
|
// editor.should_close = true,
|
||||||
}
|
}
|
||||||
["o", path] => {
|
["o", path] | ["open", path] => {
|
||||||
editor.open(path.into(), executor);
|
editor.open(path.into(), executor);
|
||||||
}
|
}
|
||||||
["w"] => {
|
["w"] | ["write"] => {
|
||||||
// TODO: non-blocking via save() command
|
// TODO: non-blocking via save() command
|
||||||
smol::block_on(editor.view_mut().doc.save());
|
smol::block_on(editor.view_mut().doc.save());
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -3,15 +3,16 @@ use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers};
|
||||||
use helix_core::Position;
|
use helix_core::Position;
|
||||||
use helix_view::Editor;
|
use helix_view::Editor;
|
||||||
use helix_view::Theme;
|
use helix_view::Theme;
|
||||||
|
use std::borrow::Cow;
|
||||||
use std::string::String;
|
use std::string::String;
|
||||||
|
|
||||||
pub struct Prompt {
|
pub struct Prompt {
|
||||||
pub prompt: String,
|
pub prompt: String,
|
||||||
pub line: String,
|
pub line: String,
|
||||||
pub cursor: usize,
|
pub cursor: usize,
|
||||||
pub completion: Vec<String>,
|
pub completion: Vec<Cow<'static, str>>,
|
||||||
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<Cow<'static, str>>>,
|
||||||
callback_fn: Box<dyn FnMut(&mut Editor, &str, PromptEvent)>,
|
callback_fn: Box<dyn FnMut(&mut Editor, &str, PromptEvent)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -28,7 +29,7 @@ pub enum PromptEvent {
|
||||||
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<Cow<'static, str>> + 'static,
|
||||||
callback_fn: impl FnMut(&mut Editor, &str, PromptEvent) + 'static,
|
callback_fn: impl FnMut(&mut Editor, &str, PromptEvent) + 'static,
|
||||||
) -> Prompt {
|
) -> Prompt {
|
||||||
Prompt {
|
Prompt {
|
||||||
|
@ -83,7 +84,19 @@ impl Prompt {
|
||||||
let index =
|
let index =
|
||||||
self.completion_selection_index.map(|i| i + 1).unwrap_or(0) % self.completion.len();
|
self.completion_selection_index.map(|i| i + 1).unwrap_or(0) % self.completion.len();
|
||||||
self.completion_selection_index = Some(index);
|
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) {
|
pub fn exit_selection(&mut self) {
|
||||||
self.completion_selection_index = None;
|
self.completion_selection_index = None;
|
||||||
|
@ -175,9 +188,14 @@ impl Component for Prompt {
|
||||||
)));
|
)));
|
||||||
|
|
||||||
match event {
|
match event {
|
||||||
|
// char or shift char
|
||||||
KeyEvent {
|
KeyEvent {
|
||||||
code: KeyCode::Char(c),
|
code: KeyCode::Char(c),
|
||||||
modifiers: KeyModifiers::NONE,
|
modifiers: KeyModifiers::NONE,
|
||||||
|
}
|
||||||
|
| KeyEvent {
|
||||||
|
code: KeyCode::Char(c),
|
||||||
|
modifiers: KeyModifiers::SHIFT,
|
||||||
} => {
|
} => {
|
||||||
self.insert_char(c);
|
self.insert_char(c);
|
||||||
(self.callback_fn)(cx.editor, &self.line, PromptEvent::Update);
|
(self.callback_fn)(cx.editor, &self.line, PromptEvent::Update);
|
||||||
|
|
Loading…
Add table
Reference in a new issue