paths.rs

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