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