paths.rs

   1use anyhow::Context;
   2use globset::{GlobBuilder, GlobSet, GlobSetBuilder};
   3use itertools::Itertools;
   4use regex::Regex;
   5use serde::{Deserialize, Serialize};
   6use std::borrow::Cow;
   7use std::cmp::Ordering;
   8use std::error::Error;
   9use std::fmt::{Display, Formatter};
  10use std::mem;
  11use std::path::StripPrefixError;
  12use std::sync::{Arc, OnceLock};
  13use std::{
  14    ffi::OsStr,
  15    path::{Path, PathBuf},
  16    sync::LazyLock,
  17};
  18
  19use crate::rel_path::RelPathBuf;
  20use crate::{rel_path::RelPath, shell::ShellKind};
  21
  22static HOME_DIR: OnceLock<PathBuf> = OnceLock::new();
  23
  24/// Returns the path to the user's home directory.
  25pub fn home_dir() -> &'static PathBuf {
  26    HOME_DIR.get_or_init(|| {
  27        if cfg!(any(test, feature = "test-support")) {
  28            if cfg!(target_os = "macos") {
  29                PathBuf::from("/Users/zed")
  30            } else if cfg!(target_os = "windows") {
  31                PathBuf::from("C:\\Users\\zed")
  32            } else {
  33                PathBuf::from("/home/zed")
  34            }
  35        } else {
  36            dirs::home_dir().expect("failed to determine home directory")
  37        }
  38    })
  39}
  40
  41pub trait PathExt {
  42    /// Compacts a given file path by replacing the user's home directory
  43    /// prefix with a tilde (`~`).
  44    ///
  45    /// # Returns
  46    ///
  47    /// * A `PathBuf` containing the compacted file path. If the input path
  48    ///   does not have the user's home directory prefix, or if we are not on
  49    ///   Linux or macOS, the original path is returned unchanged.
  50    fn compact(&self) -> PathBuf;
  51
  52    /// Returns a file's extension or, if the file is hidden, its name without the leading dot
  53    fn extension_or_hidden_file_name(&self) -> Option<&str>;
  54
  55    fn try_from_bytes<'a>(bytes: &'a [u8]) -> anyhow::Result<Self>
  56    where
  57        Self: From<&'a Path>,
  58    {
  59        #[cfg(unix)]
  60        {
  61            use std::os::unix::prelude::OsStrExt;
  62            Ok(Self::from(Path::new(OsStr::from_bytes(bytes))))
  63        }
  64        #[cfg(windows)]
  65        {
  66            use tendril::fmt::{Format, WTF8};
  67            WTF8::validate(bytes)
  68                .then(|| {
  69                    // Safety: bytes are valid WTF-8 sequence.
  70                    Self::from(Path::new(unsafe {
  71                        OsStr::from_encoded_bytes_unchecked(bytes)
  72                    }))
  73                })
  74                .with_context(|| format!("Invalid WTF-8 sequence: {bytes:?}"))
  75        }
  76    }
  77
  78    /// Converts a local path to one that can be used inside of WSL.
  79    /// Returns `None` if the path cannot be converted into a WSL one (network share).
  80    fn local_to_wsl(&self) -> Option<PathBuf>;
  81
  82    /// Returns a file's "full" joined collection of extensions, in the case where a file does not
  83    /// just have a singular extension but instead has multiple (e.g File.tar.gz, Component.stories.tsx)
  84    ///
  85    /// Will provide back the extensions joined together such as tar.gz or stories.tsx
  86    fn multiple_extensions(&self) -> Option<String>;
  87
  88    /// Try to make a shell-safe representation of the path.
  89    fn try_shell_safe(&self, shell_kind: Option<&ShellKind>) -> anyhow::Result<String>;
  90}
  91
  92impl<T: AsRef<Path>> PathExt for T {
  93    fn compact(&self) -> PathBuf {
  94        if cfg!(any(target_os = "linux", target_os = "freebsd")) || cfg!(target_os = "macos") {
  95            match self.as_ref().strip_prefix(home_dir().as_path()) {
  96                Ok(relative_path) => {
  97                    let mut shortened_path = PathBuf::new();
  98                    shortened_path.push("~");
  99                    shortened_path.push(relative_path);
 100                    shortened_path
 101                }
 102                Err(_) => self.as_ref().to_path_buf(),
 103            }
 104        } else {
 105            self.as_ref().to_path_buf()
 106        }
 107    }
 108
 109    fn extension_or_hidden_file_name(&self) -> Option<&str> {
 110        let path = self.as_ref();
 111        let file_name = path.file_name()?.to_str()?;
 112        if file_name.starts_with('.') {
 113            return file_name.strip_prefix('.');
 114        }
 115
 116        path.extension()
 117            .and_then(|e| e.to_str())
 118            .or_else(|| path.file_stem()?.to_str())
 119    }
 120
 121    fn local_to_wsl(&self) -> Option<PathBuf> {
 122        // quite sketchy to convert this back to path at the end, but a lot of functions only accept paths
 123        // todo: ideally rework them..?
 124        let mut new_path = std::ffi::OsString::new();
 125        for component in self.as_ref().components() {
 126            match component {
 127                std::path::Component::Prefix(prefix) => {
 128                    let drive_letter = prefix.as_os_str().to_string_lossy().to_lowercase();
 129                    let drive_letter = drive_letter.strip_suffix(':')?;
 130
 131                    new_path.push(format!("/mnt/{}", drive_letter));
 132                }
 133                std::path::Component::RootDir => {}
 134                std::path::Component::CurDir => {
 135                    new_path.push("/.");
 136                }
 137                std::path::Component::ParentDir => {
 138                    new_path.push("/..");
 139                }
 140                std::path::Component::Normal(os_str) => {
 141                    new_path.push("/");
 142                    new_path.push(os_str);
 143                }
 144            }
 145        }
 146
 147        Some(new_path.into())
 148    }
 149
 150    fn multiple_extensions(&self) -> Option<String> {
 151        let path = self.as_ref();
 152        let file_name = path.file_name()?.to_str()?;
 153
 154        let parts: Vec<&str> = file_name
 155            .split('.')
 156            // Skip the part with the file name extension
 157            .skip(1)
 158            .collect();
 159
 160        if parts.len() < 2 {
 161            return None;
 162        }
 163
 164        Some(parts.into_iter().join("."))
 165    }
 166
 167    fn try_shell_safe(&self, shell_kind: Option<&ShellKind>) -> anyhow::Result<String> {
 168        let path_str = self
 169            .as_ref()
 170            .to_str()
 171            .with_context(|| "Path contains invalid UTF-8")?;
 172        let quoted = match shell_kind {
 173            Some(kind) => kind.try_quote(path_str),
 174            #[cfg(windows)]
 175            None => Some(ShellKind::quote_powershell(path_str)),
 176            #[cfg(unix)]
 177            None => shlex::try_quote(path_str).ok(),
 178        };
 179        quoted
 180            .as_deref()
 181            .map(ToOwned::to_owned)
 182            .context("Failed to quote path")
 183    }
 184}
 185
 186pub fn path_ends_with(base: &Path, suffix: &Path) -> bool {
 187    strip_path_suffix(base, suffix).is_some()
 188}
 189
 190pub fn strip_path_suffix<'a>(base: &'a Path, suffix: &Path) -> Option<&'a Path> {
 191    if let Some(remainder) = base
 192        .as_os_str()
 193        .as_encoded_bytes()
 194        .strip_suffix(suffix.as_os_str().as_encoded_bytes())
 195    {
 196        if remainder
 197            .last()
 198            .is_none_or(|last_byte| std::path::is_separator(*last_byte as char))
 199        {
 200            let os_str = unsafe {
 201                OsStr::from_encoded_bytes_unchecked(
 202                    &remainder[0..remainder.len().saturating_sub(1)],
 203                )
 204            };
 205            return Some(Path::new(os_str));
 206        }
 207    }
 208    None
 209}
 210
 211/// In memory, this is identical to `Path`. On non-Windows conversions to this type are no-ops. On
 212/// windows, these conversions sanitize UNC paths by removing the `\\\\?\\` prefix.
 213#[derive(Eq, PartialEq, Hash, Ord, PartialOrd)]
 214#[repr(transparent)]
 215pub struct SanitizedPath(Path);
 216
 217impl SanitizedPath {
 218    pub fn new<T: AsRef<Path> + ?Sized>(path: &T) -> &Self {
 219        #[cfg(not(target_os = "windows"))]
 220        return Self::unchecked_new(path.as_ref());
 221
 222        #[cfg(target_os = "windows")]
 223        return Self::unchecked_new(dunce::simplified(path.as_ref()));
 224    }
 225
 226    pub fn unchecked_new<T: AsRef<Path> + ?Sized>(path: &T) -> &Self {
 227        // safe because `Path` and `SanitizedPath` have the same repr and Drop impl
 228        unsafe { mem::transmute::<&Path, &Self>(path.as_ref()) }
 229    }
 230
 231    pub fn from_arc(path: Arc<Path>) -> Arc<Self> {
 232        // safe because `Path` and `SanitizedPath` have the same repr and Drop impl
 233        #[cfg(not(target_os = "windows"))]
 234        return unsafe { mem::transmute::<Arc<Path>, Arc<Self>>(path) };
 235
 236        #[cfg(target_os = "windows")]
 237        {
 238            let simplified = dunce::simplified(path.as_ref());
 239            if simplified == path.as_ref() {
 240                // safe because `Path` and `SanitizedPath` have the same repr and Drop impl
 241                unsafe { mem::transmute::<Arc<Path>, Arc<Self>>(path) }
 242            } else {
 243                Self::unchecked_new(simplified).into()
 244            }
 245        }
 246    }
 247
 248    pub fn new_arc<T: AsRef<Path> + ?Sized>(path: &T) -> Arc<Self> {
 249        Self::new(path).into()
 250    }
 251
 252    pub fn cast_arc(path: Arc<Self>) -> Arc<Path> {
 253        // safe because `Path` and `SanitizedPath` have the same repr and Drop impl
 254        unsafe { mem::transmute::<Arc<Self>, Arc<Path>>(path) }
 255    }
 256
 257    pub fn cast_arc_ref(path: &Arc<Self>) -> &Arc<Path> {
 258        // safe because `Path` and `SanitizedPath` have the same repr and Drop impl
 259        unsafe { mem::transmute::<&Arc<Self>, &Arc<Path>>(path) }
 260    }
 261
 262    pub fn starts_with(&self, prefix: &Self) -> bool {
 263        self.0.starts_with(&prefix.0)
 264    }
 265
 266    pub fn as_path(&self) -> &Path {
 267        &self.0
 268    }
 269
 270    pub fn file_name(&self) -> Option<&std::ffi::OsStr> {
 271        self.0.file_name()
 272    }
 273
 274    pub fn extension(&self) -> Option<&std::ffi::OsStr> {
 275        self.0.extension()
 276    }
 277
 278    pub fn join<P: AsRef<Path>>(&self, path: P) -> PathBuf {
 279        self.0.join(path)
 280    }
 281
 282    pub fn parent(&self) -> Option<&Self> {
 283        self.0.parent().map(Self::unchecked_new)
 284    }
 285
 286    pub fn strip_prefix(&self, base: &Self) -> Result<&Path, StripPrefixError> {
 287        self.0.strip_prefix(base.as_path())
 288    }
 289
 290    pub fn to_str(&self) -> Option<&str> {
 291        self.0.to_str()
 292    }
 293
 294    pub fn to_path_buf(&self) -> PathBuf {
 295        self.0.to_path_buf()
 296    }
 297}
 298
 299impl std::fmt::Debug for SanitizedPath {
 300    fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
 301        std::fmt::Debug::fmt(&self.0, formatter)
 302    }
 303}
 304
 305impl Display for SanitizedPath {
 306    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
 307        write!(f, "{}", self.0.display())
 308    }
 309}
 310
 311impl From<&SanitizedPath> for Arc<SanitizedPath> {
 312    fn from(sanitized_path: &SanitizedPath) -> Self {
 313        let path: Arc<Path> = sanitized_path.0.into();
 314        // safe because `Path` and `SanitizedPath` have the same repr and Drop impl
 315        unsafe { mem::transmute(path) }
 316    }
 317}
 318
 319impl From<&SanitizedPath> for PathBuf {
 320    fn from(sanitized_path: &SanitizedPath) -> Self {
 321        sanitized_path.as_path().into()
 322    }
 323}
 324
 325impl AsRef<Path> for SanitizedPath {
 326    fn as_ref(&self) -> &Path {
 327        &self.0
 328    }
 329}
 330
 331#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
 332pub enum PathStyle {
 333    Posix,
 334    Windows,
 335}
 336
 337impl PathStyle {
 338    #[cfg(target_os = "windows")]
 339    pub const fn local() -> Self {
 340        PathStyle::Windows
 341    }
 342
 343    #[cfg(not(target_os = "windows"))]
 344    pub const fn local() -> Self {
 345        PathStyle::Posix
 346    }
 347
 348    #[inline]
 349    pub fn primary_separator(&self) -> &'static str {
 350        match self {
 351            PathStyle::Posix => "/",
 352            PathStyle::Windows => "\\",
 353        }
 354    }
 355
 356    pub fn separators(&self) -> &'static [&'static str] {
 357        match self {
 358            PathStyle::Posix => &["/"],
 359            PathStyle::Windows => &["\\", "/"],
 360        }
 361    }
 362
 363    pub fn separators_ch(&self) -> &'static [char] {
 364        match self {
 365            PathStyle::Posix => &['/'],
 366            PathStyle::Windows => &['\\', '/'],
 367        }
 368    }
 369
 370    pub fn is_absolute(&self, path_like: &str) -> bool {
 371        path_like.starts_with('/')
 372            || *self == PathStyle::Windows
 373                && (path_like.starts_with('\\')
 374                    || path_like
 375                        .chars()
 376                        .next()
 377                        .is_some_and(|c| c.is_ascii_alphabetic())
 378                        && path_like[1..]
 379                            .strip_prefix(':')
 380                            .is_some_and(|path| path.starts_with('/') || path.starts_with('\\')))
 381    }
 382
 383    pub fn is_windows(&self) -> bool {
 384        *self == PathStyle::Windows
 385    }
 386
 387    pub fn is_posix(&self) -> bool {
 388        *self == PathStyle::Posix
 389    }
 390
 391    pub fn join(self, left: impl AsRef<Path>, right: impl AsRef<Path>) -> Option<String> {
 392        let right = right.as_ref().to_str()?;
 393        if is_absolute(right, self) {
 394            return None;
 395        }
 396        let left = left.as_ref().to_str()?;
 397        if left.is_empty() {
 398            Some(right.into())
 399        } else {
 400            Some(format!(
 401                "{left}{}{right}",
 402                if left.ends_with(self.primary_separator()) {
 403                    ""
 404                } else {
 405                    self.primary_separator()
 406                }
 407            ))
 408        }
 409    }
 410
 411    pub fn split(self, path_like: &str) -> (Option<&str>, &str) {
 412        let Some(pos) = path_like.rfind(self.primary_separator()) else {
 413            return (None, path_like);
 414        };
 415        let filename_start = pos + self.primary_separator().len();
 416        (
 417            Some(&path_like[..filename_start]),
 418            &path_like[filename_start..],
 419        )
 420    }
 421
 422    pub fn strip_prefix<'a>(
 423        &self,
 424        child: &'a Path,
 425        parent: &'a Path,
 426    ) -> Option<std::borrow::Cow<'a, RelPath>> {
 427        let parent = parent.to_str()?;
 428        if parent.is_empty() {
 429            return RelPath::new(child, *self).ok();
 430        }
 431        let parent = self
 432            .separators()
 433            .iter()
 434            .find_map(|sep| parent.strip_suffix(sep))
 435            .unwrap_or(parent);
 436        let child = child.to_str()?;
 437        let stripped = child.strip_prefix(parent)?;
 438        if let Some(relative) = self
 439            .separators()
 440            .iter()
 441            .find_map(|sep| stripped.strip_prefix(sep))
 442        {
 443            RelPath::new(relative.as_ref(), *self).ok()
 444        } else if stripped.is_empty() {
 445            Some(Cow::Borrowed(RelPath::empty()))
 446        } else {
 447            None
 448        }
 449    }
 450}
 451
 452#[derive(Debug, Clone)]
 453pub struct RemotePathBuf {
 454    style: PathStyle,
 455    string: String,
 456}
 457
 458impl RemotePathBuf {
 459    pub fn new(string: String, style: PathStyle) -> Self {
 460        Self { style, string }
 461    }
 462
 463    pub fn from_str(path: &str, style: PathStyle) -> Self {
 464        Self::new(path.to_string(), style)
 465    }
 466
 467    pub fn path_style(&self) -> PathStyle {
 468        self.style
 469    }
 470
 471    pub fn to_proto(self) -> String {
 472        self.string
 473    }
 474}
 475
 476impl Display for RemotePathBuf {
 477    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
 478        write!(f, "{}", self.string)
 479    }
 480}
 481
 482pub fn is_absolute(path_like: &str, path_style: PathStyle) -> bool {
 483    path_like.starts_with('/')
 484        || path_style == PathStyle::Windows
 485            && (path_like.starts_with('\\')
 486                || path_like
 487                    .chars()
 488                    .next()
 489                    .is_some_and(|c| c.is_ascii_alphabetic())
 490                    && path_like[1..]
 491                        .strip_prefix(':')
 492                        .is_some_and(|path| path.starts_with('/') || path.starts_with('\\')))
 493}
 494
 495#[derive(Debug, PartialEq)]
 496#[non_exhaustive]
 497pub struct NormalizeError;
 498
 499impl Error for NormalizeError {}
 500
 501impl std::fmt::Display for NormalizeError {
 502    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
 503        f.write_str("parent reference `..` points outside of base directory")
 504    }
 505}
 506
 507/// Copied from stdlib where it's unstable.
 508///
 509/// Normalize a path, including `..` without traversing the filesystem.
 510///
 511/// Returns an error if normalization would leave leading `..` components.
 512///
 513/// <div class="warning">
 514///
 515/// This function always resolves `..` to the "lexical" parent.
 516/// That is "a/b/../c" will always resolve to `a/c` which can change the meaning of the path.
 517/// In particular, `a/c` and `a/b/../c` are distinct on many systems because `b` may be a symbolic link, so its parent isn't `a`.
 518///
 519/// </div>
 520///
 521/// [`path::absolute`](absolute) is an alternative that preserves `..`.
 522/// Or [`Path::canonicalize`] can be used to resolve any `..` by querying the filesystem.
 523pub fn normalize_lexically(path: &Path) -> Result<PathBuf, NormalizeError> {
 524    use std::path::Component;
 525
 526    let mut lexical = PathBuf::new();
 527    let mut iter = path.components().peekable();
 528
 529    // Find the root, if any, and add it to the lexical path.
 530    // Here we treat the Windows path "C:\" as a single "root" even though
 531    // `components` splits it into two: (Prefix, RootDir).
 532    let root = match iter.peek() {
 533        Some(Component::ParentDir) => return Err(NormalizeError),
 534        Some(p @ Component::RootDir) | Some(p @ Component::CurDir) => {
 535            lexical.push(p);
 536            iter.next();
 537            lexical.as_os_str().len()
 538        }
 539        Some(Component::Prefix(prefix)) => {
 540            lexical.push(prefix.as_os_str());
 541            iter.next();
 542            if let Some(p @ Component::RootDir) = iter.peek() {
 543                lexical.push(p);
 544                iter.next();
 545            }
 546            lexical.as_os_str().len()
 547        }
 548        None => return Ok(PathBuf::new()),
 549        Some(Component::Normal(_)) => 0,
 550    };
 551
 552    for component in iter {
 553        match component {
 554            Component::RootDir => unreachable!(),
 555            Component::Prefix(_) => return Err(NormalizeError),
 556            Component::CurDir => continue,
 557            Component::ParentDir => {
 558                // It's an error if ParentDir causes us to go above the "root".
 559                if lexical.as_os_str().len() == root {
 560                    return Err(NormalizeError);
 561                } else {
 562                    lexical.pop();
 563                }
 564            }
 565            Component::Normal(path) => lexical.push(path),
 566        }
 567    }
 568    Ok(lexical)
 569}
 570
 571/// A delimiter to use in `path_query:row_number:column_number` strings parsing.
 572pub const FILE_ROW_COLUMN_DELIMITER: char = ':';
 573
 574const ROW_COL_CAPTURE_REGEX: &str = r"(?xs)
 575    ([^\(]+)\:(?:
 576        \((\d+)[,:](\d+)\) # filename:(row,column), filename:(row:column)
 577        |
 578        \((\d+)\)()     # filename:(row)
 579    )
 580    |
 581    ([^\(]+)(?:
 582        \((\d+)[,:](\d+)\) # filename(row,column), filename(row:column)
 583        |
 584        \((\d+)\)()     # filename(row)
 585    )
 586    |
 587    (.+?)(?:
 588        \:+(\d+)\:(\d+)\:*$  # filename:row:column
 589        |
 590        \:+(\d+)\:*()$       # filename:row
 591        |
 592        \:+()()$
 593    )";
 594
 595/// A representation of a path-like string with optional row and column numbers.
 596/// Matching values example: `te`, `test.rs:22`, `te:22:5`, `test.c(22)`, `test.c(22,5)`etc.
 597#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)]
 598pub struct PathWithPosition {
 599    pub path: PathBuf,
 600    pub row: Option<u32>,
 601    // Absent if row is absent.
 602    pub column: Option<u32>,
 603}
 604
 605impl PathWithPosition {
 606    /// Returns a PathWithPosition from a path.
 607    pub fn from_path(path: PathBuf) -> Self {
 608        Self {
 609            path,
 610            row: None,
 611            column: None,
 612        }
 613    }
 614
 615    /// Parses a string that possibly has `:row:column` or `(row, column)` suffix.
 616    /// Parenthesis format is used by [MSBuild](https://learn.microsoft.com/en-us/visualstudio/msbuild/msbuild-diagnostic-format-for-tasks) compatible tools
 617    /// Ignores trailing `:`s, so `test.rs:22:` is parsed as `test.rs:22`.
 618    /// If the suffix parsing fails, the whole string is parsed as a path.
 619    ///
 620    /// Be mindful that `test_file:10:1:` is a valid posix filename.
 621    /// `PathWithPosition` class assumes that the ending position-like suffix is **not** part of the filename.
 622    ///
 623    /// # Examples
 624    ///
 625    /// ```
 626    /// # use util::paths::PathWithPosition;
 627    /// # use std::path::PathBuf;
 628    /// assert_eq!(PathWithPosition::parse_str("test_file"), PathWithPosition {
 629    ///     path: PathBuf::from("test_file"),
 630    ///     row: None,
 631    ///     column: None,
 632    /// });
 633    /// assert_eq!(PathWithPosition::parse_str("test_file:10"), PathWithPosition {
 634    ///     path: PathBuf::from("test_file"),
 635    ///     row: Some(10),
 636    ///     column: None,
 637    /// });
 638    /// assert_eq!(PathWithPosition::parse_str("test_file.rs"), PathWithPosition {
 639    ///     path: PathBuf::from("test_file.rs"),
 640    ///     row: None,
 641    ///     column: None,
 642    /// });
 643    /// assert_eq!(PathWithPosition::parse_str("test_file.rs:1"), PathWithPosition {
 644    ///     path: PathBuf::from("test_file.rs"),
 645    ///     row: Some(1),
 646    ///     column: None,
 647    /// });
 648    /// assert_eq!(PathWithPosition::parse_str("test_file.rs:1:2"), PathWithPosition {
 649    ///     path: PathBuf::from("test_file.rs"),
 650    ///     row: Some(1),
 651    ///     column: Some(2),
 652    /// });
 653    /// ```
 654    ///
 655    /// # Expected parsing results when encounter ill-formatted inputs.
 656    /// ```
 657    /// # use util::paths::PathWithPosition;
 658    /// # use std::path::PathBuf;
 659    /// assert_eq!(PathWithPosition::parse_str("test_file.rs:a"), PathWithPosition {
 660    ///     path: PathBuf::from("test_file.rs:a"),
 661    ///     row: None,
 662    ///     column: None,
 663    /// });
 664    /// assert_eq!(PathWithPosition::parse_str("test_file.rs:a:b"), PathWithPosition {
 665    ///     path: PathBuf::from("test_file.rs:a:b"),
 666    ///     row: None,
 667    ///     column: None,
 668    /// });
 669    /// assert_eq!(PathWithPosition::parse_str("test_file.rs"), PathWithPosition {
 670    ///     path: PathBuf::from("test_file.rs"),
 671    ///     row: None,
 672    ///     column: None,
 673    /// });
 674    /// assert_eq!(PathWithPosition::parse_str("test_file.rs::1"), PathWithPosition {
 675    ///     path: PathBuf::from("test_file.rs"),
 676    ///     row: Some(1),
 677    ///     column: None,
 678    /// });
 679    /// assert_eq!(PathWithPosition::parse_str("test_file.rs:1::"), PathWithPosition {
 680    ///     path: PathBuf::from("test_file.rs"),
 681    ///     row: Some(1),
 682    ///     column: None,
 683    /// });
 684    /// assert_eq!(PathWithPosition::parse_str("test_file.rs::1:2"), PathWithPosition {
 685    ///     path: PathBuf::from("test_file.rs"),
 686    ///     row: Some(1),
 687    ///     column: Some(2),
 688    /// });
 689    /// assert_eq!(PathWithPosition::parse_str("test_file.rs:1::2"), PathWithPosition {
 690    ///     path: PathBuf::from("test_file.rs:1"),
 691    ///     row: Some(2),
 692    ///     column: None,
 693    /// });
 694    /// assert_eq!(PathWithPosition::parse_str("test_file.rs:1:2:3"), PathWithPosition {
 695    ///     path: PathBuf::from("test_file.rs:1"),
 696    ///     row: Some(2),
 697    ///     column: Some(3),
 698    /// });
 699    /// ```
 700    pub fn parse_str(s: &str) -> Self {
 701        let trimmed = s.trim();
 702        let path = Path::new(trimmed);
 703        let Some(maybe_file_name_with_row_col) = path.file_name().unwrap_or_default().to_str()
 704        else {
 705            return Self {
 706                path: Path::new(s).to_path_buf(),
 707                row: None,
 708                column: None,
 709            };
 710        };
 711        if maybe_file_name_with_row_col.is_empty() {
 712            return Self {
 713                path: Path::new(s).to_path_buf(),
 714                row: None,
 715                column: None,
 716            };
 717        }
 718
 719        // Let's avoid repeated init cost on this. It is subject to thread contention, but
 720        // so far this code isn't called from multiple hot paths. Getting contention here
 721        // in the future seems unlikely.
 722        static SUFFIX_RE: LazyLock<Regex> =
 723            LazyLock::new(|| Regex::new(ROW_COL_CAPTURE_REGEX).unwrap());
 724        match SUFFIX_RE
 725            .captures(maybe_file_name_with_row_col)
 726            .map(|caps| caps.extract())
 727        {
 728            Some((_, [file_name, maybe_row, maybe_column])) => {
 729                let row = maybe_row.parse::<u32>().ok();
 730                let column = maybe_column.parse::<u32>().ok();
 731
 732                let (_, suffix) = trimmed.split_once(file_name).unwrap();
 733                let path_without_suffix = &trimmed[..trimmed.len() - suffix.len()];
 734
 735                Self {
 736                    path: Path::new(path_without_suffix).to_path_buf(),
 737                    row,
 738                    column,
 739                }
 740            }
 741            None => {
 742                // The `ROW_COL_CAPTURE_REGEX` deals with separated digits only,
 743                // but in reality there could be `foo/bar.py:22:in` inputs which we want to match too.
 744                // The regex mentioned is not very extendable with "digit or random string" checks, so do this here instead.
 745                let delimiter = ':';
 746                let mut path_parts = s
 747                    .rsplitn(3, delimiter)
 748                    .collect::<Vec<_>>()
 749                    .into_iter()
 750                    .rev()
 751                    .fuse();
 752                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();
 753                let mut row = None;
 754                let mut column = None;
 755                if let Some(maybe_row) = path_parts.next() {
 756                    if let Ok(parsed_row) = maybe_row.parse::<u32>() {
 757                        row = Some(parsed_row);
 758                        if let Some(parsed_column) = path_parts
 759                            .next()
 760                            .and_then(|maybe_col| maybe_col.parse::<u32>().ok())
 761                        {
 762                            column = Some(parsed_column);
 763                        }
 764                    } else {
 765                        path_string.push(delimiter);
 766                        path_string.push_str(maybe_row);
 767                    }
 768                }
 769                for split in path_parts {
 770                    path_string.push(delimiter);
 771                    path_string.push_str(split);
 772                }
 773
 774                Self {
 775                    path: PathBuf::from(path_string),
 776                    row,
 777                    column,
 778                }
 779            }
 780        }
 781    }
 782
 783    pub fn map_path<E>(
 784        self,
 785        mapping: impl FnOnce(PathBuf) -> Result<PathBuf, E>,
 786    ) -> Result<PathWithPosition, E> {
 787        Ok(PathWithPosition {
 788            path: mapping(self.path)?,
 789            row: self.row,
 790            column: self.column,
 791        })
 792    }
 793
 794    pub fn to_string(&self, path_to_string: impl Fn(&PathBuf) -> String) -> String {
 795        let path_string = path_to_string(&self.path);
 796        if let Some(row) = self.row {
 797            if let Some(column) = self.column {
 798                format!("{path_string}:{row}:{column}")
 799            } else {
 800                format!("{path_string}:{row}")
 801            }
 802        } else {
 803            path_string
 804        }
 805    }
 806}
 807
 808#[derive(Clone)]
 809pub struct PathMatcher {
 810    sources: Vec<(String, RelPathBuf, /*trailing separator*/ bool)>,
 811    glob: GlobSet,
 812    path_style: PathStyle,
 813}
 814
 815impl std::fmt::Debug for PathMatcher {
 816    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
 817        f.debug_struct("PathMatcher")
 818            .field("sources", &self.sources)
 819            .field("path_style", &self.path_style)
 820            .finish()
 821    }
 822}
 823
 824impl PartialEq for PathMatcher {
 825    fn eq(&self, other: &Self) -> bool {
 826        self.sources.eq(&other.sources)
 827    }
 828}
 829
 830impl Eq for PathMatcher {}
 831
 832impl PathMatcher {
 833    pub fn new(
 834        globs: impl IntoIterator<Item = impl AsRef<str>>,
 835        path_style: PathStyle,
 836    ) -> Result<Self, globset::Error> {
 837        let globs = globs
 838            .into_iter()
 839            .map(|as_str| {
 840                GlobBuilder::new(as_str.as_ref())
 841                    .backslash_escape(path_style.is_posix())
 842                    .build()
 843            })
 844            .collect::<Result<Vec<_>, _>>()?;
 845        let sources = globs
 846            .iter()
 847            .filter_map(|glob| {
 848                let glob = glob.glob();
 849                Some((
 850                    glob.to_string(),
 851                    RelPath::new(&glob.as_ref(), path_style)
 852                        .ok()
 853                        .map(std::borrow::Cow::into_owned)?,
 854                    glob.ends_with(path_style.separators_ch()),
 855                ))
 856            })
 857            .collect();
 858        let mut glob_builder = GlobSetBuilder::new();
 859        for single_glob in globs {
 860            glob_builder.add(single_glob);
 861        }
 862        let glob = glob_builder.build()?;
 863        Ok(PathMatcher {
 864            glob,
 865            sources,
 866            path_style,
 867        })
 868    }
 869
 870    pub fn sources(&self) -> impl Iterator<Item = &str> + Clone {
 871        self.sources.iter().map(|(source, ..)| source.as_str())
 872    }
 873
 874    pub fn is_match<P: AsRef<RelPath>>(&self, other: P) -> bool {
 875        let other = other.as_ref();
 876        if self
 877            .sources
 878            .iter()
 879            .any(|(_, source, _)| other.starts_with(source) || other.ends_with(source))
 880        {
 881            return true;
 882        }
 883        let other_path = other.display(self.path_style);
 884
 885        if self.glob.is_match(&*other_path) {
 886            return true;
 887        }
 888
 889        self.glob
 890            .is_match(other_path.into_owned() + self.path_style.primary_separator())
 891    }
 892
 893    pub fn is_match_std_path<P: AsRef<Path>>(&self, other: P) -> bool {
 894        let other = other.as_ref();
 895        if self.sources.iter().any(|(_, source, _)| {
 896            other.starts_with(source.as_std_path()) || other.ends_with(source.as_std_path())
 897        }) {
 898            return true;
 899        }
 900        self.glob.is_match(other)
 901    }
 902}
 903
 904impl Default for PathMatcher {
 905    fn default() -> Self {
 906        Self {
 907            path_style: PathStyle::local(),
 908            glob: GlobSet::empty(),
 909            sources: vec![],
 910        }
 911    }
 912}
 913
 914/// Compares two sequences of consecutive digits for natural sorting.
 915///
 916/// This function is a core component of natural sorting that handles numeric comparison
 917/// in a way that feels natural to humans. It extracts and compares consecutive digit
 918/// sequences from two iterators, handling various cases like leading zeros and very large numbers.
 919///
 920/// # Behavior
 921///
 922/// The function implements the following comparison rules:
 923/// 1. Different numeric values: Compares by actual numeric value (e.g., "2" < "10")
 924/// 2. Leading zeros: When values are equal, longer sequence wins (e.g., "002" > "2")
 925/// 3. Large numbers: Falls back to string comparison for numbers that would overflow u128
 926///
 927/// # Examples
 928///
 929/// ```text
 930/// "1" vs "2"      -> Less       (different values)
 931/// "2" vs "10"     -> Less       (numeric comparison)
 932/// "002" vs "2"    -> Greater    (leading zeros)
 933/// "10" vs "010"   -> Less       (leading zeros)
 934/// "999..." vs "1000..." -> Less (large number comparison)
 935/// ```
 936///
 937/// # Implementation Details
 938///
 939/// 1. Extracts consecutive digits into strings
 940/// 2. Compares sequence lengths for leading zero handling
 941/// 3. For equal lengths, compares digit by digit
 942/// 4. For different lengths:
 943///    - Attempts numeric comparison first (for numbers up to 2^128 - 1)
 944///    - Falls back to string comparison if numbers would overflow
 945///
 946/// The function advances both iterators past their respective numeric sequences,
 947/// regardless of the comparison result.
 948fn compare_numeric_segments<I>(
 949    a_iter: &mut std::iter::Peekable<I>,
 950    b_iter: &mut std::iter::Peekable<I>,
 951) -> Ordering
 952where
 953    I: Iterator<Item = char>,
 954{
 955    // Collect all consecutive digits into strings
 956    let mut a_num_str = String::new();
 957    let mut b_num_str = String::new();
 958
 959    while let Some(&c) = a_iter.peek() {
 960        if !c.is_ascii_digit() {
 961            break;
 962        }
 963
 964        a_num_str.push(c);
 965        a_iter.next();
 966    }
 967
 968    while let Some(&c) = b_iter.peek() {
 969        if !c.is_ascii_digit() {
 970            break;
 971        }
 972
 973        b_num_str.push(c);
 974        b_iter.next();
 975    }
 976
 977    // First compare lengths (handle leading zeros)
 978    match a_num_str.len().cmp(&b_num_str.len()) {
 979        Ordering::Equal => {
 980            // Same length, compare digit by digit
 981            match a_num_str.cmp(&b_num_str) {
 982                Ordering::Equal => Ordering::Equal,
 983                ordering => ordering,
 984            }
 985        }
 986
 987        // Different lengths but same value means leading zeros
 988        ordering => {
 989            // Try parsing as numbers first
 990            if let (Ok(a_val), Ok(b_val)) = (a_num_str.parse::<u128>(), b_num_str.parse::<u128>()) {
 991                match a_val.cmp(&b_val) {
 992                    Ordering::Equal => ordering, // Same value, longer one is greater (leading zeros)
 993                    ord => ord,
 994                }
 995            } else {
 996                // If parsing fails (overflow), compare as strings
 997                a_num_str.cmp(&b_num_str)
 998            }
 999        }
1000    }
1001}
1002
1003/// Performs natural sorting comparison between two strings.
1004///
1005/// Natural sorting is an ordering that handles numeric sequences in a way that matches human expectations.
1006/// For example, "file2" comes before "file10" (unlike standard lexicographic sorting).
1007///
1008/// # Characteristics
1009///
1010/// * Case-sensitive with lowercase priority: When comparing same letters, lowercase comes before uppercase
1011/// * Numbers are compared by numeric value, not character by character
1012/// * Leading zeros affect ordering when numeric values are equal
1013/// * Can handle numbers larger than u128::MAX (falls back to string comparison)
1014/// * When strings are equal case-insensitively, lowercase is prioritized (lowercase < uppercase)
1015///
1016/// # Algorithm
1017///
1018/// The function works by:
1019/// 1. Processing strings character by character in a case-insensitive manner
1020/// 2. When encountering digits, treating consecutive digits as a single number
1021/// 3. Comparing numbers by their numeric value rather than lexicographically
1022/// 4. For non-numeric characters, using case-insensitive comparison
1023/// 5. If everything is equal case-insensitively, using case-sensitive comparison as final tie-breaker
1024pub fn natural_sort(a: &str, b: &str) -> Ordering {
1025    let mut a_iter = a.chars().peekable();
1026    let mut b_iter = b.chars().peekable();
1027
1028    loop {
1029        match (a_iter.peek(), b_iter.peek()) {
1030            (None, None) => {
1031                return b.cmp(a);
1032            }
1033            (None, _) => return Ordering::Less,
1034            (_, None) => return Ordering::Greater,
1035            (Some(&a_char), Some(&b_char)) => {
1036                if a_char.is_ascii_digit() && b_char.is_ascii_digit() {
1037                    match compare_numeric_segments(&mut a_iter, &mut b_iter) {
1038                        Ordering::Equal => continue,
1039                        ordering => return ordering,
1040                    }
1041                } else {
1042                    match a_char
1043                        .to_ascii_lowercase()
1044                        .cmp(&b_char.to_ascii_lowercase())
1045                    {
1046                        Ordering::Equal => {
1047                            a_iter.next();
1048                            b_iter.next();
1049                        }
1050                        ordering => return ordering,
1051                    }
1052                }
1053            }
1054        }
1055    }
1056}
1057
1058/// Case-insensitive natural sort without applying the final lowercase/uppercase tie-breaker.
1059/// This is useful when comparing individual path components where we want to keep walking
1060/// deeper components before deciding on casing.
1061fn natural_sort_no_tiebreak(a: &str, b: &str) -> Ordering {
1062    if a.eq_ignore_ascii_case(b) {
1063        Ordering::Equal
1064    } else {
1065        natural_sort(a, b)
1066    }
1067}
1068
1069fn stem_and_extension(filename: &str) -> (Option<&str>, Option<&str>) {
1070    if filename.is_empty() {
1071        return (None, None);
1072    }
1073
1074    match filename.rsplit_once('.') {
1075        // Case 1: No dot was found. The entire name is the stem.
1076        None => (Some(filename), None),
1077
1078        // Case 2: A dot was found.
1079        Some((before, after)) => {
1080            // This is the crucial check for dotfiles like ".bashrc".
1081            // If `before` is empty, the dot was the first character.
1082            // In that case, we revert to the "whole name is the stem" logic.
1083            if before.is_empty() {
1084                (Some(filename), None)
1085            } else {
1086                // Otherwise, we have a standard stem and extension.
1087                (Some(before), Some(after))
1088            }
1089        }
1090    }
1091}
1092
1093pub fn compare_rel_paths(
1094    (path_a, a_is_file): (&RelPath, bool),
1095    (path_b, b_is_file): (&RelPath, bool),
1096) -> Ordering {
1097    let mut components_a = path_a.components();
1098    let mut components_b = path_b.components();
1099    loop {
1100        match (components_a.next(), components_b.next()) {
1101            (Some(component_a), Some(component_b)) => {
1102                let a_is_file = a_is_file && components_a.rest().is_empty();
1103                let b_is_file = b_is_file && components_b.rest().is_empty();
1104
1105                let ordering = a_is_file.cmp(&b_is_file).then_with(|| {
1106                    let (a_stem, a_extension) = a_is_file
1107                        .then(|| stem_and_extension(component_a))
1108                        .unwrap_or_default();
1109                    let path_string_a = if a_is_file { a_stem } else { Some(component_a) };
1110
1111                    let (b_stem, b_extension) = b_is_file
1112                        .then(|| stem_and_extension(component_b))
1113                        .unwrap_or_default();
1114                    let path_string_b = if b_is_file { b_stem } else { Some(component_b) };
1115
1116                    let compare_components = match (path_string_a, path_string_b) {
1117                        (Some(a), Some(b)) => natural_sort(&a, &b),
1118                        (Some(_), None) => Ordering::Greater,
1119                        (None, Some(_)) => Ordering::Less,
1120                        (None, None) => Ordering::Equal,
1121                    };
1122
1123                    compare_components.then_with(|| {
1124                        if a_is_file && b_is_file {
1125                            let ext_a = a_extension.unwrap_or_default();
1126                            let ext_b = b_extension.unwrap_or_default();
1127                            ext_a.cmp(ext_b)
1128                        } else {
1129                            Ordering::Equal
1130                        }
1131                    })
1132                });
1133
1134                if !ordering.is_eq() {
1135                    return ordering;
1136                }
1137            }
1138            (Some(_), None) => break Ordering::Greater,
1139            (None, Some(_)) => break Ordering::Less,
1140            (None, None) => break Ordering::Equal,
1141        }
1142    }
1143}
1144
1145/// Compare two relative paths with mixed files and directories using
1146/// case-insensitive natural sorting. For example, "Apple", "aardvark.txt",
1147/// and "Zebra" would be sorted as: aardvark.txt, Apple, Zebra
1148/// (case-insensitive alphabetical).
1149pub fn compare_rel_paths_mixed(
1150    (path_a, a_is_file): (&RelPath, bool),
1151    (path_b, b_is_file): (&RelPath, bool),
1152) -> Ordering {
1153    let original_paths_equal = std::ptr::eq(path_a, path_b) || path_a == path_b;
1154    let mut components_a = path_a.components();
1155    let mut components_b = path_b.components();
1156
1157    loop {
1158        match (components_a.next(), components_b.next()) {
1159            (Some(component_a), Some(component_b)) => {
1160                let a_leaf_file = a_is_file && components_a.rest().is_empty();
1161                let b_leaf_file = b_is_file && components_b.rest().is_empty();
1162
1163                let (a_stem, a_ext) = a_leaf_file
1164                    .then(|| stem_and_extension(component_a))
1165                    .unwrap_or_default();
1166                let (b_stem, b_ext) = b_leaf_file
1167                    .then(|| stem_and_extension(component_b))
1168                    .unwrap_or_default();
1169                let a_key = if a_leaf_file {
1170                    a_stem
1171                } else {
1172                    Some(component_a)
1173                };
1174                let b_key = if b_leaf_file {
1175                    b_stem
1176                } else {
1177                    Some(component_b)
1178                };
1179
1180                let ordering = match (a_key, b_key) {
1181                    (Some(a), Some(b)) => natural_sort_no_tiebreak(a, b)
1182                        .then_with(|| match (a_leaf_file, b_leaf_file) {
1183                            (true, false) if a == b => Ordering::Greater,
1184                            (false, true) if a == b => Ordering::Less,
1185                            _ => Ordering::Equal,
1186                        })
1187                        .then_with(|| {
1188                            if a_leaf_file && b_leaf_file {
1189                                let a_ext_str = a_ext.unwrap_or_default().to_lowercase();
1190                                let b_ext_str = b_ext.unwrap_or_default().to_lowercase();
1191                                b_ext_str.cmp(&a_ext_str)
1192                            } else {
1193                                Ordering::Equal
1194                            }
1195                        }),
1196                    (Some(_), None) => Ordering::Greater,
1197                    (None, Some(_)) => Ordering::Less,
1198                    (None, None) => Ordering::Equal,
1199                };
1200
1201                if !ordering.is_eq() {
1202                    return ordering;
1203                }
1204            }
1205            (Some(_), None) => return Ordering::Greater,
1206            (None, Some(_)) => return Ordering::Less,
1207            (None, None) => {
1208                // Deterministic tie-break: use natural sort to prefer lowercase when paths
1209                // are otherwise equal but still differ in casing.
1210                if !original_paths_equal {
1211                    return natural_sort(path_a.as_unix_str(), path_b.as_unix_str());
1212                }
1213                return Ordering::Equal;
1214            }
1215        }
1216    }
1217}
1218
1219/// Compare two relative paths with files before directories using
1220/// case-insensitive natural sorting. At each directory level, all files
1221/// are sorted before all directories, with case-insensitive alphabetical
1222/// ordering within each group.
1223pub fn compare_rel_paths_files_first(
1224    (path_a, a_is_file): (&RelPath, bool),
1225    (path_b, b_is_file): (&RelPath, bool),
1226) -> Ordering {
1227    let original_paths_equal = std::ptr::eq(path_a, path_b) || path_a == path_b;
1228    let mut components_a = path_a.components();
1229    let mut components_b = path_b.components();
1230
1231    loop {
1232        match (components_a.next(), components_b.next()) {
1233            (Some(component_a), Some(component_b)) => {
1234                let a_leaf_file = a_is_file && components_a.rest().is_empty();
1235                let b_leaf_file = b_is_file && components_b.rest().is_empty();
1236
1237                let (a_stem, a_ext) = a_leaf_file
1238                    .then(|| stem_and_extension(component_a))
1239                    .unwrap_or_default();
1240                let (b_stem, b_ext) = b_leaf_file
1241                    .then(|| stem_and_extension(component_b))
1242                    .unwrap_or_default();
1243                let a_key = if a_leaf_file {
1244                    a_stem
1245                } else {
1246                    Some(component_a)
1247                };
1248                let b_key = if b_leaf_file {
1249                    b_stem
1250                } else {
1251                    Some(component_b)
1252                };
1253
1254                let ordering = match (a_key, b_key) {
1255                    (Some(a), Some(b)) => {
1256                        if a_leaf_file && !b_leaf_file {
1257                            Ordering::Less
1258                        } else if !a_leaf_file && b_leaf_file {
1259                            Ordering::Greater
1260                        } else {
1261                            natural_sort_no_tiebreak(a, b).then_with(|| {
1262                                if a_leaf_file && b_leaf_file {
1263                                    let a_ext_str = a_ext.unwrap_or_default().to_lowercase();
1264                                    let b_ext_str = b_ext.unwrap_or_default().to_lowercase();
1265                                    a_ext_str.cmp(&b_ext_str)
1266                                } else {
1267                                    Ordering::Equal
1268                                }
1269                            })
1270                        }
1271                    }
1272                    (Some(_), None) => Ordering::Greater,
1273                    (None, Some(_)) => Ordering::Less,
1274                    (None, None) => Ordering::Equal,
1275                };
1276
1277                if !ordering.is_eq() {
1278                    return ordering;
1279                }
1280            }
1281            (Some(_), None) => return Ordering::Greater,
1282            (None, Some(_)) => return Ordering::Less,
1283            (None, None) => {
1284                // Deterministic tie-break: use natural sort to prefer lowercase when paths
1285                // are otherwise equal but still differ in casing.
1286                if !original_paths_equal {
1287                    return natural_sort(path_a.as_unix_str(), path_b.as_unix_str());
1288                }
1289                return Ordering::Equal;
1290            }
1291        }
1292    }
1293}
1294
1295pub fn compare_paths(
1296    (path_a, a_is_file): (&Path, bool),
1297    (path_b, b_is_file): (&Path, bool),
1298) -> Ordering {
1299    let mut components_a = path_a.components().peekable();
1300    let mut components_b = path_b.components().peekable();
1301
1302    loop {
1303        match (components_a.next(), components_b.next()) {
1304            (Some(component_a), Some(component_b)) => {
1305                let a_is_file = components_a.peek().is_none() && a_is_file;
1306                let b_is_file = components_b.peek().is_none() && b_is_file;
1307
1308                let ordering = a_is_file.cmp(&b_is_file).then_with(|| {
1309                    let path_a = Path::new(component_a.as_os_str());
1310                    let path_string_a = if a_is_file {
1311                        path_a.file_stem()
1312                    } else {
1313                        path_a.file_name()
1314                    }
1315                    .map(|s| s.to_string_lossy());
1316
1317                    let path_b = Path::new(component_b.as_os_str());
1318                    let path_string_b = if b_is_file {
1319                        path_b.file_stem()
1320                    } else {
1321                        path_b.file_name()
1322                    }
1323                    .map(|s| s.to_string_lossy());
1324
1325                    let compare_components = match (path_string_a, path_string_b) {
1326                        (Some(a), Some(b)) => natural_sort(&a, &b),
1327                        (Some(_), None) => Ordering::Greater,
1328                        (None, Some(_)) => Ordering::Less,
1329                        (None, None) => Ordering::Equal,
1330                    };
1331
1332                    compare_components.then_with(|| {
1333                        if a_is_file && b_is_file {
1334                            let ext_a = path_a.extension().unwrap_or_default();
1335                            let ext_b = path_b.extension().unwrap_or_default();
1336                            ext_a.cmp(ext_b)
1337                        } else {
1338                            Ordering::Equal
1339                        }
1340                    })
1341                });
1342
1343                if !ordering.is_eq() {
1344                    return ordering;
1345                }
1346            }
1347            (Some(_), None) => break Ordering::Greater,
1348            (None, Some(_)) => break Ordering::Less,
1349            (None, None) => break Ordering::Equal,
1350        }
1351    }
1352}
1353
1354#[derive(Debug, Clone, PartialEq, Eq)]
1355pub struct WslPath {
1356    pub distro: String,
1357
1358    // the reason this is an OsString and not any of the path types is that it needs to
1359    // represent a unix path (with '/' separators) on windows. `from_path` does this by
1360    // manually constructing it from the path components of a given windows path.
1361    pub path: std::ffi::OsString,
1362}
1363
1364impl WslPath {
1365    pub fn from_path<P: AsRef<Path>>(path: P) -> Option<WslPath> {
1366        if cfg!(not(target_os = "windows")) {
1367            return None;
1368        }
1369        use std::{
1370            ffi::OsString,
1371            path::{Component, Prefix},
1372        };
1373
1374        let mut components = path.as_ref().components();
1375        let Some(Component::Prefix(prefix)) = components.next() else {
1376            return None;
1377        };
1378        let (server, distro) = match prefix.kind() {
1379            Prefix::UNC(server, distro) => (server, distro),
1380            Prefix::VerbatimUNC(server, distro) => (server, distro),
1381            _ => return None,
1382        };
1383        let Some(Component::RootDir) = components.next() else {
1384            return None;
1385        };
1386
1387        let server_str = server.to_string_lossy();
1388        if server_str == "wsl.localhost" || server_str == "wsl$" {
1389            let mut result = OsString::from("");
1390            for c in components {
1391                use Component::*;
1392                match c {
1393                    Prefix(p) => unreachable!("got {p:?}, but already stripped prefix"),
1394                    RootDir => unreachable!("got root dir, but already stripped root"),
1395                    CurDir => continue,
1396                    ParentDir => result.push("/.."),
1397                    Normal(s) => {
1398                        result.push("/");
1399                        result.push(s);
1400                    }
1401                }
1402            }
1403            if result.is_empty() {
1404                result.push("/");
1405            }
1406            Some(WslPath {
1407                distro: distro.to_string_lossy().to_string(),
1408                path: result,
1409            })
1410        } else {
1411            None
1412        }
1413    }
1414}
1415
1416pub trait UrlExt {
1417    /// A version of `url::Url::to_file_path` that does platform handling based on the provided `PathStyle` instead of the host platform.
1418    ///
1419    /// Prefer using this over `url::Url::to_file_path` when you need to handle paths in a cross-platform way as is the case for remoting interactions.
1420    fn to_file_path_ext(&self, path_style: PathStyle) -> Result<PathBuf, ()>;
1421}
1422
1423impl UrlExt for url::Url {
1424    // Copied from `url::Url::to_file_path`, but the `cfg` handling is replaced with runtime branching on `PathStyle`
1425    fn to_file_path_ext(&self, source_path_style: PathStyle) -> Result<PathBuf, ()> {
1426        if let Some(segments) = self.path_segments() {
1427            let host = match self.host() {
1428                None | Some(url::Host::Domain("localhost")) => None,
1429                Some(_) if source_path_style.is_windows() && self.scheme() == "file" => {
1430                    self.host_str()
1431                }
1432                _ => return Err(()),
1433            };
1434
1435            let str_len = self.as_str().len();
1436            let estimated_capacity = if source_path_style.is_windows() {
1437                // remove scheme: - has possible \\ for hostname
1438                str_len.saturating_sub(self.scheme().len() + 1)
1439            } else {
1440                // remove scheme://
1441                str_len.saturating_sub(self.scheme().len() + 3)
1442            };
1443            return match source_path_style {
1444                PathStyle::Posix => {
1445                    file_url_segments_to_pathbuf_posix(estimated_capacity, host, segments)
1446                }
1447                PathStyle::Windows => {
1448                    file_url_segments_to_pathbuf_windows(estimated_capacity, host, segments)
1449                }
1450            };
1451        }
1452
1453        fn file_url_segments_to_pathbuf_posix(
1454            estimated_capacity: usize,
1455            host: Option<&str>,
1456            segments: std::str::Split<'_, char>,
1457        ) -> Result<PathBuf, ()> {
1458            use percent_encoding::percent_decode;
1459
1460            if host.is_some() {
1461                return Err(());
1462            }
1463
1464            let mut bytes = Vec::new();
1465            bytes.try_reserve(estimated_capacity).map_err(|_| ())?;
1466
1467            for segment in segments {
1468                bytes.push(b'/');
1469                bytes.extend(percent_decode(segment.as_bytes()));
1470            }
1471
1472            // A windows drive letter must end with a slash.
1473            if bytes.len() > 2
1474                && bytes[bytes.len() - 2].is_ascii_alphabetic()
1475                && matches!(bytes[bytes.len() - 1], b':' | b'|')
1476            {
1477                bytes.push(b'/');
1478            }
1479
1480            let path = String::from_utf8(bytes).map_err(|_| ())?;
1481            debug_assert!(
1482                PathStyle::Posix.is_absolute(&path),
1483                "to_file_path() failed to produce an absolute Path"
1484            );
1485
1486            Ok(PathBuf::from(path))
1487        }
1488
1489        fn file_url_segments_to_pathbuf_windows(
1490            estimated_capacity: usize,
1491            host: Option<&str>,
1492            mut segments: std::str::Split<'_, char>,
1493        ) -> Result<PathBuf, ()> {
1494            use percent_encoding::percent_decode_str;
1495            let mut string = String::new();
1496            string.try_reserve(estimated_capacity).map_err(|_| ())?;
1497            if let Some(host) = host {
1498                string.push_str(r"\\");
1499                string.push_str(host);
1500            } else {
1501                let first = segments.next().ok_or(())?;
1502
1503                match first.len() {
1504                    2 => {
1505                        if !first.starts_with(|c| char::is_ascii_alphabetic(&c))
1506                            || first.as_bytes()[1] != b':'
1507                        {
1508                            return Err(());
1509                        }
1510
1511                        string.push_str(first);
1512                    }
1513
1514                    4 => {
1515                        if !first.starts_with(|c| char::is_ascii_alphabetic(&c)) {
1516                            return Err(());
1517                        }
1518                        let bytes = first.as_bytes();
1519                        if bytes[1] != b'%'
1520                            || bytes[2] != b'3'
1521                            || (bytes[3] != b'a' && bytes[3] != b'A')
1522                        {
1523                            return Err(());
1524                        }
1525
1526                        string.push_str(&first[0..1]);
1527                        string.push(':');
1528                    }
1529
1530                    _ => return Err(()),
1531                }
1532            };
1533
1534            for segment in segments {
1535                string.push('\\');
1536
1537                // Currently non-unicode windows paths cannot be represented
1538                match percent_decode_str(segment).decode_utf8() {
1539                    Ok(s) => string.push_str(&s),
1540                    Err(..) => return Err(()),
1541                }
1542            }
1543            // ensure our estimated capacity was good
1544            if cfg!(test) {
1545                debug_assert!(
1546                    string.len() <= estimated_capacity,
1547                    "len: {}, capacity: {}",
1548                    string.len(),
1549                    estimated_capacity
1550                );
1551            }
1552            debug_assert!(
1553                PathStyle::Windows.is_absolute(&string),
1554                "to_file_path() failed to produce an absolute Path"
1555            );
1556            let path = PathBuf::from(string);
1557            Ok(path)
1558        }
1559        Err(())
1560    }
1561}
1562
1563#[cfg(test)]
1564mod tests {
1565    use crate::rel_path::rel_path;
1566
1567    use super::*;
1568    use util_macros::perf;
1569
1570    #[perf]
1571    fn compare_paths_with_dots() {
1572        let mut paths = vec![
1573            (Path::new("test_dirs"), false),
1574            (Path::new("test_dirs/1.46"), false),
1575            (Path::new("test_dirs/1.46/bar_1"), true),
1576            (Path::new("test_dirs/1.46/bar_2"), true),
1577            (Path::new("test_dirs/1.45"), false),
1578            (Path::new("test_dirs/1.45/foo_2"), true),
1579            (Path::new("test_dirs/1.45/foo_1"), true),
1580        ];
1581        paths.sort_by(|&a, &b| compare_paths(a, b));
1582        assert_eq!(
1583            paths,
1584            vec![
1585                (Path::new("test_dirs"), false),
1586                (Path::new("test_dirs/1.45"), false),
1587                (Path::new("test_dirs/1.45/foo_1"), true),
1588                (Path::new("test_dirs/1.45/foo_2"), true),
1589                (Path::new("test_dirs/1.46"), false),
1590                (Path::new("test_dirs/1.46/bar_1"), true),
1591                (Path::new("test_dirs/1.46/bar_2"), true),
1592            ]
1593        );
1594        let mut paths = vec![
1595            (Path::new("root1/one.txt"), true),
1596            (Path::new("root1/one.two.txt"), true),
1597        ];
1598        paths.sort_by(|&a, &b| compare_paths(a, b));
1599        assert_eq!(
1600            paths,
1601            vec![
1602                (Path::new("root1/one.txt"), true),
1603                (Path::new("root1/one.two.txt"), true),
1604            ]
1605        );
1606    }
1607
1608    #[perf]
1609    fn compare_paths_with_same_name_different_extensions() {
1610        let mut paths = vec![
1611            (Path::new("test_dirs/file.rs"), true),
1612            (Path::new("test_dirs/file.txt"), true),
1613            (Path::new("test_dirs/file.md"), true),
1614            (Path::new("test_dirs/file"), true),
1615            (Path::new("test_dirs/file.a"), true),
1616        ];
1617        paths.sort_by(|&a, &b| compare_paths(a, b));
1618        assert_eq!(
1619            paths,
1620            vec![
1621                (Path::new("test_dirs/file"), true),
1622                (Path::new("test_dirs/file.a"), true),
1623                (Path::new("test_dirs/file.md"), true),
1624                (Path::new("test_dirs/file.rs"), true),
1625                (Path::new("test_dirs/file.txt"), true),
1626            ]
1627        );
1628    }
1629
1630    #[perf]
1631    fn compare_paths_case_semi_sensitive() {
1632        let mut paths = vec![
1633            (Path::new("test_DIRS"), false),
1634            (Path::new("test_DIRS/foo_1"), true),
1635            (Path::new("test_DIRS/foo_2"), true),
1636            (Path::new("test_DIRS/bar"), true),
1637            (Path::new("test_DIRS/BAR"), true),
1638            (Path::new("test_dirs"), false),
1639            (Path::new("test_dirs/foo_1"), true),
1640            (Path::new("test_dirs/foo_2"), true),
1641            (Path::new("test_dirs/bar"), true),
1642            (Path::new("test_dirs/BAR"), true),
1643        ];
1644        paths.sort_by(|&a, &b| compare_paths(a, b));
1645        assert_eq!(
1646            paths,
1647            vec![
1648                (Path::new("test_dirs"), false),
1649                (Path::new("test_dirs/bar"), true),
1650                (Path::new("test_dirs/BAR"), true),
1651                (Path::new("test_dirs/foo_1"), true),
1652                (Path::new("test_dirs/foo_2"), true),
1653                (Path::new("test_DIRS"), false),
1654                (Path::new("test_DIRS/bar"), true),
1655                (Path::new("test_DIRS/BAR"), true),
1656                (Path::new("test_DIRS/foo_1"), true),
1657                (Path::new("test_DIRS/foo_2"), true),
1658            ]
1659        );
1660    }
1661
1662    #[perf]
1663    fn compare_paths_mixed_case_numeric_ordering() {
1664        let mut entries = [
1665            (Path::new(".config"), false),
1666            (Path::new("Dir1"), false),
1667            (Path::new("dir01"), false),
1668            (Path::new("dir2"), false),
1669            (Path::new("Dir02"), false),
1670            (Path::new("dir10"), false),
1671            (Path::new("Dir10"), false),
1672        ];
1673
1674        entries.sort_by(|&a, &b| compare_paths(a, b));
1675
1676        let ordered: Vec<&str> = entries
1677            .iter()
1678            .map(|(path, _)| path.to_str().unwrap())
1679            .collect();
1680
1681        assert_eq!(
1682            ordered,
1683            vec![
1684                ".config", "Dir1", "dir01", "dir2", "Dir02", "dir10", "Dir10"
1685            ]
1686        );
1687    }
1688
1689    #[perf]
1690    fn compare_rel_paths_mixed_case_insensitive() {
1691        // Test that mixed mode is case-insensitive
1692        let mut paths = vec![
1693            (RelPath::unix("zebra.txt").unwrap(), true),
1694            (RelPath::unix("Apple").unwrap(), false),
1695            (RelPath::unix("banana.rs").unwrap(), true),
1696            (RelPath::unix("Carrot").unwrap(), false),
1697            (RelPath::unix("aardvark.txt").unwrap(), true),
1698        ];
1699        paths.sort_by(|&a, &b| compare_rel_paths_mixed(a, b));
1700        // Case-insensitive: aardvark < Apple < banana < Carrot < zebra
1701        assert_eq!(
1702            paths,
1703            vec![
1704                (RelPath::unix("aardvark.txt").unwrap(), true),
1705                (RelPath::unix("Apple").unwrap(), false),
1706                (RelPath::unix("banana.rs").unwrap(), true),
1707                (RelPath::unix("Carrot").unwrap(), false),
1708                (RelPath::unix("zebra.txt").unwrap(), true),
1709            ]
1710        );
1711    }
1712
1713    #[perf]
1714    fn compare_rel_paths_files_first_basic() {
1715        // Test that files come before directories
1716        let mut paths = vec![
1717            (RelPath::unix("zebra.txt").unwrap(), true),
1718            (RelPath::unix("Apple").unwrap(), false),
1719            (RelPath::unix("banana.rs").unwrap(), true),
1720            (RelPath::unix("Carrot").unwrap(), false),
1721            (RelPath::unix("aardvark.txt").unwrap(), true),
1722        ];
1723        paths.sort_by(|&a, &b| compare_rel_paths_files_first(a, b));
1724        // Files first (case-insensitive), then directories (case-insensitive)
1725        assert_eq!(
1726            paths,
1727            vec![
1728                (RelPath::unix("aardvark.txt").unwrap(), true),
1729                (RelPath::unix("banana.rs").unwrap(), true),
1730                (RelPath::unix("zebra.txt").unwrap(), true),
1731                (RelPath::unix("Apple").unwrap(), false),
1732                (RelPath::unix("Carrot").unwrap(), false),
1733            ]
1734        );
1735    }
1736
1737    #[perf]
1738    fn compare_rel_paths_files_first_case_insensitive() {
1739        // Test case-insensitive sorting within files and directories
1740        let mut paths = vec![
1741            (RelPath::unix("Zebra.txt").unwrap(), true),
1742            (RelPath::unix("apple").unwrap(), false),
1743            (RelPath::unix("Banana.rs").unwrap(), true),
1744            (RelPath::unix("carrot").unwrap(), false),
1745            (RelPath::unix("Aardvark.txt").unwrap(), true),
1746        ];
1747        paths.sort_by(|&a, &b| compare_rel_paths_files_first(a, b));
1748        assert_eq!(
1749            paths,
1750            vec![
1751                (RelPath::unix("Aardvark.txt").unwrap(), true),
1752                (RelPath::unix("Banana.rs").unwrap(), true),
1753                (RelPath::unix("Zebra.txt").unwrap(), true),
1754                (RelPath::unix("apple").unwrap(), false),
1755                (RelPath::unix("carrot").unwrap(), false),
1756            ]
1757        );
1758    }
1759
1760    #[perf]
1761    fn compare_rel_paths_files_first_numeric() {
1762        // Test natural number sorting with files first
1763        let mut paths = vec![
1764            (RelPath::unix("file10.txt").unwrap(), true),
1765            (RelPath::unix("dir2").unwrap(), false),
1766            (RelPath::unix("file2.txt").unwrap(), true),
1767            (RelPath::unix("dir10").unwrap(), false),
1768            (RelPath::unix("file1.txt").unwrap(), true),
1769        ];
1770        paths.sort_by(|&a, &b| compare_rel_paths_files_first(a, b));
1771        assert_eq!(
1772            paths,
1773            vec![
1774                (RelPath::unix("file1.txt").unwrap(), true),
1775                (RelPath::unix("file2.txt").unwrap(), true),
1776                (RelPath::unix("file10.txt").unwrap(), true),
1777                (RelPath::unix("dir2").unwrap(), false),
1778                (RelPath::unix("dir10").unwrap(), false),
1779            ]
1780        );
1781    }
1782
1783    #[perf]
1784    fn compare_rel_paths_mixed_case() {
1785        // Test case-insensitive sorting with varied capitalization
1786        let mut paths = vec![
1787            (RelPath::unix("README.md").unwrap(), true),
1788            (RelPath::unix("readme.txt").unwrap(), true),
1789            (RelPath::unix("ReadMe.rs").unwrap(), true),
1790        ];
1791        paths.sort_by(|&a, &b| compare_rel_paths_mixed(a, b));
1792        // All "readme" variants should group together, sorted by extension
1793        assert_eq!(
1794            paths,
1795            vec![
1796                (RelPath::unix("readme.txt").unwrap(), true),
1797                (RelPath::unix("ReadMe.rs").unwrap(), true),
1798                (RelPath::unix("README.md").unwrap(), true),
1799            ]
1800        );
1801    }
1802
1803    #[perf]
1804    fn compare_rel_paths_mixed_files_and_dirs() {
1805        // Verify directories and files are still mixed
1806        let mut paths = vec![
1807            (RelPath::unix("file2.txt").unwrap(), true),
1808            (RelPath::unix("Dir1").unwrap(), false),
1809            (RelPath::unix("file1.txt").unwrap(), true),
1810            (RelPath::unix("dir2").unwrap(), false),
1811        ];
1812        paths.sort_by(|&a, &b| compare_rel_paths_mixed(a, b));
1813        // Case-insensitive: dir1, dir2, file1, file2 (all mixed)
1814        assert_eq!(
1815            paths,
1816            vec![
1817                (RelPath::unix("Dir1").unwrap(), false),
1818                (RelPath::unix("dir2").unwrap(), false),
1819                (RelPath::unix("file1.txt").unwrap(), true),
1820                (RelPath::unix("file2.txt").unwrap(), true),
1821            ]
1822        );
1823    }
1824
1825    #[perf]
1826    fn compare_rel_paths_mixed_with_nested_paths() {
1827        // Test that nested paths still work correctly
1828        let mut paths = vec![
1829            (RelPath::unix("src/main.rs").unwrap(), true),
1830            (RelPath::unix("Cargo.toml").unwrap(), true),
1831            (RelPath::unix("src").unwrap(), false),
1832            (RelPath::unix("target").unwrap(), false),
1833        ];
1834        paths.sort_by(|&a, &b| compare_rel_paths_mixed(a, b));
1835        assert_eq!(
1836            paths,
1837            vec![
1838                (RelPath::unix("Cargo.toml").unwrap(), true),
1839                (RelPath::unix("src").unwrap(), false),
1840                (RelPath::unix("src/main.rs").unwrap(), true),
1841                (RelPath::unix("target").unwrap(), false),
1842            ]
1843        );
1844    }
1845
1846    #[perf]
1847    fn compare_rel_paths_files_first_with_nested() {
1848        // Files come before directories, even with nested paths
1849        let mut paths = vec![
1850            (RelPath::unix("src/lib.rs").unwrap(), true),
1851            (RelPath::unix("README.md").unwrap(), true),
1852            (RelPath::unix("src").unwrap(), false),
1853            (RelPath::unix("tests").unwrap(), false),
1854        ];
1855        paths.sort_by(|&a, &b| compare_rel_paths_files_first(a, b));
1856        assert_eq!(
1857            paths,
1858            vec![
1859                (RelPath::unix("README.md").unwrap(), true),
1860                (RelPath::unix("src").unwrap(), false),
1861                (RelPath::unix("src/lib.rs").unwrap(), true),
1862                (RelPath::unix("tests").unwrap(), false),
1863            ]
1864        );
1865    }
1866
1867    #[perf]
1868    fn compare_rel_paths_mixed_dotfiles() {
1869        // Test that dotfiles are handled correctly in mixed mode
1870        let mut paths = vec![
1871            (RelPath::unix(".gitignore").unwrap(), true),
1872            (RelPath::unix("README.md").unwrap(), true),
1873            (RelPath::unix(".github").unwrap(), false),
1874            (RelPath::unix("src").unwrap(), false),
1875        ];
1876        paths.sort_by(|&a, &b| compare_rel_paths_mixed(a, b));
1877        assert_eq!(
1878            paths,
1879            vec![
1880                (RelPath::unix(".github").unwrap(), false),
1881                (RelPath::unix(".gitignore").unwrap(), true),
1882                (RelPath::unix("README.md").unwrap(), true),
1883                (RelPath::unix("src").unwrap(), false),
1884            ]
1885        );
1886    }
1887
1888    #[perf]
1889    fn compare_rel_paths_files_first_dotfiles() {
1890        // Test that dotfiles come first when they're files
1891        let mut paths = vec![
1892            (RelPath::unix(".gitignore").unwrap(), true),
1893            (RelPath::unix("README.md").unwrap(), true),
1894            (RelPath::unix(".github").unwrap(), false),
1895            (RelPath::unix("src").unwrap(), false),
1896        ];
1897        paths.sort_by(|&a, &b| compare_rel_paths_files_first(a, b));
1898        assert_eq!(
1899            paths,
1900            vec![
1901                (RelPath::unix(".gitignore").unwrap(), true),
1902                (RelPath::unix("README.md").unwrap(), true),
1903                (RelPath::unix(".github").unwrap(), false),
1904                (RelPath::unix("src").unwrap(), false),
1905            ]
1906        );
1907    }
1908
1909    #[perf]
1910    fn compare_rel_paths_mixed_same_stem_different_extension() {
1911        // Files with same stem but different extensions should sort by extension
1912        let mut paths = vec![
1913            (RelPath::unix("file.rs").unwrap(), true),
1914            (RelPath::unix("file.md").unwrap(), true),
1915            (RelPath::unix("file.txt").unwrap(), true),
1916        ];
1917        paths.sort_by(|&a, &b| compare_rel_paths_mixed(a, b));
1918        assert_eq!(
1919            paths,
1920            vec![
1921                (RelPath::unix("file.txt").unwrap(), true),
1922                (RelPath::unix("file.rs").unwrap(), true),
1923                (RelPath::unix("file.md").unwrap(), true),
1924            ]
1925        );
1926    }
1927
1928    #[perf]
1929    fn compare_rel_paths_files_first_same_stem() {
1930        // Same stem files should still sort by extension with files_first
1931        let mut paths = vec![
1932            (RelPath::unix("main.rs").unwrap(), true),
1933            (RelPath::unix("main.c").unwrap(), true),
1934            (RelPath::unix("main").unwrap(), false),
1935        ];
1936        paths.sort_by(|&a, &b| compare_rel_paths_files_first(a, b));
1937        assert_eq!(
1938            paths,
1939            vec![
1940                (RelPath::unix("main.c").unwrap(), true),
1941                (RelPath::unix("main.rs").unwrap(), true),
1942                (RelPath::unix("main").unwrap(), false),
1943            ]
1944        );
1945    }
1946
1947    #[perf]
1948    fn compare_rel_paths_mixed_deep_nesting() {
1949        // Test sorting with deeply nested paths
1950        let mut paths = vec![
1951            (RelPath::unix("a/b/c.txt").unwrap(), true),
1952            (RelPath::unix("A/B.txt").unwrap(), true),
1953            (RelPath::unix("a.txt").unwrap(), true),
1954            (RelPath::unix("A.txt").unwrap(), true),
1955        ];
1956        paths.sort_by(|&a, &b| compare_rel_paths_mixed(a, b));
1957        assert_eq!(
1958            paths,
1959            vec![
1960                (RelPath::unix("A/B.txt").unwrap(), true),
1961                (RelPath::unix("a/b/c.txt").unwrap(), true),
1962                (RelPath::unix("a.txt").unwrap(), true),
1963                (RelPath::unix("A.txt").unwrap(), true),
1964            ]
1965        );
1966    }
1967
1968    #[perf]
1969    fn path_with_position_parse_posix_path() {
1970        // Test POSIX filename edge cases
1971        // Read more at https://en.wikipedia.org/wiki/Filename
1972        assert_eq!(
1973            PathWithPosition::parse_str("test_file"),
1974            PathWithPosition {
1975                path: PathBuf::from("test_file"),
1976                row: None,
1977                column: None
1978            }
1979        );
1980
1981        assert_eq!(
1982            PathWithPosition::parse_str("a:bc:.zip:1"),
1983            PathWithPosition {
1984                path: PathBuf::from("a:bc:.zip"),
1985                row: Some(1),
1986                column: None
1987            }
1988        );
1989
1990        assert_eq!(
1991            PathWithPosition::parse_str("one.second.zip:1"),
1992            PathWithPosition {
1993                path: PathBuf::from("one.second.zip"),
1994                row: Some(1),
1995                column: None
1996            }
1997        );
1998
1999        // Trim off trailing `:`s for otherwise valid input.
2000        assert_eq!(
2001            PathWithPosition::parse_str("test_file:10:1:"),
2002            PathWithPosition {
2003                path: PathBuf::from("test_file"),
2004                row: Some(10),
2005                column: Some(1)
2006            }
2007        );
2008
2009        assert_eq!(
2010            PathWithPosition::parse_str("test_file.rs:"),
2011            PathWithPosition {
2012                path: PathBuf::from("test_file.rs"),
2013                row: None,
2014                column: None
2015            }
2016        );
2017
2018        assert_eq!(
2019            PathWithPosition::parse_str("test_file.rs:1:"),
2020            PathWithPosition {
2021                path: PathBuf::from("test_file.rs"),
2022                row: Some(1),
2023                column: None
2024            }
2025        );
2026
2027        assert_eq!(
2028            PathWithPosition::parse_str("ab\ncd"),
2029            PathWithPosition {
2030                path: PathBuf::from("ab\ncd"),
2031                row: None,
2032                column: None
2033            }
2034        );
2035
2036        assert_eq!(
2037            PathWithPosition::parse_str("👋\nab"),
2038            PathWithPosition {
2039                path: PathBuf::from("👋\nab"),
2040                row: None,
2041                column: None
2042            }
2043        );
2044
2045        assert_eq!(
2046            PathWithPosition::parse_str("Types.hs:(617,9)-(670,28):"),
2047            PathWithPosition {
2048                path: PathBuf::from("Types.hs"),
2049                row: Some(617),
2050                column: Some(9),
2051            }
2052        );
2053    }
2054
2055    #[perf]
2056    #[cfg(not(target_os = "windows"))]
2057    fn path_with_position_parse_posix_path_with_suffix() {
2058        assert_eq!(
2059            PathWithPosition::parse_str("foo/bar:34:in"),
2060            PathWithPosition {
2061                path: PathBuf::from("foo/bar"),
2062                row: Some(34),
2063                column: None,
2064            }
2065        );
2066        assert_eq!(
2067            PathWithPosition::parse_str("foo/bar.rs:1902:::15:"),
2068            PathWithPosition {
2069                path: PathBuf::from("foo/bar.rs:1902"),
2070                row: Some(15),
2071                column: None
2072            }
2073        );
2074
2075        assert_eq!(
2076            PathWithPosition::parse_str("app-editors:zed-0.143.6:20240710-201212.log:34:"),
2077            PathWithPosition {
2078                path: PathBuf::from("app-editors:zed-0.143.6:20240710-201212.log"),
2079                row: Some(34),
2080                column: None,
2081            }
2082        );
2083
2084        assert_eq!(
2085            PathWithPosition::parse_str("crates/file_finder/src/file_finder.rs:1902:13:"),
2086            PathWithPosition {
2087                path: PathBuf::from("crates/file_finder/src/file_finder.rs"),
2088                row: Some(1902),
2089                column: Some(13),
2090            }
2091        );
2092
2093        assert_eq!(
2094            PathWithPosition::parse_str("crate/utils/src/test:today.log:34"),
2095            PathWithPosition {
2096                path: PathBuf::from("crate/utils/src/test:today.log"),
2097                row: Some(34),
2098                column: None,
2099            }
2100        );
2101        assert_eq!(
2102            PathWithPosition::parse_str("/testing/out/src/file_finder.odin(7:15)"),
2103            PathWithPosition {
2104                path: PathBuf::from("/testing/out/src/file_finder.odin"),
2105                row: Some(7),
2106                column: Some(15),
2107            }
2108        );
2109    }
2110
2111    #[perf]
2112    #[cfg(target_os = "windows")]
2113    fn path_with_position_parse_windows_path() {
2114        assert_eq!(
2115            PathWithPosition::parse_str("crates\\utils\\paths.rs"),
2116            PathWithPosition {
2117                path: PathBuf::from("crates\\utils\\paths.rs"),
2118                row: None,
2119                column: None
2120            }
2121        );
2122
2123        assert_eq!(
2124            PathWithPosition::parse_str("C:\\Users\\someone\\test_file.rs"),
2125            PathWithPosition {
2126                path: PathBuf::from("C:\\Users\\someone\\test_file.rs"),
2127                row: None,
2128                column: None
2129            }
2130        );
2131    }
2132
2133    #[perf]
2134    #[cfg(target_os = "windows")]
2135    fn path_with_position_parse_windows_path_with_suffix() {
2136        assert_eq!(
2137            PathWithPosition::parse_str("crates\\utils\\paths.rs:101"),
2138            PathWithPosition {
2139                path: PathBuf::from("crates\\utils\\paths.rs"),
2140                row: Some(101),
2141                column: None
2142            }
2143        );
2144
2145        assert_eq!(
2146            PathWithPosition::parse_str("\\\\?\\C:\\Users\\someone\\test_file.rs:1:20"),
2147            PathWithPosition {
2148                path: PathBuf::from("\\\\?\\C:\\Users\\someone\\test_file.rs"),
2149                row: Some(1),
2150                column: Some(20)
2151            }
2152        );
2153
2154        assert_eq!(
2155            PathWithPosition::parse_str("C:\\Users\\someone\\test_file.rs(1902,13)"),
2156            PathWithPosition {
2157                path: PathBuf::from("C:\\Users\\someone\\test_file.rs"),
2158                row: Some(1902),
2159                column: Some(13)
2160            }
2161        );
2162
2163        // Trim off trailing `:`s for otherwise valid input.
2164        assert_eq!(
2165            PathWithPosition::parse_str("\\\\?\\C:\\Users\\someone\\test_file.rs:1902:13:"),
2166            PathWithPosition {
2167                path: PathBuf::from("\\\\?\\C:\\Users\\someone\\test_file.rs"),
2168                row: Some(1902),
2169                column: Some(13)
2170            }
2171        );
2172
2173        assert_eq!(
2174            PathWithPosition::parse_str("\\\\?\\C:\\Users\\someone\\test_file.rs:1902:13:15:"),
2175            PathWithPosition {
2176                path: PathBuf::from("\\\\?\\C:\\Users\\someone\\test_file.rs:1902"),
2177                row: Some(13),
2178                column: Some(15)
2179            }
2180        );
2181
2182        assert_eq!(
2183            PathWithPosition::parse_str("\\\\?\\C:\\Users\\someone\\test_file.rs:1902:::15:"),
2184            PathWithPosition {
2185                path: PathBuf::from("\\\\?\\C:\\Users\\someone\\test_file.rs:1902"),
2186                row: Some(15),
2187                column: None
2188            }
2189        );
2190
2191        assert_eq!(
2192            PathWithPosition::parse_str("\\\\?\\C:\\Users\\someone\\test_file.rs(1902,13):"),
2193            PathWithPosition {
2194                path: PathBuf::from("\\\\?\\C:\\Users\\someone\\test_file.rs"),
2195                row: Some(1902),
2196                column: Some(13),
2197            }
2198        );
2199
2200        assert_eq!(
2201            PathWithPosition::parse_str("\\\\?\\C:\\Users\\someone\\test_file.rs(1902):"),
2202            PathWithPosition {
2203                path: PathBuf::from("\\\\?\\C:\\Users\\someone\\test_file.rs"),
2204                row: Some(1902),
2205                column: None,
2206            }
2207        );
2208
2209        assert_eq!(
2210            PathWithPosition::parse_str("C:\\Users\\someone\\test_file.rs:1902:13:"),
2211            PathWithPosition {
2212                path: PathBuf::from("C:\\Users\\someone\\test_file.rs"),
2213                row: Some(1902),
2214                column: Some(13),
2215            }
2216        );
2217
2218        assert_eq!(
2219            PathWithPosition::parse_str("C:\\Users\\someone\\test_file.rs(1902,13):"),
2220            PathWithPosition {
2221                path: PathBuf::from("C:\\Users\\someone\\test_file.rs"),
2222                row: Some(1902),
2223                column: Some(13),
2224            }
2225        );
2226
2227        assert_eq!(
2228            PathWithPosition::parse_str("C:\\Users\\someone\\test_file.rs(1902):"),
2229            PathWithPosition {
2230                path: PathBuf::from("C:\\Users\\someone\\test_file.rs"),
2231                row: Some(1902),
2232                column: None,
2233            }
2234        );
2235
2236        assert_eq!(
2237            PathWithPosition::parse_str("crates/utils/paths.rs:101"),
2238            PathWithPosition {
2239                path: PathBuf::from("crates\\utils\\paths.rs"),
2240                row: Some(101),
2241                column: None,
2242            }
2243        );
2244    }
2245
2246    #[perf]
2247    fn test_path_compact() {
2248        let path: PathBuf = [
2249            home_dir().to_string_lossy().into_owned(),
2250            "some_file.txt".to_string(),
2251        ]
2252        .iter()
2253        .collect();
2254        if cfg!(any(target_os = "linux", target_os = "freebsd")) || cfg!(target_os = "macos") {
2255            assert_eq!(path.compact().to_str(), Some("~/some_file.txt"));
2256        } else {
2257            assert_eq!(path.compact().to_str(), path.to_str());
2258        }
2259    }
2260
2261    #[perf]
2262    fn test_extension_or_hidden_file_name() {
2263        // No dots in name
2264        let path = Path::new("/a/b/c/file_name.rs");
2265        assert_eq!(path.extension_or_hidden_file_name(), Some("rs"));
2266
2267        // Single dot in name
2268        let path = Path::new("/a/b/c/file.name.rs");
2269        assert_eq!(path.extension_or_hidden_file_name(), Some("rs"));
2270
2271        // Multiple dots in name
2272        let path = Path::new("/a/b/c/long.file.name.rs");
2273        assert_eq!(path.extension_or_hidden_file_name(), Some("rs"));
2274
2275        // Hidden file, no extension
2276        let path = Path::new("/a/b/c/.gitignore");
2277        assert_eq!(path.extension_or_hidden_file_name(), Some("gitignore"));
2278
2279        // Hidden file, with extension
2280        let path = Path::new("/a/b/c/.eslintrc.js");
2281        assert_eq!(path.extension_or_hidden_file_name(), Some("eslintrc.js"));
2282    }
2283
2284    #[perf]
2285    // fn edge_of_glob() {
2286    //     let path = Path::new("/work/node_modules");
2287    //     let path_matcher =
2288    //         PathMatcher::new(&["**/node_modules/**".to_owned()], PathStyle::Posix).unwrap();
2289    //     assert!(
2290    //         path_matcher.is_match(path),
2291    //         "Path matcher should match {path:?}"
2292    //     );
2293    // }
2294
2295    // #[perf]
2296    // fn file_in_dirs() {
2297    //     let path = Path::new("/work/.env");
2298    //     let path_matcher = PathMatcher::new(&["**/.env".to_owned()], PathStyle::Posix).unwrap();
2299    //     assert!(
2300    //         path_matcher.is_match(path),
2301    //         "Path matcher should match {path:?}"
2302    //     );
2303    //     let path = Path::new("/work/package.json");
2304    //     assert!(
2305    //         !path_matcher.is_match(path),
2306    //         "Path matcher should not match {path:?}"
2307    //     );
2308    // }
2309
2310    // #[perf]
2311    // fn project_search() {
2312    //     let path = Path::new("/Users/someonetoignore/work/zed/zed.dev/node_modules");
2313    //     let path_matcher =
2314    //         PathMatcher::new(&["**/node_modules/**".to_owned()], PathStyle::Posix).unwrap();
2315    //     assert!(
2316    //         path_matcher.is_match(path),
2317    //         "Path matcher should match {path:?}"
2318    //     );
2319    // }
2320    #[perf]
2321    #[cfg(target_os = "windows")]
2322    fn test_sanitized_path() {
2323        let path = Path::new("C:\\Users\\someone\\test_file.rs");
2324        let sanitized_path = SanitizedPath::new(path);
2325        assert_eq!(
2326            sanitized_path.to_string(),
2327            "C:\\Users\\someone\\test_file.rs"
2328        );
2329
2330        let path = Path::new("\\\\?\\C:\\Users\\someone\\test_file.rs");
2331        let sanitized_path = SanitizedPath::new(path);
2332        assert_eq!(
2333            sanitized_path.to_string(),
2334            "C:\\Users\\someone\\test_file.rs"
2335        );
2336    }
2337
2338    #[perf]
2339    fn test_compare_numeric_segments() {
2340        // Helper function to create peekable iterators and test
2341        fn compare(a: &str, b: &str) -> Ordering {
2342            let mut a_iter = a.chars().peekable();
2343            let mut b_iter = b.chars().peekable();
2344
2345            let result = compare_numeric_segments(&mut a_iter, &mut b_iter);
2346
2347            // Verify iterators advanced correctly
2348            assert!(
2349                !a_iter.next().is_some_and(|c| c.is_ascii_digit()),
2350                "Iterator a should have consumed all digits"
2351            );
2352            assert!(
2353                !b_iter.next().is_some_and(|c| c.is_ascii_digit()),
2354                "Iterator b should have consumed all digits"
2355            );
2356
2357            result
2358        }
2359
2360        // Basic numeric comparisons
2361        assert_eq!(compare("0", "0"), Ordering::Equal);
2362        assert_eq!(compare("1", "2"), Ordering::Less);
2363        assert_eq!(compare("9", "10"), Ordering::Less);
2364        assert_eq!(compare("10", "9"), Ordering::Greater);
2365        assert_eq!(compare("99", "100"), Ordering::Less);
2366
2367        // Leading zeros
2368        assert_eq!(compare("0", "00"), Ordering::Less);
2369        assert_eq!(compare("00", "0"), Ordering::Greater);
2370        assert_eq!(compare("01", "1"), Ordering::Greater);
2371        assert_eq!(compare("001", "1"), Ordering::Greater);
2372        assert_eq!(compare("001", "01"), Ordering::Greater);
2373
2374        // Same value different representation
2375        assert_eq!(compare("000100", "100"), Ordering::Greater);
2376        assert_eq!(compare("100", "0100"), Ordering::Less);
2377        assert_eq!(compare("0100", "00100"), Ordering::Less);
2378
2379        // Large numbers
2380        assert_eq!(compare("9999999999", "10000000000"), Ordering::Less);
2381        assert_eq!(
2382            compare(
2383                "340282366920938463463374607431768211455", // u128::MAX
2384                "340282366920938463463374607431768211456"
2385            ),
2386            Ordering::Less
2387        );
2388        assert_eq!(
2389            compare(
2390                "340282366920938463463374607431768211456", // > u128::MAX
2391                "340282366920938463463374607431768211455"
2392            ),
2393            Ordering::Greater
2394        );
2395
2396        // Iterator advancement verification
2397        let mut a_iter = "123abc".chars().peekable();
2398        let mut b_iter = "456def".chars().peekable();
2399
2400        compare_numeric_segments(&mut a_iter, &mut b_iter);
2401
2402        assert_eq!(a_iter.collect::<String>(), "abc");
2403        assert_eq!(b_iter.collect::<String>(), "def");
2404    }
2405
2406    #[perf]
2407    fn test_natural_sort() {
2408        // Basic alphanumeric
2409        assert_eq!(natural_sort("a", "b"), Ordering::Less);
2410        assert_eq!(natural_sort("b", "a"), Ordering::Greater);
2411        assert_eq!(natural_sort("a", "a"), Ordering::Equal);
2412
2413        // Case sensitivity
2414        assert_eq!(natural_sort("a", "A"), Ordering::Less);
2415        assert_eq!(natural_sort("A", "a"), Ordering::Greater);
2416        assert_eq!(natural_sort("aA", "aa"), Ordering::Greater);
2417        assert_eq!(natural_sort("aa", "aA"), Ordering::Less);
2418
2419        // Numbers
2420        assert_eq!(natural_sort("1", "2"), Ordering::Less);
2421        assert_eq!(natural_sort("2", "10"), Ordering::Less);
2422        assert_eq!(natural_sort("02", "10"), Ordering::Less);
2423        assert_eq!(natural_sort("02", "2"), Ordering::Greater);
2424
2425        // Mixed alphanumeric
2426        assert_eq!(natural_sort("a1", "a2"), Ordering::Less);
2427        assert_eq!(natural_sort("a2", "a10"), Ordering::Less);
2428        assert_eq!(natural_sort("a02", "a2"), Ordering::Greater);
2429        assert_eq!(natural_sort("a1b", "a1c"), Ordering::Less);
2430
2431        // Multiple numeric segments
2432        assert_eq!(natural_sort("1a2", "1a10"), Ordering::Less);
2433        assert_eq!(natural_sort("1a10", "1a2"), Ordering::Greater);
2434        assert_eq!(natural_sort("2a1", "10a1"), Ordering::Less);
2435
2436        // Special characters
2437        assert_eq!(natural_sort("a-1", "a-2"), Ordering::Less);
2438        assert_eq!(natural_sort("a_1", "a_2"), Ordering::Less);
2439        assert_eq!(natural_sort("a.1", "a.2"), Ordering::Less);
2440
2441        // Unicode
2442        assert_eq!(natural_sort("文1", "文2"), Ordering::Less);
2443        assert_eq!(natural_sort("文2", "文10"), Ordering::Less);
2444        assert_eq!(natural_sort("🔤1", "🔤2"), Ordering::Less);
2445
2446        // Empty and special cases
2447        assert_eq!(natural_sort("", ""), Ordering::Equal);
2448        assert_eq!(natural_sort("", "a"), Ordering::Less);
2449        assert_eq!(natural_sort("a", ""), Ordering::Greater);
2450        assert_eq!(natural_sort(" ", "  "), Ordering::Less);
2451
2452        // Mixed everything
2453        assert_eq!(natural_sort("File-1.txt", "File-2.txt"), Ordering::Less);
2454        assert_eq!(natural_sort("File-02.txt", "File-2.txt"), Ordering::Greater);
2455        assert_eq!(natural_sort("File-2.txt", "File-10.txt"), Ordering::Less);
2456        assert_eq!(natural_sort("File_A1", "File_A2"), Ordering::Less);
2457        assert_eq!(natural_sort("File_a1", "File_A1"), Ordering::Less);
2458    }
2459
2460    #[perf]
2461    fn test_compare_paths() {
2462        // Helper function for cleaner tests
2463        fn compare(a: &str, is_a_file: bool, b: &str, is_b_file: bool) -> Ordering {
2464            compare_paths((Path::new(a), is_a_file), (Path::new(b), is_b_file))
2465        }
2466
2467        // Basic path comparison
2468        assert_eq!(compare("a", true, "b", true), Ordering::Less);
2469        assert_eq!(compare("b", true, "a", true), Ordering::Greater);
2470        assert_eq!(compare("a", true, "a", true), Ordering::Equal);
2471
2472        // Files vs Directories
2473        assert_eq!(compare("a", true, "a", false), Ordering::Greater);
2474        assert_eq!(compare("a", false, "a", true), Ordering::Less);
2475        assert_eq!(compare("b", false, "a", true), Ordering::Less);
2476
2477        // Extensions
2478        assert_eq!(compare("a.txt", true, "a.md", true), Ordering::Greater);
2479        assert_eq!(compare("a.md", true, "a.txt", true), Ordering::Less);
2480        assert_eq!(compare("a", true, "a.txt", true), Ordering::Less);
2481
2482        // Nested paths
2483        assert_eq!(compare("dir/a", true, "dir/b", true), Ordering::Less);
2484        assert_eq!(compare("dir1/a", true, "dir2/a", true), Ordering::Less);
2485        assert_eq!(compare("dir/sub/a", true, "dir/a", true), Ordering::Less);
2486
2487        // Case sensitivity in paths
2488        assert_eq!(
2489            compare("Dir/file", true, "dir/file", true),
2490            Ordering::Greater
2491        );
2492        assert_eq!(
2493            compare("dir/File", true, "dir/file", true),
2494            Ordering::Greater
2495        );
2496        assert_eq!(compare("dir/file", true, "Dir/File", true), Ordering::Less);
2497
2498        // Hidden files and special names
2499        assert_eq!(compare(".hidden", true, "visible", true), Ordering::Less);
2500        assert_eq!(compare("_special", true, "normal", true), Ordering::Less);
2501        assert_eq!(compare(".config", false, ".data", false), Ordering::Less);
2502
2503        // Mixed numeric paths
2504        assert_eq!(
2505            compare("dir1/file", true, "dir2/file", true),
2506            Ordering::Less
2507        );
2508        assert_eq!(
2509            compare("dir2/file", true, "dir10/file", true),
2510            Ordering::Less
2511        );
2512        assert_eq!(
2513            compare("dir02/file", true, "dir2/file", true),
2514            Ordering::Greater
2515        );
2516
2517        // Root paths
2518        assert_eq!(compare("/a", true, "/b", true), Ordering::Less);
2519        assert_eq!(compare("/", false, "/a", true), Ordering::Less);
2520
2521        // Complex real-world examples
2522        assert_eq!(
2523            compare("project/src/main.rs", true, "project/src/lib.rs", true),
2524            Ordering::Greater
2525        );
2526        assert_eq!(
2527            compare(
2528                "project/tests/test_1.rs",
2529                true,
2530                "project/tests/test_2.rs",
2531                true
2532            ),
2533            Ordering::Less
2534        );
2535        assert_eq!(
2536            compare(
2537                "project/v1.0.0/README.md",
2538                true,
2539                "project/v1.10.0/README.md",
2540                true
2541            ),
2542            Ordering::Less
2543        );
2544    }
2545
2546    #[perf]
2547    fn test_natural_sort_case_sensitivity() {
2548        std::thread::sleep(std::time::Duration::from_millis(100));
2549        // Same letter different case - lowercase should come first
2550        assert_eq!(natural_sort("a", "A"), Ordering::Less);
2551        assert_eq!(natural_sort("A", "a"), Ordering::Greater);
2552        assert_eq!(natural_sort("a", "a"), Ordering::Equal);
2553        assert_eq!(natural_sort("A", "A"), Ordering::Equal);
2554
2555        // Mixed case strings
2556        assert_eq!(natural_sort("aaa", "AAA"), Ordering::Less);
2557        assert_eq!(natural_sort("AAA", "aaa"), Ordering::Greater);
2558        assert_eq!(natural_sort("aAa", "AaA"), Ordering::Less);
2559
2560        // Different letters
2561        assert_eq!(natural_sort("a", "b"), Ordering::Less);
2562        assert_eq!(natural_sort("A", "b"), Ordering::Less);
2563        assert_eq!(natural_sort("a", "B"), Ordering::Less);
2564    }
2565
2566    #[perf]
2567    fn test_natural_sort_with_numbers() {
2568        // Basic number ordering
2569        assert_eq!(natural_sort("file1", "file2"), Ordering::Less);
2570        assert_eq!(natural_sort("file2", "file10"), Ordering::Less);
2571        assert_eq!(natural_sort("file10", "file2"), Ordering::Greater);
2572
2573        // Numbers in different positions
2574        assert_eq!(natural_sort("1file", "2file"), Ordering::Less);
2575        assert_eq!(natural_sort("file1text", "file2text"), Ordering::Less);
2576        assert_eq!(natural_sort("text1file", "text2file"), Ordering::Less);
2577
2578        // Multiple numbers in string
2579        assert_eq!(natural_sort("file1-2", "file1-10"), Ordering::Less);
2580        assert_eq!(natural_sort("2-1file", "10-1file"), Ordering::Less);
2581
2582        // Leading zeros
2583        assert_eq!(natural_sort("file002", "file2"), Ordering::Greater);
2584        assert_eq!(natural_sort("file002", "file10"), Ordering::Less);
2585
2586        // Very large numbers
2587        assert_eq!(
2588            natural_sort("file999999999999999999999", "file999999999999999999998"),
2589            Ordering::Greater
2590        );
2591
2592        // u128 edge cases
2593
2594        // Numbers near u128::MAX (340,282,366,920,938,463,463,374,607,431,768,211,455)
2595        assert_eq!(
2596            natural_sort(
2597                "file340282366920938463463374607431768211454",
2598                "file340282366920938463463374607431768211455"
2599            ),
2600            Ordering::Less
2601        );
2602
2603        // Equal length numbers that overflow u128
2604        assert_eq!(
2605            natural_sort(
2606                "file340282366920938463463374607431768211456",
2607                "file340282366920938463463374607431768211455"
2608            ),
2609            Ordering::Greater
2610        );
2611
2612        // Different length numbers that overflow u128
2613        assert_eq!(
2614            natural_sort(
2615                "file3402823669209384634633746074317682114560",
2616                "file340282366920938463463374607431768211455"
2617            ),
2618            Ordering::Greater
2619        );
2620
2621        // Leading zeros with numbers near u128::MAX
2622        assert_eq!(
2623            natural_sort(
2624                "file0340282366920938463463374607431768211455",
2625                "file340282366920938463463374607431768211455"
2626            ),
2627            Ordering::Greater
2628        );
2629
2630        // Very large numbers with different lengths (both overflow u128)
2631        assert_eq!(
2632            natural_sort(
2633                "file999999999999999999999999999999999999999999999999",
2634                "file9999999999999999999999999999999999999999999999999"
2635            ),
2636            Ordering::Less
2637        );
2638    }
2639
2640    #[perf]
2641    fn test_natural_sort_case_sensitive() {
2642        // Numerically smaller values come first.
2643        assert_eq!(natural_sort("File1", "file2"), Ordering::Less);
2644        assert_eq!(natural_sort("file1", "File2"), Ordering::Less);
2645
2646        // Numerically equal values: the case-insensitive comparison decides first.
2647        // Case-sensitive comparison only occurs when both are equal case-insensitively.
2648        assert_eq!(natural_sort("Dir1", "dir01"), Ordering::Less);
2649        assert_eq!(natural_sort("dir2", "Dir02"), Ordering::Less);
2650        assert_eq!(natural_sort("dir2", "dir02"), Ordering::Less);
2651
2652        // Numerically equal and case-insensitively equal:
2653        // the lexicographically smaller (case-sensitive) one wins.
2654        assert_eq!(natural_sort("dir1", "Dir1"), Ordering::Less);
2655        assert_eq!(natural_sort("dir02", "Dir02"), Ordering::Less);
2656        assert_eq!(natural_sort("dir10", "Dir10"), Ordering::Less);
2657    }
2658
2659    #[perf]
2660    fn test_natural_sort_edge_cases() {
2661        // Empty strings
2662        assert_eq!(natural_sort("", ""), Ordering::Equal);
2663        assert_eq!(natural_sort("", "a"), Ordering::Less);
2664        assert_eq!(natural_sort("a", ""), Ordering::Greater);
2665
2666        // Special characters
2667        assert_eq!(natural_sort("file-1", "file_1"), Ordering::Less);
2668        assert_eq!(natural_sort("file.1", "file_1"), Ordering::Less);
2669        assert_eq!(natural_sort("file 1", "file_1"), Ordering::Less);
2670
2671        // Unicode characters
2672        // 9312 vs 9313
2673        assert_eq!(natural_sort("file①", "file②"), Ordering::Less);
2674        // 9321 vs 9313
2675        assert_eq!(natural_sort("file⑩", "file②"), Ordering::Greater);
2676        // 28450 vs 23383
2677        assert_eq!(natural_sort("file漢", "file字"), Ordering::Greater);
2678
2679        // Mixed alphanumeric with special chars
2680        assert_eq!(natural_sort("file-1a", "file-1b"), Ordering::Less);
2681        assert_eq!(natural_sort("file-1.2", "file-1.10"), Ordering::Less);
2682        assert_eq!(natural_sort("file-1.10", "file-1.2"), Ordering::Greater);
2683    }
2684
2685    #[test]
2686    fn test_multiple_extensions() {
2687        // No extensions
2688        let path = Path::new("/a/b/c/file_name");
2689        assert_eq!(path.multiple_extensions(), None);
2690
2691        // Only one extension
2692        let path = Path::new("/a/b/c/file_name.tsx");
2693        assert_eq!(path.multiple_extensions(), None);
2694
2695        // Stories sample extension
2696        let path = Path::new("/a/b/c/file_name.stories.tsx");
2697        assert_eq!(path.multiple_extensions(), Some("stories.tsx".to_string()));
2698
2699        // Longer sample extension
2700        let path = Path::new("/a/b/c/long.app.tar.gz");
2701        assert_eq!(path.multiple_extensions(), Some("app.tar.gz".to_string()));
2702    }
2703
2704    #[test]
2705    fn test_strip_path_suffix() {
2706        let base = Path::new("/a/b/c/file_name");
2707        let suffix = Path::new("file_name");
2708        assert_eq!(strip_path_suffix(base, suffix), Some(Path::new("/a/b/c")));
2709
2710        let base = Path::new("/a/b/c/file_name.tsx");
2711        let suffix = Path::new("file_name.tsx");
2712        assert_eq!(strip_path_suffix(base, suffix), Some(Path::new("/a/b/c")));
2713
2714        let base = Path::new("/a/b/c/file_name.stories.tsx");
2715        let suffix = Path::new("c/file_name.stories.tsx");
2716        assert_eq!(strip_path_suffix(base, suffix), Some(Path::new("/a/b")));
2717
2718        let base = Path::new("/a/b/c/long.app.tar.gz");
2719        let suffix = Path::new("b/c/long.app.tar.gz");
2720        assert_eq!(strip_path_suffix(base, suffix), Some(Path::new("/a")));
2721
2722        let base = Path::new("/a/b/c/long.app.tar.gz");
2723        let suffix = Path::new("/a/b/c/long.app.tar.gz");
2724        assert_eq!(strip_path_suffix(base, suffix), Some(Path::new("")));
2725
2726        let base = Path::new("/a/b/c/long.app.tar.gz");
2727        let suffix = Path::new("/a/b/c/no_match.app.tar.gz");
2728        assert_eq!(strip_path_suffix(base, suffix), None);
2729
2730        let base = Path::new("/a/b/c/long.app.tar.gz");
2731        let suffix = Path::new("app.tar.gz");
2732        assert_eq!(strip_path_suffix(base, suffix), None);
2733    }
2734
2735    #[test]
2736    fn test_strip_prefix() {
2737        let expected = [
2738            (
2739                PathStyle::Posix,
2740                "/a/b/c",
2741                "/a/b",
2742                Some(rel_path("c").into_arc()),
2743            ),
2744            (
2745                PathStyle::Posix,
2746                "/a/b/c",
2747                "/a/b/",
2748                Some(rel_path("c").into_arc()),
2749            ),
2750            (
2751                PathStyle::Posix,
2752                "/a/b/c",
2753                "/",
2754                Some(rel_path("a/b/c").into_arc()),
2755            ),
2756            (PathStyle::Posix, "/a/b/c", "", None),
2757            (PathStyle::Posix, "/a/b//c", "/a/b/", None),
2758            (PathStyle::Posix, "/a/bc", "/a/b", None),
2759            (
2760                PathStyle::Posix,
2761                "/a/b/c",
2762                "/a/b/c",
2763                Some(rel_path("").into_arc()),
2764            ),
2765            (
2766                PathStyle::Windows,
2767                "C:\\a\\b\\c",
2768                "C:\\a\\b",
2769                Some(rel_path("c").into_arc()),
2770            ),
2771            (
2772                PathStyle::Windows,
2773                "C:\\a\\b\\c",
2774                "C:\\a\\b\\",
2775                Some(rel_path("c").into_arc()),
2776            ),
2777            (
2778                PathStyle::Windows,
2779                "C:\\a\\b\\c",
2780                "C:\\",
2781                Some(rel_path("a/b/c").into_arc()),
2782            ),
2783            (PathStyle::Windows, "C:\\a\\b\\c", "", None),
2784            (PathStyle::Windows, "C:\\a\\b\\\\c", "C:\\a\\b\\", None),
2785            (PathStyle::Windows, "C:\\a\\bc", "C:\\a\\b", None),
2786            (
2787                PathStyle::Windows,
2788                "C:\\a\\b/c",
2789                "C:\\a\\b",
2790                Some(rel_path("c").into_arc()),
2791            ),
2792            (
2793                PathStyle::Windows,
2794                "C:\\a\\b/c",
2795                "C:\\a\\b\\",
2796                Some(rel_path("c").into_arc()),
2797            ),
2798            (
2799                PathStyle::Windows,
2800                "C:\\a\\b/c",
2801                "C:\\a\\b/",
2802                Some(rel_path("c").into_arc()),
2803            ),
2804        ];
2805        let actual = expected.clone().map(|(style, child, parent, _)| {
2806            (
2807                style,
2808                child,
2809                parent,
2810                style
2811                    .strip_prefix(child.as_ref(), parent.as_ref())
2812                    .map(|rel_path| rel_path.into_arc()),
2813            )
2814        });
2815        pretty_assertions::assert_eq!(actual, expected);
2816    }
2817
2818    #[cfg(target_os = "windows")]
2819    #[test]
2820    fn test_wsl_path() {
2821        use super::WslPath;
2822        let path = "/a/b/c";
2823        assert_eq!(WslPath::from_path(&path), None);
2824
2825        let path = r"\\wsl.localhost";
2826        assert_eq!(WslPath::from_path(&path), None);
2827
2828        let path = r"\\wsl.localhost\Distro";
2829        assert_eq!(
2830            WslPath::from_path(&path),
2831            Some(WslPath {
2832                distro: "Distro".to_owned(),
2833                path: "/".into(),
2834            })
2835        );
2836
2837        let path = r"\\wsl.localhost\Distro\blue";
2838        assert_eq!(
2839            WslPath::from_path(&path),
2840            Some(WslPath {
2841                distro: "Distro".to_owned(),
2842                path: "/blue".into()
2843            })
2844        );
2845
2846        let path = r"\\wsl$\archlinux\tomato\.\paprika\..\aubergine.txt";
2847        assert_eq!(
2848            WslPath::from_path(&path),
2849            Some(WslPath {
2850                distro: "archlinux".to_owned(),
2851                path: "/tomato/paprika/../aubergine.txt".into()
2852            })
2853        );
2854
2855        let path = r"\\windows.localhost\Distro\foo";
2856        assert_eq!(WslPath::from_path(&path), None);
2857    }
2858
2859    #[test]
2860    fn test_url_to_file_path_ext_posix_basic() {
2861        use super::UrlExt;
2862
2863        let url = url::Url::parse("file:///home/user/file.txt").unwrap();
2864        assert_eq!(
2865            url.to_file_path_ext(PathStyle::Posix),
2866            Ok(PathBuf::from("/home/user/file.txt"))
2867        );
2868
2869        let url = url::Url::parse("file:///").unwrap();
2870        assert_eq!(
2871            url.to_file_path_ext(PathStyle::Posix),
2872            Ok(PathBuf::from("/"))
2873        );
2874
2875        let url = url::Url::parse("file:///a/b/c/d/e").unwrap();
2876        assert_eq!(
2877            url.to_file_path_ext(PathStyle::Posix),
2878            Ok(PathBuf::from("/a/b/c/d/e"))
2879        );
2880    }
2881
2882    #[test]
2883    fn test_url_to_file_path_ext_posix_percent_encoding() {
2884        use super::UrlExt;
2885
2886        let url = url::Url::parse("file:///home/user/file%20with%20spaces.txt").unwrap();
2887        assert_eq!(
2888            url.to_file_path_ext(PathStyle::Posix),
2889            Ok(PathBuf::from("/home/user/file with spaces.txt"))
2890        );
2891
2892        let url = url::Url::parse("file:///path%2Fwith%2Fencoded%2Fslashes").unwrap();
2893        assert_eq!(
2894            url.to_file_path_ext(PathStyle::Posix),
2895            Ok(PathBuf::from("/path/with/encoded/slashes"))
2896        );
2897
2898        let url = url::Url::parse("file:///special%23chars%3F.txt").unwrap();
2899        assert_eq!(
2900            url.to_file_path_ext(PathStyle::Posix),
2901            Ok(PathBuf::from("/special#chars?.txt"))
2902        );
2903    }
2904
2905    #[test]
2906    fn test_url_to_file_path_ext_posix_localhost() {
2907        use super::UrlExt;
2908
2909        let url = url::Url::parse("file://localhost/home/user/file.txt").unwrap();
2910        assert_eq!(
2911            url.to_file_path_ext(PathStyle::Posix),
2912            Ok(PathBuf::from("/home/user/file.txt"))
2913        );
2914    }
2915
2916    #[test]
2917    fn test_url_to_file_path_ext_posix_rejects_host() {
2918        use super::UrlExt;
2919
2920        let url = url::Url::parse("file://somehost/home/user/file.txt").unwrap();
2921        assert_eq!(url.to_file_path_ext(PathStyle::Posix), Err(()));
2922    }
2923
2924    #[test]
2925    fn test_url_to_file_path_ext_posix_windows_drive_letter() {
2926        use super::UrlExt;
2927
2928        let url = url::Url::parse("file:///C:").unwrap();
2929        assert_eq!(
2930            url.to_file_path_ext(PathStyle::Posix),
2931            Ok(PathBuf::from("/C:/"))
2932        );
2933
2934        let url = url::Url::parse("file:///D|").unwrap();
2935        assert_eq!(
2936            url.to_file_path_ext(PathStyle::Posix),
2937            Ok(PathBuf::from("/D|/"))
2938        );
2939    }
2940
2941    #[test]
2942    fn test_url_to_file_path_ext_windows_basic() {
2943        use super::UrlExt;
2944
2945        let url = url::Url::parse("file:///C:/Users/user/file.txt").unwrap();
2946        assert_eq!(
2947            url.to_file_path_ext(PathStyle::Windows),
2948            Ok(PathBuf::from("C:\\Users\\user\\file.txt"))
2949        );
2950
2951        let url = url::Url::parse("file:///D:/folder/subfolder/file.rs").unwrap();
2952        assert_eq!(
2953            url.to_file_path_ext(PathStyle::Windows),
2954            Ok(PathBuf::from("D:\\folder\\subfolder\\file.rs"))
2955        );
2956
2957        let url = url::Url::parse("file:///C:/").unwrap();
2958        assert_eq!(
2959            url.to_file_path_ext(PathStyle::Windows),
2960            Ok(PathBuf::from("C:\\"))
2961        );
2962    }
2963
2964    #[test]
2965    fn test_url_to_file_path_ext_windows_encoded_drive_letter() {
2966        use super::UrlExt;
2967
2968        let url = url::Url::parse("file:///C%3A/Users/file.txt").unwrap();
2969        assert_eq!(
2970            url.to_file_path_ext(PathStyle::Windows),
2971            Ok(PathBuf::from("C:\\Users\\file.txt"))
2972        );
2973
2974        let url = url::Url::parse("file:///c%3a/Users/file.txt").unwrap();
2975        assert_eq!(
2976            url.to_file_path_ext(PathStyle::Windows),
2977            Ok(PathBuf::from("c:\\Users\\file.txt"))
2978        );
2979
2980        let url = url::Url::parse("file:///D%3A/folder/file.txt").unwrap();
2981        assert_eq!(
2982            url.to_file_path_ext(PathStyle::Windows),
2983            Ok(PathBuf::from("D:\\folder\\file.txt"))
2984        );
2985
2986        let url = url::Url::parse("file:///d%3A/folder/file.txt").unwrap();
2987        assert_eq!(
2988            url.to_file_path_ext(PathStyle::Windows),
2989            Ok(PathBuf::from("d:\\folder\\file.txt"))
2990        );
2991    }
2992
2993    #[test]
2994    fn test_url_to_file_path_ext_windows_unc_path() {
2995        use super::UrlExt;
2996
2997        let url = url::Url::parse("file://server/share/path/file.txt").unwrap();
2998        assert_eq!(
2999            url.to_file_path_ext(PathStyle::Windows),
3000            Ok(PathBuf::from("\\\\server\\share\\path\\file.txt"))
3001        );
3002
3003        let url = url::Url::parse("file://server/share").unwrap();
3004        assert_eq!(
3005            url.to_file_path_ext(PathStyle::Windows),
3006            Ok(PathBuf::from("\\\\server\\share"))
3007        );
3008    }
3009
3010    #[test]
3011    fn test_url_to_file_path_ext_windows_percent_encoding() {
3012        use super::UrlExt;
3013
3014        let url = url::Url::parse("file:///C:/Users/user/file%20with%20spaces.txt").unwrap();
3015        assert_eq!(
3016            url.to_file_path_ext(PathStyle::Windows),
3017            Ok(PathBuf::from("C:\\Users\\user\\file with spaces.txt"))
3018        );
3019
3020        let url = url::Url::parse("file:///C:/special%23chars%3F.txt").unwrap();
3021        assert_eq!(
3022            url.to_file_path_ext(PathStyle::Windows),
3023            Ok(PathBuf::from("C:\\special#chars?.txt"))
3024        );
3025    }
3026
3027    #[test]
3028    fn test_url_to_file_path_ext_windows_invalid_drive() {
3029        use super::UrlExt;
3030
3031        let url = url::Url::parse("file:///1:/path/file.txt").unwrap();
3032        assert_eq!(url.to_file_path_ext(PathStyle::Windows), Err(()));
3033
3034        let url = url::Url::parse("file:///CC:/path/file.txt").unwrap();
3035        assert_eq!(url.to_file_path_ext(PathStyle::Windows), Err(()));
3036
3037        let url = url::Url::parse("file:///C/path/file.txt").unwrap();
3038        assert_eq!(url.to_file_path_ext(PathStyle::Windows), Err(()));
3039
3040        let url = url::Url::parse("file:///invalid").unwrap();
3041        assert_eq!(url.to_file_path_ext(PathStyle::Windows), Err(()));
3042    }
3043
3044    #[test]
3045    fn test_url_to_file_path_ext_non_file_scheme() {
3046        use super::UrlExt;
3047
3048        let url = url::Url::parse("http://example.com/path").unwrap();
3049        assert_eq!(url.to_file_path_ext(PathStyle::Posix), Err(()));
3050        assert_eq!(url.to_file_path_ext(PathStyle::Windows), Err(()));
3051
3052        let url = url::Url::parse("https://example.com/path").unwrap();
3053        assert_eq!(url.to_file_path_ext(PathStyle::Posix), Err(()));
3054        assert_eq!(url.to_file_path_ext(PathStyle::Windows), Err(()));
3055    }
3056
3057    #[test]
3058    fn test_url_to_file_path_ext_windows_localhost() {
3059        use super::UrlExt;
3060
3061        let url = url::Url::parse("file://localhost/C:/Users/file.txt").unwrap();
3062        assert_eq!(
3063            url.to_file_path_ext(PathStyle::Windows),
3064            Ok(PathBuf::from("C:\\Users\\file.txt"))
3065        );
3066    }
3067}