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 const FILE_ROW_COLUMN_DELIMITER: char = ':';
666
667const ROW_COL_CAPTURE_REGEX: &str = r"(?xs)
668 ([^\(]+)\:(?:
669 \((\d+)[,:](\d+)\) # filename:(row,column), filename:(row:column)
670 |
671 \((\d+)\)() # filename:(row)
672 )
673 |
674 ([^\(]+)(?:
675 \((\d+)[,:](\d+)\) # filename(row,column), filename(row:column)
676 |
677 \((\d+)\)() # filename(row)
678 )
679 \:*$
680 |
681 (.+?)(?:
682 \:+(\d+)\:(\d+)\:*$ # filename:row:column
683 |
684 \:+(\d+)\:*()$ # filename:row
685 |
686 \:+()()$
687 )";
688
689#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)]
692pub struct PathWithPosition {
693 pub path: PathBuf,
694 pub row: Option<u32>,
695 pub column: Option<u32>,
697}
698
699impl PathWithPosition {
700 pub fn from_path(path: PathBuf) -> Self {
702 Self {
703 path,
704 row: None,
705 column: None,
706 }
707 }
708
709 pub fn parse_str(s: &str) -> Self {
795 let trimmed = s.trim();
796 let path = Path::new(trimmed);
797 let Some(maybe_file_name_with_row_col) = path.file_name().unwrap_or_default().to_str()
798 else {
799 return Self {
800 path: Path::new(s).to_path_buf(),
801 row: None,
802 column: None,
803 };
804 };
805 if maybe_file_name_with_row_col.is_empty() {
806 return Self {
807 path: Path::new(s).to_path_buf(),
808 row: None,
809 column: None,
810 };
811 }
812
813 static SUFFIX_RE: LazyLock<Regex> =
817 LazyLock::new(|| Regex::new(ROW_COL_CAPTURE_REGEX).unwrap());
818 match SUFFIX_RE
819 .captures(maybe_file_name_with_row_col)
820 .map(|caps| caps.extract())
821 {
822 Some((_, [file_name, maybe_row, maybe_column])) => {
823 let row = maybe_row.parse::<u32>().ok();
824 let column = maybe_column.parse::<u32>().ok();
825
826 let (_, suffix) = trimmed.split_once(file_name).unwrap();
827 let path_without_suffix = &trimmed[..trimmed.len() - suffix.len()];
828
829 Self {
830 path: Path::new(path_without_suffix).to_path_buf(),
831 row,
832 column,
833 }
834 }
835 None => {
836 let delimiter = ':';
840 let mut path_parts = s
841 .rsplitn(3, delimiter)
842 .collect::<Vec<_>>()
843 .into_iter()
844 .rev()
845 .fuse();
846 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();
847 let mut row = None;
848 let mut column = None;
849 if let Some(maybe_row) = path_parts.next() {
850 if let Ok(parsed_row) = maybe_row.parse::<u32>() {
851 row = Some(parsed_row);
852 if let Some(parsed_column) = path_parts
853 .next()
854 .and_then(|maybe_col| maybe_col.parse::<u32>().ok())
855 {
856 column = Some(parsed_column);
857 }
858 } else {
859 path_string.push(delimiter);
860 path_string.push_str(maybe_row);
861 }
862 }
863 for split in path_parts {
864 path_string.push(delimiter);
865 path_string.push_str(split);
866 }
867
868 Self {
869 path: PathBuf::from(path_string),
870 row,
871 column,
872 }
873 }
874 }
875 }
876
877 pub fn map_path<E>(
878 self,
879 mapping: impl FnOnce(PathBuf) -> Result<PathBuf, E>,
880 ) -> Result<PathWithPosition, E> {
881 Ok(PathWithPosition {
882 path: mapping(self.path)?,
883 row: self.row,
884 column: self.column,
885 })
886 }
887
888 pub fn to_string(&self, path_to_string: &dyn Fn(&PathBuf) -> String) -> String {
889 let path_string = path_to_string(&self.path);
890 if let Some(row) = self.row {
891 if let Some(column) = self.column {
892 format!("{path_string}:{row}:{column}")
893 } else {
894 format!("{path_string}:{row}")
895 }
896 } else {
897 path_string
898 }
899 }
900}
901
902#[derive(Clone)]
903pub struct PathMatcher {
904 sources: Vec<(String, RelPathBuf, bool)>,
905 glob: GlobSet,
906 path_style: PathStyle,
907}
908
909impl std::fmt::Debug for PathMatcher {
910 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
911 f.debug_struct("PathMatcher")
912 .field("sources", &self.sources)
913 .field("path_style", &self.path_style)
914 .finish()
915 }
916}
917
918impl PartialEq for PathMatcher {
919 fn eq(&self, other: &Self) -> bool {
920 self.sources.eq(&other.sources)
921 }
922}
923
924impl Eq for PathMatcher {}
925
926impl PathMatcher {
927 pub fn new(
928 globs: impl IntoIterator<Item = impl AsRef<str>>,
929 path_style: PathStyle,
930 ) -> Result<Self, globset::Error> {
931 let globs = globs
932 .into_iter()
933 .map(|as_str| {
934 GlobBuilder::new(as_str.as_ref())
935 .backslash_escape(path_style.is_posix())
936 .build()
937 })
938 .collect::<Result<Vec<_>, _>>()?;
939 let sources = globs
940 .iter()
941 .filter_map(|glob| {
942 let glob = glob.glob();
943 Some((
944 glob.to_string(),
945 RelPath::new(&glob.as_ref(), path_style)
946 .ok()
947 .map(std::borrow::Cow::into_owned)?,
948 glob.ends_with(path_style.separators_ch()),
949 ))
950 })
951 .collect();
952 let mut glob_builder = GlobSetBuilder::new();
953 for single_glob in globs {
954 glob_builder.add(single_glob);
955 }
956 let glob = glob_builder.build()?;
957 Ok(PathMatcher {
958 glob,
959 sources,
960 path_style,
961 })
962 }
963
964 pub fn sources(&self) -> impl Iterator<Item = &str> + Clone {
965 self.sources.iter().map(|(source, ..)| source.as_str())
966 }
967
968 pub fn is_match<P: AsRef<RelPath>>(&self, other: P) -> bool {
969 let other = other.as_ref();
970 if self
971 .sources
972 .iter()
973 .any(|(_, source, _)| other.starts_with(source) || other.ends_with(source))
974 {
975 return true;
976 }
977 let other_path = other.display(self.path_style);
978
979 if self.glob.is_match(&*other_path) {
980 return true;
981 }
982
983 self.glob
984 .is_match(other_path.into_owned() + self.path_style.primary_separator())
985 }
986
987 pub fn is_match_std_path<P: AsRef<Path>>(&self, other: P) -> bool {
988 let other = other.as_ref();
989 if self.sources.iter().any(|(_, source, _)| {
990 other.starts_with(source.as_std_path()) || other.ends_with(source.as_std_path())
991 }) {
992 return true;
993 }
994 self.glob.is_match(other)
995 }
996}
997
998impl Default for PathMatcher {
999 fn default() -> Self {
1000 Self {
1001 path_style: PathStyle::local(),
1002 glob: GlobSet::empty(),
1003 sources: vec![],
1004 }
1005 }
1006}
1007
1008fn compare_numeric_segments<I>(
1043 a_iter: &mut std::iter::Peekable<I>,
1044 b_iter: &mut std::iter::Peekable<I>,
1045) -> Ordering
1046where
1047 I: Iterator<Item = char>,
1048{
1049 let mut a_num_str = String::new();
1051 let mut b_num_str = String::new();
1052
1053 while let Some(&c) = a_iter.peek() {
1054 if !c.is_ascii_digit() {
1055 break;
1056 }
1057
1058 a_num_str.push(c);
1059 a_iter.next();
1060 }
1061
1062 while let Some(&c) = b_iter.peek() {
1063 if !c.is_ascii_digit() {
1064 break;
1065 }
1066
1067 b_num_str.push(c);
1068 b_iter.next();
1069 }
1070
1071 match a_num_str.len().cmp(&b_num_str.len()) {
1073 Ordering::Equal => {
1074 match a_num_str.cmp(&b_num_str) {
1076 Ordering::Equal => Ordering::Equal,
1077 ordering => ordering,
1078 }
1079 }
1080
1081 ordering => {
1083 if let (Ok(a_val), Ok(b_val)) = (a_num_str.parse::<u128>(), b_num_str.parse::<u128>()) {
1085 match a_val.cmp(&b_val) {
1086 Ordering::Equal => ordering, ord => ord,
1088 }
1089 } else {
1090 a_num_str.cmp(&b_num_str)
1092 }
1093 }
1094 }
1095}
1096
1097pub fn natural_sort(a: &str, b: &str) -> Ordering {
1119 let mut a_iter = a.chars().peekable();
1120 let mut b_iter = b.chars().peekable();
1121
1122 loop {
1123 match (a_iter.peek(), b_iter.peek()) {
1124 (None, None) => {
1125 return b.cmp(a);
1126 }
1127 (None, _) => return Ordering::Less,
1128 (_, None) => return Ordering::Greater,
1129 (Some(&a_char), Some(&b_char)) => {
1130 if a_char.is_ascii_digit() && b_char.is_ascii_digit() {
1131 match compare_numeric_segments(&mut a_iter, &mut b_iter) {
1132 Ordering::Equal => continue,
1133 ordering => return ordering,
1134 }
1135 } else {
1136 match a_char
1137 .to_ascii_lowercase()
1138 .cmp(&b_char.to_ascii_lowercase())
1139 {
1140 Ordering::Equal => {
1141 a_iter.next();
1142 b_iter.next();
1143 }
1144 ordering => return ordering,
1145 }
1146 }
1147 }
1148 }
1149 }
1150}
1151
1152fn natural_sort_no_tiebreak(a: &str, b: &str) -> Ordering {
1156 if a.eq_ignore_ascii_case(b) {
1157 Ordering::Equal
1158 } else {
1159 natural_sort(a, b)
1160 }
1161}
1162
1163fn stem_and_extension(filename: &str) -> (Option<&str>, Option<&str>) {
1164 if filename.is_empty() {
1165 return (None, None);
1166 }
1167
1168 match filename.rsplit_once('.') {
1169 None => (Some(filename), None),
1171
1172 Some((before, after)) => {
1174 if before.is_empty() {
1178 (Some(filename), None)
1179 } else {
1180 (Some(before), Some(after))
1182 }
1183 }
1184 }
1185}
1186
1187#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
1189pub enum SortOrder {
1190 #[default]
1193 Default,
1194 Upper,
1197 Lower,
1200 Unicode,
1203}
1204
1205#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
1207pub enum SortMode {
1208 #[default]
1210 DirectoriesFirst,
1211 Mixed,
1213 FilesFirst,
1215}
1216
1217fn case_group_key(name: &str, order: SortOrder) -> u8 {
1218 let first = match name.chars().next() {
1219 Some(c) => c,
1220 None => return 0,
1221 };
1222 match order {
1223 SortOrder::Upper => {
1224 if first.is_lowercase() {
1225 1
1226 } else {
1227 0
1228 }
1229 }
1230 SortOrder::Lower => {
1231 if first.is_uppercase() {
1232 1
1233 } else {
1234 0
1235 }
1236 }
1237 _ => 0,
1238 }
1239}
1240
1241fn compare_strings(a: &str, b: &str, order: SortOrder) -> Ordering {
1242 match order {
1243 SortOrder::Unicode => a.cmp(b),
1244 _ => natural_sort(a, b),
1245 }
1246}
1247
1248fn compare_strings_no_tiebreak(a: &str, b: &str, order: SortOrder) -> Ordering {
1249 match order {
1250 SortOrder::Unicode => a.cmp(b),
1251 _ => natural_sort_no_tiebreak(a, b),
1252 }
1253}
1254
1255pub fn compare_rel_paths(
1256 (path_a, a_is_file): (&RelPath, bool),
1257 (path_b, b_is_file): (&RelPath, bool),
1258) -> Ordering {
1259 compare_rel_paths_by(
1260 (path_a, a_is_file),
1261 (path_b, b_is_file),
1262 SortMode::DirectoriesFirst,
1263 SortOrder::Default,
1264 )
1265}
1266
1267pub fn compare_rel_paths_by(
1268 (path_a, a_is_file): (&RelPath, bool),
1269 (path_b, b_is_file): (&RelPath, bool),
1270 mode: SortMode,
1271 order: SortOrder,
1272) -> Ordering {
1273 let needs_final_tiebreak =
1274 mode != SortMode::DirectoriesFirst && !(std::ptr::eq(path_a, path_b) || path_a == path_b);
1275
1276 let mut components_a = path_a.components();
1277 let mut components_b = path_b.components();
1278
1279 loop {
1280 match (components_a.next(), components_b.next()) {
1281 (Some(component_a), Some(component_b)) => {
1282 let a_leaf_file = a_is_file && components_a.rest().is_empty();
1283 let b_leaf_file = b_is_file && components_b.rest().is_empty();
1284
1285 let file_dir_ordering = match mode {
1286 SortMode::DirectoriesFirst => a_leaf_file.cmp(&b_leaf_file),
1287 SortMode::FilesFirst => b_leaf_file.cmp(&a_leaf_file),
1288 SortMode::Mixed => Ordering::Equal,
1289 };
1290
1291 if !file_dir_ordering.is_eq() {
1292 return file_dir_ordering;
1293 }
1294
1295 let (a_stem, a_ext) = a_leaf_file
1296 .then(|| stem_and_extension(component_a))
1297 .unwrap_or_default();
1298 let (b_stem, b_ext) = b_leaf_file
1299 .then(|| stem_and_extension(component_b))
1300 .unwrap_or_default();
1301 let a_key = if a_leaf_file {
1302 a_stem
1303 } else {
1304 Some(component_a)
1305 };
1306 let b_key = if b_leaf_file {
1307 b_stem
1308 } else {
1309 Some(component_b)
1310 };
1311
1312 let ordering = match (a_key, b_key) {
1313 (Some(a), Some(b)) => {
1314 let name_cmp = case_group_key(a, order)
1315 .cmp(&case_group_key(b, order))
1316 .then_with(|| match mode {
1317 SortMode::DirectoriesFirst => compare_strings(a, b, order),
1318 _ => compare_strings_no_tiebreak(a, b, order),
1319 });
1320
1321 let name_cmp = if mode == SortMode::Mixed {
1322 name_cmp.then_with(|| match (a_leaf_file, b_leaf_file) {
1323 (true, false) if a.eq_ignore_ascii_case(b) => Ordering::Greater,
1324 (false, true) if a.eq_ignore_ascii_case(b) => Ordering::Less,
1325 _ => Ordering::Equal,
1326 })
1327 } else {
1328 name_cmp
1329 };
1330
1331 name_cmp.then_with(|| {
1332 if a_leaf_file && b_leaf_file {
1333 match order {
1334 SortOrder::Unicode => {
1335 a_ext.unwrap_or_default().cmp(b_ext.unwrap_or_default())
1336 }
1337 _ => {
1338 let a_ext_str = a_ext.unwrap_or_default().to_lowercase();
1339 let b_ext_str = b_ext.unwrap_or_default().to_lowercase();
1340 a_ext_str.cmp(&b_ext_str)
1341 }
1342 }
1343 } else {
1344 Ordering::Equal
1345 }
1346 })
1347 }
1348 (Some(_), None) => Ordering::Greater,
1349 (None, Some(_)) => Ordering::Less,
1350 (None, None) => Ordering::Equal,
1351 };
1352
1353 if !ordering.is_eq() {
1354 return ordering;
1355 }
1356 }
1357 (Some(_), None) => return Ordering::Greater,
1358 (None, Some(_)) => return Ordering::Less,
1359 (None, None) => {
1360 if needs_final_tiebreak {
1361 return compare_strings(path_a.as_unix_str(), path_b.as_unix_str(), order);
1362 }
1363 return Ordering::Equal;
1364 }
1365 }
1366 }
1367}
1368
1369pub fn compare_paths(
1370 (path_a, a_is_file): (&Path, bool),
1371 (path_b, b_is_file): (&Path, bool),
1372) -> Ordering {
1373 let mut components_a = path_a.components().peekable();
1374 let mut components_b = path_b.components().peekable();
1375
1376 loop {
1377 match (components_a.next(), components_b.next()) {
1378 (Some(component_a), Some(component_b)) => {
1379 let a_is_file = components_a.peek().is_none() && a_is_file;
1380 let b_is_file = components_b.peek().is_none() && b_is_file;
1381
1382 let ordering = a_is_file.cmp(&b_is_file).then_with(|| {
1383 let path_a = Path::new(component_a.as_os_str());
1384 let path_string_a = if a_is_file {
1385 path_a.file_stem()
1386 } else {
1387 path_a.file_name()
1388 }
1389 .map(|s| s.to_string_lossy());
1390
1391 let path_b = Path::new(component_b.as_os_str());
1392 let path_string_b = if b_is_file {
1393 path_b.file_stem()
1394 } else {
1395 path_b.file_name()
1396 }
1397 .map(|s| s.to_string_lossy());
1398
1399 let compare_components = match (path_string_a, path_string_b) {
1400 (Some(a), Some(b)) => natural_sort(&a, &b),
1401 (Some(_), None) => Ordering::Greater,
1402 (None, Some(_)) => Ordering::Less,
1403 (None, None) => Ordering::Equal,
1404 };
1405
1406 compare_components.then_with(|| {
1407 if a_is_file && b_is_file {
1408 let ext_a = path_a.extension().unwrap_or_default();
1409 let ext_b = path_b.extension().unwrap_or_default();
1410 ext_a.cmp(ext_b)
1411 } else {
1412 Ordering::Equal
1413 }
1414 })
1415 });
1416
1417 if !ordering.is_eq() {
1418 return ordering;
1419 }
1420 }
1421 (Some(_), None) => break Ordering::Greater,
1422 (None, Some(_)) => break Ordering::Less,
1423 (None, None) => break Ordering::Equal,
1424 }
1425 }
1426}
1427
1428#[derive(Debug, Clone, PartialEq, Eq)]
1429pub struct WslPath {
1430 pub distro: String,
1431
1432 pub path: std::ffi::OsString,
1436}
1437
1438impl WslPath {
1439 pub fn from_path<P: AsRef<Path>>(path: P) -> Option<WslPath> {
1440 if cfg!(not(target_os = "windows")) {
1441 return None;
1442 }
1443 use std::{
1444 ffi::OsString,
1445 path::{Component, Prefix},
1446 };
1447
1448 let mut components = path.as_ref().components();
1449 let Some(Component::Prefix(prefix)) = components.next() else {
1450 return None;
1451 };
1452 let (server, distro) = match prefix.kind() {
1453 Prefix::UNC(server, distro) => (server, distro),
1454 Prefix::VerbatimUNC(server, distro) => (server, distro),
1455 _ => return None,
1456 };
1457 let Some(Component::RootDir) = components.next() else {
1458 return None;
1459 };
1460
1461 let server_str = server.to_string_lossy();
1462 if server_str == "wsl.localhost" || server_str == "wsl$" {
1463 let mut result = OsString::from("");
1464 for c in components {
1465 use Component::*;
1466 match c {
1467 Prefix(p) => unreachable!("got {p:?}, but already stripped prefix"),
1468 RootDir => unreachable!("got root dir, but already stripped root"),
1469 CurDir => continue,
1470 ParentDir => result.push("/.."),
1471 Normal(s) => {
1472 result.push("/");
1473 result.push(s);
1474 }
1475 }
1476 }
1477 if result.is_empty() {
1478 result.push("/");
1479 }
1480 Some(WslPath {
1481 distro: distro.to_string_lossy().to_string(),
1482 path: result,
1483 })
1484 } else {
1485 None
1486 }
1487 }
1488}
1489
1490pub trait UrlExt {
1491 fn to_file_path_ext(&self, path_style: PathStyle) -> Result<PathBuf, ()>;
1495}
1496
1497impl UrlExt for url::Url {
1498 fn to_file_path_ext(&self, source_path_style: PathStyle) -> Result<PathBuf, ()> {
1500 if let Some(segments) = self.path_segments() {
1501 let host = match self.host() {
1502 None | Some(url::Host::Domain("localhost")) => None,
1503 Some(_) if source_path_style.is_windows() && self.scheme() == "file" => {
1504 self.host_str()
1505 }
1506 _ => return Err(()),
1507 };
1508
1509 let str_len = self.as_str().len();
1510 let estimated_capacity = if source_path_style.is_windows() {
1511 str_len.saturating_sub(self.scheme().len() + 1)
1513 } else {
1514 str_len.saturating_sub(self.scheme().len() + 3)
1516 };
1517 return match source_path_style {
1518 PathStyle::Posix => {
1519 file_url_segments_to_pathbuf_posix(estimated_capacity, host, segments)
1520 }
1521 PathStyle::Windows => {
1522 file_url_segments_to_pathbuf_windows(estimated_capacity, host, segments)
1523 }
1524 };
1525 }
1526
1527 fn file_url_segments_to_pathbuf_posix(
1528 estimated_capacity: usize,
1529 host: Option<&str>,
1530 segments: std::str::Split<'_, char>,
1531 ) -> Result<PathBuf, ()> {
1532 use percent_encoding::percent_decode;
1533
1534 if host.is_some() {
1535 return Err(());
1536 }
1537
1538 let mut bytes = Vec::new();
1539 bytes.try_reserve(estimated_capacity).map_err(|_| ())?;
1540
1541 for segment in segments {
1542 bytes.push(b'/');
1543 bytes.extend(percent_decode(segment.as_bytes()));
1544 }
1545
1546 if bytes.len() > 2
1548 && bytes[bytes.len() - 2].is_ascii_alphabetic()
1549 && matches!(bytes[bytes.len() - 1], b':' | b'|')
1550 {
1551 bytes.push(b'/');
1552 }
1553
1554 let path = String::from_utf8(bytes).map_err(|_| ())?;
1555 debug_assert!(
1556 PathStyle::Posix.is_absolute(&path),
1557 "to_file_path() failed to produce an absolute Path"
1558 );
1559
1560 Ok(PathBuf::from(path))
1561 }
1562
1563 fn file_url_segments_to_pathbuf_windows(
1564 estimated_capacity: usize,
1565 host: Option<&str>,
1566 mut segments: std::str::Split<'_, char>,
1567 ) -> Result<PathBuf, ()> {
1568 use percent_encoding::percent_decode_str;
1569 let mut string = String::new();
1570 string.try_reserve(estimated_capacity).map_err(|_| ())?;
1571 if let Some(host) = host {
1572 string.push_str(r"\\");
1573 string.push_str(host);
1574 } else {
1575 let first = segments.next().ok_or(())?;
1576
1577 match first.len() {
1578 2 => {
1579 if !first.starts_with(|c| char::is_ascii_alphabetic(&c))
1580 || first.as_bytes()[1] != b':'
1581 {
1582 return Err(());
1583 }
1584
1585 string.push_str(first);
1586 }
1587
1588 4 => {
1589 if !first.starts_with(|c| char::is_ascii_alphabetic(&c)) {
1590 return Err(());
1591 }
1592 let bytes = first.as_bytes();
1593 if bytes[1] != b'%'
1594 || bytes[2] != b'3'
1595 || (bytes[3] != b'a' && bytes[3] != b'A')
1596 {
1597 return Err(());
1598 }
1599
1600 string.push_str(&first[0..1]);
1601 string.push(':');
1602 }
1603
1604 _ => return Err(()),
1605 }
1606 };
1607
1608 for segment in segments {
1609 string.push('\\');
1610
1611 match percent_decode_str(segment).decode_utf8() {
1613 Ok(s) => string.push_str(&s),
1614 Err(..) => return Err(()),
1615 }
1616 }
1617 if cfg!(test) {
1619 debug_assert!(
1620 string.len() <= estimated_capacity,
1621 "len: {}, capacity: {}",
1622 string.len(),
1623 estimated_capacity
1624 );
1625 }
1626 debug_assert!(
1627 PathStyle::Windows.is_absolute(&string),
1628 "to_file_path() failed to produce an absolute Path"
1629 );
1630 let path = PathBuf::from(string);
1631 Ok(path)
1632 }
1633 Err(())
1634 }
1635}
1636
1637#[cfg(test)]
1638mod tests {
1639 use crate::rel_path::rel_path;
1640
1641 use super::*;
1642 use util_macros::perf;
1643
1644 #[test]
1645 fn test_join_path_uses_path_style_separator() {
1646 let posix_path = PathStyle::Posix
1647 .join_path(Path::new("/home/user/dev"), "worktrees")
1648 .unwrap();
1649 let windows_path = PathStyle::Windows
1650 .join_path(Path::new("C:\\Users\\user\\dev"), "worktrees")
1651 .unwrap();
1652
1653 assert_eq!(posix_path, PathBuf::from("/home/user/dev/worktrees"));
1654 assert_eq!(
1655 windows_path.to_string_lossy(),
1656 "C:\\Users\\user\\dev\\worktrees"
1657 );
1658 }
1659
1660 #[test]
1661 fn test_normalize_uses_path_style_separator() {
1662 assert_eq!(
1663 PathStyle::Posix.normalize("/home/user/dev/../worktrees/./zed"),
1664 "/home/user/worktrees/zed"
1665 );
1666 assert_eq!(
1667 PathStyle::Windows.normalize("C:\\Users\\user\\dev\\worktrees"),
1668 "C:\\Users\\user\\dev\\worktrees"
1669 );
1670 }
1671
1672 fn rel_path_entry(path: &'static str, is_file: bool) -> (&'static RelPath, bool) {
1673 (RelPath::unix(path).unwrap(), is_file)
1674 }
1675
1676 fn sorted_rel_paths(
1677 mut paths: Vec<(&'static RelPath, bool)>,
1678 mode: SortMode,
1679 order: SortOrder,
1680 ) -> Vec<(&'static RelPath, bool)> {
1681 paths.sort_by(|&a, &b| compare_rel_paths_by(a, b, mode, order));
1682 paths
1683 }
1684
1685 #[perf]
1686 fn compare_paths_with_dots() {
1687 let mut paths = vec![
1688 (Path::new("test_dirs"), false),
1689 (Path::new("test_dirs/1.46"), false),
1690 (Path::new("test_dirs/1.46/bar_1"), true),
1691 (Path::new("test_dirs/1.46/bar_2"), true),
1692 (Path::new("test_dirs/1.45"), false),
1693 (Path::new("test_dirs/1.45/foo_2"), true),
1694 (Path::new("test_dirs/1.45/foo_1"), true),
1695 ];
1696 paths.sort_by(|&a, &b| compare_paths(a, b));
1697 assert_eq!(
1698 paths,
1699 vec![
1700 (Path::new("test_dirs"), false),
1701 (Path::new("test_dirs/1.45"), false),
1702 (Path::new("test_dirs/1.45/foo_1"), true),
1703 (Path::new("test_dirs/1.45/foo_2"), true),
1704 (Path::new("test_dirs/1.46"), false),
1705 (Path::new("test_dirs/1.46/bar_1"), true),
1706 (Path::new("test_dirs/1.46/bar_2"), true),
1707 ]
1708 );
1709 let mut paths = vec![
1710 (Path::new("root1/one.txt"), true),
1711 (Path::new("root1/one.two.txt"), true),
1712 ];
1713 paths.sort_by(|&a, &b| compare_paths(a, b));
1714 assert_eq!(
1715 paths,
1716 vec![
1717 (Path::new("root1/one.txt"), true),
1718 (Path::new("root1/one.two.txt"), true),
1719 ]
1720 );
1721 }
1722
1723 #[perf]
1724 fn compare_paths_with_same_name_different_extensions() {
1725 let mut paths = vec![
1726 (Path::new("test_dirs/file.rs"), true),
1727 (Path::new("test_dirs/file.txt"), true),
1728 (Path::new("test_dirs/file.md"), true),
1729 (Path::new("test_dirs/file"), true),
1730 (Path::new("test_dirs/file.a"), true),
1731 ];
1732 paths.sort_by(|&a, &b| compare_paths(a, b));
1733 assert_eq!(
1734 paths,
1735 vec![
1736 (Path::new("test_dirs/file"), true),
1737 (Path::new("test_dirs/file.a"), true),
1738 (Path::new("test_dirs/file.md"), true),
1739 (Path::new("test_dirs/file.rs"), true),
1740 (Path::new("test_dirs/file.txt"), true),
1741 ]
1742 );
1743 }
1744
1745 #[perf]
1746 fn compare_paths_case_semi_sensitive() {
1747 let mut paths = vec![
1748 (Path::new("test_DIRS"), false),
1749 (Path::new("test_DIRS/foo_1"), true),
1750 (Path::new("test_DIRS/foo_2"), true),
1751 (Path::new("test_DIRS/bar"), true),
1752 (Path::new("test_DIRS/BAR"), true),
1753 (Path::new("test_dirs"), false),
1754 (Path::new("test_dirs/foo_1"), true),
1755 (Path::new("test_dirs/foo_2"), true),
1756 (Path::new("test_dirs/bar"), true),
1757 (Path::new("test_dirs/BAR"), true),
1758 ];
1759 paths.sort_by(|&a, &b| compare_paths(a, b));
1760 assert_eq!(
1761 paths,
1762 vec![
1763 (Path::new("test_dirs"), false),
1764 (Path::new("test_dirs/bar"), true),
1765 (Path::new("test_dirs/BAR"), true),
1766 (Path::new("test_dirs/foo_1"), true),
1767 (Path::new("test_dirs/foo_2"), true),
1768 (Path::new("test_DIRS"), false),
1769 (Path::new("test_DIRS/bar"), true),
1770 (Path::new("test_DIRS/BAR"), true),
1771 (Path::new("test_DIRS/foo_1"), true),
1772 (Path::new("test_DIRS/foo_2"), true),
1773 ]
1774 );
1775 }
1776
1777 #[perf]
1778 fn compare_paths_mixed_case_numeric_ordering() {
1779 let mut entries = [
1780 (Path::new(".config"), false),
1781 (Path::new("Dir1"), false),
1782 (Path::new("dir01"), false),
1783 (Path::new("dir2"), false),
1784 (Path::new("Dir02"), false),
1785 (Path::new("dir10"), false),
1786 (Path::new("Dir10"), false),
1787 ];
1788
1789 entries.sort_by(|&a, &b| compare_paths(a, b));
1790
1791 let ordered: Vec<&str> = entries
1792 .iter()
1793 .map(|(path, _)| path.to_str().unwrap())
1794 .collect();
1795
1796 assert_eq!(
1797 ordered,
1798 vec![
1799 ".config", "Dir1", "dir01", "dir2", "Dir02", "dir10", "Dir10"
1800 ]
1801 );
1802 }
1803
1804 #[perf]
1805 fn compare_rel_paths_mixed_case_insensitive() {
1806 let mut paths = vec![
1808 (RelPath::unix("zebra.txt").unwrap(), true),
1809 (RelPath::unix("Apple").unwrap(), false),
1810 (RelPath::unix("banana.rs").unwrap(), true),
1811 (RelPath::unix("Carrot").unwrap(), false),
1812 (RelPath::unix("aardvark.txt").unwrap(), true),
1813 ];
1814 paths.sort_by(|&a, &b| compare_rel_paths_by(a, b, SortMode::Mixed, SortOrder::Default));
1815 assert_eq!(
1817 paths,
1818 vec![
1819 (RelPath::unix("aardvark.txt").unwrap(), true),
1820 (RelPath::unix("Apple").unwrap(), false),
1821 (RelPath::unix("banana.rs").unwrap(), true),
1822 (RelPath::unix("Carrot").unwrap(), false),
1823 (RelPath::unix("zebra.txt").unwrap(), true),
1824 ]
1825 );
1826 }
1827
1828 #[perf]
1829 fn compare_rel_paths_files_first_basic() {
1830 let mut paths = vec![
1832 (RelPath::unix("zebra.txt").unwrap(), true),
1833 (RelPath::unix("Apple").unwrap(), false),
1834 (RelPath::unix("banana.rs").unwrap(), true),
1835 (RelPath::unix("Carrot").unwrap(), false),
1836 (RelPath::unix("aardvark.txt").unwrap(), true),
1837 ];
1838 paths
1839 .sort_by(|&a, &b| compare_rel_paths_by(a, b, SortMode::FilesFirst, SortOrder::Default));
1840 assert_eq!(
1842 paths,
1843 vec![
1844 (RelPath::unix("aardvark.txt").unwrap(), true),
1845 (RelPath::unix("banana.rs").unwrap(), true),
1846 (RelPath::unix("zebra.txt").unwrap(), true),
1847 (RelPath::unix("Apple").unwrap(), false),
1848 (RelPath::unix("Carrot").unwrap(), false),
1849 ]
1850 );
1851 }
1852
1853 #[perf]
1854 fn compare_rel_paths_files_first_case_insensitive() {
1855 let mut paths = vec![
1857 (RelPath::unix("Zebra.txt").unwrap(), true),
1858 (RelPath::unix("apple").unwrap(), false),
1859 (RelPath::unix("Banana.rs").unwrap(), true),
1860 (RelPath::unix("carrot").unwrap(), false),
1861 (RelPath::unix("Aardvark.txt").unwrap(), true),
1862 ];
1863 paths
1864 .sort_by(|&a, &b| compare_rel_paths_by(a, b, SortMode::FilesFirst, SortOrder::Default));
1865 assert_eq!(
1866 paths,
1867 vec![
1868 (RelPath::unix("Aardvark.txt").unwrap(), true),
1869 (RelPath::unix("Banana.rs").unwrap(), true),
1870 (RelPath::unix("Zebra.txt").unwrap(), true),
1871 (RelPath::unix("apple").unwrap(), false),
1872 (RelPath::unix("carrot").unwrap(), false),
1873 ]
1874 );
1875 }
1876
1877 #[perf]
1878 fn compare_rel_paths_files_first_numeric() {
1879 let mut paths = vec![
1881 (RelPath::unix("file10.txt").unwrap(), true),
1882 (RelPath::unix("dir2").unwrap(), false),
1883 (RelPath::unix("file2.txt").unwrap(), true),
1884 (RelPath::unix("dir10").unwrap(), false),
1885 (RelPath::unix("file1.txt").unwrap(), true),
1886 ];
1887 paths
1888 .sort_by(|&a, &b| compare_rel_paths_by(a, b, SortMode::FilesFirst, SortOrder::Default));
1889 assert_eq!(
1890 paths,
1891 vec![
1892 (RelPath::unix("file1.txt").unwrap(), true),
1893 (RelPath::unix("file2.txt").unwrap(), true),
1894 (RelPath::unix("file10.txt").unwrap(), true),
1895 (RelPath::unix("dir2").unwrap(), false),
1896 (RelPath::unix("dir10").unwrap(), false),
1897 ]
1898 );
1899 }
1900
1901 #[perf]
1902 fn compare_rel_paths_mixed_case() {
1903 let mut paths = vec![
1905 (RelPath::unix("README.md").unwrap(), true),
1906 (RelPath::unix("readme.txt").unwrap(), true),
1907 (RelPath::unix("ReadMe.rs").unwrap(), true),
1908 ];
1909 paths.sort_by(|&a, &b| compare_rel_paths_by(a, b, SortMode::Mixed, SortOrder::Default));
1910 assert_eq!(
1912 paths,
1913 vec![
1914 (RelPath::unix("README.md").unwrap(), true),
1915 (RelPath::unix("ReadMe.rs").unwrap(), true),
1916 (RelPath::unix("readme.txt").unwrap(), true),
1917 ]
1918 );
1919 }
1920
1921 #[perf]
1922 fn compare_rel_paths_mixed_files_and_dirs() {
1923 let mut paths = vec![
1925 (RelPath::unix("file2.txt").unwrap(), true),
1926 (RelPath::unix("Dir1").unwrap(), false),
1927 (RelPath::unix("file1.txt").unwrap(), true),
1928 (RelPath::unix("dir2").unwrap(), false),
1929 ];
1930 paths.sort_by(|&a, &b| compare_rel_paths_by(a, b, SortMode::Mixed, SortOrder::Default));
1931 assert_eq!(
1933 paths,
1934 vec![
1935 (RelPath::unix("Dir1").unwrap(), false),
1936 (RelPath::unix("dir2").unwrap(), false),
1937 (RelPath::unix("file1.txt").unwrap(), true),
1938 (RelPath::unix("file2.txt").unwrap(), true),
1939 ]
1940 );
1941 }
1942
1943 #[perf]
1944 fn compare_rel_paths_mixed_same_name_different_case_file_and_dir() {
1945 let mut paths = vec![
1946 (RelPath::unix("Hello.txt").unwrap(), true),
1947 (RelPath::unix("hello").unwrap(), false),
1948 ];
1949 paths.sort_by(|&a, &b| compare_rel_paths_by(a, b, SortMode::Mixed, SortOrder::Default));
1950 assert_eq!(
1951 paths,
1952 vec![
1953 (RelPath::unix("hello").unwrap(), false),
1954 (RelPath::unix("Hello.txt").unwrap(), true),
1955 ]
1956 );
1957
1958 let mut paths = vec![
1959 (RelPath::unix("hello").unwrap(), false),
1960 (RelPath::unix("Hello.txt").unwrap(), true),
1961 ];
1962 paths.sort_by(|&a, &b| compare_rel_paths_by(a, b, SortMode::Mixed, SortOrder::Default));
1963 assert_eq!(
1964 paths,
1965 vec![
1966 (RelPath::unix("hello").unwrap(), false),
1967 (RelPath::unix("Hello.txt").unwrap(), true),
1968 ]
1969 );
1970 }
1971
1972 #[perf]
1973 fn compare_rel_paths_mixed_with_nested_paths() {
1974 let mut paths = vec![
1976 (RelPath::unix("src/main.rs").unwrap(), true),
1977 (RelPath::unix("Cargo.toml").unwrap(), true),
1978 (RelPath::unix("src").unwrap(), false),
1979 (RelPath::unix("target").unwrap(), false),
1980 ];
1981 paths.sort_by(|&a, &b| compare_rel_paths_by(a, b, SortMode::Mixed, SortOrder::Default));
1982 assert_eq!(
1983 paths,
1984 vec![
1985 (RelPath::unix("Cargo.toml").unwrap(), true),
1986 (RelPath::unix("src").unwrap(), false),
1987 (RelPath::unix("src/main.rs").unwrap(), true),
1988 (RelPath::unix("target").unwrap(), false),
1989 ]
1990 );
1991 }
1992
1993 #[perf]
1994 fn compare_rel_paths_files_first_with_nested() {
1995 let mut paths = vec![
1997 (RelPath::unix("src/lib.rs").unwrap(), true),
1998 (RelPath::unix("README.md").unwrap(), true),
1999 (RelPath::unix("src").unwrap(), false),
2000 (RelPath::unix("tests").unwrap(), false),
2001 ];
2002 paths
2003 .sort_by(|&a, &b| compare_rel_paths_by(a, b, SortMode::FilesFirst, SortOrder::Default));
2004 assert_eq!(
2005 paths,
2006 vec![
2007 (RelPath::unix("README.md").unwrap(), true),
2008 (RelPath::unix("src").unwrap(), false),
2009 (RelPath::unix("src/lib.rs").unwrap(), true),
2010 (RelPath::unix("tests").unwrap(), false),
2011 ]
2012 );
2013 }
2014
2015 #[perf]
2016 fn compare_rel_paths_mixed_dotfiles() {
2017 let mut paths = vec![
2019 (RelPath::unix(".gitignore").unwrap(), true),
2020 (RelPath::unix("README.md").unwrap(), true),
2021 (RelPath::unix(".github").unwrap(), false),
2022 (RelPath::unix("src").unwrap(), false),
2023 ];
2024 paths.sort_by(|&a, &b| compare_rel_paths_by(a, b, SortMode::Mixed, SortOrder::Default));
2025 assert_eq!(
2026 paths,
2027 vec![
2028 (RelPath::unix(".github").unwrap(), false),
2029 (RelPath::unix(".gitignore").unwrap(), true),
2030 (RelPath::unix("README.md").unwrap(), true),
2031 (RelPath::unix("src").unwrap(), false),
2032 ]
2033 );
2034 }
2035
2036 #[perf]
2037 fn compare_rel_paths_files_first_dotfiles() {
2038 let mut paths = vec![
2040 (RelPath::unix(".gitignore").unwrap(), true),
2041 (RelPath::unix("README.md").unwrap(), true),
2042 (RelPath::unix(".github").unwrap(), false),
2043 (RelPath::unix("src").unwrap(), false),
2044 ];
2045 paths
2046 .sort_by(|&a, &b| compare_rel_paths_by(a, b, SortMode::FilesFirst, SortOrder::Default));
2047 assert_eq!(
2048 paths,
2049 vec![
2050 (RelPath::unix(".gitignore").unwrap(), true),
2051 (RelPath::unix("README.md").unwrap(), true),
2052 (RelPath::unix(".github").unwrap(), false),
2053 (RelPath::unix("src").unwrap(), false),
2054 ]
2055 );
2056 }
2057
2058 #[perf]
2059 fn compare_rel_paths_mixed_same_stem_different_extension() {
2060 let mut paths = vec![
2062 (RelPath::unix("file.rs").unwrap(), true),
2063 (RelPath::unix("file.md").unwrap(), true),
2064 (RelPath::unix("file.txt").unwrap(), true),
2065 ];
2066 paths.sort_by(|&a, &b| compare_rel_paths_by(a, b, SortMode::Mixed, SortOrder::Default));
2067 assert_eq!(
2068 paths,
2069 vec![
2070 (RelPath::unix("file.md").unwrap(), true),
2071 (RelPath::unix("file.rs").unwrap(), true),
2072 (RelPath::unix("file.txt").unwrap(), true),
2073 ]
2074 );
2075 }
2076
2077 #[perf]
2078 fn compare_rel_paths_files_first_same_stem() {
2079 let mut paths = vec![
2081 (RelPath::unix("main.rs").unwrap(), true),
2082 (RelPath::unix("main.c").unwrap(), true),
2083 (RelPath::unix("main").unwrap(), false),
2084 ];
2085 paths
2086 .sort_by(|&a, &b| compare_rel_paths_by(a, b, SortMode::FilesFirst, SortOrder::Default));
2087 assert_eq!(
2088 paths,
2089 vec![
2090 (RelPath::unix("main.c").unwrap(), true),
2091 (RelPath::unix("main.rs").unwrap(), true),
2092 (RelPath::unix("main").unwrap(), false),
2093 ]
2094 );
2095 }
2096
2097 #[perf]
2098 fn compare_rel_paths_mixed_deep_nesting() {
2099 let mut paths = vec![
2101 (RelPath::unix("a/b/c.txt").unwrap(), true),
2102 (RelPath::unix("A/B.txt").unwrap(), true),
2103 (RelPath::unix("a.txt").unwrap(), true),
2104 (RelPath::unix("A.txt").unwrap(), true),
2105 ];
2106 paths.sort_by(|&a, &b| compare_rel_paths_by(a, b, SortMode::Mixed, SortOrder::Default));
2107 assert_eq!(
2108 paths,
2109 vec![
2110 (RelPath::unix("a/b/c.txt").unwrap(), true),
2111 (RelPath::unix("A/B.txt").unwrap(), true),
2112 (RelPath::unix("a.txt").unwrap(), true),
2113 (RelPath::unix("A.txt").unwrap(), true),
2114 ]
2115 );
2116 }
2117
2118 #[perf]
2119 fn compare_rel_paths_upper() {
2120 let directories_only_paths = vec![
2121 rel_path_entry("mixedCase", false),
2122 rel_path_entry("Zebra", false),
2123 rel_path_entry("banana", false),
2124 rel_path_entry("ALLCAPS", false),
2125 rel_path_entry("Apple", false),
2126 rel_path_entry("dog", false),
2127 rel_path_entry(".hidden", false),
2128 rel_path_entry("Carrot", false),
2129 ];
2130 assert_eq!(
2131 sorted_rel_paths(
2132 directories_only_paths,
2133 SortMode::DirectoriesFirst,
2134 SortOrder::Upper,
2135 ),
2136 vec![
2137 rel_path_entry(".hidden", false),
2138 rel_path_entry("ALLCAPS", false),
2139 rel_path_entry("Apple", false),
2140 rel_path_entry("Carrot", false),
2141 rel_path_entry("Zebra", false),
2142 rel_path_entry("banana", false),
2143 rel_path_entry("dog", false),
2144 rel_path_entry("mixedCase", false),
2145 ]
2146 );
2147
2148 let file_and_directory_paths = vec![
2149 rel_path_entry("banana", false),
2150 rel_path_entry("Apple.txt", true),
2151 rel_path_entry("dog.md", true),
2152 rel_path_entry("ALLCAPS", false),
2153 rel_path_entry("file1.txt", true),
2154 rel_path_entry("File2.txt", true),
2155 rel_path_entry(".hidden", false),
2156 ];
2157 assert_eq!(
2158 sorted_rel_paths(
2159 file_and_directory_paths.clone(),
2160 SortMode::DirectoriesFirst,
2161 SortOrder::Upper,
2162 ),
2163 vec![
2164 rel_path_entry(".hidden", false),
2165 rel_path_entry("ALLCAPS", false),
2166 rel_path_entry("banana", false),
2167 rel_path_entry("Apple.txt", true),
2168 rel_path_entry("File2.txt", true),
2169 rel_path_entry("dog.md", true),
2170 rel_path_entry("file1.txt", true),
2171 ]
2172 );
2173 assert_eq!(
2174 sorted_rel_paths(
2175 file_and_directory_paths.clone(),
2176 SortMode::Mixed,
2177 SortOrder::Upper,
2178 ),
2179 vec![
2180 rel_path_entry(".hidden", false),
2181 rel_path_entry("ALLCAPS", false),
2182 rel_path_entry("Apple.txt", true),
2183 rel_path_entry("File2.txt", true),
2184 rel_path_entry("banana", false),
2185 rel_path_entry("dog.md", true),
2186 rel_path_entry("file1.txt", true),
2187 ]
2188 );
2189 assert_eq!(
2190 sorted_rel_paths(
2191 file_and_directory_paths,
2192 SortMode::FilesFirst,
2193 SortOrder::Upper,
2194 ),
2195 vec![
2196 rel_path_entry("Apple.txt", true),
2197 rel_path_entry("File2.txt", true),
2198 rel_path_entry("dog.md", true),
2199 rel_path_entry("file1.txt", true),
2200 rel_path_entry(".hidden", false),
2201 rel_path_entry("ALLCAPS", false),
2202 rel_path_entry("banana", false),
2203 ]
2204 );
2205
2206 let natural_sort_paths = vec![
2207 rel_path_entry("file10.txt", true),
2208 rel_path_entry("file1.txt", true),
2209 rel_path_entry("file20.txt", true),
2210 rel_path_entry("file2.txt", true),
2211 ];
2212 assert_eq!(
2213 sorted_rel_paths(natural_sort_paths, SortMode::Mixed, SortOrder::Upper,),
2214 vec![
2215 rel_path_entry("file1.txt", true),
2216 rel_path_entry("file2.txt", true),
2217 rel_path_entry("file10.txt", true),
2218 rel_path_entry("file20.txt", true),
2219 ]
2220 );
2221
2222 let accented_paths = vec![
2223 rel_path_entry("\u{00C9}something.txt", true),
2224 rel_path_entry("zebra.txt", true),
2225 rel_path_entry("Apple.txt", true),
2226 ];
2227 assert_eq!(
2228 sorted_rel_paths(accented_paths, SortMode::Mixed, SortOrder::Upper),
2229 vec![
2230 rel_path_entry("Apple.txt", true),
2231 rel_path_entry("\u{00C9}something.txt", true),
2232 rel_path_entry("zebra.txt", true),
2233 ]
2234 );
2235 }
2236
2237 #[perf]
2238 fn compare_rel_paths_lower() {
2239 let directories_only_paths = vec![
2240 rel_path_entry("mixedCase", false),
2241 rel_path_entry("Zebra", false),
2242 rel_path_entry("banana", false),
2243 rel_path_entry("ALLCAPS", false),
2244 rel_path_entry("Apple", false),
2245 rel_path_entry("dog", false),
2246 rel_path_entry(".hidden", false),
2247 rel_path_entry("Carrot", false),
2248 ];
2249 assert_eq!(
2250 sorted_rel_paths(
2251 directories_only_paths,
2252 SortMode::DirectoriesFirst,
2253 SortOrder::Lower,
2254 ),
2255 vec![
2256 rel_path_entry(".hidden", false),
2257 rel_path_entry("banana", false),
2258 rel_path_entry("dog", false),
2259 rel_path_entry("mixedCase", false),
2260 rel_path_entry("ALLCAPS", false),
2261 rel_path_entry("Apple", false),
2262 rel_path_entry("Carrot", false),
2263 rel_path_entry("Zebra", false),
2264 ]
2265 );
2266
2267 let file_and_directory_paths = vec![
2268 rel_path_entry("banana", false),
2269 rel_path_entry("Apple.txt", true),
2270 rel_path_entry("dog.md", true),
2271 rel_path_entry("ALLCAPS", false),
2272 rel_path_entry("file1.txt", true),
2273 rel_path_entry("File2.txt", true),
2274 rel_path_entry(".hidden", false),
2275 ];
2276 assert_eq!(
2277 sorted_rel_paths(
2278 file_and_directory_paths.clone(),
2279 SortMode::DirectoriesFirst,
2280 SortOrder::Lower,
2281 ),
2282 vec![
2283 rel_path_entry(".hidden", false),
2284 rel_path_entry("banana", false),
2285 rel_path_entry("ALLCAPS", false),
2286 rel_path_entry("dog.md", true),
2287 rel_path_entry("file1.txt", true),
2288 rel_path_entry("Apple.txt", true),
2289 rel_path_entry("File2.txt", true),
2290 ]
2291 );
2292 assert_eq!(
2293 sorted_rel_paths(
2294 file_and_directory_paths.clone(),
2295 SortMode::Mixed,
2296 SortOrder::Lower,
2297 ),
2298 vec![
2299 rel_path_entry(".hidden", false),
2300 rel_path_entry("banana", false),
2301 rel_path_entry("dog.md", true),
2302 rel_path_entry("file1.txt", true),
2303 rel_path_entry("ALLCAPS", false),
2304 rel_path_entry("Apple.txt", true),
2305 rel_path_entry("File2.txt", true),
2306 ]
2307 );
2308 assert_eq!(
2309 sorted_rel_paths(
2310 file_and_directory_paths,
2311 SortMode::FilesFirst,
2312 SortOrder::Lower,
2313 ),
2314 vec![
2315 rel_path_entry("dog.md", true),
2316 rel_path_entry("file1.txt", true),
2317 rel_path_entry("Apple.txt", true),
2318 rel_path_entry("File2.txt", true),
2319 rel_path_entry(".hidden", false),
2320 rel_path_entry("banana", false),
2321 rel_path_entry("ALLCAPS", false),
2322 ]
2323 );
2324 }
2325
2326 #[perf]
2327 fn compare_rel_paths_unicode() {
2328 let directories_only_paths = vec![
2329 rel_path_entry("mixedCase", false),
2330 rel_path_entry("Zebra", false),
2331 rel_path_entry("banana", false),
2332 rel_path_entry("ALLCAPS", false),
2333 rel_path_entry("Apple", false),
2334 rel_path_entry("dog", false),
2335 rel_path_entry(".hidden", false),
2336 rel_path_entry("Carrot", false),
2337 ];
2338 assert_eq!(
2339 sorted_rel_paths(
2340 directories_only_paths,
2341 SortMode::DirectoriesFirst,
2342 SortOrder::Unicode,
2343 ),
2344 vec![
2345 rel_path_entry(".hidden", false),
2346 rel_path_entry("ALLCAPS", false),
2347 rel_path_entry("Apple", false),
2348 rel_path_entry("Carrot", false),
2349 rel_path_entry("Zebra", false),
2350 rel_path_entry("banana", false),
2351 rel_path_entry("dog", false),
2352 rel_path_entry("mixedCase", false),
2353 ]
2354 );
2355
2356 let file_and_directory_paths = vec![
2357 rel_path_entry("banana", false),
2358 rel_path_entry("Apple.txt", true),
2359 rel_path_entry("dog.md", true),
2360 rel_path_entry("ALLCAPS", false),
2361 rel_path_entry("file1.txt", true),
2362 rel_path_entry("File2.txt", true),
2363 rel_path_entry(".hidden", false),
2364 ];
2365 assert_eq!(
2366 sorted_rel_paths(
2367 file_and_directory_paths.clone(),
2368 SortMode::DirectoriesFirst,
2369 SortOrder::Unicode,
2370 ),
2371 vec![
2372 rel_path_entry(".hidden", false),
2373 rel_path_entry("ALLCAPS", false),
2374 rel_path_entry("banana", false),
2375 rel_path_entry("Apple.txt", true),
2376 rel_path_entry("File2.txt", true),
2377 rel_path_entry("dog.md", true),
2378 rel_path_entry("file1.txt", true),
2379 ]
2380 );
2381 assert_eq!(
2382 sorted_rel_paths(
2383 file_and_directory_paths.clone(),
2384 SortMode::Mixed,
2385 SortOrder::Unicode,
2386 ),
2387 vec![
2388 rel_path_entry(".hidden", false),
2389 rel_path_entry("ALLCAPS", false),
2390 rel_path_entry("Apple.txt", true),
2391 rel_path_entry("File2.txt", true),
2392 rel_path_entry("banana", false),
2393 rel_path_entry("dog.md", true),
2394 rel_path_entry("file1.txt", true),
2395 ]
2396 );
2397 assert_eq!(
2398 sorted_rel_paths(
2399 file_and_directory_paths,
2400 SortMode::FilesFirst,
2401 SortOrder::Unicode,
2402 ),
2403 vec![
2404 rel_path_entry("Apple.txt", true),
2405 rel_path_entry("File2.txt", true),
2406 rel_path_entry("dog.md", true),
2407 rel_path_entry("file1.txt", true),
2408 rel_path_entry(".hidden", false),
2409 rel_path_entry("ALLCAPS", false),
2410 rel_path_entry("banana", false),
2411 ]
2412 );
2413
2414 let numeric_paths = vec![
2415 rel_path_entry("file10.txt", true),
2416 rel_path_entry("file1.txt", true),
2417 rel_path_entry("file2.txt", true),
2418 rel_path_entry("file20.txt", true),
2419 ];
2420 assert_eq!(
2421 sorted_rel_paths(numeric_paths, SortMode::Mixed, SortOrder::Unicode,),
2422 vec![
2423 rel_path_entry("file1.txt", true),
2424 rel_path_entry("file10.txt", true),
2425 rel_path_entry("file2.txt", true),
2426 rel_path_entry("file20.txt", true),
2427 ]
2428 );
2429
2430 let accented_paths = vec![
2431 rel_path_entry("\u{00C9}something.txt", true),
2432 rel_path_entry("zebra.txt", true),
2433 rel_path_entry("Apple.txt", true),
2434 ];
2435 assert_eq!(
2436 sorted_rel_paths(accented_paths, SortMode::Mixed, SortOrder::Unicode),
2437 vec![
2438 rel_path_entry("Apple.txt", true),
2439 rel_path_entry("zebra.txt", true),
2440 rel_path_entry("\u{00C9}something.txt", true),
2441 ]
2442 );
2443 }
2444
2445 #[perf]
2446 fn path_with_position_parse_posix_path() {
2447 assert_eq!(
2450 PathWithPosition::parse_str("test_file"),
2451 PathWithPosition {
2452 path: PathBuf::from("test_file"),
2453 row: None,
2454 column: None
2455 }
2456 );
2457
2458 assert_eq!(
2459 PathWithPosition::parse_str("a:bc:.zip:1"),
2460 PathWithPosition {
2461 path: PathBuf::from("a:bc:.zip"),
2462 row: Some(1),
2463 column: None
2464 }
2465 );
2466
2467 assert_eq!(
2468 PathWithPosition::parse_str("one.second.zip:1"),
2469 PathWithPosition {
2470 path: PathBuf::from("one.second.zip"),
2471 row: Some(1),
2472 column: None
2473 }
2474 );
2475
2476 assert_eq!(
2478 PathWithPosition::parse_str("test_file:10:1:"),
2479 PathWithPosition {
2480 path: PathBuf::from("test_file"),
2481 row: Some(10),
2482 column: Some(1)
2483 }
2484 );
2485
2486 assert_eq!(
2487 PathWithPosition::parse_str("test_file.rs:"),
2488 PathWithPosition {
2489 path: PathBuf::from("test_file.rs"),
2490 row: None,
2491 column: None
2492 }
2493 );
2494
2495 assert_eq!(
2496 PathWithPosition::parse_str("test_file.rs:1:"),
2497 PathWithPosition {
2498 path: PathBuf::from("test_file.rs"),
2499 row: Some(1),
2500 column: None
2501 }
2502 );
2503
2504 assert_eq!(
2505 PathWithPosition::parse_str("ab\ncd"),
2506 PathWithPosition {
2507 path: PathBuf::from("ab\ncd"),
2508 row: None,
2509 column: None
2510 }
2511 );
2512
2513 assert_eq!(
2514 PathWithPosition::parse_str("👋\nab"),
2515 PathWithPosition {
2516 path: PathBuf::from("👋\nab"),
2517 row: None,
2518 column: None
2519 }
2520 );
2521
2522 assert_eq!(
2523 PathWithPosition::parse_str("Types.hs:(617,9)-(670,28):"),
2524 PathWithPosition {
2525 path: PathBuf::from("Types.hs"),
2526 row: Some(617),
2527 column: Some(9),
2528 }
2529 );
2530
2531 assert_eq!(
2532 PathWithPosition::parse_str("main (1).log"),
2533 PathWithPosition {
2534 path: PathBuf::from("main (1).log"),
2535 row: None,
2536 column: None
2537 }
2538 );
2539 }
2540
2541 #[perf]
2542 #[cfg(not(target_os = "windows"))]
2543 fn path_with_position_parse_posix_path_with_suffix() {
2544 assert_eq!(
2545 PathWithPosition::parse_str("foo/bar:34:in"),
2546 PathWithPosition {
2547 path: PathBuf::from("foo/bar"),
2548 row: Some(34),
2549 column: None,
2550 }
2551 );
2552 assert_eq!(
2553 PathWithPosition::parse_str("foo/bar.rs:1902:::15:"),
2554 PathWithPosition {
2555 path: PathBuf::from("foo/bar.rs:1902"),
2556 row: Some(15),
2557 column: None
2558 }
2559 );
2560
2561 assert_eq!(
2562 PathWithPosition::parse_str("app-editors:zed-0.143.6:20240710-201212.log:34:"),
2563 PathWithPosition {
2564 path: PathBuf::from("app-editors:zed-0.143.6:20240710-201212.log"),
2565 row: Some(34),
2566 column: None,
2567 }
2568 );
2569
2570 assert_eq!(
2571 PathWithPosition::parse_str("crates/file_finder/src/file_finder.rs:1902:13:"),
2572 PathWithPosition {
2573 path: PathBuf::from("crates/file_finder/src/file_finder.rs"),
2574 row: Some(1902),
2575 column: Some(13),
2576 }
2577 );
2578
2579 assert_eq!(
2580 PathWithPosition::parse_str("crate/utils/src/test:today.log:34"),
2581 PathWithPosition {
2582 path: PathBuf::from("crate/utils/src/test:today.log"),
2583 row: Some(34),
2584 column: None,
2585 }
2586 );
2587 assert_eq!(
2588 PathWithPosition::parse_str("/testing/out/src/file_finder.odin(7:15)"),
2589 PathWithPosition {
2590 path: PathBuf::from("/testing/out/src/file_finder.odin"),
2591 row: Some(7),
2592 column: Some(15),
2593 }
2594 );
2595 }
2596
2597 #[perf]
2598 #[cfg(target_os = "windows")]
2599 fn path_with_position_parse_windows_path() {
2600 assert_eq!(
2601 PathWithPosition::parse_str("crates\\utils\\paths.rs"),
2602 PathWithPosition {
2603 path: PathBuf::from("crates\\utils\\paths.rs"),
2604 row: None,
2605 column: None
2606 }
2607 );
2608
2609 assert_eq!(
2610 PathWithPosition::parse_str("C:\\Users\\someone\\test_file.rs"),
2611 PathWithPosition {
2612 path: PathBuf::from("C:\\Users\\someone\\test_file.rs"),
2613 row: None,
2614 column: None
2615 }
2616 );
2617
2618 assert_eq!(
2619 PathWithPosition::parse_str("C:\\Users\\someone\\main (1).log"),
2620 PathWithPosition {
2621 path: PathBuf::from("C:\\Users\\someone\\main (1).log"),
2622 row: None,
2623 column: None
2624 }
2625 );
2626 }
2627
2628 #[perf]
2629 #[cfg(target_os = "windows")]
2630 fn path_with_position_parse_windows_path_with_suffix() {
2631 assert_eq!(
2632 PathWithPosition::parse_str("crates\\utils\\paths.rs:101"),
2633 PathWithPosition {
2634 path: PathBuf::from("crates\\utils\\paths.rs"),
2635 row: Some(101),
2636 column: None
2637 }
2638 );
2639
2640 assert_eq!(
2641 PathWithPosition::parse_str("\\\\?\\C:\\Users\\someone\\test_file.rs:1:20"),
2642 PathWithPosition {
2643 path: PathBuf::from("\\\\?\\C:\\Users\\someone\\test_file.rs"),
2644 row: Some(1),
2645 column: Some(20)
2646 }
2647 );
2648
2649 assert_eq!(
2650 PathWithPosition::parse_str("C:\\Users\\someone\\test_file.rs(1902,13)"),
2651 PathWithPosition {
2652 path: PathBuf::from("C:\\Users\\someone\\test_file.rs"),
2653 row: Some(1902),
2654 column: Some(13)
2655 }
2656 );
2657
2658 assert_eq!(
2660 PathWithPosition::parse_str("\\\\?\\C:\\Users\\someone\\test_file.rs:1902:13:"),
2661 PathWithPosition {
2662 path: PathBuf::from("\\\\?\\C:\\Users\\someone\\test_file.rs"),
2663 row: Some(1902),
2664 column: Some(13)
2665 }
2666 );
2667
2668 assert_eq!(
2669 PathWithPosition::parse_str("\\\\?\\C:\\Users\\someone\\test_file.rs:1902:13:15:"),
2670 PathWithPosition {
2671 path: PathBuf::from("\\\\?\\C:\\Users\\someone\\test_file.rs:1902"),
2672 row: Some(13),
2673 column: Some(15)
2674 }
2675 );
2676
2677 assert_eq!(
2678 PathWithPosition::parse_str("\\\\?\\C:\\Users\\someone\\test_file.rs:1902:::15:"),
2679 PathWithPosition {
2680 path: PathBuf::from("\\\\?\\C:\\Users\\someone\\test_file.rs:1902"),
2681 row: Some(15),
2682 column: None
2683 }
2684 );
2685
2686 assert_eq!(
2687 PathWithPosition::parse_str("\\\\?\\C:\\Users\\someone\\test_file.rs(1902,13):"),
2688 PathWithPosition {
2689 path: PathBuf::from("\\\\?\\C:\\Users\\someone\\test_file.rs"),
2690 row: Some(1902),
2691 column: Some(13),
2692 }
2693 );
2694
2695 assert_eq!(
2696 PathWithPosition::parse_str("\\\\?\\C:\\Users\\someone\\test_file.rs(1902):"),
2697 PathWithPosition {
2698 path: PathBuf::from("\\\\?\\C:\\Users\\someone\\test_file.rs"),
2699 row: Some(1902),
2700 column: None,
2701 }
2702 );
2703
2704 assert_eq!(
2705 PathWithPosition::parse_str("C:\\Users\\someone\\test_file.rs:1902:13:"),
2706 PathWithPosition {
2707 path: PathBuf::from("C:\\Users\\someone\\test_file.rs"),
2708 row: Some(1902),
2709 column: Some(13),
2710 }
2711 );
2712
2713 assert_eq!(
2714 PathWithPosition::parse_str("C:\\Users\\someone\\test_file.rs(1902,13):"),
2715 PathWithPosition {
2716 path: PathBuf::from("C:\\Users\\someone\\test_file.rs"),
2717 row: Some(1902),
2718 column: Some(13),
2719 }
2720 );
2721
2722 assert_eq!(
2723 PathWithPosition::parse_str("C:\\Users\\someone\\test_file.rs(1902):"),
2724 PathWithPosition {
2725 path: PathBuf::from("C:\\Users\\someone\\test_file.rs"),
2726 row: Some(1902),
2727 column: None,
2728 }
2729 );
2730
2731 assert_eq!(
2732 PathWithPosition::parse_str("crates/utils/paths.rs:101"),
2733 PathWithPosition {
2734 path: PathBuf::from("crates\\utils\\paths.rs"),
2735 row: Some(101),
2736 column: None,
2737 }
2738 );
2739 }
2740
2741 #[perf]
2742 fn test_path_compact() {
2743 let path: PathBuf = [
2744 home_dir().to_string_lossy().into_owned(),
2745 "some_file.txt".to_string(),
2746 ]
2747 .iter()
2748 .collect();
2749 if cfg!(any(target_os = "linux", target_os = "freebsd")) || cfg!(target_os = "macos") {
2750 assert_eq!(path.compact().to_str(), Some("~/some_file.txt"));
2751 } else {
2752 assert_eq!(path.compact().to_str(), path.to_str());
2753 }
2754 }
2755
2756 #[perf]
2757 fn test_extension_or_hidden_file_name() {
2758 let path = Path::new("/a/b/c/file_name.rs");
2760 assert_eq!(path.extension_or_hidden_file_name(), Some("rs"));
2761
2762 let path = Path::new("/a/b/c/file.name.rs");
2764 assert_eq!(path.extension_or_hidden_file_name(), Some("rs"));
2765
2766 let path = Path::new("/a/b/c/long.file.name.rs");
2768 assert_eq!(path.extension_or_hidden_file_name(), Some("rs"));
2769
2770 let path = Path::new("/a/b/c/.gitignore");
2772 assert_eq!(path.extension_or_hidden_file_name(), Some("gitignore"));
2773
2774 let path = Path::new("/a/b/c/.eslintrc.js");
2776 assert_eq!(path.extension_or_hidden_file_name(), Some("eslintrc.js"));
2777 }
2778
2779 #[perf]
2780 #[perf]
2816 #[cfg(target_os = "windows")]
2817 fn test_sanitized_path() {
2818 let path = Path::new("C:\\Users\\someone\\test_file.rs");
2819 let sanitized_path = SanitizedPath::new(path);
2820 assert_eq!(
2821 sanitized_path.to_string(),
2822 "C:\\Users\\someone\\test_file.rs"
2823 );
2824
2825 let path = Path::new("\\\\?\\C:\\Users\\someone\\test_file.rs");
2826 let sanitized_path = SanitizedPath::new(path);
2827 assert_eq!(
2828 sanitized_path.to_string(),
2829 "C:\\Users\\someone\\test_file.rs"
2830 );
2831 }
2832
2833 #[perf]
2834 fn test_compare_numeric_segments() {
2835 fn compare(a: &str, b: &str) -> Ordering {
2837 let mut a_iter = a.chars().peekable();
2838 let mut b_iter = b.chars().peekable();
2839
2840 let result = compare_numeric_segments(&mut a_iter, &mut b_iter);
2841
2842 assert!(
2844 !a_iter.next().is_some_and(|c| c.is_ascii_digit()),
2845 "Iterator a should have consumed all digits"
2846 );
2847 assert!(
2848 !b_iter.next().is_some_and(|c| c.is_ascii_digit()),
2849 "Iterator b should have consumed all digits"
2850 );
2851
2852 result
2853 }
2854
2855 assert_eq!(compare("0", "0"), Ordering::Equal);
2857 assert_eq!(compare("1", "2"), Ordering::Less);
2858 assert_eq!(compare("9", "10"), Ordering::Less);
2859 assert_eq!(compare("10", "9"), Ordering::Greater);
2860 assert_eq!(compare("99", "100"), Ordering::Less);
2861
2862 assert_eq!(compare("0", "00"), Ordering::Less);
2864 assert_eq!(compare("00", "0"), Ordering::Greater);
2865 assert_eq!(compare("01", "1"), Ordering::Greater);
2866 assert_eq!(compare("001", "1"), Ordering::Greater);
2867 assert_eq!(compare("001", "01"), Ordering::Greater);
2868
2869 assert_eq!(compare("000100", "100"), Ordering::Greater);
2871 assert_eq!(compare("100", "0100"), Ordering::Less);
2872 assert_eq!(compare("0100", "00100"), Ordering::Less);
2873
2874 assert_eq!(compare("9999999999", "10000000000"), Ordering::Less);
2876 assert_eq!(
2877 compare(
2878 "340282366920938463463374607431768211455", "340282366920938463463374607431768211456"
2880 ),
2881 Ordering::Less
2882 );
2883 assert_eq!(
2884 compare(
2885 "340282366920938463463374607431768211456", "340282366920938463463374607431768211455"
2887 ),
2888 Ordering::Greater
2889 );
2890
2891 let mut a_iter = "123abc".chars().peekable();
2893 let mut b_iter = "456def".chars().peekable();
2894
2895 compare_numeric_segments(&mut a_iter, &mut b_iter);
2896
2897 assert_eq!(a_iter.collect::<String>(), "abc");
2898 assert_eq!(b_iter.collect::<String>(), "def");
2899 }
2900
2901 #[perf]
2902 fn test_natural_sort() {
2903 assert_eq!(natural_sort("a", "b"), Ordering::Less);
2905 assert_eq!(natural_sort("b", "a"), Ordering::Greater);
2906 assert_eq!(natural_sort("a", "a"), Ordering::Equal);
2907
2908 assert_eq!(natural_sort("a", "A"), Ordering::Less);
2910 assert_eq!(natural_sort("A", "a"), Ordering::Greater);
2911 assert_eq!(natural_sort("aA", "aa"), Ordering::Greater);
2912 assert_eq!(natural_sort("aa", "aA"), Ordering::Less);
2913
2914 assert_eq!(natural_sort("1", "2"), Ordering::Less);
2916 assert_eq!(natural_sort("2", "10"), Ordering::Less);
2917 assert_eq!(natural_sort("02", "10"), Ordering::Less);
2918 assert_eq!(natural_sort("02", "2"), Ordering::Greater);
2919
2920 assert_eq!(natural_sort("a1", "a2"), Ordering::Less);
2922 assert_eq!(natural_sort("a2", "a10"), Ordering::Less);
2923 assert_eq!(natural_sort("a02", "a2"), Ordering::Greater);
2924 assert_eq!(natural_sort("a1b", "a1c"), Ordering::Less);
2925
2926 assert_eq!(natural_sort("1a2", "1a10"), Ordering::Less);
2928 assert_eq!(natural_sort("1a10", "1a2"), Ordering::Greater);
2929 assert_eq!(natural_sort("2a1", "10a1"), Ordering::Less);
2930
2931 assert_eq!(natural_sort("a-1", "a-2"), Ordering::Less);
2933 assert_eq!(natural_sort("a_1", "a_2"), Ordering::Less);
2934 assert_eq!(natural_sort("a.1", "a.2"), Ordering::Less);
2935
2936 assert_eq!(natural_sort("文1", "文2"), Ordering::Less);
2938 assert_eq!(natural_sort("文2", "文10"), Ordering::Less);
2939 assert_eq!(natural_sort("🔤1", "🔤2"), Ordering::Less);
2940
2941 assert_eq!(natural_sort("", ""), Ordering::Equal);
2943 assert_eq!(natural_sort("", "a"), Ordering::Less);
2944 assert_eq!(natural_sort("a", ""), Ordering::Greater);
2945 assert_eq!(natural_sort(" ", " "), Ordering::Less);
2946
2947 assert_eq!(natural_sort("File-1.txt", "File-2.txt"), Ordering::Less);
2949 assert_eq!(natural_sort("File-02.txt", "File-2.txt"), Ordering::Greater);
2950 assert_eq!(natural_sort("File-2.txt", "File-10.txt"), Ordering::Less);
2951 assert_eq!(natural_sort("File_A1", "File_A2"), Ordering::Less);
2952 assert_eq!(natural_sort("File_a1", "File_A1"), Ordering::Less);
2953 }
2954
2955 #[perf]
2956 fn test_compare_paths() {
2957 fn compare(a: &str, is_a_file: bool, b: &str, is_b_file: bool) -> Ordering {
2959 compare_paths((Path::new(a), is_a_file), (Path::new(b), is_b_file))
2960 }
2961
2962 assert_eq!(compare("a", true, "b", true), Ordering::Less);
2964 assert_eq!(compare("b", true, "a", true), Ordering::Greater);
2965 assert_eq!(compare("a", true, "a", true), Ordering::Equal);
2966
2967 assert_eq!(compare("a", true, "a", false), Ordering::Greater);
2969 assert_eq!(compare("a", false, "a", true), Ordering::Less);
2970 assert_eq!(compare("b", false, "a", true), Ordering::Less);
2971
2972 assert_eq!(compare("a.txt", true, "a.md", true), Ordering::Greater);
2974 assert_eq!(compare("a.md", true, "a.txt", true), Ordering::Less);
2975 assert_eq!(compare("a", true, "a.txt", true), Ordering::Less);
2976
2977 assert_eq!(compare("dir/a", true, "dir/b", true), Ordering::Less);
2979 assert_eq!(compare("dir1/a", true, "dir2/a", true), Ordering::Less);
2980 assert_eq!(compare("dir/sub/a", true, "dir/a", true), Ordering::Less);
2981
2982 assert_eq!(
2984 compare("Dir/file", true, "dir/file", true),
2985 Ordering::Greater
2986 );
2987 assert_eq!(
2988 compare("dir/File", true, "dir/file", true),
2989 Ordering::Greater
2990 );
2991 assert_eq!(compare("dir/file", true, "Dir/File", true), Ordering::Less);
2992
2993 assert_eq!(compare(".hidden", true, "visible", true), Ordering::Less);
2995 assert_eq!(compare("_special", true, "normal", true), Ordering::Less);
2996 assert_eq!(compare(".config", false, ".data", false), Ordering::Less);
2997
2998 assert_eq!(
3000 compare("dir1/file", true, "dir2/file", true),
3001 Ordering::Less
3002 );
3003 assert_eq!(
3004 compare("dir2/file", true, "dir10/file", true),
3005 Ordering::Less
3006 );
3007 assert_eq!(
3008 compare("dir02/file", true, "dir2/file", true),
3009 Ordering::Greater
3010 );
3011
3012 assert_eq!(compare("/a", true, "/b", true), Ordering::Less);
3014 assert_eq!(compare("/", false, "/a", true), Ordering::Less);
3015
3016 assert_eq!(
3018 compare("project/src/main.rs", true, "project/src/lib.rs", true),
3019 Ordering::Greater
3020 );
3021 assert_eq!(
3022 compare(
3023 "project/tests/test_1.rs",
3024 true,
3025 "project/tests/test_2.rs",
3026 true
3027 ),
3028 Ordering::Less
3029 );
3030 assert_eq!(
3031 compare(
3032 "project/v1.0.0/README.md",
3033 true,
3034 "project/v1.10.0/README.md",
3035 true
3036 ),
3037 Ordering::Less
3038 );
3039 }
3040
3041 #[perf]
3042 fn test_natural_sort_case_sensitivity() {
3043 std::thread::sleep(std::time::Duration::from_millis(100));
3044 assert_eq!(natural_sort("a", "A"), Ordering::Less);
3046 assert_eq!(natural_sort("A", "a"), Ordering::Greater);
3047 assert_eq!(natural_sort("a", "a"), Ordering::Equal);
3048 assert_eq!(natural_sort("A", "A"), Ordering::Equal);
3049
3050 assert_eq!(natural_sort("aaa", "AAA"), Ordering::Less);
3052 assert_eq!(natural_sort("AAA", "aaa"), Ordering::Greater);
3053 assert_eq!(natural_sort("aAa", "AaA"), Ordering::Less);
3054
3055 assert_eq!(natural_sort("a", "b"), Ordering::Less);
3057 assert_eq!(natural_sort("A", "b"), Ordering::Less);
3058 assert_eq!(natural_sort("a", "B"), Ordering::Less);
3059 }
3060
3061 #[perf]
3062 fn test_natural_sort_with_numbers() {
3063 assert_eq!(natural_sort("file1", "file2"), Ordering::Less);
3065 assert_eq!(natural_sort("file2", "file10"), Ordering::Less);
3066 assert_eq!(natural_sort("file10", "file2"), Ordering::Greater);
3067
3068 assert_eq!(natural_sort("1file", "2file"), Ordering::Less);
3070 assert_eq!(natural_sort("file1text", "file2text"), Ordering::Less);
3071 assert_eq!(natural_sort("text1file", "text2file"), Ordering::Less);
3072
3073 assert_eq!(natural_sort("file1-2", "file1-10"), Ordering::Less);
3075 assert_eq!(natural_sort("2-1file", "10-1file"), Ordering::Less);
3076
3077 assert_eq!(natural_sort("file002", "file2"), Ordering::Greater);
3079 assert_eq!(natural_sort("file002", "file10"), Ordering::Less);
3080
3081 assert_eq!(
3083 natural_sort("file999999999999999999999", "file999999999999999999998"),
3084 Ordering::Greater
3085 );
3086
3087 assert_eq!(
3091 natural_sort(
3092 "file340282366920938463463374607431768211454",
3093 "file340282366920938463463374607431768211455"
3094 ),
3095 Ordering::Less
3096 );
3097
3098 assert_eq!(
3100 natural_sort(
3101 "file340282366920938463463374607431768211456",
3102 "file340282366920938463463374607431768211455"
3103 ),
3104 Ordering::Greater
3105 );
3106
3107 assert_eq!(
3109 natural_sort(
3110 "file3402823669209384634633746074317682114560",
3111 "file340282366920938463463374607431768211455"
3112 ),
3113 Ordering::Greater
3114 );
3115
3116 assert_eq!(
3118 natural_sort(
3119 "file0340282366920938463463374607431768211455",
3120 "file340282366920938463463374607431768211455"
3121 ),
3122 Ordering::Greater
3123 );
3124
3125 assert_eq!(
3127 natural_sort(
3128 "file999999999999999999999999999999999999999999999999",
3129 "file9999999999999999999999999999999999999999999999999"
3130 ),
3131 Ordering::Less
3132 );
3133 }
3134
3135 #[perf]
3136 fn test_natural_sort_case_sensitive() {
3137 assert_eq!(natural_sort("File1", "file2"), Ordering::Less);
3139 assert_eq!(natural_sort("file1", "File2"), Ordering::Less);
3140
3141 assert_eq!(natural_sort("Dir1", "dir01"), Ordering::Less);
3144 assert_eq!(natural_sort("dir2", "Dir02"), Ordering::Less);
3145 assert_eq!(natural_sort("dir2", "dir02"), Ordering::Less);
3146
3147 assert_eq!(natural_sort("dir1", "Dir1"), Ordering::Less);
3150 assert_eq!(natural_sort("dir02", "Dir02"), Ordering::Less);
3151 assert_eq!(natural_sort("dir10", "Dir10"), Ordering::Less);
3152 }
3153
3154 #[perf]
3155 fn test_natural_sort_edge_cases() {
3156 assert_eq!(natural_sort("", ""), Ordering::Equal);
3158 assert_eq!(natural_sort("", "a"), Ordering::Less);
3159 assert_eq!(natural_sort("a", ""), Ordering::Greater);
3160
3161 assert_eq!(natural_sort("file-1", "file_1"), Ordering::Less);
3163 assert_eq!(natural_sort("file.1", "file_1"), Ordering::Less);
3164 assert_eq!(natural_sort("file 1", "file_1"), Ordering::Less);
3165
3166 assert_eq!(natural_sort("file①", "file②"), Ordering::Less);
3169 assert_eq!(natural_sort("file⑩", "file②"), Ordering::Greater);
3171 assert_eq!(natural_sort("file漢", "file字"), Ordering::Greater);
3173
3174 assert_eq!(natural_sort("file-1a", "file-1b"), Ordering::Less);
3176 assert_eq!(natural_sort("file-1.2", "file-1.10"), Ordering::Less);
3177 assert_eq!(natural_sort("file-1.10", "file-1.2"), Ordering::Greater);
3178 }
3179
3180 #[test]
3181 fn test_multiple_extensions() {
3182 let path = Path::new("/a/b/c/file_name");
3184 assert_eq!(path.multiple_extensions(), None);
3185
3186 let path = Path::new("/a/b/c/file_name.tsx");
3188 assert_eq!(path.multiple_extensions(), None);
3189
3190 let path = Path::new("/a/b/c/file_name.stories.tsx");
3192 assert_eq!(path.multiple_extensions(), Some("stories.tsx".to_string()));
3193
3194 let path = Path::new("/a/b/c/long.app.tar.gz");
3196 assert_eq!(path.multiple_extensions(), Some("app.tar.gz".to_string()));
3197 }
3198
3199 #[test]
3200 fn test_strip_path_suffix() {
3201 let base = Path::new("/a/b/c/file_name");
3202 let suffix = Path::new("file_name");
3203 assert_eq!(strip_path_suffix(base, suffix), Some(Path::new("/a/b/c")));
3204
3205 let base = Path::new("/a/b/c/file_name.tsx");
3206 let suffix = Path::new("file_name.tsx");
3207 assert_eq!(strip_path_suffix(base, suffix), Some(Path::new("/a/b/c")));
3208
3209 let base = Path::new("/a/b/c/file_name.stories.tsx");
3210 let suffix = Path::new("c/file_name.stories.tsx");
3211 assert_eq!(strip_path_suffix(base, suffix), Some(Path::new("/a/b")));
3212
3213 let base = Path::new("/a/b/c/long.app.tar.gz");
3214 let suffix = Path::new("b/c/long.app.tar.gz");
3215 assert_eq!(strip_path_suffix(base, suffix), Some(Path::new("/a")));
3216
3217 let base = Path::new("/a/b/c/long.app.tar.gz");
3218 let suffix = Path::new("/a/b/c/long.app.tar.gz");
3219 assert_eq!(strip_path_suffix(base, suffix), Some(Path::new("")));
3220
3221 let base = Path::new("/a/b/c/long.app.tar.gz");
3222 let suffix = Path::new("/a/b/c/no_match.app.tar.gz");
3223 assert_eq!(strip_path_suffix(base, suffix), None);
3224
3225 let base = Path::new("/a/b/c/long.app.tar.gz");
3226 let suffix = Path::new("app.tar.gz");
3227 assert_eq!(strip_path_suffix(base, suffix), None);
3228 }
3229
3230 #[test]
3231 fn test_strip_prefix() {
3232 let expected = [
3233 (
3234 PathStyle::Posix,
3235 "/a/b/c",
3236 "/a/b",
3237 Some(rel_path("c").into_arc()),
3238 ),
3239 (
3240 PathStyle::Posix,
3241 "/a/b/c",
3242 "/a/b/",
3243 Some(rel_path("c").into_arc()),
3244 ),
3245 (
3246 PathStyle::Posix,
3247 "/a/b/c",
3248 "/",
3249 Some(rel_path("a/b/c").into_arc()),
3250 ),
3251 (PathStyle::Posix, "/a/b/c", "", None),
3252 (PathStyle::Posix, "/a/b//c", "/a/b/", None),
3253 (PathStyle::Posix, "/a/bc", "/a/b", None),
3254 (
3255 PathStyle::Posix,
3256 "/a/b/c",
3257 "/a/b/c",
3258 Some(rel_path("").into_arc()),
3259 ),
3260 (
3261 PathStyle::Windows,
3262 "C:\\a\\b\\c",
3263 "C:\\a\\b",
3264 Some(rel_path("c").into_arc()),
3265 ),
3266 (
3267 PathStyle::Windows,
3268 "C:\\a\\b\\c",
3269 "C:\\a\\b\\",
3270 Some(rel_path("c").into_arc()),
3271 ),
3272 (
3273 PathStyle::Windows,
3274 "C:\\a\\b\\c",
3275 "C:\\",
3276 Some(rel_path("a/b/c").into_arc()),
3277 ),
3278 (PathStyle::Windows, "C:\\a\\b\\c", "", None),
3279 (PathStyle::Windows, "C:\\a\\b\\\\c", "C:\\a\\b\\", None),
3280 (PathStyle::Windows, "C:\\a\\bc", "C:\\a\\b", None),
3281 (
3282 PathStyle::Windows,
3283 "C:\\a\\b/c",
3284 "C:\\a\\b",
3285 Some(rel_path("c").into_arc()),
3286 ),
3287 (
3288 PathStyle::Windows,
3289 "C:\\a\\b/c",
3290 "C:\\a\\b\\",
3291 Some(rel_path("c").into_arc()),
3292 ),
3293 (
3294 PathStyle::Windows,
3295 "C:\\a\\b/c",
3296 "C:\\a\\b/",
3297 Some(rel_path("c").into_arc()),
3298 ),
3299 ];
3300 let actual = expected.clone().map(|(style, child, parent, _)| {
3301 (
3302 style,
3303 child,
3304 parent,
3305 style
3306 .strip_prefix(child.as_ref(), parent.as_ref())
3307 .map(|rel_path| rel_path.into_arc()),
3308 )
3309 });
3310 pretty_assertions::assert_eq!(actual, expected);
3311 }
3312
3313 #[cfg(target_os = "windows")]
3314 #[test]
3315 fn test_wsl_path() {
3316 use super::WslPath;
3317 let path = "/a/b/c";
3318 assert_eq!(WslPath::from_path(&path), None);
3319
3320 let path = r"\\wsl.localhost";
3321 assert_eq!(WslPath::from_path(&path), None);
3322
3323 let path = r"\\wsl.localhost\Distro";
3324 assert_eq!(
3325 WslPath::from_path(&path),
3326 Some(WslPath {
3327 distro: "Distro".to_owned(),
3328 path: "/".into(),
3329 })
3330 );
3331
3332 let path = r"\\wsl.localhost\Distro\blue";
3333 assert_eq!(
3334 WslPath::from_path(&path),
3335 Some(WslPath {
3336 distro: "Distro".to_owned(),
3337 path: "/blue".into()
3338 })
3339 );
3340
3341 let path = r"\\wsl$\archlinux\tomato\.\paprika\..\aubergine.txt";
3342 assert_eq!(
3343 WslPath::from_path(&path),
3344 Some(WslPath {
3345 distro: "archlinux".to_owned(),
3346 path: "/tomato/paprika/../aubergine.txt".into()
3347 })
3348 );
3349
3350 let path = r"\\windows.localhost\Distro\foo";
3351 assert_eq!(WslPath::from_path(&path), None);
3352 }
3353
3354 #[test]
3355 fn test_url_to_file_path_ext_posix_basic() {
3356 use super::UrlExt;
3357
3358 let url = url::Url::parse("file:///home/user/file.txt").unwrap();
3359 assert_eq!(
3360 url.to_file_path_ext(PathStyle::Posix),
3361 Ok(PathBuf::from("/home/user/file.txt"))
3362 );
3363
3364 let url = url::Url::parse("file:///").unwrap();
3365 assert_eq!(
3366 url.to_file_path_ext(PathStyle::Posix),
3367 Ok(PathBuf::from("/"))
3368 );
3369
3370 let url = url::Url::parse("file:///a/b/c/d/e").unwrap();
3371 assert_eq!(
3372 url.to_file_path_ext(PathStyle::Posix),
3373 Ok(PathBuf::from("/a/b/c/d/e"))
3374 );
3375 }
3376
3377 #[test]
3378 fn test_url_to_file_path_ext_posix_percent_encoding() {
3379 use super::UrlExt;
3380
3381 let url = url::Url::parse("file:///home/user/file%20with%20spaces.txt").unwrap();
3382 assert_eq!(
3383 url.to_file_path_ext(PathStyle::Posix),
3384 Ok(PathBuf::from("/home/user/file with spaces.txt"))
3385 );
3386
3387 let url = url::Url::parse("file:///path%2Fwith%2Fencoded%2Fslashes").unwrap();
3388 assert_eq!(
3389 url.to_file_path_ext(PathStyle::Posix),
3390 Ok(PathBuf::from("/path/with/encoded/slashes"))
3391 );
3392
3393 let url = url::Url::parse("file:///special%23chars%3F.txt").unwrap();
3394 assert_eq!(
3395 url.to_file_path_ext(PathStyle::Posix),
3396 Ok(PathBuf::from("/special#chars?.txt"))
3397 );
3398 }
3399
3400 #[test]
3401 fn test_url_to_file_path_ext_posix_localhost() {
3402 use super::UrlExt;
3403
3404 let url = url::Url::parse("file://localhost/home/user/file.txt").unwrap();
3405 assert_eq!(
3406 url.to_file_path_ext(PathStyle::Posix),
3407 Ok(PathBuf::from("/home/user/file.txt"))
3408 );
3409 }
3410
3411 #[test]
3412 fn test_url_to_file_path_ext_posix_rejects_host() {
3413 use super::UrlExt;
3414
3415 let url = url::Url::parse("file://somehost/home/user/file.txt").unwrap();
3416 assert_eq!(url.to_file_path_ext(PathStyle::Posix), Err(()));
3417 }
3418
3419 #[test]
3420 fn test_url_to_file_path_ext_posix_windows_drive_letter() {
3421 use super::UrlExt;
3422
3423 let url = url::Url::parse("file:///C:").unwrap();
3424 assert_eq!(
3425 url.to_file_path_ext(PathStyle::Posix),
3426 Ok(PathBuf::from("/C:/"))
3427 );
3428
3429 let url = url::Url::parse("file:///D|").unwrap();
3430 assert_eq!(
3431 url.to_file_path_ext(PathStyle::Posix),
3432 Ok(PathBuf::from("/D|/"))
3433 );
3434 }
3435
3436 #[test]
3437 fn test_url_to_file_path_ext_windows_basic() {
3438 use super::UrlExt;
3439
3440 let url = url::Url::parse("file:///C:/Users/user/file.txt").unwrap();
3441 assert_eq!(
3442 url.to_file_path_ext(PathStyle::Windows),
3443 Ok(PathBuf::from("C:\\Users\\user\\file.txt"))
3444 );
3445
3446 let url = url::Url::parse("file:///D:/folder/subfolder/file.rs").unwrap();
3447 assert_eq!(
3448 url.to_file_path_ext(PathStyle::Windows),
3449 Ok(PathBuf::from("D:\\folder\\subfolder\\file.rs"))
3450 );
3451
3452 let url = url::Url::parse("file:///C:/").unwrap();
3453 assert_eq!(
3454 url.to_file_path_ext(PathStyle::Windows),
3455 Ok(PathBuf::from("C:\\"))
3456 );
3457 }
3458
3459 #[test]
3460 fn test_url_to_file_path_ext_windows_encoded_drive_letter() {
3461 use super::UrlExt;
3462
3463 let url = url::Url::parse("file:///C%3A/Users/file.txt").unwrap();
3464 assert_eq!(
3465 url.to_file_path_ext(PathStyle::Windows),
3466 Ok(PathBuf::from("C:\\Users\\file.txt"))
3467 );
3468
3469 let url = url::Url::parse("file:///c%3a/Users/file.txt").unwrap();
3470 assert_eq!(
3471 url.to_file_path_ext(PathStyle::Windows),
3472 Ok(PathBuf::from("c:\\Users\\file.txt"))
3473 );
3474
3475 let url = url::Url::parse("file:///D%3A/folder/file.txt").unwrap();
3476 assert_eq!(
3477 url.to_file_path_ext(PathStyle::Windows),
3478 Ok(PathBuf::from("D:\\folder\\file.txt"))
3479 );
3480
3481 let url = url::Url::parse("file:///d%3A/folder/file.txt").unwrap();
3482 assert_eq!(
3483 url.to_file_path_ext(PathStyle::Windows),
3484 Ok(PathBuf::from("d:\\folder\\file.txt"))
3485 );
3486 }
3487
3488 #[test]
3489 fn test_url_to_file_path_ext_windows_unc_path() {
3490 use super::UrlExt;
3491
3492 let url = url::Url::parse("file://server/share/path/file.txt").unwrap();
3493 assert_eq!(
3494 url.to_file_path_ext(PathStyle::Windows),
3495 Ok(PathBuf::from("\\\\server\\share\\path\\file.txt"))
3496 );
3497
3498 let url = url::Url::parse("file://server/share").unwrap();
3499 assert_eq!(
3500 url.to_file_path_ext(PathStyle::Windows),
3501 Ok(PathBuf::from("\\\\server\\share"))
3502 );
3503 }
3504
3505 #[test]
3506 fn test_url_to_file_path_ext_windows_percent_encoding() {
3507 use super::UrlExt;
3508
3509 let url = url::Url::parse("file:///C:/Users/user/file%20with%20spaces.txt").unwrap();
3510 assert_eq!(
3511 url.to_file_path_ext(PathStyle::Windows),
3512 Ok(PathBuf::from("C:\\Users\\user\\file with spaces.txt"))
3513 );
3514
3515 let url = url::Url::parse("file:///C:/special%23chars%3F.txt").unwrap();
3516 assert_eq!(
3517 url.to_file_path_ext(PathStyle::Windows),
3518 Ok(PathBuf::from("C:\\special#chars?.txt"))
3519 );
3520 }
3521
3522 #[test]
3523 fn test_url_to_file_path_ext_windows_invalid_drive() {
3524 use super::UrlExt;
3525
3526 let url = url::Url::parse("file:///1:/path/file.txt").unwrap();
3527 assert_eq!(url.to_file_path_ext(PathStyle::Windows), Err(()));
3528
3529 let url = url::Url::parse("file:///CC:/path/file.txt").unwrap();
3530 assert_eq!(url.to_file_path_ext(PathStyle::Windows), Err(()));
3531
3532 let url = url::Url::parse("file:///C/path/file.txt").unwrap();
3533 assert_eq!(url.to_file_path_ext(PathStyle::Windows), Err(()));
3534
3535 let url = url::Url::parse("file:///invalid").unwrap();
3536 assert_eq!(url.to_file_path_ext(PathStyle::Windows), Err(()));
3537 }
3538
3539 #[test]
3540 fn test_url_to_file_path_ext_non_file_scheme() {
3541 use super::UrlExt;
3542
3543 let url = url::Url::parse("http://example.com/path").unwrap();
3544 assert_eq!(url.to_file_path_ext(PathStyle::Posix), Err(()));
3545 assert_eq!(url.to_file_path_ext(PathStyle::Windows), Err(()));
3546
3547 let url = url::Url::parse("https://example.com/path").unwrap();
3548 assert_eq!(url.to_file_path_ext(PathStyle::Posix), Err(()));
3549 assert_eq!(url.to_file_path_ext(PathStyle::Windows), Err(()));
3550 }
3551
3552 #[test]
3553 fn test_url_to_file_path_ext_windows_localhost() {
3554 use super::UrlExt;
3555
3556 let url = url::Url::parse("file://localhost/C:/Users/file.txt").unwrap();
3557 assert_eq!(
3558 url.to_file_path_ext(PathStyle::Windows),
3559 Ok(PathBuf::from("C:\\Users\\file.txt"))
3560 );
3561 }
3562}