feat: VFS support for the enter and leave commands (#3035)

This commit is contained in:
三咲雅 misaki masa 2025-08-05 20:45:23 +08:00 committed by GitHub
parent d5f225bc4a
commit 0b45ed868f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 119 additions and 88 deletions

36
Cargo.lock generated
View file

@ -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",

View file

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

View file

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

View file

@ -15,6 +15,8 @@ impl Actor for Enter {
fn act(cx: &mut Ctx, _: Self::Options) -> Result<Data> {
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))
}
}

View file

@ -38,7 +38,7 @@ impl Actor for HoverDo {
fn act(cx: &mut Ctx, opt: Self::Options) -> Result<Data> {
// 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

View file

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

View file

@ -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::<Self>()?.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)| {

View file

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

View file

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

View file

@ -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

View file

@ -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<Function> {
fn read_dir(lua: &Lua) -> mlua::Result<Function> {
lua.create_async_function(|lua, (dir, options): (UrlRef, Table)| async move {
// FIXME: VFS
let glob = if let Ok(s) = options.raw_get::<mlua::String>("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::<mlua::String>("glob") {
Some(Pattern::from_str(&s.to_str()?)?)
} else {
None
};
@ -132,14 +121,10 @@ fn read_dir(lua: &Lua) -> mlua::Result<Function> {
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<Function> {
} 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)
})
}

View file

@ -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<Path>) -> 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<Url> {
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<Url>) -> Option<Self> {
let base = base.as_ref();
if !self.scheme.covariant(&base.scheme) {
return None;
}
pub fn strip_prefix(&self, base: impl AsRef<Url>) -> 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]

View file

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