mirror of
https://github.com/helix-editor/helix.git
synced 2025-04-05 03:47:51 +03:00
Add support for path completion (#2608)
Co-authored-by: Michael Davis <mcarsondavis@gmail.com> Co-authored-by: Pascal Kuthe <pascalkuthe@pm.me>
This commit is contained in:
parent
f305c7299d
commit
dc941d6d24
23 changed files with 1124 additions and 212 deletions
|
@ -1,9 +1,12 @@
|
|||
use std::{
|
||||
ffi::OsStr,
|
||||
borrow::Cow,
|
||||
ffi::{OsStr, OsString},
|
||||
path::{Path, PathBuf},
|
||||
sync::RwLock,
|
||||
};
|
||||
|
||||
use once_cell::sync::Lazy;
|
||||
|
||||
static CWD: RwLock<Option<PathBuf>> = RwLock::new(None);
|
||||
|
||||
// Get the current working directory.
|
||||
|
@ -59,6 +62,93 @@ pub fn which<T: AsRef<OsStr>>(
|
|||
})
|
||||
}
|
||||
|
||||
fn find_brace_end(src: &[u8]) -> Option<usize> {
|
||||
use regex_automata::meta::Regex;
|
||||
|
||||
static REGEX: Lazy<Regex> = Lazy::new(|| Regex::builder().build("[{}]").unwrap());
|
||||
let mut depth = 0;
|
||||
for mat in REGEX.find_iter(src) {
|
||||
let pos = mat.start();
|
||||
match src[pos] {
|
||||
b'{' => depth += 1,
|
||||
b'}' if depth == 0 => return Some(pos),
|
||||
b'}' => depth -= 1,
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn expand_impl(src: &OsStr, mut resolve: impl FnMut(&OsStr) -> Option<OsString>) -> Cow<OsStr> {
|
||||
use regex_automata::meta::Regex;
|
||||
|
||||
static REGEX: Lazy<Regex> = Lazy::new(|| {
|
||||
Regex::builder()
|
||||
.build_many(&[
|
||||
r"\$\{([^\}:]+):-",
|
||||
r"\$\{([^\}:]+):=",
|
||||
r"\$\{([^\}-]+)-",
|
||||
r"\$\{([^\}=]+)=",
|
||||
r"\$\{([^\}]+)",
|
||||
r"\$(\w+)",
|
||||
])
|
||||
.unwrap()
|
||||
});
|
||||
|
||||
let bytes = src.as_encoded_bytes();
|
||||
let mut res = Vec::with_capacity(bytes.len());
|
||||
let mut pos = 0;
|
||||
for captures in REGEX.captures_iter(bytes) {
|
||||
let mat = captures.get_match().unwrap();
|
||||
let pattern_id = mat.pattern().as_usize();
|
||||
let mut range = mat.range();
|
||||
let var = &bytes[captures.get_group(1).unwrap().range()];
|
||||
let default = if pattern_id != 5 {
|
||||
let Some(bracket_pos) = find_brace_end(&bytes[range.end..]) else {
|
||||
break;
|
||||
};
|
||||
let default = &bytes[range.end..range.end + bracket_pos];
|
||||
range.end += bracket_pos + 1;
|
||||
default
|
||||
} else {
|
||||
&[]
|
||||
};
|
||||
// safety: this is a codepoint aligned substring of an osstr (always valid)
|
||||
let var = unsafe { OsStr::from_encoded_bytes_unchecked(var) };
|
||||
let expansion = resolve(var);
|
||||
let expansion = match &expansion {
|
||||
Some(val) => {
|
||||
if val.is_empty() && pattern_id < 2 {
|
||||
default
|
||||
} else {
|
||||
val.as_encoded_bytes()
|
||||
}
|
||||
}
|
||||
None => default,
|
||||
};
|
||||
res.extend_from_slice(&bytes[pos..range.start]);
|
||||
pos = range.end;
|
||||
res.extend_from_slice(expansion);
|
||||
}
|
||||
if pos == 0 {
|
||||
src.into()
|
||||
} else {
|
||||
res.extend_from_slice(&bytes[pos..]);
|
||||
// safety: this is a composition of valid osstr (and codepoint aligned slices which are also valid)
|
||||
unsafe { OsString::from_encoded_bytes_unchecked(res) }.into()
|
||||
}
|
||||
}
|
||||
|
||||
/// performs substitution of enviorment variables. Supports the following (POSIX) syntax:
|
||||
///
|
||||
/// * `$<var>`, `${<var>}`
|
||||
/// * `${<var>:-<default>}`, `${<var>-<default>}`
|
||||
/// * `${<var>:=<default>}`, `${<var>=default}`
|
||||
///
|
||||
pub fn expand<S: AsRef<OsStr> + ?Sized>(src: &S) -> Cow<OsStr> {
|
||||
expand_impl(src.as_ref(), |var| std::env::var_os(var))
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ExecutableNotFoundError {
|
||||
command: String,
|
||||
|
@ -75,7 +165,9 @@ impl std::error::Error for ExecutableNotFoundError {}
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{current_working_dir, set_current_working_dir};
|
||||
use std::ffi::{OsStr, OsString};
|
||||
|
||||
use super::{current_working_dir, expand_impl, set_current_working_dir};
|
||||
|
||||
#[test]
|
||||
fn current_dir_is_set() {
|
||||
|
@ -88,4 +180,34 @@ mod tests {
|
|||
let cwd = current_working_dir();
|
||||
assert_eq!(cwd, new_path);
|
||||
}
|
||||
|
||||
macro_rules! assert_env_expand {
|
||||
($env: expr, $lhs: expr, $rhs: expr) => {
|
||||
assert_eq!(&*expand_impl($lhs.as_ref(), $env), OsStr::new($rhs));
|
||||
};
|
||||
}
|
||||
|
||||
/// paths that should work on all platforms
|
||||
#[test]
|
||||
fn test_env_expand() {
|
||||
let env = |var: &OsStr| -> Option<OsString> {
|
||||
match var.to_str().unwrap() {
|
||||
"FOO" => Some("foo".into()),
|
||||
"EMPTY" => Some("".into()),
|
||||
_ => None,
|
||||
}
|
||||
};
|
||||
assert_env_expand!(env, "pass_trough", "pass_trough");
|
||||
assert_env_expand!(env, "$FOO", "foo");
|
||||
assert_env_expand!(env, "bar/$FOO/baz", "bar/foo/baz");
|
||||
assert_env_expand!(env, "bar/${FOO}/baz", "bar/foo/baz");
|
||||
assert_env_expand!(env, "baz/${BAR:-bar}/foo", "baz/bar/foo");
|
||||
assert_env_expand!(env, "baz/${BAR:=bar}/foo", "baz/bar/foo");
|
||||
assert_env_expand!(env, "baz/${BAR-bar}/foo", "baz/bar/foo");
|
||||
assert_env_expand!(env, "baz/${BAR=bar}/foo", "baz/bar/foo");
|
||||
assert_env_expand!(env, "baz/${EMPTY:-bar}/foo", "baz/bar/foo");
|
||||
assert_env_expand!(env, "baz/${EMPTY:=bar}/foo", "baz/bar/foo");
|
||||
assert_env_expand!(env, "baz/${EMPTY-bar}/foo", "baz//foo");
|
||||
assert_env_expand!(env, "baz/${EMPTY=bar}/foo", "baz//foo");
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue