Optimization of tilde expansion (#9709)

* Use next and avoid a redundant prefix strip

* Avoid allocations

Especially when `expand_tilde` is claled on a path
that doesn't contain a tilde.

* Add a test

* Use Into<Cow<…>>

* Put the expand_tilde test at the end of the file

* Remove unused importsw
This commit is contained in:
Mo 2024-02-24 16:59:11 +01:00 committed by GitHub
parent ec9efdef3b
commit 6db666fce1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 57 additions and 22 deletions

View File

@ -53,7 +53,7 @@ fn prioritize_runtime_dirs() -> Vec<PathBuf> {
rt_dirs.push(conf_rt_dir); rt_dirs.push(conf_rt_dir);
if let Ok(dir) = std::env::var("HELIX_RUNTIME") { if let Ok(dir) = std::env::var("HELIX_RUNTIME") {
let dir = path::expand_tilde(dir); let dir = path::expand_tilde(Path::new(&dir));
rt_dirs.push(path::normalize(dir)); rt_dirs.push(path::normalize(dir));
} }

View File

@ -1,6 +1,9 @@
pub use etcetera::home_dir; pub use etcetera::home_dir;
use std::path::{Component, Path, PathBuf}; use std::{
borrow::Cow,
path::{Component, Path, PathBuf},
};
use crate::env::current_working_dir; use crate::env::current_working_dir;
@ -19,19 +22,22 @@ pub fn fold_home_dir(path: &Path) -> PathBuf {
/// Expands tilde `~` into users home directory if available, otherwise returns the path /// Expands tilde `~` into users home directory if available, otherwise returns the path
/// unchanged. The tilde will only be expanded when present as the first component of the path /// unchanged. The tilde will only be expanded when present as the first component of the path
/// and only slash follows it. /// and only slash follows it.
pub fn expand_tilde(path: impl AsRef<Path>) -> PathBuf { pub fn expand_tilde<'a, P>(path: P) -> Cow<'a, Path>
let path = path.as_ref(); where
let mut components = path.components().peekable(); P: Into<Cow<'a, Path>>,
if let Some(Component::Normal(c)) = components.peek() { {
if c == &"~" { let path = path.into();
if let Ok(home) = home_dir() { let mut components = path.components();
// it's ok to unwrap, the path starts with `~` if let Some(Component::Normal(c)) = components.next() {
return home.join(path.strip_prefix("~").unwrap()); if c == "~" {
if let Ok(mut buf) = home_dir() {
buf.push(components);
return Cow::Owned(buf);
} }
} }
} }
path.to_path_buf() path
} }
/// Normalize a path without resolving symlinks. /// Normalize a path without resolving symlinks.
@ -109,9 +115,9 @@ pub fn normalize(path: impl AsRef<Path>) -> PathBuf {
/// This function is used instead of [`std::fs::canonicalize`] because we don't want to verify /// This function is used instead of [`std::fs::canonicalize`] because we don't want to verify
/// here if the path exists, just normalize it's components. /// here if the path exists, just normalize it's components.
pub fn canonicalize(path: impl AsRef<Path>) -> PathBuf { pub fn canonicalize(path: impl AsRef<Path>) -> PathBuf {
let path = expand_tilde(path); let path = expand_tilde(path.as_ref());
let path = if path.is_relative() { let path = if path.is_relative() {
current_working_dir().join(path) Cow::Owned(current_working_dir().join(path))
} else { } else {
path path
}; };
@ -183,3 +189,32 @@ pub fn get_truncated_path(path: impl AsRef<Path>) -> PathBuf {
ret.push(file); ret.push(file);
ret ret
} }
#[cfg(test)]
mod tests {
use std::{
ffi::OsStr,
path::{Component, Path},
};
use crate::path;
#[test]
fn expand_tilde() {
for path in ["~", "~/foo"] {
let expanded = path::expand_tilde(Path::new(path));
let tilde = Component::Normal(OsStr::new("~"));
let mut component_count = 0;
for component in expanded.components() {
// No tilde left.
assert_ne!(component, tilde);
component_count += 1;
}
// The path was at least expanded to something.
assert_ne!(component_count, 0);
}
}
}

View File

@ -110,14 +110,14 @@ fn open(cx: &mut compositor::Context, args: &[Cow<str>], event: PromptEvent) ->
ensure!(!args.is_empty(), "wrong argument count"); ensure!(!args.is_empty(), "wrong argument count");
for arg in args { for arg in args {
let (path, pos) = args::parse_file(arg); let (path, pos) = args::parse_file(arg);
let path = helix_stdx::path::expand_tilde(&path); let path = helix_stdx::path::expand_tilde(path);
// If the path is a directory, open a file picker on that directory and update the status // If the path is a directory, open a file picker on that directory and update the status
// message // message
if let Ok(true) = std::fs::canonicalize(&path).map(|p| p.is_dir()) { if let Ok(true) = std::fs::canonicalize(&path).map(|p| p.is_dir()) {
let callback = async move { let callback = async move {
let call: job::Callback = job::Callback::EditorCompositor(Box::new( let call: job::Callback = job::Callback::EditorCompositor(Box::new(
move |editor: &mut Editor, compositor: &mut Compositor| { move |editor: &mut Editor, compositor: &mut Compositor| {
let picker = ui::file_picker(path, &editor.config()); let picker = ui::file_picker(path.into_owned(), &editor.config());
compositor.push(Box::new(overlaid(picker))); compositor.push(Box::new(overlaid(picker)));
}, },
)); ));
@ -1078,11 +1078,11 @@ fn change_current_directory(
return Ok(()); return Ok(());
} }
let dir = helix_stdx::path::expand_tilde( let dir = args
args.first() .first()
.context("target directory not provided")? .context("target directory not provided")?
.as_ref(), .as_ref();
); let dir = helix_stdx::path::expand_tilde(Path::new(dir));
helix_stdx::env::set_current_working_dir(dir)?; helix_stdx::env::set_current_working_dir(dir)?;

View File

@ -428,9 +428,9 @@ fn filename_impl<F>(
path path
} else { } else {
match path.parent() { match path.parent() {
Some(path) if !path.as_os_str().is_empty() => path.to_path_buf(), Some(path) if !path.as_os_str().is_empty() => Cow::Borrowed(path),
// Path::new("h")'s parent is Some("")... // Path::new("h")'s parent is Some("")...
_ => helix_stdx::env::current_working_dir(), _ => Cow::Owned(helix_stdx::env::current_working_dir()),
} }
}; };