mirror of
https://github.com/ilyakooo0/helix.git
synced 2024-11-11 14:34:31 +03:00
ui: Rework command mode, implement file path completion.
This commit is contained in:
parent
a16c6e2585
commit
857bce0e30
@ -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());
|
||||
}
|
||||
|
@ -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_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);
|
||||
|
Loading…
Reference in New Issue
Block a user