paths.rs

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