1use globset::{Glob, GlobSet, GlobSetBuilder};
2use regex::Regex;
3use serde::{Deserialize, Serialize};
4use std::cmp::Ordering;
5use std::path::StripPrefixError;
6use std::sync::{Arc, OnceLock};
7use std::{
8 ffi::OsStr,
9 path::{Path, PathBuf},
10 sync::LazyLock,
11};
12
13/// Returns the path to the user's home directory.
14pub fn home_dir() -> &'static PathBuf {
15 static HOME_DIR: OnceLock<PathBuf> = OnceLock::new();
16 HOME_DIR.get_or_init(|| dirs::home_dir().expect("failed to determine home directory"))
17}
18
19pub trait PathExt {
20 fn compact(&self) -> PathBuf;
21 fn extension_or_hidden_file_name(&self) -> Option<&str>;
22 fn to_sanitized_string(&self) -> String;
23 fn try_from_bytes<'a>(bytes: &'a [u8]) -> anyhow::Result<Self>
24 where
25 Self: From<&'a Path>,
26 {
27 #[cfg(unix)]
28 {
29 use std::os::unix::prelude::OsStrExt;
30 Ok(Self::from(Path::new(OsStr::from_bytes(bytes))))
31 }
32 #[cfg(windows)]
33 {
34 use anyhow::Context as _;
35 use tendril::fmt::{Format, WTF8};
36 WTF8::validate(bytes)
37 .then(|| {
38 // Safety: bytes are valid WTF-8 sequence.
39 Self::from(Path::new(unsafe {
40 OsStr::from_encoded_bytes_unchecked(bytes)
41 }))
42 })
43 .with_context(|| format!("Invalid WTF-8 sequence: {bytes:?}"))
44 }
45 }
46}
47
48impl<T: AsRef<Path>> PathExt for T {
49 /// Compacts a given file path by replacing the user's home directory
50 /// prefix with a tilde (`~`).
51 ///
52 /// # Returns
53 ///
54 /// * A `PathBuf` containing the compacted file path. If the input path
55 /// does not have the user's home directory prefix, or if we are not on
56 /// Linux or macOS, the original path is returned unchanged.
57 fn compact(&self) -> PathBuf {
58 if cfg!(any(target_os = "linux", target_os = "freebsd")) || cfg!(target_os = "macos") {
59 match self.as_ref().strip_prefix(home_dir().as_path()) {
60 Ok(relative_path) => {
61 let mut shortened_path = PathBuf::new();
62 shortened_path.push("~");
63 shortened_path.push(relative_path);
64 shortened_path
65 }
66 Err(_) => self.as_ref().to_path_buf(),
67 }
68 } else {
69 self.as_ref().to_path_buf()
70 }
71 }
72
73 /// Returns a file's extension or, if the file is hidden, its name without the leading dot
74 fn extension_or_hidden_file_name(&self) -> Option<&str> {
75 let path = self.as_ref();
76 let file_name = path.file_name()?.to_str()?;
77 if file_name.starts_with('.') {
78 return file_name.strip_prefix('.');
79 }
80
81 path.extension()
82 .and_then(|e| e.to_str())
83 .or_else(|| path.file_stem()?.to_str())
84 }
85
86 /// Returns a sanitized string representation of the path.
87 /// Note, on Windows, this assumes that the path is a valid UTF-8 string and
88 /// is not a UNC path.
89 fn to_sanitized_string(&self) -> String {
90 #[cfg(target_os = "windows")]
91 {
92 self.as_ref().to_string_lossy().replace("/", "\\")
93 }
94 #[cfg(not(target_os = "windows"))]
95 {
96 self.as_ref().to_string_lossy().to_string()
97 }
98 }
99}
100
101/// Due to the issue of UNC paths on Windows, which can cause bugs in various parts of Zed, introducing this `SanitizedPath`
102/// leverages Rust's type system to ensure that all paths entering Zed are always "sanitized" by removing the `\\\\?\\` prefix.
103/// On non-Windows operating systems, this struct is effectively a no-op.
104#[derive(Debug, Clone, PartialEq, Eq, Hash)]
105pub struct SanitizedPath(pub Arc<Path>);
106
107impl SanitizedPath {
108 pub fn starts_with(&self, prefix: &SanitizedPath) -> bool {
109 self.0.starts_with(&prefix.0)
110 }
111
112 pub fn as_path(&self) -> &Arc<Path> {
113 &self.0
114 }
115
116 pub fn to_string(&self) -> String {
117 self.0.to_string_lossy().to_string()
118 }
119
120 pub fn to_glob_string(&self) -> String {
121 #[cfg(target_os = "windows")]
122 {
123 self.0.to_string_lossy().replace("/", "\\")
124 }
125 #[cfg(not(target_os = "windows"))]
126 {
127 self.0.to_string_lossy().to_string()
128 }
129 }
130
131 pub fn join(&self, path: &Self) -> Self {
132 self.0.join(&path.0).into()
133 }
134
135 pub fn strip_prefix(&self, base: &Self) -> Result<&Path, StripPrefixError> {
136 self.0.strip_prefix(base.as_path())
137 }
138}
139
140impl From<SanitizedPath> for Arc<Path> {
141 fn from(sanitized_path: SanitizedPath) -> Self {
142 sanitized_path.0
143 }
144}
145
146impl From<SanitizedPath> for PathBuf {
147 fn from(sanitized_path: SanitizedPath) -> Self {
148 sanitized_path.0.as_ref().into()
149 }
150}
151
152impl<T: AsRef<Path>> From<T> for SanitizedPath {
153 #[cfg(not(target_os = "windows"))]
154 fn from(path: T) -> Self {
155 let path = path.as_ref();
156 SanitizedPath(path.into())
157 }
158
159 #[cfg(target_os = "windows")]
160 fn from(path: T) -> Self {
161 let path = path.as_ref();
162 SanitizedPath(dunce::simplified(path).into())
163 }
164}
165
166#[derive(Debug, Clone, Copy)]
167pub enum PathStyle {
168 Posix,
169 Windows,
170}
171
172impl PathStyle {
173 #[cfg(target_os = "windows")]
174 pub const fn current() -> Self {
175 PathStyle::Windows
176 }
177
178 #[cfg(not(target_os = "windows"))]
179 pub const fn current() -> Self {
180 PathStyle::Posix
181 }
182
183 #[inline]
184 pub fn separator(&self) -> &str {
185 match self {
186 PathStyle::Posix => "/",
187 PathStyle::Windows => "\\",
188 }
189 }
190}
191
192#[derive(Debug, Clone)]
193pub struct RemotePathBuf {
194 inner: PathBuf,
195 style: PathStyle,
196 string: String, // Cached string representation
197}
198
199impl RemotePathBuf {
200 pub fn new(path: PathBuf, style: PathStyle) -> Self {
201 #[cfg(target_os = "windows")]
202 let string = match style {
203 PathStyle::Posix => path.to_string_lossy().replace('\\', "/"),
204 PathStyle::Windows => path.to_string_lossy().into(),
205 };
206 #[cfg(not(target_os = "windows"))]
207 let string = match style {
208 PathStyle::Posix => path.to_string_lossy().to_string(),
209 PathStyle::Windows => path.to_string_lossy().replace('/', "\\"),
210 };
211 Self {
212 inner: path,
213 style,
214 string,
215 }
216 }
217
218 pub fn from_str(path: &str, style: PathStyle) -> Self {
219 let path_buf = PathBuf::from(path);
220 Self::new(path_buf, style)
221 }
222
223 pub fn to_string(&self) -> String {
224 self.string.clone()
225 }
226
227 #[cfg(target_os = "windows")]
228 pub fn to_proto(self) -> String {
229 match self.path_style() {
230 PathStyle::Posix => self.to_string(),
231 PathStyle::Windows => self.inner.to_string_lossy().replace('\\', "/"),
232 }
233 }
234
235 #[cfg(not(target_os = "windows"))]
236 pub fn to_proto(self) -> String {
237 match self.path_style() {
238 PathStyle::Posix => self.inner.to_string_lossy().to_string(),
239 PathStyle::Windows => self.to_string(),
240 }
241 }
242
243 pub fn as_path(&self) -> &Path {
244 &self.inner
245 }
246
247 pub fn path_style(&self) -> PathStyle {
248 self.style
249 }
250
251 pub fn parent(&self) -> Option<RemotePathBuf> {
252 self.inner
253 .parent()
254 .map(|p| RemotePathBuf::new(p.to_path_buf(), self.style))
255 }
256}
257
258/// A delimiter to use in `path_query:row_number:column_number` strings parsing.
259pub const FILE_ROW_COLUMN_DELIMITER: char = ':';
260
261const ROW_COL_CAPTURE_REGEX: &str = r"(?xs)
262 ([^\(]+)\:(?:
263 \((\d+)[,:](\d+)\) # filename:(row,column), filename:(row:column)
264 |
265 \((\d+)\)() # filename:(row)
266 )
267 |
268 ([^\(]+)(?:
269 \((\d+)[,:](\d+)\) # filename(row,column), filename(row:column)
270 |
271 \((\d+)\)() # filename(row)
272 )
273 |
274 (.+?)(?:
275 \:+(\d+)\:(\d+)\:*$ # filename:row:column
276 |
277 \:+(\d+)\:*()$ # filename:row
278 )";
279
280/// A representation of a path-like string with optional row and column numbers.
281/// Matching values example: `te`, `test.rs:22`, `te:22:5`, `test.c(22)`, `test.c(22,5)`etc.
282#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)]
283pub struct PathWithPosition {
284 pub path: PathBuf,
285 pub row: Option<u32>,
286 // Absent if row is absent.
287 pub column: Option<u32>,
288}
289
290impl PathWithPosition {
291 /// Returns a PathWithPosition from a path.
292 pub fn from_path(path: PathBuf) -> Self {
293 Self {
294 path,
295 row: None,
296 column: None,
297 }
298 }
299
300 /// Parses a string that possibly has `:row:column` or `(row, column)` suffix.
301 /// Parenthesis format is used by [MSBuild](https://learn.microsoft.com/en-us/visualstudio/msbuild/msbuild-diagnostic-format-for-tasks) compatible tools
302 /// Ignores trailing `:`s, so `test.rs:22:` is parsed as `test.rs:22`.
303 /// If the suffix parsing fails, the whole string is parsed as a path.
304 ///
305 /// Be mindful that `test_file:10:1:` is a valid posix filename.
306 /// `PathWithPosition` class assumes that the ending position-like suffix is **not** part of the filename.
307 ///
308 /// # Examples
309 ///
310 /// ```
311 /// # use util::paths::PathWithPosition;
312 /// # use std::path::PathBuf;
313 /// assert_eq!(PathWithPosition::parse_str("test_file"), PathWithPosition {
314 /// path: PathBuf::from("test_file"),
315 /// row: None,
316 /// column: None,
317 /// });
318 /// assert_eq!(PathWithPosition::parse_str("test_file:10"), PathWithPosition {
319 /// path: PathBuf::from("test_file"),
320 /// row: Some(10),
321 /// column: None,
322 /// });
323 /// assert_eq!(PathWithPosition::parse_str("test_file.rs"), PathWithPosition {
324 /// path: PathBuf::from("test_file.rs"),
325 /// row: None,
326 /// column: None,
327 /// });
328 /// assert_eq!(PathWithPosition::parse_str("test_file.rs:1"), PathWithPosition {
329 /// path: PathBuf::from("test_file.rs"),
330 /// row: Some(1),
331 /// column: None,
332 /// });
333 /// assert_eq!(PathWithPosition::parse_str("test_file.rs:1:2"), PathWithPosition {
334 /// path: PathBuf::from("test_file.rs"),
335 /// row: Some(1),
336 /// column: Some(2),
337 /// });
338 /// ```
339 ///
340 /// # Expected parsing results when encounter ill-formatted inputs.
341 /// ```
342 /// # use util::paths::PathWithPosition;
343 /// # use std::path::PathBuf;
344 /// assert_eq!(PathWithPosition::parse_str("test_file.rs:a"), PathWithPosition {
345 /// path: PathBuf::from("test_file.rs:a"),
346 /// row: None,
347 /// column: None,
348 /// });
349 /// assert_eq!(PathWithPosition::parse_str("test_file.rs:a:b"), PathWithPosition {
350 /// path: PathBuf::from("test_file.rs:a:b"),
351 /// row: None,
352 /// column: None,
353 /// });
354 /// assert_eq!(PathWithPosition::parse_str("test_file.rs::"), PathWithPosition {
355 /// path: PathBuf::from("test_file.rs::"),
356 /// row: None,
357 /// column: None,
358 /// });
359 /// assert_eq!(PathWithPosition::parse_str("test_file.rs::1"), PathWithPosition {
360 /// path: PathBuf::from("test_file.rs"),
361 /// row: Some(1),
362 /// column: None,
363 /// });
364 /// assert_eq!(PathWithPosition::parse_str("test_file.rs:1::"), PathWithPosition {
365 /// path: PathBuf::from("test_file.rs"),
366 /// row: Some(1),
367 /// column: None,
368 /// });
369 /// assert_eq!(PathWithPosition::parse_str("test_file.rs::1:2"), PathWithPosition {
370 /// path: PathBuf::from("test_file.rs"),
371 /// row: Some(1),
372 /// column: Some(2),
373 /// });
374 /// assert_eq!(PathWithPosition::parse_str("test_file.rs:1::2"), PathWithPosition {
375 /// path: PathBuf::from("test_file.rs:1"),
376 /// row: Some(2),
377 /// column: None,
378 /// });
379 /// assert_eq!(PathWithPosition::parse_str("test_file.rs:1:2:3"), PathWithPosition {
380 /// path: PathBuf::from("test_file.rs:1"),
381 /// row: Some(2),
382 /// column: Some(3),
383 /// });
384 /// ```
385 pub fn parse_str(s: &str) -> Self {
386 let trimmed = s.trim();
387 let path = Path::new(trimmed);
388 let maybe_file_name_with_row_col = path.file_name().unwrap_or_default().to_string_lossy();
389 if maybe_file_name_with_row_col.is_empty() {
390 return Self {
391 path: Path::new(s).to_path_buf(),
392 row: None,
393 column: None,
394 };
395 }
396
397 // Let's avoid repeated init cost on this. It is subject to thread contention, but
398 // so far this code isn't called from multiple hot paths. Getting contention here
399 // in the future seems unlikely.
400 static SUFFIX_RE: LazyLock<Regex> =
401 LazyLock::new(|| Regex::new(ROW_COL_CAPTURE_REGEX).unwrap());
402 match SUFFIX_RE
403 .captures(&maybe_file_name_with_row_col)
404 .map(|caps| caps.extract())
405 {
406 Some((_, [file_name, maybe_row, maybe_column])) => {
407 let row = maybe_row.parse::<u32>().ok();
408 let column = maybe_column.parse::<u32>().ok();
409
410 let suffix_length = maybe_file_name_with_row_col.len() - file_name.len();
411 let path_without_suffix = &trimmed[..trimmed.len() - suffix_length];
412
413 Self {
414 path: Path::new(path_without_suffix).to_path_buf(),
415 row,
416 column,
417 }
418 }
419 None => {
420 // The `ROW_COL_CAPTURE_REGEX` deals with separated digits only,
421 // but in reality there could be `foo/bar.py:22:in` inputs which we want to match too.
422 // The regex mentioned is not very extendable with "digit or random string" checks, so do this here instead.
423 let delimiter = ':';
424 let mut path_parts = s
425 .rsplitn(3, delimiter)
426 .collect::<Vec<_>>()
427 .into_iter()
428 .rev()
429 .fuse();
430 let mut path_string = path_parts.next().expect("rsplitn should have the rest of the string as its last parameter that we reversed").to_owned();
431 let mut row = None;
432 let mut column = None;
433 if let Some(maybe_row) = path_parts.next() {
434 if let Ok(parsed_row) = maybe_row.parse::<u32>() {
435 row = Some(parsed_row);
436 if let Some(parsed_column) = path_parts
437 .next()
438 .and_then(|maybe_col| maybe_col.parse::<u32>().ok())
439 {
440 column = Some(parsed_column);
441 }
442 } else {
443 path_string.push(delimiter);
444 path_string.push_str(maybe_row);
445 }
446 }
447 for split in path_parts {
448 path_string.push(delimiter);
449 path_string.push_str(split);
450 }
451
452 Self {
453 path: PathBuf::from(path_string),
454 row,
455 column,
456 }
457 }
458 }
459 }
460
461 pub fn map_path<E>(
462 self,
463 mapping: impl FnOnce(PathBuf) -> Result<PathBuf, E>,
464 ) -> Result<PathWithPosition, E> {
465 Ok(PathWithPosition {
466 path: mapping(self.path)?,
467 row: self.row,
468 column: self.column,
469 })
470 }
471
472 pub fn to_string(&self, path_to_string: impl Fn(&PathBuf) -> String) -> String {
473 let path_string = path_to_string(&self.path);
474 if let Some(row) = self.row {
475 if let Some(column) = self.column {
476 format!("{path_string}:{row}:{column}")
477 } else {
478 format!("{path_string}:{row}")
479 }
480 } else {
481 path_string
482 }
483 }
484}
485
486#[derive(Clone, Debug, Default)]
487pub struct PathMatcher {
488 sources: Vec<String>,
489 glob: GlobSet,
490}
491
492// impl std::fmt::Display for PathMatcher {
493// fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
494// self.sources.fmt(f)
495// }
496// }
497
498impl PartialEq for PathMatcher {
499 fn eq(&self, other: &Self) -> bool {
500 self.sources.eq(&other.sources)
501 }
502}
503
504impl Eq for PathMatcher {}
505
506impl PathMatcher {
507 pub fn new(globs: impl IntoIterator<Item = impl AsRef<str>>) -> Result<Self, globset::Error> {
508 let globs = globs
509 .into_iter()
510 .map(|as_str| Glob::new(as_str.as_ref()))
511 .collect::<Result<Vec<_>, _>>()?;
512 let sources = globs.iter().map(|glob| glob.glob().to_owned()).collect();
513 let mut glob_builder = GlobSetBuilder::new();
514 for single_glob in globs {
515 glob_builder.add(single_glob);
516 }
517 let glob = glob_builder.build()?;
518 Ok(PathMatcher { glob, sources })
519 }
520
521 pub fn sources(&self) -> &[String] {
522 &self.sources
523 }
524
525 pub fn is_match<P: AsRef<Path>>(&self, other: P) -> bool {
526 let other_path = other.as_ref();
527 self.sources.iter().any(|source| {
528 let as_bytes = other_path.as_os_str().as_encoded_bytes();
529 as_bytes.starts_with(source.as_bytes()) || as_bytes.ends_with(source.as_bytes())
530 }) || self.glob.is_match(other_path)
531 || self.check_with_end_separator(other_path)
532 }
533
534 fn check_with_end_separator(&self, path: &Path) -> bool {
535 let path_str = path.to_string_lossy();
536 let separator = std::path::MAIN_SEPARATOR_STR;
537 if path_str.ends_with(separator) {
538 false
539 } else {
540 self.glob.is_match(path_str.to_string() + separator)
541 }
542 }
543}
544
545/// Custom character comparison that prioritizes lowercase for same letters
546fn compare_chars(a: char, b: char) -> Ordering {
547 // First compare case-insensitive
548 match a.to_ascii_lowercase().cmp(&b.to_ascii_lowercase()) {
549 Ordering::Equal => {
550 // If same letter, prioritize lowercase (lowercase < uppercase)
551 match (a.is_ascii_lowercase(), b.is_ascii_lowercase()) {
552 (true, false) => Ordering::Less, // lowercase comes first
553 (false, true) => Ordering::Greater, // uppercase comes after
554 _ => Ordering::Equal, // both same case or both non-ascii
555 }
556 }
557 other => other,
558 }
559}
560
561/// Compares two sequences of consecutive digits for natural sorting.
562///
563/// This function is a core component of natural sorting that handles numeric comparison
564/// in a way that feels natural to humans. It extracts and compares consecutive digit
565/// sequences from two iterators, handling various cases like leading zeros and very large numbers.
566///
567/// # Behavior
568///
569/// The function implements the following comparison rules:
570/// 1. Different numeric values: Compares by actual numeric value (e.g., "2" < "10")
571/// 2. Leading zeros: When values are equal, longer sequence wins (e.g., "002" > "2")
572/// 3. Large numbers: Falls back to string comparison for numbers that would overflow u128
573///
574/// # Examples
575///
576/// ```text
577/// "1" vs "2" -> Less (different values)
578/// "2" vs "10" -> Less (numeric comparison)
579/// "002" vs "2" -> Greater (leading zeros)
580/// "10" vs "010" -> Less (leading zeros)
581/// "999..." vs "1000..." -> Less (large number comparison)
582/// ```
583///
584/// # Implementation Details
585///
586/// 1. Extracts consecutive digits into strings
587/// 2. Compares sequence lengths for leading zero handling
588/// 3. For equal lengths, compares digit by digit
589/// 4. For different lengths:
590/// - Attempts numeric comparison first (for numbers up to 2^128 - 1)
591/// - Falls back to string comparison if numbers would overflow
592///
593/// The function advances both iterators past their respective numeric sequences,
594/// regardless of the comparison result.
595fn compare_numeric_segments<I>(
596 a_iter: &mut std::iter::Peekable<I>,
597 b_iter: &mut std::iter::Peekable<I>,
598) -> Ordering
599where
600 I: Iterator<Item = char>,
601{
602 // Collect all consecutive digits into strings
603 let mut a_num_str = String::new();
604 let mut b_num_str = String::new();
605
606 while let Some(&c) = a_iter.peek() {
607 if !c.is_ascii_digit() {
608 break;
609 }
610
611 a_num_str.push(c);
612 a_iter.next();
613 }
614
615 while let Some(&c) = b_iter.peek() {
616 if !c.is_ascii_digit() {
617 break;
618 }
619
620 b_num_str.push(c);
621 b_iter.next();
622 }
623
624 // First compare lengths (handle leading zeros)
625 match a_num_str.len().cmp(&b_num_str.len()) {
626 Ordering::Equal => {
627 // Same length, compare digit by digit
628 match a_num_str.cmp(&b_num_str) {
629 Ordering::Equal => Ordering::Equal,
630 ordering => ordering,
631 }
632 }
633
634 // Different lengths but same value means leading zeros
635 ordering => {
636 // Try parsing as numbers first
637 if let (Ok(a_val), Ok(b_val)) = (a_num_str.parse::<u128>(), b_num_str.parse::<u128>()) {
638 match a_val.cmp(&b_val) {
639 Ordering::Equal => ordering, // Same value, longer one is greater (leading zeros)
640 ord => ord,
641 }
642 } else {
643 // If parsing fails (overflow), compare as strings
644 a_num_str.cmp(&b_num_str)
645 }
646 }
647 }
648}
649
650/// Performs natural sorting comparison between two strings.
651///
652/// Natural sorting is an ordering that handles numeric sequences in a way that matches human expectations.
653/// For example, "file2" comes before "file10" (unlike standard lexicographic sorting).
654///
655/// # Characteristics
656///
657/// * Case-sensitive with lowercase priority: When comparing same letters, lowercase comes before uppercase
658/// * Numbers are compared by numeric value, not character by character
659/// * Leading zeros affect ordering when numeric values are equal
660/// * Can handle numbers larger than u128::MAX (falls back to string comparison)
661///
662/// # Algorithm
663///
664/// The function works by:
665/// 1. Processing strings character by character
666/// 2. When encountering digits, treating consecutive digits as a single number
667/// 3. Comparing numbers by their numeric value rather than lexicographically
668/// 4. For non-numeric characters, using case-sensitive comparison with lowercase priority
669fn natural_sort(a: &str, b: &str) -> Ordering {
670 let mut a_iter = a.chars().peekable();
671 let mut b_iter = b.chars().peekable();
672
673 loop {
674 match (a_iter.peek(), b_iter.peek()) {
675 (None, None) => return Ordering::Equal,
676 (None, _) => return Ordering::Less,
677 (_, None) => return Ordering::Greater,
678 (Some(&a_char), Some(&b_char)) => {
679 if a_char.is_ascii_digit() && b_char.is_ascii_digit() {
680 match compare_numeric_segments(&mut a_iter, &mut b_iter) {
681 Ordering::Equal => continue,
682 ordering => return ordering,
683 }
684 } else {
685 match compare_chars(a_char, b_char) {
686 Ordering::Equal => {
687 a_iter.next();
688 b_iter.next();
689 }
690 ordering => return ordering,
691 }
692 }
693 }
694 }
695 }
696}
697
698pub fn compare_paths(
699 (path_a, a_is_file): (&Path, bool),
700 (path_b, b_is_file): (&Path, bool),
701) -> Ordering {
702 let mut components_a = path_a.components().peekable();
703 let mut components_b = path_b.components().peekable();
704
705 loop {
706 match (components_a.next(), components_b.next()) {
707 (Some(component_a), Some(component_b)) => {
708 let a_is_file = components_a.peek().is_none() && a_is_file;
709 let b_is_file = components_b.peek().is_none() && b_is_file;
710
711 let ordering = a_is_file.cmp(&b_is_file).then_with(|| {
712 let path_a = Path::new(component_a.as_os_str());
713 let path_string_a = if a_is_file {
714 path_a.file_stem()
715 } else {
716 path_a.file_name()
717 }
718 .map(|s| s.to_string_lossy());
719
720 let path_b = Path::new(component_b.as_os_str());
721 let path_string_b = if b_is_file {
722 path_b.file_stem()
723 } else {
724 path_b.file_name()
725 }
726 .map(|s| s.to_string_lossy());
727
728 let compare_components = match (path_string_a, path_string_b) {
729 (Some(a), Some(b)) => natural_sort(&a, &b),
730 (Some(_), None) => Ordering::Greater,
731 (None, Some(_)) => Ordering::Less,
732 (None, None) => Ordering::Equal,
733 };
734
735 compare_components.then_with(|| {
736 if a_is_file && b_is_file {
737 let ext_a = path_a.extension().unwrap_or_default();
738 let ext_b = path_b.extension().unwrap_or_default();
739 ext_a.cmp(ext_b)
740 } else {
741 Ordering::Equal
742 }
743 })
744 });
745
746 if !ordering.is_eq() {
747 return ordering;
748 }
749 }
750 (Some(_), None) => break Ordering::Greater,
751 (None, Some(_)) => break Ordering::Less,
752 (None, None) => break Ordering::Equal,
753 }
754 }
755}
756
757#[cfg(test)]
758mod tests {
759 use super::*;
760
761 #[test]
762 fn compare_paths_with_dots() {
763 let mut paths = vec![
764 (Path::new("test_dirs"), false),
765 (Path::new("test_dirs/1.46"), false),
766 (Path::new("test_dirs/1.46/bar_1"), true),
767 (Path::new("test_dirs/1.46/bar_2"), true),
768 (Path::new("test_dirs/1.45"), false),
769 (Path::new("test_dirs/1.45/foo_2"), true),
770 (Path::new("test_dirs/1.45/foo_1"), true),
771 ];
772 paths.sort_by(|&a, &b| compare_paths(a, b));
773 assert_eq!(
774 paths,
775 vec![
776 (Path::new("test_dirs"), false),
777 (Path::new("test_dirs/1.45"), false),
778 (Path::new("test_dirs/1.45/foo_1"), true),
779 (Path::new("test_dirs/1.45/foo_2"), true),
780 (Path::new("test_dirs/1.46"), false),
781 (Path::new("test_dirs/1.46/bar_1"), true),
782 (Path::new("test_dirs/1.46/bar_2"), true),
783 ]
784 );
785 let mut paths = vec![
786 (Path::new("root1/one.txt"), true),
787 (Path::new("root1/one.two.txt"), true),
788 ];
789 paths.sort_by(|&a, &b| compare_paths(a, b));
790 assert_eq!(
791 paths,
792 vec![
793 (Path::new("root1/one.txt"), true),
794 (Path::new("root1/one.two.txt"), true),
795 ]
796 );
797 }
798
799 #[test]
800 fn compare_paths_with_same_name_different_extensions() {
801 let mut paths = vec![
802 (Path::new("test_dirs/file.rs"), true),
803 (Path::new("test_dirs/file.txt"), true),
804 (Path::new("test_dirs/file.md"), true),
805 (Path::new("test_dirs/file"), true),
806 (Path::new("test_dirs/file.a"), true),
807 ];
808 paths.sort_by(|&a, &b| compare_paths(a, b));
809 assert_eq!(
810 paths,
811 vec![
812 (Path::new("test_dirs/file"), true),
813 (Path::new("test_dirs/file.a"), true),
814 (Path::new("test_dirs/file.md"), true),
815 (Path::new("test_dirs/file.rs"), true),
816 (Path::new("test_dirs/file.txt"), true),
817 ]
818 );
819 }
820
821 #[test]
822 fn compare_paths_case_semi_sensitive() {
823 let mut paths = vec![
824 (Path::new("test_DIRS"), false),
825 (Path::new("test_DIRS/foo_1"), true),
826 (Path::new("test_DIRS/foo_2"), true),
827 (Path::new("test_DIRS/bar"), true),
828 (Path::new("test_DIRS/BAR"), true),
829 (Path::new("test_dirs"), false),
830 (Path::new("test_dirs/foo_1"), true),
831 (Path::new("test_dirs/foo_2"), true),
832 (Path::new("test_dirs/bar"), true),
833 (Path::new("test_dirs/BAR"), true),
834 ];
835 paths.sort_by(|&a, &b| compare_paths(a, b));
836 assert_eq!(
837 paths,
838 vec![
839 (Path::new("test_dirs"), false),
840 (Path::new("test_dirs/bar"), true),
841 (Path::new("test_dirs/BAR"), true),
842 (Path::new("test_dirs/foo_1"), true),
843 (Path::new("test_dirs/foo_2"), true),
844 (Path::new("test_DIRS"), false),
845 (Path::new("test_DIRS/bar"), true),
846 (Path::new("test_DIRS/BAR"), true),
847 (Path::new("test_DIRS/foo_1"), true),
848 (Path::new("test_DIRS/foo_2"), true),
849 ]
850 );
851 }
852
853 #[test]
854 fn path_with_position_parse_posix_path() {
855 // Test POSIX filename edge cases
856 // Read more at https://en.wikipedia.org/wiki/Filename
857 assert_eq!(
858 PathWithPosition::parse_str("test_file"),
859 PathWithPosition {
860 path: PathBuf::from("test_file"),
861 row: None,
862 column: None
863 }
864 );
865
866 assert_eq!(
867 PathWithPosition::parse_str("a:bc:.zip:1"),
868 PathWithPosition {
869 path: PathBuf::from("a:bc:.zip"),
870 row: Some(1),
871 column: None
872 }
873 );
874
875 assert_eq!(
876 PathWithPosition::parse_str("one.second.zip:1"),
877 PathWithPosition {
878 path: PathBuf::from("one.second.zip"),
879 row: Some(1),
880 column: None
881 }
882 );
883
884 // Trim off trailing `:`s for otherwise valid input.
885 assert_eq!(
886 PathWithPosition::parse_str("test_file:10:1:"),
887 PathWithPosition {
888 path: PathBuf::from("test_file"),
889 row: Some(10),
890 column: Some(1)
891 }
892 );
893
894 assert_eq!(
895 PathWithPosition::parse_str("test_file.rs:"),
896 PathWithPosition {
897 path: PathBuf::from("test_file.rs:"),
898 row: None,
899 column: None
900 }
901 );
902
903 assert_eq!(
904 PathWithPosition::parse_str("test_file.rs:1:"),
905 PathWithPosition {
906 path: PathBuf::from("test_file.rs"),
907 row: Some(1),
908 column: None
909 }
910 );
911
912 assert_eq!(
913 PathWithPosition::parse_str("ab\ncd"),
914 PathWithPosition {
915 path: PathBuf::from("ab\ncd"),
916 row: None,
917 column: None
918 }
919 );
920
921 assert_eq!(
922 PathWithPosition::parse_str("👋\nab"),
923 PathWithPosition {
924 path: PathBuf::from("👋\nab"),
925 row: None,
926 column: None
927 }
928 );
929
930 assert_eq!(
931 PathWithPosition::parse_str("Types.hs:(617,9)-(670,28):"),
932 PathWithPosition {
933 path: PathBuf::from("Types.hs"),
934 row: Some(617),
935 column: Some(9),
936 }
937 );
938 }
939
940 #[test]
941 #[cfg(not(target_os = "windows"))]
942 fn path_with_position_parse_posix_path_with_suffix() {
943 assert_eq!(
944 PathWithPosition::parse_str("foo/bar:34:in"),
945 PathWithPosition {
946 path: PathBuf::from("foo/bar"),
947 row: Some(34),
948 column: None,
949 }
950 );
951 assert_eq!(
952 PathWithPosition::parse_str("foo/bar.rs:1902:::15:"),
953 PathWithPosition {
954 path: PathBuf::from("foo/bar.rs:1902"),
955 row: Some(15),
956 column: None
957 }
958 );
959
960 assert_eq!(
961 PathWithPosition::parse_str("app-editors:zed-0.143.6:20240710-201212.log:34:"),
962 PathWithPosition {
963 path: PathBuf::from("app-editors:zed-0.143.6:20240710-201212.log"),
964 row: Some(34),
965 column: None,
966 }
967 );
968
969 assert_eq!(
970 PathWithPosition::parse_str("crates/file_finder/src/file_finder.rs:1902:13:"),
971 PathWithPosition {
972 path: PathBuf::from("crates/file_finder/src/file_finder.rs"),
973 row: Some(1902),
974 column: Some(13),
975 }
976 );
977
978 assert_eq!(
979 PathWithPosition::parse_str("crate/utils/src/test:today.log:34"),
980 PathWithPosition {
981 path: PathBuf::from("crate/utils/src/test:today.log"),
982 row: Some(34),
983 column: None,
984 }
985 );
986 assert_eq!(
987 PathWithPosition::parse_str("/testing/out/src/file_finder.odin(7:15)"),
988 PathWithPosition {
989 path: PathBuf::from("/testing/out/src/file_finder.odin"),
990 row: Some(7),
991 column: Some(15),
992 }
993 );
994 }
995
996 #[test]
997 #[cfg(target_os = "windows")]
998 fn path_with_position_parse_windows_path() {
999 assert_eq!(
1000 PathWithPosition::parse_str("crates\\utils\\paths.rs"),
1001 PathWithPosition {
1002 path: PathBuf::from("crates\\utils\\paths.rs"),
1003 row: None,
1004 column: None
1005 }
1006 );
1007
1008 assert_eq!(
1009 PathWithPosition::parse_str("C:\\Users\\someone\\test_file.rs"),
1010 PathWithPosition {
1011 path: PathBuf::from("C:\\Users\\someone\\test_file.rs"),
1012 row: None,
1013 column: None
1014 }
1015 );
1016 }
1017
1018 #[test]
1019 #[cfg(target_os = "windows")]
1020 fn path_with_position_parse_windows_path_with_suffix() {
1021 assert_eq!(
1022 PathWithPosition::parse_str("crates\\utils\\paths.rs:101"),
1023 PathWithPosition {
1024 path: PathBuf::from("crates\\utils\\paths.rs"),
1025 row: Some(101),
1026 column: None
1027 }
1028 );
1029
1030 assert_eq!(
1031 PathWithPosition::parse_str("\\\\?\\C:\\Users\\someone\\test_file.rs:1:20"),
1032 PathWithPosition {
1033 path: PathBuf::from("\\\\?\\C:\\Users\\someone\\test_file.rs"),
1034 row: Some(1),
1035 column: Some(20)
1036 }
1037 );
1038
1039 assert_eq!(
1040 PathWithPosition::parse_str("C:\\Users\\someone\\test_file.rs(1902,13)"),
1041 PathWithPosition {
1042 path: PathBuf::from("C:\\Users\\someone\\test_file.rs"),
1043 row: Some(1902),
1044 column: Some(13)
1045 }
1046 );
1047
1048 // Trim off trailing `:`s for otherwise valid input.
1049 assert_eq!(
1050 PathWithPosition::parse_str("\\\\?\\C:\\Users\\someone\\test_file.rs:1902:13:"),
1051 PathWithPosition {
1052 path: PathBuf::from("\\\\?\\C:\\Users\\someone\\test_file.rs"),
1053 row: Some(1902),
1054 column: Some(13)
1055 }
1056 );
1057
1058 assert_eq!(
1059 PathWithPosition::parse_str("\\\\?\\C:\\Users\\someone\\test_file.rs:1902:13:15:"),
1060 PathWithPosition {
1061 path: PathBuf::from("\\\\?\\C:\\Users\\someone\\test_file.rs:1902"),
1062 row: Some(13),
1063 column: Some(15)
1064 }
1065 );
1066
1067 assert_eq!(
1068 PathWithPosition::parse_str("\\\\?\\C:\\Users\\someone\\test_file.rs:1902:::15:"),
1069 PathWithPosition {
1070 path: PathBuf::from("\\\\?\\C:\\Users\\someone\\test_file.rs:1902"),
1071 row: Some(15),
1072 column: None
1073 }
1074 );
1075
1076 assert_eq!(
1077 PathWithPosition::parse_str("\\\\?\\C:\\Users\\someone\\test_file.rs(1902,13):"),
1078 PathWithPosition {
1079 path: PathBuf::from("\\\\?\\C:\\Users\\someone\\test_file.rs"),
1080 row: Some(1902),
1081 column: Some(13),
1082 }
1083 );
1084
1085 assert_eq!(
1086 PathWithPosition::parse_str("\\\\?\\C:\\Users\\someone\\test_file.rs(1902):"),
1087 PathWithPosition {
1088 path: PathBuf::from("\\\\?\\C:\\Users\\someone\\test_file.rs"),
1089 row: Some(1902),
1090 column: None,
1091 }
1092 );
1093
1094 assert_eq!(
1095 PathWithPosition::parse_str("C:\\Users\\someone\\test_file.rs:1902:13:"),
1096 PathWithPosition {
1097 path: PathBuf::from("C:\\Users\\someone\\test_file.rs"),
1098 row: Some(1902),
1099 column: Some(13),
1100 }
1101 );
1102
1103 assert_eq!(
1104 PathWithPosition::parse_str("C:\\Users\\someone\\test_file.rs(1902,13):"),
1105 PathWithPosition {
1106 path: PathBuf::from("C:\\Users\\someone\\test_file.rs"),
1107 row: Some(1902),
1108 column: Some(13),
1109 }
1110 );
1111
1112 assert_eq!(
1113 PathWithPosition::parse_str("C:\\Users\\someone\\test_file.rs(1902):"),
1114 PathWithPosition {
1115 path: PathBuf::from("C:\\Users\\someone\\test_file.rs"),
1116 row: Some(1902),
1117 column: None,
1118 }
1119 );
1120
1121 assert_eq!(
1122 PathWithPosition::parse_str("crates/utils/paths.rs:101"),
1123 PathWithPosition {
1124 path: PathBuf::from("crates\\utils\\paths.rs"),
1125 row: Some(101),
1126 column: None,
1127 }
1128 );
1129 }
1130
1131 #[test]
1132 fn test_path_compact() {
1133 let path: PathBuf = [
1134 home_dir().to_string_lossy().to_string(),
1135 "some_file.txt".to_string(),
1136 ]
1137 .iter()
1138 .collect();
1139 if cfg!(any(target_os = "linux", target_os = "freebsd")) || cfg!(target_os = "macos") {
1140 assert_eq!(path.compact().to_str(), Some("~/some_file.txt"));
1141 } else {
1142 assert_eq!(path.compact().to_str(), path.to_str());
1143 }
1144 }
1145
1146 #[test]
1147 fn test_extension_or_hidden_file_name() {
1148 // No dots in name
1149 let path = Path::new("/a/b/c/file_name.rs");
1150 assert_eq!(path.extension_or_hidden_file_name(), Some("rs"));
1151
1152 // Single dot in name
1153 let path = Path::new("/a/b/c/file.name.rs");
1154 assert_eq!(path.extension_or_hidden_file_name(), Some("rs"));
1155
1156 // Multiple dots in name
1157 let path = Path::new("/a/b/c/long.file.name.rs");
1158 assert_eq!(path.extension_or_hidden_file_name(), Some("rs"));
1159
1160 // Hidden file, no extension
1161 let path = Path::new("/a/b/c/.gitignore");
1162 assert_eq!(path.extension_or_hidden_file_name(), Some("gitignore"));
1163
1164 // Hidden file, with extension
1165 let path = Path::new("/a/b/c/.eslintrc.js");
1166 assert_eq!(path.extension_or_hidden_file_name(), Some("eslintrc.js"));
1167 }
1168
1169 #[test]
1170 fn edge_of_glob() {
1171 let path = Path::new("/work/node_modules");
1172 let path_matcher = PathMatcher::new(&["**/node_modules/**".to_owned()]).unwrap();
1173 assert!(
1174 path_matcher.is_match(path),
1175 "Path matcher should match {path:?}"
1176 );
1177 }
1178
1179 #[test]
1180 fn project_search() {
1181 let path = Path::new("/Users/someonetoignore/work/zed/zed.dev/node_modules");
1182 let path_matcher = PathMatcher::new(&["**/node_modules/**".to_owned()]).unwrap();
1183 assert!(
1184 path_matcher.is_match(path),
1185 "Path matcher should match {path:?}"
1186 );
1187 }
1188
1189 #[test]
1190 #[cfg(target_os = "windows")]
1191 fn test_sanitized_path() {
1192 let path = Path::new("C:\\Users\\someone\\test_file.rs");
1193 let sanitized_path = SanitizedPath::from(path);
1194 assert_eq!(
1195 sanitized_path.to_string(),
1196 "C:\\Users\\someone\\test_file.rs"
1197 );
1198
1199 let path = Path::new("\\\\?\\C:\\Users\\someone\\test_file.rs");
1200 let sanitized_path = SanitizedPath::from(path);
1201 assert_eq!(
1202 sanitized_path.to_string(),
1203 "C:\\Users\\someone\\test_file.rs"
1204 );
1205 }
1206
1207 #[test]
1208 fn test_compare_numeric_segments() {
1209 // Helper function to create peekable iterators and test
1210 fn compare(a: &str, b: &str) -> Ordering {
1211 let mut a_iter = a.chars().peekable();
1212 let mut b_iter = b.chars().peekable();
1213
1214 let result = compare_numeric_segments(&mut a_iter, &mut b_iter);
1215
1216 // Verify iterators advanced correctly
1217 assert!(
1218 !a_iter.next().is_some_and(|c| c.is_ascii_digit()),
1219 "Iterator a should have consumed all digits"
1220 );
1221 assert!(
1222 !b_iter.next().is_some_and(|c| c.is_ascii_digit()),
1223 "Iterator b should have consumed all digits"
1224 );
1225
1226 result
1227 }
1228
1229 // Basic numeric comparisons
1230 assert_eq!(compare("0", "0"), Ordering::Equal);
1231 assert_eq!(compare("1", "2"), Ordering::Less);
1232 assert_eq!(compare("9", "10"), Ordering::Less);
1233 assert_eq!(compare("10", "9"), Ordering::Greater);
1234 assert_eq!(compare("99", "100"), Ordering::Less);
1235
1236 // Leading zeros
1237 assert_eq!(compare("0", "00"), Ordering::Less);
1238 assert_eq!(compare("00", "0"), Ordering::Greater);
1239 assert_eq!(compare("01", "1"), Ordering::Greater);
1240 assert_eq!(compare("001", "1"), Ordering::Greater);
1241 assert_eq!(compare("001", "01"), Ordering::Greater);
1242
1243 // Same value different representation
1244 assert_eq!(compare("000100", "100"), Ordering::Greater);
1245 assert_eq!(compare("100", "0100"), Ordering::Less);
1246 assert_eq!(compare("0100", "00100"), Ordering::Less);
1247
1248 // Large numbers
1249 assert_eq!(compare("9999999999", "10000000000"), Ordering::Less);
1250 assert_eq!(
1251 compare(
1252 "340282366920938463463374607431768211455", // u128::MAX
1253 "340282366920938463463374607431768211456"
1254 ),
1255 Ordering::Less
1256 );
1257 assert_eq!(
1258 compare(
1259 "340282366920938463463374607431768211456", // > u128::MAX
1260 "340282366920938463463374607431768211455"
1261 ),
1262 Ordering::Greater
1263 );
1264
1265 // Iterator advancement verification
1266 let mut a_iter = "123abc".chars().peekable();
1267 let mut b_iter = "456def".chars().peekable();
1268
1269 compare_numeric_segments(&mut a_iter, &mut b_iter);
1270
1271 assert_eq!(a_iter.collect::<String>(), "abc");
1272 assert_eq!(b_iter.collect::<String>(), "def");
1273 }
1274
1275 #[test]
1276 fn test_natural_sort() {
1277 // Basic alphanumeric
1278 assert_eq!(natural_sort("a", "b"), Ordering::Less);
1279 assert_eq!(natural_sort("b", "a"), Ordering::Greater);
1280 assert_eq!(natural_sort("a", "a"), Ordering::Equal);
1281
1282 // Case sensitivity
1283 assert_eq!(natural_sort("a", "A"), Ordering::Less);
1284 assert_eq!(natural_sort("A", "a"), Ordering::Greater);
1285 assert_eq!(natural_sort("aA", "aa"), Ordering::Greater);
1286 assert_eq!(natural_sort("aa", "aA"), Ordering::Less);
1287
1288 // Numbers
1289 assert_eq!(natural_sort("1", "2"), Ordering::Less);
1290 assert_eq!(natural_sort("2", "10"), Ordering::Less);
1291 assert_eq!(natural_sort("02", "10"), Ordering::Less);
1292 assert_eq!(natural_sort("02", "2"), Ordering::Greater);
1293
1294 // Mixed alphanumeric
1295 assert_eq!(natural_sort("a1", "a2"), Ordering::Less);
1296 assert_eq!(natural_sort("a2", "a10"), Ordering::Less);
1297 assert_eq!(natural_sort("a02", "a2"), Ordering::Greater);
1298 assert_eq!(natural_sort("a1b", "a1c"), Ordering::Less);
1299
1300 // Multiple numeric segments
1301 assert_eq!(natural_sort("1a2", "1a10"), Ordering::Less);
1302 assert_eq!(natural_sort("1a10", "1a2"), Ordering::Greater);
1303 assert_eq!(natural_sort("2a1", "10a1"), Ordering::Less);
1304
1305 // Special characters
1306 assert_eq!(natural_sort("a-1", "a-2"), Ordering::Less);
1307 assert_eq!(natural_sort("a_1", "a_2"), Ordering::Less);
1308 assert_eq!(natural_sort("a.1", "a.2"), Ordering::Less);
1309
1310 // Unicode
1311 assert_eq!(natural_sort("文1", "文2"), Ordering::Less);
1312 assert_eq!(natural_sort("文2", "文10"), Ordering::Less);
1313 assert_eq!(natural_sort("🔤1", "🔤2"), Ordering::Less);
1314
1315 // Empty and special cases
1316 assert_eq!(natural_sort("", ""), Ordering::Equal);
1317 assert_eq!(natural_sort("", "a"), Ordering::Less);
1318 assert_eq!(natural_sort("a", ""), Ordering::Greater);
1319 assert_eq!(natural_sort(" ", " "), Ordering::Less);
1320
1321 // Mixed everything
1322 assert_eq!(natural_sort("File-1.txt", "File-2.txt"), Ordering::Less);
1323 assert_eq!(natural_sort("File-02.txt", "File-2.txt"), Ordering::Greater);
1324 assert_eq!(natural_sort("File-2.txt", "File-10.txt"), Ordering::Less);
1325 assert_eq!(natural_sort("File_A1", "File_A2"), Ordering::Less);
1326 assert_eq!(natural_sort("File_a1", "File_A1"), Ordering::Less);
1327 }
1328
1329 #[test]
1330 fn test_compare_paths() {
1331 // Helper function for cleaner tests
1332 fn compare(a: &str, is_a_file: bool, b: &str, is_b_file: bool) -> Ordering {
1333 compare_paths((Path::new(a), is_a_file), (Path::new(b), is_b_file))
1334 }
1335
1336 // Basic path comparison
1337 assert_eq!(compare("a", true, "b", true), Ordering::Less);
1338 assert_eq!(compare("b", true, "a", true), Ordering::Greater);
1339 assert_eq!(compare("a", true, "a", true), Ordering::Equal);
1340
1341 // Files vs Directories
1342 assert_eq!(compare("a", true, "a", false), Ordering::Greater);
1343 assert_eq!(compare("a", false, "a", true), Ordering::Less);
1344 assert_eq!(compare("b", false, "a", true), Ordering::Less);
1345
1346 // Extensions
1347 assert_eq!(compare("a.txt", true, "a.md", true), Ordering::Greater);
1348 assert_eq!(compare("a.md", true, "a.txt", true), Ordering::Less);
1349 assert_eq!(compare("a", true, "a.txt", true), Ordering::Less);
1350
1351 // Nested paths
1352 assert_eq!(compare("dir/a", true, "dir/b", true), Ordering::Less);
1353 assert_eq!(compare("dir1/a", true, "dir2/a", true), Ordering::Less);
1354 assert_eq!(compare("dir/sub/a", true, "dir/a", true), Ordering::Less);
1355
1356 // Case sensitivity in paths
1357 assert_eq!(
1358 compare("Dir/file", true, "dir/file", true),
1359 Ordering::Greater
1360 );
1361 assert_eq!(
1362 compare("dir/File", true, "dir/file", true),
1363 Ordering::Greater
1364 );
1365 assert_eq!(compare("dir/file", true, "Dir/File", true), Ordering::Less);
1366
1367 // Hidden files and special names
1368 assert_eq!(compare(".hidden", true, "visible", true), Ordering::Less);
1369 assert_eq!(compare("_special", true, "normal", true), Ordering::Less);
1370 assert_eq!(compare(".config", false, ".data", false), Ordering::Less);
1371
1372 // Mixed numeric paths
1373 assert_eq!(
1374 compare("dir1/file", true, "dir2/file", true),
1375 Ordering::Less
1376 );
1377 assert_eq!(
1378 compare("dir2/file", true, "dir10/file", true),
1379 Ordering::Less
1380 );
1381 assert_eq!(
1382 compare("dir02/file", true, "dir2/file", true),
1383 Ordering::Greater
1384 );
1385
1386 // Root paths
1387 assert_eq!(compare("/a", true, "/b", true), Ordering::Less);
1388 assert_eq!(compare("/", false, "/a", true), Ordering::Less);
1389
1390 // Complex real-world examples
1391 assert_eq!(
1392 compare("project/src/main.rs", true, "project/src/lib.rs", true),
1393 Ordering::Greater
1394 );
1395 assert_eq!(
1396 compare(
1397 "project/tests/test_1.rs",
1398 true,
1399 "project/tests/test_2.rs",
1400 true
1401 ),
1402 Ordering::Less
1403 );
1404 assert_eq!(
1405 compare(
1406 "project/v1.0.0/README.md",
1407 true,
1408 "project/v1.10.0/README.md",
1409 true
1410 ),
1411 Ordering::Less
1412 );
1413 }
1414
1415 #[test]
1416 fn test_natural_sort_case_sensitivity() {
1417 // Same letter different case - lowercase should come first
1418 assert_eq!(natural_sort("a", "A"), Ordering::Less);
1419 assert_eq!(natural_sort("A", "a"), Ordering::Greater);
1420 assert_eq!(natural_sort("a", "a"), Ordering::Equal);
1421 assert_eq!(natural_sort("A", "A"), Ordering::Equal);
1422
1423 // Mixed case strings
1424 assert_eq!(natural_sort("aaa", "AAA"), Ordering::Less);
1425 assert_eq!(natural_sort("AAA", "aaa"), Ordering::Greater);
1426 assert_eq!(natural_sort("aAa", "AaA"), Ordering::Less);
1427
1428 // Different letters
1429 assert_eq!(natural_sort("a", "b"), Ordering::Less);
1430 assert_eq!(natural_sort("A", "b"), Ordering::Less);
1431 assert_eq!(natural_sort("a", "B"), Ordering::Less);
1432 }
1433
1434 #[test]
1435 fn test_natural_sort_with_numbers() {
1436 // Basic number ordering
1437 assert_eq!(natural_sort("file1", "file2"), Ordering::Less);
1438 assert_eq!(natural_sort("file2", "file10"), Ordering::Less);
1439 assert_eq!(natural_sort("file10", "file2"), Ordering::Greater);
1440
1441 // Numbers in different positions
1442 assert_eq!(natural_sort("1file", "2file"), Ordering::Less);
1443 assert_eq!(natural_sort("file1text", "file2text"), Ordering::Less);
1444 assert_eq!(natural_sort("text1file", "text2file"), Ordering::Less);
1445
1446 // Multiple numbers in string
1447 assert_eq!(natural_sort("file1-2", "file1-10"), Ordering::Less);
1448 assert_eq!(natural_sort("2-1file", "10-1file"), Ordering::Less);
1449
1450 // Leading zeros
1451 assert_eq!(natural_sort("file002", "file2"), Ordering::Greater);
1452 assert_eq!(natural_sort("file002", "file10"), Ordering::Less);
1453
1454 // Very large numbers
1455 assert_eq!(
1456 natural_sort("file999999999999999999999", "file999999999999999999998"),
1457 Ordering::Greater
1458 );
1459
1460 // u128 edge cases
1461
1462 // Numbers near u128::MAX (340,282,366,920,938,463,463,374,607,431,768,211,455)
1463 assert_eq!(
1464 natural_sort(
1465 "file340282366920938463463374607431768211454",
1466 "file340282366920938463463374607431768211455"
1467 ),
1468 Ordering::Less
1469 );
1470
1471 // Equal length numbers that overflow u128
1472 assert_eq!(
1473 natural_sort(
1474 "file340282366920938463463374607431768211456",
1475 "file340282366920938463463374607431768211455"
1476 ),
1477 Ordering::Greater
1478 );
1479
1480 // Different length numbers that overflow u128
1481 assert_eq!(
1482 natural_sort(
1483 "file3402823669209384634633746074317682114560",
1484 "file340282366920938463463374607431768211455"
1485 ),
1486 Ordering::Greater
1487 );
1488
1489 // Leading zeros with numbers near u128::MAX
1490 assert_eq!(
1491 natural_sort(
1492 "file0340282366920938463463374607431768211455",
1493 "file340282366920938463463374607431768211455"
1494 ),
1495 Ordering::Greater
1496 );
1497
1498 // Very large numbers with different lengths (both overflow u128)
1499 assert_eq!(
1500 natural_sort(
1501 "file999999999999999999999999999999999999999999999999",
1502 "file9999999999999999999999999999999999999999999999999"
1503 ),
1504 Ordering::Less
1505 );
1506
1507 // Mixed case with numbers
1508 assert_eq!(natural_sort("File1", "file2"), Ordering::Greater);
1509 assert_eq!(natural_sort("file1", "File2"), Ordering::Less);
1510 }
1511
1512 #[test]
1513 fn test_natural_sort_edge_cases() {
1514 // Empty strings
1515 assert_eq!(natural_sort("", ""), Ordering::Equal);
1516 assert_eq!(natural_sort("", "a"), Ordering::Less);
1517 assert_eq!(natural_sort("a", ""), Ordering::Greater);
1518
1519 // Special characters
1520 assert_eq!(natural_sort("file-1", "file_1"), Ordering::Less);
1521 assert_eq!(natural_sort("file.1", "file_1"), Ordering::Less);
1522 assert_eq!(natural_sort("file 1", "file_1"), Ordering::Less);
1523
1524 // Unicode characters
1525 // 9312 vs 9313
1526 assert_eq!(natural_sort("file①", "file②"), Ordering::Less);
1527 // 9321 vs 9313
1528 assert_eq!(natural_sort("file⑩", "file②"), Ordering::Greater);
1529 // 28450 vs 23383
1530 assert_eq!(natural_sort("file漢", "file字"), Ordering::Greater);
1531
1532 // Mixed alphanumeric with special chars
1533 assert_eq!(natural_sort("file-1a", "file-1b"), Ordering::Less);
1534 assert_eq!(natural_sort("file-1.2", "file-1.10"), Ordering::Less);
1535 assert_eq!(natural_sort("file-1.10", "file-1.2"), Ordering::Greater);
1536 }
1537}