feat: cd path auto-completion supports ~ expansion (#1081)

This commit is contained in:
三咲雅 · Misaki Masa 2024-05-23 17:07:04 +08:00 committed by sxyazi
parent 58e5d2280a
commit 5f21998665
No known key found for this signature in database
6 changed files with 50 additions and 36 deletions

1
Cargo.lock generated
View file

@ -2769,6 +2769,7 @@ dependencies = [
"anyhow",
"bitflags 2.5.0",
"crossterm",
"dirs",
"futures",
"libc",
"notify",

View file

@ -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" ] }

View file

@ -10,6 +10,9 @@ pub struct Opt {
impl From<Cmd> for Opt {
fn from(c: Cmd) -> Self { Self { submit: c.bool("submit") } }
}
impl From<bool> for Opt {
fn from(submit: bool) -> Self { Self { submit } }
}
impl Completion {
pub fn close(&mut self, opt: impl Into<Opt>) {

View file

@ -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"));
}
}

View file

@ -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())
}

View file

@ -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() {