mirror of https://github.com/helix-editor/helix
Add support for path completion (#2608)
Co-authored-by: Michael Davis <mcarsondavis@gmail.com> Co-authored-by: Pascal Kuthe <pascalkuthe@pm.me>pull/11315/head^2
parent
f305c7299d
commit
dc941d6d24
@ -0,0 +1,12 @@
|
|||||||
|
use std::borrow::Cow;
|
||||||
|
|
||||||
|
use crate::Transaction;
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Clone)]
|
||||||
|
pub struct CompletionItem {
|
||||||
|
pub transaction: Transaction,
|
||||||
|
pub label: Cow<'static, str>,
|
||||||
|
pub kind: Cow<'static, str>,
|
||||||
|
/// Containing Markdown
|
||||||
|
pub documentation: String,
|
||||||
|
}
|
@ -0,0 +1,41 @@
|
|||||||
|
use helix_lsp::{lsp, LanguageServerId};
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Clone)]
|
||||||
|
pub struct LspCompletionItem {
|
||||||
|
pub item: lsp::CompletionItem,
|
||||||
|
pub provider: LanguageServerId,
|
||||||
|
pub resolved: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Clone)]
|
||||||
|
pub enum CompletionItem {
|
||||||
|
Lsp(LspCompletionItem),
|
||||||
|
Other(helix_core::CompletionItem),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialEq<CompletionItem> for LspCompletionItem {
|
||||||
|
fn eq(&self, other: &CompletionItem) -> bool {
|
||||||
|
match other {
|
||||||
|
CompletionItem::Lsp(other) => self == other,
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialEq<CompletionItem> for helix_core::CompletionItem {
|
||||||
|
fn eq(&self, other: &CompletionItem) -> bool {
|
||||||
|
match other {
|
||||||
|
CompletionItem::Other(other) => self == other,
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CompletionItem {
|
||||||
|
pub fn preselect(&self) -> bool {
|
||||||
|
match self {
|
||||||
|
CompletionItem::Lsp(LspCompletionItem { item, .. }) => item.preselect.unwrap_or(false),
|
||||||
|
CompletionItem::Other(_) => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue