Unnamed repository; edit this file 'description' to name the repository.
Diffstat (limited to 'helix-term/src/handlers/completion/path.rs')
| -rw-r--r-- | helix-term/src/handlers/completion/path.rs | 189 |
1 files changed, 189 insertions, 0 deletions
diff --git a/helix-term/src/handlers/completion/path.rs b/helix-term/src/handlers/completion/path.rs new file mode 100644 index 00000000..b7b60507 --- /dev/null +++ b/helix-term/src/handlers/completion/path.rs @@ -0,0 +1,189 @@ +use std::{ + borrow::Cow, + fs, + path::{Path, PathBuf}, + str::FromStr as _, +}; + +use futures_util::{future::BoxFuture, FutureExt as _}; +use helix_core as core; +use helix_core::Transaction; +use helix_event::TaskHandle; +use helix_stdx::path::{self, canonicalize, fold_home_dir, get_path_suffix}; +use helix_view::Document; +use url::Url; + +use super::item::CompletionItem; + +pub(crate) fn path_completion( + cursor: usize, + text: core::Rope, + doc: &Document, + handle: TaskHandle, +) -> Option<BoxFuture<'static, anyhow::Result<Vec<CompletionItem>>>> { + if !doc.path_completion_enabled() { + return None; + } + + let cur_line = text.char_to_line(cursor); + let start = text.line_to_char(cur_line).max(cursor.saturating_sub(1000)); + let line_until_cursor = text.slice(start..cursor); + + let (dir_path, typed_file_name) = + get_path_suffix(line_until_cursor, false).and_then(|matched_path| { + let matched_path = Cow::from(matched_path); + let path: Cow<_> = if matched_path.starts_with("file://") { + Url::from_str(&matched_path) + .ok() + .and_then(|url| url.to_file_path().ok())? + .into() + } else { + Path::new(&*matched_path).into() + }; + let path = path::expand(&path); + let parent_dir = doc.path().and_then(|dp| dp.parent()); + let path = match parent_dir { + Some(parent_dir) if path.is_relative() => parent_dir.join(&path), + _ => path.into_owned(), + }; + #[cfg(windows)] + let ends_with_slash = matches!(matched_path.as_bytes().last(), Some(b'/' | b'\\')); + #[cfg(not(windows))] + let ends_with_slash = matches!(matched_path.as_bytes().last(), Some(b'/')); + + if ends_with_slash { + Some((PathBuf::from(path.as_path()), None)) + } else { + path.parent().map(|parent_path| { + ( + PathBuf::from(parent_path), + path.file_name().and_then(|f| f.to_str().map(String::from)), + ) + }) + } + })?; + + if handle.is_canceled() { + return None; + } + + let future = tokio::task::spawn_blocking(move || { + let Ok(read_dir) = std::fs::read_dir(&dir_path) else { + return Vec::new(); + }; + + read_dir + .filter_map(Result::ok) + .filter_map(|dir_entry| { + dir_entry + .metadata() + .ok() + .and_then(|md| Some((dir_entry.file_name().into_string().ok()?, md))) + }) + .map_while(|(file_name, md)| { + if handle.is_canceled() { + return None; + } + + let kind = path_kind(&md); + let documentation = path_documentation(&md, &dir_path.join(&file_name), kind); + + let edit_diff = typed_file_name + .as_ref() + .map(|f| f.len()) + .unwrap_or_default(); + + let transaction = Transaction::change( + &text, + std::iter::once((cursor - edit_diff, cursor, Some((&file_name).into()))), + ); + + Some(CompletionItem::Other(core::CompletionItem { + kind: Cow::Borrowed(kind), + label: file_name.into(), + transaction, + documentation, + })) + }) + .collect::<Vec<_>>() + }); + + Some(async move { Ok(future.await?) }.boxed()) +} + +#[cfg(unix)] +fn path_documentation(md: &fs::Metadata, full_path: &Path, kind: &str) -> String { + let full_path = fold_home_dir(canonicalize(full_path)); + let full_path_name = full_path.to_string_lossy(); + + use std::os::unix::prelude::PermissionsExt; + let mode = md.permissions().mode(); + + let perms = [ + (libc::S_IRUSR, 'r'), + (libc::S_IWUSR, 'w'), + (libc::S_IXUSR, 'x'), + (libc::S_IRGRP, 'r'), + (libc::S_IWGRP, 'w'), + (libc::S_IXGRP, 'x'), + (libc::S_IROTH, 'r'), + (libc::S_IWOTH, 'w'), + (libc::S_IXOTH, 'x'), + ] + .into_iter() + .fold(String::with_capacity(9), |mut acc, (p, s)| { + // This cast is necessary on some platforms such as macos as `mode_t` is u16 there + #[allow(clippy::unnecessary_cast)] + acc.push(if mode & (p as u32) > 0 { s } else { '-' }); + acc + }); + + // TODO it would be great to be able to individually color the documentation, + // but this will likely require a custom doc implementation (i.e. not `lsp::Documentation`) + // and/or different rendering in completion.rs + format!( + "type: `{kind}`\n\ + permissions: `[{perms}]`\n\ + full path: `{full_path_name}`", + ) +} + +#[cfg(not(unix))] +fn path_documentation(md: &fs::Metadata, full_path: &Path, kind: &str) -> String { + let full_path = fold_home_dir(canonicalize(full_path)); + let full_path_name = full_path.to_string_lossy(); + format!("type: `{kind}`\nfull path: `{full_path_name}`",) +} + +#[cfg(unix)] +fn path_kind(md: &fs::Metadata) -> &'static str { + if md.is_symlink() { + "link" + } else if md.is_dir() { + "folder" + } else { + use std::os::unix::fs::FileTypeExt; + if md.file_type().is_block_device() { + "block" + } else if md.file_type().is_socket() { + "socket" + } else if md.file_type().is_char_device() { + "char_device" + } else if md.file_type().is_fifo() { + "fifo" + } else { + "file" + } + } +} + +#[cfg(not(unix))] +fn path_kind(md: &fs::Metadata) -> &'static str { + if md.is_symlink() { + "link" + } else if md.is_dir() { + "folder" + } else { + "file" + } +} |