project search: make sorting comparator comply with Ord preconditions (#17604)

Closes #17493
/cc @SomeoneToIgnore /cc @ConradIrwin 

Release Notes:

- N/A
This commit is contained in:
Piotr Osiewicz 2024-09-10 12:55:46 +02:00 committed by GitHub
parent 2fc74a1b71
commit 56bc3c36ad
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 65 additions and 63 deletions

View file

@ -3519,9 +3519,9 @@ mod tests {
" > .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",
@ -3542,10 +3542,10 @@ mod tests {
" > .git", " > .git",
" > a", " > a",
" v b", " v b",
" > [PROCESSING: 'new-dir']", " > 3",
" > 3 <== selected",
" > 4", " > 4",
" a-different-filename.tar.gz", " > [PROCESSING: 'new-dir']",
" a-different-filename.tar.gz <== selected",
" > C", " > C",
" .dockerignore", " .dockerignore",
] ]
@ -3559,10 +3559,10 @@ mod tests {
" > .git", " > .git",
" > a", " > a",
" v b", " v b",
" > 3 <== selected", " > 3",
" > 4", " > 4",
" > new-dir", " > new-dir",
" a-different-filename.tar.gz", " a-different-filename.tar.gz <== selected",
" > C", " > C",
" .dockerignore", " .dockerignore",
] ]
@ -3576,10 +3576,10 @@ mod tests {
" > .git", " > .git",
" > a", " > a",
" v b", " v b",
" > [EDITOR: '3'] <== selected", " > 3",
" > 4", " > 4",
" > new-dir", " > new-dir",
" a-different-filename.tar.gz", " [EDITOR: 'a-different-filename.tar.gz'] <== selected",
" > C", " > C",
" .dockerignore", " .dockerignore",
] ]
@ -3594,10 +3594,10 @@ mod tests {
" > .git", " > .git",
" > a", " > a",
" v b", " v b",
" > 3 <== selected", " > 3",
" > 4", " > 4",
" > new-dir", " > new-dir",
" a-different-filename.tar.gz", " a-different-filename.tar.gz <== selected",
" > C", " > C",
" .dockerignore", " .dockerignore",
] ]
@ -3844,8 +3844,8 @@ mod tests {
&[ &[
// //
"v root1", "v root1",
" one.two.txt <== selected", " one.txt <== selected",
" one.txt", " one.two.txt",
] ]
); );
@ -3862,9 +3862,9 @@ mod tests {
&[ &[
// //
"v root1", "v root1",
" one.two copy.txt <== selected",
" one.two.txt",
" one.txt", " one.txt",
" one copy.txt <== selected",
" one.two.txt",
] ]
); );
@ -3878,10 +3878,10 @@ mod tests {
&[ &[
// //
"v root1", "v root1",
" one.two copy 1.txt <== selected",
" one.two copy.txt",
" one.two.txt",
" one.txt", " one.txt",
" one copy.txt",
" one copy 1.txt <== selected",
" one.two.txt",
] ]
); );
} }
@ -4074,8 +4074,8 @@ mod tests {
" > b", " > b",
" four.txt", " four.txt",
" one.txt", " one.txt",
" three copy.txt <== selected",
" three.txt", " three.txt",
" three copy.txt <== selected",
" two.txt", " two.txt",
] ]
); );
@ -4105,8 +4105,8 @@ mod tests {
" > b", " > b",
" four.txt", " four.txt",
" one.txt", " one.txt",
" three copy.txt",
" three.txt", " three.txt",
" three copy.txt",
" two.txt", " two.txt",
] ]
); );

View file

@ -9,9 +9,8 @@ use std::{
use globset::{Glob, GlobSet, GlobSetBuilder}; use globset::{Glob, GlobSet, GlobSetBuilder};
use regex::Regex; use regex::Regex;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use unicase::UniCase;
use crate::{maybe, NumericPrefixWithSuffix}; 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 {
@ -282,34 +281,29 @@ pub fn compare_paths(
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 maybe_numeric_ordering = maybe!({ let path_a = Path::new(component_a.as_os_str());
let path_a = Path::new(component_a.as_os_str()); let num_and_remainder_a = NumericPrefixWithSuffix::from_numeric_prefixed_str(
let num_and_remainder_a = if a_is_file { if a_is_file {
path_a.file_stem() path_a.file_stem()
} else { } else {
path_a.file_name() path_a.file_name()
} }
.and_then(|s| s.to_str()) .and_then(|s| s.to_str())
.and_then(NumericPrefixWithSuffix::from_numeric_prefixed_str)?; .unwrap_or_default(),
);
let path_b = Path::new(component_b.as_os_str()); let path_b = Path::new(component_b.as_os_str());
let num_and_remainder_b = if b_is_file { let num_and_remainder_b = NumericPrefixWithSuffix::from_numeric_prefixed_str(
if b_is_file {
path_b.file_stem() path_b.file_stem()
} else { } else {
path_b.file_name() path_b.file_name()
} }
.and_then(|s| s.to_str()) .and_then(|s| s.to_str())
.and_then(NumericPrefixWithSuffix::from_numeric_prefixed_str)?; .unwrap_or_default(),
);
num_and_remainder_a.partial_cmp(&num_and_remainder_b) num_and_remainder_a.cmp(&num_and_remainder_b)
});
maybe_numeric_ordering.unwrap_or_else(|| {
let name_a = UniCase::new(component_a.as_os_str().to_string_lossy());
let name_b = UniCase::new(component_b.as_os_str().to_string_lossy());
name_a.cmp(&name_b)
})
}); });
if !ordering.is_eq() { if !ordering.is_eq() {
return ordering; return ordering;
@ -350,6 +344,18 @@ mod tests {
(Path::new("test_dirs/1.46/bar_2"), true), (Path::new("test_dirs/1.46/bar_2"), true),
] ]
); );
let mut paths = vec![
(Path::new("root1/one.txt"), true),
(Path::new("root1/one.two.txt"), true),
];
paths.sort_by(|&a, &b| compare_paths(a, b));
assert_eq!(
paths,
vec![
(Path::new("root1/one.txt"), true),
(Path::new("root1/one.two.txt"), true),
]
);
} }
#[test] #[test]

View file

@ -644,27 +644,27 @@ impl<T: Ord + Clone> RangeExt<T> for RangeInclusive<T> {
/// This is useful for turning regular alphanumerically sorted sequences as `1-abc, 10, 11-def, .., 2, 21-abc` /// This is useful for turning regular alphanumerically sorted sequences as `1-abc, 10, 11-def, .., 2, 21-abc`
/// into `1-abc, 2, 10, 11-def, .., 21-abc` /// into `1-abc, 2, 10, 11-def, .., 21-abc`
#[derive(Debug, PartialEq, Eq)] #[derive(Debug, PartialEq, Eq)]
pub struct NumericPrefixWithSuffix<'a>(i32, &'a str); pub struct NumericPrefixWithSuffix<'a>(Option<u32>, &'a str);
impl<'a> NumericPrefixWithSuffix<'a> { impl<'a> NumericPrefixWithSuffix<'a> {
pub fn from_numeric_prefixed_str(str: &'a str) -> Option<Self> { pub fn from_numeric_prefixed_str(str: &'a str) -> Self {
let i = str.chars().take_while(|c| c.is_ascii_digit()).count(); let i = str.chars().take_while(|c| c.is_ascii_digit()).count();
let (prefix, remainder) = str.split_at(i); let (prefix, remainder) = str.split_at(i);
match prefix.parse::<i32>() { let prefix = prefix.parse().ok();
Ok(prefix) => Some(NumericPrefixWithSuffix(prefix, remainder)), Self(prefix, remainder)
Err(_) => None,
}
} }
} }
impl Ord for NumericPrefixWithSuffix<'_> { impl Ord for NumericPrefixWithSuffix<'_> {
fn cmp(&self, other: &Self) -> Ordering { fn cmp(&self, other: &Self) -> Ordering {
let NumericPrefixWithSuffix(num_a, remainder_a) = self; match (self.0, other.0) {
let NumericPrefixWithSuffix(num_b, remainder_b) = other; (None, None) => UniCase::new(self.1).cmp(&UniCase::new(other.1)),
num_a (None, Some(_)) => Ordering::Greater,
.cmp(num_b) (Some(_), None) => Ordering::Less,
.then_with(|| UniCase::new(remainder_a).cmp(&UniCase::new(remainder_b))) (Some(a), Some(b)) => a
.cmp(&b)
.then_with(|| UniCase::new(self.1).cmp(&UniCase::new(other.1))),
}
} }
} }
@ -737,66 +737,62 @@ mod tests {
let target = "1a"; let target = "1a";
assert_eq!( assert_eq!(
NumericPrefixWithSuffix::from_numeric_prefixed_str(target), NumericPrefixWithSuffix::from_numeric_prefixed_str(target),
Some(NumericPrefixWithSuffix(1, "a")) NumericPrefixWithSuffix(Some(1), "a")
); );
let target = "12ab"; let target = "12ab";
assert_eq!( assert_eq!(
NumericPrefixWithSuffix::from_numeric_prefixed_str(target), NumericPrefixWithSuffix::from_numeric_prefixed_str(target),
Some(NumericPrefixWithSuffix(12, "ab")) NumericPrefixWithSuffix(Some(12), "ab")
); );
let target = "12_ab"; let target = "12_ab";
assert_eq!( assert_eq!(
NumericPrefixWithSuffix::from_numeric_prefixed_str(target), NumericPrefixWithSuffix::from_numeric_prefixed_str(target),
Some(NumericPrefixWithSuffix(12, "_ab")) NumericPrefixWithSuffix(Some(12), "_ab")
); );
let target = "1_2ab"; let target = "1_2ab";
assert_eq!( assert_eq!(
NumericPrefixWithSuffix::from_numeric_prefixed_str(target), NumericPrefixWithSuffix::from_numeric_prefixed_str(target),
Some(NumericPrefixWithSuffix(1, "_2ab")) NumericPrefixWithSuffix(Some(1), "_2ab")
); );
let target = "1.2"; let target = "1.2";
assert_eq!( assert_eq!(
NumericPrefixWithSuffix::from_numeric_prefixed_str(target), NumericPrefixWithSuffix::from_numeric_prefixed_str(target),
Some(NumericPrefixWithSuffix(1, ".2")) NumericPrefixWithSuffix(Some(1), ".2")
); );
let target = "1.2_a"; let target = "1.2_a";
assert_eq!( assert_eq!(
NumericPrefixWithSuffix::from_numeric_prefixed_str(target), NumericPrefixWithSuffix::from_numeric_prefixed_str(target),
Some(NumericPrefixWithSuffix(1, ".2_a")) NumericPrefixWithSuffix(Some(1), ".2_a")
); );
let target = "12.2_a"; let target = "12.2_a";
assert_eq!( assert_eq!(
NumericPrefixWithSuffix::from_numeric_prefixed_str(target), NumericPrefixWithSuffix::from_numeric_prefixed_str(target),
Some(NumericPrefixWithSuffix(12, ".2_a")) NumericPrefixWithSuffix(Some(12), ".2_a")
); );
let target = "12a.2_a"; let target = "12a.2_a";
assert_eq!( assert_eq!(
NumericPrefixWithSuffix::from_numeric_prefixed_str(target), NumericPrefixWithSuffix::from_numeric_prefixed_str(target),
Some(NumericPrefixWithSuffix(12, "a.2_a")) NumericPrefixWithSuffix(Some(12), "a.2_a")
); );
} }
#[test] #[test]
fn test_numeric_prefix_with_suffix() { fn test_numeric_prefix_with_suffix() {
let mut sorted = vec!["1-abc", "10", "11def", "2", "21-abc"]; let mut sorted = vec!["1-abc", "10", "11def", "2", "21-abc"];
sorted.sort_by_key(|s| { sorted.sort_by_key(|s| NumericPrefixWithSuffix::from_numeric_prefixed_str(s));
NumericPrefixWithSuffix::from_numeric_prefixed_str(s).unwrap_or_else(|| {
panic!("Cannot convert string `{s}` into NumericPrefixWithSuffix")
})
});
assert_eq!(sorted, ["1-abc", "2", "10", "11def", "21-abc"]); assert_eq!(sorted, ["1-abc", "2", "10", "11def", "21-abc"]);
for numeric_prefix_less in ["numeric_prefix_less", "aaa", "~™£"] { for numeric_prefix_less in ["numeric_prefix_less", "aaa", "~™£"] {
assert_eq!( assert_eq!(
NumericPrefixWithSuffix::from_numeric_prefixed_str(numeric_prefix_less), NumericPrefixWithSuffix::from_numeric_prefixed_str(numeric_prefix_less),
None, NumericPrefixWithSuffix(None, numeric_prefix_less),
"String without numeric prefix `{numeric_prefix_less}` should not be converted into NumericPrefixWithSuffix" "String without numeric prefix `{numeric_prefix_less}` should not be converted into NumericPrefixWithSuffix"
) )
} }