mirror of
https://github.com/sxyazi/yazi.git
synced 2026-07-25 08:41:05 +00:00
fix: ensure the selected file list is always ordered (#2244)
This commit is contained in:
parent
a4f8e86471
commit
29284613a0
15 changed files with 45 additions and 44 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
|
@ -3408,6 +3408,7 @@ dependencies = [
|
||||||
"crossterm",
|
"crossterm",
|
||||||
"dirs",
|
"dirs",
|
||||||
"futures",
|
"futures",
|
||||||
|
"indexmap",
|
||||||
"libc",
|
"libc",
|
||||||
"notify",
|
"notify",
|
||||||
"parking_lot",
|
"parking_lot",
|
||||||
|
|
@ -3473,6 +3474,7 @@ dependencies = [
|
||||||
"crossterm",
|
"crossterm",
|
||||||
"fdlimit",
|
"fdlimit",
|
||||||
"futures",
|
"futures",
|
||||||
|
"indexmap",
|
||||||
"libc",
|
"libc",
|
||||||
"mlua",
|
"mlua",
|
||||||
"ratatui",
|
"ratatui",
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ crossterm = { version = "0.28.1", features = [ "event-stream" ] }
|
||||||
dirs = "6.0.0"
|
dirs = "6.0.0"
|
||||||
futures = "0.3.31"
|
futures = "0.3.31"
|
||||||
globset = "0.4.15"
|
globset = "0.4.15"
|
||||||
|
indexmap = { version = "2.7.1", features = [ "serde" ] }
|
||||||
libc = "0.2.169"
|
libc = "0.2.169"
|
||||||
md-5 = "0.10.6"
|
md-5 = "0.10.6"
|
||||||
mlua = { version = "0.10.2", features = [ "anyhow", "async", "error-send", "lua54", "macros", "serialize" ] }
|
mlua = { version = "0.10.2", features = [ "anyhow", "async", "error-send", "lua54", "macros", "serialize" ] }
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ anyhow = { workspace = true }
|
||||||
bitflags = { workspace = true }
|
bitflags = { workspace = true }
|
||||||
crossterm = { workspace = true }
|
crossterm = { workspace = true }
|
||||||
globset = { workspace = true }
|
globset = { workspace = true }
|
||||||
indexmap = { version = "2.7.1", features = [ "serde" ] }
|
indexmap = { workspace = true }
|
||||||
ratatui = { workspace = true }
|
ratatui = { workspace = true }
|
||||||
regex = { workspace = true }
|
regex = { workspace = true }
|
||||||
serde = { workspace = true }
|
serde = { workspace = true }
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ bitflags = { workspace = true }
|
||||||
crossterm = { workspace = true }
|
crossterm = { workspace = true }
|
||||||
dirs = { workspace = true }
|
dirs = { workspace = true }
|
||||||
futures = { workspace = true }
|
futures = { workspace = true }
|
||||||
|
indexmap = { workspace = true }
|
||||||
notify = { version = "8.0.0", default-features = false, features = [ "macos_fsevent" ] }
|
notify = { version = "8.0.0", default-features = false, features = [ "macos_fsevent" ] }
|
||||||
parking_lot = { workspace = true }
|
parking_lot = { workspace = true }
|
||||||
ratatui = { workspace = true }
|
ratatui = { workspace = true }
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ impl Manager {
|
||||||
return AppProxy::notify_warn("Bulk rename", "No text opener found");
|
return AppProxy::notify_warn("Bulk rename", "No text opener found");
|
||||||
};
|
};
|
||||||
|
|
||||||
let old: Vec<_> = self.selected_or_hovered(true).collect();
|
let old: Vec<_> = self.selected_or_hovered().collect();
|
||||||
|
|
||||||
let root = max_common_root(&old);
|
let root = max_common_root(&old);
|
||||||
let old: Vec<_> = old.into_iter().map(|p| p.strip_prefix(&root).unwrap().to_owned()).collect();
|
let old: Vec<_> = old.into_iter().map(|p| p.strip_prefix(&root).unwrap().to_owned()).collect();
|
||||||
|
|
|
||||||
|
|
@ -33,9 +33,7 @@ impl Manager {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
let selected =
|
let selected = if opt.hovered { vec![&hovered] } else { self.selected_or_hovered().collect() };
|
||||||
if opt.hovered { vec![&hovered] } else { self.selected_or_hovered(true).collect() };
|
|
||||||
|
|
||||||
if Self::quit_with_selected(opt, &selected) {
|
if Self::quit_with_selected(opt, &selected) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@ impl Manager {
|
||||||
opt.targets = if opt.hovered {
|
opt.targets = if opt.hovered {
|
||||||
self.hovered().map_or(vec![], |h| vec![h.url.clone()])
|
self.hovered().map_or(vec![], |h| vec![h.url.clone()])
|
||||||
} else {
|
} else {
|
||||||
self.selected_or_hovered(true).cloned().collect()
|
self.selected_or_hovered().cloned().collect()
|
||||||
};
|
};
|
||||||
|
|
||||||
if opt.targets.is_empty() {
|
if opt.targets.is_empty() {
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ impl Manager {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.yanked = Yanked::new(opt.cut, self.selected_or_hovered(false).cloned().collect());
|
self.yanked = Yanked::new(opt.cut, self.selected_or_hovered().cloned().collect());
|
||||||
render!(self.yanked.catchup_revision(true));
|
render!(self.yanked.catchup_revision(true));
|
||||||
|
|
||||||
self.active_mut().escape_select();
|
self.active_mut().escape_select();
|
||||||
|
|
|
||||||
|
|
@ -77,12 +77,12 @@ impl Manager {
|
||||||
pub fn hovered_folder(&self) -> Option<&Folder> { self.active().hovered_folder() }
|
pub fn hovered_folder(&self) -> Option<&Folder> { self.active().hovered_folder() }
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn selected_or_hovered(&self, reorder: bool) -> Box<dyn Iterator<Item = &Url> + '_> {
|
pub fn selected_or_hovered(&self) -> Box<dyn Iterator<Item = &Url> + '_> {
|
||||||
self.tabs.active().selected_or_hovered(reorder)
|
self.tabs.active().selected_or_hovered()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn hovered_and_selected(&self, reorder: bool) -> Box<dyn Iterator<Item = &Url> + '_> {
|
pub fn hovered_and_selected(&self) -> Box<dyn Iterator<Item = &Url> + '_> {
|
||||||
self.tabs.active().hovered_and_selected(reorder)
|
self.tabs.active().hovered_and_selected()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ impl Tab {
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut s = OsString::new();
|
let mut s = OsString::new();
|
||||||
let mut it = self.selected_or_hovered(true).peekable();
|
let mut it = self.selected_or_hovered().peekable();
|
||||||
while let Some(u) = it.next() {
|
while let Some(u) = it.next() {
|
||||||
s.push(match opt.type_.as_ref() {
|
s.push(match opt.type_.as_ref() {
|
||||||
"path" => opt.separator.transform(u),
|
"path" => opt.separator.transform(u),
|
||||||
|
|
|
||||||
|
|
@ -53,7 +53,7 @@ impl Tab {
|
||||||
};
|
};
|
||||||
|
|
||||||
let cwd = opt.cwd.take().unwrap_or_else(|| self.cwd().clone());
|
let cwd = opt.cwd.take().unwrap_or_else(|| self.cwd().clone());
|
||||||
let selected = self.hovered_and_selected(true).cloned().collect();
|
let selected = self.hovered_and_selected().cloned().collect();
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
if opt.interactive {
|
if opt.interactive {
|
||||||
let mut result =
|
let mut result =
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,17 @@
|
||||||
use std::{collections::HashMap, ops::Deref};
|
use std::{collections::HashMap, ops::Deref};
|
||||||
|
|
||||||
|
use indexmap::IndexMap;
|
||||||
use yazi_fs::FilesOp;
|
use yazi_fs::FilesOp;
|
||||||
use yazi_shared::{timestamp_us, url::Url};
|
use yazi_shared::{timestamp_us, url::Url};
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct Selected {
|
pub struct Selected {
|
||||||
inner: HashMap<Url, u64>,
|
inner: IndexMap<Url, u64>,
|
||||||
parents: HashMap<Url, usize>,
|
parents: HashMap<Url, usize>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Deref for Selected {
|
impl Deref for Selected {
|
||||||
type Target = HashMap<Url, u64>;
|
type Target = IndexMap<Url, u64>;
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target { &self.inner }
|
fn deref(&self) -> &Self::Target { &self.inner }
|
||||||
}
|
}
|
||||||
|
|
@ -66,21 +67,26 @@ impl Selected {
|
||||||
pub fn remove(&mut self, url: &Url) -> bool { self.remove_same(&[url]) == 1 }
|
pub fn remove(&mut self, url: &Url) -> bool { self.remove_same(&[url]) == 1 }
|
||||||
|
|
||||||
pub fn remove_many(&mut self, urls: &[impl AsRef<Url>], same: bool) -> usize {
|
pub fn remove_many(&mut self, urls: &[impl AsRef<Url>], same: bool) -> usize {
|
||||||
if same {
|
let affected = if same {
|
||||||
return self.remove_same(urls);
|
self.remove_same(urls)
|
||||||
}
|
} else {
|
||||||
|
let mut grouped: HashMap<_, Vec<_>> = Default::default();
|
||||||
let mut grouped: HashMap<_, Vec<_>> = Default::default();
|
for u in urls {
|
||||||
for u in urls {
|
if let Some(p) = u.as_ref().parent_url() {
|
||||||
if let Some(p) = u.as_ref().parent_url() {
|
grouped.entry(p).or_default().push(u);
|
||||||
grouped.entry(p).or_default().push(u);
|
}
|
||||||
}
|
}
|
||||||
|
grouped.into_values().map(|v| self.remove_same(&v)).sum()
|
||||||
|
};
|
||||||
|
|
||||||
|
if affected > 0 {
|
||||||
|
self.inner.sort_unstable_by(|_, a, _, b| a.cmp(b));
|
||||||
}
|
}
|
||||||
grouped.into_values().map(|v| self.remove_same(&v)).sum()
|
affected
|
||||||
}
|
}
|
||||||
|
|
||||||
fn remove_same(&mut self, urls: &[impl AsRef<Url>]) -> usize {
|
fn remove_same(&mut self, urls: &[impl AsRef<Url>]) -> usize {
|
||||||
let count = urls.iter().filter_map(|u| self.inner.remove(u.as_ref())).count();
|
let count = urls.iter().filter_map(|u| self.inner.swap_remove(u.as_ref())).count();
|
||||||
if count == 0 {
|
if count == 0 {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -85,29 +85,20 @@ impl Tab {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn selected_or_hovered(&self, reorder: bool) -> Box<dyn Iterator<Item = &Url> + '_> {
|
pub fn selected_or_hovered(&self) -> Box<dyn Iterator<Item = &Url> + '_> {
|
||||||
if self.selected.is_empty() {
|
if self.selected.is_empty() {
|
||||||
Box::new(self.hovered().map(|h| vec![&h.url]).unwrap_or_default().into_iter())
|
Box::new(self.hovered().map(|h| vec![&h.url]).unwrap_or_default().into_iter())
|
||||||
} else if !reorder {
|
|
||||||
Box::new(self.selected.keys())
|
|
||||||
} else {
|
} else {
|
||||||
let mut vec: Vec<_> = self.selected.iter().collect();
|
Box::new(self.selected.keys())
|
||||||
vec.sort_unstable_by(|(_, a), (_, b)| a.cmp(b));
|
|
||||||
Box::new(vec.into_iter().map(|(k, _)| k))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn hovered_and_selected(&self, reorder: bool) -> Box<dyn Iterator<Item = &Url> + '_> {
|
pub fn hovered_and_selected(&self) -> Box<dyn Iterator<Item = &Url> + '_> {
|
||||||
let Some(h) = self.hovered() else { return Box::new(iter::empty()) };
|
let Some(h) = self.hovered() else { return Box::new(iter::empty()) };
|
||||||
|
|
||||||
if self.selected.is_empty() {
|
if self.selected.is_empty() {
|
||||||
Box::new([&h.url, &h.url].into_iter())
|
Box::new([&h.url, &h.url].into_iter())
|
||||||
} else if !reorder {
|
|
||||||
Box::new([&h.url].into_iter().chain(self.selected.keys()))
|
|
||||||
} else {
|
} else {
|
||||||
let mut vec: Vec<_> = self.selected.iter().collect();
|
Box::new([&h.url].into_iter().chain(self.selected.keys()))
|
||||||
vec.sort_unstable_by(|(_, a), (_, b)| a.cmp(b));
|
|
||||||
Box::new([&h.url].into_iter().chain(vec.into_iter().map(|(k, _)| k)))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@ better-panic = "0.3.0"
|
||||||
crossterm = { workspace = true }
|
crossterm = { workspace = true }
|
||||||
fdlimit = "0.3.0"
|
fdlimit = "0.3.0"
|
||||||
futures = { workspace = true }
|
futures = { workspace = true }
|
||||||
|
indexmap = { workspace = true }
|
||||||
mlua = { workspace = true }
|
mlua = { workspace = true }
|
||||||
ratatui = { workspace = true }
|
ratatui = { workspace = true }
|
||||||
scopeguard = { workspace = true }
|
scopeguard = { workspace = true }
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
use std::{collections::{HashMap, hash_map}, ops::Deref};
|
use std::ops::Deref;
|
||||||
|
|
||||||
|
use indexmap::{IndexMap, map::Keys};
|
||||||
use mlua::{AnyUserData, IntoLuaMulti, MetaMethod, UserData, UserDataMethods, UserDataRefMut};
|
use mlua::{AnyUserData, IntoLuaMulti, MetaMethod, UserData, UserDataMethods, UserDataRefMut};
|
||||||
use yazi_plugin::{bindings::Cast, url::Url};
|
use yazi_plugin::{bindings::Cast, url::Url};
|
||||||
|
|
||||||
|
|
@ -7,23 +8,23 @@ use super::{Iter, Lives};
|
||||||
|
|
||||||
#[derive(Clone, Copy)]
|
#[derive(Clone, Copy)]
|
||||||
pub(super) struct Selected {
|
pub(super) struct Selected {
|
||||||
inner: *const HashMap<yazi_shared::url::Url, u64>,
|
inner: *const IndexMap<yazi_shared::url::Url, u64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Deref for Selected {
|
impl Deref for Selected {
|
||||||
type Target = HashMap<yazi_shared::url::Url, u64>;
|
type Target = IndexMap<yazi_shared::url::Url, u64>;
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target { self.inner() }
|
fn deref(&self) -> &Self::Target { self.inner() }
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Selected {
|
impl Selected {
|
||||||
#[inline]
|
#[inline]
|
||||||
pub(super) fn make(inner: &HashMap<yazi_shared::url::Url, u64>) -> mlua::Result<AnyUserData> {
|
pub(super) fn make(inner: &IndexMap<yazi_shared::url::Url, u64>) -> mlua::Result<AnyUserData> {
|
||||||
Lives::scoped_userdata(Self { inner })
|
Lives::scoped_userdata(Self { inner })
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn inner(&self) -> &'static HashMap<yazi_shared::url::Url, u64> { unsafe { &*self.inner } }
|
fn inner(&self) -> &'static IndexMap<yazi_shared::url::Url, u64> { unsafe { &*self.inner } }
|
||||||
}
|
}
|
||||||
|
|
||||||
impl UserData for Selected {
|
impl UserData for Selected {
|
||||||
|
|
@ -32,7 +33,7 @@ impl UserData for Selected {
|
||||||
|
|
||||||
methods.add_meta_method(MetaMethod::Pairs, |lua, me, ()| {
|
methods.add_meta_method(MetaMethod::Pairs, |lua, me, ()| {
|
||||||
let iter = lua.create_function(
|
let iter = lua.create_function(
|
||||||
|lua, mut iter: UserDataRefMut<Iter<hash_map::Keys<yazi_shared::url::Url, u64>, _>>| {
|
|lua, mut iter: UserDataRefMut<Iter<Keys<yazi_shared::url::Url, u64>, _>>| {
|
||||||
if let Some(next) = iter.next() {
|
if let Some(next) = iter.next() {
|
||||||
(next.0, Url::cast(lua, next.1.clone())?).into_lua_multi(lua)
|
(next.0, Url::cast(lua, next.1.clone())?).into_lua_multi(lua)
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue