Skip to main content

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