@@ -1109,141 +1109,95 @@ fn stem_and_extension(filename: &str) -> (Option<&str>, Option<&str>) {
}
}
-pub fn compare_rel_paths(
- (path_a, a_is_file): (&RelPath, bool),
- (path_b, b_is_file): (&RelPath, bool),
-) -> Ordering {
- let mut components_a = path_a.components();
- let mut components_b = path_b.components();
- loop {
- match (components_a.next(), components_b.next()) {
- (Some(component_a), Some(component_b)) => {
- let a_is_file = a_is_file && components_a.rest().is_empty();
- let b_is_file = b_is_file && components_b.rest().is_empty();
-
- let ordering = a_is_file.cmp(&b_is_file).then_with(|| {
- let (a_stem, a_extension) = a_is_file
- .then(|| stem_and_extension(component_a))
- .unwrap_or_default();
- let path_string_a = if a_is_file { a_stem } else { Some(component_a) };
-
- let (b_stem, b_extension) = b_is_file
- .then(|| stem_and_extension(component_b))
- .unwrap_or_default();
- let path_string_b = if b_is_file { b_stem } else { Some(component_b) };
-
- 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,
- };
+/// Controls the lexicographic sorting of file and folder names.
+#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
+pub enum SortOrder {
+ /// Case-insensitive natural sort with lowercase preferred in ties.
+ /// Numbers in file names are compared by value (e.g., `file2` before `file10`).
+ #[default]
+ Default,
+ /// Uppercase names are grouped before lowercase names, with case-insensitive
+ /// natural sort within each group. Dot-prefixed names sort before both groups.
+ Upper,
+ /// Lowercase names are grouped before uppercase names, with case-insensitive
+ /// natural sort within each group. Dot-prefixed names sort before both groups.
+ Lower,
+ /// Pure Unicode codepoint comparison. No case folding, no natural number sorting.
+ /// Uppercase ASCII sorts before lowercase. Accented characters sort after ASCII.
+ Unicode,
+}
- compare_components.then_with(|| {
- if a_is_file && b_is_file {
- let ext_a = a_extension.unwrap_or_default();
- let ext_b = b_extension.unwrap_or_default();
- ext_a.cmp(ext_b)
- } else {
- Ordering::Equal
- }
- })
- });
+/// Controls how files and directories are ordered relative to each other.
+#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
+pub enum SortMode {
+ /// Directories are listed before files at each level.
+ #[default]
+ DirectoriesFirst,
+ /// Files and directories are interleaved alphabetically.
+ Mixed,
+ /// Files are listed before directories at each level.
+ FilesFirst,
+}
- if !ordering.is_eq() {
- return ordering;
- }
+fn case_group_key(name: &str, order: SortOrder) -> u8 {
+ let first = match name.chars().next() {
+ Some(c) => c,
+ None => return 0,
+ };
+ match order {
+ SortOrder::Upper => {
+ if first.is_lowercase() {
+ 1
+ } else {
+ 0
+ }
+ }
+ SortOrder::Lower => {
+ if first.is_uppercase() {
+ 1
+ } else {
+ 0
}
- (Some(_), None) => break Ordering::Greater,
- (None, Some(_)) => break Ordering::Less,
- (None, None) => break Ordering::Equal,
}
+ _ => 0,
+ }
+}
+
+fn compare_strings(a: &str, b: &str, order: SortOrder) -> Ordering {
+ match order {
+ SortOrder::Unicode => a.cmp(b),
+ _ => natural_sort(a, b),
}
}
-/// Compare two relative paths with mixed files and directories using
-/// case-insensitive natural sorting. For example, "Apple", "aardvark.txt",
-/// and "Zebra" would be sorted as: aardvark.txt, Apple, Zebra
-/// (case-insensitive alphabetical).
-pub fn compare_rel_paths_mixed(
+fn compare_strings_no_tiebreak(a: &str, b: &str, order: SortOrder) -> Ordering {
+ match order {
+ SortOrder::Unicode => a.cmp(b),
+ _ => natural_sort_no_tiebreak(a, b),
+ }
+}
+
+pub fn compare_rel_paths(
(path_a, a_is_file): (&RelPath, bool),
(path_b, b_is_file): (&RelPath, bool),
) -> Ordering {
- let original_paths_equal = std::ptr::eq(path_a, path_b) || path_a == path_b;
- let mut components_a = path_a.components();
- let mut components_b = path_b.components();
-
- loop {
- match (components_a.next(), components_b.next()) {
- (Some(component_a), Some(component_b)) => {
- let a_leaf_file = a_is_file && components_a.rest().is_empty();
- let b_leaf_file = b_is_file && components_b.rest().is_empty();
-
- let (a_stem, a_ext) = a_leaf_file
- .then(|| stem_and_extension(component_a))
- .unwrap_or_default();
- let (b_stem, b_ext) = b_leaf_file
- .then(|| stem_and_extension(component_b))
- .unwrap_or_default();
- let a_key = if a_leaf_file {
- a_stem
- } else {
- Some(component_a)
- };
- let b_key = if b_leaf_file {
- b_stem
- } else {
- Some(component_b)
- };
-
- 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.eq_ignore_ascii_case(b) => Ordering::Greater,
- (false, true) if a.eq_ignore_ascii_case(b) => Ordering::Less,
- _ => Ordering::Equal,
- })
- .then_with(|| {
- if a_leaf_file && b_leaf_file {
- let a_ext_str = a_ext.unwrap_or_default().to_lowercase();
- let b_ext_str = b_ext.unwrap_or_default().to_lowercase();
- b_ext_str.cmp(&a_ext_str)
- } else {
- Ordering::Equal
- }
- }),
- (Some(_), None) => Ordering::Greater,
- (None, Some(_)) => Ordering::Less,
- (None, None) => Ordering::Equal,
- };
-
- if !ordering.is_eq() {
- return ordering;
- }
- }
- (Some(_), None) => return Ordering::Greater,
- (None, Some(_)) => return Ordering::Less,
- (None, None) => {
- // Deterministic tie-break: use natural sort to prefer lowercase when paths
- // are otherwise equal but still differ in casing.
- if !original_paths_equal {
- return natural_sort(path_a.as_unix_str(), path_b.as_unix_str());
- }
- return Ordering::Equal;
- }
- }
- }
+ compare_rel_paths_by(
+ (path_a, a_is_file),
+ (path_b, b_is_file),
+ SortMode::DirectoriesFirst,
+ SortOrder::Default,
+ )
}
-/// Compare two relative paths with files before directories using
-/// case-insensitive natural sorting. At each directory level, all files
-/// are sorted before all directories, with case-insensitive alphabetical
-/// ordering within each group.
-pub fn compare_rel_paths_files_first(
+pub fn compare_rel_paths_by(
(path_a, a_is_file): (&RelPath, bool),
(path_b, b_is_file): (&RelPath, bool),
+ mode: SortMode,
+ order: SortOrder,
) -> Ordering {
- let original_paths_equal = std::ptr::eq(path_a, path_b) || path_a == path_b;
+ let needs_final_tiebreak =
+ mode != SortMode::DirectoriesFirst && !(std::ptr::eq(path_a, path_b) || path_a == path_b);
+
let mut components_a = path_a.components();
let mut components_b = path_b.components();
@@ -1253,6 +1207,16 @@ pub fn compare_rel_paths_files_first(
let a_leaf_file = a_is_file && components_a.rest().is_empty();
let b_leaf_file = b_is_file && components_b.rest().is_empty();
+ let file_dir_ordering = match mode {
+ SortMode::DirectoriesFirst => a_leaf_file.cmp(&b_leaf_file),
+ SortMode::FilesFirst => b_leaf_file.cmp(&a_leaf_file),
+ SortMode::Mixed => Ordering::Equal,
+ };
+
+ if !file_dir_ordering.is_eq() {
+ return file_dir_ordering;
+ }
+
let (a_stem, a_ext) = a_leaf_file
.then(|| stem_and_extension(component_a))
.unwrap_or_default();
@@ -1272,21 +1236,39 @@ pub fn compare_rel_paths_files_first(
let ordering = match (a_key, b_key) {
(Some(a), Some(b)) => {
- if a_leaf_file && !b_leaf_file {
- Ordering::Less
- } else if !a_leaf_file && b_leaf_file {
- Ordering::Greater
+ let name_cmp = case_group_key(a, order)
+ .cmp(&case_group_key(b, order))
+ .then_with(|| match mode {
+ SortMode::DirectoriesFirst => compare_strings(a, b, order),
+ _ => compare_strings_no_tiebreak(a, b, order),
+ });
+
+ let name_cmp = if mode == SortMode::Mixed {
+ name_cmp.then_with(|| match (a_leaf_file, b_leaf_file) {
+ (true, false) if a.eq_ignore_ascii_case(b) => Ordering::Greater,
+ (false, true) if a.eq_ignore_ascii_case(b) => Ordering::Less,
+ _ => Ordering::Equal,
+ })
} else {
- natural_sort_no_tiebreak(a, b).then_with(|| {
- if a_leaf_file && b_leaf_file {
- let a_ext_str = a_ext.unwrap_or_default().to_lowercase();
- let b_ext_str = b_ext.unwrap_or_default().to_lowercase();
- a_ext_str.cmp(&b_ext_str)
- } else {
- Ordering::Equal
+ name_cmp
+ };
+
+ name_cmp.then_with(|| {
+ if a_leaf_file && b_leaf_file {
+ match order {
+ SortOrder::Unicode => {
+ a_ext.unwrap_or_default().cmp(b_ext.unwrap_or_default())
+ }
+ _ => {
+ let a_ext_str = a_ext.unwrap_or_default().to_lowercase();
+ let b_ext_str = b_ext.unwrap_or_default().to_lowercase();
+ a_ext_str.cmp(&b_ext_str)
+ }
}
- })
- }
+ } else {
+ Ordering::Equal
+ }
+ })
}
(Some(_), None) => Ordering::Greater,
(None, Some(_)) => Ordering::Less,
@@ -1300,10 +1282,8 @@ pub fn compare_rel_paths_files_first(
(Some(_), None) => return Ordering::Greater,
(None, Some(_)) => return Ordering::Less,
(None, None) => {
- // Deterministic tie-break: use natural sort to prefer lowercase when paths
- // are otherwise equal but still differ in casing.
- if !original_paths_equal {
- return natural_sort(path_a.as_unix_str(), path_b.as_unix_str());
+ if needs_final_tiebreak {
+ return compare_strings(path_a.as_unix_str(), path_b.as_unix_str(), order);
}
return Ordering::Equal;
}
@@ -1586,6 +1566,19 @@ mod tests {
use super::*;
use util_macros::perf;
+ fn rel_path_entry(path: &'static str, is_file: bool) -> (&'static RelPath, bool) {
+ (RelPath::unix(path).unwrap(), is_file)
+ }
+
+ fn sorted_rel_paths(
+ mut paths: Vec<(&'static RelPath, bool)>,
+ mode: SortMode,
+ order: SortOrder,
+ ) -> Vec<(&'static RelPath, bool)> {
+ paths.sort_by(|&a, &b| compare_rel_paths_by(a, b, mode, order));
+ paths
+ }
+
#[perf]
fn compare_paths_with_dots() {
let mut paths = vec![
@@ -1715,7 +1708,7 @@ mod tests {
(RelPath::unix("Carrot").unwrap(), false),
(RelPath::unix("aardvark.txt").unwrap(), true),
];
- paths.sort_by(|&a, &b| compare_rel_paths_mixed(a, b));
+ paths.sort_by(|&a, &b| compare_rel_paths_by(a, b, SortMode::Mixed, SortOrder::Default));
// Case-insensitive: aardvark < Apple < banana < Carrot < zebra
assert_eq!(
paths,
@@ -1739,7 +1732,8 @@ mod tests {
(RelPath::unix("Carrot").unwrap(), false),
(RelPath::unix("aardvark.txt").unwrap(), true),
];
- paths.sort_by(|&a, &b| compare_rel_paths_files_first(a, b));
+ paths
+ .sort_by(|&a, &b| compare_rel_paths_by(a, b, SortMode::FilesFirst, SortOrder::Default));
// Files first (case-insensitive), then directories (case-insensitive)
assert_eq!(
paths,
@@ -1763,7 +1757,8 @@ mod tests {
(RelPath::unix("carrot").unwrap(), false),
(RelPath::unix("Aardvark.txt").unwrap(), true),
];
- paths.sort_by(|&a, &b| compare_rel_paths_files_first(a, b));
+ paths
+ .sort_by(|&a, &b| compare_rel_paths_by(a, b, SortMode::FilesFirst, SortOrder::Default));
assert_eq!(
paths,
vec![
@@ -1786,7 +1781,8 @@ mod tests {
(RelPath::unix("dir10").unwrap(), false),
(RelPath::unix("file1.txt").unwrap(), true),
];
- paths.sort_by(|&a, &b| compare_rel_paths_files_first(a, b));
+ paths
+ .sort_by(|&a, &b| compare_rel_paths_by(a, b, SortMode::FilesFirst, SortOrder::Default));
assert_eq!(
paths,
vec![
@@ -1807,14 +1803,14 @@ mod tests {
(RelPath::unix("readme.txt").unwrap(), true),
(RelPath::unix("ReadMe.rs").unwrap(), true),
];
- paths.sort_by(|&a, &b| compare_rel_paths_mixed(a, b));
+ paths.sort_by(|&a, &b| compare_rel_paths_by(a, b, SortMode::Mixed, SortOrder::Default));
// All "readme" variants should group together, sorted by extension
assert_eq!(
paths,
vec![
- (RelPath::unix("readme.txt").unwrap(), true),
- (RelPath::unix("ReadMe.rs").unwrap(), true),
(RelPath::unix("README.md").unwrap(), true),
+ (RelPath::unix("ReadMe.rs").unwrap(), true),
+ (RelPath::unix("readme.txt").unwrap(), true),
]
);
}
@@ -1828,7 +1824,7 @@ mod tests {
(RelPath::unix("file1.txt").unwrap(), true),
(RelPath::unix("dir2").unwrap(), false),
];
- paths.sort_by(|&a, &b| compare_rel_paths_mixed(a, b));
+ paths.sort_by(|&a, &b| compare_rel_paths_by(a, b, SortMode::Mixed, SortOrder::Default));
// Case-insensitive: dir1, dir2, file1, file2 (all mixed)
assert_eq!(
paths,
@@ -1847,7 +1843,7 @@ mod tests {
(RelPath::unix("Hello.txt").unwrap(), true),
(RelPath::unix("hello").unwrap(), false),
];
- paths.sort_by(|&a, &b| compare_rel_paths_mixed(a, b));
+ paths.sort_by(|&a, &b| compare_rel_paths_by(a, b, SortMode::Mixed, SortOrder::Default));
assert_eq!(
paths,
vec![
@@ -1860,7 +1856,7 @@ mod tests {
(RelPath::unix("hello").unwrap(), false),
(RelPath::unix("Hello.txt").unwrap(), true),
];
- paths.sort_by(|&a, &b| compare_rel_paths_mixed(a, b));
+ paths.sort_by(|&a, &b| compare_rel_paths_by(a, b, SortMode::Mixed, SortOrder::Default));
assert_eq!(
paths,
vec![
@@ -1879,7 +1875,7 @@ mod tests {
(RelPath::unix("src").unwrap(), false),
(RelPath::unix("target").unwrap(), false),
];
- paths.sort_by(|&a, &b| compare_rel_paths_mixed(a, b));
+ paths.sort_by(|&a, &b| compare_rel_paths_by(a, b, SortMode::Mixed, SortOrder::Default));
assert_eq!(
paths,
vec![
@@ -1900,7 +1896,8 @@ mod tests {
(RelPath::unix("src").unwrap(), false),
(RelPath::unix("tests").unwrap(), false),
];
- paths.sort_by(|&a, &b| compare_rel_paths_files_first(a, b));
+ paths
+ .sort_by(|&a, &b| compare_rel_paths_by(a, b, SortMode::FilesFirst, SortOrder::Default));
assert_eq!(
paths,
vec![
@@ -1921,7 +1918,7 @@ mod tests {
(RelPath::unix(".github").unwrap(), false),
(RelPath::unix("src").unwrap(), false),
];
- paths.sort_by(|&a, &b| compare_rel_paths_mixed(a, b));
+ paths.sort_by(|&a, &b| compare_rel_paths_by(a, b, SortMode::Mixed, SortOrder::Default));
assert_eq!(
paths,
vec![
@@ -1942,7 +1939,8 @@ mod tests {
(RelPath::unix(".github").unwrap(), false),
(RelPath::unix("src").unwrap(), false),
];
- paths.sort_by(|&a, &b| compare_rel_paths_files_first(a, b));
+ paths
+ .sort_by(|&a, &b| compare_rel_paths_by(a, b, SortMode::FilesFirst, SortOrder::Default));
assert_eq!(
paths,
vec![
@@ -1962,13 +1960,13 @@ mod tests {
(RelPath::unix("file.md").unwrap(), true),
(RelPath::unix("file.txt").unwrap(), true),
];
- paths.sort_by(|&a, &b| compare_rel_paths_mixed(a, b));
+ paths.sort_by(|&a, &b| compare_rel_paths_by(a, b, SortMode::Mixed, SortOrder::Default));
assert_eq!(
paths,
vec![
- (RelPath::unix("file.txt").unwrap(), true),
- (RelPath::unix("file.rs").unwrap(), true),
(RelPath::unix("file.md").unwrap(), true),
+ (RelPath::unix("file.rs").unwrap(), true),
+ (RelPath::unix("file.txt").unwrap(), true),
]
);
}
@@ -1981,7 +1979,8 @@ mod tests {
(RelPath::unix("main.c").unwrap(), true),
(RelPath::unix("main").unwrap(), false),
];
- paths.sort_by(|&a, &b| compare_rel_paths_files_first(a, b));
+ paths
+ .sort_by(|&a, &b| compare_rel_paths_by(a, b, SortMode::FilesFirst, SortOrder::Default));
assert_eq!(
paths,
vec![
@@ -2001,7 +2000,7 @@ mod tests {
(RelPath::unix("a.txt").unwrap(), true),
(RelPath::unix("A.txt").unwrap(), true),
];
- paths.sort_by(|&a, &b| compare_rel_paths_mixed(a, b));
+ paths.sort_by(|&a, &b| compare_rel_paths_by(a, b, SortMode::Mixed, SortOrder::Default));
assert_eq!(
paths,
vec![
@@ -2013,6 +2012,333 @@ mod tests {
);
}
+ #[perf]
+ fn compare_rel_paths_upper() {
+ let directories_only_paths = vec![
+ rel_path_entry("mixedCase", false),
+ rel_path_entry("Zebra", false),
+ rel_path_entry("banana", false),
+ rel_path_entry("ALLCAPS", false),
+ rel_path_entry("Apple", false),
+ rel_path_entry("dog", false),
+ rel_path_entry(".hidden", false),
+ rel_path_entry("Carrot", false),
+ ];
+ assert_eq!(
+ sorted_rel_paths(
+ directories_only_paths,
+ SortMode::DirectoriesFirst,
+ SortOrder::Upper,
+ ),
+ vec![
+ rel_path_entry(".hidden", false),
+ rel_path_entry("ALLCAPS", false),
+ rel_path_entry("Apple", false),
+ rel_path_entry("Carrot", false),
+ rel_path_entry("Zebra", false),
+ rel_path_entry("banana", false),
+ rel_path_entry("dog", false),
+ rel_path_entry("mixedCase", false),
+ ]
+ );
+
+ let file_and_directory_paths = vec![
+ rel_path_entry("banana", false),
+ rel_path_entry("Apple.txt", true),
+ rel_path_entry("dog.md", true),
+ rel_path_entry("ALLCAPS", false),
+ rel_path_entry("file1.txt", true),
+ rel_path_entry("File2.txt", true),
+ rel_path_entry(".hidden", false),
+ ];
+ assert_eq!(
+ sorted_rel_paths(
+ file_and_directory_paths.clone(),
+ SortMode::DirectoriesFirst,
+ SortOrder::Upper,
+ ),
+ vec![
+ rel_path_entry(".hidden", false),
+ rel_path_entry("ALLCAPS", false),
+ rel_path_entry("banana", false),
+ rel_path_entry("Apple.txt", true),
+ rel_path_entry("File2.txt", true),
+ rel_path_entry("dog.md", true),
+ rel_path_entry("file1.txt", true),
+ ]
+ );
+ assert_eq!(
+ sorted_rel_paths(
+ file_and_directory_paths.clone(),
+ SortMode::Mixed,
+ SortOrder::Upper,
+ ),
+ vec![
+ rel_path_entry(".hidden", false),
+ rel_path_entry("ALLCAPS", false),
+ rel_path_entry("Apple.txt", true),
+ rel_path_entry("File2.txt", true),
+ rel_path_entry("banana", false),
+ rel_path_entry("dog.md", true),
+ rel_path_entry("file1.txt", true),
+ ]
+ );
+ assert_eq!(
+ sorted_rel_paths(
+ file_and_directory_paths,
+ SortMode::FilesFirst,
+ SortOrder::Upper,
+ ),
+ vec![
+ rel_path_entry("Apple.txt", true),
+ rel_path_entry("File2.txt", true),
+ rel_path_entry("dog.md", true),
+ rel_path_entry("file1.txt", true),
+ rel_path_entry(".hidden", false),
+ rel_path_entry("ALLCAPS", false),
+ rel_path_entry("banana", false),
+ ]
+ );
+
+ let natural_sort_paths = vec![
+ rel_path_entry("file10.txt", true),
+ rel_path_entry("file1.txt", true),
+ rel_path_entry("file20.txt", true),
+ rel_path_entry("file2.txt", true),
+ ];
+ assert_eq!(
+ sorted_rel_paths(natural_sort_paths, SortMode::Mixed, SortOrder::Upper,),
+ vec![
+ rel_path_entry("file1.txt", true),
+ rel_path_entry("file2.txt", true),
+ rel_path_entry("file10.txt", true),
+ rel_path_entry("file20.txt", true),
+ ]
+ );
+
+ let accented_paths = vec![
+ rel_path_entry("\u{00C9}something.txt", true),
+ rel_path_entry("zebra.txt", true),
+ rel_path_entry("Apple.txt", true),
+ ];
+ assert_eq!(
+ sorted_rel_paths(accented_paths, SortMode::Mixed, SortOrder::Upper),
+ vec![
+ rel_path_entry("Apple.txt", true),
+ rel_path_entry("\u{00C9}something.txt", true),
+ rel_path_entry("zebra.txt", true),
+ ]
+ );
+ }
+
+ #[perf]
+ fn compare_rel_paths_lower() {
+ let directories_only_paths = vec![
+ rel_path_entry("mixedCase", false),
+ rel_path_entry("Zebra", false),
+ rel_path_entry("banana", false),
+ rel_path_entry("ALLCAPS", false),
+ rel_path_entry("Apple", false),
+ rel_path_entry("dog", false),
+ rel_path_entry(".hidden", false),
+ rel_path_entry("Carrot", false),
+ ];
+ assert_eq!(
+ sorted_rel_paths(
+ directories_only_paths,
+ SortMode::DirectoriesFirst,
+ SortOrder::Lower,
+ ),
+ vec![
+ rel_path_entry(".hidden", false),
+ rel_path_entry("banana", false),
+ rel_path_entry("dog", false),
+ rel_path_entry("mixedCase", false),
+ rel_path_entry("ALLCAPS", false),
+ rel_path_entry("Apple", false),
+ rel_path_entry("Carrot", false),
+ rel_path_entry("Zebra", false),
+ ]
+ );
+
+ let file_and_directory_paths = vec![
+ rel_path_entry("banana", false),
+ rel_path_entry("Apple.txt", true),
+ rel_path_entry("dog.md", true),
+ rel_path_entry("ALLCAPS", false),
+ rel_path_entry("file1.txt", true),
+ rel_path_entry("File2.txt", true),
+ rel_path_entry(".hidden", false),
+ ];
+ assert_eq!(
+ sorted_rel_paths(
+ file_and_directory_paths.clone(),
+ SortMode::DirectoriesFirst,
+ SortOrder::Lower,
+ ),
+ vec![
+ rel_path_entry(".hidden", false),
+ rel_path_entry("banana", false),
+ rel_path_entry("ALLCAPS", false),
+ rel_path_entry("dog.md", true),
+ rel_path_entry("file1.txt", true),
+ rel_path_entry("Apple.txt", true),
+ rel_path_entry("File2.txt", true),
+ ]
+ );
+ assert_eq!(
+ sorted_rel_paths(
+ file_and_directory_paths.clone(),
+ SortMode::Mixed,
+ SortOrder::Lower,
+ ),
+ vec![
+ rel_path_entry(".hidden", false),
+ rel_path_entry("banana", false),
+ rel_path_entry("dog.md", true),
+ rel_path_entry("file1.txt", true),
+ rel_path_entry("ALLCAPS", false),
+ rel_path_entry("Apple.txt", true),
+ rel_path_entry("File2.txt", true),
+ ]
+ );
+ assert_eq!(
+ sorted_rel_paths(
+ file_and_directory_paths,
+ SortMode::FilesFirst,
+ SortOrder::Lower,
+ ),
+ vec![
+ rel_path_entry("dog.md", true),
+ rel_path_entry("file1.txt", true),
+ rel_path_entry("Apple.txt", true),
+ rel_path_entry("File2.txt", true),
+ rel_path_entry(".hidden", false),
+ rel_path_entry("banana", false),
+ rel_path_entry("ALLCAPS", false),
+ ]
+ );
+ }
+
+ #[perf]
+ fn compare_rel_paths_unicode() {
+ let directories_only_paths = vec![
+ rel_path_entry("mixedCase", false),
+ rel_path_entry("Zebra", false),
+ rel_path_entry("banana", false),
+ rel_path_entry("ALLCAPS", false),
+ rel_path_entry("Apple", false),
+ rel_path_entry("dog", false),
+ rel_path_entry(".hidden", false),
+ rel_path_entry("Carrot", false),
+ ];
+ assert_eq!(
+ sorted_rel_paths(
+ directories_only_paths,
+ SortMode::DirectoriesFirst,
+ SortOrder::Unicode,
+ ),
+ vec![
+ rel_path_entry(".hidden", false),
+ rel_path_entry("ALLCAPS", false),
+ rel_path_entry("Apple", false),
+ rel_path_entry("Carrot", false),
+ rel_path_entry("Zebra", false),
+ rel_path_entry("banana", false),
+ rel_path_entry("dog", false),
+ rel_path_entry("mixedCase", false),
+ ]
+ );
+
+ let file_and_directory_paths = vec![
+ rel_path_entry("banana", false),
+ rel_path_entry("Apple.txt", true),
+ rel_path_entry("dog.md", true),
+ rel_path_entry("ALLCAPS", false),
+ rel_path_entry("file1.txt", true),
+ rel_path_entry("File2.txt", true),
+ rel_path_entry(".hidden", false),
+ ];
+ assert_eq!(
+ sorted_rel_paths(
+ file_and_directory_paths.clone(),
+ SortMode::DirectoriesFirst,
+ SortOrder::Unicode,
+ ),
+ vec![
+ rel_path_entry(".hidden", false),
+ rel_path_entry("ALLCAPS", false),
+ rel_path_entry("banana", false),
+ rel_path_entry("Apple.txt", true),
+ rel_path_entry("File2.txt", true),
+ rel_path_entry("dog.md", true),
+ rel_path_entry("file1.txt", true),
+ ]
+ );
+ assert_eq!(
+ sorted_rel_paths(
+ file_and_directory_paths.clone(),
+ SortMode::Mixed,
+ SortOrder::Unicode,
+ ),
+ vec![
+ rel_path_entry(".hidden", false),
+ rel_path_entry("ALLCAPS", false),
+ rel_path_entry("Apple.txt", true),
+ rel_path_entry("File2.txt", true),
+ rel_path_entry("banana", false),
+ rel_path_entry("dog.md", true),
+ rel_path_entry("file1.txt", true),
+ ]
+ );
+ assert_eq!(
+ sorted_rel_paths(
+ file_and_directory_paths,
+ SortMode::FilesFirst,
+ SortOrder::Unicode,
+ ),
+ vec![
+ rel_path_entry("Apple.txt", true),
+ rel_path_entry("File2.txt", true),
+ rel_path_entry("dog.md", true),
+ rel_path_entry("file1.txt", true),
+ rel_path_entry(".hidden", false),
+ rel_path_entry("ALLCAPS", false),
+ rel_path_entry("banana", false),
+ ]
+ );
+
+ let numeric_paths = vec![
+ rel_path_entry("file10.txt", true),
+ rel_path_entry("file1.txt", true),
+ rel_path_entry("file2.txt", true),
+ rel_path_entry("file20.txt", true),
+ ];
+ assert_eq!(
+ sorted_rel_paths(numeric_paths, SortMode::Mixed, SortOrder::Unicode,),
+ vec![
+ rel_path_entry("file1.txt", true),
+ rel_path_entry("file10.txt", true),
+ rel_path_entry("file2.txt", true),
+ rel_path_entry("file20.txt", true),
+ ]
+ );
+
+ let accented_paths = vec![
+ rel_path_entry("\u{00C9}something.txt", true),
+ rel_path_entry("zebra.txt", true),
+ rel_path_entry("Apple.txt", true),
+ ];
+ assert_eq!(
+ sorted_rel_paths(accented_paths, SortMode::Mixed, SortOrder::Unicode),
+ vec![
+ rel_path_entry("Apple.txt", true),
+ rel_path_entry("zebra.txt", true),
+ rel_path_entry("\u{00C9}something.txt", true),
+ ]
+ );
+ }
+
#[perf]
fn path_with_position_parse_posix_path() {
// Test POSIX filename edge cases