aboutsummaryrefslogtreecommitdiff
path: root/helix-term
diff options
context:
space:
mode:
authorBlaž Hrastnik2021-03-01 09:02:31 +0000
committerBlaž Hrastnik2021-03-01 09:02:31 +0000
commit857bce0e301e81e3a90fd2f8a9683327bfc395d4 (patch)
treea33adfce8f6370c1ffb632ac298626bac4b3242e /helix-term
parenta16c6e25852d985d90e56981a3aba62be012071d (diff)
ui: Rework command mode, implement file path completion.
Diffstat (limited to 'helix-term')
-rw-r--r--helix-term/src/commands.rs43
-rw-r--r--helix-term/src/ui/mod.rs76
-rw-r--r--helix-term/src/ui/prompt.rs26
3 files changed, 127 insertions, 18 deletions
diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs
index 55cbf0fe..4a329f28 100644
--- a/helix-term/src/commands.rs
+++ b/helix-term/src/commands.rs
@@ -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());
}
diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs
index ea1d22ab..1526a210 100644
--- a/helix-term/src/ui/mod.rs
+++ b/helix-term/src/ui/mod.rs
@@ -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
+ }
+}
diff --git a/helix-term/src/ui/prompt.rs b/helix-term/src/ui/prompt.rs
index 7228b38c..700bc8a0 100644
--- a/helix-term/src/ui/prompt.rs
+++ b/helix-term/src/ui/prompt.rs
@@ -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);