refactor: simplify filesystem design to improve performance (#89)

This commit is contained in:
三咲雅 · Misaki Masa 2023-08-29 11:54:33 +08:00 committed by GitHub
parent 2b70107f1f
commit e31bc6a075
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 526 additions and 348 deletions

View file

@ -131,7 +131,7 @@ impl App {
let calc = matches!(op, FilesOp::Read(..) | FilesOp::Search(..));
let b = match op {
FilesOp::Read(..) => manager.update_read(op),
FilesOp::Sort(..) => manager.update_read(op),
FilesOp::Size(..) => manager.update_read(op),
FilesOp::Search(..) => manager.update_search(op),
FilesOp::IOErr(..) => manager.update_ioerr(op),
};
@ -144,7 +144,7 @@ impl App {
}
Event::Pages(page) => {
if manager.current().page == page {
let targets = self.cx.manager.current().paginate().into_iter().map(|(_, f)| f).collect();
let targets = self.cx.manager.current().paginate();
tasks.precache_mime(targets, &self.cx.manager.mimetype);
}
}

View file

@ -36,7 +36,7 @@ impl Ctx {
}
Position::Hovered(rect @ Rect { mut x, y, width, height }) => {
let Some(r) =
self.manager.hovered().and_then(|h| self.manager.current().rect_current(&h.path))
self.manager.hovered().and_then(|h| self.manager.current().rect_current(h.path()))
else {
return self.area(&Position::Top(*rect));
};

View file

@ -106,7 +106,7 @@ impl Executor {
}
}
"remove" => {
let targets = cx.manager.selected().into_iter().map(|p| p.path()).collect();
let targets = cx.manager.selected().into_iter().map(|f| f.path_owned()).collect();
cx.tasks.file_remove(targets, exec.named.contains_key("permanently"))
}
"create" => cx.manager.create(),
@ -117,11 +117,13 @@ impl Executor {
exec.named.contains_key("block"),
exec.named.contains_key("confirm"),
),
"hidden" => cx.manager.current_mut().hidden(match exec.args.get(0).map(|s| s.as_str()) {
Some("show") => Some(true),
Some("hide") => Some(false),
_ => None,
}),
"hidden" => {
cx.manager.active_mut().set_show_hidden(match exec.args.get(0).map(|s| s.as_str()) {
Some("show") => Some(true),
Some("hide") => Some(false),
_ => None,
})
}
"search" => match exec.args.get(0).map(|s| s.as_str()).unwrap_or("") {
"rg" => cx.manager.active_mut().search(true),
"fd" => cx.manager.active_mut().search(false),
@ -135,7 +137,7 @@ impl Executor {
// Sorting
"sort" => {
let b = cx.manager.current_mut().files.set_sorter(FilesSorter {
let b = cx.manager.active_mut().set_sorter(FilesSorter {
by: SortBy::try_from(exec.args.get(0).cloned().unwrap_or_default())
.unwrap_or_default(),
reverse: exec.named.contains_key("reverse"),

View file

@ -36,7 +36,7 @@ impl<'a> Folder<'a> {
THEME
.filetypes
.iter()
.find(|x| x.matches(&file.path, mimetype.get(&file.path).cloned(), file.meta.is_dir()))
.find(|x| x.matches(file.path(), mimetype.get(file.path()), file.is_dir()))
.map(|x| x.style.get())
.unwrap_or_else(Style::new)
}
@ -56,16 +56,17 @@ impl<'a> Widget for Folder<'a> {
let items = window
.iter()
.enumerate()
.map(|(i, (k, v))| {
.map(|(i, f)| {
let icon = THEME
.icons
.iter()
.find(|x| x.name.match_path(k, Some(v.meta.is_dir())))
.find(|x| x.name.match_path(f.path(), Some(f.is_dir())))
.map(|x| x.display.as_ref())
.unwrap_or("");
if (!self.is_selection && v.is_selected)
|| (self.is_selection && mode.pending(i, v.is_selected))
let is_selected = self.folder.files.is_selected(f.path());
if (!self.is_selection && is_selected)
|| (self.is_selection && mode.pending(self.folder.offset() + i, is_selected))
{
buf.set_style(
Rect { x: area.x.saturating_sub(1), y: i as u16 + 1, width: 1, height: 1 },
@ -77,17 +78,17 @@ impl<'a> Widget for Folder<'a> {
);
}
let hovered = matches!(self.folder.hovered, Some(ref h) if h.path == *k);
let hovered = matches!(self.folder.hovered, Some(ref h) if h.path() == f.path());
let style = if self.is_preview && hovered {
THEME.preview.hovered.get()
} else if hovered {
THEME.selection.hovered.get()
} else {
self.file_style(v)
self.file_style(f)
};
let mut path = format!(" {icon} {}", readable_path(k, &self.folder.cwd));
if let Some(ref link_to) = v.link_to {
let mut path = format!(" {icon} {}", readable_path(f.path(), &self.folder.cwd));
if let Some(link_to) = f.link_to() {
if MANAGER.show_symlink {
path.push_str(&format!(" -> {}", link_to.display()));
}

View file

@ -17,7 +17,7 @@ impl<'a> Preview<'a> {
impl<'a> Widget for Preview<'a> {
fn render(self, area: Rect, buf: &mut Buffer) {
let manager = &self.cx.manager;
let Some(hovered) = manager.hovered().map(|h| &h.path) else {
let Some(hovered) = manager.hovered().map(|h| h.path()) else {
return;
};

View file

@ -35,13 +35,13 @@ impl<'a> Widget for Left<'a> {
if let Some(h) = &manager.hovered {
// Length
if let Some(len) = h.length {
if let Some(len) = h.length() {
spans.push(Span::styled(format!(" {} ", readable_size(len)), body.bg().fg(**primary)));
spans.push(Span::styled(&separator.closing, body.fg()));
}
// Filename
spans.push(Span::raw(format!(" {} ", h.name().unwrap())));
spans.push(Span::raw(format!(" {} ", h.name_display().unwrap())));
}
Paragraph::new(Line::from(spans)).render(area, buf);

View file

@ -71,7 +71,7 @@ impl Widget for Right<'_> {
#[cfg(not(target_os = "windows"))]
if let Some(h) = &manager.hovered {
use std::os::unix::prelude::PermissionsExt;
spans.extend(self.permissions(&shared::file_mode(h.meta.permissions().mode())))
spans.extend(self.permissions(&shared::file_mode(h.meta().permissions().mode())))
}
// Position

View file

@ -12,12 +12,12 @@ pub struct Filetype {
}
impl Filetype {
pub fn matches(&self, path: &Path, mime: Option<String>, is_dir: bool) -> bool {
pub fn matches(&self, path: &Path, mime: Option<impl AsRef<str>>, is_dir: bool) -> bool {
if self.name.as_ref().map_or(false, |e| e.match_path(path, Some(is_dir))) {
return true;
}
if let Some(mime) = mime {
return self.mime.as_ref().map_or(false, |m| m.matches(&mime));
return self.mime.as_ref().map_or(false, |m| m.matches(mime));
}
false
}

View file

@ -1,27 +1,26 @@
use std::{borrow::Cow, fs::Metadata, path::{Path, PathBuf}};
use std::{borrow::Cow, ffi::OsStr, fs::Metadata, path::{Path, PathBuf}};
use anyhow::Result;
use tokio::fs;
#[derive(Clone, Debug)]
pub struct File {
pub path: PathBuf,
pub meta: Metadata,
pub length: Option<u64>,
pub link_to: Option<PathBuf>,
pub is_link: bool,
pub is_hidden: bool,
pub is_selected: bool,
pub(super) path: PathBuf,
pub(super) meta: Metadata,
pub(super) length: Option<u64>,
pub(super) link_to: Option<PathBuf>,
pub(super) is_link: bool,
pub(super) is_hidden: bool,
}
impl File {
#[inline]
pub async fn from(path: &Path) -> Result<File> {
pub async fn from(path: &Path) -> Result<Self> {
let meta = fs::metadata(path).await?;
Ok(Self::from_meta(path, meta).await)
}
pub async fn from_meta(path: &Path, mut meta: Metadata) -> File {
pub async fn from_meta(path: &Path, mut meta: Metadata) -> Self {
let is_link = meta.is_symlink();
let mut link_to = None;
@ -32,20 +31,53 @@ impl File {
let length = if meta.is_dir() { None } else { Some(meta.len()) };
let is_hidden = path.file_name().map(|s| s.to_string_lossy().starts_with('.')).unwrap_or(false);
File { path: path.to_path_buf(), meta, length, link_to, is_link, is_hidden, is_selected: false }
Self { path: path.to_path_buf(), meta, length, link_to, is_link, is_hidden }
}
}
impl File {
// --- Path
#[inline]
pub fn path(&self) -> PathBuf { self.path.clone() }
pub fn path(&self) -> &PathBuf { &self.path }
#[inline]
pub fn set_path(mut self, path: &Path) -> Self {
self.path = path.to_path_buf();
self
pub fn set_path(&mut self, path: PathBuf) { self.path = path; }
#[inline]
pub fn path_owned(&self) -> PathBuf { self.path.clone() }
#[inline]
pub fn path_os_str(&self) -> &OsStr { self.path.as_os_str() }
#[inline]
pub fn name(&self) -> Option<&OsStr> { self.path.file_name() }
#[inline]
pub fn name_display(&self) -> Option<Cow<str>> {
self.path.file_name().map(|s| s.to_string_lossy())
}
#[inline]
pub fn name(&self) -> Option<Cow<str>> { self.path.file_name().map(|s| s.to_string_lossy()) }
pub fn stem(&self) -> Option<&OsStr> { self.path.file_stem() }
#[inline]
pub fn parent(&self) -> Option<&Path> { self.path.parent() }
// --- Meta
#[inline]
pub fn meta(&self) -> &Metadata { &self.meta }
#[inline]
pub fn is_file(&self) -> bool { self.meta.is_file() }
#[inline]
pub fn is_dir(&self) -> bool { self.meta.is_dir() }
// --- Length
#[inline]
pub fn length(&self) -> Option<u64> { self.length }
// --- Link to
#[inline]
pub fn link_to(&self) -> Option<&PathBuf> { self.link_to.as_ref() }
}

View file

@ -1,110 +1,149 @@
use std::{collections::BTreeMap, ops::{Deref, DerefMut}, path::{Path, PathBuf}};
use std::{collections::{BTreeMap, BTreeSet}, mem, ops::Deref, path::{Path, PathBuf}};
use anyhow::Result;
use config::MANAGER;
use indexmap::IndexMap;
use config::manager::SortBy;
use tokio::fs;
use super::{File, FilesSorter};
pub struct Files {
items: IndexMap<PathBuf, File>,
pub sorter: FilesSorter,
pub show_hidden: bool,
items: Vec<File>,
hidden: Vec<File>,
sizes: BTreeMap<PathBuf, u64>,
selected: BTreeSet<PathBuf>,
sorter: FilesSorter,
show_hidden: bool,
}
impl Default for Files {
fn default() -> Self {
Self {
items: Default::default(),
items: Default::default(),
hidden: Default::default(),
sizes: Default::default(),
selected: Default::default(),
sorter: Default::default(),
show_hidden: MANAGER.show_hidden,
show_hidden: true,
}
}
}
impl Deref for Files {
type Target = Vec<File>;
fn deref(&self) -> &Self::Target { &self.items }
}
impl Files {
pub async fn read(paths: Vec<PathBuf>) -> BTreeMap<PathBuf, File> {
let mut items = BTreeMap::new();
pub async fn read(paths: &[impl AsRef<Path>]) -> Vec<File> {
let mut items = Vec::with_capacity(paths.len());
for path in paths {
if let Ok(file) = File::from(&path).await {
items.insert(path, file);
if let Ok(file) = File::from(path.as_ref()).await {
items.push(file);
}
}
items
}
pub async fn read_dir(path: &Path) -> Result<BTreeMap<PathBuf, File>> {
pub async fn read_dir(path: &Path) -> Result<Vec<File>> {
let mut it = fs::read_dir(path).await?;
let mut items = BTreeMap::new();
let mut items = Vec::new();
while let Ok(Some(item)) = it.next_entry().await {
if let Ok(meta) = item.metadata().await {
let path = item.path();
let file = File::from_meta(&path, meta).await;
items.insert(path, file);
items.push(File::from_meta(&item.path(), meta).await);
}
}
Ok(items)
}
}
impl Files {
#[inline]
pub fn duplicate(&self, idx: usize) -> Option<File> {
self.items.get_index(idx).map(|(_, file)| file.clone())
}
pub fn select(&mut self, path: &Path, state: Option<bool>) -> bool {
let old = self.selected.contains(path);
let new = if let Some(new) = state { new } else { !old };
#[inline]
pub fn set_sorter(&mut self, sort: FilesSorter) -> bool {
if self.sorter == sort {
if new == old {
return false;
}
self.sorter = sort;
self.sorter.sort(&mut self.items)
if new {
self.selected.insert(path.to_owned());
} else {
self.selected.remove(path);
}
true
}
pub fn update_read(&mut self, mut items: BTreeMap<PathBuf, File>) -> bool {
if !self.show_hidden {
items.retain(|_, item| !item.is_hidden);
}
for (path, item) in &mut items {
if let Some(old) = self.items.get(path) {
item.is_selected = old.is_selected;
// Calculate the size of directories is expensive, so we keep the old value,
// before a new value is calculated and comes to.
if item.meta.is_dir() {
item.length = old.length;
pub fn select_all(&mut self, state: Option<bool>) -> bool {
match state {
Some(true) => {
self.selected = self.iter().map(|f| f.path_owned()).collect();
}
Some(false) => {
self.selected.clear();
}
None => {
for item in &self.items {
if self.selected.contains(&item.path) {
self.selected.remove(&item.path);
} else {
self.selected.insert(item.path_owned());
}
}
}
}
self.items.clear();
self.items.extend(items);
self.sorter.sort(&mut self.items);
true
!self.items.is_empty()
}
pub fn update_sort(&mut self, mut items: BTreeMap<PathBuf, File>) -> bool {
for (path, item) in &mut items {
if let Some(old) = self.items.get(path) {
item.is_selected = old.is_selected;
}
pub fn select_index(&mut self, indices: &BTreeSet<usize>, state: Option<bool>) -> bool {
let mut applied = false;
let paths: Vec<_> = self.pick(indices).iter().map(|f| f.path_owned()).collect();
for path in paths {
applied |= self.select(&path, state);
}
applied
}
self.items.extend(items);
self.sorter.sort(&mut self.items);
pub fn update_read(&mut self, mut items: Vec<File>) -> bool {
if !self.show_hidden {
(self.hidden, items) = items.into_iter().partition(|f| f.is_hidden);
}
self.sorter.sort(&mut items);
self.items = items;
true
}
pub fn update_search(&mut self, items: BTreeMap<PathBuf, File>) -> bool {
pub fn update_size(&mut self, items: BTreeMap<PathBuf, u64>) -> bool {
self.sizes.extend(items);
if self.sorter.by == SortBy::Size {
self.sorter.sort(&mut self.items);
}
true
}
pub fn update_search(&mut self, items: Vec<File>) -> bool {
if !items.is_empty() {
self.items.extend(items);
if self.show_hidden {
self.items.extend(items);
} else {
let (hidden, items): (Vec<_>, Vec<_>) = items.into_iter().partition(|f| f.is_hidden);
self.items.extend(items);
self.hidden.extend(hidden);
}
self.sorter.sort(&mut self.items);
return true;
}
if !self.items.is_empty() {
self.items.clear();
self.hidden.clear();
return true;
}
@ -112,39 +151,93 @@ impl Files {
}
}
impl Deref for Files {
type Target = IndexMap<PathBuf, File>;
fn deref(&self) -> &Self::Target { &self.items }
}
impl DerefMut for Files {
fn deref_mut(&mut self) -> &mut Self::Target { &mut self.items }
}
#[derive(Debug)]
pub enum FilesOp {
Read(PathBuf, BTreeMap<PathBuf, File>),
Sort(PathBuf, BTreeMap<PathBuf, File>),
Search(PathBuf, BTreeMap<PathBuf, File>),
IOErr(PathBuf),
}
impl FilesOp {
#[inline]
pub fn path(&self) -> PathBuf {
match self {
Self::Read(path, _) => path,
Self::Sort(path, _) => path,
Self::Search(path, _) => path,
Self::IOErr(path) => path,
impl Files {
// --- Items
pub fn pick(&self, indices: &BTreeSet<usize>) -> Vec<&File> {
let mut items = Vec::with_capacity(indices.len());
for (i, item) in self.iter().enumerate() {
if indices.contains(&i) {
items.push(item);
}
}
.clone()
items
}
#[inline]
pub fn read_empty(path: &Path) -> Self { Self::Read(path.to_path_buf(), BTreeMap::new()) }
pub fn position(&self, path: &Path) -> Option<usize> { self.iter().position(|f| f.path == path) }
#[inline]
pub fn search_empty(path: &Path) -> Self { Self::Search(path.to_path_buf(), BTreeMap::new()) }
pub fn duplicate(&self, idx: usize) -> Option<File> { self.items.get(idx).cloned() }
// --- Selected
pub fn selected(&self, pending: &BTreeSet<usize>, unset: bool) -> Vec<&File> {
if self.selected.is_empty() && (unset || pending.is_empty()) {
return Vec::new();
}
let selected: BTreeSet<_> = self.selected.iter().collect();
let pending: BTreeSet<_> =
pending.iter().filter_map(|&i| self.items.get(i)).map(|f| &f.path).collect();
let selected: BTreeSet<_> = if unset {
selected.difference(&pending).cloned().collect()
} else {
selected.union(&pending).cloned().collect()
};
let mut items = Vec::with_capacity(selected.len());
for item in &self.items {
if selected.contains(&item.path) {
items.push(item);
}
if items.len() == selected.len() {
break;
}
}
items
}
#[inline]
pub fn is_selected(&self, path: &Path) -> bool { self.selected.contains(path) }
#[inline]
pub fn has_selected(&self) -> bool {
if self.selected.is_empty() {
return false;
}
self.iter().any(|f| self.selected.contains(&f.path))
}
// --- Sorter
pub fn sorter(&self) -> &FilesSorter { &self.sorter }
#[inline]
pub fn set_sorter(&mut self, sorter: FilesSorter) -> bool {
if self.sorter == sorter {
return false;
}
self.sorter = sorter;
self.sorter.sort(&mut self.items)
}
// --- Show hidden
#[inline]
pub fn set_show_hidden(&mut self, state: bool) -> bool {
if state == self.show_hidden {
return false;
} else if state && self.hidden.is_empty() {
return false;
}
if state {
self.items.append(&mut self.hidden);
self.sorter.sort(&mut self.items);
} else {
let items = mem::take(&mut self.items);
(self.hidden, self.items) = items.into_iter().partition(|f| f.is_hidden);
}
self.show_hidden = state;
true
}
}

View file

@ -1,7 +1,9 @@
mod file;
mod files;
mod op;
mod sorter;
pub use file::*;
pub use files::*;
pub use op::*;
pub use sorter::*;

30
core/src/files/op.rs Normal file
View file

@ -0,0 +1,30 @@
use std::{collections::BTreeMap, path::{Path, PathBuf}};
use super::File;
#[derive(Debug)]
pub enum FilesOp {
Read(PathBuf, Vec<File>),
Size(PathBuf, BTreeMap<PathBuf, u64>),
Search(PathBuf, Vec<File>),
IOErr(PathBuf),
}
impl FilesOp {
#[inline]
pub fn path(&self) -> PathBuf {
match self {
Self::Read(path, _) => path,
Self::Size(path, _) => path,
Self::Search(path, _) => path,
Self::IOErr(path) => path,
}
.clone()
}
#[inline]
pub fn read_empty(path: &Path) -> Self { Self::Read(path.to_path_buf(), Vec::new()) }
#[inline]
pub fn search_empty(path: &Path) -> Self { Self::Search(path.to_path_buf(), Vec::new()) }
}

View file

@ -1,7 +1,6 @@
use std::{cmp::Ordering, path::PathBuf};
use std::{cmp::Ordering, mem, path::PathBuf};
use config::{manager::SortBy, MANAGER};
use indexmap::IndexMap;
use super::File;
@ -23,41 +22,41 @@ impl Default for FilesSorter {
}
impl FilesSorter {
pub(super) fn sort(&self, items: &mut IndexMap<PathBuf, File>) -> bool {
pub(super) fn sort(&self, items: &mut Vec<File>) -> bool {
if items.is_empty() {
return false;
}
match self.by {
SortBy::Alphabetical => {
items.sort_unstable_by(|_, a, _, b| self.cmp(&a.path, &b.path, self.promote(a, b)))
items.sort_unstable_by(|a, b| self.cmp(&a.path, &b.path, self.promote(a, b)))
}
SortBy::Created => items.sort_unstable_by(|_, a, _, b| {
SortBy::Created => items.sort_unstable_by(|a, b| {
if let (Ok(aa), Ok(bb)) = (a.meta.created(), b.meta.created()) {
return self.cmp(aa, bb, self.promote(a, b));
}
Ordering::Equal
}),
SortBy::Modified => items.sort_unstable_by(|_, a, _, b| {
SortBy::Modified => items.sort_unstable_by(|a, b| {
if let (Ok(aa), Ok(bb)) = (a.meta.modified(), b.meta.modified()) {
return self.cmp(aa, bb, self.promote(a, b));
}
Ordering::Equal
}),
SortBy::Natural => self.sort_naturally(items),
SortBy::Size => items.sort_unstable_by(|_, a, _, b| {
self.cmp(a.length.unwrap_or(0), b.length.unwrap_or(0), self.promote(a, b))
SortBy::Size => items.sort_unstable_by(|a, b| {
self.cmp(a.length().unwrap_or(0), b.length().unwrap_or(0), self.promote(a, b))
}),
}
true
}
fn sort_naturally(&self, items: &mut IndexMap<PathBuf, File>) {
fn sort_naturally(&self, items: &mut Vec<File>) {
let mut indices = Vec::with_capacity(items.len());
let mut entities = Vec::with_capacity(items.len());
for (i, (path, file)) in items.into_iter().enumerate() {
for (i, file) in items.iter().enumerate() {
indices.push(i);
entities.push((path.to_string_lossy(), file));
entities.push((file.path.to_string_lossy(), file));
}
indices.sort_unstable_by(|&a, &b| {
@ -71,10 +70,18 @@ impl FilesSorter {
}
});
let mut new = IndexMap::with_capacity(indices.len());
let dummy = File {
path: PathBuf::new(),
meta: items[0].meta.clone(),
length: None,
link_to: None,
is_link: false,
is_hidden: false,
};
let mut new = Vec::with_capacity(indices.len());
for i in indices {
let file = entities[i].1.clone();
new.insert(file.path(), file);
new.push(mem::replace(&mut items[i], dummy.clone()));
}
*items = new;
}
@ -91,6 +98,6 @@ impl FilesSorter {
#[inline]
fn promote(&self, a: &File, b: &File) -> Ordering {
if self.dir_first { b.meta.is_dir().cmp(&a.meta.is_dir()) } else { Ordering::Equal }
if self.dir_first { b.is_dir().cmp(&a.is_dir()) } else { Ordering::Equal }
}
}

View file

@ -1,7 +1,6 @@
use std::path::{Path, PathBuf};
use config::MANAGER;
use indexmap::map::Slice;
use ratatui::layout::Rect;
use crate::{emit, files::{File, Files, FilesOp}};
@ -28,7 +27,7 @@ impl Folder {
pub fn update(&mut self, op: FilesOp) -> bool {
let b = match op {
FilesOp::Read(_, items) => self.files.update_read(items),
FilesOp::Sort(_, items) => self.files.update_sort(items),
FilesOp::Size(_, items) => self.files.update_size(items),
FilesOp::Search(_, items) => self.files.update_search(items),
_ => unreachable!(),
};
@ -41,11 +40,8 @@ impl Folder {
self.cursor = self.cursor.min(len.saturating_sub(1));
self.set_page(true);
if let Some(h) = self.hovered.as_ref().map(|h| h.path()) {
self.hover(&h);
}
self.hover_repos();
self.hovered = self.files.duplicate(self.cursor);
true
}
@ -93,68 +89,31 @@ impl Folder {
old != self.cursor
}
pub fn hidden(&mut self, show: Option<bool>) -> bool {
if show.is_none() || self.files.show_hidden != show.unwrap() {
self.files.show_hidden = !self.files.show_hidden;
emit!(Refresh);
}
false
}
#[inline]
pub fn window(&self) -> &Slice<PathBuf, File> {
pub fn window(&self) -> &[File] {
let end = (self.offset + MANAGER.layout.folder_height()).min(self.files.len());
self.files.get_range(self.offset..end).unwrap()
&self.files[self.offset..end]
}
#[inline]
pub fn window_for(&self, offset: usize) -> &Slice<PathBuf, File> {
pub fn window_for(&self, offset: usize) -> &[File] {
let start = offset.min(self.files.len().saturating_sub(1));
let end = (offset + MANAGER.layout.folder_height()).min(self.files.len());
self.files.get_range(start..end).unwrap()
}
pub fn select(&mut self, idx: Option<usize>, state: Option<bool>) -> bool {
let len = self.files.len();
let mut apply = |idx: usize, state: Option<bool>| -> bool {
let Some(state) = state else {
self.files[idx].is_selected = !self.files[idx].is_selected;
return true;
};
if state != self.files[idx].is_selected {
self.files[idx].is_selected = state;
return true;
}
false
};
if let Some(idx) = idx {
if idx < len {
return apply(idx, state);
}
} else {
let mut applied = false;
for i in 0..len {
if apply(i, state) {
applied = true;
}
}
return applied;
}
false
&self.files[start..end]
}
pub fn hover(&mut self, path: &Path) -> bool {
let new = self.position(path).unwrap_or(self.cursor);
let new = self.files.position(path).unwrap_or(self.cursor);
if new > self.cursor { self.next(new - self.cursor) } else { self.prev(self.cursor - new) }
}
#[inline]
pub fn hover_repos(&mut self) -> bool {
self.hover(&self.hovered.as_ref().map(|h| h.path_owned()).unwrap_or_default())
}
pub fn hover_force(&mut self, file: File) -> bool {
if self.hover(&file.path) {
if self.hover(file.path()) {
return true;
}
@ -170,25 +129,17 @@ impl Folder {
#[inline]
pub fn cursor(&self) -> usize { self.cursor }
#[inline]
pub fn position(&self, path: &Path) -> Option<usize> {
self.files.iter().position(|(p, _)| p == path)
}
pub fn paginate(&self) -> &Slice<PathBuf, File> {
pub fn paginate(&self) -> &[File] {
let len = self.files.len();
let limit = MANAGER.layout.folder_height();
let start = (self.page * limit).min(len.saturating_sub(1));
let end = (start + limit).min(len);
self.files.get_range(start..end).unwrap()
&self.files[start..end]
}
#[inline]
pub fn has_selected(&self) -> bool { self.files.iter().any(|(_, f)| f.is_selected) }
pub fn rect_current(&self, path: &Path) -> Option<Rect> {
let y = self.position(path)? - self.offset;
let y = self.files.position(path)? - self.offset;
let mut rect = MANAGER.layout.folder_rect();
rect.y = rect.y.saturating_sub(1) + y as u16;

View file

@ -30,6 +30,8 @@ impl Manager {
pub fn refresh(&mut self) {
env::set_current_dir(self.cwd()).ok();
self.active_mut().apply_show_hidden();
if let Some(f) = self.parent() {
self.watcher.trigger_dirs(&[self.cwd(), &f.cwd]);
} else {
@ -39,15 +41,15 @@ impl Manager {
let mut to_watch = BTreeSet::new();
for tab in self.tabs.iter() {
to_watch.insert(tab.current.cwd.clone());
if let Some(ref p) = tab.parent {
to_watch.insert(p.cwd.clone());
}
to_watch.insert(&tab.current.cwd);
if let Some(ref h) = tab.current.hovered {
if h.meta.is_dir() {
if h.is_dir() {
to_watch.insert(h.path());
}
}
if let Some(ref p) = tab.parent {
to_watch.insert(&p.cwd);
}
}
self.watcher.watch(to_watch);
}
@ -61,13 +63,13 @@ impl Manager {
self.active_mut().preview_reset_image();
}
let mime = if hovered.meta.is_dir() {
let mime = if hovered.is_dir() {
MIME_DIR.to_owned()
} else if let Some(m) = self.mimetype.get(&hovered.path).cloned() {
} else if let Some(m) = self.mimetype.get(hovered.path()).cloned() {
m
} else {
tokio::spawn(async move {
if let Ok(mimes) = external::file(&[hovered.path]).await {
if let Ok(mimes) = external::file(&[hovered.path()]).await {
emit!(Mimetype(mimes));
}
});
@ -75,18 +77,16 @@ impl Manager {
};
if sequent {
self.active_mut().preview.sequent(&hovered.path, &mime, show_image);
self.active_mut().preview.sequent(hovered.path(), &mime, show_image);
} else {
self.active_mut().preview.go(&hovered.path, &mime, show_image);
self.active_mut().preview.go(hovered.path(), &mime, show_image);
}
false
}
pub fn yank(&mut self, cut: bool) -> bool {
let selected = self.selected().into_iter().map(|f| f.path()).collect::<Vec<_>>();
self.yanked.0 = cut;
self.yanked.1.clear();
self.yanked.1.extend(selected);
self.yanked.1 = self.selected().into_iter().map(|f| f.path_owned()).collect();
false
}
@ -119,20 +119,16 @@ impl Manager {
}
pub fn open(&mut self, interactive: bool) -> bool {
let mut files = self
let mut files: Vec<_> = self
.selected()
.into_iter()
.map(|f| {
(
f.path().into_os_string(),
if f.meta.is_dir() {
Some(MIME_DIR.to_owned())
} else {
self.mimetype.get(&f.path).cloned()
},
f.path_os_str().to_owned(),
if f.is_dir() { Some(MIME_DIR.to_owned()) } else { self.mimetype.get(f.path()).cloned() },
)
})
.collect::<Vec<_>>();
.collect();
if files.is_empty() {
return false;
@ -203,7 +199,7 @@ impl Manager {
return self.bulk_rename();
}
let Some(hovered) = self.hovered().map(|h| h.path()) else {
let Some(hovered) = self.hovered().map(|h| h.path_owned()) else {
return false;
};
@ -221,10 +217,10 @@ impl Manager {
}
pub fn bulk_rename(&self) -> bool {
let mut old: Vec<_> = self.selected().iter().map(|&f| f.path()).collect();
let old: Vec<_> = self.selected().into_iter().map(|f| f.path()).collect();
let root = max_common_root(&old);
old.iter_mut().for_each(|p| *p = p.strip_prefix(&root).unwrap().to_owned());
let old: Vec<_> = old.into_iter().map(|p| p.strip_prefix(&root).unwrap().to_owned()).collect();
let tmp = BOOT.tmpfile("bulk");
tokio::spawn(async move {
@ -327,7 +323,7 @@ impl Manager {
pub fn update_read(&mut self, op: FilesOp) -> bool {
let path = op.path();
let cwd = self.cwd().to_owned();
let hovered = self.hovered().map(|h| h.path());
let hovered = self.hovered().map(|h| h.path_owned());
let mut b = if cwd == path && !self.current().in_search {
self.current_mut().update(op)
@ -341,13 +337,13 @@ impl Manager {
.or_insert_with(|| Folder::new(&path))
.update(op);
matches!(self.hovered(), Some(h) if h.path == path)
matches!(self.hovered(), Some(h) if h.path() == &path)
};
b |= self.active_mut().parent.as_mut().map_or(false, |p| p.hover(&cwd));
b |= hovered.as_ref().map_or(false, |h| self.current_mut().hover(h));
if hovered != self.hovered().map(|h| h.path()) {
if hovered.as_ref() != self.hovered().map(|h| h.path()) {
emit!(Hover);
}
b
@ -403,8 +399,8 @@ impl Manager {
return b;
};
if hovered.meta.is_dir() {
self.watcher.trigger_dirs(&[&hovered.path]);
if hovered.is_dir() {
self.watcher.trigger_dirs(&[hovered.path()]);
}
b
}

View file

@ -49,6 +49,12 @@ impl Mode {
}
impl Mode {
#[inline]
pub fn is_select(&self) -> bool { matches!(self, Mode::Select(..)) }
#[inline]
pub fn is_unset(&self) -> bool { matches!(self, Mode::Unset(..)) }
#[inline]
pub fn is_visual(&self) -> bool { matches!(self, Mode::Select(..) | Mode::Unset(..)) }
}

View file

@ -1,4 +1,4 @@
use std::{collections::{BTreeMap, BTreeSet}, ffi::{OsStr, OsString}, mem, path::{Path, PathBuf}};
use std::{borrow::Cow, collections::{BTreeMap, BTreeSet}, ffi::{OsStr, OsString}, mem, path::{Path, PathBuf}};
use anyhow::{Error, Result};
use config::{open::Opener, MANAGER};
@ -7,17 +7,18 @@ use shared::{Defer, MIME_DIR};
use tokio::task::JoinHandle;
use super::{Folder, Mode, Preview, PreviewLock};
use crate::{emit, external::{self, FzfOpt, ZoxideOpt}, files::{File, Files, FilesOp}, input::InputOpt, Event, BLOCKER};
use crate::{emit, external::{self, FzfOpt, ZoxideOpt}, files::{File, Files, FilesOp, FilesSorter}, input::InputOpt, Event, BLOCKER};
pub struct Tab {
pub(super) mode: Mode,
pub(super) current: Folder,
pub(super) parent: Option<Folder>,
search: Option<JoinHandle<Result<()>>>,
pub(super) history: BTreeMap<PathBuf, Folder>,
pub(super) preview: Preview,
search: Option<JoinHandle<Result<()>>>,
pub(super) show_hidden: bool,
}
impl Tab {
@ -27,20 +28,17 @@ impl Tab {
current: Folder::new(path),
parent: path.parent().map(Folder::new),
search: None,
history: Default::default(),
preview: Default::default(),
search: None,
show_hidden: true,
}
}
pub fn escape(&mut self) -> bool {
if let Some((_, indices)) = self.mode.visual() {
let b = matches!(self.mode, Mode::Select(..));
for idx in indices.iter() {
self.current.select(Some(*idx), Some(b));
}
self.current.files.select_index(indices, Some(self.mode.is_select()));
self.mode = Mode::Normal;
return true;
}
@ -82,7 +80,7 @@ impl Tab {
};
let mut hovered = None;
if !file.meta.is_dir() {
if !file.is_dir() {
hovered = Some(file);
target = target.parent().unwrap().to_path_buf();
}
@ -131,11 +129,11 @@ impl Tab {
let Some(hovered) = self.current.hovered.clone() else {
return false;
};
if !hovered.meta.is_dir() {
if !hovered.is_dir() {
return false;
}
let rep = self.history_new(&hovered.path);
let rep = self.history_new(hovered.path());
let rep = mem::replace(&mut self.current, rep);
if !rep.in_search {
self.history.insert(rep.cwd.clone(), rep);
@ -144,7 +142,7 @@ impl Tab {
if let Some(rep) = self.parent.take() {
self.history.insert(rep.cwd.clone(), rep);
}
self.parent = Some(self.history_new(hovered.path.parent().unwrap()));
self.parent = Some(self.history_new(hovered.parent().unwrap()));
emit!(Refresh);
true
@ -155,7 +153,7 @@ impl Tab {
.current
.hovered
.as_ref()
.and_then(|h| h.path.parent())
.and_then(|h| h.parent())
.and_then(|p| if p == self.current.cwd { None } else { Some(p) })
.or_else(|| self.current.cwd.parent());
@ -187,11 +185,13 @@ impl Tab {
pub fn forward(&mut self) -> bool { false }
pub fn select(&mut self, state: Option<bool>) -> bool {
let idx = Some(self.current.cursor());
self.current.select(idx, state)
if let Some(ref hovered) = self.current.hovered {
return self.current.files.select(hovered.path(), state);
}
false
}
pub fn select_all(&mut self, state: Option<bool>) -> bool { self.current.select(None, state) }
pub fn select_all(&mut self, state: Option<bool>) -> bool { self.current.files.select_all(state) }
pub fn visual_mode(&mut self, unset: bool) -> bool {
let idx = self.current.cursor();
@ -209,10 +209,10 @@ impl Tab {
let mut it = self.selected().into_iter().peekable();
while let Some(f) = it.next() {
s.push(match type_ {
"path" => f.path.as_os_str(),
"dirname" => f.path.parent().map_or(OsStr::new(""), |p| p.as_os_str()),
"filename" => f.path.file_name().unwrap_or(OsStr::new("")),
"name_without_ext" => f.path.file_stem().unwrap_or(OsStr::new("")),
"path" => f.path_os_str(),
"dirname" => f.parent().map_or(OsStr::new(""), |p| p.as_os_str()),
"filename" => f.name().unwrap_or(OsStr::new("")),
"name_without_ext" => f.stem().unwrap_or(OsStr::new("")),
_ => return false,
});
if it.peek().is_some() {
@ -230,7 +230,7 @@ impl Tab {
}
let cwd = self.current.cwd.clone();
let hidden = self.current.files.show_hidden;
let hidden = self.show_hidden;
self.search = Some(tokio::spawn(async move {
let subject = emit!(Input(InputOpt::top("Search:"))).await?;
@ -243,7 +243,7 @@ impl Tab {
emit!(Files(FilesOp::search_empty(&cwd)));
while let Some(chunk) = rx.next().await {
emit!(Files(FilesOp::Search(cwd.clone(), Files::read(chunk).await)));
emit!(Files(FilesOp::Search(cwd.clone(), Files::read(&chunk).await)));
}
Ok(())
}));
@ -288,7 +288,7 @@ impl Tab {
let selected: Vec<_> = self
.selected()
.into_iter()
.map(|f| (f.path.as_os_str().to_owned(), Default::default()))
.map(|f| (f.path_os_str().to_owned(), Default::default()))
.collect();
let mut exec = exec.to_owned();
@ -315,7 +315,7 @@ impl Tab {
return;
};
if path.as_ref().map(|p| *p != hovered.path).unwrap_or(false) {
if path.as_ref().map(|p| p != hovered.path()).unwrap_or(false) {
return;
} else if !self.preview.arrow(step, path.is_some()) {
return;
@ -333,7 +333,7 @@ impl Tab {
}
pub fn update_preview(&mut self, lock: PreviewLock) -> bool {
let Some(hovered) = self.current.hovered.as_ref().map(|h| &h.path) else {
let Some(hovered) = self.current.hovered.as_ref().map(|h| h.path()) else {
return self.preview.reset();
};
@ -347,9 +347,16 @@ impl Tab {
}
impl Tab {
// --- Mode
#[inline]
pub fn mode(&self) -> &Mode { &self.mode }
#[inline]
pub fn in_selecting(&self) -> bool {
self.mode().is_visual() || self.current.files.has_selected()
}
// --- Current
#[inline]
pub fn name(&self) -> &str {
self
@ -363,19 +370,9 @@ impl Tab {
pub fn selected(&self) -> Vec<&File> {
let mode = self.mode();
let files = &self.current.files;
let selected: Vec<_> = if !mode.is_visual() {
files.iter().filter(|(_, f)| f.is_selected).map(|(_, f)| f).collect()
} else {
files
.iter()
.enumerate()
.filter(|(i, (_, f))| mode.pending(*i, f.is_selected))
.map(|(_, (_, f))| f)
.collect()
};
let pending = mode.visual().map(|(_, p)| Cow::Borrowed(p)).unwrap_or_default();
let selected = self.current.files.selected(&pending, mode.is_unset());
if selected.is_empty() {
self.current.hovered.as_ref().map(|h| vec![h]).unwrap_or_default()
} else {
@ -383,9 +380,7 @@ impl Tab {
}
}
#[inline]
pub fn in_selecting(&self) -> bool { self.mode().is_visual() || self.current.has_selected() }
// --- History
#[inline]
pub fn history(&self, path: &Path) -> Option<&Folder> { self.history.get(path) }
@ -394,6 +389,7 @@ impl Tab {
self.history.remove(path).unwrap_or_else(|| Folder::new(path))
}
// --- Preview
#[inline]
pub fn preview(&self) -> &Preview { &self.preview }
@ -402,4 +398,47 @@ impl Tab {
#[inline]
pub fn preview_reset_image(&mut self) -> bool { self.preview.reset_image() }
// --- Sorter
pub fn set_sorter(&mut self, sorter: FilesSorter) -> bool {
if !self.current.files.set_sorter(sorter) {
return false;
}
self.current.hover_repos();
true
}
// --- Show hidden
pub fn set_show_hidden(&mut self, state: Option<bool>) -> bool {
let state = state.unwrap_or(!self.show_hidden);
if state == self.show_hidden {
return false;
}
self.show_hidden = state;
self.apply_show_hidden();
true
}
pub fn apply_show_hidden(&mut self) -> bool {
let state = self.show_hidden;
let mut applied = false;
applied |= self.current.files.set_show_hidden(state);
if let Some(parent) = self.parent.as_mut() {
applied |= parent.files.set_show_hidden(state);
}
applied |= match self.current.hovered {
Some(ref h) if h.is_dir() => {
self.history.get_mut(h.path()).map(|f| f.files.set_show_hidden(state)).unwrap_or(false)
}
_ => false,
};
self.current.hover_repos();
applied
}
}

View file

@ -1,6 +1,6 @@
use std::path::Path;
use config::BOOT;
use config::{BOOT, MANAGER};
use super::Tab;
use crate::emit;
@ -14,7 +14,10 @@ pub struct Tabs {
impl Tabs {
pub fn make() -> Self {
let mut tabs = Self { idx: usize::MAX, items: vec![Tab::new(&BOOT.cwd)] };
let mut tab = Tab::new(&BOOT.cwd);
tab.set_show_hidden(Some(MANAGER.show_hidden));
let mut tabs = Self { idx: usize::MAX, items: vec![tab] };
tabs.set_idx(0);
tabs
}
@ -24,7 +27,10 @@ impl Tabs {
return false;
}
self.items.insert(self.idx + 1, Tab::new(path));
let mut tab = Tab::new(path);
tab.set_show_hidden(Some(self.active().show_hidden));
self.items.insert(self.idx + 1, tab);
self.set_idx(self.idx + 1);
true
}

View file

@ -1,4 +1,4 @@
use std::{collections::{BTreeMap, BTreeSet}, path::{Path, PathBuf}, sync::Arc, time::Duration};
use std::{collections::BTreeSet, path::{Path, PathBuf}, sync::Arc, time::Duration};
use futures::StreamExt;
use indexmap::IndexMap;
@ -69,44 +69,55 @@ impl Watcher {
instance
}
pub(super) fn watch(&mut self, mut to_watch: BTreeSet<PathBuf>) {
let keys = self.watched.read().keys().cloned().collect::<BTreeSet<_>>();
for p in keys.difference(&to_watch) {
self.watcher.unwatch(p).ok();
pub(super) fn watch(&mut self, mut watched: BTreeSet<&PathBuf>) {
let (to_unwatch, to_watch): (BTreeSet<_>, BTreeSet<_>) = {
let guard = self.watched.read();
let keys = guard.keys().collect::<BTreeSet<_>>();
(
keys.difference(&watched).map(|&x| x.clone()).collect(),
watched.difference(&keys).map(|&x| x.clone()).collect(),
)
};
for p in to_unwatch {
self.watcher.unwatch(&p).ok();
}
for p in to_watch.clone().difference(&keys) {
if self.watcher.watch(p, RecursiveMode::NonRecursive).is_err() {
to_watch.remove(p);
for p in to_watch {
if self.watcher.watch(&p, RecursiveMode::NonRecursive).is_err() {
watched.remove(&p);
}
}
let mut todo = Vec::new();
let mut watched = self.watched.write();
*watched = IndexMap::from_iter(to_watch.into_iter().map(|k| {
if let Some(v) = watched.remove(&k) {
(k, v)
} else {
todo.push(k.clone());
(k, None)
}
}));
watched.sort_unstable_by(|_, a, _, b| b.cmp(a));
let mut to_resolve = Vec::new();
let mut guard = self.watched.write();
*guard = watched
.into_iter()
.map(|k| {
if let Some((k, v)) = guard.remove_entry(k) {
(k, v)
} else {
to_resolve.push(k.clone());
(k.clone(), None)
}
})
.collect();
guard.sort_unstable_by(|_, a, _, b| b.cmp(a));
let watched = self.watched.clone();
let lock = self.watched.clone();
tokio::spawn(async move {
let mut ext = IndexMap::new();
for k in todo {
for k in to_resolve {
match fs::canonicalize(&k).await {
Ok(v) if v != k => {
Ok(v) if v != *k => {
ext.insert(k, Some(v));
}
_ => {}
}
}
let mut watched = watched.write();
watched.extend(ext);
watched.sort_unstable_by(|_, a, _, b| b.cmp(a));
let mut guard = lock.write();
guard.extend(ext);
guard.sort_unstable_by(|_, a, _, b| b.cmp(a));
});
}
@ -170,11 +181,12 @@ impl Watcher {
for ori in linked {
emit!(Files(match &result {
Ok(items) => {
let files = BTreeMap::from_iter(items.iter().map(|(p, f)| {
let p = ori.join(p.strip_prefix(path).unwrap());
let f = f.clone().set_path(&p);
(p, f)
}));
let mut files = Vec::with_capacity(items.len());
for item in items {
let mut file = item.clone();
file.set_path(ori.join(item.path().strip_prefix(path).unwrap()));
files.push(file);
}
FilesOp::Read(ori, files)
}
Err(_) => FilesOp::IOErr(ori),

View file

@ -318,13 +318,13 @@ impl Scheduler {
});
}
pub(super) fn precache_size(&self, targets: Vec<PathBuf>) {
pub(super) fn precache_size(&self, targets: Vec<&PathBuf>) {
let throttle = Arc::new(Throttle::new(targets.len(), Duration::from_millis(300)));
let mut handing = self.precache.size_handing.lock();
let mut running = self.running.write();
for target in targets {
if !handing.contains(&target) {
if !handing.contains(target) {
handing.insert(target.clone());
} else {
continue;
@ -333,6 +333,7 @@ impl Scheduler {
let id = running.add(format!("Calculate the size of {:?}", target));
let _ = self.todo.send_blocking({
let precache = self.precache.clone();
let target = target.clone();
let throttle = throttle.clone();
async move {
precache.size(PrecacheOpSize { id, target, throttle }).await.ok();

View file

@ -212,15 +212,12 @@ impl Tasks {
#[inline]
pub fn precache_size(&self, targets: &Files) -> bool {
if targets.sorter.by != SortBy::Size {
if targets.sorter().by != SortBy::Size {
return false;
}
let targets = targets
.iter()
.filter(|(_, f)| f.meta.is_dir() && f.length.is_none())
.map(|(p, _)| p.clone())
.collect::<Vec<_>>();
let targets: Vec<_> =
targets.iter().filter(|f| f.is_dir() && f.length().is_none()).map(|f| f.path()).collect();
if !targets.is_empty() {
self.scheduler.precache_size(targets);
@ -230,12 +227,12 @@ impl Tasks {
}
#[inline]
pub fn precache_mime(&self, targets: Vec<&File>, mimetype: &HashMap<PathBuf, String>) -> bool {
let targets = targets
.into_iter()
.filter(|f| f.meta.is_file() && !mimetype.contains_key(&f.path))
.map(|f| f.path.clone())
.collect::<Vec<_>>();
pub fn precache_mime(&self, targets: &[File], mimetype: &HashMap<PathBuf, String>) -> bool {
let targets: Vec<_> = targets
.iter()
.filter(|f| f.is_file() && !mimetype.contains_key(f.path()))
.map(|f| f.path_owned())
.collect();
if !targets.is_empty() {
self.scheduler.precache_mime(targets);

View file

@ -7,7 +7,7 @@ use parking_lot::Mutex;
use shared::{calculate_size, Throttle};
use tokio::{fs, sync::mpsc};
use crate::{emit, external, files::{File, FilesOp}, tasks::TaskOp};
use crate::{emit, external, files::FilesOp, tasks::TaskOp};
pub(crate) struct Precache {
rx: async_channel::Receiver<PrecacheOp>,
@ -29,7 +29,7 @@ pub(crate) enum PrecacheOp {
pub(crate) struct PrecacheOpSize {
pub id: usize,
pub target: PathBuf,
pub throttle: Arc<Throttle<(PathBuf, File)>>,
pub throttle: Arc<Throttle<(PathBuf, u64)>>,
}
#[derive(Debug)]
@ -121,21 +121,16 @@ impl Precache {
pub(crate) async fn size(&self, task: PrecacheOpSize) -> Result<()> {
self.sch.send(TaskOp::New(task.id, 0))?;
let length = Some(calculate_size(&task.target).await);
if let Ok(mut file) = File::from(&task.target).await {
file.length = length;
task.throttle.done((task.target, file), |buf| {
let mut handing = self.size_handing.lock();
for (path, _) in &buf {
handing.remove(path);
}
let length = calculate_size(&task.target).await;
task.throttle.done((task.target, length), |buf| {
let mut handing = self.size_handing.lock();
for (path, _) in &buf {
handing.remove(path);
}
let parent = buf[0].0.parent().unwrap().to_path_buf();
emit!(Files(FilesOp::Sort(parent, BTreeMap::from_iter(buf))));
});
} else {
self.size_handing.lock().remove(&task.target);
};
let parent = buf[0].0.parent().unwrap().to_path_buf();
emit!(Files(FilesOp::Size(parent, BTreeMap::from_iter(buf))));
});
self.sch.send(TaskOp::Adv(task.id, 1, 0))?;
self.done(task.id)

View file

@ -148,12 +148,12 @@ pub fn file_mode(mode: u32) -> String {
// Find the max common root of a list of files
// e.g. /a/b/c, /a/b/d -> /a/b
// /aa/bb/cc, /aa/dd/ee -> /aa
pub fn max_common_root(files: &[PathBuf]) -> PathBuf {
pub fn max_common_root(files: &[impl AsRef<Path>]) -> PathBuf {
if files.is_empty() {
return PathBuf::new();
}
let mut it = files.iter().map(|p| p.parent().unwrap_or(Path::new("")).components());
let mut it = files.iter().map(|p| p.as_ref().parent().unwrap_or(Path::new("")).components());
let mut root = it.next().unwrap().collect::<PathBuf>();
for components in it {
let mut new_root = PathBuf::new();
@ -170,16 +170,24 @@ pub fn max_common_root(files: &[PathBuf]) -> PathBuf {
#[test]
fn test_max_common_root() {
assert_eq!(max_common_root(&[]).as_os_str(), "");
assert_eq!(max_common_root(&["".into()]).as_os_str(), "");
assert_eq!(max_common_root(&["a".into()]).as_os_str(), "");
assert_eq!(max_common_root(&["/a".into()]).as_os_str(), "/");
assert_eq!(max_common_root(&["/a/b".into()]).as_os_str(), "/a");
assert_eq!(max_common_root(&["/a/b/c".into(), "/a/b/d".into()]).as_os_str(), "/a/b");
assert_eq!(max_common_root(&["/aa/bb/cc".into(), "/aa/dd/ee".into()]).as_os_str(), "/aa");
assert_eq!(max_common_root(&[] as &[PathBuf]).as_os_str(), "");
assert_eq!(max_common_root(&["".into()] as &[PathBuf]).as_os_str(), "");
assert_eq!(max_common_root(&["a".into()] as &[PathBuf]).as_os_str(), "");
assert_eq!(max_common_root(&["/a".into()] as &[PathBuf]).as_os_str(), "/");
assert_eq!(max_common_root(&["/a/b".into()] as &[PathBuf]).as_os_str(), "/a");
assert_eq!(
max_common_root(&["/aa/bb/cc".into(), "/aa/bb/cc/dd/ee".into(), "/aa/bb/cc/ff".into()])
.as_os_str(),
max_common_root(&["/a/b/c".into(), "/a/b/d".into()] as &[PathBuf]).as_os_str(),
"/a/b"
);
assert_eq!(
max_common_root(&["/aa/bb/cc".into(), "/aa/dd/ee".into()] as &[PathBuf]).as_os_str(),
"/aa"
);
assert_eq!(
max_common_root(
&["/aa/bb/cc".into(), "/aa/bb/cc/dd/ee".into(), "/aa/bb/cc/ff".into()] as &[PathBuf]
)
.as_os_str(),
"/aa/bb"
);
}