Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: nested selection conflict detection #689

Merged
merged 15 commits into from
Feb 18, 2024
2 changes: 1 addition & 1 deletion yazi-core/src/tab/commands/escape.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,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);
}
Expand Down
4 changes: 2 additions & 2 deletions yazi-core/src/tab/commands/select.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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),
});
}
}
4 changes: 2 additions & 2 deletions yazi-core/src/tab/commands/select_all.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand All @@ -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);
}
}
}
Expand Down
1 change: 1 addition & 0 deletions yazi-core/src/tab/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ mod config;
mod finder;
mod mode;
mod preview;
mod selected;
mod tab;

pub use backstack::*;
Expand Down
206 changes: 206 additions & 0 deletions yazi-core/src/tab/selected.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
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]) }

pub fn add_many(&mut self, urls: &[&Url]) -> bool {
if urls.is_empty() {
return true;
}

let mut parent = urls[0].parent_url();
while let Some(u) = parent {
if self.inner.contains(&u) {
return false;
}
parent = u.parent_url();
}

if self.parents.contains_key(urls[0]) {
return false;
}

let mut parent = urls[0].parent_url();
while let Some(u) = parent {
parent = u.parent_url();
*self.parents.entry(u).or_insert(0) += urls.len();
}

self.inner.extend(urls.iter().map(|&u| u.clone()));
sxyazi marked this conversation as resolved.
Show resolved Hide resolved
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());
}
}
6 changes: 3 additions & 3 deletions yazi-core/src/tab/tab.rs
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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>,
Expand Down