Make alphabetical sorting the default (#32315)

Tom Planche created

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

Change summary

crates/project_panel/src/project_panel_tests.rs |  32 
crates/util/src/paths.rs                        | 523 ++++++++++++++++++
2 files changed, 520 insertions(+), 35 deletions(-)

Detailed changes

crates/project_panel/src/project_panel_tests.rs πŸ”—

@@ -740,9 +740,9 @@ async fn test_editing_files(cx: &mut gpui::TestAppContext) {
             "    > .git",
             "    > a",
             "    v b",
+            "        > [EDITOR: '']  <== selected",
             "        > 3",
             "        > 4",
-            "        > [EDITOR: '']  <== selected",
             "          a-different-filename.tar.gz",
             "    > C",
             "      .dockerignore",
@@ -765,10 +765,10 @@ async fn test_editing_files(cx: &mut gpui::TestAppContext) {
             "    > .git",
             "    > a",
             "    v b",
-            "        > 3",
-            "        > 4",
             "        > [PROCESSING: 'new-dir']",
-            "          a-different-filename.tar.gz  <== selected",
+            "        > 3  <== selected",
+            "        > 4",
+            "          a-different-filename.tar.gz",
             "    > C",
             "      .dockerignore",
         ]
@@ -782,10 +782,10 @@ async fn test_editing_files(cx: &mut gpui::TestAppContext) {
             "    > .git",
             "    > a",
             "    v b",
-            "        > 3",
+            "        > 3  <== selected",
             "        > 4",
             "        > new-dir",
-            "          a-different-filename.tar.gz  <== selected",
+            "          a-different-filename.tar.gz",
             "    > C",
             "      .dockerignore",
         ]
@@ -801,10 +801,10 @@ async fn test_editing_files(cx: &mut gpui::TestAppContext) {
             "    > .git",
             "    > a",
             "    v b",
-            "        > 3",
+            "        > [EDITOR: '3']  <== selected",
             "        > 4",
             "        > new-dir",
-            "          [EDITOR: 'a-different-filename.tar.gz']  <== selected",
+            "          a-different-filename.tar.gz",
             "    > C",
             "      .dockerignore",
         ]
@@ -819,10 +819,10 @@ async fn test_editing_files(cx: &mut gpui::TestAppContext) {
             "    > .git",
             "    > a",
             "    v b",
-            "        > 3",
+            "        > 3  <== selected",
             "        > 4",
             "        > new-dir",
-            "          a-different-filename.tar.gz  <== selected",
+            "          a-different-filename.tar.gz",
             "    > C",
             "      .dockerignore",
         ]
@@ -837,12 +837,12 @@ async fn test_editing_files(cx: &mut gpui::TestAppContext) {
             "    > .git",
             "    > a",
             "    v b",
-            "        > 3",
+            "        v 3",
+            "              [EDITOR: '']  <== selected",
+            "              Q",
             "        > 4",
             "        > new-dir",
-            "          [EDITOR: '']  <== selected",
             "          a-different-filename.tar.gz",
-            "    > C",
         ]
     );
     panel.update_in(cx, |panel, window, cx| {
@@ -863,12 +863,12 @@ async fn test_editing_files(cx: &mut gpui::TestAppContext) {
             "    > .git",
             "    > a",
             "    v b",
-            "        > 3",
+            "        v 3  <== selected",
+            "              Q",
             "        > 4",
             "        > new-dir",
-            "          a-different-filename.tar.gz  <== selected",
+            "          a-different-filename.tar.gz",
             "    > C",
-            "      .dockerignore",
         ]
     );
 }

crates/util/src/paths.rs πŸ”—

@@ -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::sync::{Arc, OnceLock};
 use std::{
@@ -7,12 +10,6 @@ use std::{
     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.
 pub fn home_dir() -> &'static PathBuf {
     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(
     (path_a, a_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_b = path_b.components().peekable();
+
     loop {
         match (components_a.next(), components_b.next()) {
             (Some(component_a), Some(component_b)) => {
                 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 ordering = a_is_file.cmp(&b_is_file).then_with(|| {
                     let path_a = Path::new(component_a.as_os_str());
                     let path_string_a = if a_is_file {
@@ -564,9 +716,6 @@ pub fn compare_paths(
                         path_a.file_name()
                     }
                     .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_string_b = if b_is_file {
@@ -575,27 +724,32 @@ pub fn compare_paths(
                         path_b.file_name()
                     }
                     .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 {
                             let ext_a = path_a.extension().unwrap_or_default();
                             let ext_b = path_b.extension().unwrap_or_default();
                             ext_a.cmp(ext_b)
                         } else {
-                            cmp::Ordering::Equal
+                            Ordering::Equal
                         }
                     })
                 });
+
                 if !ordering.is_eq() {
                     return ordering;
                 }
             }
-            (Some(_), None) => break cmp::Ordering::Greater,
-            (None, Some(_)) => break cmp::Ordering::Less,
-            (None, None) => break cmp::Ordering::Equal,
+            (Some(_), None) => break Ordering::Greater,
+            (None, Some(_)) => break Ordering::Less,
+            (None, None) => break Ordering::Equal,
         }
     }
 }
@@ -1049,4 +1203,335 @@ mod tests {
             "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);
+    }
 }