refactor: tab-specific selection (#653)

This commit is contained in:
三咲雅 · Misaki Masa 2024-02-13 02:40:13 +08:00 committed by GitHub
parent 84616bd15f
commit a764e42098
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 167 additions and 168 deletions

View file

@ -15,7 +15,6 @@ pub struct Files {
pub(crate) revision: u64,
pub sizes: BTreeMap<Url, u64>,
selected: BTreeSet<Url>,
sorter: FilesSorter,
filter: Option<Filter>,
@ -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>) -> 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>) -> 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<usize>, state: Option<bool>) -> 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<File>) {
self.ticket = FILES_TICKET.fetch_add(1, Ordering::Relaxed);
@ -379,16 +316,6 @@ impl Files {
impl Files {
// --- Items
pub fn pick(&self, indices: &BTreeSet<usize>) -> 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<usize> { 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<usize>, 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 }

View file

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

View file

@ -14,6 +14,7 @@ mod preview;
mod reveal;
mod search;
mod select;
mod select_all;
mod shell;
mod sort;
mod visual_mode;

View file

@ -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<Cow<'a, Url>>,
state: Option<bool>,
}
impl From<Cmd> for Opt {
fn from(c: Cmd) -> Self {
impl<'a> From<Cmd> 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<Cmd> for Opt {
}
}
}
impl From<Option<bool>> for Opt {
fn from(state: Option<bool>) -> Self { Self { state } }
}
impl Tab {
pub fn select(&mut self, opt: impl Into<Opt>) {
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<Opt<'a>>) {
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<Opt>) {
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()),
});
}
}

View file

@ -0,0 +1,46 @@
use yazi_shared::{event::Cmd, render};
use crate::tab::Tab;
pub struct Opt {
state: Option<bool>,
}
impl From<Cmd> 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<Option<bool>> for Opt {
fn from(state: Option<bool>) -> Self { Self { state } }
}
impl Tab {
pub fn select_all(&mut self, opt: impl Into<Opt>) {
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);
}
}

View file

@ -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<Url>,
pub history: BTreeMap<Url, Folder>,
pub selected: BTreeSet<Url>,
pub preview: Preview,
pub finder: Option<Finder>,
@ -32,6 +33,7 @@ impl From<Url> 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))

View file

@ -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<AnyUserData<'static>> {
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::<CtxRef>("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 } }
}

View file

@ -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<usize>,
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<usize>,
folder: &yazi_core::folder::Folder,
tab: &yazi_core::tab::Tab,
) -> mlua::Result<AnyUserData<'static>> {
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 } }
}

View file

@ -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<usize>,
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<Range<usize>>,
inner: &yazi_core::folder::Folder,
tab: &yazi_core::tab::Tab,
) -> mlua::Result<AnyUserData<'static>> {
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::<Self>(|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 } }
}

View file

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

View file

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

View file

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

View file

@ -9,17 +9,26 @@ pub struct Url;
impl Url {
pub fn register(lua: &Lua) -> mlua::Result<()> {
lua.register_userdata_type::<yazi_shared::fs::Url>(|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();