aboutsummaryrefslogtreecommitdiff
path: root/helix-term/src/ui
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/src/ui
parenta16c6e25852d985d90e56981a3aba62be012071d (diff)
ui: Rework command mode, implement file path completion.
Diffstat (limited to 'helix-term/src/ui')
-rw-r--r--helix-term/src/ui/mod.rs76
-rw-r--r--helix-term/src/ui/prompt.rs26
2 files changed, 98 insertions, 4 deletions
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);