From 5f21998665d78fe34e1e1440fc83214fed4fb1aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=89=E5=92=B2=E9=9B=85=20=C2=B7=20Misaki=20Masa?= Date: Thu, 23 May 2024 17:07:04 +0800 Subject: [PATCH] feat: `cd` path auto-completion supports `~` expansion (#1081) --- Cargo.lock | 1 + yazi-core/Cargo.toml | 3 +- yazi-core/src/completion/commands/close.rs | 3 + yazi-core/src/completion/commands/trigger.rs | 62 +++++++++++++------- yazi-core/src/manager/commands/refresh.rs | 10 +--- yazi-shared/src/fs/path.rs | 7 +-- 6 files changed, 50 insertions(+), 36 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ca66e99d..1ceea592 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2769,6 +2769,7 @@ dependencies = [ "anyhow", "bitflags 2.5.0", "crossterm", + "dirs", "futures", "libc", "notify", diff --git a/yazi-core/Cargo.toml b/yazi-core/Cargo.toml index 5bdf8f92..8c85f75a 100644 --- a/yazi-core/Cargo.toml +++ b/yazi-core/Cargo.toml @@ -22,6 +22,7 @@ yazi-shared = { path = "../yazi-shared", version = "0.2.5" } anyhow = "1.0.86" bitflags = "2.5.0" crossterm = "0.27.0" +dirs = "5.0.1" futures = "0.3.30" notify = { version = "6.1.1", default-features = false, features = [ "macos_fsevent" ] } parking_lot = "0.12.2" @@ -29,11 +30,11 @@ ratatui = "0.26.3" regex = "1.10.4" scopeguard = "1.2.0" serde = "1.0.202" +shell-words = "1.1.0" tokio = { version = "1.37.0", features = [ "full" ] } tokio-stream = "0.1.15" tokio-util = "0.7.11" unicode-width = "0.1.12" -shell-words = "1.1.0" # Logging tracing = { version = "0.1.40", features = [ "max_level_debug", "release_max_level_warn" ] } diff --git a/yazi-core/src/completion/commands/close.rs b/yazi-core/src/completion/commands/close.rs index d331978b..447e9ef7 100644 --- a/yazi-core/src/completion/commands/close.rs +++ b/yazi-core/src/completion/commands/close.rs @@ -10,6 +10,9 @@ pub struct Opt { impl From for Opt { fn from(c: Cmd) -> Self { Self { submit: c.bool("submit") } } } +impl From for Opt { + fn from(submit: bool) -> Self { Self { submit } } +} impl Completion { pub fn close(&mut self, opt: impl Into) { diff --git a/yazi-core/src/completion/commands/trigger.rs b/yazi-core/src/completion/commands/trigger.rs index 535fc3c7..788d5555 100644 --- a/yazi-core/src/completion/commands/trigger.rs +++ b/yazi-core/src/completion/commands/trigger.rs @@ -1,4 +1,4 @@ -use std::{mem, path::{MAIN_SEPARATOR, MAIN_SEPARATOR_STR}}; +use std::{borrow::Cow, mem, path::{MAIN_SEPARATOR, MAIN_SEPARATOR_STR}}; use tokio::fs; use yazi_shared::{emit, event::{Cmd, Data}, render, Layer}; @@ -33,7 +33,9 @@ impl Completion { } self.ticket = opt.ticket; - let (parent, child) = Self::split_path(&opt.word); + let Some((parent, child)) = Self::split_path(&opt.word) else { + return self.close(false); + }; if self.caches.contains_key(&parent) { return self.show( @@ -72,12 +74,24 @@ impl Completion { render!(mem::replace(&mut self.visible, false)); } - #[inline] - fn split_path(s: &str) -> (String, String) { - match s.rsplit_once(SEPARATOR) { - Some((p, c)) => (format!("{p}{}", MAIN_SEPARATOR), c.to_owned()), - None => (".".to_owned(), s.to_owned()), + fn split_path(s: &str) -> Option<(String, String)> { + if s == "~" { + return None; // We don't autocomplete a `~`, but `~/` } + + let s = if let Some(rest) = s.strip_prefix("~") { + Cow::Owned(format!( + "{}{rest}", + dirs::home_dir().unwrap_or_default().to_string_lossy().trim_end_matches(SEPARATOR), + )) + } else { + Cow::Borrowed(s) + }; + + Some(match s.rsplit_once(SEPARATOR) { + Some((p, c)) => (format!("{p}{}", MAIN_SEPARATOR), c.to_owned()), + None => (".".to_owned(), s.into_owned()), + }) } } @@ -85,28 +99,32 @@ impl Completion { mod tests { use super::*; + fn compare(s: &str, parent: &str, child: &str) -> bool { + matches!(Completion::split_path(s), Some((p, c)) if p == parent && c == child) + } + #[cfg(unix)] #[test] fn test_split() { - assert_eq!(Completion::split_path(""), (".".to_owned(), "".to_owned())); - assert_eq!(Completion::split_path(" "), (".".to_owned(), " ".to_owned())); - assert_eq!(Completion::split_path("/"), ("/".to_owned(), "".to_owned())); - assert_eq!(Completion::split_path("//"), ("//".to_owned(), "".to_owned())); - assert_eq!(Completion::split_path("/foo"), ("/".to_owned(), "foo".to_owned())); - assert_eq!(Completion::split_path("/foo/"), ("/foo/".to_owned(), "".to_owned())); - assert_eq!(Completion::split_path("/foo/bar"), ("/foo/".to_owned(), "bar".to_owned())); + assert!(compare("", ".", "")); + assert!(compare(" ", ".", " ")); + assert!(compare("/", "/", "")); + assert!(compare("//", "//", "")); + assert!(compare("/foo", "/", "foo")); + assert!(compare("/foo/", "/foo/", "")); + assert!(compare("/foo/bar", "/foo/", "bar")); } #[cfg(windows)] #[test] fn test_split() { - assert_eq!(Completion::split_path("foo"), (".".to_owned(), "foo".to_owned())); - assert_eq!(Completion::split_path("foo\\"), ("foo\\".to_owned(), "".to_owned())); - assert_eq!(Completion::split_path("foo\\bar"), ("foo\\".to_owned(), "bar".to_owned())); - assert_eq!(Completion::split_path("foo\\bar\\"), ("foo\\bar\\".to_owned(), "".to_owned())); - assert_eq!(Completion::split_path("C:\\"), ("C:\\".to_owned(), "".to_owned())); - assert_eq!(Completion::split_path("C:\\foo"), ("C:\\".to_owned(), "foo".to_owned())); - assert_eq!(Completion::split_path("C:\\foo\\"), ("C:\\foo\\".to_owned(), "".to_owned())); - assert_eq!(Completion::split_path("C:\\foo\\bar"), ("C:\\foo\\".to_owned(), "bar".to_owned())); + assert!(compare("foo", ".", "foo")); + assert!(compare("foo\\", "foo\\", "")); + assert!(compare("foo\\bar", "foo\\", "bar")); + assert!(compare("foo\\bar\\", "foo\\bar\\", "")); + assert!(compare("C:\\", "C:\\", "")); + assert!(compare("C:\\foo", "C:\\", "foo")); + assert!(compare("C:\\foo\\", "C:\\foo\\", "")); + assert!(compare("C:\\foo\\bar", "C:\\foo\\", "bar")); } } diff --git a/yazi-core/src/manager/commands/refresh.rs b/yazi-core/src/manager/commands/refresh.rs index 7724116c..8076c53e 100644 --- a/yazi-core/src/manager/commands/refresh.rs +++ b/yazi-core/src/manager/commands/refresh.rs @@ -1,4 +1,4 @@ -use std::env; +use std::{env, path::MAIN_SEPARATOR}; use crossterm::{execute, terminal::SetTitle}; use yazi_shared::event::Cmd; @@ -7,13 +7,9 @@ use crate::{manager::Manager, tasks::Tasks}; impl Manager { fn title(&self) -> String { - #[cfg(unix)] - let home = env::var_os("HOME").unwrap_or_default(); - #[cfg(windows)] - let home = env::var_os("USERPROFILE").unwrap_or_default(); - + let home = dirs::home_dir().unwrap_or_default(); if let Some(p) = self.cwd().strip_prefix(home) { - format!("Yazi: ~/{}", p.display()) + format!("Yazi: ~{}{}", MAIN_SEPARATOR, p.display()) } else { format!("Yazi: {}", self.cwd().display()) } diff --git a/yazi-shared/src/fs/path.rs b/yazi-shared/src/fs/path.rs index 91c07cb4..d21847ae 100644 --- a/yazi-shared/src/fs/path.rs +++ b/yazi-shared/src/fs/path.rs @@ -37,12 +37,7 @@ fn _expand_path(p: &Path) -> PathBuf { let p = Path::new(s.as_ref()); if let Ok(rest) = p.strip_prefix("~") { - #[cfg(unix)] - let home = env::var_os("HOME"); - #[cfg(windows)] - let home = env::var_os("USERPROFILE"); - - return if let Some(p) = home { PathBuf::from(p).join(rest) } else { rest.to_path_buf() }; + return dirs::home_dir().unwrap_or_default().join(rest); } if p.is_absolute() {