diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index fdf906a29675d..f26b3d648e8c7 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -3965,9 +3965,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". @@ -3987,33 +3984,72 @@ pub fn completion_path(cx: &mut Context) { // we're not in insert mode anymore return; } - // read dir for a possibly matched path - let items = PATH_REGEX + let doc = doc!(editor); + + // 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 16e431c5aee59..c790c47b2ee03 100644 --- a/helix-term/src/ui/completion.rs +++ b/helix-term/src/ui/completion.rs @@ -1,5 +1,7 @@ use crate::compositor::{Component, Context, Event, EventResult}; +use helix_core::regex::Regex; use helix_view::{apply_transaction, editor::CompleteAction}; +use once_cell::sync::Lazy; use tui::buffer::Buffer as Surface; use tui::text::Spans; @@ -19,6 +21,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, @@ -113,7 +119,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", @@ -192,8 +198,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 7a34c62c0347f..5188886723863 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -15,7 +15,7 @@ mod text; use crate::compositor::{Component, Compositor}; use crate::job::{self, Callback}; -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;