project_panel: Fix mixed sort with incorrect ordering when same file and dir name (#47863)

Luis created

Closes #47678

When using mixed sort mode in the project panel, a folder and file with
the same name but different case (e.g., `hello` folder and `Hello.txt`
file) would sort incorrectly. The file could appear between an expanded
folder and its contents.

The issue was in `compare_rel_paths_mixed`: the tie-breaker logic used
case-sensitive comparison (`a == b`) to decide directory-before-file
ordering, but `natural_sort_no_tiebreak` already considers entries equal
case-insensitively. Changed to use `eq_ignore_ascii_case` to match.

Release Notes:

- Fixed project panel mixed sort mode ordering incorrectly when a file
and folder share the same name with different casing.

Change summary

crates/util/src/paths.rs | 35 ++++++++++++++++++++++++++++++++---
1 file changed, 32 insertions(+), 3 deletions(-)

Detailed changes

crates/util/src/paths.rs 🔗

@@ -1174,8 +1174,8 @@ pub fn compare_rel_paths_mixed(
                 let ordering = match (a_key, b_key) {
                     (Some(a), Some(b)) => natural_sort_no_tiebreak(a, b)
                         .then_with(|| match (a_leaf_file, b_leaf_file) {
-                            (true, false) if a == b => Ordering::Greater,
-                            (false, true) if a == b => Ordering::Less,
+                            (true, false) if a.eq_ignore_ascii_case(b) => Ordering::Greater,
+                            (false, true) if a.eq_ignore_ascii_case(b) => Ordering::Less,
                             _ => Ordering::Equal,
                         })
                         .then_with(|| {
@@ -1816,6 +1816,35 @@ mod tests {
         );
     }
 
+    #[perf]
+    fn compare_rel_paths_mixed_same_name_different_case_file_and_dir() {
+        let mut paths = vec![
+            (RelPath::unix("Hello.txt").unwrap(), true),
+            (RelPath::unix("hello").unwrap(), false),
+        ];
+        paths.sort_by(|&a, &b| compare_rel_paths_mixed(a, b));
+        assert_eq!(
+            paths,
+            vec![
+                (RelPath::unix("hello").unwrap(), false),
+                (RelPath::unix("Hello.txt").unwrap(), true),
+            ]
+        );
+
+        let mut paths = vec![
+            (RelPath::unix("hello").unwrap(), false),
+            (RelPath::unix("Hello.txt").unwrap(), true),
+        ];
+        paths.sort_by(|&a, &b| compare_rel_paths_mixed(a, b));
+        assert_eq!(
+            paths,
+            vec![
+                (RelPath::unix("hello").unwrap(), false),
+                (RelPath::unix("Hello.txt").unwrap(), true),
+            ]
+        );
+    }
+
     #[perf]
     fn compare_rel_paths_mixed_with_nested_paths() {
         // Test that nested paths still work correctly
@@ -1951,8 +1980,8 @@ mod tests {
         assert_eq!(
             paths,
             vec![
-                (RelPath::unix("A/B.txt").unwrap(), true),
                 (RelPath::unix("a/b/c.txt").unwrap(), true),
+                (RelPath::unix("A/B.txt").unwrap(), true),
                 (RelPath::unix("a.txt").unwrap(), true),
                 (RelPath::unix("A.txt").unwrap(), true),
             ]