paths.rs

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