Skip to main content

fc_gpui_util/
paths.rs

1use anyhow::Context;
2use globset::{Glob, GlobSet, GlobSetBuilder};
3use itertools::Itertools;
4use regex::Regex;
5use serde::{Deserialize, Serialize};
6use std::cmp::Ordering;
7use std::error::Error;
8use std::fmt::{Display, Formatter};
9use std::mem;
10use std::path::StripPrefixError;
11use std::sync::{Arc, OnceLock};
12use std::{
13    ffi::OsStr,
14    path::{Path, PathBuf},
15    sync::LazyLock,
16};
17
18use crate::rel_path::RelPath;
19
20static HOME_DIR: OnceLock<PathBuf> = OnceLock::new();
21
22/// Returns the path to the user's home directory.
23///
24/// This crate's own tests see a fixed fake path. Every other build, including
25/// builds with `test-support`, sees the real home directory.
26pub fn home_dir() -> &'static PathBuf {
27    HOME_DIR.get_or_init(|| {
28        if cfg!(test) {
29            if cfg!(target_os = "macos") {
30                PathBuf::from("/Users/zed")
31            } else if cfg!(target_os = "windows") {
32                PathBuf::from("C:\\Users\\zed")
33            } else {
34                PathBuf::from("/home/zed")
35            }
36        } else {
37            dirs::home_dir().expect("failed to determine home directory")
38        }
39    })
40}
41
42pub trait PathExt {
43    /// Compacts a given file path by replacing the user's home directory
44    /// prefix with a tilde (`~`).
45    ///
46    /// # Returns
47    ///
48    /// * A `PathBuf` containing the compacted file path. If the input path
49    ///   does not have the user's home directory prefix, or if we are not on
50    ///   Linux or macOS, the original path is returned unchanged.
51    fn compact(&self) -> PathBuf;
52
53    /// Returns a file's extension or, if the file is hidden, its name without the leading dot
54    fn extension_or_hidden_file_name(&self) -> Option<&str>;
55
56    fn try_from_bytes<'a>(bytes: &'a [u8]) -> anyhow::Result<Self>
57    where
58        Self: From<&'a Path>,
59    {
60        #[cfg(unix)]
61        {
62            use std::os::unix::prelude::OsStrExt;
63            Ok(Self::from(Path::new(OsStr::from_bytes(bytes))))
64        }
65        #[cfg(windows)]
66        {
67            use tendril::fmt::{Format, WTF8};
68            WTF8::validate(bytes)
69                .then(|| {
70                    // Safety: bytes are valid WTF-8 sequence.
71                    Self::from(Path::new(unsafe {
72                        OsStr::from_encoded_bytes_unchecked(bytes)
73                    }))
74                })
75                .with_context(|| format!("Invalid WTF-8 sequence: {bytes:?}"))
76        }
77    }
78
79    /// Converts a local path to one that can be used inside of WSL.
80    /// Returns `None` if the path cannot be converted into a WSL one (network share).
81    fn local_to_wsl(&self) -> Option<PathBuf>;
82
83    /// Returns a file's "full" joined collection of extensions, in the case where a file does not
84    /// just have a singular extension but instead has multiple (e.g File.tar.gz, Component.stories.tsx)
85    ///
86    /// Will provide back the extensions joined together such as tar.gz or stories.tsx
87    fn multiple_extensions(&self) -> Option<String>;
88
89    /// Try to make a shell-safe representation of the path.
90    ///
91    /// For Unix, the path is escaped to be safe for POSIX shells
92    fn try_shell_safe(&self) -> anyhow::Result<String>;
93}
94
95impl<T: AsRef<Path>> PathExt for T {
96    fn compact(&self) -> PathBuf {
97        if cfg!(any(target_os = "linux", target_os = "freebsd")) || cfg!(target_os = "macos") {
98            match self.as_ref().strip_prefix(home_dir().as_path()) {
99                Ok(relative_path) => {
100                    let mut shortened_path = PathBuf::new();
101                    shortened_path.push("~");
102                    shortened_path.push(relative_path);
103                    shortened_path
104                }
105                Err(_) => self.as_ref().to_path_buf(),
106            }
107        } else {
108            self.as_ref().to_path_buf()
109        }
110    }
111
112    fn extension_or_hidden_file_name(&self) -> Option<&str> {
113        let path = self.as_ref();
114        let file_name = path.file_name()?.to_str()?;
115        if file_name.starts_with('.') {
116            return file_name.strip_prefix('.');
117        }
118
119        path.extension()
120            .and_then(|e| e.to_str())
121            .or_else(|| path.file_stem()?.to_str())
122    }
123
124    fn local_to_wsl(&self) -> Option<PathBuf> {
125        // quite sketchy to convert this back to path at the end, but a lot of functions only accept paths
126        // todo: ideally rework them..?
127        let mut new_path = std::ffi::OsString::new();
128        for component in self.as_ref().components() {
129            match component {
130                std::path::Component::Prefix(prefix) => {
131                    let drive_letter = prefix.as_os_str().to_string_lossy().to_lowercase();
132                    let drive_letter = drive_letter.strip_suffix(':')?;
133
134                    new_path.push(format!("/mnt/{}", drive_letter));
135                }
136                std::path::Component::RootDir => {}
137                std::path::Component::CurDir => {
138                    new_path.push("/.");
139                }
140                std::path::Component::ParentDir => {
141                    new_path.push("/..");
142                }
143                std::path::Component::Normal(os_str) => {
144                    new_path.push("/");
145                    new_path.push(os_str);
146                }
147            }
148        }
149
150        Some(new_path.into())
151    }
152
153    fn multiple_extensions(&self) -> Option<String> {
154        let path = self.as_ref();
155        let file_name = path.file_name()?.to_str()?;
156
157        let parts: Vec<&str> = file_name
158            .split('.')
159            // Skip the part with the file name extension
160            .skip(1)
161            .collect();
162
163        if parts.len() < 2 {
164            return None;
165        }
166
167        Some(parts.into_iter().join("."))
168    }
169
170    fn try_shell_safe(&self) -> anyhow::Result<String> {
171        #[cfg(target_os = "windows")]
172        {
173            Ok(self.as_ref().to_string_lossy().to_string())
174        }
175
176        #[cfg(not(target_os = "windows"))]
177        {
178            let path_str = self
179                .as_ref()
180                .to_str()
181                .with_context(|| "Path contains invalid UTF-8")?;
182
183            // As of writing, this can only be fail if the path contains a null byte, which shouldn't be possible
184            // but shlex has annotated the error as #[non_exhaustive] so we can't make it a compile error if other
185            // errors are introduced in the future :(
186            Ok(shlex::try_quote(path_str)?.into_owned())
187        }
188    }
189}
190
191pub fn path_ends_with(base: &Path, suffix: &Path) -> bool {
192    strip_path_suffix(base, suffix).is_some()
193}
194
195pub fn strip_path_suffix<'a>(base: &'a Path, suffix: &Path) -> Option<&'a Path> {
196    if let Some(remainder) = base
197        .as_os_str()
198        .as_encoded_bytes()
199        .strip_suffix(suffix.as_os_str().as_encoded_bytes())
200    {
201        if remainder
202            .last()
203            .is_none_or(|last_byte| std::path::is_separator(*last_byte as char))
204        {
205            let os_str = unsafe {
206                OsStr::from_encoded_bytes_unchecked(
207                    &remainder[0..remainder.len().saturating_sub(1)],
208                )
209            };
210            return Some(Path::new(os_str));
211        }
212    }
213    None
214}
215
216/// In memory, this is identical to `Path`. On non-Windows conversions to this type are no-ops. On
217/// windows, these conversions sanitize UNC paths by removing the `\\\\?\\` prefix.
218#[derive(Eq, PartialEq, Hash, Ord, PartialOrd)]
219#[repr(transparent)]
220pub struct SanitizedPath(Path);
221
222impl SanitizedPath {
223    pub fn new<T: AsRef<Path> + ?Sized>(path: &T) -> &Self {
224        #[cfg(not(target_os = "windows"))]
225        return Self::unchecked_new(path.as_ref());
226
227        #[cfg(target_os = "windows")]
228        return Self::unchecked_new(dunce::simplified(path.as_ref()));
229    }
230
231    pub fn unchecked_new<T: AsRef<Path> + ?Sized>(path: &T) -> &Self {
232        // safe because `Path` and `SanitizedPath` have the same repr and Drop impl
233        unsafe { mem::transmute::<&Path, &Self>(path.as_ref()) }
234    }
235
236    pub fn from_arc(path: Arc<Path>) -> Arc<Self> {
237        // safe because `Path` and `SanitizedPath` have the same repr and Drop impl
238        #[cfg(not(target_os = "windows"))]
239        return unsafe { mem::transmute::<Arc<Path>, Arc<Self>>(path) };
240
241        #[cfg(target_os = "windows")]
242        {
243            // `dunce` strips `\\?\C:\...` but leaves verbatim UNC (`\\?\UNC\...`).
244            // Rewriting that prefix allocates, so it cannot live in `new`, which
245            // returns a borrow of its input. `new_arc` still goes through `new`.
246            // Match `Prefix::VerbatimUNC` instead of `Path::to_str` so a filename
247            // with an unpaired UTF-16 surrogate is still rewritten.
248            let path = match rewrite_verbatim_unc_prefix(&path) {
249                Some(rewritten) => rewritten.into(),
250                None => path,
251            };
252            // TODO: could avoid allocating here if dunce::simplified results in the same path
253            Self::new(&path).into()
254        }
255    }
256
257    pub fn new_arc<T: AsRef<Path> + ?Sized>(path: &T) -> Arc<Self> {
258        Self::new(path).into()
259    }
260
261    pub fn cast_arc(path: Arc<Self>) -> Arc<Path> {
262        // safe because `Path` and `SanitizedPath` have the same repr and Drop impl
263        unsafe { mem::transmute::<Arc<Self>, Arc<Path>>(path) }
264    }
265
266    pub fn cast_arc_ref(path: &Arc<Self>) -> &Arc<Path> {
267        // safe because `Path` and `SanitizedPath` have the same repr and Drop impl
268        unsafe { mem::transmute::<&Arc<Self>, &Arc<Path>>(path) }
269    }
270
271    pub fn starts_with(&self, prefix: &Self) -> bool {
272        self.0.starts_with(&prefix.0)
273    }
274
275    pub fn as_path(&self) -> &Path {
276        &self.0
277    }
278
279    pub fn file_name(&self) -> Option<&std::ffi::OsStr> {
280        self.0.file_name()
281    }
282
283    pub fn extension(&self) -> Option<&std::ffi::OsStr> {
284        self.0.extension()
285    }
286
287    pub fn join<P: AsRef<Path>>(&self, path: P) -> PathBuf {
288        self.0.join(path)
289    }
290
291    pub fn parent(&self) -> Option<&Self> {
292        self.0.parent().map(Self::unchecked_new)
293    }
294
295    pub fn strip_prefix(&self, base: &Self) -> Result<&Path, StripPrefixError> {
296        self.0.strip_prefix(base.as_path())
297    }
298
299    pub fn to_str(&self) -> Option<&str> {
300        self.0.to_str()
301    }
302
303    pub fn to_path_buf(&self) -> PathBuf {
304        self.0.to_path_buf()
305    }
306}
307
308/// Join `\\server\share` with the remaining path components.
309///
310/// `OsStr` parts keep non-UTF-8 names (unpaired UTF-16 surrogates on Windows).
311#[cfg(any(test, target_os = "windows"))]
312fn assemble_unc_path<'a>(
313    server: &OsStr,
314    share: &OsStr,
315    rest: impl IntoIterator<Item = &'a OsStr>,
316) -> PathBuf {
317    let mut rewritten = std::ffi::OsString::from(r"\\");
318    rewritten.push(server);
319    rewritten.push(r"\");
320    rewritten.push(share);
321    for part in rest {
322        rewritten.push(r"\");
323        rewritten.push(part);
324    }
325    PathBuf::from(rewritten)
326}
327
328/// Rewrite a verbatim UNC path (`\\?\UNC\server\share\...`) to a normal UNC
329/// path (`\\server\share\...`). `dunce::simplified` only strips verbatim disk
330/// prefixes, so this runs before it. `None` means the path is left unchanged.
331///
332/// The prefix is classified with [`std::path::Prefix`], so the whole path does
333/// not need to be valid UTF-8. Verbatim disk (`\\?\C:\...`) and ordinary UNC
334/// are left for `dunce::simplified`.
335#[cfg(target_os = "windows")]
336fn rewrite_verbatim_unc_prefix(path: &Path) -> Option<PathBuf> {
337    use std::path::{Component, Prefix};
338
339    let mut components = path.components();
340    let (server, share) = match components.next()? {
341        Component::Prefix(prefix) => match prefix.kind() {
342            Prefix::VerbatimUNC(server, share) => (server, share),
343            _ => return None,
344        },
345        _ => return None,
346    };
347
348    let rest = components.filter_map(|component| match component {
349        Component::RootDir | Component::Prefix(_) => None,
350        Component::CurDir => Some(OsStr::new(".")),
351        Component::ParentDir => Some(OsStr::new("..")),
352        Component::Normal(part) => Some(part),
353    });
354    Some(assemble_unc_path(server, share, rest))
355}
356
357impl std::fmt::Debug for SanitizedPath {
358    fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
359        std::fmt::Debug::fmt(&self.0, formatter)
360    }
361}
362
363impl Display for SanitizedPath {
364    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
365        write!(f, "{}", self.0.display())
366    }
367}
368
369impl From<&SanitizedPath> for Arc<SanitizedPath> {
370    fn from(sanitized_path: &SanitizedPath) -> Self {
371        let path: Arc<Path> = sanitized_path.0.into();
372        // safe because `Path` and `SanitizedPath` have the same repr and Drop impl
373        unsafe { mem::transmute(path) }
374    }
375}
376
377impl From<&SanitizedPath> for PathBuf {
378    fn from(sanitized_path: &SanitizedPath) -> Self {
379        sanitized_path.as_path().into()
380    }
381}
382
383impl AsRef<Path> for SanitizedPath {
384    fn as_ref(&self) -> &Path {
385        &self.0
386    }
387}
388
389#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
390pub enum PathStyle {
391    Posix,
392    Windows,
393}
394
395impl PathStyle {
396    #[cfg(target_os = "windows")]
397    pub const fn local() -> Self {
398        PathStyle::Windows
399    }
400
401    #[cfg(not(target_os = "windows"))]
402    pub const fn local() -> Self {
403        PathStyle::Posix
404    }
405
406    #[inline]
407    pub fn separator(&self) -> &'static str {
408        match self {
409            PathStyle::Posix => "/",
410            PathStyle::Windows => "\\",
411        }
412    }
413
414    pub fn is_windows(&self) -> bool {
415        *self == PathStyle::Windows
416    }
417
418    pub fn join(self, left: impl AsRef<Path>, right: impl AsRef<Path>) -> Option<String> {
419        let right = right.as_ref().to_str()?;
420        if is_absolute(right, self) {
421            return None;
422        }
423        let left = left.as_ref().to_str()?;
424        if left.is_empty() {
425            Some(right.into())
426        } else {
427            Some(format!(
428                "{left}{}{right}",
429                if left.ends_with(self.separator()) {
430                    ""
431                } else {
432                    self.separator()
433                }
434            ))
435        }
436    }
437
438    pub fn split(self, path_like: &str) -> (Option<&str>, &str) {
439        let Some(pos) = path_like.rfind(self.separator()) else {
440            return (None, path_like);
441        };
442        let filename_start = pos + self.separator().len();
443        (
444            Some(&path_like[..filename_start]),
445            &path_like[filename_start..],
446        )
447    }
448}
449
450#[derive(Debug, Clone)]
451pub struct RemotePathBuf {
452    style: PathStyle,
453    string: String,
454}
455
456impl RemotePathBuf {
457    pub fn new(string: String, style: PathStyle) -> Self {
458        Self { style, string }
459    }
460
461    pub fn from_str(path: &str, style: PathStyle) -> Self {
462        Self::new(path.to_string(), style)
463    }
464
465    pub fn path_style(&self) -> PathStyle {
466        self.style
467    }
468
469    pub fn to_proto(self) -> String {
470        self.string
471    }
472}
473
474impl Display for RemotePathBuf {
475    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
476        write!(f, "{}", self.string)
477    }
478}
479
480pub fn is_absolute(path_like: &str, path_style: PathStyle) -> bool {
481    path_like.starts_with('/')
482        || path_style == PathStyle::Windows
483            && (path_like.starts_with('\\')
484                || path_like
485                    .chars()
486                    .next()
487                    .is_some_and(|c| c.is_ascii_alphabetic())
488                    && path_like[1..]
489                        .strip_prefix(':')
490                        .is_some_and(|path| path.starts_with('/') || path.starts_with('\\')))
491}
492
493#[derive(Debug, PartialEq)]
494#[non_exhaustive]
495pub struct NormalizeError;
496
497impl Error for NormalizeError {}
498
499impl std::fmt::Display for NormalizeError {
500    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
501        f.write_str("parent reference `..` points outside of base directory")
502    }
503}
504
505/// Copied from stdlib where it's unstable.
506///
507/// Normalize a path, including `..` without traversing the filesystem.
508///
509/// Returns an error if normalization would leave leading `..` components.
510///
511/// <div class="warning">
512///
513/// This function always resolves `..` to the "lexical" parent.
514/// That is "a/b/../c" will always resolve to `a/c` which can change the meaning of the path.
515/// 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`.
516///
517/// </div>
518///
519/// [`path::absolute`](absolute) is an alternative that preserves `..`.
520/// Or [`Path::canonicalize`] can be used to resolve any `..` by querying the filesystem.
521pub fn normalize_lexically(path: &Path) -> Result<PathBuf, NormalizeError> {
522    use std::path::Component;
523
524    let mut lexical = PathBuf::new();
525    let mut iter = path.components().peekable();
526
527    // Find the root, if any, and add it to the lexical path.
528    // Here we treat the Windows path "C:\" as a single "root" even though
529    // `components` splits it into two: (Prefix, RootDir).
530    let root = match iter.peek() {
531        Some(Component::ParentDir) => return Err(NormalizeError),
532        Some(p @ Component::RootDir) | Some(p @ Component::CurDir) => {
533            lexical.push(p);
534            iter.next();
535            lexical.as_os_str().len()
536        }
537        Some(Component::Prefix(prefix)) => {
538            lexical.push(prefix.as_os_str());
539            iter.next();
540            if let Some(p @ Component::RootDir) = iter.peek() {
541                lexical.push(p);
542                iter.next();
543            }
544            lexical.as_os_str().len()
545        }
546        None => return Ok(PathBuf::new()),
547        Some(Component::Normal(_)) => 0,
548    };
549
550    for component in iter {
551        match component {
552            Component::RootDir => unreachable!(),
553            Component::Prefix(_) => return Err(NormalizeError),
554            Component::CurDir => continue,
555            Component::ParentDir => {
556                // It's an error if ParentDir causes us to go above the "root".
557                if lexical.as_os_str().len() == root {
558                    return Err(NormalizeError);
559                } else {
560                    lexical.pop();
561                }
562            }
563            Component::Normal(path) => lexical.push(path),
564        }
565    }
566    Ok(lexical)
567}
568
569/// A delimiter to use in `path_query:row_number:column_number` strings parsing.
570pub const FILE_ROW_COLUMN_DELIMITER: char = ':';
571
572const ROW_COL_CAPTURE_REGEX: &str = r"(?xs)
573    ([^\(]+)\:(?:
574        \((\d+)[,:](\d+)\) # filename:(row,column), filename:(row:column)
575        |
576        \((\d+)\)()     # filename:(row)
577    )
578    |
579    ([^\(]+)(?:
580        \((\d+)[,:](\d+)\) # filename(row,column), filename(row:column)
581        |
582        \((\d+)\)()     # filename(row)
583    )
584    |
585    (.+?)(?:
586        \:+(\d+)\:(\d+)\:*$  # filename:row:column
587        |
588        \:+(\d+)\:*()$       # filename:row
589        |
590        \:+()()$
591    )";
592
593/// A representation of a path-like string with optional row and column numbers.
594/// Matching values example: `te`, `test.rs:22`, `te:22:5`, `test.c(22)`, `test.c(22,5)`etc.
595#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)]
596pub struct PathWithPosition {
597    pub path: PathBuf,
598    pub row: Option<u32>,
599    // Absent if row is absent.
600    pub column: Option<u32>,
601}
602
603impl PathWithPosition {
604    /// Returns a PathWithPosition from a path.
605    pub fn from_path(path: PathBuf) -> Self {
606        Self {
607            path,
608            row: None,
609            column: None,
610        }
611    }
612
613    /// Parses a string that possibly has `:row:column` or `(row, column)` suffix.
614    /// Parenthesis format is used by [MSBuild](https://learn.microsoft.com/en-us/visualstudio/msbuild/msbuild-diagnostic-format-for-tasks) compatible tools
615    /// Ignores trailing `:`s, so `test.rs:22:` is parsed as `test.rs:22`.
616    /// If the suffix parsing fails, the whole string is parsed as a path.
617    ///
618    /// Be mindful that `test_file:10:1:` is a valid posix filename.
619    /// `PathWithPosition` class assumes that the ending position-like suffix is **not** part of the filename.
620    ///
621    /// # Examples
622    ///
623    /// ```
624    /// # use fc_gpui_util::paths::PathWithPosition;
625    /// # use std::path::PathBuf;
626    /// assert_eq!(PathWithPosition::parse_str("test_file"), PathWithPosition {
627    ///     path: PathBuf::from("test_file"),
628    ///     row: None,
629    ///     column: None,
630    /// });
631    /// assert_eq!(PathWithPosition::parse_str("test_file:10"), PathWithPosition {
632    ///     path: PathBuf::from("test_file"),
633    ///     row: Some(10),
634    ///     column: None,
635    /// });
636    /// assert_eq!(PathWithPosition::parse_str("test_file.rs"), PathWithPosition {
637    ///     path: PathBuf::from("test_file.rs"),
638    ///     row: None,
639    ///     column: None,
640    /// });
641    /// assert_eq!(PathWithPosition::parse_str("test_file.rs:1"), PathWithPosition {
642    ///     path: PathBuf::from("test_file.rs"),
643    ///     row: Some(1),
644    ///     column: None,
645    /// });
646    /// assert_eq!(PathWithPosition::parse_str("test_file.rs:1:2"), PathWithPosition {
647    ///     path: PathBuf::from("test_file.rs"),
648    ///     row: Some(1),
649    ///     column: Some(2),
650    /// });
651    /// ```
652    ///
653    /// # Expected parsing results when encounter ill-formatted inputs.
654    /// ```
655    /// # use fc_gpui_util::paths::PathWithPosition;
656    /// # use std::path::PathBuf;
657    /// assert_eq!(PathWithPosition::parse_str("test_file.rs:a"), PathWithPosition {
658    ///     path: PathBuf::from("test_file.rs:a"),
659    ///     row: None,
660    ///     column: None,
661    /// });
662    /// assert_eq!(PathWithPosition::parse_str("test_file.rs:a:b"), PathWithPosition {
663    ///     path: PathBuf::from("test_file.rs:a:b"),
664    ///     row: None,
665    ///     column: None,
666    /// });
667    /// assert_eq!(PathWithPosition::parse_str("test_file.rs"), PathWithPosition {
668    ///     path: PathBuf::from("test_file.rs"),
669    ///     row: None,
670    ///     column: None,
671    /// });
672    /// assert_eq!(PathWithPosition::parse_str("test_file.rs::1"), PathWithPosition {
673    ///     path: PathBuf::from("test_file.rs"),
674    ///     row: Some(1),
675    ///     column: None,
676    /// });
677    /// assert_eq!(PathWithPosition::parse_str("test_file.rs:1::"), PathWithPosition {
678    ///     path: PathBuf::from("test_file.rs"),
679    ///     row: Some(1),
680    ///     column: None,
681    /// });
682    /// assert_eq!(PathWithPosition::parse_str("test_file.rs::1:2"), PathWithPosition {
683    ///     path: PathBuf::from("test_file.rs"),
684    ///     row: Some(1),
685    ///     column: Some(2),
686    /// });
687    /// assert_eq!(PathWithPosition::parse_str("test_file.rs:1::2"), PathWithPosition {
688    ///     path: PathBuf::from("test_file.rs:1"),
689    ///     row: Some(2),
690    ///     column: None,
691    /// });
692    /// assert_eq!(PathWithPosition::parse_str("test_file.rs:1:2:3"), PathWithPosition {
693    ///     path: PathBuf::from("test_file.rs:1"),
694    ///     row: Some(2),
695    ///     column: Some(3),
696    /// });
697    /// ```
698    pub fn parse_str(s: &str) -> Self {
699        let trimmed = s.trim();
700        let path = Path::new(trimmed);
701        let maybe_file_name_with_row_col = path.file_name().unwrap_or_default().to_string_lossy();
702        if maybe_file_name_with_row_col.is_empty() {
703            return Self {
704                path: Path::new(s).to_path_buf(),
705                row: None,
706                column: None,
707            };
708        }
709
710        // Let's avoid repeated init cost on this. It is subject to thread contention, but
711        // so far this code isn't called from multiple hot paths. Getting contention here
712        // in the future seems unlikely.
713        static SUFFIX_RE: LazyLock<Regex> =
714            LazyLock::new(|| Regex::new(ROW_COL_CAPTURE_REGEX).unwrap());
715        match SUFFIX_RE
716            .captures(&maybe_file_name_with_row_col)
717            .map(|caps| caps.extract())
718        {
719            Some((_, [file_name, maybe_row, maybe_column])) => {
720                let row = maybe_row.parse::<u32>().ok();
721                let column = maybe_column.parse::<u32>().ok();
722
723                let suffix_length = maybe_file_name_with_row_col.len() - file_name.len();
724                let path_without_suffix = &trimmed[..trimmed.len() - suffix_length];
725
726                Self {
727                    path: Path::new(path_without_suffix).to_path_buf(),
728                    row,
729                    column,
730                }
731            }
732            None => {
733                // The `ROW_COL_CAPTURE_REGEX` deals with separated digits only,
734                // but in reality there could be `foo/bar.py:22:in` inputs which we want to match too.
735                // The regex mentioned is not very extendable with "digit or random string" checks, so do this here instead.
736                let delimiter = ':';
737                let mut path_parts = s
738                    .rsplitn(3, delimiter)
739                    .collect::<Vec<_>>()
740                    .into_iter()
741                    .rev()
742                    .fuse();
743                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();
744                let mut row = None;
745                let mut column = None;
746                if let Some(maybe_row) = path_parts.next() {
747                    if let Ok(parsed_row) = maybe_row.parse::<u32>() {
748                        row = Some(parsed_row);
749                        if let Some(parsed_column) = path_parts
750                            .next()
751                            .and_then(|maybe_col| maybe_col.parse::<u32>().ok())
752                        {
753                            column = Some(parsed_column);
754                        }
755                    } else {
756                        path_string.push(delimiter);
757                        path_string.push_str(maybe_row);
758                    }
759                }
760                for split in path_parts {
761                    path_string.push(delimiter);
762                    path_string.push_str(split);
763                }
764
765                Self {
766                    path: PathBuf::from(path_string),
767                    row,
768                    column,
769                }
770            }
771        }
772    }
773
774    pub fn map_path<E>(
775        self,
776        mapping: impl FnOnce(PathBuf) -> Result<PathBuf, E>,
777    ) -> Result<PathWithPosition, E> {
778        Ok(PathWithPosition {
779            path: mapping(self.path)?,
780            row: self.row,
781            column: self.column,
782        })
783    }
784
785    pub fn to_string(&self, path_to_string: impl Fn(&PathBuf) -> String) -> String {
786        let path_string = path_to_string(&self.path);
787        if let Some(row) = self.row {
788            if let Some(column) = self.column {
789                format!("{path_string}:{row}:{column}")
790            } else {
791                format!("{path_string}:{row}")
792            }
793        } else {
794            path_string
795        }
796    }
797}
798
799#[derive(Clone, Debug)]
800pub struct PathMatcher {
801    sources: Vec<String>,
802    glob: GlobSet,
803    path_style: PathStyle,
804}
805
806// impl std::fmt::Display for PathMatcher {
807//     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
808//         self.sources.fmt(f)
809//     }
810// }
811
812impl PartialEq for PathMatcher {
813    fn eq(&self, other: &Self) -> bool {
814        self.sources.eq(&other.sources)
815    }
816}
817
818impl Eq for PathMatcher {}
819
820impl PathMatcher {
821    pub fn new(
822        globs: impl IntoIterator<Item = impl AsRef<str>>,
823        path_style: PathStyle,
824    ) -> Result<Self, globset::Error> {
825        let globs = globs
826            .into_iter()
827            .map(|as_str| Glob::new(as_str.as_ref()))
828            .collect::<Result<Vec<_>, _>>()?;
829        Self::from_globs(globs, path_style)
830    }
831
832    /// Skips invalid globs, reporting each error to `on_error`.
833    /// If the combined set cannot be built, reports the error and matches nothing.
834    pub fn new_lenient(
835        globs: impl IntoIterator<Item = impl AsRef<str>>,
836        path_style: PathStyle,
837        mut on_error: impl FnMut(globset::Error),
838    ) -> Self {
839        let globs = globs
840            .into_iter()
841            .filter_map(|pattern| match Glob::new(pattern.as_ref()) {
842                Ok(glob) => Some(glob),
843                Err(error) => {
844                    on_error(error);
845                    None
846                }
847            })
848            .collect();
849        match Self::from_globs(globs, path_style) {
850            Ok(matcher) => matcher,
851            Err(error) => {
852                on_error(error);
853                Self {
854                    path_style,
855                    ..Self::default()
856                }
857            }
858        }
859    }
860
861    fn from_globs(globs: Vec<Glob>, path_style: PathStyle) -> Result<Self, globset::Error> {
862        let sources = globs.iter().map(|glob| glob.glob().to_owned()).collect();
863        let mut glob_builder = GlobSetBuilder::new();
864        for single_glob in globs {
865            glob_builder.add(single_glob);
866        }
867        let glob = glob_builder.build()?;
868        Ok(PathMatcher {
869            glob,
870            sources,
871            path_style,
872        })
873    }
874
875    pub fn sources(&self) -> &[String] {
876        &self.sources
877    }
878
879    pub fn is_match<P: AsRef<Path>>(&self, other: P) -> bool {
880        let other_path = other.as_ref();
881        self.sources.iter().any(|source| {
882            let as_bytes = other_path.as_os_str().as_encoded_bytes();
883            as_bytes.starts_with(source.as_bytes()) || as_bytes.ends_with(source.as_bytes())
884        }) || self.glob.is_match(other_path)
885            || self.check_with_end_separator(other_path)
886    }
887
888    fn check_with_end_separator(&self, path: &Path) -> bool {
889        let path_str = path.to_string_lossy();
890        let separator = self.path_style.separator();
891        if path_str.ends_with(separator) {
892            false
893        } else {
894            self.glob.is_match(path_str.to_string() + separator)
895        }
896    }
897}
898
899impl Default for PathMatcher {
900    fn default() -> Self {
901        Self {
902            path_style: PathStyle::local(),
903            glob: GlobSet::empty(),
904            sources: vec![],
905        }
906    }
907}
908
909/// Custom character comparison that prioritizes lowercase for same letters
910fn compare_chars(a: char, b: char) -> Ordering {
911    // First compare case-insensitive
912    match a.to_ascii_lowercase().cmp(&b.to_ascii_lowercase()) {
913        Ordering::Equal => {
914            // If same letter, prioritize lowercase (lowercase < uppercase)
915            match (a.is_ascii_lowercase(), b.is_ascii_lowercase()) {
916                (true, false) => Ordering::Less,    // lowercase comes first
917                (false, true) => Ordering::Greater, // uppercase comes after
918                _ => Ordering::Equal,               // both same case or both non-ascii
919            }
920        }
921        other => other,
922    }
923}
924
925/// Compares two sequences of consecutive digits for natural sorting.
926///
927/// This function is a core component of natural sorting that handles numeric comparison
928/// in a way that feels natural to humans. It extracts and compares consecutive digit
929/// sequences from two iterators, handling various cases like leading zeros and very large numbers.
930///
931/// # Behavior
932///
933/// The function implements the following comparison rules:
934/// 1. Different numeric values: Compares by actual numeric value (e.g., "2" < "10")
935/// 2. Leading zeros: When values are equal, longer sequence wins (e.g., "002" > "2")
936/// 3. Large numbers: Falls back to string comparison for numbers that would overflow u128
937///
938/// # Examples
939///
940/// ```text
941/// "1" vs "2"      -> Less       (different values)
942/// "2" vs "10"     -> Less       (numeric comparison)
943/// "002" vs "2"    -> Greater    (leading zeros)
944/// "10" vs "010"   -> Less       (leading zeros)
945/// "999..." vs "1000..." -> Less (large number comparison)
946/// ```
947///
948/// # Implementation Details
949///
950/// 1. Extracts consecutive digits into strings
951/// 2. Compares sequence lengths for leading zero handling
952/// 3. For equal lengths, compares digit by digit
953/// 4. For different lengths:
954///    - Attempts numeric comparison first (for numbers up to 2^128 - 1)
955///    - Falls back to string comparison if numbers would overflow
956///
957/// The function advances both iterators past their respective numeric sequences,
958/// regardless of the comparison result.
959fn compare_numeric_segments<I>(
960    a_iter: &mut std::iter::Peekable<I>,
961    b_iter: &mut std::iter::Peekable<I>,
962) -> Ordering
963where
964    I: Iterator<Item = char>,
965{
966    // Collect all consecutive digits into strings
967    let mut a_num_str = String::new();
968    let mut b_num_str = String::new();
969
970    while let Some(&c) = a_iter.peek() {
971        if !c.is_ascii_digit() {
972            break;
973        }
974
975        a_num_str.push(c);
976        a_iter.next();
977    }
978
979    while let Some(&c) = b_iter.peek() {
980        if !c.is_ascii_digit() {
981            break;
982        }
983
984        b_num_str.push(c);
985        b_iter.next();
986    }
987
988    // First compare lengths (handle leading zeros)
989    match a_num_str.len().cmp(&b_num_str.len()) {
990        Ordering::Equal => {
991            // Same length, compare digit by digit
992            match a_num_str.cmp(&b_num_str) {
993                Ordering::Equal => Ordering::Equal,
994                ordering => ordering,
995            }
996        }
997
998        // Different lengths but same value means leading zeros
999        ordering => {
1000            // Try parsing as numbers first
1001            if let (Ok(a_val), Ok(b_val)) = (a_num_str.parse::<u128>(), b_num_str.parse::<u128>()) {
1002                match a_val.cmp(&b_val) {
1003                    Ordering::Equal => ordering, // Same value, longer one is greater (leading zeros)
1004                    ord => ord,
1005                }
1006            } else {
1007                // If parsing fails (overflow), compare as strings
1008                a_num_str.cmp(&b_num_str)
1009            }
1010        }
1011    }
1012}
1013
1014/// Performs natural sorting comparison between two strings.
1015///
1016/// Natural sorting is an ordering that handles numeric sequences in a way that matches human expectations.
1017/// For example, "file2" comes before "file10" (unlike standard lexicographic sorting).
1018///
1019/// # Characteristics
1020///
1021/// * Case-sensitive with lowercase priority: When comparing same letters, lowercase comes before uppercase
1022/// * Numbers are compared by numeric value, not character by character
1023/// * Leading zeros affect ordering when numeric values are equal
1024/// * Can handle numbers larger than u128::MAX (falls back to string comparison)
1025///
1026/// # Algorithm
1027///
1028/// The function works by:
1029/// 1. Processing strings character by character
1030/// 2. When encountering digits, treating consecutive digits as a single number
1031/// 3. Comparing numbers by their numeric value rather than lexicographically
1032/// 4. For non-numeric characters, using case-sensitive comparison with lowercase priority
1033fn natural_sort(a: &str, b: &str) -> Ordering {
1034    let mut a_iter = a.chars().peekable();
1035    let mut b_iter = b.chars().peekable();
1036
1037    loop {
1038        match (a_iter.peek(), b_iter.peek()) {
1039            (None, None) => return Ordering::Equal,
1040            (None, _) => return Ordering::Less,
1041            (_, None) => return Ordering::Greater,
1042            (Some(&a_char), Some(&b_char)) => {
1043                if a_char.is_ascii_digit() && b_char.is_ascii_digit() {
1044                    match compare_numeric_segments(&mut a_iter, &mut b_iter) {
1045                        Ordering::Equal => continue,
1046                        ordering => return ordering,
1047                    }
1048                } else {
1049                    match compare_chars(a_char, b_char) {
1050                        Ordering::Equal => {
1051                            a_iter.next();
1052                            b_iter.next();
1053                        }
1054                        ordering => return ordering,
1055                    }
1056                }
1057            }
1058        }
1059    }
1060}
1061pub fn compare_rel_paths(
1062    (path_a, a_is_file): (&RelPath, bool),
1063    (path_b, b_is_file): (&RelPath, bool),
1064) -> Ordering {
1065    let mut components_a = path_a.components();
1066    let mut components_b = path_b.components();
1067
1068    fn stem_and_extension(filename: &str) -> (Option<&str>, Option<&str>) {
1069        if filename.is_empty() {
1070            return (None, None);
1071        }
1072
1073        match filename.rsplit_once('.') {
1074            // Case 1: No dot was found. The entire name is the stem.
1075            None => (Some(filename), None),
1076
1077            // Case 2: A dot was found.
1078            Some((before, after)) => {
1079                // This is the crucial check for dotfiles like ".bashrc".
1080                // If `before` is empty, the dot was the first character.
1081                // In that case, we revert to the "whole name is the stem" logic.
1082                if before.is_empty() {
1083                    (Some(filename), None)
1084                } else {
1085                    // Otherwise, we have a standard stem and extension.
1086                    (Some(before), Some(after))
1087                }
1088            }
1089        }
1090    }
1091    loop {
1092        match (components_a.next(), components_b.next()) {
1093            (Some(component_a), Some(component_b)) => {
1094                let a_is_file = a_is_file && components_a.rest().is_empty();
1095                let b_is_file = b_is_file && components_b.rest().is_empty();
1096
1097                let ordering = a_is_file.cmp(&b_is_file).then_with(|| {
1098                    let (a_stem, a_extension) = a_is_file
1099                        .then(|| stem_and_extension(component_a))
1100                        .unwrap_or_default();
1101                    let path_string_a = if a_is_file { a_stem } else { Some(component_a) };
1102
1103                    let (b_stem, b_extension) = b_is_file
1104                        .then(|| stem_and_extension(component_b))
1105                        .unwrap_or_default();
1106                    let path_string_b = if b_is_file { b_stem } else { Some(component_b) };
1107
1108                    let compare_components = match (path_string_a, path_string_b) {
1109                        (Some(a), Some(b)) => natural_sort(&a, &b),
1110                        (Some(_), None) => Ordering::Greater,
1111                        (None, Some(_)) => Ordering::Less,
1112                        (None, None) => Ordering::Equal,
1113                    };
1114
1115                    compare_components.then_with(|| {
1116                        if a_is_file && b_is_file {
1117                            let ext_a = a_extension.unwrap_or_default();
1118                            let ext_b = b_extension.unwrap_or_default();
1119                            ext_a.cmp(ext_b)
1120                        } else {
1121                            Ordering::Equal
1122                        }
1123                    })
1124                });
1125
1126                if !ordering.is_eq() {
1127                    return ordering;
1128                }
1129            }
1130            (Some(_), None) => break Ordering::Greater,
1131            (None, Some(_)) => break Ordering::Less,
1132            (None, None) => break Ordering::Equal,
1133        }
1134    }
1135}
1136
1137pub fn compare_paths(
1138    (path_a, a_is_file): (&Path, bool),
1139    (path_b, b_is_file): (&Path, bool),
1140) -> Ordering {
1141    let mut components_a = path_a.components().peekable();
1142    let mut components_b = path_b.components().peekable();
1143
1144    loop {
1145        match (components_a.next(), components_b.next()) {
1146            (Some(component_a), Some(component_b)) => {
1147                let a_is_file = components_a.peek().is_none() && a_is_file;
1148                let b_is_file = components_b.peek().is_none() && b_is_file;
1149
1150                let ordering = a_is_file.cmp(&b_is_file).then_with(|| {
1151                    let path_a = Path::new(component_a.as_os_str());
1152                    let path_string_a = if a_is_file {
1153                        path_a.file_stem()
1154                    } else {
1155                        path_a.file_name()
1156                    }
1157                    .map(|s| s.to_string_lossy());
1158
1159                    let path_b = Path::new(component_b.as_os_str());
1160                    let path_string_b = if b_is_file {
1161                        path_b.file_stem()
1162                    } else {
1163                        path_b.file_name()
1164                    }
1165                    .map(|s| s.to_string_lossy());
1166
1167                    let compare_components = match (path_string_a, path_string_b) {
1168                        (Some(a), Some(b)) => natural_sort(&a, &b),
1169                        (Some(_), None) => Ordering::Greater,
1170                        (None, Some(_)) => Ordering::Less,
1171                        (None, None) => Ordering::Equal,
1172                    };
1173
1174                    compare_components.then_with(|| {
1175                        if a_is_file && b_is_file {
1176                            let ext_a = path_a.extension().unwrap_or_default();
1177                            let ext_b = path_b.extension().unwrap_or_default();
1178                            ext_a.cmp(ext_b)
1179                        } else {
1180                            Ordering::Equal
1181                        }
1182                    })
1183                });
1184
1185                if !ordering.is_eq() {
1186                    return ordering;
1187                }
1188            }
1189            (Some(_), None) => break Ordering::Greater,
1190            (None, Some(_)) => break Ordering::Less,
1191            (None, None) => break Ordering::Equal,
1192        }
1193    }
1194}
1195
1196#[cfg(test)]
1197mod tests {
1198    use super::*;
1199    use util_macros::perf;
1200
1201    #[perf]
1202    fn compare_paths_with_dots() {
1203        let mut paths = vec![
1204            (Path::new("test_dirs"), false),
1205            (Path::new("test_dirs/1.46"), false),
1206            (Path::new("test_dirs/1.46/bar_1"), true),
1207            (Path::new("test_dirs/1.46/bar_2"), true),
1208            (Path::new("test_dirs/1.45"), false),
1209            (Path::new("test_dirs/1.45/foo_2"), true),
1210            (Path::new("test_dirs/1.45/foo_1"), true),
1211        ];
1212        paths.sort_by(|&a, &b| compare_paths(a, b));
1213        assert_eq!(
1214            paths,
1215            vec![
1216                (Path::new("test_dirs"), false),
1217                (Path::new("test_dirs/1.45"), false),
1218                (Path::new("test_dirs/1.45/foo_1"), true),
1219                (Path::new("test_dirs/1.45/foo_2"), true),
1220                (Path::new("test_dirs/1.46"), false),
1221                (Path::new("test_dirs/1.46/bar_1"), true),
1222                (Path::new("test_dirs/1.46/bar_2"), true),
1223            ]
1224        );
1225        let mut paths = vec![
1226            (Path::new("root1/one.txt"), true),
1227            (Path::new("root1/one.two.txt"), true),
1228        ];
1229        paths.sort_by(|&a, &b| compare_paths(a, b));
1230        assert_eq!(
1231            paths,
1232            vec![
1233                (Path::new("root1/one.txt"), true),
1234                (Path::new("root1/one.two.txt"), true),
1235            ]
1236        );
1237    }
1238
1239    #[perf]
1240    fn compare_paths_with_same_name_different_extensions() {
1241        let mut paths = vec![
1242            (Path::new("test_dirs/file.rs"), true),
1243            (Path::new("test_dirs/file.txt"), true),
1244            (Path::new("test_dirs/file.md"), true),
1245            (Path::new("test_dirs/file"), true),
1246            (Path::new("test_dirs/file.a"), true),
1247        ];
1248        paths.sort_by(|&a, &b| compare_paths(a, b));
1249        assert_eq!(
1250            paths,
1251            vec![
1252                (Path::new("test_dirs/file"), true),
1253                (Path::new("test_dirs/file.a"), true),
1254                (Path::new("test_dirs/file.md"), true),
1255                (Path::new("test_dirs/file.rs"), true),
1256                (Path::new("test_dirs/file.txt"), true),
1257            ]
1258        );
1259    }
1260
1261    #[perf]
1262    fn compare_paths_case_semi_sensitive() {
1263        let mut paths = vec![
1264            (Path::new("test_DIRS"), false),
1265            (Path::new("test_DIRS/foo_1"), true),
1266            (Path::new("test_DIRS/foo_2"), true),
1267            (Path::new("test_DIRS/bar"), true),
1268            (Path::new("test_DIRS/BAR"), true),
1269            (Path::new("test_dirs"), false),
1270            (Path::new("test_dirs/foo_1"), true),
1271            (Path::new("test_dirs/foo_2"), true),
1272            (Path::new("test_dirs/bar"), true),
1273            (Path::new("test_dirs/BAR"), true),
1274        ];
1275        paths.sort_by(|&a, &b| compare_paths(a, b));
1276        assert_eq!(
1277            paths,
1278            vec![
1279                (Path::new("test_dirs"), false),
1280                (Path::new("test_dirs/bar"), true),
1281                (Path::new("test_dirs/BAR"), true),
1282                (Path::new("test_dirs/foo_1"), true),
1283                (Path::new("test_dirs/foo_2"), true),
1284                (Path::new("test_DIRS"), false),
1285                (Path::new("test_DIRS/bar"), true),
1286                (Path::new("test_DIRS/BAR"), true),
1287                (Path::new("test_DIRS/foo_1"), true),
1288                (Path::new("test_DIRS/foo_2"), true),
1289            ]
1290        );
1291    }
1292
1293    #[perf]
1294    fn path_with_position_parse_posix_path() {
1295        // Test POSIX filename edge cases
1296        // Read more at https://en.wikipedia.org/wiki/Filename
1297        assert_eq!(
1298            PathWithPosition::parse_str("test_file"),
1299            PathWithPosition {
1300                path: PathBuf::from("test_file"),
1301                row: None,
1302                column: None
1303            }
1304        );
1305
1306        assert_eq!(
1307            PathWithPosition::parse_str("a:bc:.zip:1"),
1308            PathWithPosition {
1309                path: PathBuf::from("a:bc:.zip"),
1310                row: Some(1),
1311                column: None
1312            }
1313        );
1314
1315        assert_eq!(
1316            PathWithPosition::parse_str("one.second.zip:1"),
1317            PathWithPosition {
1318                path: PathBuf::from("one.second.zip"),
1319                row: Some(1),
1320                column: None
1321            }
1322        );
1323
1324        // Trim off trailing `:`s for otherwise valid input.
1325        assert_eq!(
1326            PathWithPosition::parse_str("test_file:10:1:"),
1327            PathWithPosition {
1328                path: PathBuf::from("test_file"),
1329                row: Some(10),
1330                column: Some(1)
1331            }
1332        );
1333
1334        assert_eq!(
1335            PathWithPosition::parse_str("test_file.rs:"),
1336            PathWithPosition {
1337                path: PathBuf::from("test_file.rs"),
1338                row: None,
1339                column: None
1340            }
1341        );
1342
1343        assert_eq!(
1344            PathWithPosition::parse_str("test_file.rs:1:"),
1345            PathWithPosition {
1346                path: PathBuf::from("test_file.rs"),
1347                row: Some(1),
1348                column: None
1349            }
1350        );
1351
1352        assert_eq!(
1353            PathWithPosition::parse_str("ab\ncd"),
1354            PathWithPosition {
1355                path: PathBuf::from("ab\ncd"),
1356                row: None,
1357                column: None
1358            }
1359        );
1360
1361        assert_eq!(
1362            PathWithPosition::parse_str("👋\nab"),
1363            PathWithPosition {
1364                path: PathBuf::from("👋\nab"),
1365                row: None,
1366                column: None
1367            }
1368        );
1369
1370        assert_eq!(
1371            PathWithPosition::parse_str("Types.hs:(617,9)-(670,28):"),
1372            PathWithPosition {
1373                path: PathBuf::from("Types.hs"),
1374                row: Some(617),
1375                column: Some(9),
1376            }
1377        );
1378    }
1379
1380    #[perf]
1381    #[cfg(not(target_os = "windows"))]
1382    fn path_with_position_parse_posix_path_with_suffix() {
1383        assert_eq!(
1384            PathWithPosition::parse_str("foo/bar:34:in"),
1385            PathWithPosition {
1386                path: PathBuf::from("foo/bar"),
1387                row: Some(34),
1388                column: None,
1389            }
1390        );
1391        assert_eq!(
1392            PathWithPosition::parse_str("foo/bar.rs:1902:::15:"),
1393            PathWithPosition {
1394                path: PathBuf::from("foo/bar.rs:1902"),
1395                row: Some(15),
1396                column: None
1397            }
1398        );
1399
1400        assert_eq!(
1401            PathWithPosition::parse_str("app-editors:zed-0.143.6:20240710-201212.log:34:"),
1402            PathWithPosition {
1403                path: PathBuf::from("app-editors:zed-0.143.6:20240710-201212.log"),
1404                row: Some(34),
1405                column: None,
1406            }
1407        );
1408
1409        assert_eq!(
1410            PathWithPosition::parse_str("crates/file_finder/src/file_finder.rs:1902:13:"),
1411            PathWithPosition {
1412                path: PathBuf::from("crates/file_finder/src/file_finder.rs"),
1413                row: Some(1902),
1414                column: Some(13),
1415            }
1416        );
1417
1418        assert_eq!(
1419            PathWithPosition::parse_str("crate/utils/src/test:today.log:34"),
1420            PathWithPosition {
1421                path: PathBuf::from("crate/utils/src/test:today.log"),
1422                row: Some(34),
1423                column: None,
1424            }
1425        );
1426        assert_eq!(
1427            PathWithPosition::parse_str("/testing/out/src/file_finder.odin(7:15)"),
1428            PathWithPosition {
1429                path: PathBuf::from("/testing/out/src/file_finder.odin"),
1430                row: Some(7),
1431                column: Some(15),
1432            }
1433        );
1434    }
1435
1436    #[perf]
1437    #[cfg(target_os = "windows")]
1438    fn path_with_position_parse_windows_path() {
1439        assert_eq!(
1440            PathWithPosition::parse_str("crates\\utils\\paths.rs"),
1441            PathWithPosition {
1442                path: PathBuf::from("crates\\utils\\paths.rs"),
1443                row: None,
1444                column: None
1445            }
1446        );
1447
1448        assert_eq!(
1449            PathWithPosition::parse_str("C:\\Users\\someone\\test_file.rs"),
1450            PathWithPosition {
1451                path: PathBuf::from("C:\\Users\\someone\\test_file.rs"),
1452                row: None,
1453                column: None
1454            }
1455        );
1456    }
1457
1458    #[perf]
1459    #[cfg(target_os = "windows")]
1460    fn path_with_position_parse_windows_path_with_suffix() {
1461        assert_eq!(
1462            PathWithPosition::parse_str("crates\\utils\\paths.rs:101"),
1463            PathWithPosition {
1464                path: PathBuf::from("crates\\utils\\paths.rs"),
1465                row: Some(101),
1466                column: None
1467            }
1468        );
1469
1470        assert_eq!(
1471            PathWithPosition::parse_str("\\\\?\\C:\\Users\\someone\\test_file.rs:1:20"),
1472            PathWithPosition {
1473                path: PathBuf::from("\\\\?\\C:\\Users\\someone\\test_file.rs"),
1474                row: Some(1),
1475                column: Some(20)
1476            }
1477        );
1478
1479        assert_eq!(
1480            PathWithPosition::parse_str("C:\\Users\\someone\\test_file.rs(1902,13)"),
1481            PathWithPosition {
1482                path: PathBuf::from("C:\\Users\\someone\\test_file.rs"),
1483                row: Some(1902),
1484                column: Some(13)
1485            }
1486        );
1487
1488        // Trim off trailing `:`s for otherwise valid input.
1489        assert_eq!(
1490            PathWithPosition::parse_str("\\\\?\\C:\\Users\\someone\\test_file.rs:1902:13:"),
1491            PathWithPosition {
1492                path: PathBuf::from("\\\\?\\C:\\Users\\someone\\test_file.rs"),
1493                row: Some(1902),
1494                column: Some(13)
1495            }
1496        );
1497
1498        assert_eq!(
1499            PathWithPosition::parse_str("\\\\?\\C:\\Users\\someone\\test_file.rs:1902:13:15:"),
1500            PathWithPosition {
1501                path: PathBuf::from("\\\\?\\C:\\Users\\someone\\test_file.rs:1902"),
1502                row: Some(13),
1503                column: Some(15)
1504            }
1505        );
1506
1507        assert_eq!(
1508            PathWithPosition::parse_str("\\\\?\\C:\\Users\\someone\\test_file.rs:1902:::15:"),
1509            PathWithPosition {
1510                path: PathBuf::from("\\\\?\\C:\\Users\\someone\\test_file.rs:1902"),
1511                row: Some(15),
1512                column: None
1513            }
1514        );
1515
1516        assert_eq!(
1517            PathWithPosition::parse_str("\\\\?\\C:\\Users\\someone\\test_file.rs(1902,13):"),
1518            PathWithPosition {
1519                path: PathBuf::from("\\\\?\\C:\\Users\\someone\\test_file.rs"),
1520                row: Some(1902),
1521                column: Some(13),
1522            }
1523        );
1524
1525        assert_eq!(
1526            PathWithPosition::parse_str("\\\\?\\C:\\Users\\someone\\test_file.rs(1902):"),
1527            PathWithPosition {
1528                path: PathBuf::from("\\\\?\\C:\\Users\\someone\\test_file.rs"),
1529                row: Some(1902),
1530                column: None,
1531            }
1532        );
1533
1534        assert_eq!(
1535            PathWithPosition::parse_str("C:\\Users\\someone\\test_file.rs:1902:13:"),
1536            PathWithPosition {
1537                path: PathBuf::from("C:\\Users\\someone\\test_file.rs"),
1538                row: Some(1902),
1539                column: Some(13),
1540            }
1541        );
1542
1543        assert_eq!(
1544            PathWithPosition::parse_str("C:\\Users\\someone\\test_file.rs(1902,13):"),
1545            PathWithPosition {
1546                path: PathBuf::from("C:\\Users\\someone\\test_file.rs"),
1547                row: Some(1902),
1548                column: Some(13),
1549            }
1550        );
1551
1552        assert_eq!(
1553            PathWithPosition::parse_str("C:\\Users\\someone\\test_file.rs(1902):"),
1554            PathWithPosition {
1555                path: PathBuf::from("C:\\Users\\someone\\test_file.rs"),
1556                row: Some(1902),
1557                column: None,
1558            }
1559        );
1560
1561        assert_eq!(
1562            PathWithPosition::parse_str("crates/utils/paths.rs:101"),
1563            PathWithPosition {
1564                path: PathBuf::from("crates\\utils\\paths.rs"),
1565                row: Some(101),
1566                column: None,
1567            }
1568        );
1569    }
1570
1571    #[perf]
1572    fn test_path_compact() {
1573        let path: PathBuf = [
1574            home_dir().to_string_lossy().into_owned(),
1575            "some_file.txt".to_string(),
1576        ]
1577        .iter()
1578        .collect();
1579        if cfg!(any(target_os = "linux", target_os = "freebsd")) || cfg!(target_os = "macos") {
1580            assert_eq!(path.compact().to_str(), Some("~/some_file.txt"));
1581        } else {
1582            assert_eq!(path.compact().to_str(), path.to_str());
1583        }
1584    }
1585
1586    #[perf]
1587    fn test_extension_or_hidden_file_name() {
1588        // No dots in name
1589        let path = Path::new("/a/b/c/file_name.rs");
1590        assert_eq!(path.extension_or_hidden_file_name(), Some("rs"));
1591
1592        // Single dot in name
1593        let path = Path::new("/a/b/c/file.name.rs");
1594        assert_eq!(path.extension_or_hidden_file_name(), Some("rs"));
1595
1596        // Multiple dots in name
1597        let path = Path::new("/a/b/c/long.file.name.rs");
1598        assert_eq!(path.extension_or_hidden_file_name(), Some("rs"));
1599
1600        // Hidden file, no extension
1601        let path = Path::new("/a/b/c/.gitignore");
1602        assert_eq!(path.extension_or_hidden_file_name(), Some("gitignore"));
1603
1604        // Hidden file, with extension
1605        let path = Path::new("/a/b/c/.eslintrc.js");
1606        assert_eq!(path.extension_or_hidden_file_name(), Some("eslintrc.js"));
1607    }
1608
1609    #[test]
1610    fn test_lenient_path_matcher() {
1611        for path_style in [PathStyle::Posix, PathStyle::Windows] {
1612            let patterns = ["**/.git", "[", "target/**", "{"];
1613            let mut errors = Vec::new();
1614            let matcher =
1615                PathMatcher::new_lenient(patterns, path_style, |error| errors.push(error));
1616            let expected = PathMatcher::new(["**/.git", "target/**"], path_style).unwrap();
1617            assert_eq!(matcher, expected);
1618            assert_eq!(errors.len(), 2);
1619            assert_eq!(errors[0].glob(), Some("["));
1620            assert_eq!(errors[1].glob(), Some("{"));
1621            for path in ["nested/.git", "src/file.rs", "target/file.rs"] {
1622                let path = Path::new(path);
1623                assert_eq!(matcher.is_match(path), expected.is_match(path));
1624            }
1625            if path_style == PathStyle::local() {
1626                assert!(matcher.is_match(Path::new("nested/.git")));
1627                assert!(matcher.is_match(Path::new("target/file.rs")));
1628            }
1629            assert!(PathMatcher::new(patterns, path_style).is_err());
1630
1631            let mut errors = Vec::new();
1632            let matcher =
1633                PathMatcher::new_lenient(["[", "{"], path_style, |error| errors.push(error));
1634            assert_eq!(errors.len(), 2);
1635            assert_eq!(matcher.path_style, path_style);
1636            assert_eq!(matcher.sources().len(), 0);
1637            assert!(!matcher.is_match(Path::new("file.rs")));
1638
1639            let matcher = PathMatcher::new_lenient([] as [&str; 0], path_style, |_| {
1640                panic!("empty patterns are valid")
1641            });
1642            assert_eq!(matcher.path_style, path_style);
1643            assert_eq!(matcher.sources().len(), 0);
1644            assert!(!matcher.is_match(Path::new("file.rs")));
1645        }
1646    }
1647
1648    #[test]
1649    fn test_lenient_path_matcher_preserves_escaping() {
1650        for path_style in [PathStyle::Posix, PathStyle::Windows] {
1651            let patterns = [r"directory\file.rs", r"literal\*", r"literal\[name]"];
1652            let strict = PathMatcher::new(patterns, path_style).unwrap();
1653            let lenient = PathMatcher::new_lenient(patterns, path_style, |_| {
1654                panic!("valid patterns are preserved")
1655            });
1656            assert_eq!(lenient, strict);
1657            for path in [
1658                "directory/file.rs",
1659                "literal*",
1660                "literal/file.rs",
1661                "literal[name]",
1662            ] {
1663                let path = Path::new(path);
1664                assert_eq!(lenient.is_match(path), strict.is_match(path));
1665            }
1666        }
1667    }
1668
1669    #[perf]
1670    fn edge_of_glob() {
1671        let path = Path::new("/work/node_modules");
1672        let path_matcher =
1673            PathMatcher::new(&["**/node_modules/**".to_owned()], PathStyle::Posix).unwrap();
1674        assert!(
1675            path_matcher.is_match(path),
1676            "Path matcher should match {path:?}"
1677        );
1678    }
1679
1680    #[perf]
1681    fn project_search() {
1682        let path = Path::new("/Users/someonetoignore/work/zed/zed.dev/node_modules");
1683        let path_matcher =
1684            PathMatcher::new(&["**/node_modules/**".to_owned()], PathStyle::Posix).unwrap();
1685        assert!(
1686            path_matcher.is_match(path),
1687            "Path matcher should match {path:?}"
1688        );
1689    }
1690
1691    #[perf]
1692    #[cfg(target_os = "windows")]
1693    fn test_sanitized_path() {
1694        let path = Path::new("C:\\Users\\someone\\test_file.rs");
1695        let sanitized_path = SanitizedPath::new(path);
1696        assert_eq!(
1697            sanitized_path.to_string(),
1698            "C:\\Users\\someone\\test_file.rs"
1699        );
1700
1701        let path = Path::new("\\\\?\\C:\\Users\\someone\\test_file.rs");
1702        let sanitized_path = SanitizedPath::new(path);
1703        assert_eq!(
1704            sanitized_path.to_string(),
1705            "C:\\Users\\someone\\test_file.rs"
1706        );
1707    }
1708
1709    #[perf]
1710    fn test_assemble_unc_path() {
1711        assert_eq!(
1712            assemble_unc_path(
1713                OsStr::new("server"),
1714                OsStr::new("share"),
1715                [OsStr::new("file.txt")]
1716            )
1717            .as_os_str(),
1718            OsStr::new(r"\\server\share\file.txt")
1719        );
1720    }
1721
1722    /// Host stand-in for the Windows regression: rebuilding a UNC path must
1723    /// keep a component that is not valid UTF-8. Prefix stripping itself is
1724    /// covered by `test_rewrite_verbatim_unc_prefix` on Windows.
1725    #[cfg(unix)]
1726    #[perf]
1727    fn test_assemble_unc_path_keeps_non_utf8_component() {
1728        use std::os::unix::ffi::OsStrExt;
1729
1730        let non_utf8 = OsStr::from_bytes(&[0xff]);
1731        let path = assemble_unc_path(
1732            OsStr::new("server"),
1733            OsStr::new("share"),
1734            [non_utf8, OsStr::new("file.txt")],
1735        );
1736        assert!(path.to_str().is_none());
1737        let bytes = path.as_os_str().as_bytes();
1738        assert!(bytes.starts_with(br"\\server\share\"));
1739        assert!(bytes.windows(1).any(|window| window == [0xff]));
1740        assert_eq!(&bytes[bytes.len() - br"\file.txt".len()..], br"\file.txt");
1741        assert!(
1742            !bytes
1743                .windows(br"\\?\UNC\".len())
1744                .any(|window| window == br"\\?\UNC\")
1745        );
1746    }
1747
1748    #[perf]
1749    #[cfg(target_os = "windows")]
1750    fn test_rewrite_verbatim_unc_prefix() {
1751        assert_eq!(
1752            rewrite_verbatim_unc_prefix(Path::new(r"\\?\UNC\server\share\file.txt")).as_deref(),
1753            Some(Path::new(r"\\server\share\file.txt"))
1754        );
1755        assert_eq!(
1756            rewrite_verbatim_unc_prefix(Path::new(r"\\?\C:\Users\someone\test_file.rs")),
1757            None
1758        );
1759        assert_eq!(
1760            rewrite_verbatim_unc_prefix(Path::new(r"\\server\share\file.txt")),
1761            None
1762        );
1763    }
1764
1765    #[perf]
1766    #[cfg(target_os = "windows")]
1767    fn test_sanitized_path_verbatim_unc() {
1768        let path: Arc<Path> = PathBuf::from("\\\\?\\UNC\\server\\share\\file.txt").into();
1769        let sanitized_path = SanitizedPath::from_arc(path);
1770        assert_eq!(sanitized_path.to_string(), "\\\\server\\share\\file.txt");
1771
1772        // `new` returns a borrow, so it does not allocate a rewritten path.
1773        let path = Path::new("\\\\?\\UNC\\server\\share\\file.txt");
1774        assert_eq!(
1775            SanitizedPath::new(path).to_string(),
1776            "\\\\?\\UNC\\server\\share\\file.txt"
1777        );
1778    }
1779
1780    #[perf]
1781    #[cfg(target_os = "windows")]
1782    fn test_sanitized_path_verbatim_unc_non_utf8() {
1783        use std::os::windows::ffi::{OsStrExt, OsStringExt};
1784        use std::path::{Component, Prefix};
1785
1786        // Unpaired UTF-16 surrogate: a valid Windows filename that is not UTF-8.
1787        let mut wide: Vec<u16> = r"\\?\UNC\server\share\".encode_utf16().collect();
1788        wide.push(0xD800);
1789        wide.extend(r"\file.txt".encode_utf16());
1790        let path = PathBuf::from(std::ffi::OsString::from_wide(&wide));
1791        assert!(path.to_str().is_none());
1792
1793        let sanitized = SanitizedPath::from_arc(Arc::<Path>::from(path.as_path()));
1794        let mut components = sanitized.as_path().components();
1795        match components.next() {
1796            Some(Component::Prefix(prefix)) => match prefix.kind() {
1797                Prefix::UNC(server, share) => {
1798                    assert_eq!(server, "server");
1799                    assert_eq!(share, "share");
1800                }
1801                other => panic!("expected ordinary UNC prefix, got {other:?}"),
1802            },
1803            other => panic!("expected a UNC prefix, got {other:?}"),
1804        }
1805        let names: Vec<_> = components
1806            .filter_map(|component| match component {
1807                Component::Normal(name) => Some(name),
1808                Component::RootDir => None,
1809                other => panic!("unexpected component {other:?}"),
1810            })
1811            .collect();
1812        assert_eq!(names.len(), 2);
1813        assert_eq!(names[0].encode_wide().collect::<Vec<_>>(), vec![0xD800]);
1814        assert_eq!(names[1], OsStr::new("file.txt"));
1815
1816        // `new` still borrows the original path and does not allocate a rewrite.
1817        match SanitizedPath::new(&path).as_path().components().next() {
1818            Some(Component::Prefix(prefix)) => {
1819                assert!(matches!(prefix.kind(), Prefix::VerbatimUNC(_, _)));
1820            }
1821            other => panic!("expected verbatim UNC from new(), got {other:?}"),
1822        }
1823    }
1824
1825    #[perf]
1826    fn test_compare_numeric_segments() {
1827        // Helper function to create peekable iterators and test
1828        fn compare(a: &str, b: &str) -> Ordering {
1829            let mut a_iter = a.chars().peekable();
1830            let mut b_iter = b.chars().peekable();
1831
1832            let result = compare_numeric_segments(&mut a_iter, &mut b_iter);
1833
1834            // Verify iterators advanced correctly
1835            assert!(
1836                !a_iter.next().is_some_and(|c| c.is_ascii_digit()),
1837                "Iterator a should have consumed all digits"
1838            );
1839            assert!(
1840                !b_iter.next().is_some_and(|c| c.is_ascii_digit()),
1841                "Iterator b should have consumed all digits"
1842            );
1843
1844            result
1845        }
1846
1847        // Basic numeric comparisons
1848        assert_eq!(compare("0", "0"), Ordering::Equal);
1849        assert_eq!(compare("1", "2"), Ordering::Less);
1850        assert_eq!(compare("9", "10"), Ordering::Less);
1851        assert_eq!(compare("10", "9"), Ordering::Greater);
1852        assert_eq!(compare("99", "100"), Ordering::Less);
1853
1854        // Leading zeros
1855        assert_eq!(compare("0", "00"), Ordering::Less);
1856        assert_eq!(compare("00", "0"), Ordering::Greater);
1857        assert_eq!(compare("01", "1"), Ordering::Greater);
1858        assert_eq!(compare("001", "1"), Ordering::Greater);
1859        assert_eq!(compare("001", "01"), Ordering::Greater);
1860
1861        // Same value different representation
1862        assert_eq!(compare("000100", "100"), Ordering::Greater);
1863        assert_eq!(compare("100", "0100"), Ordering::Less);
1864        assert_eq!(compare("0100", "00100"), Ordering::Less);
1865
1866        // Large numbers
1867        assert_eq!(compare("9999999999", "10000000000"), Ordering::Less);
1868        assert_eq!(
1869            compare(
1870                "340282366920938463463374607431768211455", // u128::MAX
1871                "340282366920938463463374607431768211456"
1872            ),
1873            Ordering::Less
1874        );
1875        assert_eq!(
1876            compare(
1877                "340282366920938463463374607431768211456", // > u128::MAX
1878                "340282366920938463463374607431768211455"
1879            ),
1880            Ordering::Greater
1881        );
1882
1883        // Iterator advancement verification
1884        let mut a_iter = "123abc".chars().peekable();
1885        let mut b_iter = "456def".chars().peekable();
1886
1887        compare_numeric_segments(&mut a_iter, &mut b_iter);
1888
1889        assert_eq!(a_iter.collect::<String>(), "abc");
1890        assert_eq!(b_iter.collect::<String>(), "def");
1891    }
1892
1893    #[perf]
1894    fn test_natural_sort() {
1895        // Basic alphanumeric
1896        assert_eq!(natural_sort("a", "b"), Ordering::Less);
1897        assert_eq!(natural_sort("b", "a"), Ordering::Greater);
1898        assert_eq!(natural_sort("a", "a"), Ordering::Equal);
1899
1900        // Case sensitivity
1901        assert_eq!(natural_sort("a", "A"), Ordering::Less);
1902        assert_eq!(natural_sort("A", "a"), Ordering::Greater);
1903        assert_eq!(natural_sort("aA", "aa"), Ordering::Greater);
1904        assert_eq!(natural_sort("aa", "aA"), Ordering::Less);
1905
1906        // Numbers
1907        assert_eq!(natural_sort("1", "2"), Ordering::Less);
1908        assert_eq!(natural_sort("2", "10"), Ordering::Less);
1909        assert_eq!(natural_sort("02", "10"), Ordering::Less);
1910        assert_eq!(natural_sort("02", "2"), Ordering::Greater);
1911
1912        // Mixed alphanumeric
1913        assert_eq!(natural_sort("a1", "a2"), Ordering::Less);
1914        assert_eq!(natural_sort("a2", "a10"), Ordering::Less);
1915        assert_eq!(natural_sort("a02", "a2"), Ordering::Greater);
1916        assert_eq!(natural_sort("a1b", "a1c"), Ordering::Less);
1917
1918        // Multiple numeric segments
1919        assert_eq!(natural_sort("1a2", "1a10"), Ordering::Less);
1920        assert_eq!(natural_sort("1a10", "1a2"), Ordering::Greater);
1921        assert_eq!(natural_sort("2a1", "10a1"), Ordering::Less);
1922
1923        // Special characters
1924        assert_eq!(natural_sort("a-1", "a-2"), Ordering::Less);
1925        assert_eq!(natural_sort("a_1", "a_2"), Ordering::Less);
1926        assert_eq!(natural_sort("a.1", "a.2"), Ordering::Less);
1927
1928        // Unicode
1929        assert_eq!(natural_sort("文1", "文2"), Ordering::Less);
1930        assert_eq!(natural_sort("文2", "文10"), Ordering::Less);
1931        assert_eq!(natural_sort("🔤1", "🔤2"), Ordering::Less);
1932
1933        // Empty and special cases
1934        assert_eq!(natural_sort("", ""), Ordering::Equal);
1935        assert_eq!(natural_sort("", "a"), Ordering::Less);
1936        assert_eq!(natural_sort("a", ""), Ordering::Greater);
1937        assert_eq!(natural_sort(" ", "  "), Ordering::Less);
1938
1939        // Mixed everything
1940        assert_eq!(natural_sort("File-1.txt", "File-2.txt"), Ordering::Less);
1941        assert_eq!(natural_sort("File-02.txt", "File-2.txt"), Ordering::Greater);
1942        assert_eq!(natural_sort("File-2.txt", "File-10.txt"), Ordering::Less);
1943        assert_eq!(natural_sort("File_A1", "File_A2"), Ordering::Less);
1944        assert_eq!(natural_sort("File_a1", "File_A1"), Ordering::Less);
1945    }
1946
1947    #[perf]
1948    fn test_compare_paths() {
1949        // Helper function for cleaner tests
1950        fn compare(a: &str, is_a_file: bool, b: &str, is_b_file: bool) -> Ordering {
1951            compare_paths((Path::new(a), is_a_file), (Path::new(b), is_b_file))
1952        }
1953
1954        // Basic path comparison
1955        assert_eq!(compare("a", true, "b", true), Ordering::Less);
1956        assert_eq!(compare("b", true, "a", true), Ordering::Greater);
1957        assert_eq!(compare("a", true, "a", true), Ordering::Equal);
1958
1959        // Files vs Directories
1960        assert_eq!(compare("a", true, "a", false), Ordering::Greater);
1961        assert_eq!(compare("a", false, "a", true), Ordering::Less);
1962        assert_eq!(compare("b", false, "a", true), Ordering::Less);
1963
1964        // Extensions
1965        assert_eq!(compare("a.txt", true, "a.md", true), Ordering::Greater);
1966        assert_eq!(compare("a.md", true, "a.txt", true), Ordering::Less);
1967        assert_eq!(compare("a", true, "a.txt", true), Ordering::Less);
1968
1969        // Nested paths
1970        assert_eq!(compare("dir/a", true, "dir/b", true), Ordering::Less);
1971        assert_eq!(compare("dir1/a", true, "dir2/a", true), Ordering::Less);
1972        assert_eq!(compare("dir/sub/a", true, "dir/a", true), Ordering::Less);
1973
1974        // Case sensitivity in paths
1975        assert_eq!(
1976            compare("Dir/file", true, "dir/file", true),
1977            Ordering::Greater
1978        );
1979        assert_eq!(
1980            compare("dir/File", true, "dir/file", true),
1981            Ordering::Greater
1982        );
1983        assert_eq!(compare("dir/file", true, "Dir/File", true), Ordering::Less);
1984
1985        // Hidden files and special names
1986        assert_eq!(compare(".hidden", true, "visible", true), Ordering::Less);
1987        assert_eq!(compare("_special", true, "normal", true), Ordering::Less);
1988        assert_eq!(compare(".config", false, ".data", false), Ordering::Less);
1989
1990        // Mixed numeric paths
1991        assert_eq!(
1992            compare("dir1/file", true, "dir2/file", true),
1993            Ordering::Less
1994        );
1995        assert_eq!(
1996            compare("dir2/file", true, "dir10/file", true),
1997            Ordering::Less
1998        );
1999        assert_eq!(
2000            compare("dir02/file", true, "dir2/file", true),
2001            Ordering::Greater
2002        );
2003
2004        // Root paths
2005        assert_eq!(compare("/a", true, "/b", true), Ordering::Less);
2006        assert_eq!(compare("/", false, "/a", true), Ordering::Less);
2007
2008        // Complex real-world examples
2009        assert_eq!(
2010            compare("project/src/main.rs", true, "project/src/lib.rs", true),
2011            Ordering::Greater
2012        );
2013        assert_eq!(
2014            compare(
2015                "project/tests/test_1.rs",
2016                true,
2017                "project/tests/test_2.rs",
2018                true
2019            ),
2020            Ordering::Less
2021        );
2022        assert_eq!(
2023            compare(
2024                "project/v1.0.0/README.md",
2025                true,
2026                "project/v1.10.0/README.md",
2027                true
2028            ),
2029            Ordering::Less
2030        );
2031    }
2032
2033    #[perf]
2034    fn test_natural_sort_case_sensitivity() {
2035        std::thread::sleep(std::time::Duration::from_millis(100));
2036        // Same letter different case - lowercase should come first
2037        assert_eq!(natural_sort("a", "A"), Ordering::Less);
2038        assert_eq!(natural_sort("A", "a"), Ordering::Greater);
2039        assert_eq!(natural_sort("a", "a"), Ordering::Equal);
2040        assert_eq!(natural_sort("A", "A"), Ordering::Equal);
2041
2042        // Mixed case strings
2043        assert_eq!(natural_sort("aaa", "AAA"), Ordering::Less);
2044        assert_eq!(natural_sort("AAA", "aaa"), Ordering::Greater);
2045        assert_eq!(natural_sort("aAa", "AaA"), Ordering::Less);
2046
2047        // Different letters
2048        assert_eq!(natural_sort("a", "b"), Ordering::Less);
2049        assert_eq!(natural_sort("A", "b"), Ordering::Less);
2050        assert_eq!(natural_sort("a", "B"), Ordering::Less);
2051    }
2052
2053    #[perf]
2054    fn test_natural_sort_with_numbers() {
2055        // Basic number ordering
2056        assert_eq!(natural_sort("file1", "file2"), Ordering::Less);
2057        assert_eq!(natural_sort("file2", "file10"), Ordering::Less);
2058        assert_eq!(natural_sort("file10", "file2"), Ordering::Greater);
2059
2060        // Numbers in different positions
2061        assert_eq!(natural_sort("1file", "2file"), Ordering::Less);
2062        assert_eq!(natural_sort("file1text", "file2text"), Ordering::Less);
2063        assert_eq!(natural_sort("text1file", "text2file"), Ordering::Less);
2064
2065        // Multiple numbers in string
2066        assert_eq!(natural_sort("file1-2", "file1-10"), Ordering::Less);
2067        assert_eq!(natural_sort("2-1file", "10-1file"), Ordering::Less);
2068
2069        // Leading zeros
2070        assert_eq!(natural_sort("file002", "file2"), Ordering::Greater);
2071        assert_eq!(natural_sort("file002", "file10"), Ordering::Less);
2072
2073        // Very large numbers
2074        assert_eq!(
2075            natural_sort("file999999999999999999999", "file999999999999999999998"),
2076            Ordering::Greater
2077        );
2078
2079        // u128 edge cases
2080
2081        // Numbers near u128::MAX (340,282,366,920,938,463,463,374,607,431,768,211,455)
2082        assert_eq!(
2083            natural_sort(
2084                "file340282366920938463463374607431768211454",
2085                "file340282366920938463463374607431768211455"
2086            ),
2087            Ordering::Less
2088        );
2089
2090        // Equal length numbers that overflow u128
2091        assert_eq!(
2092            natural_sort(
2093                "file340282366920938463463374607431768211456",
2094                "file340282366920938463463374607431768211455"
2095            ),
2096            Ordering::Greater
2097        );
2098
2099        // Different length numbers that overflow u128
2100        assert_eq!(
2101            natural_sort(
2102                "file3402823669209384634633746074317682114560",
2103                "file340282366920938463463374607431768211455"
2104            ),
2105            Ordering::Greater
2106        );
2107
2108        // Leading zeros with numbers near u128::MAX
2109        assert_eq!(
2110            natural_sort(
2111                "file0340282366920938463463374607431768211455",
2112                "file340282366920938463463374607431768211455"
2113            ),
2114            Ordering::Greater
2115        );
2116
2117        // Very large numbers with different lengths (both overflow u128)
2118        assert_eq!(
2119            natural_sort(
2120                "file999999999999999999999999999999999999999999999999",
2121                "file9999999999999999999999999999999999999999999999999"
2122            ),
2123            Ordering::Less
2124        );
2125
2126        // Mixed case with numbers
2127        assert_eq!(natural_sort("File1", "file2"), Ordering::Greater);
2128        assert_eq!(natural_sort("file1", "File2"), Ordering::Less);
2129    }
2130
2131    #[perf]
2132    fn test_natural_sort_edge_cases() {
2133        // Empty strings
2134        assert_eq!(natural_sort("", ""), Ordering::Equal);
2135        assert_eq!(natural_sort("", "a"), Ordering::Less);
2136        assert_eq!(natural_sort("a", ""), Ordering::Greater);
2137
2138        // Special characters
2139        assert_eq!(natural_sort("file-1", "file_1"), Ordering::Less);
2140        assert_eq!(natural_sort("file.1", "file_1"), Ordering::Less);
2141        assert_eq!(natural_sort("file 1", "file_1"), Ordering::Less);
2142
2143        // Unicode characters
2144        // 9312 vs 9313
2145        assert_eq!(natural_sort("file①", "file②"), Ordering::Less);
2146        // 9321 vs 9313
2147        assert_eq!(natural_sort("file⑩", "file②"), Ordering::Greater);
2148        // 28450 vs 23383
2149        assert_eq!(natural_sort("file漢", "file字"), Ordering::Greater);
2150
2151        // Mixed alphanumeric with special chars
2152        assert_eq!(natural_sort("file-1a", "file-1b"), Ordering::Less);
2153        assert_eq!(natural_sort("file-1.2", "file-1.10"), Ordering::Less);
2154        assert_eq!(natural_sort("file-1.10", "file-1.2"), Ordering::Greater);
2155    }
2156
2157    #[test]
2158    fn test_multiple_extensions() {
2159        // No extensions
2160        let path = Path::new("/a/b/c/file_name");
2161        assert_eq!(path.multiple_extensions(), None);
2162
2163        // Only one extension
2164        let path = Path::new("/a/b/c/file_name.tsx");
2165        assert_eq!(path.multiple_extensions(), None);
2166
2167        // Stories sample extension
2168        let path = Path::new("/a/b/c/file_name.stories.tsx");
2169        assert_eq!(path.multiple_extensions(), Some("stories.tsx".to_string()));
2170
2171        // Longer sample extension
2172        let path = Path::new("/a/b/c/long.app.tar.gz");
2173        assert_eq!(path.multiple_extensions(), Some("app.tar.gz".to_string()));
2174    }
2175
2176    #[test]
2177    fn test_strip_path_suffix() {
2178        let base = Path::new("/a/b/c/file_name");
2179        let suffix = Path::new("file_name");
2180        assert_eq!(strip_path_suffix(base, suffix), Some(Path::new("/a/b/c")));
2181
2182        let base = Path::new("/a/b/c/file_name.tsx");
2183        let suffix = Path::new("file_name.tsx");
2184        assert_eq!(strip_path_suffix(base, suffix), Some(Path::new("/a/b/c")));
2185
2186        let base = Path::new("/a/b/c/file_name.stories.tsx");
2187        let suffix = Path::new("c/file_name.stories.tsx");
2188        assert_eq!(strip_path_suffix(base, suffix), Some(Path::new("/a/b")));
2189
2190        let base = Path::new("/a/b/c/long.app.tar.gz");
2191        let suffix = Path::new("b/c/long.app.tar.gz");
2192        assert_eq!(strip_path_suffix(base, suffix), Some(Path::new("/a")));
2193
2194        let base = Path::new("/a/b/c/long.app.tar.gz");
2195        let suffix = Path::new("/a/b/c/long.app.tar.gz");
2196        assert_eq!(strip_path_suffix(base, suffix), Some(Path::new("")));
2197
2198        let base = Path::new("/a/b/c/long.app.tar.gz");
2199        let suffix = Path::new("/a/b/c/no_match.app.tar.gz");
2200        assert_eq!(strip_path_suffix(base, suffix), None);
2201
2202        let base = Path::new("/a/b/c/long.app.tar.gz");
2203        let suffix = Path::new("app.tar.gz");
2204        assert_eq!(strip_path_suffix(base, suffix), None);
2205    }
2206}