1use globset::{GlobBuilder, GlobSet, GlobSetBuilder};
2use itertools::Itertools;
3use regex::Regex;
4use serde::{Deserialize, Serialize};
5use std::borrow::Cow;
6use std::cmp::Ordering;
7use std::error::Error;
8use std::fmt::{Display, Formatter};
9use std::mem;
10use std::path::StripPrefixError;
11use std::sync::Arc;
12use std::{
13 ffi::OsStr,
14 path::{Path, PathBuf},
15 sync::LazyLock,
16};
17
18use crate::rel_path::RelPath;
19use crate::rel_path::RelPathBuf;
20
21pub fn home_dir() -> &'static PathBuf {
23 static HOME_DIR: std::sync::OnceLock<PathBuf> = std::sync::OnceLock::new();
24 HOME_DIR.get_or_init(|| {
25 if cfg!(any(test, feature = "test-support")) {
26 if cfg!(target_os = "macos") {
27 PathBuf::from("/Users/zed")
28 } else if cfg!(target_os = "windows") {
29 PathBuf::from("C:\\Users\\zed")
30 } else {
31 PathBuf::from("/home/zed")
32 }
33 } else {
34 dirs::home_dir().expect("failed to determine home directory")
35 }
36 })
37}
38
39pub trait PathExt {
40 fn compact(&self) -> PathBuf;
49
50 fn extension_or_hidden_file_name(&self) -> Option<&str>;
52
53 fn try_from_bytes<'a>(bytes: &'a [u8]) -> anyhow::Result<Self>
54 where
55 Self: From<&'a Path>,
56 {
57 #[cfg(target_family = "wasm")]
58 {
59 std::str::from_utf8(bytes)
60 .map(Path::new)
61 .map(Into::into)
62 .map_err(Into::into)
63 }
64 #[cfg(unix)]
65 {
66 use std::os::unix::prelude::OsStrExt;
67 Ok(Self::from(Path::new(OsStr::from_bytes(bytes))))
68 }
69 #[cfg(windows)]
70 {
71 use anyhow::Context;
72 use tendril::fmt::{Format, WTF8};
73 WTF8::validate(bytes)
74 .then(|| {
75 Self::from(Path::new(unsafe {
77 OsStr::from_encoded_bytes_unchecked(bytes)
78 }))
79 })
80 .with_context(|| format!("Invalid WTF-8 sequence: {bytes:?}"))
81 }
82 }
83
84 fn local_to_wsl(&self) -> Option<PathBuf>;
87
88 fn multiple_extensions(&self) -> Option<String>;
93
94 #[cfg(not(target_family = "wasm"))]
96 fn try_shell_safe(&self, shell_kind: crate::shell::ShellKind) -> anyhow::Result<String>;
97}
98
99impl<T: AsRef<Path>> PathExt for T {
100 fn compact(&self) -> PathBuf {
101 #[cfg(target_family = "wasm")]
102 {
103 self.as_ref().to_path_buf()
104 }
105 #[cfg(not(target_family = "wasm"))]
106 if cfg!(any(target_os = "linux", target_os = "freebsd")) || cfg!(target_os = "macos") {
107 match self.as_ref().strip_prefix(home_dir().as_path()) {
108 Ok(relative_path) => {
109 let mut shortened_path = PathBuf::new();
110 shortened_path.push("~");
111 shortened_path.push(relative_path);
112 shortened_path
113 }
114 Err(_) => self.as_ref().to_path_buf(),
115 }
116 } else {
117 self.as_ref().to_path_buf()
118 }
119 }
120
121 fn extension_or_hidden_file_name(&self) -> Option<&str> {
122 let path = self.as_ref();
123 let file_name = path.file_name()?.to_str()?;
124 if file_name.starts_with('.') {
125 return file_name.strip_prefix('.');
126 }
127
128 path.extension()
129 .and_then(|e| e.to_str())
130 .or_else(|| path.file_stem()?.to_str())
131 }
132
133 fn local_to_wsl(&self) -> Option<PathBuf> {
134 let mut new_path = std::ffi::OsString::new();
137 for component in self.as_ref().components() {
138 match component {
139 std::path::Component::Prefix(prefix) => {
140 let drive_letter = prefix.as_os_str().to_string_lossy().to_lowercase();
141 let drive_letter = drive_letter.strip_suffix(':')?;
142
143 new_path.push(format!("/mnt/{}", drive_letter));
144 }
145 std::path::Component::RootDir => {}
146 std::path::Component::CurDir => {
147 new_path.push("/.");
148 }
149 std::path::Component::ParentDir => {
150 new_path.push("/..");
151 }
152 std::path::Component::Normal(os_str) => {
153 new_path.push("/");
154 new_path.push(os_str);
155 }
156 }
157 }
158
159 Some(new_path.into())
160 }
161
162 fn multiple_extensions(&self) -> Option<String> {
163 let path = self.as_ref();
164 let file_name = path.file_name()?.to_str()?;
165
166 let parts: Vec<&str> = file_name
167 .split('.')
168 .skip(1)
170 .collect();
171
172 if parts.len() < 2 {
173 return None;
174 }
175
176 Some(parts.into_iter().join("."))
177 }
178
179 #[cfg(not(target_family = "wasm"))]
180 fn try_shell_safe(&self, shell_kind: crate::shell::ShellKind) -> anyhow::Result<String> {
181 use anyhow::Context;
182 let path_str = self
183 .as_ref()
184 .to_str()
185 .with_context(|| "Path contains invalid UTF-8")?;
186 shell_kind
187 .try_quote(path_str)
188 .as_deref()
189 .map(ToOwned::to_owned)
190 .context("Failed to quote path")
191 }
192}
193
194pub fn path_ends_with(base: &Path, suffix: &Path) -> bool {
195 strip_path_suffix(base, suffix).is_some()
196}
197
198pub fn component_matches_ignore_ascii_case(component: &OsStr, name: &str) -> bool {
206 component
207 .to_str()
208 .is_some_and(|s| s.eq_ignore_ascii_case(name))
209}
210
211pub fn strip_path_suffix<'a>(base: &'a Path, suffix: &Path) -> Option<&'a Path> {
212 if let Some(remainder) = base
213 .as_os_str()
214 .as_encoded_bytes()
215 .strip_suffix(suffix.as_os_str().as_encoded_bytes())
216 {
217 if remainder
218 .last()
219 .is_none_or(|last_byte| std::path::is_separator(*last_byte as char))
220 {
221 let os_str = unsafe {
222 OsStr::from_encoded_bytes_unchecked(
223 &remainder[0..remainder.len().saturating_sub(1)],
224 )
225 };
226 return Some(Path::new(os_str));
227 }
228 }
229 None
230}
231
232#[derive(Eq, PartialEq, Hash, Ord, PartialOrd)]
235#[repr(transparent)]
236pub struct SanitizedPath(Path);
237
238impl SanitizedPath {
239 pub fn new<T: AsRef<Path> + ?Sized>(path: &T) -> &Self {
240 #[cfg(not(target_os = "windows"))]
241 return Self::unchecked_new(path.as_ref());
242
243 #[cfg(target_os = "windows")]
244 return Self::unchecked_new(dunce::simplified(path.as_ref()));
245 }
246
247 pub fn unchecked_new<T: AsRef<Path> + ?Sized>(path: &T) -> &Self {
248 unsafe { mem::transmute::<&Path, &Self>(path.as_ref()) }
250 }
251
252 pub fn from_arc(path: Arc<Path>) -> Arc<Self> {
253 #[cfg(not(target_os = "windows"))]
255 return unsafe { mem::transmute::<Arc<Path>, Arc<Self>>(path) };
256
257 #[cfg(target_os = "windows")]
258 {
259 let simplified = dunce::simplified(path.as_ref());
260 if simplified == path.as_ref() {
261 unsafe { mem::transmute::<Arc<Path>, Arc<Self>>(path) }
263 } else {
264 Self::unchecked_new(simplified).into()
265 }
266 }
267 }
268
269 pub fn new_arc<T: AsRef<Path> + ?Sized>(path: &T) -> Arc<Self> {
270 Self::new(path).into()
271 }
272
273 pub fn cast_arc(path: Arc<Self>) -> Arc<Path> {
274 unsafe { mem::transmute::<Arc<Self>, Arc<Path>>(path) }
276 }
277
278 pub fn cast_arc_ref(path: &Arc<Self>) -> &Arc<Path> {
279 unsafe { mem::transmute::<&Arc<Self>, &Arc<Path>>(path) }
281 }
282
283 pub fn starts_with(&self, prefix: &Self) -> bool {
284 self.0.starts_with(&prefix.0)
285 }
286
287 pub fn as_path(&self) -> &Path {
288 &self.0
289 }
290
291 pub fn file_name(&self) -> Option<&std::ffi::OsStr> {
292 self.0.file_name()
293 }
294
295 pub fn extension(&self) -> Option<&std::ffi::OsStr> {
296 self.0.extension()
297 }
298
299 pub fn join<P: AsRef<Path>>(&self, path: P) -> PathBuf {
300 self.0.join(path)
301 }
302
303 pub fn parent(&self) -> Option<&Self> {
304 self.0.parent().map(Self::unchecked_new)
305 }
306
307 pub fn strip_prefix(&self, base: &Self) -> Result<&Path, StripPrefixError> {
308 self.0.strip_prefix(base.as_path())
309 }
310
311 pub fn to_str(&self) -> Option<&str> {
312 self.0.to_str()
313 }
314
315 pub fn to_path_buf(&self) -> PathBuf {
316 self.0.to_path_buf()
317 }
318}
319
320impl std::fmt::Debug for SanitizedPath {
321 fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
322 std::fmt::Debug::fmt(&self.0, formatter)
323 }
324}
325
326impl Display for SanitizedPath {
327 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
328 write!(f, "{}", self.0.display())
329 }
330}
331
332impl From<&SanitizedPath> for Arc<SanitizedPath> {
333 fn from(sanitized_path: &SanitizedPath) -> Self {
334 let path: Arc<Path> = sanitized_path.0.into();
335 unsafe { mem::transmute(path) }
337 }
338}
339
340impl From<&SanitizedPath> for PathBuf {
341 fn from(sanitized_path: &SanitizedPath) -> Self {
342 sanitized_path.as_path().into()
343 }
344}
345
346impl AsRef<Path> for SanitizedPath {
347 fn as_ref(&self) -> &Path {
348 &self.0
349 }
350}
351
352#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
353pub enum PathStyle {
354 Posix,
355 Windows,
356}
357
358impl PathStyle {
359 #[cfg(target_os = "windows")]
360 pub const fn local() -> Self {
361 PathStyle::Windows
362 }
363
364 #[cfg(not(target_os = "windows"))]
365 pub const fn local() -> Self {
366 PathStyle::Posix
367 }
368
369 #[inline]
370 pub fn primary_separator(&self) -> &'static str {
371 match self {
372 PathStyle::Posix => "/",
373 PathStyle::Windows => "\\",
374 }
375 }
376
377 pub fn separators(&self) -> &'static [&'static str] {
378 match self {
379 PathStyle::Posix => &["/"],
380 PathStyle::Windows => &["\\", "/"],
381 }
382 }
383
384 pub fn separators_ch(&self) -> &'static [char] {
385 match self {
386 PathStyle::Posix => &['/'],
387 PathStyle::Windows => &['\\', '/'],
388 }
389 }
390
391 pub fn is_absolute(&self, path_like: &str) -> bool {
392 path_like.starts_with('/')
393 || *self == PathStyle::Windows
394 && (path_like.starts_with('\\')
395 || path_like
396 .chars()
397 .next()
398 .is_some_and(|c| c.is_ascii_alphabetic())
399 && path_like[1..]
400 .strip_prefix(':')
401 .is_some_and(|path| path.starts_with('/') || path.starts_with('\\')))
402 }
403
404 pub fn is_windows(&self) -> bool {
405 *self == PathStyle::Windows
406 }
407
408 pub fn is_posix(&self) -> bool {
409 *self == PathStyle::Posix
410 }
411
412 pub fn join(self, left: impl AsRef<Path>, right: impl AsRef<Path>) -> Option<String> {
413 let right = right.as_ref().to_str()?;
414 if is_absolute(right, self) {
415 return None;
416 }
417 let left = left.as_ref().to_str()?;
418 if left.is_empty() {
419 Some(right.into())
420 } else {
421 Some(format!(
422 "{left}{}{right}",
423 if left.ends_with(self.primary_separator()) {
424 ""
425 } else {
426 self.primary_separator()
427 }
428 ))
429 }
430 }
431
432 pub fn join_path(
433 self,
434 left: impl AsRef<Path>,
435 right: impl AsRef<Path>,
436 ) -> anyhow::Result<PathBuf> {
437 let left = left
438 .as_ref()
439 .to_str()
440 .ok_or_else(|| anyhow::anyhow!("Path contains invalid UTF-8"))?;
441 let right = right.as_ref();
442 let right_string = right
443 .to_str()
444 .ok_or_else(|| anyhow::anyhow!("Path contains invalid UTF-8"))?;
445 let joined = self
446 .join(left, right_string)
447 .ok_or_else(|| anyhow::anyhow!("Path must be relative: {right:?}"))?;
448 Ok(PathBuf::from(self.normalize(&joined)))
449 }
450
451 pub fn normalize(self, path_like: &str) -> String {
452 match self {
453 PathStyle::Windows => crate::normalize_path(Path::new(path_like))
454 .to_string_lossy()
455 .into_owned(),
456 PathStyle::Posix => {
457 let is_absolute = path_like.starts_with('/');
458 let remainder = if is_absolute {
459 path_like.trim_start_matches('/')
460 } else {
461 path_like
462 };
463
464 let mut components = Vec::new();
465 for component in remainder.split(self.separators_ch()) {
466 match component {
467 "" | "." => {}
468 ".." => {
469 if components
470 .last()
471 .is_some_and(|component| *component != "..")
472 {
473 components.pop();
474 } else if !is_absolute {
475 components.push(component);
476 }
477 }
478 component => components.push(component),
479 }
480 }
481
482 let normalized = components.join(self.primary_separator());
483 if is_absolute && normalized.is_empty() {
484 "/".to_string()
485 } else if is_absolute {
486 format!("/{normalized}")
487 } else {
488 normalized
489 }
490 }
491 }
492 }
493
494 pub fn split(self, path_like: &str) -> (Option<&str>, &str) {
495 let Some(pos) = path_like.rfind(self.primary_separator()) else {
496 return (None, path_like);
497 };
498 let filename_start = pos + self.primary_separator().len();
499 (
500 Some(&path_like[..filename_start]),
501 &path_like[filename_start..],
502 )
503 }
504
505 pub fn strip_prefix<'a>(
506 &self,
507 child: &'a Path,
508 parent: &'a Path,
509 ) -> Option<std::borrow::Cow<'a, RelPath>> {
510 let parent = parent.to_str()?;
511 if parent.is_empty() {
512 return RelPath::new(child, *self).ok();
513 }
514 let parent = self
515 .separators()
516 .iter()
517 .find_map(|sep| parent.strip_suffix(sep))
518 .unwrap_or(parent);
519 let child = child.to_str()?;
520
521 let stripped = if self.is_windows()
523 && child.as_bytes().get(1) == Some(&b':')
524 && parent.as_bytes().get(1) == Some(&b':')
525 && child.as_bytes()[0].eq_ignore_ascii_case(&parent.as_bytes()[0])
526 {
527 child[2..].strip_prefix(&parent[2..])?
528 } else {
529 child.strip_prefix(parent)?
530 };
531 if let Some(relative) = self
532 .separators()
533 .iter()
534 .find_map(|sep| stripped.strip_prefix(sep))
535 {
536 RelPath::new(relative.as_ref(), *self).ok()
537 } else if stripped.is_empty() {
538 Some(Cow::Borrowed(RelPath::empty()))
539 } else {
540 None
541 }
542 }
543}
544
545#[derive(Debug, Clone)]
546pub struct RemotePathBuf {
547 style: PathStyle,
548 string: String,
549}
550
551impl RemotePathBuf {
552 pub fn new(string: String, style: PathStyle) -> Self {
553 Self { style, string }
554 }
555
556 pub fn from_str(path: &str, style: PathStyle) -> Self {
557 Self::new(path.to_string(), style)
558 }
559
560 pub fn path_style(&self) -> PathStyle {
561 self.style
562 }
563
564 pub fn to_proto(self) -> String {
565 self.string
566 }
567}
568
569impl Display for RemotePathBuf {
570 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
571 write!(f, "{}", self.string)
572 }
573}
574
575pub fn is_absolute(path_like: &str, path_style: PathStyle) -> bool {
576 path_like.starts_with('/')
577 || path_style == PathStyle::Windows
578 && (path_like.starts_with('\\')
579 || path_like
580 .chars()
581 .next()
582 .is_some_and(|c| c.is_ascii_alphabetic())
583 && path_like[1..]
584 .strip_prefix(':')
585 .is_some_and(|path| path.starts_with('/') || path.starts_with('\\')))
586}
587
588#[derive(Debug, PartialEq)]
589#[non_exhaustive]
590pub struct NormalizeError;
591
592impl Error for NormalizeError {}
593
594impl std::fmt::Display for NormalizeError {
595 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
596 f.write_str("parent reference `..` points outside of base directory")
597 }
598}
599
600pub fn normalize_lexically(path: &Path) -> Result<PathBuf, NormalizeError> {
617 use std::path::Component;
618
619 let mut lexical = PathBuf::new();
620 let mut iter = path.components().peekable();
621
622 let root = match iter.peek() {
626 Some(Component::ParentDir) => return Err(NormalizeError),
627 Some(p @ Component::RootDir) | Some(p @ Component::CurDir) => {
628 lexical.push(p);
629 iter.next();
630 lexical.as_os_str().len()
631 }
632 Some(Component::Prefix(prefix)) => {
633 lexical.push(prefix.as_os_str());
634 iter.next();
635 if let Some(p @ Component::RootDir) = iter.peek() {
636 lexical.push(p);
637 iter.next();
638 }
639 lexical.as_os_str().len()
640 }
641 None => return Ok(PathBuf::new()),
642 Some(Component::Normal(_)) => 0,
643 };
644
645 for component in iter {
646 match component {
647 Component::RootDir => unreachable!(),
648 Component::Prefix(_) => return Err(NormalizeError),
649 Component::CurDir => continue,
650 Component::ParentDir => {
651 if lexical.as_os_str().len() == root {
653 return Err(NormalizeError);
654 } else {
655 lexical.pop();
656 }
657 }
658 Component::Normal(path) => lexical.push(path),
659 }
660 }
661 Ok(lexical)
662}
663
664pub fn insert_subtree(subtrees: &mut Vec<PathBuf>, path: PathBuf) {
673 if subtrees.iter().any(|existing| path.starts_with(existing)) {
674 return;
675 }
676 subtrees.retain(|existing| !existing.starts_with(&path));
677 subtrees.push(path);
678}
679
680pub fn path_within_subtree<'a>(path: &Path, mut subtrees: impl Iterator<Item = &'a Path>) -> bool {
684 subtrees.any(|granted| path.starts_with(granted))
685}
686
687pub const FILE_ROW_COLUMN_DELIMITER: char = ':';
689
690const ROW_COL_CAPTURE_REGEX: &str = r"(?xs)
691 ([^\(]+)\:(?:
692 \((\d+)[,:](\d+)\) # filename:(row,column), filename:(row:column)
693 |
694 \((\d+)\)() # filename:(row)
695 )
696 |
697 ([^\(]+)(?:
698 \((\d+)[,:](\d+)\) # filename(row,column), filename(row:column)
699 |
700 \((\d+)\)() # filename(row)
701 )
702 \:*$
703 |
704 (.+?)(?:
705 \:+(\d+)\:(\d+)\:*$ # filename:row:column
706 |
707 \:+(\d+)\:*()$ # filename:row
708 |
709 \:+()()$
710 )";
711
712#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)]
715pub struct PathWithPosition {
716 pub path: PathBuf,
717 pub row: Option<u32>,
718 pub column: Option<u32>,
720}
721
722impl PathWithPosition {
723 pub fn from_path(path: PathBuf) -> Self {
725 Self {
726 path,
727 row: None,
728 column: None,
729 }
730 }
731
732 pub fn parse_str(s: &str) -> Self {
818 let trimmed = s.trim();
819 let path = Path::new(trimmed);
820 let Some(maybe_file_name_with_row_col) = path.file_name().unwrap_or_default().to_str()
821 else {
822 return Self {
823 path: Path::new(s).to_path_buf(),
824 row: None,
825 column: None,
826 };
827 };
828 if maybe_file_name_with_row_col.is_empty() {
829 return Self {
830 path: Path::new(s).to_path_buf(),
831 row: None,
832 column: None,
833 };
834 }
835
836 static SUFFIX_RE: LazyLock<Regex> =
840 LazyLock::new(|| Regex::new(ROW_COL_CAPTURE_REGEX).unwrap());
841 match SUFFIX_RE
842 .captures(maybe_file_name_with_row_col)
843 .map(|caps| caps.extract())
844 {
845 Some((_, [file_name, maybe_row, maybe_column])) => {
846 let row = maybe_row.parse::<u32>().ok();
847 let column = maybe_column.parse::<u32>().ok();
848
849 let (_, suffix) = trimmed.split_once(file_name).unwrap();
850 let path_without_suffix = &trimmed[..trimmed.len() - suffix.len()];
851
852 Self {
853 path: Path::new(path_without_suffix).to_path_buf(),
854 row,
855 column,
856 }
857 }
858 None => {
859 let delimiter = ':';
863 let mut path_parts = s
864 .rsplitn(3, delimiter)
865 .collect::<Vec<_>>()
866 .into_iter()
867 .rev()
868 .fuse();
869 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();
870 let mut row = None;
871 let mut column = None;
872 if let Some(maybe_row) = path_parts.next() {
873 if let Ok(parsed_row) = maybe_row.parse::<u32>() {
874 row = Some(parsed_row);
875 if let Some(parsed_column) = path_parts
876 .next()
877 .and_then(|maybe_col| maybe_col.parse::<u32>().ok())
878 {
879 column = Some(parsed_column);
880 }
881 } else {
882 path_string.push(delimiter);
883 path_string.push_str(maybe_row);
884 }
885 }
886 for split in path_parts {
887 path_string.push(delimiter);
888 path_string.push_str(split);
889 }
890
891 Self {
892 path: PathBuf::from(path_string),
893 row,
894 column,
895 }
896 }
897 }
898 }
899
900 pub fn map_path<E>(
901 self,
902 mapping: impl FnOnce(PathBuf) -> Result<PathBuf, E>,
903 ) -> Result<PathWithPosition, E> {
904 Ok(PathWithPosition {
905 path: mapping(self.path)?,
906 row: self.row,
907 column: self.column,
908 })
909 }
910
911 pub fn to_string(&self, path_to_string: &dyn Fn(&PathBuf) -> String) -> String {
912 let path_string = path_to_string(&self.path);
913 if let Some(row) = self.row {
914 if let Some(column) = self.column {
915 format!("{path_string}:{row}:{column}")
916 } else {
917 format!("{path_string}:{row}")
918 }
919 } else {
920 path_string
921 }
922 }
923}
924
925#[derive(Clone)]
926pub struct PathMatcher {
927 sources: Vec<(String, RelPathBuf, bool)>,
928 glob: GlobSet,
929 path_style: PathStyle,
930}
931
932impl std::fmt::Debug for PathMatcher {
933 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
934 f.debug_struct("PathMatcher")
935 .field("sources", &self.sources)
936 .field("path_style", &self.path_style)
937 .finish()
938 }
939}
940
941impl PartialEq for PathMatcher {
942 fn eq(&self, other: &Self) -> bool {
943 self.sources.eq(&other.sources)
944 }
945}
946
947impl Eq for PathMatcher {}
948
949impl PathMatcher {
950 pub fn new(
951 globs: impl IntoIterator<Item = impl AsRef<str>>,
952 path_style: PathStyle,
953 ) -> Result<Self, globset::Error> {
954 let globs = globs
955 .into_iter()
956 .map(|as_str| {
957 GlobBuilder::new(as_str.as_ref())
958 .backslash_escape(path_style.is_posix())
959 .build()
960 })
961 .collect::<Result<Vec<_>, _>>()?;
962 let sources = globs
963 .iter()
964 .filter_map(|glob| {
965 let glob = glob.glob();
966 Some((
967 glob.to_string(),
968 RelPath::new(&glob.as_ref(), path_style)
969 .ok()
970 .map(std::borrow::Cow::into_owned)?,
971 glob.ends_with(path_style.separators_ch()),
972 ))
973 })
974 .collect();
975 let mut glob_builder = GlobSetBuilder::new();
976 for single_glob in globs {
977 glob_builder.add(single_glob);
978 }
979 let glob = glob_builder.build()?;
980 Ok(PathMatcher {
981 glob,
982 sources,
983 path_style,
984 })
985 }
986
987 pub fn sources(&self) -> impl Iterator<Item = &str> + Clone {
988 self.sources.iter().map(|(source, ..)| source.as_str())
989 }
990
991 pub fn is_match<P: AsRef<RelPath>>(&self, other: P) -> bool {
992 let other = other.as_ref();
993 if self
994 .sources
995 .iter()
996 .any(|(_, source, _)| other.starts_with(source) || other.ends_with(source))
997 {
998 return true;
999 }
1000 let other_path = other.display(self.path_style);
1001
1002 if self.glob.is_match(&*other_path) {
1003 return true;
1004 }
1005
1006 self.glob
1007 .is_match(other_path.into_owned() + self.path_style.primary_separator())
1008 }
1009
1010 pub fn is_match_std_path<P: AsRef<Path>>(&self, other: P) -> bool {
1011 let other = other.as_ref();
1012 if self.sources.iter().any(|(_, source, _)| {
1013 other.starts_with(source.as_std_path()) || other.ends_with(source.as_std_path())
1014 }) {
1015 return true;
1016 }
1017 self.glob.is_match(other)
1018 }
1019}
1020
1021impl Default for PathMatcher {
1022 fn default() -> Self {
1023 Self {
1024 path_style: PathStyle::local(),
1025 glob: GlobSet::empty(),
1026 sources: vec![],
1027 }
1028 }
1029}
1030
1031fn compare_numeric_segments<I>(
1066 a_iter: &mut std::iter::Peekable<I>,
1067 b_iter: &mut std::iter::Peekable<I>,
1068) -> Ordering
1069where
1070 I: Iterator<Item = char>,
1071{
1072 let mut a_num_str = String::new();
1074 let mut b_num_str = String::new();
1075
1076 while let Some(&c) = a_iter.peek() {
1077 if !c.is_ascii_digit() {
1078 break;
1079 }
1080
1081 a_num_str.push(c);
1082 a_iter.next();
1083 }
1084
1085 while let Some(&c) = b_iter.peek() {
1086 if !c.is_ascii_digit() {
1087 break;
1088 }
1089
1090 b_num_str.push(c);
1091 b_iter.next();
1092 }
1093
1094 match a_num_str.len().cmp(&b_num_str.len()) {
1096 Ordering::Equal => {
1097 match a_num_str.cmp(&b_num_str) {
1099 Ordering::Equal => Ordering::Equal,
1100 ordering => ordering,
1101 }
1102 }
1103
1104 ordering => {
1106 if let (Ok(a_val), Ok(b_val)) = (a_num_str.parse::<u128>(), b_num_str.parse::<u128>()) {
1108 match a_val.cmp(&b_val) {
1109 Ordering::Equal => ordering, ord => ord,
1111 }
1112 } else {
1113 a_num_str.cmp(&b_num_str)
1115 }
1116 }
1117 }
1118}
1119
1120pub fn natural_sort(a: &str, b: &str) -> Ordering {
1142 let mut a_iter = a.chars().peekable();
1143 let mut b_iter = b.chars().peekable();
1144
1145 loop {
1146 match (a_iter.peek(), b_iter.peek()) {
1147 (None, None) => {
1148 return b.cmp(a);
1149 }
1150 (None, _) => return Ordering::Less,
1151 (_, None) => return Ordering::Greater,
1152 (Some(&a_char), Some(&b_char)) => {
1153 if a_char.is_ascii_digit() && b_char.is_ascii_digit() {
1154 match compare_numeric_segments(&mut a_iter, &mut b_iter) {
1155 Ordering::Equal => continue,
1156 ordering => return ordering,
1157 }
1158 } else {
1159 match a_char
1160 .to_ascii_lowercase()
1161 .cmp(&b_char.to_ascii_lowercase())
1162 {
1163 Ordering::Equal => {
1164 a_iter.next();
1165 b_iter.next();
1166 }
1167 ordering => return ordering,
1168 }
1169 }
1170 }
1171 }
1172 }
1173}
1174
1175fn natural_sort_no_tiebreak(a: &str, b: &str) -> Ordering {
1179 if a.eq_ignore_ascii_case(b) {
1180 Ordering::Equal
1181 } else {
1182 natural_sort(a, b)
1183 }
1184}
1185
1186fn stem_and_extension(filename: &str) -> (Option<&str>, Option<&str>) {
1187 if filename.is_empty() {
1188 return (None, None);
1189 }
1190
1191 match filename.rsplit_once('.') {
1192 None => (Some(filename), None),
1194
1195 Some((before, after)) => {
1197 if before.is_empty() {
1201 (Some(filename), None)
1202 } else {
1203 (Some(before), Some(after))
1205 }
1206 }
1207 }
1208}
1209
1210#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
1212pub enum SortOrder {
1213 #[default]
1216 Default,
1217 Upper,
1220 Lower,
1223 Unicode,
1226}
1227
1228#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
1230pub enum SortMode {
1231 #[default]
1233 DirectoriesFirst,
1234 Mixed,
1236 FilesFirst,
1238}
1239
1240fn case_group_key(name: &str, order: SortOrder) -> u8 {
1241 let first = match name.chars().next() {
1242 Some(c) => c,
1243 None => return 0,
1244 };
1245 match order {
1246 SortOrder::Upper => {
1247 if first.is_lowercase() {
1248 1
1249 } else {
1250 0
1251 }
1252 }
1253 SortOrder::Lower => {
1254 if first.is_uppercase() {
1255 1
1256 } else {
1257 0
1258 }
1259 }
1260 _ => 0,
1261 }
1262}
1263
1264fn compare_strings(a: &str, b: &str, order: SortOrder) -> Ordering {
1265 match order {
1266 SortOrder::Unicode => a.cmp(b),
1267 _ => natural_sort(a, b),
1268 }
1269}
1270
1271fn compare_strings_no_tiebreak(a: &str, b: &str, order: SortOrder) -> Ordering {
1272 match order {
1273 SortOrder::Unicode => a.cmp(b),
1274 _ => natural_sort_no_tiebreak(a, b),
1275 }
1276}
1277
1278pub fn compare_rel_paths(
1279 (path_a, a_is_file): (&RelPath, bool),
1280 (path_b, b_is_file): (&RelPath, bool),
1281) -> Ordering {
1282 compare_rel_paths_by(
1283 (path_a, a_is_file),
1284 (path_b, b_is_file),
1285 SortMode::DirectoriesFirst,
1286 SortOrder::Default,
1287 )
1288}
1289
1290pub fn compare_rel_paths_by(
1291 (path_a, a_is_file): (&RelPath, bool),
1292 (path_b, b_is_file): (&RelPath, bool),
1293 mode: SortMode,
1294 order: SortOrder,
1295) -> Ordering {
1296 let needs_final_tiebreak =
1297 mode != SortMode::DirectoriesFirst && !(std::ptr::eq(path_a, path_b) || path_a == path_b);
1298
1299 let mut components_a = path_a.components();
1300 let mut components_b = path_b.components();
1301
1302 loop {
1303 match (components_a.next(), components_b.next()) {
1304 (Some(component_a), Some(component_b)) => {
1305 let a_leaf_file = a_is_file && components_a.rest().is_empty();
1306 let b_leaf_file = b_is_file && components_b.rest().is_empty();
1307
1308 let file_dir_ordering = match mode {
1309 SortMode::DirectoriesFirst => a_leaf_file.cmp(&b_leaf_file),
1310 SortMode::FilesFirst => b_leaf_file.cmp(&a_leaf_file),
1311 SortMode::Mixed => Ordering::Equal,
1312 };
1313
1314 if !file_dir_ordering.is_eq() {
1315 return file_dir_ordering;
1316 }
1317
1318 let (a_stem, a_ext) = a_leaf_file
1319 .then(|| stem_and_extension(component_a))
1320 .unwrap_or_default();
1321 let (b_stem, b_ext) = b_leaf_file
1322 .then(|| stem_and_extension(component_b))
1323 .unwrap_or_default();
1324 let a_key = if a_leaf_file {
1325 a_stem
1326 } else {
1327 Some(component_a)
1328 };
1329 let b_key = if b_leaf_file {
1330 b_stem
1331 } else {
1332 Some(component_b)
1333 };
1334
1335 let ordering = match (a_key, b_key) {
1336 (Some(a), Some(b)) => {
1337 let name_cmp = case_group_key(a, order)
1338 .cmp(&case_group_key(b, order))
1339 .then_with(|| match mode {
1340 SortMode::DirectoriesFirst => compare_strings(a, b, order),
1341 _ => compare_strings_no_tiebreak(a, b, order),
1342 });
1343
1344 let name_cmp = if mode == SortMode::Mixed {
1345 name_cmp.then_with(|| match (a_leaf_file, b_leaf_file) {
1346 (true, false) if a.eq_ignore_ascii_case(b) => Ordering::Greater,
1347 (false, true) if a.eq_ignore_ascii_case(b) => Ordering::Less,
1348 _ => Ordering::Equal,
1349 })
1350 } else {
1351 name_cmp
1352 };
1353
1354 name_cmp.then_with(|| {
1355 if a_leaf_file && b_leaf_file {
1356 match order {
1357 SortOrder::Unicode => {
1358 a_ext.unwrap_or_default().cmp(b_ext.unwrap_or_default())
1359 }
1360 _ => {
1361 let a_ext_str = a_ext.unwrap_or_default().to_lowercase();
1362 let b_ext_str = b_ext.unwrap_or_default().to_lowercase();
1363 a_ext_str.cmp(&b_ext_str)
1364 }
1365 }
1366 } else {
1367 Ordering::Equal
1368 }
1369 })
1370 }
1371 (Some(_), None) => Ordering::Greater,
1372 (None, Some(_)) => Ordering::Less,
1373 (None, None) => Ordering::Equal,
1374 };
1375
1376 if !ordering.is_eq() {
1377 return ordering;
1378 }
1379 }
1380 (Some(_), None) => return Ordering::Greater,
1381 (None, Some(_)) => return Ordering::Less,
1382 (None, None) => {
1383 if needs_final_tiebreak {
1384 return compare_strings(path_a.as_unix_str(), path_b.as_unix_str(), order);
1385 }
1386 return Ordering::Equal;
1387 }
1388 }
1389 }
1390}
1391
1392pub fn compare_paths(
1393 (path_a, a_is_file): (&Path, bool),
1394 (path_b, b_is_file): (&Path, bool),
1395) -> Ordering {
1396 let mut components_a = path_a.components().peekable();
1397 let mut components_b = path_b.components().peekable();
1398
1399 loop {
1400 match (components_a.next(), components_b.next()) {
1401 (Some(component_a), Some(component_b)) => {
1402 let a_is_file = components_a.peek().is_none() && a_is_file;
1403 let b_is_file = components_b.peek().is_none() && b_is_file;
1404
1405 let ordering = a_is_file.cmp(&b_is_file).then_with(|| {
1406 let path_a = Path::new(component_a.as_os_str());
1407 let path_string_a = if a_is_file {
1408 path_a.file_stem()
1409 } else {
1410 path_a.file_name()
1411 }
1412 .map(|s| s.to_string_lossy());
1413
1414 let path_b = Path::new(component_b.as_os_str());
1415 let path_string_b = if b_is_file {
1416 path_b.file_stem()
1417 } else {
1418 path_b.file_name()
1419 }
1420 .map(|s| s.to_string_lossy());
1421
1422 let compare_components = match (path_string_a, path_string_b) {
1423 (Some(a), Some(b)) => natural_sort(&a, &b),
1424 (Some(_), None) => Ordering::Greater,
1425 (None, Some(_)) => Ordering::Less,
1426 (None, None) => Ordering::Equal,
1427 };
1428
1429 compare_components.then_with(|| {
1430 if a_is_file && b_is_file {
1431 let ext_a = path_a.extension().unwrap_or_default();
1432 let ext_b = path_b.extension().unwrap_or_default();
1433 ext_a.cmp(ext_b)
1434 } else {
1435 Ordering::Equal
1436 }
1437 })
1438 });
1439
1440 if !ordering.is_eq() {
1441 return ordering;
1442 }
1443 }
1444 (Some(_), None) => break Ordering::Greater,
1445 (None, Some(_)) => break Ordering::Less,
1446 (None, None) => break Ordering::Equal,
1447 }
1448 }
1449}
1450
1451#[derive(Debug, Clone, PartialEq, Eq)]
1452pub struct WslPath {
1453 pub distro: String,
1454
1455 pub path: std::ffi::OsString,
1459}
1460
1461impl WslPath {
1462 pub fn from_path<P: AsRef<Path>>(path: P) -> Option<WslPath> {
1463 if cfg!(not(target_os = "windows")) {
1464 return None;
1465 }
1466 use std::{
1467 ffi::OsString,
1468 path::{Component, Prefix},
1469 };
1470
1471 let mut components = path.as_ref().components();
1472 let Some(Component::Prefix(prefix)) = components.next() else {
1473 return None;
1474 };
1475 let (server, distro) = match prefix.kind() {
1476 Prefix::UNC(server, distro) => (server, distro),
1477 Prefix::VerbatimUNC(server, distro) => (server, distro),
1478 _ => return None,
1479 };
1480 let Some(Component::RootDir) = components.next() else {
1481 return None;
1482 };
1483
1484 let server_str = server.to_string_lossy();
1485 if server_str == "wsl.localhost" || server_str == "wsl$" {
1486 let mut result = OsString::from("");
1487 for c in components {
1488 use Component::*;
1489 match c {
1490 Prefix(p) => unreachable!("got {p:?}, but already stripped prefix"),
1491 RootDir => unreachable!("got root dir, but already stripped root"),
1492 CurDir => continue,
1493 ParentDir => result.push("/.."),
1494 Normal(s) => {
1495 result.push("/");
1496 result.push(s);
1497 }
1498 }
1499 }
1500 if result.is_empty() {
1501 result.push("/");
1502 }
1503 Some(WslPath {
1504 distro: distro.to_string_lossy().to_string(),
1505 path: result,
1506 })
1507 } else {
1508 None
1509 }
1510 }
1511}
1512
1513pub trait UrlExt {
1514 fn to_file_path_ext(&self, path_style: PathStyle) -> Result<PathBuf, ()>;
1518}
1519
1520impl UrlExt for url::Url {
1521 fn to_file_path_ext(&self, source_path_style: PathStyle) -> Result<PathBuf, ()> {
1523 if let Some(segments) = self.path_segments() {
1524 let host = match self.host() {
1525 None | Some(url::Host::Domain("localhost")) => None,
1526 Some(_) if source_path_style.is_windows() && self.scheme() == "file" => {
1527 self.host_str()
1528 }
1529 _ => return Err(()),
1530 };
1531
1532 let str_len = self.as_str().len();
1533 let estimated_capacity = if source_path_style.is_windows() {
1534 str_len.saturating_sub(self.scheme().len() + 1)
1536 } else {
1537 str_len.saturating_sub(self.scheme().len() + 3)
1539 };
1540 return match source_path_style {
1541 PathStyle::Posix => {
1542 file_url_segments_to_pathbuf_posix(estimated_capacity, host, segments)
1543 }
1544 PathStyle::Windows => {
1545 file_url_segments_to_pathbuf_windows(estimated_capacity, host, segments)
1546 }
1547 };
1548 }
1549
1550 fn file_url_segments_to_pathbuf_posix(
1551 estimated_capacity: usize,
1552 host: Option<&str>,
1553 segments: std::str::Split<'_, char>,
1554 ) -> Result<PathBuf, ()> {
1555 use percent_encoding::percent_decode;
1556
1557 if host.is_some() {
1558 return Err(());
1559 }
1560
1561 let mut bytes = Vec::new();
1562 bytes.try_reserve(estimated_capacity).map_err(|_| ())?;
1563
1564 for segment in segments {
1565 bytes.push(b'/');
1566 bytes.extend(percent_decode(segment.as_bytes()));
1567 }
1568
1569 if bytes.len() > 2
1571 && bytes[bytes.len() - 2].is_ascii_alphabetic()
1572 && matches!(bytes[bytes.len() - 1], b':' | b'|')
1573 {
1574 bytes.push(b'/');
1575 }
1576
1577 let path = String::from_utf8(bytes).map_err(|_| ())?;
1578 debug_assert!(
1579 PathStyle::Posix.is_absolute(&path),
1580 "to_file_path() failed to produce an absolute Path"
1581 );
1582
1583 Ok(PathBuf::from(path))
1584 }
1585
1586 fn file_url_segments_to_pathbuf_windows(
1587 estimated_capacity: usize,
1588 host: Option<&str>,
1589 mut segments: std::str::Split<'_, char>,
1590 ) -> Result<PathBuf, ()> {
1591 use percent_encoding::percent_decode_str;
1592 let mut string = String::new();
1593 string.try_reserve(estimated_capacity).map_err(|_| ())?;
1594 if let Some(host) = host {
1595 string.push_str(r"\\");
1596 string.push_str(host);
1597 } else {
1598 let first = segments.next().ok_or(())?;
1599
1600 match first.len() {
1601 2 => {
1602 if !first.starts_with(|c| char::is_ascii_alphabetic(&c))
1603 || first.as_bytes()[1] != b':'
1604 {
1605 return Err(());
1606 }
1607
1608 string.push_str(first);
1609 }
1610
1611 4 => {
1612 if !first.starts_with(|c| char::is_ascii_alphabetic(&c)) {
1613 return Err(());
1614 }
1615 let bytes = first.as_bytes();
1616 if bytes[1] != b'%'
1617 || bytes[2] != b'3'
1618 || (bytes[3] != b'a' && bytes[3] != b'A')
1619 {
1620 return Err(());
1621 }
1622
1623 string.push_str(&first[0..1]);
1624 string.push(':');
1625 }
1626
1627 _ => return Err(()),
1628 }
1629 };
1630
1631 for segment in segments {
1632 string.push('\\');
1633
1634 match percent_decode_str(segment).decode_utf8() {
1636 Ok(s) => string.push_str(&s),
1637 Err(..) => return Err(()),
1638 }
1639 }
1640 if cfg!(test) {
1642 debug_assert!(
1643 string.len() <= estimated_capacity,
1644 "len: {}, capacity: {}",
1645 string.len(),
1646 estimated_capacity
1647 );
1648 }
1649 debug_assert!(
1650 PathStyle::Windows.is_absolute(&string),
1651 "to_file_path() failed to produce an absolute Path"
1652 );
1653 let path = PathBuf::from(string);
1654 Ok(path)
1655 }
1656 Err(())
1657 }
1658}
1659
1660#[cfg(test)]
1661mod tests {
1662 use crate::rel_path::rel_path;
1663
1664 use super::*;
1665 use open_gpui_util_macros::perf;
1666
1667 #[test]
1668 fn test_join_path_uses_path_style_separator() {
1669 let posix_path = PathStyle::Posix
1670 .join_path(Path::new("/home/user/dev"), "worktrees")
1671 .unwrap();
1672 let windows_path = PathStyle::Windows
1673 .join_path(Path::new("C:\\Users\\user\\dev"), "worktrees")
1674 .unwrap();
1675
1676 assert_eq!(posix_path, PathBuf::from("/home/user/dev/worktrees"));
1677 assert_eq!(
1678 windows_path.to_string_lossy(),
1679 "C:\\Users\\user\\dev\\worktrees"
1680 );
1681 }
1682
1683 #[test]
1684 fn test_normalize_uses_path_style_separator() {
1685 assert_eq!(
1686 PathStyle::Posix.normalize("/home/user/dev/../worktrees/./zed"),
1687 "/home/user/worktrees/zed"
1688 );
1689 assert_eq!(
1690 PathStyle::Windows.normalize("C:\\Users\\user\\dev\\worktrees"),
1691 "C:\\Users\\user\\dev\\worktrees"
1692 );
1693 }
1694
1695 fn rel_path_entry(path: &'static str, is_file: bool) -> (&'static RelPath, bool) {
1696 (RelPath::unix(path).unwrap(), is_file)
1697 }
1698
1699 fn sorted_rel_paths(
1700 mut paths: Vec<(&'static RelPath, bool)>,
1701 mode: SortMode,
1702 order: SortOrder,
1703 ) -> Vec<(&'static RelPath, bool)> {
1704 paths.sort_by(|&a, &b| compare_rel_paths_by(a, b, mode, order));
1705 paths
1706 }
1707
1708 #[perf]
1709 fn compare_paths_with_dots() {
1710 let mut paths = vec![
1711 (Path::new("test_dirs"), false),
1712 (Path::new("test_dirs/1.46"), false),
1713 (Path::new("test_dirs/1.46/bar_1"), true),
1714 (Path::new("test_dirs/1.46/bar_2"), true),
1715 (Path::new("test_dirs/1.45"), false),
1716 (Path::new("test_dirs/1.45/foo_2"), true),
1717 (Path::new("test_dirs/1.45/foo_1"), true),
1718 ];
1719 paths.sort_by(|&a, &b| compare_paths(a, b));
1720 assert_eq!(
1721 paths,
1722 vec![
1723 (Path::new("test_dirs"), false),
1724 (Path::new("test_dirs/1.45"), false),
1725 (Path::new("test_dirs/1.45/foo_1"), true),
1726 (Path::new("test_dirs/1.45/foo_2"), true),
1727 (Path::new("test_dirs/1.46"), false),
1728 (Path::new("test_dirs/1.46/bar_1"), true),
1729 (Path::new("test_dirs/1.46/bar_2"), true),
1730 ]
1731 );
1732 let mut paths = vec![
1733 (Path::new("root1/one.txt"), true),
1734 (Path::new("root1/one.two.txt"), true),
1735 ];
1736 paths.sort_by(|&a, &b| compare_paths(a, b));
1737 assert_eq!(
1738 paths,
1739 vec![
1740 (Path::new("root1/one.txt"), true),
1741 (Path::new("root1/one.two.txt"), true),
1742 ]
1743 );
1744 }
1745
1746 #[perf]
1747 fn compare_paths_with_same_name_different_extensions() {
1748 let mut paths = vec![
1749 (Path::new("test_dirs/file.rs"), true),
1750 (Path::new("test_dirs/file.txt"), true),
1751 (Path::new("test_dirs/file.md"), true),
1752 (Path::new("test_dirs/file"), true),
1753 (Path::new("test_dirs/file.a"), true),
1754 ];
1755 paths.sort_by(|&a, &b| compare_paths(a, b));
1756 assert_eq!(
1757 paths,
1758 vec![
1759 (Path::new("test_dirs/file"), true),
1760 (Path::new("test_dirs/file.a"), true),
1761 (Path::new("test_dirs/file.md"), true),
1762 (Path::new("test_dirs/file.rs"), true),
1763 (Path::new("test_dirs/file.txt"), true),
1764 ]
1765 );
1766 }
1767
1768 #[perf]
1769 fn compare_paths_case_semi_sensitive() {
1770 let mut paths = vec![
1771 (Path::new("test_DIRS"), false),
1772 (Path::new("test_DIRS/foo_1"), true),
1773 (Path::new("test_DIRS/foo_2"), true),
1774 (Path::new("test_DIRS/bar"), true),
1775 (Path::new("test_DIRS/BAR"), true),
1776 (Path::new("test_dirs"), false),
1777 (Path::new("test_dirs/foo_1"), true),
1778 (Path::new("test_dirs/foo_2"), true),
1779 (Path::new("test_dirs/bar"), true),
1780 (Path::new("test_dirs/BAR"), true),
1781 ];
1782 paths.sort_by(|&a, &b| compare_paths(a, b));
1783 assert_eq!(
1784 paths,
1785 vec![
1786 (Path::new("test_dirs"), false),
1787 (Path::new("test_dirs/bar"), true),
1788 (Path::new("test_dirs/BAR"), true),
1789 (Path::new("test_dirs/foo_1"), true),
1790 (Path::new("test_dirs/foo_2"), true),
1791 (Path::new("test_DIRS"), false),
1792 (Path::new("test_DIRS/bar"), true),
1793 (Path::new("test_DIRS/BAR"), true),
1794 (Path::new("test_DIRS/foo_1"), true),
1795 (Path::new("test_DIRS/foo_2"), true),
1796 ]
1797 );
1798 }
1799
1800 #[perf]
1801 fn compare_paths_mixed_case_numeric_ordering() {
1802 let mut entries = [
1803 (Path::new(".config"), false),
1804 (Path::new("Dir1"), false),
1805 (Path::new("dir01"), false),
1806 (Path::new("dir2"), false),
1807 (Path::new("Dir02"), false),
1808 (Path::new("dir10"), false),
1809 (Path::new("Dir10"), false),
1810 ];
1811
1812 entries.sort_by(|&a, &b| compare_paths(a, b));
1813
1814 let ordered: Vec<&str> = entries
1815 .iter()
1816 .map(|(path, _)| path.to_str().unwrap())
1817 .collect();
1818
1819 assert_eq!(
1820 ordered,
1821 vec![
1822 ".config", "Dir1", "dir01", "dir2", "Dir02", "dir10", "Dir10"
1823 ]
1824 );
1825 }
1826
1827 #[perf]
1828 fn compare_rel_paths_mixed_case_insensitive() {
1829 let mut paths = vec![
1831 (RelPath::unix("zebra.txt").unwrap(), true),
1832 (RelPath::unix("Apple").unwrap(), false),
1833 (RelPath::unix("banana.rs").unwrap(), true),
1834 (RelPath::unix("Carrot").unwrap(), false),
1835 (RelPath::unix("aardvark.txt").unwrap(), true),
1836 ];
1837 paths.sort_by(|&a, &b| compare_rel_paths_by(a, b, SortMode::Mixed, SortOrder::Default));
1838 assert_eq!(
1840 paths,
1841 vec![
1842 (RelPath::unix("aardvark.txt").unwrap(), true),
1843 (RelPath::unix("Apple").unwrap(), false),
1844 (RelPath::unix("banana.rs").unwrap(), true),
1845 (RelPath::unix("Carrot").unwrap(), false),
1846 (RelPath::unix("zebra.txt").unwrap(), true),
1847 ]
1848 );
1849 }
1850
1851 #[perf]
1852 fn compare_rel_paths_files_first_basic() {
1853 let mut paths = vec![
1855 (RelPath::unix("zebra.txt").unwrap(), true),
1856 (RelPath::unix("Apple").unwrap(), false),
1857 (RelPath::unix("banana.rs").unwrap(), true),
1858 (RelPath::unix("Carrot").unwrap(), false),
1859 (RelPath::unix("aardvark.txt").unwrap(), true),
1860 ];
1861 paths
1862 .sort_by(|&a, &b| compare_rel_paths_by(a, b, SortMode::FilesFirst, SortOrder::Default));
1863 assert_eq!(
1865 paths,
1866 vec![
1867 (RelPath::unix("aardvark.txt").unwrap(), true),
1868 (RelPath::unix("banana.rs").unwrap(), true),
1869 (RelPath::unix("zebra.txt").unwrap(), true),
1870 (RelPath::unix("Apple").unwrap(), false),
1871 (RelPath::unix("Carrot").unwrap(), false),
1872 ]
1873 );
1874 }
1875
1876 #[perf]
1877 fn compare_rel_paths_files_first_case_insensitive() {
1878 let mut paths = vec![
1880 (RelPath::unix("Zebra.txt").unwrap(), true),
1881 (RelPath::unix("apple").unwrap(), false),
1882 (RelPath::unix("Banana.rs").unwrap(), true),
1883 (RelPath::unix("carrot").unwrap(), false),
1884 (RelPath::unix("Aardvark.txt").unwrap(), true),
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::unix("Aardvark.txt").unwrap(), true),
1892 (RelPath::unix("Banana.rs").unwrap(), true),
1893 (RelPath::unix("Zebra.txt").unwrap(), true),
1894 (RelPath::unix("apple").unwrap(), false),
1895 (RelPath::unix("carrot").unwrap(), false),
1896 ]
1897 );
1898 }
1899
1900 #[perf]
1901 fn compare_rel_paths_files_first_numeric() {
1902 let mut paths = vec![
1904 (RelPath::unix("file10.txt").unwrap(), true),
1905 (RelPath::unix("dir2").unwrap(), false),
1906 (RelPath::unix("file2.txt").unwrap(), true),
1907 (RelPath::unix("dir10").unwrap(), false),
1908 (RelPath::unix("file1.txt").unwrap(), true),
1909 ];
1910 paths
1911 .sort_by(|&a, &b| compare_rel_paths_by(a, b, SortMode::FilesFirst, SortOrder::Default));
1912 assert_eq!(
1913 paths,
1914 vec![
1915 (RelPath::unix("file1.txt").unwrap(), true),
1916 (RelPath::unix("file2.txt").unwrap(), true),
1917 (RelPath::unix("file10.txt").unwrap(), true),
1918 (RelPath::unix("dir2").unwrap(), false),
1919 (RelPath::unix("dir10").unwrap(), false),
1920 ]
1921 );
1922 }
1923
1924 #[perf]
1925 fn compare_rel_paths_mixed_case() {
1926 let mut paths = vec![
1928 (RelPath::unix("README.md").unwrap(), true),
1929 (RelPath::unix("readme.txt").unwrap(), true),
1930 (RelPath::unix("ReadMe.rs").unwrap(), true),
1931 ];
1932 paths.sort_by(|&a, &b| compare_rel_paths_by(a, b, SortMode::Mixed, SortOrder::Default));
1933 assert_eq!(
1935 paths,
1936 vec![
1937 (RelPath::unix("README.md").unwrap(), true),
1938 (RelPath::unix("ReadMe.rs").unwrap(), true),
1939 (RelPath::unix("readme.txt").unwrap(), true),
1940 ]
1941 );
1942 }
1943
1944 #[perf]
1945 fn compare_rel_paths_mixed_files_and_dirs() {
1946 let mut paths = vec![
1948 (RelPath::unix("file2.txt").unwrap(), true),
1949 (RelPath::unix("Dir1").unwrap(), false),
1950 (RelPath::unix("file1.txt").unwrap(), true),
1951 (RelPath::unix("dir2").unwrap(), false),
1952 ];
1953 paths.sort_by(|&a, &b| compare_rel_paths_by(a, b, SortMode::Mixed, SortOrder::Default));
1954 assert_eq!(
1956 paths,
1957 vec![
1958 (RelPath::unix("Dir1").unwrap(), false),
1959 (RelPath::unix("dir2").unwrap(), false),
1960 (RelPath::unix("file1.txt").unwrap(), true),
1961 (RelPath::unix("file2.txt").unwrap(), true),
1962 ]
1963 );
1964 }
1965
1966 #[perf]
1967 fn compare_rel_paths_mixed_same_name_different_case_file_and_dir() {
1968 let mut paths = vec![
1969 (RelPath::unix("Hello.txt").unwrap(), true),
1970 (RelPath::unix("hello").unwrap(), false),
1971 ];
1972 paths.sort_by(|&a, &b| compare_rel_paths_by(a, b, SortMode::Mixed, SortOrder::Default));
1973 assert_eq!(
1974 paths,
1975 vec![
1976 (RelPath::unix("hello").unwrap(), false),
1977 (RelPath::unix("Hello.txt").unwrap(), true),
1978 ]
1979 );
1980
1981 let mut paths = vec![
1982 (RelPath::unix("hello").unwrap(), false),
1983 (RelPath::unix("Hello.txt").unwrap(), true),
1984 ];
1985 paths.sort_by(|&a, &b| compare_rel_paths_by(a, b, SortMode::Mixed, SortOrder::Default));
1986 assert_eq!(
1987 paths,
1988 vec![
1989 (RelPath::unix("hello").unwrap(), false),
1990 (RelPath::unix("Hello.txt").unwrap(), true),
1991 ]
1992 );
1993 }
1994
1995 #[perf]
1996 fn compare_rel_paths_mixed_with_nested_paths() {
1997 let mut paths = vec![
1999 (RelPath::unix("src/main.rs").unwrap(), true),
2000 (RelPath::unix("Cargo.toml").unwrap(), true),
2001 (RelPath::unix("src").unwrap(), false),
2002 (RelPath::unix("target").unwrap(), false),
2003 ];
2004 paths.sort_by(|&a, &b| compare_rel_paths_by(a, b, SortMode::Mixed, SortOrder::Default));
2005 assert_eq!(
2006 paths,
2007 vec![
2008 (RelPath::unix("Cargo.toml").unwrap(), true),
2009 (RelPath::unix("src").unwrap(), false),
2010 (RelPath::unix("src/main.rs").unwrap(), true),
2011 (RelPath::unix("target").unwrap(), false),
2012 ]
2013 );
2014 }
2015
2016 #[perf]
2017 fn compare_rel_paths_files_first_with_nested() {
2018 let mut paths = vec![
2020 (RelPath::unix("src/lib.rs").unwrap(), true),
2021 (RelPath::unix("README.md").unwrap(), true),
2022 (RelPath::unix("src").unwrap(), false),
2023 (RelPath::unix("tests").unwrap(), false),
2024 ];
2025 paths
2026 .sort_by(|&a, &b| compare_rel_paths_by(a, b, SortMode::FilesFirst, SortOrder::Default));
2027 assert_eq!(
2028 paths,
2029 vec![
2030 (RelPath::unix("README.md").unwrap(), true),
2031 (RelPath::unix("src").unwrap(), false),
2032 (RelPath::unix("src/lib.rs").unwrap(), true),
2033 (RelPath::unix("tests").unwrap(), false),
2034 ]
2035 );
2036 }
2037
2038 #[perf]
2039 fn compare_rel_paths_mixed_dotfiles() {
2040 let mut paths = vec![
2042 (RelPath::unix(".gitignore").unwrap(), true),
2043 (RelPath::unix("README.md").unwrap(), true),
2044 (RelPath::unix(".github").unwrap(), false),
2045 (RelPath::unix("src").unwrap(), false),
2046 ];
2047 paths.sort_by(|&a, &b| compare_rel_paths_by(a, b, SortMode::Mixed, SortOrder::Default));
2048 assert_eq!(
2049 paths,
2050 vec![
2051 (RelPath::unix(".github").unwrap(), false),
2052 (RelPath::unix(".gitignore").unwrap(), true),
2053 (RelPath::unix("README.md").unwrap(), true),
2054 (RelPath::unix("src").unwrap(), false),
2055 ]
2056 );
2057 }
2058
2059 #[perf]
2060 fn compare_rel_paths_files_first_dotfiles() {
2061 let mut paths = vec![
2063 (RelPath::unix(".gitignore").unwrap(), true),
2064 (RelPath::unix("README.md").unwrap(), true),
2065 (RelPath::unix(".github").unwrap(), false),
2066 (RelPath::unix("src").unwrap(), false),
2067 ];
2068 paths
2069 .sort_by(|&a, &b| compare_rel_paths_by(a, b, SortMode::FilesFirst, SortOrder::Default));
2070 assert_eq!(
2071 paths,
2072 vec![
2073 (RelPath::unix(".gitignore").unwrap(), true),
2074 (RelPath::unix("README.md").unwrap(), true),
2075 (RelPath::unix(".github").unwrap(), false),
2076 (RelPath::unix("src").unwrap(), false),
2077 ]
2078 );
2079 }
2080
2081 #[perf]
2082 fn compare_rel_paths_mixed_same_stem_different_extension() {
2083 let mut paths = vec![
2085 (RelPath::unix("file.rs").unwrap(), true),
2086 (RelPath::unix("file.md").unwrap(), true),
2087 (RelPath::unix("file.txt").unwrap(), true),
2088 ];
2089 paths.sort_by(|&a, &b| compare_rel_paths_by(a, b, SortMode::Mixed, SortOrder::Default));
2090 assert_eq!(
2091 paths,
2092 vec![
2093 (RelPath::unix("file.md").unwrap(), true),
2094 (RelPath::unix("file.rs").unwrap(), true),
2095 (RelPath::unix("file.txt").unwrap(), true),
2096 ]
2097 );
2098 }
2099
2100 #[perf]
2101 fn compare_rel_paths_files_first_same_stem() {
2102 let mut paths = vec![
2104 (RelPath::unix("main.rs").unwrap(), true),
2105 (RelPath::unix("main.c").unwrap(), true),
2106 (RelPath::unix("main").unwrap(), false),
2107 ];
2108 paths
2109 .sort_by(|&a, &b| compare_rel_paths_by(a, b, SortMode::FilesFirst, SortOrder::Default));
2110 assert_eq!(
2111 paths,
2112 vec![
2113 (RelPath::unix("main.c").unwrap(), true),
2114 (RelPath::unix("main.rs").unwrap(), true),
2115 (RelPath::unix("main").unwrap(), false),
2116 ]
2117 );
2118 }
2119
2120 #[perf]
2121 fn compare_rel_paths_mixed_deep_nesting() {
2122 let mut paths = vec![
2124 (RelPath::unix("a/b/c.txt").unwrap(), true),
2125 (RelPath::unix("A/B.txt").unwrap(), true),
2126 (RelPath::unix("a.txt").unwrap(), true),
2127 (RelPath::unix("A.txt").unwrap(), true),
2128 ];
2129 paths.sort_by(|&a, &b| compare_rel_paths_by(a, b, SortMode::Mixed, SortOrder::Default));
2130 assert_eq!(
2131 paths,
2132 vec![
2133 (RelPath::unix("a/b/c.txt").unwrap(), true),
2134 (RelPath::unix("A/B.txt").unwrap(), true),
2135 (RelPath::unix("a.txt").unwrap(), true),
2136 (RelPath::unix("A.txt").unwrap(), true),
2137 ]
2138 );
2139 }
2140
2141 #[perf]
2142 fn compare_rel_paths_upper() {
2143 let directories_only_paths = vec![
2144 rel_path_entry("mixedCase", false),
2145 rel_path_entry("Zebra", false),
2146 rel_path_entry("banana", false),
2147 rel_path_entry("ALLCAPS", false),
2148 rel_path_entry("Apple", false),
2149 rel_path_entry("dog", false),
2150 rel_path_entry(".hidden", false),
2151 rel_path_entry("Carrot", false),
2152 ];
2153 assert_eq!(
2154 sorted_rel_paths(
2155 directories_only_paths,
2156 SortMode::DirectoriesFirst,
2157 SortOrder::Upper,
2158 ),
2159 vec![
2160 rel_path_entry(".hidden", false),
2161 rel_path_entry("ALLCAPS", false),
2162 rel_path_entry("Apple", false),
2163 rel_path_entry("Carrot", false),
2164 rel_path_entry("Zebra", false),
2165 rel_path_entry("banana", false),
2166 rel_path_entry("dog", false),
2167 rel_path_entry("mixedCase", false),
2168 ]
2169 );
2170
2171 let file_and_directory_paths = vec![
2172 rel_path_entry("banana", false),
2173 rel_path_entry("Apple.txt", true),
2174 rel_path_entry("dog.md", true),
2175 rel_path_entry("ALLCAPS", false),
2176 rel_path_entry("file1.txt", true),
2177 rel_path_entry("File2.txt", true),
2178 rel_path_entry(".hidden", false),
2179 ];
2180 assert_eq!(
2181 sorted_rel_paths(
2182 file_and_directory_paths.clone(),
2183 SortMode::DirectoriesFirst,
2184 SortOrder::Upper,
2185 ),
2186 vec![
2187 rel_path_entry(".hidden", false),
2188 rel_path_entry("ALLCAPS", false),
2189 rel_path_entry("banana", false),
2190 rel_path_entry("Apple.txt", true),
2191 rel_path_entry("File2.txt", true),
2192 rel_path_entry("dog.md", true),
2193 rel_path_entry("file1.txt", true),
2194 ]
2195 );
2196 assert_eq!(
2197 sorted_rel_paths(
2198 file_and_directory_paths.clone(),
2199 SortMode::Mixed,
2200 SortOrder::Upper,
2201 ),
2202 vec![
2203 rel_path_entry(".hidden", false),
2204 rel_path_entry("ALLCAPS", false),
2205 rel_path_entry("Apple.txt", true),
2206 rel_path_entry("File2.txt", true),
2207 rel_path_entry("banana", false),
2208 rel_path_entry("dog.md", true),
2209 rel_path_entry("file1.txt", true),
2210 ]
2211 );
2212 assert_eq!(
2213 sorted_rel_paths(
2214 file_and_directory_paths,
2215 SortMode::FilesFirst,
2216 SortOrder::Upper,
2217 ),
2218 vec![
2219 rel_path_entry("Apple.txt", true),
2220 rel_path_entry("File2.txt", true),
2221 rel_path_entry("dog.md", true),
2222 rel_path_entry("file1.txt", true),
2223 rel_path_entry(".hidden", false),
2224 rel_path_entry("ALLCAPS", false),
2225 rel_path_entry("banana", false),
2226 ]
2227 );
2228
2229 let natural_sort_paths = vec![
2230 rel_path_entry("file10.txt", true),
2231 rel_path_entry("file1.txt", true),
2232 rel_path_entry("file20.txt", true),
2233 rel_path_entry("file2.txt", true),
2234 ];
2235 assert_eq!(
2236 sorted_rel_paths(natural_sort_paths, SortMode::Mixed, SortOrder::Upper,),
2237 vec![
2238 rel_path_entry("file1.txt", true),
2239 rel_path_entry("file2.txt", true),
2240 rel_path_entry("file10.txt", true),
2241 rel_path_entry("file20.txt", true),
2242 ]
2243 );
2244
2245 let accented_paths = vec![
2246 rel_path_entry("\u{00C9}something.txt", true),
2247 rel_path_entry("zebra.txt", true),
2248 rel_path_entry("Apple.txt", true),
2249 ];
2250 assert_eq!(
2251 sorted_rel_paths(accented_paths, SortMode::Mixed, SortOrder::Upper),
2252 vec![
2253 rel_path_entry("Apple.txt", true),
2254 rel_path_entry("\u{00C9}something.txt", true),
2255 rel_path_entry("zebra.txt", true),
2256 ]
2257 );
2258 }
2259
2260 #[perf]
2261 fn compare_rel_paths_lower() {
2262 let directories_only_paths = vec![
2263 rel_path_entry("mixedCase", false),
2264 rel_path_entry("Zebra", false),
2265 rel_path_entry("banana", false),
2266 rel_path_entry("ALLCAPS", false),
2267 rel_path_entry("Apple", false),
2268 rel_path_entry("dog", false),
2269 rel_path_entry(".hidden", false),
2270 rel_path_entry("Carrot", false),
2271 ];
2272 assert_eq!(
2273 sorted_rel_paths(
2274 directories_only_paths,
2275 SortMode::DirectoriesFirst,
2276 SortOrder::Lower,
2277 ),
2278 vec![
2279 rel_path_entry(".hidden", false),
2280 rel_path_entry("banana", false),
2281 rel_path_entry("dog", false),
2282 rel_path_entry("mixedCase", false),
2283 rel_path_entry("ALLCAPS", false),
2284 rel_path_entry("Apple", false),
2285 rel_path_entry("Carrot", false),
2286 rel_path_entry("Zebra", false),
2287 ]
2288 );
2289
2290 let file_and_directory_paths = vec![
2291 rel_path_entry("banana", false),
2292 rel_path_entry("Apple.txt", true),
2293 rel_path_entry("dog.md", true),
2294 rel_path_entry("ALLCAPS", false),
2295 rel_path_entry("file1.txt", true),
2296 rel_path_entry("File2.txt", true),
2297 rel_path_entry(".hidden", false),
2298 ];
2299 assert_eq!(
2300 sorted_rel_paths(
2301 file_and_directory_paths.clone(),
2302 SortMode::DirectoriesFirst,
2303 SortOrder::Lower,
2304 ),
2305 vec![
2306 rel_path_entry(".hidden", false),
2307 rel_path_entry("banana", false),
2308 rel_path_entry("ALLCAPS", false),
2309 rel_path_entry("dog.md", true),
2310 rel_path_entry("file1.txt", true),
2311 rel_path_entry("Apple.txt", true),
2312 rel_path_entry("File2.txt", true),
2313 ]
2314 );
2315 assert_eq!(
2316 sorted_rel_paths(
2317 file_and_directory_paths.clone(),
2318 SortMode::Mixed,
2319 SortOrder::Lower,
2320 ),
2321 vec![
2322 rel_path_entry(".hidden", false),
2323 rel_path_entry("banana", false),
2324 rel_path_entry("dog.md", true),
2325 rel_path_entry("file1.txt", true),
2326 rel_path_entry("ALLCAPS", false),
2327 rel_path_entry("Apple.txt", true),
2328 rel_path_entry("File2.txt", true),
2329 ]
2330 );
2331 assert_eq!(
2332 sorted_rel_paths(
2333 file_and_directory_paths,
2334 SortMode::FilesFirst,
2335 SortOrder::Lower,
2336 ),
2337 vec![
2338 rel_path_entry("dog.md", true),
2339 rel_path_entry("file1.txt", true),
2340 rel_path_entry("Apple.txt", true),
2341 rel_path_entry("File2.txt", true),
2342 rel_path_entry(".hidden", false),
2343 rel_path_entry("banana", false),
2344 rel_path_entry("ALLCAPS", false),
2345 ]
2346 );
2347 }
2348
2349 #[perf]
2350 fn compare_rel_paths_unicode() {
2351 let directories_only_paths = vec![
2352 rel_path_entry("mixedCase", false),
2353 rel_path_entry("Zebra", false),
2354 rel_path_entry("banana", false),
2355 rel_path_entry("ALLCAPS", false),
2356 rel_path_entry("Apple", false),
2357 rel_path_entry("dog", false),
2358 rel_path_entry(".hidden", false),
2359 rel_path_entry("Carrot", false),
2360 ];
2361 assert_eq!(
2362 sorted_rel_paths(
2363 directories_only_paths,
2364 SortMode::DirectoriesFirst,
2365 SortOrder::Unicode,
2366 ),
2367 vec![
2368 rel_path_entry(".hidden", false),
2369 rel_path_entry("ALLCAPS", false),
2370 rel_path_entry("Apple", false),
2371 rel_path_entry("Carrot", false),
2372 rel_path_entry("Zebra", false),
2373 rel_path_entry("banana", false),
2374 rel_path_entry("dog", false),
2375 rel_path_entry("mixedCase", false),
2376 ]
2377 );
2378
2379 let file_and_directory_paths = vec![
2380 rel_path_entry("banana", false),
2381 rel_path_entry("Apple.txt", true),
2382 rel_path_entry("dog.md", true),
2383 rel_path_entry("ALLCAPS", false),
2384 rel_path_entry("file1.txt", true),
2385 rel_path_entry("File2.txt", true),
2386 rel_path_entry(".hidden", false),
2387 ];
2388 assert_eq!(
2389 sorted_rel_paths(
2390 file_and_directory_paths.clone(),
2391 SortMode::DirectoriesFirst,
2392 SortOrder::Unicode,
2393 ),
2394 vec![
2395 rel_path_entry(".hidden", false),
2396 rel_path_entry("ALLCAPS", false),
2397 rel_path_entry("banana", false),
2398 rel_path_entry("Apple.txt", true),
2399 rel_path_entry("File2.txt", true),
2400 rel_path_entry("dog.md", true),
2401 rel_path_entry("file1.txt", true),
2402 ]
2403 );
2404 assert_eq!(
2405 sorted_rel_paths(
2406 file_and_directory_paths.clone(),
2407 SortMode::Mixed,
2408 SortOrder::Unicode,
2409 ),
2410 vec![
2411 rel_path_entry(".hidden", false),
2412 rel_path_entry("ALLCAPS", false),
2413 rel_path_entry("Apple.txt", true),
2414 rel_path_entry("File2.txt", true),
2415 rel_path_entry("banana", false),
2416 rel_path_entry("dog.md", true),
2417 rel_path_entry("file1.txt", true),
2418 ]
2419 );
2420 assert_eq!(
2421 sorted_rel_paths(
2422 file_and_directory_paths,
2423 SortMode::FilesFirst,
2424 SortOrder::Unicode,
2425 ),
2426 vec![
2427 rel_path_entry("Apple.txt", true),
2428 rel_path_entry("File2.txt", true),
2429 rel_path_entry("dog.md", true),
2430 rel_path_entry("file1.txt", true),
2431 rel_path_entry(".hidden", false),
2432 rel_path_entry("ALLCAPS", false),
2433 rel_path_entry("banana", false),
2434 ]
2435 );
2436
2437 let numeric_paths = vec![
2438 rel_path_entry("file10.txt", true),
2439 rel_path_entry("file1.txt", true),
2440 rel_path_entry("file2.txt", true),
2441 rel_path_entry("file20.txt", true),
2442 ];
2443 assert_eq!(
2444 sorted_rel_paths(numeric_paths, SortMode::Mixed, SortOrder::Unicode,),
2445 vec![
2446 rel_path_entry("file1.txt", true),
2447 rel_path_entry("file10.txt", true),
2448 rel_path_entry("file2.txt", true),
2449 rel_path_entry("file20.txt", true),
2450 ]
2451 );
2452
2453 let accented_paths = vec![
2454 rel_path_entry("\u{00C9}something.txt", true),
2455 rel_path_entry("zebra.txt", true),
2456 rel_path_entry("Apple.txt", true),
2457 ];
2458 assert_eq!(
2459 sorted_rel_paths(accented_paths, SortMode::Mixed, SortOrder::Unicode),
2460 vec![
2461 rel_path_entry("Apple.txt", true),
2462 rel_path_entry("zebra.txt", true),
2463 rel_path_entry("\u{00C9}something.txt", true),
2464 ]
2465 );
2466 }
2467
2468 #[perf]
2469 fn path_with_position_parse_posix_path() {
2470 assert_eq!(
2473 PathWithPosition::parse_str("test_file"),
2474 PathWithPosition {
2475 path: PathBuf::from("test_file"),
2476 row: None,
2477 column: None
2478 }
2479 );
2480
2481 assert_eq!(
2482 PathWithPosition::parse_str("a:bc:.zip:1"),
2483 PathWithPosition {
2484 path: PathBuf::from("a:bc:.zip"),
2485 row: Some(1),
2486 column: None
2487 }
2488 );
2489
2490 assert_eq!(
2491 PathWithPosition::parse_str("one.second.zip:1"),
2492 PathWithPosition {
2493 path: PathBuf::from("one.second.zip"),
2494 row: Some(1),
2495 column: None
2496 }
2497 );
2498
2499 assert_eq!(
2501 PathWithPosition::parse_str("test_file:10:1:"),
2502 PathWithPosition {
2503 path: PathBuf::from("test_file"),
2504 row: Some(10),
2505 column: Some(1)
2506 }
2507 );
2508
2509 assert_eq!(
2510 PathWithPosition::parse_str("test_file.rs:"),
2511 PathWithPosition {
2512 path: PathBuf::from("test_file.rs"),
2513 row: None,
2514 column: None
2515 }
2516 );
2517
2518 assert_eq!(
2519 PathWithPosition::parse_str("test_file.rs:1:"),
2520 PathWithPosition {
2521 path: PathBuf::from("test_file.rs"),
2522 row: Some(1),
2523 column: None
2524 }
2525 );
2526
2527 assert_eq!(
2528 PathWithPosition::parse_str("ab\ncd"),
2529 PathWithPosition {
2530 path: PathBuf::from("ab\ncd"),
2531 row: None,
2532 column: None
2533 }
2534 );
2535
2536 assert_eq!(
2537 PathWithPosition::parse_str("👋\nab"),
2538 PathWithPosition {
2539 path: PathBuf::from("👋\nab"),
2540 row: None,
2541 column: None
2542 }
2543 );
2544
2545 assert_eq!(
2546 PathWithPosition::parse_str("Types.hs:(617,9)-(670,28):"),
2547 PathWithPosition {
2548 path: PathBuf::from("Types.hs"),
2549 row: Some(617),
2550 column: Some(9),
2551 }
2552 );
2553
2554 assert_eq!(
2555 PathWithPosition::parse_str("main (1).log"),
2556 PathWithPosition {
2557 path: PathBuf::from("main (1).log"),
2558 row: None,
2559 column: None
2560 }
2561 );
2562 }
2563
2564 #[perf]
2565 #[cfg(not(target_os = "windows"))]
2566 fn path_with_position_parse_posix_path_with_suffix() {
2567 assert_eq!(
2568 PathWithPosition::parse_str("foo/bar:34:in"),
2569 PathWithPosition {
2570 path: PathBuf::from("foo/bar"),
2571 row: Some(34),
2572 column: None,
2573 }
2574 );
2575 assert_eq!(
2576 PathWithPosition::parse_str("foo/bar.rs:1902:::15:"),
2577 PathWithPosition {
2578 path: PathBuf::from("foo/bar.rs:1902"),
2579 row: Some(15),
2580 column: None
2581 }
2582 );
2583
2584 assert_eq!(
2585 PathWithPosition::parse_str("app-editors:zed-0.143.6:20240710-201212.log:34:"),
2586 PathWithPosition {
2587 path: PathBuf::from("app-editors:zed-0.143.6:20240710-201212.log"),
2588 row: Some(34),
2589 column: None,
2590 }
2591 );
2592
2593 assert_eq!(
2594 PathWithPosition::parse_str("crates/file_finder/src/file_finder.rs:1902:13:"),
2595 PathWithPosition {
2596 path: PathBuf::from("crates/file_finder/src/file_finder.rs"),
2597 row: Some(1902),
2598 column: Some(13),
2599 }
2600 );
2601
2602 assert_eq!(
2603 PathWithPosition::parse_str("crate/utils/src/test:today.log:34"),
2604 PathWithPosition {
2605 path: PathBuf::from("crate/utils/src/test:today.log"),
2606 row: Some(34),
2607 column: None,
2608 }
2609 );
2610 assert_eq!(
2611 PathWithPosition::parse_str("/testing/out/src/file_finder.odin(7:15)"),
2612 PathWithPosition {
2613 path: PathBuf::from("/testing/out/src/file_finder.odin"),
2614 row: Some(7),
2615 column: Some(15),
2616 }
2617 );
2618 }
2619
2620 #[perf]
2621 #[cfg(target_os = "windows")]
2622 fn path_with_position_parse_windows_path() {
2623 assert_eq!(
2624 PathWithPosition::parse_str("crates\\utils\\paths.rs"),
2625 PathWithPosition {
2626 path: PathBuf::from("crates\\utils\\paths.rs"),
2627 row: None,
2628 column: None
2629 }
2630 );
2631
2632 assert_eq!(
2633 PathWithPosition::parse_str("C:\\Users\\someone\\test_file.rs"),
2634 PathWithPosition {
2635 path: PathBuf::from("C:\\Users\\someone\\test_file.rs"),
2636 row: None,
2637 column: None
2638 }
2639 );
2640
2641 assert_eq!(
2642 PathWithPosition::parse_str("C:\\Users\\someone\\main (1).log"),
2643 PathWithPosition {
2644 path: PathBuf::from("C:\\Users\\someone\\main (1).log"),
2645 row: None,
2646 column: None
2647 }
2648 );
2649 }
2650
2651 #[perf]
2652 #[cfg(target_os = "windows")]
2653 fn path_with_position_parse_windows_path_with_suffix() {
2654 assert_eq!(
2655 PathWithPosition::parse_str("crates\\utils\\paths.rs:101"),
2656 PathWithPosition {
2657 path: PathBuf::from("crates\\utils\\paths.rs"),
2658 row: Some(101),
2659 column: None
2660 }
2661 );
2662
2663 assert_eq!(
2664 PathWithPosition::parse_str("\\\\?\\C:\\Users\\someone\\test_file.rs:1:20"),
2665 PathWithPosition {
2666 path: PathBuf::from("\\\\?\\C:\\Users\\someone\\test_file.rs"),
2667 row: Some(1),
2668 column: Some(20)
2669 }
2670 );
2671
2672 assert_eq!(
2673 PathWithPosition::parse_str("C:\\Users\\someone\\test_file.rs(1902,13)"),
2674 PathWithPosition {
2675 path: PathBuf::from("C:\\Users\\someone\\test_file.rs"),
2676 row: Some(1902),
2677 column: Some(13)
2678 }
2679 );
2680
2681 assert_eq!(
2683 PathWithPosition::parse_str("\\\\?\\C:\\Users\\someone\\test_file.rs:1902:13:"),
2684 PathWithPosition {
2685 path: PathBuf::from("\\\\?\\C:\\Users\\someone\\test_file.rs"),
2686 row: Some(1902),
2687 column: Some(13)
2688 }
2689 );
2690
2691 assert_eq!(
2692 PathWithPosition::parse_str("\\\\?\\C:\\Users\\someone\\test_file.rs:1902:13:15:"),
2693 PathWithPosition {
2694 path: PathBuf::from("\\\\?\\C:\\Users\\someone\\test_file.rs:1902"),
2695 row: Some(13),
2696 column: Some(15)
2697 }
2698 );
2699
2700 assert_eq!(
2701 PathWithPosition::parse_str("\\\\?\\C:\\Users\\someone\\test_file.rs:1902:::15:"),
2702 PathWithPosition {
2703 path: PathBuf::from("\\\\?\\C:\\Users\\someone\\test_file.rs:1902"),
2704 row: Some(15),
2705 column: None
2706 }
2707 );
2708
2709 assert_eq!(
2710 PathWithPosition::parse_str("\\\\?\\C:\\Users\\someone\\test_file.rs(1902,13):"),
2711 PathWithPosition {
2712 path: PathBuf::from("\\\\?\\C:\\Users\\someone\\test_file.rs"),
2713 row: Some(1902),
2714 column: Some(13),
2715 }
2716 );
2717
2718 assert_eq!(
2719 PathWithPosition::parse_str("\\\\?\\C:\\Users\\someone\\test_file.rs(1902):"),
2720 PathWithPosition {
2721 path: PathBuf::from("\\\\?\\C:\\Users\\someone\\test_file.rs"),
2722 row: Some(1902),
2723 column: None,
2724 }
2725 );
2726
2727 assert_eq!(
2728 PathWithPosition::parse_str("C:\\Users\\someone\\test_file.rs:1902:13:"),
2729 PathWithPosition {
2730 path: PathBuf::from("C:\\Users\\someone\\test_file.rs"),
2731 row: Some(1902),
2732 column: Some(13),
2733 }
2734 );
2735
2736 assert_eq!(
2737 PathWithPosition::parse_str("C:\\Users\\someone\\test_file.rs(1902,13):"),
2738 PathWithPosition {
2739 path: PathBuf::from("C:\\Users\\someone\\test_file.rs"),
2740 row: Some(1902),
2741 column: Some(13),
2742 }
2743 );
2744
2745 assert_eq!(
2746 PathWithPosition::parse_str("C:\\Users\\someone\\test_file.rs(1902):"),
2747 PathWithPosition {
2748 path: PathBuf::from("C:\\Users\\someone\\test_file.rs"),
2749 row: Some(1902),
2750 column: None,
2751 }
2752 );
2753
2754 assert_eq!(
2755 PathWithPosition::parse_str("crates/utils/paths.rs:101"),
2756 PathWithPosition {
2757 path: PathBuf::from("crates\\utils\\paths.rs"),
2758 row: Some(101),
2759 column: None,
2760 }
2761 );
2762 }
2763
2764 #[perf]
2765 fn test_path_compact() {
2766 let path: PathBuf = [
2767 home_dir().to_string_lossy().into_owned(),
2768 "some_file.txt".to_string(),
2769 ]
2770 .iter()
2771 .collect();
2772 if cfg!(any(target_os = "linux", target_os = "freebsd")) || cfg!(target_os = "macos") {
2773 assert_eq!(path.compact().to_str(), Some("~/some_file.txt"));
2774 } else {
2775 assert_eq!(path.compact().to_str(), path.to_str());
2776 }
2777 }
2778
2779 #[perf]
2780 fn test_extension_or_hidden_file_name() {
2781 let path = Path::new("/a/b/c/file_name.rs");
2783 assert_eq!(path.extension_or_hidden_file_name(), Some("rs"));
2784
2785 let path = Path::new("/a/b/c/file.name.rs");
2787 assert_eq!(path.extension_or_hidden_file_name(), Some("rs"));
2788
2789 let path = Path::new("/a/b/c/long.file.name.rs");
2791 assert_eq!(path.extension_or_hidden_file_name(), Some("rs"));
2792
2793 let path = Path::new("/a/b/c/.gitignore");
2795 assert_eq!(path.extension_or_hidden_file_name(), Some("gitignore"));
2796
2797 let path = Path::new("/a/b/c/.eslintrc.js");
2799 assert_eq!(path.extension_or_hidden_file_name(), Some("eslintrc.js"));
2800 }
2801
2802 #[perf]
2803 #[perf]
2839 #[cfg(target_os = "windows")]
2840 fn test_sanitized_path() {
2841 let path = Path::new("C:\\Users\\someone\\test_file.rs");
2842 let sanitized_path = SanitizedPath::new(path);
2843 assert_eq!(
2844 sanitized_path.to_string(),
2845 "C:\\Users\\someone\\test_file.rs"
2846 );
2847
2848 let path = Path::new("\\\\?\\C:\\Users\\someone\\test_file.rs");
2849 let sanitized_path = SanitizedPath::new(path);
2850 assert_eq!(
2851 sanitized_path.to_string(),
2852 "C:\\Users\\someone\\test_file.rs"
2853 );
2854 }
2855
2856 #[perf]
2857 fn test_compare_numeric_segments() {
2858 fn compare(a: &str, b: &str) -> Ordering {
2860 let mut a_iter = a.chars().peekable();
2861 let mut b_iter = b.chars().peekable();
2862
2863 let result = compare_numeric_segments(&mut a_iter, &mut b_iter);
2864
2865 assert!(
2867 !a_iter.next().is_some_and(|c| c.is_ascii_digit()),
2868 "Iterator a should have consumed all digits"
2869 );
2870 assert!(
2871 !b_iter.next().is_some_and(|c| c.is_ascii_digit()),
2872 "Iterator b should have consumed all digits"
2873 );
2874
2875 result
2876 }
2877
2878 assert_eq!(compare("0", "0"), Ordering::Equal);
2880 assert_eq!(compare("1", "2"), Ordering::Less);
2881 assert_eq!(compare("9", "10"), Ordering::Less);
2882 assert_eq!(compare("10", "9"), Ordering::Greater);
2883 assert_eq!(compare("99", "100"), Ordering::Less);
2884
2885 assert_eq!(compare("0", "00"), Ordering::Less);
2887 assert_eq!(compare("00", "0"), Ordering::Greater);
2888 assert_eq!(compare("01", "1"), Ordering::Greater);
2889 assert_eq!(compare("001", "1"), Ordering::Greater);
2890 assert_eq!(compare("001", "01"), Ordering::Greater);
2891
2892 assert_eq!(compare("000100", "100"), Ordering::Greater);
2894 assert_eq!(compare("100", "0100"), Ordering::Less);
2895 assert_eq!(compare("0100", "00100"), Ordering::Less);
2896
2897 assert_eq!(compare("9999999999", "10000000000"), Ordering::Less);
2899 assert_eq!(
2900 compare(
2901 "340282366920938463463374607431768211455", "340282366920938463463374607431768211456"
2903 ),
2904 Ordering::Less
2905 );
2906 assert_eq!(
2907 compare(
2908 "340282366920938463463374607431768211456", "340282366920938463463374607431768211455"
2910 ),
2911 Ordering::Greater
2912 );
2913
2914 let mut a_iter = "123abc".chars().peekable();
2916 let mut b_iter = "456def".chars().peekable();
2917
2918 compare_numeric_segments(&mut a_iter, &mut b_iter);
2919
2920 assert_eq!(a_iter.collect::<String>(), "abc");
2921 assert_eq!(b_iter.collect::<String>(), "def");
2922 }
2923
2924 #[perf]
2925 fn test_natural_sort() {
2926 assert_eq!(natural_sort("a", "b"), Ordering::Less);
2928 assert_eq!(natural_sort("b", "a"), Ordering::Greater);
2929 assert_eq!(natural_sort("a", "a"), Ordering::Equal);
2930
2931 assert_eq!(natural_sort("a", "A"), Ordering::Less);
2933 assert_eq!(natural_sort("A", "a"), Ordering::Greater);
2934 assert_eq!(natural_sort("aA", "aa"), Ordering::Greater);
2935 assert_eq!(natural_sort("aa", "aA"), Ordering::Less);
2936
2937 assert_eq!(natural_sort("1", "2"), Ordering::Less);
2939 assert_eq!(natural_sort("2", "10"), Ordering::Less);
2940 assert_eq!(natural_sort("02", "10"), Ordering::Less);
2941 assert_eq!(natural_sort("02", "2"), Ordering::Greater);
2942
2943 assert_eq!(natural_sort("a1", "a2"), Ordering::Less);
2945 assert_eq!(natural_sort("a2", "a10"), Ordering::Less);
2946 assert_eq!(natural_sort("a02", "a2"), Ordering::Greater);
2947 assert_eq!(natural_sort("a1b", "a1c"), Ordering::Less);
2948
2949 assert_eq!(natural_sort("1a2", "1a10"), Ordering::Less);
2951 assert_eq!(natural_sort("1a10", "1a2"), Ordering::Greater);
2952 assert_eq!(natural_sort("2a1", "10a1"), Ordering::Less);
2953
2954 assert_eq!(natural_sort("a-1", "a-2"), Ordering::Less);
2956 assert_eq!(natural_sort("a_1", "a_2"), Ordering::Less);
2957 assert_eq!(natural_sort("a.1", "a.2"), Ordering::Less);
2958
2959 assert_eq!(natural_sort("文1", "文2"), Ordering::Less);
2961 assert_eq!(natural_sort("文2", "文10"), Ordering::Less);
2962 assert_eq!(natural_sort("🔤1", "🔤2"), Ordering::Less);
2963
2964 assert_eq!(natural_sort("", ""), Ordering::Equal);
2966 assert_eq!(natural_sort("", "a"), Ordering::Less);
2967 assert_eq!(natural_sort("a", ""), Ordering::Greater);
2968 assert_eq!(natural_sort(" ", " "), Ordering::Less);
2969
2970 assert_eq!(natural_sort("File-1.txt", "File-2.txt"), Ordering::Less);
2972 assert_eq!(natural_sort("File-02.txt", "File-2.txt"), Ordering::Greater);
2973 assert_eq!(natural_sort("File-2.txt", "File-10.txt"), Ordering::Less);
2974 assert_eq!(natural_sort("File_A1", "File_A2"), Ordering::Less);
2975 assert_eq!(natural_sort("File_a1", "File_A1"), Ordering::Less);
2976 }
2977
2978 #[perf]
2979 fn test_compare_paths() {
2980 fn compare(a: &str, is_a_file: bool, b: &str, is_b_file: bool) -> Ordering {
2982 compare_paths((Path::new(a), is_a_file), (Path::new(b), is_b_file))
2983 }
2984
2985 assert_eq!(compare("a", true, "b", true), Ordering::Less);
2987 assert_eq!(compare("b", true, "a", true), Ordering::Greater);
2988 assert_eq!(compare("a", true, "a", true), Ordering::Equal);
2989
2990 assert_eq!(compare("a", true, "a", false), Ordering::Greater);
2992 assert_eq!(compare("a", false, "a", true), Ordering::Less);
2993 assert_eq!(compare("b", false, "a", true), Ordering::Less);
2994
2995 assert_eq!(compare("a.txt", true, "a.md", true), Ordering::Greater);
2997 assert_eq!(compare("a.md", true, "a.txt", true), Ordering::Less);
2998 assert_eq!(compare("a", true, "a.txt", true), Ordering::Less);
2999
3000 assert_eq!(compare("dir/a", true, "dir/b", true), Ordering::Less);
3002 assert_eq!(compare("dir1/a", true, "dir2/a", true), Ordering::Less);
3003 assert_eq!(compare("dir/sub/a", true, "dir/a", true), Ordering::Less);
3004
3005 assert_eq!(
3007 compare("Dir/file", true, "dir/file", true),
3008 Ordering::Greater
3009 );
3010 assert_eq!(
3011 compare("dir/File", true, "dir/file", true),
3012 Ordering::Greater
3013 );
3014 assert_eq!(compare("dir/file", true, "Dir/File", true), Ordering::Less);
3015
3016 assert_eq!(compare(".hidden", true, "visible", true), Ordering::Less);
3018 assert_eq!(compare("_special", true, "normal", true), Ordering::Less);
3019 assert_eq!(compare(".config", false, ".data", false), Ordering::Less);
3020
3021 assert_eq!(
3023 compare("dir1/file", true, "dir2/file", true),
3024 Ordering::Less
3025 );
3026 assert_eq!(
3027 compare("dir2/file", true, "dir10/file", true),
3028 Ordering::Less
3029 );
3030 assert_eq!(
3031 compare("dir02/file", true, "dir2/file", true),
3032 Ordering::Greater
3033 );
3034
3035 assert_eq!(compare("/a", true, "/b", true), Ordering::Less);
3037 assert_eq!(compare("/", false, "/a", true), Ordering::Less);
3038
3039 assert_eq!(
3041 compare("project/src/main.rs", true, "project/src/lib.rs", true),
3042 Ordering::Greater
3043 );
3044 assert_eq!(
3045 compare(
3046 "project/tests/test_1.rs",
3047 true,
3048 "project/tests/test_2.rs",
3049 true
3050 ),
3051 Ordering::Less
3052 );
3053 assert_eq!(
3054 compare(
3055 "project/v1.0.0/README.md",
3056 true,
3057 "project/v1.10.0/README.md",
3058 true
3059 ),
3060 Ordering::Less
3061 );
3062 }
3063
3064 #[perf]
3065 fn test_natural_sort_case_sensitivity() {
3066 std::thread::sleep(std::time::Duration::from_millis(100));
3067 assert_eq!(natural_sort("a", "A"), Ordering::Less);
3069 assert_eq!(natural_sort("A", "a"), Ordering::Greater);
3070 assert_eq!(natural_sort("a", "a"), Ordering::Equal);
3071 assert_eq!(natural_sort("A", "A"), Ordering::Equal);
3072
3073 assert_eq!(natural_sort("aaa", "AAA"), Ordering::Less);
3075 assert_eq!(natural_sort("AAA", "aaa"), Ordering::Greater);
3076 assert_eq!(natural_sort("aAa", "AaA"), Ordering::Less);
3077
3078 assert_eq!(natural_sort("a", "b"), Ordering::Less);
3080 assert_eq!(natural_sort("A", "b"), Ordering::Less);
3081 assert_eq!(natural_sort("a", "B"), Ordering::Less);
3082 }
3083
3084 #[perf]
3085 fn test_natural_sort_with_numbers() {
3086 assert_eq!(natural_sort("file1", "file2"), Ordering::Less);
3088 assert_eq!(natural_sort("file2", "file10"), Ordering::Less);
3089 assert_eq!(natural_sort("file10", "file2"), Ordering::Greater);
3090
3091 assert_eq!(natural_sort("1file", "2file"), Ordering::Less);
3093 assert_eq!(natural_sort("file1text", "file2text"), Ordering::Less);
3094 assert_eq!(natural_sort("text1file", "text2file"), Ordering::Less);
3095
3096 assert_eq!(natural_sort("file1-2", "file1-10"), Ordering::Less);
3098 assert_eq!(natural_sort("2-1file", "10-1file"), Ordering::Less);
3099
3100 assert_eq!(natural_sort("file002", "file2"), Ordering::Greater);
3102 assert_eq!(natural_sort("file002", "file10"), Ordering::Less);
3103
3104 assert_eq!(
3106 natural_sort("file999999999999999999999", "file999999999999999999998"),
3107 Ordering::Greater
3108 );
3109
3110 assert_eq!(
3114 natural_sort(
3115 "file340282366920938463463374607431768211454",
3116 "file340282366920938463463374607431768211455"
3117 ),
3118 Ordering::Less
3119 );
3120
3121 assert_eq!(
3123 natural_sort(
3124 "file340282366920938463463374607431768211456",
3125 "file340282366920938463463374607431768211455"
3126 ),
3127 Ordering::Greater
3128 );
3129
3130 assert_eq!(
3132 natural_sort(
3133 "file3402823669209384634633746074317682114560",
3134 "file340282366920938463463374607431768211455"
3135 ),
3136 Ordering::Greater
3137 );
3138
3139 assert_eq!(
3141 natural_sort(
3142 "file0340282366920938463463374607431768211455",
3143 "file340282366920938463463374607431768211455"
3144 ),
3145 Ordering::Greater
3146 );
3147
3148 assert_eq!(
3150 natural_sort(
3151 "file999999999999999999999999999999999999999999999999",
3152 "file9999999999999999999999999999999999999999999999999"
3153 ),
3154 Ordering::Less
3155 );
3156 }
3157
3158 #[perf]
3159 fn test_natural_sort_case_sensitive() {
3160 assert_eq!(natural_sort("File1", "file2"), Ordering::Less);
3162 assert_eq!(natural_sort("file1", "File2"), Ordering::Less);
3163
3164 assert_eq!(natural_sort("Dir1", "dir01"), Ordering::Less);
3167 assert_eq!(natural_sort("dir2", "Dir02"), Ordering::Less);
3168 assert_eq!(natural_sort("dir2", "dir02"), Ordering::Less);
3169
3170 assert_eq!(natural_sort("dir1", "Dir1"), Ordering::Less);
3173 assert_eq!(natural_sort("dir02", "Dir02"), Ordering::Less);
3174 assert_eq!(natural_sort("dir10", "Dir10"), Ordering::Less);
3175 }
3176
3177 #[perf]
3178 fn test_natural_sort_edge_cases() {
3179 assert_eq!(natural_sort("", ""), Ordering::Equal);
3181 assert_eq!(natural_sort("", "a"), Ordering::Less);
3182 assert_eq!(natural_sort("a", ""), Ordering::Greater);
3183
3184 assert_eq!(natural_sort("file-1", "file_1"), Ordering::Less);
3186 assert_eq!(natural_sort("file.1", "file_1"), Ordering::Less);
3187 assert_eq!(natural_sort("file 1", "file_1"), Ordering::Less);
3188
3189 assert_eq!(natural_sort("file①", "file②"), Ordering::Less);
3192 assert_eq!(natural_sort("file⑩", "file②"), Ordering::Greater);
3194 assert_eq!(natural_sort("file漢", "file字"), Ordering::Greater);
3196
3197 assert_eq!(natural_sort("file-1a", "file-1b"), Ordering::Less);
3199 assert_eq!(natural_sort("file-1.2", "file-1.10"), Ordering::Less);
3200 assert_eq!(natural_sort("file-1.10", "file-1.2"), Ordering::Greater);
3201 }
3202
3203 #[test]
3204 fn test_multiple_extensions() {
3205 let path = Path::new("/a/b/c/file_name");
3207 assert_eq!(path.multiple_extensions(), None);
3208
3209 let path = Path::new("/a/b/c/file_name.tsx");
3211 assert_eq!(path.multiple_extensions(), None);
3212
3213 let path = Path::new("/a/b/c/file_name.stories.tsx");
3215 assert_eq!(path.multiple_extensions(), Some("stories.tsx".to_string()));
3216
3217 let path = Path::new("/a/b/c/long.app.tar.gz");
3219 assert_eq!(path.multiple_extensions(), Some("app.tar.gz".to_string()));
3220 }
3221
3222 #[test]
3223 fn test_strip_path_suffix() {
3224 let base = Path::new("/a/b/c/file_name");
3225 let suffix = Path::new("file_name");
3226 assert_eq!(strip_path_suffix(base, suffix), Some(Path::new("/a/b/c")));
3227
3228 let base = Path::new("/a/b/c/file_name.tsx");
3229 let suffix = Path::new("file_name.tsx");
3230 assert_eq!(strip_path_suffix(base, suffix), Some(Path::new("/a/b/c")));
3231
3232 let base = Path::new("/a/b/c/file_name.stories.tsx");
3233 let suffix = Path::new("c/file_name.stories.tsx");
3234 assert_eq!(strip_path_suffix(base, suffix), Some(Path::new("/a/b")));
3235
3236 let base = Path::new("/a/b/c/long.app.tar.gz");
3237 let suffix = Path::new("b/c/long.app.tar.gz");
3238 assert_eq!(strip_path_suffix(base, suffix), Some(Path::new("/a")));
3239
3240 let base = Path::new("/a/b/c/long.app.tar.gz");
3241 let suffix = Path::new("/a/b/c/long.app.tar.gz");
3242 assert_eq!(strip_path_suffix(base, suffix), Some(Path::new("")));
3243
3244 let base = Path::new("/a/b/c/long.app.tar.gz");
3245 let suffix = Path::new("/a/b/c/no_match.app.tar.gz");
3246 assert_eq!(strip_path_suffix(base, suffix), None);
3247
3248 let base = Path::new("/a/b/c/long.app.tar.gz");
3249 let suffix = Path::new("app.tar.gz");
3250 assert_eq!(strip_path_suffix(base, suffix), None);
3251 }
3252
3253 #[test]
3254 fn test_strip_prefix() {
3255 let expected = [
3256 (
3257 PathStyle::Posix,
3258 "/a/b/c",
3259 "/a/b",
3260 Some(rel_path("c").into_arc()),
3261 ),
3262 (
3263 PathStyle::Posix,
3264 "/a/b/c",
3265 "/a/b/",
3266 Some(rel_path("c").into_arc()),
3267 ),
3268 (
3269 PathStyle::Posix,
3270 "/a/b/c",
3271 "/",
3272 Some(rel_path("a/b/c").into_arc()),
3273 ),
3274 (PathStyle::Posix, "/a/b/c", "", None),
3275 (PathStyle::Posix, "/a/b//c", "/a/b/", None),
3276 (PathStyle::Posix, "/a/bc", "/a/b", None),
3277 (
3278 PathStyle::Posix,
3279 "/a/b/c",
3280 "/a/b/c",
3281 Some(rel_path("").into_arc()),
3282 ),
3283 (
3284 PathStyle::Windows,
3285 "C:\\a\\b\\c",
3286 "C:\\a\\b",
3287 Some(rel_path("c").into_arc()),
3288 ),
3289 (
3290 PathStyle::Windows,
3291 "C:\\a\\b\\c",
3292 "C:\\a\\b\\",
3293 Some(rel_path("c").into_arc()),
3294 ),
3295 (
3296 PathStyle::Windows,
3297 "C:\\a\\b\\c",
3298 "C:\\",
3299 Some(rel_path("a/b/c").into_arc()),
3300 ),
3301 (PathStyle::Windows, "C:\\a\\b\\c", "", None),
3302 (PathStyle::Windows, "C:\\a\\b\\\\c", "C:\\a\\b\\", None),
3303 (PathStyle::Windows, "C:\\a\\bc", "C:\\a\\b", None),
3304 (
3305 PathStyle::Windows,
3306 "C:\\a\\b/c",
3307 "C:\\a\\b",
3308 Some(rel_path("c").into_arc()),
3309 ),
3310 (
3311 PathStyle::Windows,
3312 "C:\\a\\b/c",
3313 "C:\\a\\b\\",
3314 Some(rel_path("c").into_arc()),
3315 ),
3316 (
3317 PathStyle::Windows,
3318 "C:\\a\\b/c",
3319 "C:\\a\\b/",
3320 Some(rel_path("c").into_arc()),
3321 ),
3322 ];
3323 let actual = expected.clone().map(|(style, child, parent, _)| {
3324 (
3325 style,
3326 child,
3327 parent,
3328 style
3329 .strip_prefix(child.as_ref(), parent.as_ref())
3330 .map(|rel_path| rel_path.into_arc()),
3331 )
3332 });
3333 pretty_assertions::assert_eq!(actual, expected);
3334 }
3335
3336 #[cfg(target_os = "windows")]
3337 #[test]
3338 fn test_wsl_path() {
3339 use super::WslPath;
3340 let path = "/a/b/c";
3341 assert_eq!(WslPath::from_path(&path), None);
3342
3343 let path = r"\\wsl.localhost";
3344 assert_eq!(WslPath::from_path(&path), None);
3345
3346 let path = r"\\wsl.localhost\Distro";
3347 assert_eq!(
3348 WslPath::from_path(&path),
3349 Some(WslPath {
3350 distro: "Distro".to_owned(),
3351 path: "/".into(),
3352 })
3353 );
3354
3355 let path = r"\\wsl.localhost\Distro\blue";
3356 assert_eq!(
3357 WslPath::from_path(&path),
3358 Some(WslPath {
3359 distro: "Distro".to_owned(),
3360 path: "/blue".into()
3361 })
3362 );
3363
3364 let path = r"\\wsl$\archlinux\tomato\.\paprika\..\aubergine.txt";
3365 assert_eq!(
3366 WslPath::from_path(&path),
3367 Some(WslPath {
3368 distro: "archlinux".to_owned(),
3369 path: "/tomato/paprika/../aubergine.txt".into()
3370 })
3371 );
3372
3373 let path = r"\\windows.localhost\Distro\foo";
3374 assert_eq!(WslPath::from_path(&path), None);
3375 }
3376
3377 #[test]
3378 fn test_url_to_file_path_ext_posix_basic() {
3379 use super::UrlExt;
3380
3381 let url = url::Url::parse("file:///home/user/file.txt").unwrap();
3382 assert_eq!(
3383 url.to_file_path_ext(PathStyle::Posix),
3384 Ok(PathBuf::from("/home/user/file.txt"))
3385 );
3386
3387 let url = url::Url::parse("file:///").unwrap();
3388 assert_eq!(
3389 url.to_file_path_ext(PathStyle::Posix),
3390 Ok(PathBuf::from("/"))
3391 );
3392
3393 let url = url::Url::parse("file:///a/b/c/d/e").unwrap();
3394 assert_eq!(
3395 url.to_file_path_ext(PathStyle::Posix),
3396 Ok(PathBuf::from("/a/b/c/d/e"))
3397 );
3398 }
3399
3400 #[test]
3401 fn test_url_to_file_path_ext_posix_percent_encoding() {
3402 use super::UrlExt;
3403
3404 let url = url::Url::parse("file:///home/user/file%20with%20spaces.txt").unwrap();
3405 assert_eq!(
3406 url.to_file_path_ext(PathStyle::Posix),
3407 Ok(PathBuf::from("/home/user/file with spaces.txt"))
3408 );
3409
3410 let url = url::Url::parse("file:///path%2Fwith%2Fencoded%2Fslashes").unwrap();
3411 assert_eq!(
3412 url.to_file_path_ext(PathStyle::Posix),
3413 Ok(PathBuf::from("/path/with/encoded/slashes"))
3414 );
3415
3416 let url = url::Url::parse("file:///special%23chars%3F.txt").unwrap();
3417 assert_eq!(
3418 url.to_file_path_ext(PathStyle::Posix),
3419 Ok(PathBuf::from("/special#chars?.txt"))
3420 );
3421 }
3422
3423 #[test]
3424 fn test_url_to_file_path_ext_posix_localhost() {
3425 use super::UrlExt;
3426
3427 let url = url::Url::parse("file://localhost/home/user/file.txt").unwrap();
3428 assert_eq!(
3429 url.to_file_path_ext(PathStyle::Posix),
3430 Ok(PathBuf::from("/home/user/file.txt"))
3431 );
3432 }
3433
3434 #[test]
3435 fn test_url_to_file_path_ext_posix_rejects_host() {
3436 use super::UrlExt;
3437
3438 let url = url::Url::parse("file://somehost/home/user/file.txt").unwrap();
3439 assert_eq!(url.to_file_path_ext(PathStyle::Posix), Err(()));
3440 }
3441
3442 #[test]
3443 fn test_url_to_file_path_ext_posix_windows_drive_letter() {
3444 use super::UrlExt;
3445
3446 let url = url::Url::parse("file:///C:").unwrap();
3447 assert_eq!(
3448 url.to_file_path_ext(PathStyle::Posix),
3449 Ok(PathBuf::from("/C:/"))
3450 );
3451
3452 let url = url::Url::parse("file:///D|").unwrap();
3453 assert_eq!(
3454 url.to_file_path_ext(PathStyle::Posix),
3455 Ok(PathBuf::from("/D|/"))
3456 );
3457 }
3458
3459 #[test]
3460 fn test_url_to_file_path_ext_windows_basic() {
3461 use super::UrlExt;
3462
3463 let url = url::Url::parse("file:///C:/Users/user/file.txt").unwrap();
3464 assert_eq!(
3465 url.to_file_path_ext(PathStyle::Windows),
3466 Ok(PathBuf::from("C:\\Users\\user\\file.txt"))
3467 );
3468
3469 let url = url::Url::parse("file:///D:/folder/subfolder/file.rs").unwrap();
3470 assert_eq!(
3471 url.to_file_path_ext(PathStyle::Windows),
3472 Ok(PathBuf::from("D:\\folder\\subfolder\\file.rs"))
3473 );
3474
3475 let url = url::Url::parse("file:///C:/").unwrap();
3476 assert_eq!(
3477 url.to_file_path_ext(PathStyle::Windows),
3478 Ok(PathBuf::from("C:\\"))
3479 );
3480 }
3481
3482 #[test]
3483 fn test_url_to_file_path_ext_windows_encoded_drive_letter() {
3484 use super::UrlExt;
3485
3486 let url = url::Url::parse("file:///C%3A/Users/file.txt").unwrap();
3487 assert_eq!(
3488 url.to_file_path_ext(PathStyle::Windows),
3489 Ok(PathBuf::from("C:\\Users\\file.txt"))
3490 );
3491
3492 let url = url::Url::parse("file:///c%3a/Users/file.txt").unwrap();
3493 assert_eq!(
3494 url.to_file_path_ext(PathStyle::Windows),
3495 Ok(PathBuf::from("c:\\Users\\file.txt"))
3496 );
3497
3498 let url = url::Url::parse("file:///D%3A/folder/file.txt").unwrap();
3499 assert_eq!(
3500 url.to_file_path_ext(PathStyle::Windows),
3501 Ok(PathBuf::from("D:\\folder\\file.txt"))
3502 );
3503
3504 let url = url::Url::parse("file:///d%3A/folder/file.txt").unwrap();
3505 assert_eq!(
3506 url.to_file_path_ext(PathStyle::Windows),
3507 Ok(PathBuf::from("d:\\folder\\file.txt"))
3508 );
3509 }
3510
3511 #[test]
3512 fn test_url_to_file_path_ext_windows_unc_path() {
3513 use super::UrlExt;
3514
3515 let url = url::Url::parse("file://server/share/path/file.txt").unwrap();
3516 assert_eq!(
3517 url.to_file_path_ext(PathStyle::Windows),
3518 Ok(PathBuf::from("\\\\server\\share\\path\\file.txt"))
3519 );
3520
3521 let url = url::Url::parse("file://server/share").unwrap();
3522 assert_eq!(
3523 url.to_file_path_ext(PathStyle::Windows),
3524 Ok(PathBuf::from("\\\\server\\share"))
3525 );
3526 }
3527
3528 #[test]
3529 fn test_url_to_file_path_ext_windows_percent_encoding() {
3530 use super::UrlExt;
3531
3532 let url = url::Url::parse("file:///C:/Users/user/file%20with%20spaces.txt").unwrap();
3533 assert_eq!(
3534 url.to_file_path_ext(PathStyle::Windows),
3535 Ok(PathBuf::from("C:\\Users\\user\\file with spaces.txt"))
3536 );
3537
3538 let url = url::Url::parse("file:///C:/special%23chars%3F.txt").unwrap();
3539 assert_eq!(
3540 url.to_file_path_ext(PathStyle::Windows),
3541 Ok(PathBuf::from("C:\\special#chars?.txt"))
3542 );
3543 }
3544
3545 #[test]
3546 fn test_url_to_file_path_ext_windows_invalid_drive() {
3547 use super::UrlExt;
3548
3549 let url = url::Url::parse("file:///1:/path/file.txt").unwrap();
3550 assert_eq!(url.to_file_path_ext(PathStyle::Windows), Err(()));
3551
3552 let url = url::Url::parse("file:///CC:/path/file.txt").unwrap();
3553 assert_eq!(url.to_file_path_ext(PathStyle::Windows), Err(()));
3554
3555 let url = url::Url::parse("file:///C/path/file.txt").unwrap();
3556 assert_eq!(url.to_file_path_ext(PathStyle::Windows), Err(()));
3557
3558 let url = url::Url::parse("file:///invalid").unwrap();
3559 assert_eq!(url.to_file_path_ext(PathStyle::Windows), Err(()));
3560 }
3561
3562 #[test]
3563 fn test_url_to_file_path_ext_non_file_scheme() {
3564 use super::UrlExt;
3565
3566 let url = url::Url::parse("http://example.com/path").unwrap();
3567 assert_eq!(url.to_file_path_ext(PathStyle::Posix), Err(()));
3568 assert_eq!(url.to_file_path_ext(PathStyle::Windows), Err(()));
3569
3570 let url = url::Url::parse("https://example.com/path").unwrap();
3571 assert_eq!(url.to_file_path_ext(PathStyle::Posix), Err(()));
3572 assert_eq!(url.to_file_path_ext(PathStyle::Windows), Err(()));
3573 }
3574
3575 #[test]
3576 fn test_url_to_file_path_ext_windows_localhost() {
3577 use super::UrlExt;
3578
3579 let url = url::Url::parse("file://localhost/C:/Users/file.txt").unwrap();
3580 assert_eq!(
3581 url.to_file_path_ext(PathStyle::Windows),
3582 Ok(PathBuf::from("C:\\Users\\file.txt"))
3583 );
3584 }
3585}