Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T02:55:46.726Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

paths.rs

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