diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 6e7cc3e78921e..5e168f8497e4b 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -3703,9 +3703,6 @@ pub fn completion_path(cx: &mut Context) { let cur_line = text.char_to_line(cursor); let begin_line = text.line_to_char(cur_line); let line_until_cursor = text.slice(begin_line..cursor).to_string(); - // TODO find a good regex for most use cases (especially Windows, which is not yet covered...) - // currently only one path match per line is possible in unix - static PATH_REGEX: Lazy = Lazy::new(|| Regex::new(r"((?:\.{0,2}/)+.*)$").unwrap()); // TODO: trigger_offset should be the cursor offset but we also need a starting offset from where we want to apply // completion filtering. For example logger.te| should filter the initial suggestion list with "te". @@ -3721,37 +3718,76 @@ pub fn completion_path(cx: &mut Context) { let callback = async move { let call: job::Callback = Box::new(move |editor: &mut Editor, compositor: &mut Compositor| { - if doc!(editor).mode() != Mode::Insert { + let doc = doc!(editor); + if doc.mode() != Mode::Insert { // we're not in insert mode anymore return; } - // read dir for a possibly matched path - let items = PATH_REGEX + + // TODO async path completion (for this probably the whole completion system has to be reworked to be async without producing race conditions) + let items = ui::PATH_REGEX .find(&line_until_cursor) - .and_then(|m| { - let mut path = PathBuf::from(m.as_str()); - if path.starts_with(".") { - path = std::env::current_dir().unwrap().join(path); + .and_then(|matched_path| { + let matched_path = matched_path.as_str(); + let mut path = PathBuf::from(matched_path); + if path.is_relative() { + if let Some(doc_path) = doc.path().and_then(|dp| dp.parent()) { + path = doc_path.join(path); + } } - std::fs::read_dir(path).ok().map(|rd| { - rd.filter_map(|re| re.ok()) - .filter_map(|re| { - re.metadata().ok().map(|m| CompletionItem::Path { - path: re.path(), - permissions: m.permissions(), - path_type: if m.is_file() { + let ends_with_slash = match matched_path.chars().last() { + Some('/') => true, // TODO support Windows + None => return Some(vec![]), + _ => false, + }; + // check if there are chars after the last slash, and if these chars represent a directory + let (dir_path, typed_file_name) = match std::fs::metadata(path.clone()).ok() + { + Some(m) if m.is_dir() && ends_with_slash => { + (Some(path.as_path()), None) + } + _ if !ends_with_slash => { + (path.parent(), path.file_name().and_then(|f| f.to_str())) + } + _ => return Some(vec![]), + }; + // read dir for a possibly matched path + dir_path + .and_then(|path| std::fs::read_dir(path).ok()) + .map(|read_dir| { + read_dir + .filter_map(|dir_entry| dir_entry.ok()) + .filter_map(|dir_entry| { + let path = dir_entry.path(); + // check if in / matches the start of the filename + let filename_starts_with_prefix = match ( + path.file_name().and_then(|f| f.to_str()), + typed_file_name, + ) { + (Some(re_stem), Some(t)) => re_stem.starts_with(t), + _ => true, + }; + if filename_starts_with_prefix { + dir_entry.metadata().ok().map(|md| (path, md)) + } else { + None + } + }) + .map(|(path, md)| CompletionItem::Path { + path, + permissions: md.permissions(), + path_type: if md.is_file() { PathType::File - } else if m.is_dir() { + } else if md.is_dir() { PathType::Dir - } else if m.is_symlink() { + } else if md.is_symlink() { PathType::Symlink } else { PathType::Unknown }, }) - }) - .collect::>() - }) + .collect::>() + }) }) .unwrap_or_default(); diff --git a/helix-term/src/ui/completion.rs b/helix-term/src/ui/completion.rs index 94edaf00a0324..98763b47c4505 100644 --- a/helix-term/src/ui/completion.rs +++ b/helix-term/src/ui/completion.rs @@ -1,6 +1,8 @@ use crate::compositor::{Component, Context, EventResult}; use crossterm::event::{Event, KeyCode, KeyEvent}; +use helix_core::regex::Regex; use helix_view::editor::CompleteAction; +use once_cell::sync::Lazy; use tui::buffer::Buffer as Surface; use std::borrow::Cow; @@ -15,6 +17,10 @@ use crate::ui::{menu, Markdown, Menu, Popup, PromptEvent}; use helix_lsp::{lsp, util, OffsetEncoding}; +// TODO find a good regex for most use cases (especially Windows, which is not yet covered...) +// currently only one path match per line is possible in unix +pub static PATH_REGEX: Lazy = Lazy::new(|| Regex::new(r"((?:\.{0,2}/)+.*)$").unwrap()); + #[derive(Debug, Clone)] pub enum PathType { Dir, @@ -43,9 +49,11 @@ impl menu::Item for CompletionItem { CompletionItem::LSP { item, .. } => { item.filter_text.as_ref().unwrap_or(&item.label).as_str() } - CompletionItem::Path { path, .. } => { - path.file_name().unwrap_or_default().to_str().unwrap() - } // TODO unwrap good enough (because of performance)? + CompletionItem::Path { path, .. } => path + .file_name() + .unwrap_or_default() + .to_str() + .unwrap_or_default(), } } @@ -54,18 +62,22 @@ impl menu::Item for CompletionItem { CompletionItem::LSP { item, .. } => { item.filter_text.as_ref().unwrap_or(&item.label).as_str() } - CompletionItem::Path { path, .. } => { - path.file_name().unwrap_or_default().to_str().unwrap() - } + CompletionItem::Path { path, .. } => path + .file_name() + .unwrap_or_default() + .to_str() + .unwrap_or_default(), } } fn label(&self) -> &str { match self { CompletionItem::LSP { item, .. } => item.label.as_str(), - CompletionItem::Path { path, .. } => { - path.file_name().unwrap_or_default().to_str().unwrap() - } + CompletionItem::Path { path, .. } => path + .file_name() + .unwrap_or_default() + .to_str() + .unwrap_or_default(), } } @@ -112,7 +124,7 @@ impl menu::Item for CompletionItem { CompletionItem::Path { path_type, .. } => menu::Cell::from({ // TODO probably check permissions/or (coloring maybe) match path_type { - PathType::Dir => "dir", + PathType::Dir => "folder", PathType::File => "file", PathType::Symlink => "symlink", PathType::Unknown => "unknown", @@ -182,8 +194,23 @@ impl Completion { transaction } CompletionItem::Path { path, .. } => { + let text = doc.text().slice(..); + let cur_line = text.char_to_line(trigger_offset); + let begin_line = text.line_to_char(cur_line); let path_head = path.file_name().unwrap().to_string_lossy(); - let prefix = Cow::from(doc.text().slice(start_offset..trigger_offset)); + let line_until_trigger_offset = + Cow::from(doc.text().slice(begin_line..trigger_offset)); + let mat = PATH_REGEX.find(&line_until_trigger_offset).unwrap(); + let path = PathBuf::from(mat.as_str()); + let mut prefix = path + .file_name() + .and_then(|f| f.to_str()) + .unwrap_or_default() + .to_string(); + // TODO support Windows + if path.to_str().map(|p| p.ends_with('/')).unwrap_or_default() { + prefix += "/"; + } let text = path_head.trim_start_matches::<&str>(&prefix); Transaction::change( doc.text(), diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index 76401b3efb21a..790a20ed2dd33 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -10,7 +10,7 @@ mod prompt; mod spinner; mod text; -pub use completion::{Completion, CompletionItem, PathType}; +pub use completion::{Completion, CompletionItem, PathType, PATH_REGEX}; pub use editor::EditorView; pub use markdown::Markdown; pub use menu::Menu;