Skip to main content

util/
paths.rs

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