From c7bfdc556cb429d7ea4421c4b21eac57f5595fb2 Mon Sep 17 00:00:00 2001 From: little camel <54983536+evpeople@users.noreply.github.com> Date: Sun, 18 Feb 2024 15:28:59 +0800 Subject: [PATCH] feat: nested selection conflict detection (#689) --- yazi-core/src/tab/commands/escape.rs | 2 +- yazi-core/src/tab/commands/select.rs | 4 +- yazi-core/src/tab/commands/select_all.rs | 4 +- yazi-core/src/tab/mod.rs | 2 + yazi-core/src/tab/selected.rs | 244 +++++++++++++++++++++++ yazi-core/src/tab/tab.rs | 6 +- 6 files changed, 254 insertions(+), 8 deletions(-) create mode 100644 yazi-core/src/tab/selected.rs diff --git a/yazi-core/src/tab/commands/escape.rs b/yazi-core/src/tab/commands/escape.rs index 5f6ffcc0..fb7d139d 100644 --- a/yazi-core/src/tab/commands/escape.rs +++ b/yazi-core/src/tab/commands/escape.rs @@ -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); } diff --git a/yazi-core/src/tab/commands/select.rs b/yazi-core/src/tab/commands/select.rs index 8d8219fe..6ddfd0ec 100644 --- a/yazi-core/src/tab/commands/select.rs +++ b/yazi-core/src/tab/commands/select.rs @@ -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), }); } } diff --git a/yazi-core/src/tab/commands/select_all.rs b/yazi-core/src/tab/commands/select_all.rs index 5ad5e5a5..d225ab23 100644 --- a/yazi-core/src/tab/commands/select_all.rs +++ b/yazi-core/src/tab/commands/select_all.rs @@ -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); } } } diff --git a/yazi-core/src/tab/mod.rs b/yazi-core/src/tab/mod.rs index b002a2fa..08d7a616 100644 --- a/yazi-core/src/tab/mod.rs +++ b/yazi-core/src/tab/mod.rs @@ -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::*; diff --git a/yazi-core/src/tab/selected.rs b/yazi-core/src/tab/selected.rs new file mode 100644 index 00000000..cbb8e842 --- /dev/null +++ b/yazi-core/src/tab/selected.rs @@ -0,0 +1,244 @@ +use std::{collections::{BTreeSet, HashMap}, ops::Deref}; + +use yazi_shared::fs::Url; + +#[derive(Default)] +pub struct Selected { + inner: BTreeSet, + parents: HashMap, +} + +impl Deref for Selected { + type Target = BTreeSet; + + 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()); + } +} diff --git a/yazi-core/src/tab/tab.rs b/yazi-core/src/tab/tab.rs index f8f1678f..eef9ab47 100644 --- a/yazi-core/src/tab/tab.rs +++ b/yazi-core/src/tab/tab.rs @@ -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, pub history: BTreeMap, - pub selected: BTreeSet, + pub selected: Selected, pub preview: Preview, pub finder: Option,