Skip to main content

util/
paths.rs

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