From 0b45ed868f2965f585abd30fb976550712d4aa60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=89=E5=92=B2=E9=9B=85=20misaki=20masa?= Date: Tue, 5 Aug 2025 20:45:23 +0800 Subject: [PATCH] feat: VFS support for the `enter` and `leave` commands (#3035) --- Cargo.lock | 36 ++++++++-------- Cargo.toml | 6 +-- yazi-actor/src/cmp/trigger.rs | 6 ++- yazi-actor/src/mgr/enter.rs | 4 +- yazi-actor/src/mgr/hover.rs | 2 +- yazi-actor/src/mgr/leave.rs | 7 +++- yazi-binding/src/url.rs | 4 +- yazi-config/src/pattern.rs | 10 ++--- yazi-core/Cargo.toml | 2 +- yazi-core/src/tab/tab.rs | 6 ++- yazi-plugin/src/fs/fs.rs | 42 ++++++------------- yazi-shared/src/url/url.rs | 79 +++++++++++++++++++++++++---------- yazi-shared/src/url/urn.rs | 3 ++ 13 files changed, 119 insertions(+), 88 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b6ddd520..dc16be74 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -352,9 +352,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.30" +version = "1.2.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "deec109607ca693028562ed836a5f1c4b8bd77755c4e132fc5ce11b0b6211ae7" +checksum = "c3a42d84bb6b69d3a8b3eaacf0d88f179e1929695e1ad012b6cf64d9caaa5fd2" dependencies = [ "jobserver", "libc", @@ -1335,9 +1335,9 @@ dependencies = [ [[package]] name = "libredox" -version = "0.1.8" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "360e552c93fa0e8152ab463bc4c4837fce76a225df11dfaeea66c313de5e61f7" +checksum = "391290121bad3d37fbddad76d8f5d1c1c314cfc646d143d7e07a3086ddff0ce3" dependencies = [ "bitflags 2.9.1", "libc", @@ -1557,9 +1557,9 @@ checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8" [[package]] name = "notify" -version = "8.1.0" +version = "8.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3163f59cd3fa0e9ef8c32f242966a7b9994fd7378366099593e0e73077cd8c97" +checksum = "4d3d07927151ff8575b7087f245456e549fea62edf0ec4e565a5ee50c8402bc3" dependencies = [ "bitflags 2.9.1", "fsevent-sys", @@ -1984,9 +1984,9 @@ checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" [[package]] name = "quick-xml" -version = "0.38.0" +version = "0.38.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8927b0664f5c5a98265138b7e3f90aa19a6b21353182469ace36d4ac527b7b1b" +checksum = "9845d9dccf565065824e69f9f235fafba1587031eda353c1f1561cd6a6be78f4" dependencies = [ "memchr", ] @@ -2164,9 +2164,9 @@ dependencies = [ [[package]] name = "redox_users" -version = "0.5.0" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b" +checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" dependencies = [ "getrandom 0.2.16", "libredox", @@ -2344,9 +2344,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.141" +version = "1.0.142" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30b9eff21ebe718216c6ec64e1d9ac57087aad11efc64e32002bce4a0d4c03d3" +checksum = "030fedb782600dcbd6f02d479bf0d817ac3bb40d644745b769d6a96bc3afc5a7" dependencies = [ "itoa", "memchr", @@ -2410,9 +2410,9 @@ dependencies = [ [[package]] name = "signal-hook-registry" -version = "1.4.5" +version = "1.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9203b8055f63a2a00e2f593bb0510367fe707d7ff1e5c872de2f537b339e5410" +checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" dependencies = [ "libc", ] @@ -2684,9 +2684,9 @@ dependencies = [ [[package]] name = "tokio" -version = "1.47.0" +version = "1.47.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43864ed400b6043a4757a25c7a64a8efde741aed79a056a2fb348a406701bb35" +checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038" dependencies = [ "backtrace", "bytes", @@ -2726,9 +2726,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.15" +version = "0.7.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66a539a9ad6d5d281510d5bd368c973d636c02dbf8a67300bfb6b950696ad7df" +checksum = "14307c986784f72ef81c89db7d9e28d6ac26d16213b109ea501696195e6e3ce5" dependencies = [ "bytes", "futures-core", diff --git a/Cargo.toml b/Cargo.toml index 7be4ade9..23332ee3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,11 +43,11 @@ ratatui = { version = "0.29.0", features = [ "unstable-rendered-line regex = "1.11.1" scopeguard = "1.2.0" serde = { version = "1.0.219", features = [ "derive" ] } -serde_json = "1.0.141" +serde_json = "1.0.142" syntect = { version = "5.2.0", default-features = false, features = [ "parsing", "plist-load", "regex-onig" ] } -tokio = { version = "1.47.0", features = [ "full" ] } +tokio = { version = "1.47.1", features = [ "full" ] } tokio-stream = "0.1.17" -tokio-util = "0.7.15" +tokio-util = "0.7.16" toml = { version = "0.9.4" } tracing = { version = "0.1.41", features = [ "max_level_debug", "release_max_level_debug" ] } twox-hash = { version = "2.1.1", default-features = false, features = [ "std", "random", "xxhash3_128" ] } diff --git a/yazi-actor/src/cmp/trigger.rs b/yazi-actor/src/cmp/trigger.rs index d7f838c7..f987920c 100644 --- a/yazi-actor/src/cmp/trigger.rs +++ b/yazi-actor/src/cmp/trigger.rs @@ -93,8 +93,10 @@ mod tests { use super::*; fn compare(s: &str, parent: &str, child: &str) { - let (p, c) = Trigger::split_url(s).unwrap(); - let p = p.strip_prefix(yazi_fs::CWD.load().as_ref()).unwrap_or(p); + let (mut p, c) = Trigger::split_url(s).unwrap(); + if let Some(u) = p.strip_prefix(yazi_fs::CWD.load().as_ref()) { + p = Url::from(&**u); + } assert_eq!((p, c.as_urn()), (parent.parse().unwrap(), Urn::new(child))); } diff --git a/yazi-actor/src/mgr/enter.rs b/yazi-actor/src/mgr/enter.rs index c6afb1e3..7fd43280 100644 --- a/yazi-actor/src/mgr/enter.rs +++ b/yazi-actor/src/mgr/enter.rs @@ -15,6 +15,8 @@ impl Actor for Enter { fn act(cx: &mut Ctx, _: Self::Options) -> Result { let Some(h) = cx.hovered().filter(|h| h.is_dir()) else { succ!() }; - act!(mgr:cd, cx, (h.url.to_regular(), CdSource::Enter)) + let url = if h.url.is_search() { h.url.to_regular() } else { h.url.clone() }; + + act!(mgr:cd, cx, (url, CdSource::Enter)) } } diff --git a/yazi-actor/src/mgr/hover.rs b/yazi-actor/src/mgr/hover.rs index ec2a6d81..0d7afab7 100644 --- a/yazi-actor/src/mgr/hover.rs +++ b/yazi-actor/src/mgr/hover.rs @@ -38,7 +38,7 @@ impl Actor for HoverDo { fn act(cx: &mut Ctx, opt: Self::Options) -> Result { // Hover on the file if let Some(u) = opt.url.strip_prefix(cx.cwd()) { - render!(cx.current_mut().hover(u.urn())); + render!(cx.current_mut().hover(u)); } // Turn on tracing diff --git a/yazi-actor/src/mgr/leave.rs b/yazi-actor/src/mgr/leave.rs index 0d005582..8aa7a04a 100644 --- a/yazi-actor/src/mgr/leave.rs +++ b/yazi-actor/src/mgr/leave.rs @@ -19,6 +19,11 @@ impl Actor for Leave { .filter(|u| u != cx.cwd()) .or_else(|| cx.cwd().parent_url()); - if let Some(u) = url { act!(mgr:cd, cx, (u.into_regular(), CdSource::Leave)) } else { succ!() } + let Some(mut url) = url else { succ!() }; + if url.is_search() { + url = url.into_regular(); + } + + act!(mgr:cd, cx, (url, CdSource::Leave)) } } diff --git a/yazi-binding/src/url.rs b/yazi-binding/src/url.rs index 7bf73728..0dde1e69 100644 --- a/yazi-binding/src/url.rs +++ b/yazi-binding/src/url.rs @@ -146,12 +146,12 @@ impl UserData for Url { }) }); methods.add_method("strip_prefix", |_, me, base: Value| { - let url = match base { + let urn = match base { Value::String(s) => me.strip_prefix(Self::try_from(s.as_bytes().as_ref())?), Value::UserData(ud) => me.strip_prefix(&ud.borrow::()?.inner), _ => Err("must be a string or a Url".into_lua_err())?, }; - Ok(url.map(Self::new)) + Ok(urn.map(Deref::deref).map(Self::new)) // TODO: return `Urn` instead of `Url` }); methods.add_function_mut("into_search", |_, (ud, domain): (AnyUserData, mlua::String)| { diff --git a/yazi-config/src/pattern.rs b/yazi-config/src/pattern.rs index 7bdae907..395754bf 100644 --- a/yazi-config/src/pattern.rs +++ b/yazi-config/src/pattern.rs @@ -8,12 +8,12 @@ use yazi_shared::url::{Scheme, Url}; #[derive(Debug, Deserialize)] #[serde(try_from = "String")] pub struct Pattern { - inner: globset::GlobMatcher, - scheme: PatternScheme, - is_dir: bool, - is_star: bool, + inner: globset::GlobMatcher, + scheme: PatternScheme, + pub is_dir: bool, + is_star: bool, #[cfg(windows)] - sep_lit: bool, + sep_lit: bool, } impl Pattern { diff --git a/yazi-core/Cargo.toml b/yazi-core/Cargo.toml index 3aff4bbf..e29df4db 100644 --- a/yazi-core/Cargo.toml +++ b/yazi-core/Cargo.toml @@ -26,7 +26,7 @@ anyhow = { workspace = true } crossterm = { workspace = true } futures = { workspace = true } indexmap = { workspace = true } -notify = { version = "8.1.0", default-features = false, features = [ "macos_fsevent" ] } +notify = { version = "8.2.0", default-features = false, features = [ "macos_fsevent" ] } parking_lot = { workspace = true } ratatui = { workspace = true } tokio = { workspace = true } diff --git a/yazi-core/src/tab/tab.rs b/yazi-core/src/tab/tab.rs index 9d4af76b..87de1323 100644 --- a/yazi-core/src/tab/tab.rs +++ b/yazi-core/src/tab/tab.rs @@ -7,7 +7,7 @@ use yazi_adapter::Dimension; use yazi_config::{LAYOUT, popup::{Origin, Position}}; use yazi_fs::{File, FolderStage}; use yazi_macro::render; -use yazi_shared::{Id, Ids, url::Url}; +use yazi_shared::{Id, Ids, url::{Url, Urn}}; use super::{Backstack, Finder, Folder, History, Mode, Preference, Preview}; use crate::{spot::Spot, tab::Selected}; @@ -125,7 +125,9 @@ impl Tab { if let Some(parent) = &mut self.parent { apply(parent); - parent.hover(self.current.url.urn()); // The parent should always track the CWD + + // The parent should always track the CWD + parent.hover(self.current.url.strip_prefix(&parent.url).unwrap_or(Urn::new(""))); } self diff --git a/yazi-plugin/src/fs/fs.rs b/yazi-plugin/src/fs/fs.rs index 900dd903..83ceb6f6 100644 --- a/yazi-plugin/src/fs/fs.rs +++ b/yazi-plugin/src/fs/fs.rs @@ -1,8 +1,8 @@ -use std::borrow::Cow; +use std::{borrow::Cow, str::FromStr}; -use globset::GlobBuilder; -use mlua::{ExternalError, ExternalResult, Function, IntoLua, IntoLuaMulti, Lua, Table, Value}; +use mlua::{ExternalError, Function, IntoLua, IntoLuaMulti, Lua, Table, Value}; use yazi_binding::{Cha, Composer, ComposerGet, ComposerSet, Error, File, Url, UrlRef}; +use yazi_config::Pattern; use yazi_fs::{mounts::PARTITIONS, provider, remove_dir_clean}; use crate::bindings::SizeCalculator; @@ -105,19 +105,8 @@ fn remove(lua: &Lua) -> mlua::Result { fn read_dir(lua: &Lua) -> mlua::Result { lua.create_async_function(|lua, (dir, options): (UrlRef, Table)| async move { - // FIXME: VFS - let glob = if let Ok(s) = options.raw_get::("glob") { - return Err("`glob` is disabled temporarily".into_lua_err()); - Some( - GlobBuilder::new(&s.to_str()?) - .case_insensitive(true) - .literal_separator(true) - .backslash_escape(false) - .empty_alternates(true) - .build() - .into_lua_err()? - .compile_matcher(), - ) + let pat = if let Ok(s) = options.raw_get::("glob") { + Some(Pattern::from_str(&s.to_str()?)?) } else { None }; @@ -132,14 +121,10 @@ fn read_dir(lua: &Lua) -> mlua::Result { let mut files = vec![]; while let Ok(Some(next)) = it.next_entry().await { - if files.len() >= limit { - break; - } - let url = next.url(); - // if glob.as_ref().is_some_and(|g| !g.is_match(&path)) { - // continue; - // } + if pat.as_ref().is_some_and(|p| !p.match_url(&url, p.is_dir)) { + continue; + } let file = if !resolve { yazi_fs::File::from_dummy(url, next.file_type().await.ok()) @@ -148,15 +133,14 @@ fn read_dir(lua: &Lua) -> mlua::Result { } else { yazi_fs::File::from_dummy(url, next.file_type().await.ok()) }; + files.push(File::new(file)); + if files.len() == limit { + break; + } } - let tbl = lua.create_table_with_capacity(files.len(), 0)?; - for f in files { - tbl.raw_push(f)?; - } - - tbl.into_lua_multi(&lua) + lua.create_sequence_from(files)?.into_lua_multi(&lua) }) } diff --git a/yazi-shared/src/url/url.rs b/yazi-shared/src/url/url.rs index 6abb58eb..740a925e 100644 --- a/yazi-shared/src/url/url.rs +++ b/yazi-shared/src/url/url.rs @@ -94,21 +94,26 @@ impl Url { #[inline] pub fn base(&self) -> Url { + use Scheme as S; + let loc: Loc = self.loc.base().into(); - match &self.scheme { - Scheme::Regular => Self { loc, scheme: Scheme::Regular }, - Scheme::Search(_) => self.with(loc), - Scheme::Archive(_) => self.with(loc), - Scheme::Sftp(_) => self.with(loc), + match self.scheme { + S::Regular => Self { loc, scheme: S::Regular }, + S::Search(_) => self.with(loc), + S::Archive(_) => self.with(loc), + S::Sftp(_) => self.with(loc), } } pub fn join(&self, path: impl AsRef) -> Self { + use Scheme as S; + + let loc: Loc = self.loc.join(path).into(); match self.scheme { - Scheme::Regular => Self { loc: self.loc.join(path).into(), scheme: Scheme::Regular }, - Scheme::Search(_) => self.with(self.loc.join(path)), - Scheme::Archive(_) => self.with(self.loc.join(path)), - Scheme::Sftp(_) => self.with(self.loc.join(path)), + S::Regular => Self { loc, scheme: S::Regular }, + S::Search(_) => self.with(loc), + S::Archive(_) => self.with(loc), + S::Sftp(_) => Self { loc, scheme: self.scheme.clone() }, } } @@ -127,27 +132,55 @@ impl Url { pub fn os_str(&self) -> Cow<'_, OsStr> { self.components().os_str() } pub fn parent_url(&self) -> Option { + use Scheme as S; + let parent = self.loc.parent()?; let urn = self.loc.urn(); - Some(match &self.scheme { - Scheme::Regular => Self { loc: parent.into(), scheme: Scheme::Regular }, - Scheme::Search(_) if urn == Urn::new("") => { - Self { loc: parent.into(), scheme: Scheme::Regular } - } - Scheme::Search(_) => self.with(parent), - Scheme::Archive(_) => self.with(parent), - Scheme::Sftp(_) => self.with(parent), + Some(match self.scheme { + S::Regular => Self { loc: parent.into(), scheme: S::Regular }, + + S::Search(_) if urn.is_empty() => Self { loc: parent.into(), scheme: S::Regular }, + S::Search(_) => self.with(parent), + + S::Archive(_) if urn.is_empty() => Self { loc: parent.into(), scheme: S::Regular }, + S::Archive(_) => self.with(parent), + + S::Sftp(_) => Self { loc: parent.into(), scheme: self.scheme.clone() }, }) } - pub fn strip_prefix(&self, base: impl AsRef) -> Option { - let base = base.as_ref(); - if !self.scheme.covariant(&base.scheme) { - return None; - } + pub fn strip_prefix(&self, base: impl AsRef) -> Option<&Urn> { + use Scheme as S; - Some(self.with(self.loc.strip_prefix(&base.loc).ok()?)) + let base = base.as_ref(); + let prefix = self.loc.strip_prefix(&base.loc).ok()?; + + Some(Urn::new(match (&self.scheme, &base.scheme) { + // Same scheme + (S::Regular, S::Regular) => Some(prefix), + (S::Search(_), S::Search(_)) => Some(prefix), + (S::Archive(a), S::Archive(b)) => Some(prefix).filter(|_| a == b), + (S::Sftp(a), S::Sftp(b)) => Some(prefix).filter(|_| a == b), + + // Both are local files + (S::Regular, S::Search(_)) => Some(prefix), + (S::Search(_), S::Regular) => Some(prefix), + + // Only the entry of archives is a local file + (S::Regular, S::Archive(_)) => Some(prefix).filter(|_| base.urn().is_empty()), + (S::Search(_), S::Archive(_)) => Some(prefix).filter(|_| base.urn().is_empty()), + (S::Archive(_), S::Regular) => Some(prefix).filter(|_| self.urn().is_empty()), + (S::Archive(_), S::Search(_)) => Some(prefix).filter(|_| self.urn().is_empty()), + + // Independent virtual file space + (S::Regular, S::Sftp(_)) => None, + (S::Search(_), S::Sftp(_)) => None, + (S::Archive(_), S::Sftp(_)) => None, + (S::Sftp(_), S::Regular) => None, + (S::Sftp(_), S::Search(_)) => None, + (S::Sftp(_), S::Archive(_)) => None, + }?)) } #[inline] diff --git a/yazi-shared/src/url/urn.rs b/yazi-shared/src/url/urn.rs index 746403c6..98ce60af 100644 --- a/yazi-shared/src/url/urn.rs +++ b/yazi-shared/src/url/urn.rs @@ -24,6 +24,9 @@ impl Urn { use std::os::unix::ffi::OsStrExt; self.name().is_some_and(|s| s.as_bytes().starts_with(b".")) } + + #[inline] + pub fn is_empty(&self) -> bool { self.0.as_os_str().is_empty() } } impl Deref for Urn {