Make alphabetical sorting the default (#32315)
Follow up of this pr: #25148 Release Notes: - Improved file sorting. As described in #20126, I was fed up with lexicographical file sorting in the project panel. The current sorting behavior doesn't handle numeric segments properly, leading to unintuitive ordering like `file_1.rs`, `file_10.rs`, `file_2.rs`. ## Example Sorting Results Using `lexicographical` (default): ``` . ├── file_01.rs ├── file_1.rs ├── file_10.rs ├── file_1025.rs ├── file_2.rs ``` Using alphabetical (natural) sorting: ``` . ├── file_1.rs ├── file_01.rs ├── file_2.rs ├── file_10.rs ├── file_1025.rs ```
This commit is contained in:
parent
293992f5b1
commit
e67b2da20c
2 changed files with 520 additions and 35 deletions
|
@ -740,9 +740,9 @@ async fn test_editing_files(cx: &mut gpui::TestAppContext) {
|
||||||
" > .git",
|
" > .git",
|
||||||
" > a",
|
" > a",
|
||||||
" v b",
|
" v b",
|
||||||
|
" > [EDITOR: ''] <== selected",
|
||||||
" > 3",
|
" > 3",
|
||||||
" > 4",
|
" > 4",
|
||||||
" > [EDITOR: ''] <== selected",
|
|
||||||
" a-different-filename.tar.gz",
|
" a-different-filename.tar.gz",
|
||||||
" > C",
|
" > C",
|
||||||
" .dockerignore",
|
" .dockerignore",
|
||||||
|
@ -765,10 +765,10 @@ async fn test_editing_files(cx: &mut gpui::TestAppContext) {
|
||||||
" > .git",
|
" > .git",
|
||||||
" > a",
|
" > a",
|
||||||
" v b",
|
" v b",
|
||||||
" > 3",
|
|
||||||
" > 4",
|
|
||||||
" > [PROCESSING: 'new-dir']",
|
" > [PROCESSING: 'new-dir']",
|
||||||
" a-different-filename.tar.gz <== selected",
|
" > 3 <== selected",
|
||||||
|
" > 4",
|
||||||
|
" a-different-filename.tar.gz",
|
||||||
" > C",
|
" > C",
|
||||||
" .dockerignore",
|
" .dockerignore",
|
||||||
]
|
]
|
||||||
|
@ -782,10 +782,10 @@ async fn test_editing_files(cx: &mut gpui::TestAppContext) {
|
||||||
" > .git",
|
" > .git",
|
||||||
" > a",
|
" > a",
|
||||||
" v b",
|
" v b",
|
||||||
" > 3",
|
" > 3 <== selected",
|
||||||
" > 4",
|
" > 4",
|
||||||
" > new-dir",
|
" > new-dir",
|
||||||
" a-different-filename.tar.gz <== selected",
|
" a-different-filename.tar.gz",
|
||||||
" > C",
|
" > C",
|
||||||
" .dockerignore",
|
" .dockerignore",
|
||||||
]
|
]
|
||||||
|
@ -801,10 +801,10 @@ async fn test_editing_files(cx: &mut gpui::TestAppContext) {
|
||||||
" > .git",
|
" > .git",
|
||||||
" > a",
|
" > a",
|
||||||
" v b",
|
" v b",
|
||||||
" > 3",
|
" > [EDITOR: '3'] <== selected",
|
||||||
" > 4",
|
" > 4",
|
||||||
" > new-dir",
|
" > new-dir",
|
||||||
" [EDITOR: 'a-different-filename.tar.gz'] <== selected",
|
" a-different-filename.tar.gz",
|
||||||
" > C",
|
" > C",
|
||||||
" .dockerignore",
|
" .dockerignore",
|
||||||
]
|
]
|
||||||
|
@ -819,10 +819,10 @@ async fn test_editing_files(cx: &mut gpui::TestAppContext) {
|
||||||
" > .git",
|
" > .git",
|
||||||
" > a",
|
" > a",
|
||||||
" v b",
|
" v b",
|
||||||
" > 3",
|
" > 3 <== selected",
|
||||||
" > 4",
|
" > 4",
|
||||||
" > new-dir",
|
" > new-dir",
|
||||||
" a-different-filename.tar.gz <== selected",
|
" a-different-filename.tar.gz",
|
||||||
" > C",
|
" > C",
|
||||||
" .dockerignore",
|
" .dockerignore",
|
||||||
]
|
]
|
||||||
|
@ -837,12 +837,12 @@ async fn test_editing_files(cx: &mut gpui::TestAppContext) {
|
||||||
" > .git",
|
" > .git",
|
||||||
" > a",
|
" > a",
|
||||||
" v b",
|
" v b",
|
||||||
" > 3",
|
" v 3",
|
||||||
|
" [EDITOR: ''] <== selected",
|
||||||
|
" Q",
|
||||||
" > 4",
|
" > 4",
|
||||||
" > new-dir",
|
" > new-dir",
|
||||||
" [EDITOR: ''] <== selected",
|
|
||||||
" a-different-filename.tar.gz",
|
" a-different-filename.tar.gz",
|
||||||
" > C",
|
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
panel.update_in(cx, |panel, window, cx| {
|
panel.update_in(cx, |panel, window, cx| {
|
||||||
|
@ -863,12 +863,12 @@ async fn test_editing_files(cx: &mut gpui::TestAppContext) {
|
||||||
" > .git",
|
" > .git",
|
||||||
" > a",
|
" > a",
|
||||||
" v b",
|
" v b",
|
||||||
" > 3",
|
" v 3 <== selected",
|
||||||
|
" Q",
|
||||||
" > 4",
|
" > 4",
|
||||||
" > new-dir",
|
" > new-dir",
|
||||||
" a-different-filename.tar.gz <== selected",
|
" a-different-filename.tar.gz",
|
||||||
" > C",
|
" > C",
|
||||||
" .dockerignore",
|
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,7 @@
|
||||||
use std::cmp;
|
use globset::{Glob, GlobSet, GlobSetBuilder};
|
||||||
|
use regex::Regex;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::cmp::Ordering;
|
||||||
use std::path::StripPrefixError;
|
use std::path::StripPrefixError;
|
||||||
use std::sync::{Arc, OnceLock};
|
use std::sync::{Arc, OnceLock};
|
||||||
use std::{
|
use std::{
|
||||||
|
@ -7,12 +10,6 @@ use std::{
|
||||||
sync::LazyLock,
|
sync::LazyLock,
|
||||||
};
|
};
|
||||||
|
|
||||||
use globset::{Glob, GlobSet, GlobSetBuilder};
|
|
||||||
use regex::Regex;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
use crate::NumericPrefixWithSuffix;
|
|
||||||
|
|
||||||
/// Returns the path to the user's home directory.
|
/// Returns the path to the user's home directory.
|
||||||
pub fn home_dir() -> &'static PathBuf {
|
pub fn home_dir() -> &'static PathBuf {
|
||||||
static HOME_DIR: OnceLock<PathBuf> = OnceLock::new();
|
static HOME_DIR: OnceLock<PathBuf> = OnceLock::new();
|
||||||
|
@ -545,17 +542,172 @@ impl PathMatcher {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Custom character comparison that prioritizes lowercase for same letters
|
||||||
|
fn compare_chars(a: char, b: char) -> Ordering {
|
||||||
|
// First compare case-insensitive
|
||||||
|
match a.to_ascii_lowercase().cmp(&b.to_ascii_lowercase()) {
|
||||||
|
Ordering::Equal => {
|
||||||
|
// If same letter, prioritize lowercase (lowercase < uppercase)
|
||||||
|
match (a.is_ascii_lowercase(), b.is_ascii_lowercase()) {
|
||||||
|
(true, false) => Ordering::Less, // lowercase comes first
|
||||||
|
(false, true) => Ordering::Greater, // uppercase comes after
|
||||||
|
_ => Ordering::Equal, // both same case or both non-ascii
|
||||||
|
}
|
||||||
|
}
|
||||||
|
other => other,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Compares two sequences of consecutive digits for natural sorting.
|
||||||
|
///
|
||||||
|
/// This function is a core component of natural sorting that handles numeric comparison
|
||||||
|
/// in a way that feels natural to humans. It extracts and compares consecutive digit
|
||||||
|
/// sequences from two iterators, handling various cases like leading zeros and very large numbers.
|
||||||
|
///
|
||||||
|
/// # Behavior
|
||||||
|
///
|
||||||
|
/// The function implements the following comparison rules:
|
||||||
|
/// 1. Different numeric values: Compares by actual numeric value (e.g., "2" < "10")
|
||||||
|
/// 2. Leading zeros: When values are equal, longer sequence wins (e.g., "002" > "2")
|
||||||
|
/// 3. Large numbers: Falls back to string comparison for numbers that would overflow u128
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// ```text
|
||||||
|
/// "1" vs "2" -> Less (different values)
|
||||||
|
/// "2" vs "10" -> Less (numeric comparison)
|
||||||
|
/// "002" vs "2" -> Greater (leading zeros)
|
||||||
|
/// "10" vs "010" -> Less (leading zeros)
|
||||||
|
/// "999..." vs "1000..." -> Less (large number comparison)
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// # Implementation Details
|
||||||
|
///
|
||||||
|
/// 1. Extracts consecutive digits into strings
|
||||||
|
/// 2. Compares sequence lengths for leading zero handling
|
||||||
|
/// 3. For equal lengths, compares digit by digit
|
||||||
|
/// 4. For different lengths:
|
||||||
|
/// - Attempts numeric comparison first (for numbers up to 2^128 - 1)
|
||||||
|
/// - Falls back to string comparison if numbers would overflow
|
||||||
|
///
|
||||||
|
/// The function advances both iterators past their respective numeric sequences,
|
||||||
|
/// regardless of the comparison result.
|
||||||
|
fn compare_numeric_segments<I>(
|
||||||
|
a_iter: &mut std::iter::Peekable<I>,
|
||||||
|
b_iter: &mut std::iter::Peekable<I>,
|
||||||
|
) -> Ordering
|
||||||
|
where
|
||||||
|
I: Iterator<Item = char>,
|
||||||
|
{
|
||||||
|
// Collect all consecutive digits into strings
|
||||||
|
let mut a_num_str = String::new();
|
||||||
|
let mut b_num_str = String::new();
|
||||||
|
|
||||||
|
while let Some(&c) = a_iter.peek() {
|
||||||
|
if !c.is_ascii_digit() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
a_num_str.push(c);
|
||||||
|
a_iter.next();
|
||||||
|
}
|
||||||
|
|
||||||
|
while let Some(&c) = b_iter.peek() {
|
||||||
|
if !c.is_ascii_digit() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
b_num_str.push(c);
|
||||||
|
b_iter.next();
|
||||||
|
}
|
||||||
|
|
||||||
|
// First compare lengths (handle leading zeros)
|
||||||
|
match a_num_str.len().cmp(&b_num_str.len()) {
|
||||||
|
Ordering::Equal => {
|
||||||
|
// Same length, compare digit by digit
|
||||||
|
match a_num_str.cmp(&b_num_str) {
|
||||||
|
Ordering::Equal => Ordering::Equal,
|
||||||
|
ordering => ordering,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Different lengths but same value means leading zeros
|
||||||
|
ordering => {
|
||||||
|
// Try parsing as numbers first
|
||||||
|
if let (Ok(a_val), Ok(b_val)) = (a_num_str.parse::<u128>(), b_num_str.parse::<u128>()) {
|
||||||
|
match a_val.cmp(&b_val) {
|
||||||
|
Ordering::Equal => ordering, // Same value, longer one is greater (leading zeros)
|
||||||
|
ord => ord,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// If parsing fails (overflow), compare as strings
|
||||||
|
a_num_str.cmp(&b_num_str)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Performs natural sorting comparison between two strings.
|
||||||
|
///
|
||||||
|
/// Natural sorting is an ordering that handles numeric sequences in a way that matches human expectations.
|
||||||
|
/// For example, "file2" comes before "file10" (unlike standard lexicographic sorting).
|
||||||
|
///
|
||||||
|
/// # Characteristics
|
||||||
|
///
|
||||||
|
/// * Case-sensitive with lowercase priority: When comparing same letters, lowercase comes before uppercase
|
||||||
|
/// * Numbers are compared by numeric value, not character by character
|
||||||
|
/// * Leading zeros affect ordering when numeric values are equal
|
||||||
|
/// * Can handle numbers larger than u128::MAX (falls back to string comparison)
|
||||||
|
///
|
||||||
|
/// # Algorithm
|
||||||
|
///
|
||||||
|
/// The function works by:
|
||||||
|
/// 1. Processing strings character by character
|
||||||
|
/// 2. When encountering digits, treating consecutive digits as a single number
|
||||||
|
/// 3. Comparing numbers by their numeric value rather than lexicographically
|
||||||
|
/// 4. For non-numeric characters, using case-sensitive comparison with lowercase priority
|
||||||
|
fn natural_sort(a: &str, b: &str) -> Ordering {
|
||||||
|
let mut a_iter = a.chars().peekable();
|
||||||
|
let mut b_iter = b.chars().peekable();
|
||||||
|
|
||||||
|
loop {
|
||||||
|
match (a_iter.peek(), b_iter.peek()) {
|
||||||
|
(None, None) => return Ordering::Equal,
|
||||||
|
(None, _) => return Ordering::Less,
|
||||||
|
(_, None) => return Ordering::Greater,
|
||||||
|
(Some(&a_char), Some(&b_char)) => {
|
||||||
|
if a_char.is_ascii_digit() && b_char.is_ascii_digit() {
|
||||||
|
match compare_numeric_segments(&mut a_iter, &mut b_iter) {
|
||||||
|
Ordering::Equal => continue,
|
||||||
|
ordering => return ordering,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
match compare_chars(a_char, b_char) {
|
||||||
|
Ordering::Equal => {
|
||||||
|
a_iter.next();
|
||||||
|
b_iter.next();
|
||||||
|
}
|
||||||
|
ordering => return ordering,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn compare_paths(
|
pub fn compare_paths(
|
||||||
(path_a, a_is_file): (&Path, bool),
|
(path_a, a_is_file): (&Path, bool),
|
||||||
(path_b, b_is_file): (&Path, bool),
|
(path_b, b_is_file): (&Path, bool),
|
||||||
) -> cmp::Ordering {
|
) -> Ordering {
|
||||||
let mut components_a = path_a.components().peekable();
|
let mut components_a = path_a.components().peekable();
|
||||||
let mut components_b = path_b.components().peekable();
|
let mut components_b = path_b.components().peekable();
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
match (components_a.next(), components_b.next()) {
|
match (components_a.next(), components_b.next()) {
|
||||||
(Some(component_a), Some(component_b)) => {
|
(Some(component_a), Some(component_b)) => {
|
||||||
let a_is_file = components_a.peek().is_none() && a_is_file;
|
let a_is_file = components_a.peek().is_none() && a_is_file;
|
||||||
let b_is_file = components_b.peek().is_none() && b_is_file;
|
let b_is_file = components_b.peek().is_none() && b_is_file;
|
||||||
|
|
||||||
let ordering = a_is_file.cmp(&b_is_file).then_with(|| {
|
let ordering = a_is_file.cmp(&b_is_file).then_with(|| {
|
||||||
let path_a = Path::new(component_a.as_os_str());
|
let path_a = Path::new(component_a.as_os_str());
|
||||||
let path_string_a = if a_is_file {
|
let path_string_a = if a_is_file {
|
||||||
|
@ -564,9 +716,6 @@ pub fn compare_paths(
|
||||||
path_a.file_name()
|
path_a.file_name()
|
||||||
}
|
}
|
||||||
.map(|s| s.to_string_lossy());
|
.map(|s| s.to_string_lossy());
|
||||||
let num_and_remainder_a = path_string_a
|
|
||||||
.as_deref()
|
|
||||||
.map(NumericPrefixWithSuffix::from_numeric_prefixed_str);
|
|
||||||
|
|
||||||
let path_b = Path::new(component_b.as_os_str());
|
let path_b = Path::new(component_b.as_os_str());
|
||||||
let path_string_b = if b_is_file {
|
let path_string_b = if b_is_file {
|
||||||
|
@ -575,27 +724,32 @@ pub fn compare_paths(
|
||||||
path_b.file_name()
|
path_b.file_name()
|
||||||
}
|
}
|
||||||
.map(|s| s.to_string_lossy());
|
.map(|s| s.to_string_lossy());
|
||||||
let num_and_remainder_b = path_string_b
|
|
||||||
.as_deref()
|
|
||||||
.map(NumericPrefixWithSuffix::from_numeric_prefixed_str);
|
|
||||||
|
|
||||||
num_and_remainder_a.cmp(&num_and_remainder_b).then_with(|| {
|
let compare_components = match (path_string_a, path_string_b) {
|
||||||
|
(Some(a), Some(b)) => natural_sort(&a, &b),
|
||||||
|
(Some(_), None) => Ordering::Greater,
|
||||||
|
(None, Some(_)) => Ordering::Less,
|
||||||
|
(None, None) => Ordering::Equal,
|
||||||
|
};
|
||||||
|
|
||||||
|
compare_components.then_with(|| {
|
||||||
if a_is_file && b_is_file {
|
if a_is_file && b_is_file {
|
||||||
let ext_a = path_a.extension().unwrap_or_default();
|
let ext_a = path_a.extension().unwrap_or_default();
|
||||||
let ext_b = path_b.extension().unwrap_or_default();
|
let ext_b = path_b.extension().unwrap_or_default();
|
||||||
ext_a.cmp(ext_b)
|
ext_a.cmp(ext_b)
|
||||||
} else {
|
} else {
|
||||||
cmp::Ordering::Equal
|
Ordering::Equal
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
if !ordering.is_eq() {
|
if !ordering.is_eq() {
|
||||||
return ordering;
|
return ordering;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
(Some(_), None) => break cmp::Ordering::Greater,
|
(Some(_), None) => break Ordering::Greater,
|
||||||
(None, Some(_)) => break cmp::Ordering::Less,
|
(None, Some(_)) => break Ordering::Less,
|
||||||
(None, None) => break cmp::Ordering::Equal,
|
(None, None) => break Ordering::Equal,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1049,4 +1203,335 @@ mod tests {
|
||||||
"C:\\Users\\someone\\test_file.rs"
|
"C:\\Users\\someone\\test_file.rs"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_compare_numeric_segments() {
|
||||||
|
// Helper function to create peekable iterators and test
|
||||||
|
fn compare(a: &str, b: &str) -> Ordering {
|
||||||
|
let mut a_iter = a.chars().peekable();
|
||||||
|
let mut b_iter = b.chars().peekable();
|
||||||
|
|
||||||
|
let result = compare_numeric_segments(&mut a_iter, &mut b_iter);
|
||||||
|
|
||||||
|
// Verify iterators advanced correctly
|
||||||
|
assert!(
|
||||||
|
!a_iter.next().map_or(false, |c| c.is_ascii_digit()),
|
||||||
|
"Iterator a should have consumed all digits"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
!b_iter.next().map_or(false, |c| c.is_ascii_digit()),
|
||||||
|
"Iterator b should have consumed all digits"
|
||||||
|
);
|
||||||
|
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
// Basic numeric comparisons
|
||||||
|
assert_eq!(compare("0", "0"), Ordering::Equal);
|
||||||
|
assert_eq!(compare("1", "2"), Ordering::Less);
|
||||||
|
assert_eq!(compare("9", "10"), Ordering::Less);
|
||||||
|
assert_eq!(compare("10", "9"), Ordering::Greater);
|
||||||
|
assert_eq!(compare("99", "100"), Ordering::Less);
|
||||||
|
|
||||||
|
// Leading zeros
|
||||||
|
assert_eq!(compare("0", "00"), Ordering::Less);
|
||||||
|
assert_eq!(compare("00", "0"), Ordering::Greater);
|
||||||
|
assert_eq!(compare("01", "1"), Ordering::Greater);
|
||||||
|
assert_eq!(compare("001", "1"), Ordering::Greater);
|
||||||
|
assert_eq!(compare("001", "01"), Ordering::Greater);
|
||||||
|
|
||||||
|
// Same value different representation
|
||||||
|
assert_eq!(compare("000100", "100"), Ordering::Greater);
|
||||||
|
assert_eq!(compare("100", "0100"), Ordering::Less);
|
||||||
|
assert_eq!(compare("0100", "00100"), Ordering::Less);
|
||||||
|
|
||||||
|
// Large numbers
|
||||||
|
assert_eq!(compare("9999999999", "10000000000"), Ordering::Less);
|
||||||
|
assert_eq!(
|
||||||
|
compare(
|
||||||
|
"340282366920938463463374607431768211455", // u128::MAX
|
||||||
|
"340282366920938463463374607431768211456"
|
||||||
|
),
|
||||||
|
Ordering::Less
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
compare(
|
||||||
|
"340282366920938463463374607431768211456", // > u128::MAX
|
||||||
|
"340282366920938463463374607431768211455"
|
||||||
|
),
|
||||||
|
Ordering::Greater
|
||||||
|
);
|
||||||
|
|
||||||
|
// Iterator advancement verification
|
||||||
|
let mut a_iter = "123abc".chars().peekable();
|
||||||
|
let mut b_iter = "456def".chars().peekable();
|
||||||
|
|
||||||
|
compare_numeric_segments(&mut a_iter, &mut b_iter);
|
||||||
|
|
||||||
|
assert_eq!(a_iter.collect::<String>(), "abc");
|
||||||
|
assert_eq!(b_iter.collect::<String>(), "def");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_natural_sort() {
|
||||||
|
// Basic alphanumeric
|
||||||
|
assert_eq!(natural_sort("a", "b"), Ordering::Less);
|
||||||
|
assert_eq!(natural_sort("b", "a"), Ordering::Greater);
|
||||||
|
assert_eq!(natural_sort("a", "a"), Ordering::Equal);
|
||||||
|
|
||||||
|
// Case sensitivity
|
||||||
|
assert_eq!(natural_sort("a", "A"), Ordering::Less);
|
||||||
|
assert_eq!(natural_sort("A", "a"), Ordering::Greater);
|
||||||
|
assert_eq!(natural_sort("aA", "aa"), Ordering::Greater);
|
||||||
|
assert_eq!(natural_sort("aa", "aA"), Ordering::Less);
|
||||||
|
|
||||||
|
// Numbers
|
||||||
|
assert_eq!(natural_sort("1", "2"), Ordering::Less);
|
||||||
|
assert_eq!(natural_sort("2", "10"), Ordering::Less);
|
||||||
|
assert_eq!(natural_sort("02", "10"), Ordering::Less);
|
||||||
|
assert_eq!(natural_sort("02", "2"), Ordering::Greater);
|
||||||
|
|
||||||
|
// Mixed alphanumeric
|
||||||
|
assert_eq!(natural_sort("a1", "a2"), Ordering::Less);
|
||||||
|
assert_eq!(natural_sort("a2", "a10"), Ordering::Less);
|
||||||
|
assert_eq!(natural_sort("a02", "a2"), Ordering::Greater);
|
||||||
|
assert_eq!(natural_sort("a1b", "a1c"), Ordering::Less);
|
||||||
|
|
||||||
|
// Multiple numeric segments
|
||||||
|
assert_eq!(natural_sort("1a2", "1a10"), Ordering::Less);
|
||||||
|
assert_eq!(natural_sort("1a10", "1a2"), Ordering::Greater);
|
||||||
|
assert_eq!(natural_sort("2a1", "10a1"), Ordering::Less);
|
||||||
|
|
||||||
|
// Special characters
|
||||||
|
assert_eq!(natural_sort("a-1", "a-2"), Ordering::Less);
|
||||||
|
assert_eq!(natural_sort("a_1", "a_2"), Ordering::Less);
|
||||||
|
assert_eq!(natural_sort("a.1", "a.2"), Ordering::Less);
|
||||||
|
|
||||||
|
// Unicode
|
||||||
|
assert_eq!(natural_sort("文1", "文2"), Ordering::Less);
|
||||||
|
assert_eq!(natural_sort("文2", "文10"), Ordering::Less);
|
||||||
|
assert_eq!(natural_sort("🔤1", "🔤2"), Ordering::Less);
|
||||||
|
|
||||||
|
// Empty and special cases
|
||||||
|
assert_eq!(natural_sort("", ""), Ordering::Equal);
|
||||||
|
assert_eq!(natural_sort("", "a"), Ordering::Less);
|
||||||
|
assert_eq!(natural_sort("a", ""), Ordering::Greater);
|
||||||
|
assert_eq!(natural_sort(" ", " "), Ordering::Less);
|
||||||
|
|
||||||
|
// Mixed everything
|
||||||
|
assert_eq!(natural_sort("File-1.txt", "File-2.txt"), Ordering::Less);
|
||||||
|
assert_eq!(natural_sort("File-02.txt", "File-2.txt"), Ordering::Greater);
|
||||||
|
assert_eq!(natural_sort("File-2.txt", "File-10.txt"), Ordering::Less);
|
||||||
|
assert_eq!(natural_sort("File_A1", "File_A2"), Ordering::Less);
|
||||||
|
assert_eq!(natural_sort("File_a1", "File_A1"), Ordering::Less);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_compare_paths() {
|
||||||
|
// Helper function for cleaner tests
|
||||||
|
fn compare(a: &str, is_a_file: bool, b: &str, is_b_file: bool) -> Ordering {
|
||||||
|
compare_paths((Path::new(a), is_a_file), (Path::new(b), is_b_file))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Basic path comparison
|
||||||
|
assert_eq!(compare("a", true, "b", true), Ordering::Less);
|
||||||
|
assert_eq!(compare("b", true, "a", true), Ordering::Greater);
|
||||||
|
assert_eq!(compare("a", true, "a", true), Ordering::Equal);
|
||||||
|
|
||||||
|
// Files vs Directories
|
||||||
|
assert_eq!(compare("a", true, "a", false), Ordering::Greater);
|
||||||
|
assert_eq!(compare("a", false, "a", true), Ordering::Less);
|
||||||
|
assert_eq!(compare("b", false, "a", true), Ordering::Less);
|
||||||
|
|
||||||
|
// Extensions
|
||||||
|
assert_eq!(compare("a.txt", true, "a.md", true), Ordering::Greater);
|
||||||
|
assert_eq!(compare("a.md", true, "a.txt", true), Ordering::Less);
|
||||||
|
assert_eq!(compare("a", true, "a.txt", true), Ordering::Less);
|
||||||
|
|
||||||
|
// Nested paths
|
||||||
|
assert_eq!(compare("dir/a", true, "dir/b", true), Ordering::Less);
|
||||||
|
assert_eq!(compare("dir1/a", true, "dir2/a", true), Ordering::Less);
|
||||||
|
assert_eq!(compare("dir/sub/a", true, "dir/a", true), Ordering::Less);
|
||||||
|
|
||||||
|
// Case sensitivity in paths
|
||||||
|
assert_eq!(
|
||||||
|
compare("Dir/file", true, "dir/file", true),
|
||||||
|
Ordering::Greater
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
compare("dir/File", true, "dir/file", true),
|
||||||
|
Ordering::Greater
|
||||||
|
);
|
||||||
|
assert_eq!(compare("dir/file", true, "Dir/File", true), Ordering::Less);
|
||||||
|
|
||||||
|
// Hidden files and special names
|
||||||
|
assert_eq!(compare(".hidden", true, "visible", true), Ordering::Less);
|
||||||
|
assert_eq!(compare("_special", true, "normal", true), Ordering::Less);
|
||||||
|
assert_eq!(compare(".config", false, ".data", false), Ordering::Less);
|
||||||
|
|
||||||
|
// Mixed numeric paths
|
||||||
|
assert_eq!(
|
||||||
|
compare("dir1/file", true, "dir2/file", true),
|
||||||
|
Ordering::Less
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
compare("dir2/file", true, "dir10/file", true),
|
||||||
|
Ordering::Less
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
compare("dir02/file", true, "dir2/file", true),
|
||||||
|
Ordering::Greater
|
||||||
|
);
|
||||||
|
|
||||||
|
// Root paths
|
||||||
|
assert_eq!(compare("/a", true, "/b", true), Ordering::Less);
|
||||||
|
assert_eq!(compare("/", false, "/a", true), Ordering::Less);
|
||||||
|
|
||||||
|
// Complex real-world examples
|
||||||
|
assert_eq!(
|
||||||
|
compare("project/src/main.rs", true, "project/src/lib.rs", true),
|
||||||
|
Ordering::Greater
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
compare(
|
||||||
|
"project/tests/test_1.rs",
|
||||||
|
true,
|
||||||
|
"project/tests/test_2.rs",
|
||||||
|
true
|
||||||
|
),
|
||||||
|
Ordering::Less
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
compare(
|
||||||
|
"project/v1.0.0/README.md",
|
||||||
|
true,
|
||||||
|
"project/v1.10.0/README.md",
|
||||||
|
true
|
||||||
|
),
|
||||||
|
Ordering::Less
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_natural_sort_case_sensitivity() {
|
||||||
|
// Same letter different case - lowercase should come first
|
||||||
|
assert_eq!(natural_sort("a", "A"), Ordering::Less);
|
||||||
|
assert_eq!(natural_sort("A", "a"), Ordering::Greater);
|
||||||
|
assert_eq!(natural_sort("a", "a"), Ordering::Equal);
|
||||||
|
assert_eq!(natural_sort("A", "A"), Ordering::Equal);
|
||||||
|
|
||||||
|
// Mixed case strings
|
||||||
|
assert_eq!(natural_sort("aaa", "AAA"), Ordering::Less);
|
||||||
|
assert_eq!(natural_sort("AAA", "aaa"), Ordering::Greater);
|
||||||
|
assert_eq!(natural_sort("aAa", "AaA"), Ordering::Less);
|
||||||
|
|
||||||
|
// Different letters
|
||||||
|
assert_eq!(natural_sort("a", "b"), Ordering::Less);
|
||||||
|
assert_eq!(natural_sort("A", "b"), Ordering::Less);
|
||||||
|
assert_eq!(natural_sort("a", "B"), Ordering::Less);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_natural_sort_with_numbers() {
|
||||||
|
// Basic number ordering
|
||||||
|
assert_eq!(natural_sort("file1", "file2"), Ordering::Less);
|
||||||
|
assert_eq!(natural_sort("file2", "file10"), Ordering::Less);
|
||||||
|
assert_eq!(natural_sort("file10", "file2"), Ordering::Greater);
|
||||||
|
|
||||||
|
// Numbers in different positions
|
||||||
|
assert_eq!(natural_sort("1file", "2file"), Ordering::Less);
|
||||||
|
assert_eq!(natural_sort("file1text", "file2text"), Ordering::Less);
|
||||||
|
assert_eq!(natural_sort("text1file", "text2file"), Ordering::Less);
|
||||||
|
|
||||||
|
// Multiple numbers in string
|
||||||
|
assert_eq!(natural_sort("file1-2", "file1-10"), Ordering::Less);
|
||||||
|
assert_eq!(natural_sort("2-1file", "10-1file"), Ordering::Less);
|
||||||
|
|
||||||
|
// Leading zeros
|
||||||
|
assert_eq!(natural_sort("file002", "file2"), Ordering::Greater);
|
||||||
|
assert_eq!(natural_sort("file002", "file10"), Ordering::Less);
|
||||||
|
|
||||||
|
// Very large numbers
|
||||||
|
assert_eq!(
|
||||||
|
natural_sort("file999999999999999999999", "file999999999999999999998"),
|
||||||
|
Ordering::Greater
|
||||||
|
);
|
||||||
|
|
||||||
|
// u128 edge cases
|
||||||
|
|
||||||
|
// Numbers near u128::MAX (340,282,366,920,938,463,463,374,607,431,768,211,455)
|
||||||
|
assert_eq!(
|
||||||
|
natural_sort(
|
||||||
|
"file340282366920938463463374607431768211454",
|
||||||
|
"file340282366920938463463374607431768211455"
|
||||||
|
),
|
||||||
|
Ordering::Less
|
||||||
|
);
|
||||||
|
|
||||||
|
// Equal length numbers that overflow u128
|
||||||
|
assert_eq!(
|
||||||
|
natural_sort(
|
||||||
|
"file340282366920938463463374607431768211456",
|
||||||
|
"file340282366920938463463374607431768211455"
|
||||||
|
),
|
||||||
|
Ordering::Greater
|
||||||
|
);
|
||||||
|
|
||||||
|
// Different length numbers that overflow u128
|
||||||
|
assert_eq!(
|
||||||
|
natural_sort(
|
||||||
|
"file3402823669209384634633746074317682114560",
|
||||||
|
"file340282366920938463463374607431768211455"
|
||||||
|
),
|
||||||
|
Ordering::Greater
|
||||||
|
);
|
||||||
|
|
||||||
|
// Leading zeros with numbers near u128::MAX
|
||||||
|
assert_eq!(
|
||||||
|
natural_sort(
|
||||||
|
"file0340282366920938463463374607431768211455",
|
||||||
|
"file340282366920938463463374607431768211455"
|
||||||
|
),
|
||||||
|
Ordering::Greater
|
||||||
|
);
|
||||||
|
|
||||||
|
// Very large numbers with different lengths (both overflow u128)
|
||||||
|
assert_eq!(
|
||||||
|
natural_sort(
|
||||||
|
"file999999999999999999999999999999999999999999999999",
|
||||||
|
"file9999999999999999999999999999999999999999999999999"
|
||||||
|
),
|
||||||
|
Ordering::Less
|
||||||
|
);
|
||||||
|
|
||||||
|
// Mixed case with numbers
|
||||||
|
assert_eq!(natural_sort("File1", "file2"), Ordering::Greater);
|
||||||
|
assert_eq!(natural_sort("file1", "File2"), Ordering::Less);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_natural_sort_edge_cases() {
|
||||||
|
// Empty strings
|
||||||
|
assert_eq!(natural_sort("", ""), Ordering::Equal);
|
||||||
|
assert_eq!(natural_sort("", "a"), Ordering::Less);
|
||||||
|
assert_eq!(natural_sort("a", ""), Ordering::Greater);
|
||||||
|
|
||||||
|
// Special characters
|
||||||
|
assert_eq!(natural_sort("file-1", "file_1"), Ordering::Less);
|
||||||
|
assert_eq!(natural_sort("file.1", "file_1"), Ordering::Less);
|
||||||
|
assert_eq!(natural_sort("file 1", "file_1"), Ordering::Less);
|
||||||
|
|
||||||
|
// Unicode characters
|
||||||
|
// 9312 vs 9313
|
||||||
|
assert_eq!(natural_sort("file①", "file②"), Ordering::Less);
|
||||||
|
// 9321 vs 9313
|
||||||
|
assert_eq!(natural_sort("file⑩", "file②"), Ordering::Greater);
|
||||||
|
// 28450 vs 23383
|
||||||
|
assert_eq!(natural_sort("file漢", "file字"), Ordering::Greater);
|
||||||
|
|
||||||
|
// Mixed alphanumeric with special chars
|
||||||
|
assert_eq!(natural_sort("file-1a", "file-1b"), Ordering::Less);
|
||||||
|
assert_eq!(natural_sort("file-1.2", "file-1.10"), Ordering::Less);
|
||||||
|
assert_eq!(natural_sort("file-1.10", "file-1.2"), Ordering::Greater);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue