mirror of
https://github.com/sxyazi/yazi.git
synced 2024-11-28 11:28:02 +03:00
feat: nested selection conflict detection (#689)
This commit is contained in:
parent
2f784e7ae7
commit
c7bfdc556c
@ -68,7 +68,7 @@ impl Tab {
|
||||
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());
|
||||
self.selected.add(&f.url);
|
||||
} else {
|
||||
self.selected.remove(&f.url);
|
||||
}
|
||||
|
@ -31,9 +31,9 @@ impl<'a> Tab {
|
||||
};
|
||||
|
||||
render!(match opt.state {
|
||||
Some(true) => self.selected.insert(url.into_owned()),
|
||||
Some(true) => self.selected.add(&url),
|
||||
Some(false) => self.selected.remove(&url),
|
||||
None => self.selected.remove(&url) || self.selected.insert(url.into_owned()),
|
||||
None => self.selected.remove(&url) || self.selected.add(&url),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -27,7 +27,7 @@ impl Tab {
|
||||
match opt.into().state {
|
||||
Some(true) => {
|
||||
for f in self.current.files.iter() {
|
||||
b |= self.selected.insert(f.url());
|
||||
b |= self.selected.add(&f.url);
|
||||
}
|
||||
}
|
||||
Some(false) => {
|
||||
@ -37,7 +37,7 @@ impl Tab {
|
||||
}
|
||||
None => {
|
||||
for f in self.current.files.iter() {
|
||||
b |= self.selected.remove(&f.url) || self.selected.insert(f.url());
|
||||
b |= self.selected.remove(&f.url) || self.selected.add(&f.url);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ mod config;
|
||||
mod finder;
|
||||
mod mode;
|
||||
mod preview;
|
||||
mod selected;
|
||||
mod tab;
|
||||
|
||||
pub use backstack::*;
|
||||
@ -11,4 +12,5 @@ pub use config::*;
|
||||
pub use finder::*;
|
||||
pub use mode::*;
|
||||
pub use preview::*;
|
||||
pub use selected::*;
|
||||
pub use tab::*;
|
||||
|
244
yazi-core/src/tab/selected.rs
Normal file
244
yazi-core/src/tab/selected.rs
Normal file
@ -0,0 +1,244 @@
|
||||
use std::{collections::{BTreeSet, HashMap}, ops::Deref};
|
||||
|
||||
use yazi_shared::fs::Url;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Selected {
|
||||
inner: BTreeSet<Url>,
|
||||
parents: HashMap<Url, usize>,
|
||||
}
|
||||
|
||||
impl Deref for Selected {
|
||||
type Target = BTreeSet<Url>;
|
||||
|
||||
fn deref(&self) -> &Self::Target { &self.inner }
|
||||
}
|
||||
|
||||
impl Selected {
|
||||
pub fn add(&mut self, url: &Url) -> bool { self.add_many(&[url]) }
|
||||
|
||||
/// Adds a list of URLs to the user structure.
|
||||
///
|
||||
/// This method attempts to add a slice of `Url` references to the internal
|
||||
/// structure, ensuring that all URLs have the same parent directory. For
|
||||
/// example, URLs such as `/a/b/c`, `/a/b/d`, `/a/b/e`, and `/a/b/f` are
|
||||
/// acceptable, while `/a/b/c` and `/a/e/f` would not be, due to differing
|
||||
/// parent directories.
|
||||
///
|
||||
/// The addition will fail under the following conditions:
|
||||
/// - Any of the URLs already exists within the `inner` collection.
|
||||
/// - The parent directory of the URLs already exists as a key in the
|
||||
/// `parents` map.
|
||||
///
|
||||
/// When the provided list of URLs is empty, the method will return `true` as
|
||||
/// there are no URLs to process, which is considered a successful operation.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `urls` - A slice of references to `Url` objects that are to be added.
|
||||
/// All URLs should have the same parent path.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// Returns `true` if all URLs were successfully added, or if the input list
|
||||
/// is empty. Returns `false` if any URL could not be added due to the
|
||||
/// existence of its parent directory in the structure, or if the URL itself
|
||||
/// is already present.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// # use yazi_core::tab::Selected;
|
||||
/// # use yazi_shared::fs::Url;
|
||||
/// let mut s = Selected::default();
|
||||
///
|
||||
/// let url1 = Url::from("/a/b/c");
|
||||
/// let url2 = Url::from("/a/b/d");
|
||||
/// assert!(s.add_many(&[&url1, &url2]));
|
||||
/// ```
|
||||
pub fn add_many(&mut self, urls: &[&Url]) -> bool {
|
||||
if urls.is_empty() {
|
||||
return true;
|
||||
} else if self.parents.contains_key(urls[0]) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let mut parent = urls[0].parent_url();
|
||||
let mut parents = vec![];
|
||||
while let Some(u) = parent {
|
||||
if self.inner.contains(&u) {
|
||||
return false;
|
||||
}
|
||||
|
||||
parent = u.parent_url();
|
||||
parents.push(u);
|
||||
}
|
||||
|
||||
for u in parents {
|
||||
*self.parents.entry(u).or_insert(0) += urls.len();
|
||||
}
|
||||
|
||||
self.inner.extend(urls.iter().map(|&u| u.clone()));
|
||||
true
|
||||
}
|
||||
|
||||
pub fn remove(&mut self, url: &Url) -> bool {
|
||||
if !self.inner.remove(url) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let mut parent = url.parent_url();
|
||||
while let Some(u) = parent {
|
||||
let n = self.parents.get_mut(&u).unwrap();
|
||||
if *n == 1 {
|
||||
self.parents.remove(&u);
|
||||
} else {
|
||||
*n -= 1;
|
||||
}
|
||||
|
||||
parent = u.parent_url();
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
pub fn clear(&mut self) {
|
||||
self.inner.clear();
|
||||
self.parents.clear();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_insert_non_conflicting() {
|
||||
let mut s = Selected::default();
|
||||
|
||||
assert!(s.add(&Url::from("/a/b")));
|
||||
assert!(s.add(&Url::from("/c/d")));
|
||||
assert_eq!(s.inner.len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_insert_conflicting_parent() {
|
||||
let mut s = Selected::default();
|
||||
|
||||
assert!(s.add(&Url::from("/a")));
|
||||
assert!(!s.add(&Url::from("/a/b")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_insert_conflicting_child() {
|
||||
let mut s = Selected::default();
|
||||
|
||||
assert!(s.add(&Url::from("/a/b/c")));
|
||||
assert!(!s.add(&Url::from("/a/b")));
|
||||
assert!(s.add(&Url::from("/a/b/d")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_remove() {
|
||||
let mut s = Selected::default();
|
||||
|
||||
assert!(s.add(&Url::from("/a/b")));
|
||||
assert!(!s.remove(&Url::from("/a/c")));
|
||||
assert!(s.remove(&Url::from("/a/b")));
|
||||
assert!(!s.remove(&Url::from("/a/b")));
|
||||
assert!(s.inner.is_empty());
|
||||
assert!(s.parents.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn insert_many_success() {
|
||||
let mut s = Selected::default();
|
||||
|
||||
assert!(s.add_many(&[
|
||||
&Url::from("/parent/child1"),
|
||||
&Url::from("/parent/child2"),
|
||||
&Url::from("/parent/child3")
|
||||
]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn insert_many_with_existing_parent_fails() {
|
||||
let mut s = Selected::default();
|
||||
|
||||
s.add(&Url::from("/parent"));
|
||||
assert!(!s.add_many(&[&Url::from("/parent/child1"), &Url::from("/parent/child2"),]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn insert_many_with_existing_child_fails() {
|
||||
let mut s = Selected::default();
|
||||
|
||||
s.add(&Url::from("/parent/child1"));
|
||||
assert!(s.add_many(&[&Url::from("/parent/child1"), &Url::from("/parent/child2")]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn insert_many_empty_urls_list() {
|
||||
let mut s = Selected::default();
|
||||
|
||||
assert!(s.add_many(&[]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn insert_many_with_parent_as_child_of_another_url() {
|
||||
let mut s = Selected::default();
|
||||
|
||||
s.add(&Url::from("/parent/child"));
|
||||
assert!(!s.add_many(&[&Url::from("/parent/child/child1"), &Url::from("/parent/child/child2")]));
|
||||
}
|
||||
#[test]
|
||||
fn insert_many_with_direct_parent_fails() {
|
||||
let mut s = Selected::default();
|
||||
|
||||
s.add(&Url::from("/a"));
|
||||
assert!(!s.add_many(&[&Url::from("/a/b")]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn insert_many_with_nested_child_fails() {
|
||||
let mut s = Selected::default();
|
||||
|
||||
s.add(&Url::from("/a/b"));
|
||||
assert!(!s.add_many(&[&Url::from("/a")]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn insert_many_sibling_directories_success() {
|
||||
let mut s = Selected::default();
|
||||
|
||||
assert!(s.add_many(&[&Url::from("/a/b"), &Url::from("/a/c")]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn insert_many_with_grandchild_fails() {
|
||||
let mut s = Selected::default();
|
||||
|
||||
s.add(&Url::from("/a/b"));
|
||||
assert!(!s.add_many(&[&Url::from("/a/b/c")]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_insert_many_with_remove() {
|
||||
let mut s = Selected::default();
|
||||
|
||||
let child1 = Url::from("/parent/child1");
|
||||
let child2 = Url::from("/parent/child2");
|
||||
let child3 = Url::from("/parent/child3");
|
||||
assert!(s.add_many(&[&child1, &child2, &child3]));
|
||||
|
||||
assert!(s.remove(&child1));
|
||||
assert_eq!(s.inner.len(), 2);
|
||||
assert!(!s.parents.is_empty());
|
||||
|
||||
assert!(s.remove(&child2));
|
||||
assert!(!s.parents.is_empty());
|
||||
|
||||
assert!(s.remove(&child3));
|
||||
assert!(s.inner.is_empty());
|
||||
assert!(s.parents.is_empty());
|
||||
}
|
||||
}
|
@ -1,11 +1,11 @@
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use anyhow::Result;
|
||||
use tokio::task::JoinHandle;
|
||||
use yazi_shared::{fs::Url, render};
|
||||
|
||||
use super::{Backstack, Config, Finder, Mode, Preview};
|
||||
use crate::folder::{Folder, FolderStage};
|
||||
use crate::{folder::{Folder, FolderStage}, tab::selected::Selected};
|
||||
|
||||
pub struct Tab {
|
||||
pub mode: Mode,
|
||||
@ -15,7 +15,7 @@ pub struct Tab {
|
||||
|
||||
pub backstack: Backstack<Url>,
|
||||
pub history: BTreeMap<Url, Folder>,
|
||||
pub selected: BTreeSet<Url>,
|
||||
pub selected: Selected,
|
||||
|
||||
pub preview: Preview,
|
||||
pub finder: Option<Finder>,
|
||||
|
Loading…
Reference in New Issue
Block a user