feat: nested selection conflict detection (#689)

This commit is contained in:
little camel 2024-02-18 15:28:59 +08:00 committed by GitHub
parent 2f784e7ae7
commit c7bfdc556c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 254 additions and 8 deletions

View File

@ -68,7 +68,7 @@ impl Tab {
let state = self.mode.is_select(); let state = self.mode.is_select();
for f in indices.iter().filter_map(|i| self.current.files.get(*i)) { for f in indices.iter().filter_map(|i| self.current.files.get(*i)) {
if state { if state {
self.selected.insert(f.url()); self.selected.add(&f.url);
} else { } else {
self.selected.remove(&f.url); self.selected.remove(&f.url);
} }

View File

@ -31,9 +31,9 @@ impl<'a> Tab {
}; };
render!(match opt.state { render!(match opt.state {
Some(true) => self.selected.insert(url.into_owned()), Some(true) => self.selected.add(&url),
Some(false) => self.selected.remove(&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),
}); });
} }
} }

View File

@ -27,7 +27,7 @@ impl Tab {
match opt.into().state { match opt.into().state {
Some(true) => { Some(true) => {
for f in self.current.files.iter() { for f in self.current.files.iter() {
b |= self.selected.insert(f.url()); b |= self.selected.add(&f.url);
} }
} }
Some(false) => { Some(false) => {
@ -37,7 +37,7 @@ impl Tab {
} }
None => { None => {
for f in self.current.files.iter() { 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);
} }
} }
} }

View File

@ -4,6 +4,7 @@ mod config;
mod finder; mod finder;
mod mode; mod mode;
mod preview; mod preview;
mod selected;
mod tab; mod tab;
pub use backstack::*; pub use backstack::*;
@ -11,4 +12,5 @@ pub use config::*;
pub use finder::*; pub use finder::*;
pub use mode::*; pub use mode::*;
pub use preview::*; pub use preview::*;
pub use selected::*;
pub use tab::*; pub use tab::*;

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

View File

@ -1,11 +1,11 @@
use std::collections::{BTreeMap, BTreeSet}; use std::collections::BTreeMap;
use anyhow::Result; use anyhow::Result;
use tokio::task::JoinHandle; use tokio::task::JoinHandle;
use yazi_shared::{fs::Url, render}; use yazi_shared::{fs::Url, render};
use super::{Backstack, Config, Finder, Mode, Preview}; use super::{Backstack, Config, Finder, Mode, Preview};
use crate::folder::{Folder, FolderStage}; use crate::{folder::{Folder, FolderStage}, tab::selected::Selected};
pub struct Tab { pub struct Tab {
pub mode: Mode, pub mode: Mode,
@ -15,7 +15,7 @@ pub struct Tab {
pub backstack: Backstack<Url>, pub backstack: Backstack<Url>,
pub history: BTreeMap<Url, Folder>, pub history: BTreeMap<Url, Folder>,
pub selected: BTreeSet<Url>, pub selected: Selected,
pub preview: Preview, pub preview: Preview,
pub finder: Option<Finder>, pub finder: Option<Finder>,