From 388ad43537c802eeec3d1bde49078dff410e67f8 Mon Sep 17 00:00:00 2001 From: Anton Simonov Date: Sat, 26 Aug 2023 06:15:07 +0300 Subject: [PATCH] feat: sort naturally (#82) --- Cargo.lock | 7 +++ app/src/executor.rs | 4 +- config/docs/yazi.md | 3 +- config/preset/keymap.toml | 18 ++++--- config/src/manager/sorting.rs | 2 + core/Cargo.toml | 1 + core/src/files/files.rs | 86 +++++-------------------------- core/src/files/mod.rs | 2 + core/src/files/sorter.rs | 96 +++++++++++++++++++++++++++++++++++ core/src/tasks/tasks.rs | 2 +- cspell.json | 2 +- 11 files changed, 136 insertions(+), 87 deletions(-) create mode 100644 core/src/files/sorter.rs diff --git a/Cargo.lock b/Cargo.lock index 375fcd70..a4927088 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -381,6 +381,7 @@ dependencies = [ "crossterm 0.27.0", "futures", "indexmap 2.0.0", + "natord", "notify", "parking_lot", "ratatui", @@ -1133,6 +1134,12 @@ dependencies = [ "getrandom", ] +[[package]] +name = "natord" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "308d96db8debc727c3fd9744aac51751243420e46edf401010908da7f8d5e57c" + [[package]] name = "nom" version = "7.1.3" diff --git a/app/src/executor.rs b/app/src/executor.rs index 11df46c4..909bd952 100644 --- a/app/src/executor.rs +++ b/app/src/executor.rs @@ -1,4 +1,4 @@ -use core::{emit, files::FilesSort, input::InputMode}; +use core::{emit, files::FilesSorter, input::InputMode}; use std::path::PathBuf; use config::{keymap::{Control, Exec, Key, KeymapLayer}, manager::SortBy, KEYMAP}; @@ -130,7 +130,7 @@ impl Executor { // Sorting "sort" => { - let b = cx.manager.current_mut().files.set_sort(FilesSort { + let b = cx.manager.current_mut().files.set_sorter(FilesSorter { by: SortBy::try_from(exec.args.get(0).cloned().unwrap_or_default()) .unwrap_or_default(), reverse: exec.named.contains_key("reverse"), diff --git a/config/docs/yazi.md b/config/docs/yazi.md index b375b8b3..1e221f4b 100644 --- a/config/docs/yazi.md +++ b/config/docs/yazi.md @@ -8,9 +8,10 @@ - sort_by: File sorting method - - `"alphabetical"`: Sort alphabetically + - `"alphabetical"`: Sort alphabetically, e.g. `1.md` < `10.md` < `2.md` - `"created"`: Sort by creation time - `"modified"`: Sort by last modified time + - `"natural"`: Sort naturally, e.g. `1.md` < `2.md` < `10.md` - `"size"`: Sort by file size - sort_reverse: Display files in reverse order diff --git a/config/preset/keymap.toml b/config/preset/keymap.toml index adfb38c7..ae7cc053 100644 --- a/config/preset/keymap.toml +++ b/config/preset/keymap.toml @@ -62,14 +62,16 @@ keymap = [ { on = [ "c", "n" ], exec = "copy name_without_ext" }, # Sorting - { on = [ ",", "a" ], exec = "sort alphabetical" }, - { on = [ ",", "A" ], exec = "sort alphabetical --reverse" }, - { on = [ ",", "c" ], exec = "sort created" }, - { on = [ ",", "C" ], exec = "sort created --reverse" }, - { on = [ ",", "m" ], exec = "sort modified" }, - { on = [ ",", "M" ], exec = "sort modified --reverse" }, - { on = [ ",", "s" ], exec = "sort size" }, - { on = [ ",", "S" ], exec = "sort size --reverse" }, + { on = [ ",", "a" ], exec = "sort alphabetical --dir_first" }, + { on = [ ",", "A" ], exec = "sort alphabetical --reverse --dir_first" }, + { on = [ ",", "c" ], exec = "sort created --dir_first" }, + { on = [ ",", "C" ], exec = "sort created --reverse --dir_first" }, + { on = [ ",", "m" ], exec = "sort modified --dir_first" }, + { on = [ ",", "M" ], exec = "sort modified --reverse --dir_first" }, + { on = [ ",", "n" ], exec = "sort natural --dir_first" }, + { on = [ ",", "N" ], exec = "sort natural --reverse --dir_first" }, + { on = [ ",", "s" ], exec = "sort size --dir_first" }, + { on = [ ",", "S" ], exec = "sort size --reverse --dir_first" }, # Tabs { on = [ "t" ], exec = "tab_create --current" }, diff --git a/config/src/manager/sorting.rs b/config/src/manager/sorting.rs index 8c0010e5..6d0b565f 100644 --- a/config/src/manager/sorting.rs +++ b/config/src/manager/sorting.rs @@ -8,6 +8,7 @@ pub enum SortBy { Alphabetical, Created, Modified, + Natural, Size, } @@ -19,6 +20,7 @@ impl TryFrom for SortBy { "alphabetical" => Self::Alphabetical, "created" => Self::Created, "modified" => Self::Modified, + "natural" => Self::Natural, "size" => Self::Size, _ => bail!("invalid sort_by value: {s}"), }) diff --git a/core/Cargo.toml b/core/Cargo.toml index 3645e372..9997d120 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -14,6 +14,7 @@ async-channel = "^1" crossterm = "^0" futures = "^0" indexmap = "^2" +natord = "^1" notify = { version = "^6", default-features = false, features = [ "macos_fsevent" ] } parking_lot = "^0" ratatui = "^0" diff --git a/core/src/files/files.rs b/core/src/files/files.rs index 8af98a90..255af694 100644 --- a/core/src/files/files.rs +++ b/core/src/files/files.rs @@ -1,15 +1,15 @@ -use std::{cmp::Ordering, collections::BTreeMap, ops::{Deref, DerefMut}, path::{Path, PathBuf}}; +use std::{collections::BTreeMap, ops::{Deref, DerefMut}, path::{Path, PathBuf}}; use anyhow::Result; -use config::{manager::SortBy, MANAGER}; +use config::MANAGER; use indexmap::IndexMap; use tokio::fs; -use super::File; +use super::{File, FilesSorter}; pub struct Files { items: IndexMap, - pub sort: FilesSort, + pub sorter: FilesSorter, pub show_hidden: bool, } @@ -17,7 +17,7 @@ impl Default for Files { fn default() -> Self { Self { items: Default::default(), - sort: Default::default(), + sorter: Default::default(), show_hidden: MANAGER.show_hidden, } } @@ -53,12 +53,12 @@ impl Files { } #[inline] - pub fn set_sort(&mut self, sort: FilesSort) -> bool { - if self.sort == sort { + pub fn set_sorter(&mut self, sort: FilesSorter) -> bool { + if self.sorter == sort { return false; } - self.sort = sort; - self.sort() + self.sorter = sort; + self.sorter.sort(&mut self.items) } pub fn update_read(&mut self, mut items: BTreeMap) -> bool { @@ -80,7 +80,7 @@ impl Files { self.items.clear(); self.items.extend(items); - self.sort(); + self.sorter.sort(&mut self.items); true } @@ -92,14 +92,14 @@ impl Files { } self.items.extend(items); - self.sort(); + self.sorter.sort(&mut self.items); true } pub fn update_search(&mut self, items: BTreeMap) -> bool { if !items.is_empty() { self.items.extend(items); - self.sort(); + self.sorter.sort(&mut self.items); return true; } @@ -110,51 +110,6 @@ impl Files { false } - - fn sort(&mut self) -> bool { - if self.items.is_empty() { - return false; - } - - #[inline] - #[allow(clippy::collapsible_else_if)] - fn cmp(a: T, b: T, reverse: bool, promote: Ordering) -> Ordering { - if promote != Ordering::Equal { - promote - } else { - if reverse { b.cmp(&a) } else { a.cmp(&b) } - } - } - - #[inline] - fn promote(a: &File, b: &File, dir_first: bool) -> Ordering { - if dir_first { b.meta.is_dir().cmp(&a.meta.is_dir()) } else { Ordering::Equal } - } - - let reverse = self.sort.reverse; - let dir_first = self.sort.dir_first; - match self.sort.by { - SortBy::Alphabetical => { - self.items.sort_by(|_, a, _, b| cmp(&a.path, &b.path, reverse, promote(a, b, dir_first))) - } - SortBy::Created => self.items.sort_by(|_, a, _, b| { - if let (Ok(aa), Ok(bb)) = (a.meta.created(), b.meta.created()) { - return cmp(aa, bb, reverse, promote(a, b, dir_first)); - } - Ordering::Equal - }), - SortBy::Modified => self.items.sort_by(|_, a, _, b| { - if let (Ok(aa), Ok(bb)) = (a.meta.modified(), b.meta.modified()) { - return cmp(aa, bb, reverse, promote(a, b, dir_first)); - } - Ordering::Equal - }), - SortBy::Size => self.items.sort_by(|_, a, _, b| { - cmp(a.length.unwrap_or(0), b.length.unwrap_or(0), reverse, promote(a, b, dir_first)) - }), - } - true - } } impl Deref for Files { @@ -167,23 +122,6 @@ impl DerefMut for Files { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.items } } -#[derive(PartialEq)] -pub struct FilesSort { - pub by: SortBy, - pub reverse: bool, - pub dir_first: bool, -} - -impl Default for FilesSort { - fn default() -> Self { - Self { - by: MANAGER.sort_by, - reverse: MANAGER.sort_reverse, - dir_first: MANAGER.sort_dir_first, - } - } -} - #[derive(Debug)] pub enum FilesOp { Read(PathBuf, BTreeMap), diff --git a/core/src/files/mod.rs b/core/src/files/mod.rs index 35de6151..9d437be9 100644 --- a/core/src/files/mod.rs +++ b/core/src/files/mod.rs @@ -1,5 +1,7 @@ mod file; mod files; +mod sorter; pub use file::*; pub use files::*; +pub use sorter::*; diff --git a/core/src/files/sorter.rs b/core/src/files/sorter.rs new file mode 100644 index 00000000..c7dc7bc5 --- /dev/null +++ b/core/src/files/sorter.rs @@ -0,0 +1,96 @@ +use std::{cmp::Ordering, path::PathBuf}; + +use config::{manager::SortBy, MANAGER}; +use indexmap::IndexMap; + +use super::File; + +#[derive(PartialEq)] +pub struct FilesSorter { + pub by: SortBy, + pub reverse: bool, + pub dir_first: bool, +} + +impl Default for FilesSorter { + fn default() -> Self { + Self { + by: MANAGER.sort_by, + reverse: MANAGER.sort_reverse, + dir_first: MANAGER.sort_dir_first, + } + } +} + +impl FilesSorter { + pub(super) fn sort(&self, items: &mut IndexMap) -> 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))) + } + 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| { + 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)) + }), + } + true + } + + fn sort_naturally(&self, items: &mut IndexMap) { + 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() { + indices.push(i); + entities.push((path.to_string_lossy(), file)); + } + + indices.sort_unstable_by(|&a, &b| { + let promote = self.promote(entities[a].1, entities[b].1); + if promote != Ordering::Equal { + promote + } else if self.reverse { + natord::compare(&entities[b].0, &entities[a].0) + } else { + natord::compare(&entities[a].0, &entities[b].0) + } + }); + + let mut new = IndexMap::with_capacity(indices.len()); + for i in indices { + let file = entities[i].1.clone(); + new.insert(file.path(), file); + } + *items = new; + } + + #[inline] + #[allow(clippy::collapsible_else_if)] + fn cmp(&self, a: T, b: T, promote: Ordering) -> Ordering { + if promote != Ordering::Equal { + promote + } else { + if self.reverse { b.cmp(&a) } else { a.cmp(&b) } + } + } + + #[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 } + } +} diff --git a/core/src/tasks/tasks.rs b/core/src/tasks/tasks.rs index d3134b51..cb8267f6 100644 --- a/core/src/tasks/tasks.rs +++ b/core/src/tasks/tasks.rs @@ -212,7 +212,7 @@ impl Tasks { #[inline] pub fn precache_size(&self, targets: &Files) -> bool { - if targets.sort.by != SortBy::Size { + if targets.sorter.by != SortBy::Size { return false; } diff --git a/cspell.json b/cspell.json index 9058cffe..ed49cb39 100644 --- a/cspell.json +++ b/cspell.json @@ -1 +1 @@ -{"words":["Punct","KEYMAP","splitn","crossterm","YAZI","unar","peekable","ratatui","syntect","pbpaste","pbcopy","ffmpegthumbnailer","oneshot","Posix","Lsar","XADDOS","zoxide","cands","Deque","precache","imageops","IFBLK","IFCHR","IFDIR","IFIFO","IFLNK","IFMT","IFSOCK","IRGRP","IROTH","IRUSR","ISGID","ISUID","ISVTX","IWGRP","IWOTH","IWUSR","IXGRP","IXOTH","IXUSR","libc","winsize","TIOCGWINSZ","xpixel","ypixel","ioerr","appender","Catppuccin","macchiato","gitmodules","Dotfiles","bashprofile","vimrc","flac","webp","exiftool","mediainfo","ripgrep","nvim","indexmap","indexmap","unwatch","canonicalize","serde","fsevent","Ueberzug","iterm","wezterm","sixel","chafa","ueberzugpp","️ Überzug","️ Überzug","Konsole","Alacritty","Überzug","pkgs","paru","unarchiver","pdftoppm","poppler","prebuild","singlefile","jpegopt","EXIF","rustfmt","mktemp","nanos","xclip","xsel"],"language":"en","flagWords":[],"version":"0.2"} +{"flagWords":[],"words":["Punct","KEYMAP","splitn","crossterm","YAZI","unar","peekable","ratatui","syntect","pbpaste","pbcopy","ffmpegthumbnailer","oneshot","Posix","Lsar","XADDOS","zoxide","cands","Deque","precache","imageops","IFBLK","IFCHR","IFDIR","IFIFO","IFLNK","IFMT","IFSOCK","IRGRP","IROTH","IRUSR","ISGID","ISUID","ISVTX","IWGRP","IWOTH","IWUSR","IXGRP","IXOTH","IXUSR","libc","winsize","TIOCGWINSZ","xpixel","ypixel","ioerr","appender","Catppuccin","macchiato","gitmodules","Dotfiles","bashprofile","vimrc","flac","webp","exiftool","mediainfo","ripgrep","nvim","indexmap","indexmap","unwatch","canonicalize","serde","fsevent","Ueberzug","iterm","wezterm","sixel","chafa","ueberzugpp","️ Überzug","️ Überzug","Konsole","Alacritty","Überzug","pkgs","paru","unarchiver","pdftoppm","poppler","prebuild","singlefile","jpegopt","EXIF","rustfmt","mktemp","nanos","xclip","xsel","natord"],"language":"en","version":"0.2"}