project_panel: Make natural sort ordering consistent with other apps (#41080)
The existing sorting approach when faced with `Dir1`, `dir2`, `Dir3`, would only get as far as comparing the stems without numbers (`dir` and `Dir`), and then the lowercase-first tie breaker in that function would determine that `dir2` should come first, resulting in an undesirable order of `dir2`, `Dir1`, `Dir3`. This patch defers tie-breaking until it's determined that there's no other difference in the strings outside of case to order on, at which point we tie-break to provide a stable sort. Natural number sorting is still preserved, and mixing different cases alphabetically (as opposed to all lowercase alpha, followed by all uppercase alpha) is preserved. Closes #41080 Release Notes: - Fixed: ProjectPanel sorting bug Screenshots: Before | After ----|--- <img width="237" height="325" alt="image" src="https://github.com/user-attachments/assets/6e92e8c0-2172-4a8f-a058-484749da047b" /> | <img width="239" height="325" alt="image" src="https://github.com/user-attachments/assets/874ad29f-7238-4bfc-b89b-fd64f9b8889a" /> I'm having trouble reasoning through what was previously going wrong with `docs` in the before screenshot, but it also seems to now appear alphabetically where you'd expect it with this patch --------- Co-authored-by: Smit Barmase <heysmitbarmase@gmail.com>
This commit is contained in:
co-authored by
Smit Barmase
parent
6b239c3a9a
commit
fd837b348f
+56
-22
@@ -800,22 +800,6 @@ impl Default for 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
|
||||
@@ -916,21 +900,25 @@ where
|
||||
/// * 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)
|
||||
/// * When strings are equal case-insensitively, lowercase is prioritized (lowercase < uppercase)
|
||||
///
|
||||
/// # Algorithm
|
||||
///
|
||||
/// The function works by:
|
||||
/// 1. Processing strings character by character
|
||||
/// 1. Processing strings character by character in a case-insensitive manner
|
||||
/// 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
|
||||
/// 4. For non-numeric characters, using case-insensitive comparison
|
||||
/// 5. If everything is equal case-insensitively, using case-sensitive comparison as final tie-breaker
|
||||
pub 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, None) => {
|
||||
return b.cmp(a);
|
||||
}
|
||||
(None, _) => return Ordering::Less,
|
||||
(_, None) => return Ordering::Greater,
|
||||
(Some(&a_char), Some(&b_char)) => {
|
||||
@@ -940,7 +928,10 @@ pub fn natural_sort(a: &str, b: &str) -> Ordering {
|
||||
ordering => return ordering,
|
||||
}
|
||||
} else {
|
||||
match compare_chars(a_char, b_char) {
|
||||
match a_char
|
||||
.to_ascii_lowercase()
|
||||
.cmp(&b_char.to_ascii_lowercase())
|
||||
{
|
||||
Ordering::Equal => {
|
||||
a_iter.next();
|
||||
b_iter.next();
|
||||
@@ -952,6 +943,7 @@ pub fn natural_sort(a: &str, b: &str) -> Ordering {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn compare_rel_paths(
|
||||
(path_a, a_is_file): (&RelPath, bool),
|
||||
(path_b, b_is_file): (&RelPath, bool),
|
||||
@@ -1246,6 +1238,33 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[perf]
|
||||
fn compare_paths_mixed_case_numeric_ordering() {
|
||||
let mut entries = [
|
||||
(Path::new(".config"), false),
|
||||
(Path::new("Dir1"), false),
|
||||
(Path::new("dir01"), false),
|
||||
(Path::new("dir2"), false),
|
||||
(Path::new("Dir02"), false),
|
||||
(Path::new("dir10"), false),
|
||||
(Path::new("Dir10"), false),
|
||||
];
|
||||
|
||||
entries.sort_by(|&a, &b| compare_paths(a, b));
|
||||
|
||||
let ordered: Vec<&str> = entries
|
||||
.iter()
|
||||
.map(|(path, _)| path.to_str().unwrap())
|
||||
.collect();
|
||||
|
||||
assert_eq!(
|
||||
ordered,
|
||||
vec![
|
||||
".config", "Dir1", "dir01", "dir2", "Dir02", "dir10", "Dir10"
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[perf]
|
||||
fn path_with_position_parse_posix_path() {
|
||||
// Test POSIX filename edge cases
|
||||
@@ -1917,10 +1936,25 @@ mod tests {
|
||||
),
|
||||
Ordering::Less
|
||||
);
|
||||
}
|
||||
|
||||
// Mixed case with numbers
|
||||
assert_eq!(natural_sort("File1", "file2"), Ordering::Greater);
|
||||
#[perf]
|
||||
fn test_natural_sort_case_sensitive() {
|
||||
// Numerically smaller values come first.
|
||||
assert_eq!(natural_sort("File1", "file2"), Ordering::Less);
|
||||
assert_eq!(natural_sort("file1", "File2"), Ordering::Less);
|
||||
|
||||
// Numerically equal values: the case-insensitive comparison decides first.
|
||||
// Case-sensitive comparison only occurs when both are equal case-insensitively.
|
||||
assert_eq!(natural_sort("Dir1", "dir01"), Ordering::Less);
|
||||
assert_eq!(natural_sort("dir2", "Dir02"), Ordering::Less);
|
||||
assert_eq!(natural_sort("dir2", "dir02"), Ordering::Less);
|
||||
|
||||
// Numerically equal and case-insensitively equal:
|
||||
// the lexicographically smaller (case-sensitive) one wins.
|
||||
assert_eq!(natural_sort("dir1", "Dir1"), Ordering::Less);
|
||||
assert_eq!(natural_sort("dir02", "Dir02"), Ordering::Less);
|
||||
assert_eq!(natural_sort("dir10", "Dir10"), Ordering::Less);
|
||||
}
|
||||
|
||||
#[perf]
|
||||
|
||||
Reference in New Issue
Block a user