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