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