diff --git a/yazi-core/src/manager/commands/bulk_rename.rs b/yazi-core/src/manager/commands/bulk_rename.rs index 2aaafd4cd..09385a98c 100644 --- a/yazi-core/src/manager/commands/bulk_rename.rs +++ b/yazi-core/src/manager/commands/bulk_rename.rs @@ -6,7 +6,7 @@ use tokio::{fs::{self, OpenOptions}, io::{stdin, AsyncReadExt, AsyncWriteExt}}; use yazi_config::{OPEN, PREVIEW}; use yazi_dds::Pubsub; use yazi_proxy::{AppProxy, TasksProxy, HIDER, WATCHER}; -use yazi_shared::{fs::{max_common_root, maybe_exists, paths_to_same_file, File, FilesOp, Url}, terminal_clear}; +use yazi_shared::{fs::{max_common_root, maybe_exists, paths_to_same_file, File, Url}, terminal_clear}; use crate::manager::Manager; @@ -98,7 +98,8 @@ impl Manager { // FIXME: consider old and new in the different directories if !succeeded.is_empty() { Pubsub::pub_from_bulk(succeeded.iter().map(|(u, f)| (u, f.url())).collect()); - FilesOp::Upserting(cwd, succeeded).emit(); + // FIXME 0 + // FilesOp::Upserting(cwd, succeeded).emit(); } drop(permit); diff --git a/yazi-core/src/manager/commands/create.rs b/yazi-core/src/manager/commands/create.rs index 6e4ade2ab..586a7e701 100644 --- a/yazi-core/src/manager/commands/create.rs +++ b/yazi-core/src/manager/commands/create.rs @@ -1,10 +1,10 @@ -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use anyhow::Result; use tokio::fs; use yazi_config::popup::{ConfirmCfg, InputCfg}; use yazi_proxy::{ConfirmProxy, InputProxy, TabProxy, WATCHER}; -use yazi_shared::{event::Cmd, fs::{maybe_exists, ok_or_not_found, symlink_realpath, File, FilesOp, Url}}; +use yazi_shared::{event::Cmd, fs::{maybe_exists, ok_or_not_found, realname, File, FilesOp, Url, UrnBuf}}; use crate::manager::Manager; @@ -48,9 +48,9 @@ impl Manager { if dir { fs::create_dir_all(&new).await?; - } else if let Ok(real) = symlink_realpath(&new).await { + } else if let Some(real) = realname(&new).await { ok_or_not_found(fs::remove_file(&new).await)?; - FilesOp::Deleting(parent.clone(), vec![Url::from(real)]).emit(); + FilesOp::Deleting(parent.clone(), HashSet::from_iter([UrnBuf::_from(real)])).emit(); fs::File::create(&new).await?; } else { fs::create_dir_all(&parent).await.ok(); @@ -59,7 +59,7 @@ impl Manager { } if let Ok(f) = File::from(new.clone()).await { - FilesOp::Upserting(parent, HashMap::from_iter([(f.url_owned(), f)])).emit(); + FilesOp::Upserting(parent, HashMap::from_iter([(f.urn_owned(), f)])).emit(); TabProxy::reveal(&new) } Ok(()) diff --git a/yazi-core/src/manager/commands/rename.rs b/yazi-core/src/manager/commands/rename.rs index eb1aca5e8..bb49be349 100644 --- a/yazi-core/src/manager/commands/rename.rs +++ b/yazi-core/src/manager/commands/rename.rs @@ -1,11 +1,11 @@ -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use anyhow::Result; use tokio::fs; use yazi_config::popup::{ConfirmCfg, InputCfg}; use yazi_dds::Pubsub; use yazi_proxy::{ConfirmProxy, InputProxy, TabProxy, WATCHER}; -use yazi_shared::{event::Cmd, fs::{maybe_exists, ok_or_not_found, paths_to_same_file, symlink_realpath, File, FilesOp, Url}}; +use yazi_shared::{event::Cmd, fs::{maybe_exists, ok_or_not_found, paths_to_same_file, realname, File, FilesOp, Url, UrnBuf}}; use crate::manager::Manager; @@ -74,22 +74,27 @@ impl Manager { } async fn rename_do(tab: usize, old: Url, new: Url) -> Result<()> { - let Some(p_old) = old.parent_url() else { return Ok(()) }; - let Some(p_new) = new.parent_url() else { return Ok(()) }; + let Some((p_old, n_old)) = old.pair() else { return Ok(()) }; + let Some((p_new, n_new)) = new.pair() else { return Ok(()) }; let _permit = WATCHER.acquire().await.unwrap(); - let overwritten = symlink_realpath(&new).await; + let overwritten = realname(&new).await; fs::rename(&old, &new).await?; - if let Ok(o) = overwritten { - ok_or_not_found(fs::rename(&o, &new).await)?; - FilesOp::Deleting(p_new.clone(), vec![Url::from(o)]).emit(); + if let Some(o) = overwritten { + ok_or_not_found(fs::rename(p_new.join(&o), &new).await)?; + FilesOp::Deleting(p_new.clone(), HashSet::from_iter([UrnBuf::_from(o)])).emit(); } Pubsub::pub_from_rename(tab, &old, &new); let file = File::from(new.clone()).await?; - FilesOp::Deleting(p_old, vec![old]).emit(); - FilesOp::Upserting(p_new, HashMap::from_iter([(new.clone(), file)])).emit(); + if p_new == p_old { + FilesOp::Upserting(p_old, HashMap::from_iter([(n_old, file)])).emit(); + } else { + FilesOp::Deleting(p_old, HashSet::from_iter([n_old])).emit(); + FilesOp::Upserting(p_new, HashMap::from_iter([(n_new, file)])).emit(); + } + Ok(TabProxy::reveal(&new)) } diff --git a/yazi-core/src/manager/commands/update_files.rs b/yazi-core/src/manager/commands/update_files.rs index 8f98b22a6..934ebe331 100644 --- a/yazi-core/src/manager/commands/update_files.rs +++ b/yazi-core/src/manager/commands/update_files.rs @@ -26,7 +26,7 @@ impl Manager { let mut ops = vec![opt.op]; for u in LINKED.read().from_dir(ops[0].cwd()) { - ops.push(ops[0].chroot(u)); + ops.push(ops[0].rebase(u)); } for op in ops { @@ -60,10 +60,7 @@ impl Manager { fn update_parent(tab: &mut Tab, op: Cow) { let urn = tab.cwd().urn_owned(); - // FIXME - let leave = false; - // let leave = matches!(*op, FilesOp::Deleting(_, ref urls) if - // urls.contains(&urn)); + let leave = matches!(*op, FilesOp::Deleting(_, ref urns) if urns.contains(&urn)); if let Some(f) = tab.parent.as_mut() { render!(f.update(op.into_owned())); @@ -111,8 +108,8 @@ impl Manager { } fn update_history(tab: &mut Tab, op: Cow) { - let leave = tab.parent.as_ref().and_then(|f| f.loc.parent_url().map(|p| (&f.loc, p))).is_some_and( - |(p, pp)| matches!(*op, FilesOp::Deleting(ref parent, ref urls) if *parent == pp && urls.contains(p)), + let leave = tab.parent.as_ref().and_then(|f| f.loc.parent_url().map(|p| (p, f.loc.urn()))).is_some_and( + |(p, n)| matches!(*op, FilesOp::Deleting(ref parent, ref urns) if *parent == p && urns.contains(n)), ); let folder = tab.history.entry(op.cwd().clone()).or_insert_with(|| Folder::from(op.cwd())); diff --git a/yazi-core/src/manager/watcher.rs b/yazi-core/src/manager/watcher.rs index 6756361e7..f2e114a4d 100644 --- a/yazi-core/src/manager/watcher.rs +++ b/yazi-core/src/manager/watcher.rs @@ -9,7 +9,7 @@ use tracing::error; use yazi_fs::{Files, Folder}; use yazi_plugin::isolate; use yazi_proxy::WATCHER; -use yazi_shared::{fs::{symlink_realname, Cha, File, FilesOp, Url}, RoCell}; +use yazi_shared::{fs::{realname_unchecked, Cha, File, FilesOp, Url}, RoCell}; use super::Linked; @@ -124,27 +124,26 @@ impl Watcher { let _permit = WATCHER.acquire().await.unwrap(); let mut reload = Vec::with_capacity(urls.len()); - for url in urls { - let Some(name) = url.file_name() else { continue }; - let Some(parent) = url.parent_url() else { continue }; - - let Ok(file) = File::from(url.clone()).await else { - FilesOp::Deleting(parent, vec![url]).emit(); + for u in urls { + let Some((parent, urn)) = u.pair() else { continue }; + let Ok(file) = File::from(u).await else { + FilesOp::Deleting(parent, HashSet::from_iter([urn])).emit(); continue; }; - let eq = (!file.is_link() && fs::canonicalize(&url).await.is_ok_and(|p| p == *url)) - || symlink_realname(&url, &mut cached).await.is_ok_and(|s| s == name); + let u = file.url(); + let eq = (!file.is_link() && fs::canonicalize(u).await.is_ok_and(|p| p == **u)) + || realname_unchecked(u, &mut cached).await.is_ok_and(|s| s == urn._deref()._as_path()); if !eq { - FilesOp::Deleting(parent, vec![url]).emit(); + FilesOp::Deleting(parent, HashSet::from_iter([urn])).emit(); continue; } if !file.is_dir() { reload.push(file.clone()); } - FilesOp::Upserting(parent, HashMap::from_iter([(url, file)])).emit(); + FilesOp::Upserting(parent, HashMap::from_iter([(urn, file)])).emit(); } if reload.is_empty() { diff --git a/yazi-core/src/manager/yanked.rs b/yazi-core/src/manager/yanked.rs index 23e8c8520..ec53bdb70 100644 --- a/yazi-core/src/manager/yanked.rs +++ b/yazi-core/src/manager/yanked.rs @@ -39,17 +39,10 @@ impl Yanked { } pub fn apply_op(&mut self, op: &FilesOp) { - let (removal, addition) = match op { - FilesOp::Deleting(_, urls) => (urls.iter().collect(), vec![]), - FilesOp::Updating(_, urls) | FilesOp::Upserting(_, urls) => { - urls.iter().filter(|(u, _)| self.contains(u)).map(|(u, f)| (u, f.url_owned())).unzip() - } - _ => (vec![], vec![]), - }; - + let (removal, addition) = op.diff_recoverable(|u| self.contains(u)); if !removal.is_empty() { let old = self.urls.len(); - self.urls.retain(|u| !removal.contains(&u)); + self.urls.retain(|u| !removal.contains(u)); self.revision += (old != self.urls.len()) as u64; } diff --git a/yazi-core/src/tab/selected.rs b/yazi-core/src/tab/selected.rs index 756003963..73c51b67d 100644 --- a/yazi-core/src/tab/selected.rs +++ b/yazi-core/src/tab/selected.rs @@ -18,23 +18,24 @@ impl Selected { #[inline] pub fn add(&mut self, url: &Url) -> bool { self.add_same(&[url]) == 1 } - pub fn add_many(&mut self, urls: &[&Url], same: bool) -> usize { + pub fn add_many(&mut self, urls: &[impl AsRef], same: bool) -> usize { if same { return self.add_same(urls); } let mut grouped: HashMap<_, Vec<_>> = Default::default(); - for &u in urls { - if let Some(p) = u.parent_url() { + for u in urls { + if let Some(p) = u.as_ref().parent_url() { grouped.entry(p).or_default().push(u); } } grouped.into_values().map(|v| self.add_same(&v)).sum() } - fn add_same(&mut self, urls: &[&Url]) -> usize { + fn add_same(&mut self, urls: &[impl AsRef]) -> usize { // If it has appeared as a parent - let urls: Vec<_> = urls.iter().filter(|&&u| !self.parents.contains_key(u)).collect(); + let urls: Vec<_> = + urls.iter().map(|u| u.as_ref()).filter(|&u| !self.parents.contains_key(u)).collect(); if urls.is_empty() { return 0; } @@ -52,7 +53,7 @@ impl Selected { } let (now, len) = (timestamp_us(), self.inner.len()); - self.inner.extend(urls.iter().enumerate().map(|(i, &&u)| (u.clone(), now + i as u64))); + self.inner.extend(urls.iter().enumerate().map(|(i, &u)| (u.clone(), now + i as u64))); for u in parents { *self.parents.entry(u).or_insert(0) += self.inner.len() - len; @@ -103,14 +104,7 @@ impl Selected { } pub fn apply_op(&mut self, op: &FilesOp) { - let (removal, addition) = match op { - FilesOp::Deleting(_, urls) => (urls.iter().collect(), vec![]), - FilesOp::Updating(_, urls) | FilesOp::Upserting(_, urls) => { - urls.iter().filter(|&(u, _)| self.contains_key(u)).map(|(u, f)| (u, f.url())).unzip() - } - _ => (vec![], vec![]), - }; - + let (removal, addition) = op.diff_recoverable(|u| self.contains_key(u)); if !removal.is_empty() { self.remove_many(&removal, !op.cwd().is_search()); } @@ -196,7 +190,7 @@ mod tests { fn insert_many_empty_urls_list() { let mut s = Selected::default(); - assert_eq!(0, s.add_same(&[])); + assert_eq!(0, s.add_same(&[] as &[&Url])); } #[test] diff --git a/yazi-core/src/tasks/preload.rs b/yazi-core/src/tasks/preload.rs index cf86d0634..6af581c2e 100644 --- a/yazi-core/src/tasks/preload.rs +++ b/yazi-core/src/tasks/preload.rs @@ -72,7 +72,7 @@ impl Tasks { targets .iter() .filter(|f| { - f.is_dir() && !targets.sizes.contains_key(f.url()) && !loading.contains(f.url()) + f.is_dir() && !targets.sizes.contains_key(f.urn()) && !loading.contains(f.url()) }) .map(|f| f.url()) .collect() diff --git a/yazi-fm/src/lives/file.rs b/yazi-fm/src/lives/file.rs index 852812f88..2985e613e 100644 --- a/yazi-fm/src/lives/file.rs +++ b/yazi-fm/src/lives/file.rs @@ -39,7 +39,7 @@ impl File { reg.add_field_method_get("idx", |_, me| Ok(me.idx + 1)); reg.add_method("size", |_, me, ()| { - Ok(if me.is_dir() { me.folder().files.sizes.get(me.url()).copied() } else { Some(me.len) }) + Ok(if me.is_dir() { me.folder().files.sizes.get(me.urn()).copied() } else { Some(me.len) }) }); reg.add_method("mime", |lua, me, ()| { let cx = lua.named_registry_value::("cx")?; diff --git a/yazi-fs/src/files.rs b/yazi-fs/src/files.rs index c3fce4d96..2577a2be4 100644 --- a/yazi-fs/src/files.rs +++ b/yazi-fs/src/files.rs @@ -2,7 +2,7 @@ use std::{collections::{HashMap, HashSet}, mem, ops::Deref, sync::atomic::Orderi use tokio::{fs::{self, DirEntry}, select, sync::mpsc::{self, UnboundedReceiver}}; use yazi_config::{manager::SortBy, MANAGER}; -use yazi_shared::fs::{maybe_exists, Cha, File, FilesOp, Url, Urn, FILES_TICKET}; +use yazi_shared::fs::{maybe_exists, Cha, File, FilesOp, Url, Urn, UrnBuf, FILES_TICKET}; use super::{FilesSorter, Filter}; @@ -13,7 +13,7 @@ pub struct Files { version: u64, pub revision: u64, - pub sizes: HashMap, + pub sizes: HashMap, sorter: FilesSorter, filter: Option, @@ -99,7 +99,7 @@ impl Files { pub async fn assert_stale(cwd: &Url, cha: Cha) -> Option { match fs::metadata(cwd).await.map(Cha::from) { Ok(c) if !c.is_dir() => { - // FIXME: use `ErrorKind::NotADirectory` instead once it gets stabilized + // TODO: use `ErrorKind::NotADirectory` instead once it gets stabilized FilesOp::IOErr(cwd.clone(), std::io::ErrorKind::AlreadyExists).emit(); } Ok(c) if c.hits(cha) => {} @@ -107,8 +107,8 @@ impl Files { Err(e) => { if maybe_exists(cwd).await { FilesOp::IOErr(cwd.clone(), e.kind()).emit(); - } else if let Some(p) = cwd.parent_url() { - FilesOp::Deleting(p, vec![cwd.clone()]).emit(); + } else if let Some((p, n)) = cwd.pair() { + FilesOp::Deleting(p, HashSet::from_iter([n])).emit(); } } } @@ -150,7 +150,7 @@ impl Files { } } - pub fn update_size(&mut self, sizes: HashMap) { + pub fn update_size(&mut self, sizes: HashMap) { if sizes.is_empty() { return; } @@ -197,31 +197,30 @@ impl Files { } #[cfg(unix)] - pub fn update_deleting(&mut self, urls: Vec) { - if urls.is_empty() { + pub fn update_deleting(&mut self, urns: HashSet) { + if urns.is_empty() { return; } macro_rules! go { ($dist:expr, $src:expr, $inc:literal) => { - let mut todo: HashSet<_> = $src.into_iter().collect(); let len = $dist.len(); - - $dist.retain(|f| !todo.remove(f.url())); + $dist.retain(|f| !$src.remove(f.urn())); if $dist.len() != len { self.revision += $inc; } }; } - let (hidden, items) = if let Some(filter) = &self.filter { - urls.into_iter().partition(|u| { - (!self.show_hidden && u.is_hidden()) || !u.file_name().is_some_and(|s| filter.matches(s)) + let (mut hidden, mut items) = if let Some(filter) = &self.filter { + urns.into_iter().partition(|u| { + (!self.show_hidden && u._deref().is_hidden()) + || !u._deref().name().is_some_and(|s| filter.matches(s)) }) } else if self.show_hidden { - (vec![], urls) + (HashSet::new(), urns) } else { - urls.into_iter().partition(|u| u.is_hidden()) + urns.into_iter().partition(|u| u._deref().is_hidden()) }; if !items.is_empty() { @@ -233,31 +232,29 @@ impl Files { } #[cfg(windows)] - pub fn update_deleting(&mut self, urls: Vec) { + pub fn update_deleting(&mut self, mut urns: HashSet) { macro_rules! go { ($dist:expr, $src:expr, $inc:literal) => { let len = $dist.len(); - - $dist.retain(|f| !$src.remove(f.url())); + $dist.retain(|f| !$src.remove(f.urn())); if $dist.len() != len { self.revision += $inc; } }; } - let mut urls: HashSet<_> = urls.into_iter().collect(); - if !urls.is_empty() { - go!(self.items, urls, 1); + if !urns.is_empty() { + go!(self.items, urns, 1); } - if !urls.is_empty() { - go!(self.hidden, urls, 0); + if !urns.is_empty() { + go!(self.hidden, urns, 0); } } pub fn update_updating( &mut self, - files: HashMap, - ) -> (HashMap, HashMap) { + files: HashMap, + ) -> (HashMap, HashMap) { if files.is_empty() { return Default::default(); } @@ -266,7 +263,7 @@ impl Files { ($dist:expr, $src:expr, $inc:literal) => { let mut b = true; for i in 0..$dist.len() { - if let Some(f) = $src.remove($dist[i].url()) { + if let Some(f) = $src.remove($dist[i].urn()) { b &= $dist[i].cha.hits(f.cha); $dist[i] = f; @@ -298,7 +295,7 @@ impl Files { (hidden, items) } - pub fn update_upserting(&mut self, files: HashMap) { + pub fn update_upserting(&mut self, files: HashMap) { if files.is_empty() { return; } diff --git a/yazi-fs/src/folder.rs b/yazi-fs/src/folder.rs index 384c0623a..c28b9cce0 100644 --- a/yazi-fs/src/folder.rs +++ b/yazi-fs/src/folder.rs @@ -52,7 +52,7 @@ impl Folder { FilesOp::IOErr(..) => self.files.update_ioerr(), FilesOp::Creating(_, files) => self.files.update_creating(files), - FilesOp::Deleting(_, urls) => self.files.update_deleting(urls), + FilesOp::Deleting(_, urns) => self.files.update_deleting(urns), FilesOp::Updating(_, files) => _ = self.files.update_updating(files), FilesOp::Upserting(_, files) => self.files.update_upserting(files), } diff --git a/yazi-fs/src/sorter.rs b/yazi-fs/src/sorter.rs index e50cdb7b5..98a61cad3 100644 --- a/yazi-fs/src/sorter.rs +++ b/yazi-fs/src/sorter.rs @@ -1,7 +1,7 @@ use std::{cmp::Ordering, collections::HashMap, mem}; use yazi_config::manager::SortBy; -use yazi_shared::{fs::{File, Url}, natsort, LcgRng, Transliterator}; +use yazi_shared::{fs::{File, UrnBuf}, natsort, LcgRng, Transliterator}; #[derive(Clone, Copy, Default, PartialEq)] pub struct FilesSorter { @@ -13,7 +13,7 @@ pub struct FilesSorter { } impl FilesSorter { - pub(super) fn sort(&self, items: &mut Vec, sizes: &HashMap) { + pub(super) fn sort(&self, items: &mut Vec, sizes: &HashMap) { if items.is_empty() { return; } @@ -51,8 +51,8 @@ impl FilesSorter { SortBy::Alphabetical => items.sort_unstable_by(by_alphabetical), SortBy::Natural => self.sort_naturally(items), SortBy::Size => items.sort_unstable_by(|a, b| { - let aa = if a.is_dir() { sizes.get(a.url()).copied() } else { None }; - let bb = if b.is_dir() { sizes.get(b.url()).copied() } else { None }; + let aa = if a.is_dir() { sizes.get(a.urn()).copied() } else { None }; + let bb = if b.is_dir() { sizes.get(b.urn()).copied() } else { None }; let ord = self.cmp(aa.unwrap_or(a.len), bb.unwrap_or(b.len), self.promote(a, b)); if ord == Ordering::Equal { by_alphabetical(a, b) } else { ord } }), diff --git a/yazi-plugin/src/fs/fs.rs b/yazi-plugin/src/fs/fs.rs index 13e5fb3d5..07d9136c0 100644 --- a/yazi-plugin/src/fs/fs.rs +++ b/yazi-plugin/src/fs/fs.rs @@ -111,7 +111,8 @@ pub fn install(lua: &Lua) -> mlua::Result<()> { ( "unique_name", lua.create_async_function(|lua, url: UrlRef| async move { - // FIXME: handle errors + // FIXME: should return a `std::io::Result` to handle errors such as + // permission denied Url::cast(lua, yazi_shared::fs::unique_name(url.clone()).await) })?, ), diff --git a/yazi-scheduler/src/prework/prework.rs b/yazi-scheduler/src/prework/prework.rs index 7e36abef2..28bc37769 100644 --- a/yazi-scheduler/src/prework/prework.rs +++ b/yazi-scheduler/src/prework/prework.rs @@ -92,7 +92,8 @@ impl Prework { } let parent = buf[0].0.parent_url().unwrap(); - FilesOp::Size(parent, HashMap::from_iter(buf)).emit(); + // FIXME 0 + // FilesOp::Size(parent, HashMap::from_iter(buf)).emit(); }); self.prog.send(TaskProg::Adv(task.id, 1, 0))?; } diff --git a/yazi-shared/src/fs/file.rs b/yazi-shared/src/fs/file.rs index e8c40e724..638be8631 100644 --- a/yazi-shared/src/fs/file.rs +++ b/yazi-shared/src/fs/file.rs @@ -81,7 +81,7 @@ impl File { } #[cfg(unix)] - if loc.url().is_hidden() { + if loc.urn().is_hidden() { ck |= ChaKind::HIDDEN; } #[cfg(windows)] diff --git a/yazi-shared/src/fs/fns.rs b/yazi-shared/src/fs/fns.rs index dae881559..ec3770302 100644 --- a/yazi-shared/src/fs/fns.rs +++ b/yazi-shared/src/fs/fns.rs @@ -1,4 +1,4 @@ -use std::{borrow::Cow, collections::{HashMap, VecDeque}, ffi::{OsStr, OsString}, fs::Metadata, path::{Path, PathBuf}}; +use std::{borrow::Cow, collections::{HashMap, HashSet, VecDeque}, ffi::{OsStr, OsString}, fs::Metadata, path::{Path, PathBuf}}; use anyhow::{bail, Result}; use tokio::{fs, io, select, sync::{mpsc, oneshot}, time}; @@ -76,19 +76,22 @@ async fn _paths_to_same_file(a: &Path, b: &Path) -> std::io::Result { Ok(final_name(a).await? == final_name(b).await?) } -pub async fn symlink_realpath(path: &Path) -> Result { - let p = fs::canonicalize(path).await?; - if p == path { - return Ok(p); +pub async fn realname(p: &Path) -> Option { + let name = p.file_name()?; + if p == fs::canonicalize(p).await.ok()? { + return None; } - let Some(parent) = path.parent() else { bail!("no parent") }; - symlink_realname(path, &mut HashMap::new()).await.map(|n| parent.join(n)) + realname_unchecked(p, &mut HashMap::new()) + .await + .ok() + .filter(|s| s != name) + .map(|s| s.into_owned()) } #[cfg(unix)] #[tokio::test] -async fn test_symlink_realpath() { +async fn test_realname_unchecked() { fs::remove_dir_all("/tmp/issue-1173").await.ok(); fs::create_dir_all("/tmp/issue-1173/real-dir").await.unwrap(); fs::File::create("/tmp/issue-1173/A").await.unwrap(); @@ -97,64 +100,52 @@ async fn test_symlink_realpath() { fs::symlink("/tmp/issue-1173/b", "/tmp/issue-1173/D").await.unwrap(); fs::symlink("real-dir", "/tmp/issue-1173/link-dir").await.unwrap(); - async fn check(a: &str, b: &str) { - let expected = if a == b || cfg!(windows) || cfg!(target_os = "macos") { - Some(PathBuf::from(b)) - } else { - None - }; - assert_eq!(symlink_realpath(Path::new(a)).await.ok(), expected); + let c = &mut HashMap::new(); + async fn check(a: &str, b: &str, c: &mut HashMap>) { + assert_eq!(realname_unchecked(Path::new(a), c).await.ok(), Some(OsStr::new(b).into())); } - check("/tmp/issue-1173/a", "/tmp/issue-1173/A").await; - check("/tmp/issue-1173/A", "/tmp/issue-1173/A").await; + check("/tmp/issue-1173/a", "A", c).await; + check("/tmp/issue-1173/A", "A", c).await; - check("/tmp/issue-1173/b", "/tmp/issue-1173/b").await; - check("/tmp/issue-1173/B", "/tmp/issue-1173/b").await; + check("/tmp/issue-1173/b", "b", c).await; + check("/tmp/issue-1173/B", "b", c).await; - check("/tmp/issue-1173/link-dir/c", "/tmp/issue-1173/link-dir/C").await; - check("/tmp/issue-1173/link-dir/C", "/tmp/issue-1173/link-dir/C").await; + check("/tmp/issue-1173/link-dir/c", "C", c).await; + check("/tmp/issue-1173/link-dir/C", "C", c).await; - check("/tmp/issue-1173/d", "/tmp/issue-1173/D").await; - check("/tmp/issue-1173/D", "/tmp/issue-1173/D").await; + check("/tmp/issue-1173/d", "D", c).await; + check("/tmp/issue-1173/D", "D", c).await; } // realpath(3) without resolving symlinks. This is useful for case-insensitive // filesystems. // // Make sure the file of the path exists. -pub async fn symlink_realname<'a>( +pub async fn realname_unchecked<'a>( path: &'a Path, - cached: &'a mut HashMap>, + cached: &'a mut HashMap>, ) -> Result> { - let Some(name) = path.file_name() else { bail!("no file name") }; - let Some(parent) = path.parent() else { return Ok(name.into()) }; + let Some(name) = path.file_name() else { bail!("no filename") }; + let Some(parent) = path.parent() else { return Ok(Cow::Borrowed(name)) }; if !cached.contains_key(parent) { - let mut map = HashMap::new(); + let mut set = HashSet::new(); let mut it = fs::read_dir(parent).await?; while let Some(entry) = it.next_entry().await? { - let n = entry.file_name(); - if n.as_encoded_bytes().iter().all(|&b| b.is_ascii_lowercase()) { - map.insert(n, OsString::new()); - } else { - map.insert(n.to_ascii_lowercase(), n); - } + set.insert(entry.file_name()); } - cached.insert(parent.to_owned(), map); + cached.insert(parent.to_owned(), set); } let c = &cached[parent]; - if let Some(s) = c.get(name) { - return if s.is_empty() { Ok(name.into()) } else { Ok(s.into()) }; - } - - let lowercased = name.to_ascii_lowercase(); - if let Some(s) = c.get(&lowercased) { - return if s.is_empty() { Ok(lowercased.into()) } else { Ok(s.into()) }; + if c.contains(name) { + Ok(Cow::Borrowed(name)) + } else if let Some(n) = c.iter().find(|&n| n.eq_ignore_ascii_case(name)) { + Ok(Cow::Borrowed(n)) + } else { + bail!("no such file") } - - Ok(name.into()) } pub async fn calculate_size(path: &Path) -> u64 { diff --git a/yazi-shared/src/fs/op.rs b/yazi-shared/src/fs/op.rs index a15f3eb57..3207d5b85 100644 --- a/yazi-shared/src/fs/op.rs +++ b/yazi-shared/src/fs/op.rs @@ -1,6 +1,6 @@ -use std::{collections::HashMap, sync::atomic::{AtomicU64, Ordering}}; +use std::{collections::{HashMap, HashSet}, sync::atomic::{AtomicU64, Ordering}}; -use super::{Cha, File}; +use super::{Cha, File, UrnBuf}; use crate::{emit, event::Cmd, fs::Url, Layer}; pub static FILES_TICKET: AtomicU64 = AtomicU64::new(0); @@ -10,13 +10,13 @@ pub enum FilesOp { Full(Url, Vec, Cha), Part(Url, Vec, u64), Done(Url, Cha, u64), - Size(Url, HashMap), + Size(Url, HashMap), IOErr(Url, std::io::ErrorKind), Creating(Url, Vec), - Deleting(Url, Vec), - Updating(Url, HashMap), - Upserting(Url, HashMap), + Deleting(Url, HashSet), + Updating(Url, HashMap), + Upserting(Url, HashMap), } impl FilesOp { @@ -47,15 +47,12 @@ impl FilesOp { ticket } - pub fn chroot(&self, new: &Url) -> Self { - macro_rules! new { - ($url:expr) => {{ new.join($url.file_name().unwrap()) }}; - } + pub fn rebase(&self, new: &Url) -> Self { macro_rules! files { ($files:expr) => {{ $files.iter().map(|f| f.rebase(new)).collect() }}; } macro_rules! map { - ($map:expr) => {{ $map.iter().map(|(u, f)| (new!(u), f.rebase(new))).collect() }}; + ($map:expr) => {{ $map.iter().map(|(u, f)| (u.clone(), f.rebase(new))).collect() }}; } let n = new.clone(); @@ -63,13 +60,29 @@ impl FilesOp { Self::Full(_, files, mtime) => Self::Full(n, files!(files), *mtime), Self::Part(_, files, ticket) => Self::Part(n, files!(files), *ticket), Self::Done(_, mtime, ticket) => Self::Done(n, *mtime, *ticket), - Self::Size(_, map) => Self::Size(n, map.iter().map(|(u, &s)| (new!(u), s)).collect()), + Self::Size(_, map) => Self::Size(n, map.iter().map(|(u, &s)| (u.clone(), s)).collect()), Self::IOErr(_, err) => Self::IOErr(n, *err), Self::Creating(_, files) => Self::Creating(n, files!(files)), - Self::Deleting(_, urls) => Self::Deleting(n, urls.iter().map(|u| new!(u)).collect()), + Self::Deleting(_, urns) => Self::Deleting(n, urns.clone()), Self::Updating(_, map) => Self::Updating(n, map!(map)), Self::Upserting(_, map) => Self::Upserting(n, map!(map)), } } + + pub fn diff_recoverable(&self, contains: impl Fn(&Url) -> bool) -> (Vec, Vec) { + match self { + Self::Deleting(cwd, urns) => { + (urns.iter().map(|u| cwd.join(u._deref()._as_path())).collect(), vec![]) + } + Self::Updating(cwd, urns) | Self::Upserting(cwd, urns) => urns + .iter() + .filter(|&(u, f)| u != f.urn()) + .map(|(u, f)| (cwd.join(u._deref()._as_path()), f)) + .filter(|(u, _)| contains(u)) + .map(|(u, f)| (u, f.url_owned())) + .unzip(), + _ => (vec![], vec![]), + } + } } diff --git a/yazi-shared/src/fs/path.rs b/yazi-shared/src/fs/path.rs index 238f44ed5..64a150b46 100644 --- a/yazi-shared/src/fs/path.rs +++ b/yazi-shared/src/fs/path.rs @@ -72,8 +72,6 @@ fn _expand_path(p: &Path) -> PathBuf { } } -// FIXME: should return a `std::io::Result` to handle errors such as -// permission denied pub async fn unique_name(mut u: Url) -> Url { let Some(stem) = u.file_stem().map(|s| s.to_owned()) else { return u; diff --git a/yazi-shared/src/fs/url.rs b/yazi-shared/src/fs/url.rs index a1f1b525c..d89b49c3b 100644 --- a/yazi-shared/src/fs/url.rs +++ b/yazi-shared/src/fs/url.rs @@ -3,7 +3,7 @@ use std::{ffi::{OsStr, OsString}, fmt::{Debug, Display, Formatter}, ops::{Deref, use percent_encoding::{percent_decode_str, percent_encode, AsciiSet, CONTROLS}; use serde::{Deserialize, Serialize}; -use super::Loc; +use super::{Loc, UrnBuf}; const ENCODE_SET: &AsciiSet = &CONTROLS.add(b'#'); @@ -150,6 +150,12 @@ impl Url { }) } + #[inline] + pub fn pair(&self) -> Option<(Self, UrnBuf)> { + let urn = UrnBuf::_from(self.path.file_name()?); + Some((self.parent_url()?, urn)) + } + #[inline] pub fn strip_prefix(&self, base: impl AsRef) -> Option<&Path> { self.path.strip_prefix(base).ok() @@ -158,12 +164,6 @@ impl Url { #[inline] pub fn into_os_string(self) -> OsString { self.path.into_os_string() } - #[cfg(unix)] - #[inline] - pub fn is_hidden(&self) -> bool { - self.file_name().map_or(false, |s| s.as_encoded_bytes().starts_with(b".")) - } - #[inline] pub fn to_loc(&self, cwd: &Url) -> Loc { self.clone().into_loc(cwd) } diff --git a/yazi-shared/src/fs/urn.rs b/yazi-shared/src/fs/urn.rs index 086ca8a6b..c4b6d66f7 100644 --- a/yazi-shared/src/fs/urn.rs +++ b/yazi-shared/src/fs/urn.rs @@ -1,6 +1,6 @@ -use std::{borrow::Borrow, path::{Path, PathBuf}}; +use std::{borrow::Borrow, ffi::OsStr, path::{Path, PathBuf}}; -#[derive(Debug, Eq, PartialEq)] +#[derive(Debug, Eq, PartialEq, Hash)] #[repr(transparent)] pub struct Urn(Path); @@ -10,7 +10,16 @@ impl Urn { unsafe { &*(p.as_ref() as *const Path as *const Self) } } - // FIXME: remove this + #[inline] + pub fn name(&self) -> Option<&OsStr> { self.0.file_name() } + + #[cfg(unix)] + #[inline] + pub fn is_hidden(&self) -> bool { + self.name().map_or(false, |s| s.as_encoded_bytes().starts_with(b".")) + } + + // FIXME 1: remove this pub fn _as_path(&self) -> &Path { &self.0 } } @@ -21,13 +30,21 @@ impl ToOwned for Urn { } // --- UrnBuf +#[derive(Clone, Debug, Eq, PartialEq, Hash)] pub struct UrnBuf(PathBuf); impl Borrow for UrnBuf { fn borrow(&self) -> &Urn { Urn::new(&self.0) } } +impl PartialEq for UrnBuf { + fn eq(&self, other: &Urn) -> bool { self.0 == other.0 } +} + impl UrnBuf { - // FIXME: remove this + // FIXME 1: remove this pub fn _deref(&self) -> &Urn { Urn::new(&self.0) } + + // FIXME 1: remove this + pub fn _from(p: impl Into) -> Self { Self(p.into()) } }