feat: sort naturally (#82)

This commit is contained in:
Anton Simonov 2023-08-26 06:15:07 +03:00 committed by GitHub
parent a7a085e41c
commit 388ad43537
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 136 additions and 87 deletions

7
Cargo.lock generated
View file

@ -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"

View file

@ -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"),

View file

@ -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

View file

@ -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" },

View file

@ -8,6 +8,7 @@ pub enum SortBy {
Alphabetical,
Created,
Modified,
Natural,
Size,
}
@ -19,6 +20,7 @@ impl TryFrom<String> for SortBy {
"alphabetical" => Self::Alphabetical,
"created" => Self::Created,
"modified" => Self::Modified,
"natural" => Self::Natural,
"size" => Self::Size,
_ => bail!("invalid sort_by value: {s}"),
})

View file

@ -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"

View file

@ -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<PathBuf, File>,
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<PathBuf, File>) -> 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<PathBuf, File>) -> 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<T: Ord>(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<PathBuf, File>),

View file

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

96
core/src/files/sorter.rs Normal file
View file

@ -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<PathBuf, 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)))
}
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<PathBuf, 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() {
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<T: Ord>(&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 }
}
}

View file

@ -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;
}

View file

@ -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"}