diff --git a/yazi-core/src/folder/files.rs b/yazi-core/src/folder/files.rs index 88980949..59837ca3 100644 --- a/yazi-core/src/folder/files.rs +++ b/yazi-core/src/folder/files.rs @@ -15,7 +15,6 @@ pub struct Files { pub(crate) revision: u64, pub sizes: BTreeMap, - selected: BTreeSet, sorter: FilesSorter, filter: Option, @@ -31,8 +30,7 @@ impl Default for Files { version: Default::default(), revision: Default::default(), - sizes: Default::default(), - selected: Default::default(), + sizes: Default::default(), sorter: Default::default(), filter: Default::default(), @@ -97,67 +95,6 @@ impl Files { } impl Files { - #[inline] - pub fn select(&mut self, url: &Url, state: Option) -> bool { - let old = self.selected.contains(url); - let new = state.unwrap_or(!old); - - if new == old { - return false; - } - - if new { - self.selected.insert(url.to_owned()); - } else { - self.selected.remove(url); - } - true - } - - pub fn select_all(&mut self, state: Option) -> bool { - match state { - Some(true) => { - let b = if self.selected.len() < self.items.len() { - true - } else { - self.items.iter().any(|f| !self.selected.contains(&f.url)) - }; - - self.selected = self.iter().map(|f| f.url()).collect(); - b - } - Some(false) => { - if self.selected.is_empty() { - return false; - } - - let b = self.items.iter().any(|f| self.selected.contains(&f.url)); - self.selected.clear(); - b - } - None => { - for item in &self.items { - if self.selected.contains(&item.url) { - self.selected.remove(&item.url); - } else { - self.selected.insert(item.url()); - } - } - !self.items.is_empty() - } - } - } - - pub fn select_index(&mut self, indices: &BTreeSet, state: Option) -> bool { - let mut applied = false; - let paths: Vec<_> = self.pick(indices).iter().map(|f| f.url()).collect(); - - for path in paths { - applied |= self.select(&path, state); - } - applied - } - pub fn update_full(&mut self, files: Vec) { self.ticket = FILES_TICKET.fetch_add(1, Ordering::Relaxed); @@ -379,16 +316,6 @@ impl Files { impl Files { // --- Items - pub fn pick(&self, indices: &BTreeSet) -> Vec<&File> { - let mut items = Vec::with_capacity(indices.len()); - for (i, item) in self.iter().enumerate() { - if indices.contains(&i) { - items.push(item); - } - } - items - } - #[inline] pub fn position(&self, url: &Url) -> Option { self.iter().position(|f| &f.url == url) } @@ -396,45 +323,6 @@ impl Files { #[inline] pub fn ticket(&self) -> u64 { self.ticket } - // --- Selected - pub fn selected(&self, pending: &BTreeSet, unset: bool) -> Vec<&File> { - if self.selected.is_empty() && (unset || pending.is_empty()) { - return vec![]; - } - - let selected: BTreeSet<_> = self.selected.iter().collect(); - let pending: BTreeSet<_> = - pending.iter().filter_map(|&i| self.items.get(i)).map(|f| &f.url).collect(); - - let selected: BTreeSet<_> = if unset { - selected.difference(&pending).cloned().collect() - } else { - selected.union(&pending).cloned().collect() - }; - - let mut items = Vec::with_capacity(selected.len()); - for item in &self.items { - if selected.contains(&item.url) { - items.push(item); - } - if items.len() == selected.len() { - break; - } - } - items - } - - #[inline] - pub fn is_selected(&self, url: &Url) -> bool { self.selected.contains(url) } - - #[inline] - pub fn has_selected(&self) -> bool { - if self.selected.is_empty() { - return false; - } - self.iter().any(|f| self.selected.contains(&f.url)) - } - // --- Sorter #[inline] pub fn sorter(&self) -> &FilesSorter { &self.sorter } diff --git a/yazi-core/src/tab/commands/escape.rs b/yazi-core/src/tab/commands/escape.rs index 152b778e..df31e2ef 100644 --- a/yazi-core/src/tab/commands/escape.rs +++ b/yazi-core/src/tab/commands/escape.rs @@ -33,16 +33,30 @@ impl Tab { #[inline] fn escape_visual(&mut self) -> bool { - if let Some((_, indices)) = self.mode.visual() { - self.current.files.select_index(indices, Some(self.mode.is_select())); - self.mode = Mode::Normal; - return true; + let Some((_, indices)) = self.mode.visual() else { + return false; + }; + + let state = self.mode.is_select(); + for f in indices.iter().filter_map(|i| self.current.files.get(*i)) { + if state { + self.selected.insert(f.url()); + } else { + self.selected.remove(&f.url); + } } - false + + self.mode = Mode::Normal; + render!(); + true } #[inline] - fn escape_select(&mut self) -> bool { self.current.files.select_all(Some(false)) } + fn escape_select(&mut self) -> bool { + let old = self.selected.len(); + self.select_all(Some(false)); + old != self.selected.len() + } #[inline] fn escape_filter(&mut self) -> bool { diff --git a/yazi-core/src/tab/commands/mod.rs b/yazi-core/src/tab/commands/mod.rs index 5170b0a5..0f6801e0 100644 --- a/yazi-core/src/tab/commands/mod.rs +++ b/yazi-core/src/tab/commands/mod.rs @@ -14,6 +14,7 @@ mod preview; mod reveal; mod search; mod select; +mod select_all; mod shell; mod sort; mod visual_mode; diff --git a/yazi-core/src/tab/commands/select.rs b/yazi-core/src/tab/commands/select.rs index 6e28dd1a..8d8219fe 100644 --- a/yazi-core/src/tab/commands/select.rs +++ b/yazi-core/src/tab/commands/select.rs @@ -1,14 +1,18 @@ -use yazi_shared::{event::Cmd, render}; +use std::borrow::Cow; + +use yazi_shared::{event::Cmd, fs::Url, render}; use crate::tab::Tab; -pub struct Opt { +pub struct Opt<'a> { + url: Option>, state: Option, } -impl From for Opt { - fn from(c: Cmd) -> Self { +impl<'a> From for Opt<'a> { + fn from(mut c: Cmd) -> Self { Self { + url: c.take_name("url").map(|s| Cow::Owned(Url::from(s))), state: match c.named.get("state").map(|s| s.as_str()) { Some("true") => Some(true), Some("false") => Some(false), @@ -17,18 +21,19 @@ impl From for Opt { } } } -impl From> for Opt { - fn from(state: Option) -> Self { Self { state } } -} -impl Tab { - pub fn select(&mut self, opt: impl Into) { - if let Some(u) = self.current.hovered().map(|h| h.url()) { - render!(self.current.files.select(&u, opt.into().state)); - } - } +impl<'a> Tab { + pub fn select(&mut self, opt: impl Into>) { + let opt = opt.into() as Opt; + let Some(url) = opt.url.or_else(|| self.current.hovered().map(|h| Cow::Borrowed(&h.url))) + else { + return; + }; - pub fn select_all(&mut self, opt: impl Into) { - render!(self.current.files.select_all(opt.into().state)); + render!(match opt.state { + Some(true) => self.selected.insert(url.into_owned()), + Some(false) => self.selected.remove(&url), + None => self.selected.remove(&url) || self.selected.insert(url.into_owned()), + }); } } diff --git a/yazi-core/src/tab/commands/select_all.rs b/yazi-core/src/tab/commands/select_all.rs new file mode 100644 index 00000000..5ad5e5a5 --- /dev/null +++ b/yazi-core/src/tab/commands/select_all.rs @@ -0,0 +1,46 @@ +use yazi_shared::{event::Cmd, render}; + +use crate::tab::Tab; + +pub struct Opt { + state: Option, +} + +impl From for Opt { + fn from(c: Cmd) -> Self { + Self { + state: match c.named.get("state").map(|s| s.as_str()) { + Some("true") => Some(true), + Some("false") => Some(false), + _ => None, + }, + } + } +} +impl From> for Opt { + fn from(state: Option) -> Self { Self { state } } +} + +impl Tab { + pub fn select_all(&mut self, opt: impl Into) { + let mut b = false; + match opt.into().state { + Some(true) => { + for f in self.current.files.iter() { + b |= self.selected.insert(f.url()); + } + } + Some(false) => { + for f in self.current.files.iter() { + b |= self.selected.remove(&f.url); + } + } + None => { + for f in self.current.files.iter() { + b |= self.selected.remove(&f.url) || self.selected.insert(f.url()); + } + } + } + render!(b); + } +} diff --git a/yazi-core/src/tab/tab.rs b/yazi-core/src/tab/tab.rs index 6266d03e..ac6a4f29 100644 --- a/yazi-core/src/tab/tab.rs +++ b/yazi-core/src/tab/tab.rs @@ -1,4 +1,4 @@ -use std::{borrow::Cow, collections::BTreeMap}; +use std::{borrow::Cow, collections::{BTreeMap, BTreeSet}}; use anyhow::Result; use tokio::task::JoinHandle; @@ -15,6 +15,7 @@ pub struct Tab { pub backstack: Backstack, pub history: BTreeMap, + pub selected: BTreeSet, pub preview: Preview, pub finder: Option, @@ -32,6 +33,7 @@ impl From for Tab { backstack: Backstack::new(url), history: Default::default(), + selected: Default::default(), preview: Default::default(), finder: None, @@ -47,26 +49,45 @@ impl From<&Url> for Tab { } impl Tab { - // --- Mode - #[inline] - pub fn in_selecting(&self) -> bool { self.mode.is_visual() || self.current.files.has_selected() } - // --- Current + #[inline] + pub fn in_selecting(&self) -> bool { + !self.selected.is_empty() || self.mode.visual().is_some_and(|(_, indices)| !indices.is_empty()) + } + pub fn selected(&self) -> Vec<&File> { let pending = self.mode.visual().map(|(_, p)| Cow::Borrowed(p)).unwrap_or_default(); - let selected = self.current.files.selected(&pending, self.mode.is_unset()); - - if selected.is_empty() { - self.current.hovered().map(|h| vec![h]).unwrap_or_default() - } else { - selected + let is_unset = self.mode.is_unset(); + if self.selected.is_empty() && (is_unset || pending.is_empty()) { + return self.current.hovered().map(|h| vec![h]).unwrap_or_default(); } + + let selected: BTreeSet<_> = self.selected.iter().collect(); + let pending: BTreeSet<_> = + pending.iter().filter_map(|&i| self.current.files.get(i)).map(|f| &f.url).collect(); + + let urls: BTreeSet<_> = if is_unset { + selected.difference(&pending).copied().collect() + } else { + selected.union(&pending).copied().collect() + }; + + let mut items = Vec::with_capacity(urls.len()); + for item in self.current.files.iter() { + if urls.contains(&item.url) { + items.push(item); + if items.len() == urls.len() { + break; + } + } + } + Some(items) + .filter(|v| !v.is_empty()) + .or_else(|| self.current.hovered().map(|h| vec![h])) + .unwrap_or_default() } // --- History - #[inline] - pub fn history(&self, url: &Url) -> Option<&Folder> { self.history.get(url) } - #[inline] pub fn history_new(&mut self, url: &Url) -> Folder { self.history.remove(url).unwrap_or_else(|| Folder::from(url)) diff --git a/yazi-fm/src/lives/file.rs b/yazi-fm/src/lives/file.rs index bf2dbe87..3827966a 100644 --- a/yazi-fm/src/lives/file.rs +++ b/yazi-fm/src/lives/file.rs @@ -10,6 +10,7 @@ use super::{CtxRef, SCOPE}; pub(super) struct File { idx: usize, folder: *const yazi_core::folder::Folder, + tab: *const yazi_core::tab::Tab, } impl Deref for File { @@ -23,8 +24,9 @@ impl File { pub(super) fn make( idx: usize, folder: &yazi_core::folder::Folder, + tab: &yazi_core::tab::Tab, ) -> mlua::Result> { - SCOPE.create_any_userdata(Self { idx, folder }) + SCOPE.create_any_userdata(Self { idx, folder, tab }) } pub(super) fn register(lua: &Lua) -> mlua::Result<()> { @@ -84,7 +86,7 @@ impl File { }); reg.add_method("is_selected", |lua, me, ()| { let cx = lua.named_registry_value::("cx")?; - let selected = me.folder().files.is_selected(&me.url); + let selected = me.tab().selected.contains(&me.url); Ok(if !cx.manager.active().mode.is_visual() { selected @@ -124,4 +126,7 @@ impl File { #[inline] fn folder(&self) -> &yazi_core::folder::Folder { unsafe { &*self.folder } } + + #[inline] + fn tab(&self) -> &yazi_core::tab::Tab { unsafe { &*self.tab } } } diff --git a/yazi-fm/src/lives/files.rs b/yazi-fm/src/lives/files.rs index 2724c33f..62a8900c 100644 --- a/yazi-fm/src/lives/files.rs +++ b/yazi-fm/src/lives/files.rs @@ -5,8 +5,9 @@ use mlua::{AnyUserData, Lua, MetaMethod, UserDataMethods}; use super::{File, SCOPE}; pub(super) struct Files { - folder: *const yazi_core::folder::Folder, window: Range, + folder: *const yazi_core::folder::Folder, + tab: *const yazi_core::tab::Tab, } impl Deref for Files { @@ -18,10 +19,11 @@ impl Deref for Files { impl Files { #[inline] pub(super) fn make( - folder: &yazi_core::folder::Folder, window: Range, + folder: &yazi_core::folder::Folder, + tab: &yazi_core::tab::Tab, ) -> mlua::Result> { - SCOPE.create_any_userdata(Self { folder, window }) + SCOPE.create_any_userdata(Self { window, folder, tab }) } pub(super) fn register(lua: &Lua) -> mlua::Result<()> { @@ -33,7 +35,7 @@ impl Files { if idx > me.window.end || idx == 0 { Ok(None) } else { - Some(File::make(idx - 1, me.folder())).transpose() + Some(File::make(idx - 1, me.folder(), me.tab())).transpose() } }); })?; @@ -43,4 +45,7 @@ impl Files { #[inline] fn folder(&self) -> &yazi_core::folder::Folder { unsafe { &*self.folder } } + + #[inline] + fn tab(&self) -> &yazi_core::tab::Tab { unsafe { &*self.tab } } } diff --git a/yazi-fm/src/lives/folder.rs b/yazi-fm/src/lives/folder.rs index 5e913261..4c430f53 100644 --- a/yazi-fm/src/lives/folder.rs +++ b/yazi-fm/src/lives/folder.rs @@ -7,8 +7,9 @@ use yazi_plugin::{bindings::Cast, url::Url}; use super::{File, Files, SCOPE}; pub(super) struct Folder { - inner: *const yazi_core::folder::Folder, window: Range, + inner: *const yazi_core::folder::Folder, + tab: *const yazi_core::tab::Tab, } impl Deref for Folder { @@ -20,8 +21,9 @@ impl Deref for Folder { impl Folder { #[inline] pub(super) fn make( - inner: &yazi_core::folder::Folder, window: Option>, + inner: &yazi_core::folder::Folder, + tab: &yazi_core::tab::Tab, ) -> mlua::Result> { let window = match window { Some(w) => w, @@ -31,20 +33,20 @@ impl Folder { } }; - SCOPE.create_any_userdata(Self { inner, window }) + SCOPE.create_any_userdata(Self { window, inner, tab }) } pub(super) fn register(lua: &Lua) -> mlua::Result<()> { lua.register_userdata_type::(|reg| { reg.add_field_method_get("cwd", |lua, me| Url::cast(lua, me.cwd.clone())); - reg.add_field_method_get("files", |_, me| Files::make(me, 0..me.files.len())); + reg.add_field_method_get("files", |_, me| Files::make(0..me.files.len(), me, me.tab())); reg.add_field_method_get("stage", |lua, me| lua.create_any_userdata(me.stage)); - reg.add_field_method_get("window", |_, me| Files::make(me, me.window.clone())); + reg.add_field_method_get("window", |_, me| Files::make(me.window.clone(), me, me.tab())); reg.add_field_method_get("offset", |_, me| Ok(me.offset)); reg.add_field_method_get("cursor", |_, me| Ok(me.cursor)); reg.add_field_method_get("hovered", |_, me| { - me.hovered().map(|_| File::make(me.cursor, me)).transpose() + me.hovered().map(|_| File::make(me.cursor, me, me.tab())).transpose() }); })?; @@ -61,4 +63,7 @@ impl Folder { Ok(()) } + + #[inline] + fn tab(&self) -> &yazi_core::tab::Tab { unsafe { &*self.tab } } } diff --git a/yazi-fm/src/lives/preview.rs b/yazi-fm/src/lives/preview.rs index d6072ebf..2e8d07cd 100644 --- a/yazi-fm/src/lives/preview.rs +++ b/yazi-fm/src/lives/preview.rs @@ -29,10 +29,10 @@ impl Preview { .current .hovered() .filter(|&f| f.is_dir()) - .and_then(|f| me.tab().history(&f.url)) + .and_then(|f| me.tab().history.get(&f.url)) .map(|f| { let limit = LAYOUT.load().preview.height as usize; - Folder::make(f, Some(me.skip..f.files.len().min(me.skip + limit))) + Folder::make(Some(me.skip..f.files.len().min(me.skip + limit)), f, me.tab()) }) .transpose() }); diff --git a/yazi-fm/src/lives/tab.rs b/yazi-fm/src/lives/tab.rs index fafb7f50..f393340f 100644 --- a/yazi-fm/src/lives/tab.rs +++ b/yazi-fm/src/lives/tab.rs @@ -37,9 +37,9 @@ impl Tab { reg.add_field_method_get("mode", |_, me| Mode::make(&me.mode)); reg.add_field_method_get("conf", |_, me| Config::make(&me.conf)); reg.add_field_method_get("parent", |_, me| { - me.parent.as_ref().map(|f| Folder::make(f, None)).transpose() + me.parent.as_ref().map(|f| Folder::make(None, f, me)).transpose() }); - reg.add_field_method_get("current", |_, me| Folder::make(&me.current, None)); + reg.add_field_method_get("current", |_, me| Folder::make(None, &me.current, me)); reg.add_field_method_get("preview", |_, me| Preview::make(me)); })?; diff --git a/yazi-plugin/preset/components/header.lua b/yazi-plugin/preset/components/header.lua index db119820..447936a1 100644 --- a/yazi-plugin/preset/components/header.lua +++ b/yazi-plugin/preset/components/header.lua @@ -9,7 +9,7 @@ function Header:cwd() if not cwd.is_search then span = ui.Span(ya.readable_path(tostring(cwd))) else - span = ui.Span(string.format("%s (search: %s)", ya.readable_path(tostring(cwd)), cwd.frag)) + span = ui.Span(string.format("%s (search: %s)", ya.readable_path(tostring(cwd)), cwd:frag())) end return span:style(THEME.manager.cwd) end diff --git a/yazi-plugin/src/url/url.rs b/yazi-plugin/src/url/url.rs index e6301f22..26b641e0 100644 --- a/yazi-plugin/src/url/url.rs +++ b/yazi-plugin/src/url/url.rs @@ -9,17 +9,26 @@ pub struct Url; impl Url { pub fn register(lua: &Lua) -> mlua::Result<()> { lua.register_userdata_type::(|reg| { - reg.add_field_method_get("frag", |lua, me| lua.create_string(me.frag())); + reg.add_method("frag", |lua, me, ()| lua.create_string(me.frag())); reg.add_field_method_get("is_regular", |_, me| Ok(me.is_regular())); reg.add_field_method_get("is_search", |_, me| Ok(me.is_search())); reg.add_field_method_get("is_archive", |_, me| Ok(me.is_archive())); - reg.add_meta_method(MetaMethod::Eq, |_, me, other: UrlRef| Ok(me == &*other)); + reg.add_method("name", |lua, me, ()| { + me.file_name().map(|s| lua.create_string(s.as_encoded_bytes())).transpose() + }); + reg.add_method("stem", |lua, me, ()| { + me.file_stem().map(|s| lua.create_string(s.as_encoded_bytes())).transpose() + }); + reg.add_method("join", |lua, me, other: UrlRef| Self::cast(lua, me.join(&*other))); + reg.add_method("parent", |lua, me, ()| { + me.parent_url().map(|u| Self::cast(lua, u)).transpose() + }); + reg.add_meta_method(MetaMethod::Eq, |_, me, other: UrlRef| Ok(me == &*other)); reg.add_meta_method(MetaMethod::ToString, |lua, me, ()| { lua.create_string(me.as_os_str().as_encoded_bytes()) }); - reg.add_meta_method(MetaMethod::Concat, |lua, me, other: mlua::String| { let me = me.as_os_str().as_encoded_bytes(); let other = other.as_bytes();