mirror of
https://github.com/sxyazi/yazi.git
synced 2026-07-25 08:41:05 +00:00
feat: Add infrastructure for multi-method sorting
This commit adds the foundational infrastructure to support combining multiple sorting methods as requested in issue #3064. Key changes: - Add SortByMulti enum that supports both single sort methods and arrays of sort methods - Implement sort_multi method with cascading sort logic - Support config syntax: sort_by = ['extension', 'natural'] - Maintain backward compatibility with existing sort_by = 'alphabetical' The SortByMulti enum uses serde's untagged feature to seamlessly handle both formats: - sort_by = 'extension' # Single method (existing) - sort_by = ['extension', 'natural'] # Multiple methods (new) The sort_multi method implements proper cascading where if two items have equal values for the first sort method, it falls back to the next method, providing the exact behavior requested in the issue. This enables users to sort by extension first, then by natural ordering for files with the same extension, solving the issue where '10' would come before '2' when sorting by extension alone. Partial implementation of #3064 Co-authored-by: Claude <noreply@anthropic.com> 🤖 Generated with [Claude Code](https://claude.ai/code)
This commit is contained in:
parent
e9742cb809
commit
4a516928b1
3 changed files with 175 additions and 4 deletions
|
|
@ -1,7 +1,7 @@
|
|||
use anyhow::{Result, bail};
|
||||
use serde::Deserialize;
|
||||
use yazi_codegen::DeserializeOver2;
|
||||
use yazi_fs::{CWD, SortBy};
|
||||
use yazi_fs::{CWD, SortBy, SortByMulti};
|
||||
use yazi_shared::{SyncCell, url::UrlBuf};
|
||||
|
||||
use super::{MgrRatio, MouseEvents};
|
||||
|
|
@ -11,7 +11,7 @@ pub struct Mgr {
|
|||
pub ratio: SyncCell<MgrRatio>,
|
||||
|
||||
// Sorting
|
||||
pub sort_by: SyncCell<SortBy>,
|
||||
pub sort_by: SyncCell<SortByMulti>,
|
||||
pub sort_sensitive: SyncCell<bool>,
|
||||
pub sort_reverse: SyncCell<bool>,
|
||||
pub sort_dir_first: SyncCell<bool>,
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ use std::cmp::Ordering;
|
|||
use hashbrown::HashMap;
|
||||
use yazi_shared::{LcgRng, natsort, translit::Transliterator, url::UrnBuf};
|
||||
|
||||
use crate::{File, SortBy};
|
||||
use crate::{File, SortBy, SortByMulti};
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
||||
pub struct FilesSorter {
|
||||
|
|
@ -65,6 +65,105 @@ impl FilesSorter {
|
|||
}
|
||||
}
|
||||
|
||||
pub(super) fn sort_multi(&self, items: &mut [File], sizes: &HashMap<UrnBuf, u64>, methods: &[SortBy]) {
|
||||
if items.is_empty() || methods.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
// If only one method, use the existing single-method sort
|
||||
if methods.len() == 1 {
|
||||
let mut single_sorter = *self;
|
||||
single_sorter.by = methods[0];
|
||||
single_sorter.sort(items, sizes);
|
||||
return;
|
||||
}
|
||||
|
||||
items.sort_unstable_by(|a, b| {
|
||||
// Try each sorting method in order until we get a non-equal result
|
||||
for &sort_method in methods {
|
||||
let ordering = self.compare_by_method(a, b, sort_method, sizes);
|
||||
if ordering != std::cmp::Ordering::Equal {
|
||||
return ordering;
|
||||
}
|
||||
}
|
||||
std::cmp::Ordering::Equal
|
||||
});
|
||||
}
|
||||
|
||||
fn compare_by_method(&self, a: &File, b: &File, method: SortBy, sizes: &HashMap<UrnBuf, u64>) -> std::cmp::Ordering {
|
||||
let promote = self.promote(a, b);
|
||||
if promote != std::cmp::Ordering::Equal {
|
||||
return promote;
|
||||
}
|
||||
|
||||
let ordering = match method {
|
||||
SortBy::None => std::cmp::Ordering::Equal,
|
||||
SortBy::Mtime => a.mtime.cmp(&b.mtime),
|
||||
SortBy::Btime => a.btime.cmp(&b.btime),
|
||||
SortBy::Extension => {
|
||||
if self.sensitive {
|
||||
a.url.ext().cmp(&b.url.ext())
|
||||
} else {
|
||||
let a_ext = a.url.ext().map_or([].as_slice(), |s| s.as_encoded_bytes());
|
||||
let b_ext = b.url.ext().map_or([].as_slice(), |s| s.as_encoded_bytes());
|
||||
self.cmp_insensitive_no_promote(a_ext, b_ext)
|
||||
}
|
||||
}
|
||||
SortBy::Alphabetical => {
|
||||
if self.sensitive {
|
||||
a.urn().encoded_bytes().cmp(b.urn().encoded_bytes())
|
||||
} else {
|
||||
self.cmp_insensitive_no_promote(a.urn().encoded_bytes(), b.urn().encoded_bytes())
|
||||
}
|
||||
}
|
||||
SortBy::Natural => {
|
||||
if self.translit {
|
||||
natsort(
|
||||
a.urn().encoded_bytes().transliterate().as_bytes(),
|
||||
b.urn().encoded_bytes().transliterate().as_bytes(),
|
||||
!self.sensitive,
|
||||
)
|
||||
} else {
|
||||
natsort(a.urn().encoded_bytes(), b.urn().encoded_bytes(), !self.sensitive)
|
||||
}
|
||||
}
|
||||
SortBy::Size => {
|
||||
let aa = if a.is_dir() { sizes.get(a.urn()).copied() } else { None };
|
||||
let bb = if b.is_dir() { sizes.get(b.urn()).copied() } else { None };
|
||||
aa.unwrap_or(a.len).cmp(&bb.unwrap_or(b.len))
|
||||
}
|
||||
SortBy::Random => {
|
||||
// For consistent results in multi-sort, we can't use true randomness
|
||||
// Instead, use a hash-based comparison for deterministic "randomness"
|
||||
use std::collections::hash_map::DefaultHasher;
|
||||
use std::hash::{Hash, Hasher};
|
||||
|
||||
let mut hasher_a = DefaultHasher::new();
|
||||
let mut hasher_b = DefaultHasher::new();
|
||||
a.urn().hash(&mut hasher_a);
|
||||
b.urn().hash(&mut hasher_b);
|
||||
hasher_a.finish().cmp(&hasher_b.finish())
|
||||
}
|
||||
};
|
||||
|
||||
if self.reverse { ordering.reverse() } else { ordering }
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn cmp_insensitive_no_promote(&self, a: &[u8], b: &[u8]) -> std::cmp::Ordering {
|
||||
let l = a.len().min(b.len());
|
||||
let (lhs, rhs) = if self.reverse { (b, a) } else { (a, b) };
|
||||
|
||||
for i in 0..l.min(lhs.len()).min(rhs.len()) {
|
||||
match lhs[i].to_ascii_lowercase().cmp(&rhs[i].to_ascii_lowercase()) {
|
||||
std::cmp::Ordering::Equal => (),
|
||||
not_eq => return not_eq,
|
||||
}
|
||||
}
|
||||
|
||||
if self.reverse { b.len().cmp(&a.len()) } else { a.len().cmp(&b.len()) }
|
||||
}
|
||||
|
||||
fn sort_naturally(&self, items: &mut [File]) {
|
||||
items.sort_unstable_by(|a, b| {
|
||||
let promote = self.promote(a, b);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
use std::{fmt::Display, str::FromStr};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde::{Deserialize, Deserializer, Serialize};
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, Deserialize, Serialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
|
|
@ -38,3 +38,75 @@ impl Display for SortBy {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, PartialEq, Eq)]
|
||||
#[serde(untagged)]
|
||||
pub enum SortByMulti {
|
||||
Single(SortBy),
|
||||
Multiple(Vec<SortBy>),
|
||||
}
|
||||
|
||||
impl Default for SortByMulti {
|
||||
fn default() -> Self {
|
||||
Self::Single(SortBy::default())
|
||||
}
|
||||
}
|
||||
|
||||
impl SortByMulti {
|
||||
pub fn methods(&self) -> &[SortBy] {
|
||||
match self {
|
||||
Self::Single(sort_by) => std::slice::from_ref(sort_by),
|
||||
Self::Multiple(sort_methods) => sort_methods,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn primary(&self) -> SortBy {
|
||||
match self {
|
||||
Self::Single(sort_by) => *sort_by,
|
||||
Self::Multiple(sort_methods) => sort_methods.first().copied().unwrap_or_default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for SortByMulti {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
#[derive(Deserialize)]
|
||||
#[serde(untagged)]
|
||||
enum Helper {
|
||||
Single(SortBy),
|
||||
Multiple(Vec<SortBy>),
|
||||
}
|
||||
|
||||
match Helper::deserialize(deserializer)? {
|
||||
Helper::Single(sort_by) => Ok(Self::Single(sort_by)),
|
||||
Helper::Multiple(methods) => {
|
||||
if methods.is_empty() {
|
||||
Ok(Self::Single(SortBy::None))
|
||||
} else {
|
||||
Ok(Self::Multiple(methods))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SortBy> for SortByMulti {
|
||||
fn from(sort_by: SortBy) -> Self {
|
||||
Self::Single(sort_by)
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for SortByMulti {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Single(sort_by) => sort_by.fmt(f),
|
||||
Self::Multiple(methods) => {
|
||||
let methods_str: Vec<String> = methods.iter().map(|m| m.to_string()).collect();
|
||||
write!(f, "[{}]", methods_str.join(", "))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue