Skip to main content

util/
paths.rs

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