1use globset::{Glob, GlobBuilder, GlobSet, GlobSetBuilder};
2use itertools::Itertools;
3use regex::Regex;
4use serde::{Deserialize, Serialize};
5use std::cmp::Ordering;
6use std::error::Error;
7use std::fmt::{Display, Formatter};
8use std::mem;
9use std::path::StripPrefixError;
10use std::sync::Arc;
11use std::{
12 ffi::OsStr,
13 path::{Path, PathBuf},
14 sync::LazyLock,
15};
16
17use path::rel_path::RelPath;
18use path::rel_path::RelPathBuf;
19
20pub use path::PathStyle;
21
22#[cfg(not(target_family = "wasm"))]
24pub fn home_dir() -> &'static PathBuf {
25 static HOME_DIR: std::sync::OnceLock<PathBuf> = std::sync::OnceLock::new();
26 HOME_DIR.get_or_init(|| {
27 if cfg!(any(test, feature = "test-support")) {
28 if cfg!(target_os = "macos") {
29 PathBuf::from("/Users/zed")
30 } else if cfg!(target_os = "windows") {
31 PathBuf::from("C:\\Users\\zed")
32 } else {
33 PathBuf::from("/home/zed")
34 }
35 } else {
36 dirs::home_dir().expect("failed to determine home directory")
37 }
38 })
39}
40
41pub trait PathExt {
42 fn compact(&self) -> PathBuf;
51
52 fn extension_or_hidden_file_name(&self) -> Option<&str>;
54
55 fn try_from_bytes<'a>(bytes: &'a [u8]) -> anyhow::Result<Self>
56 where
57 Self: From<&'a Path>,
58 {
59 #[cfg(target_family = "wasm")]
60 {
61 std::str::from_utf8(bytes)
62 .map(Path::new)
63 .map(Into::into)
64 .map_err(Into::into)
65 }
66 #[cfg(unix)]
67 {
68 use std::os::unix::prelude::OsStrExt;
69 Ok(Self::from(Path::new(OsStr::from_bytes(bytes))))
70 }
71 #[cfg(windows)]
72 {
73 use anyhow::Context;
74 use tendril::fmt::{Format, WTF8};
75 WTF8::validate(bytes)
76 .then(|| {
77 Self::from(Path::new(unsafe {
79 OsStr::from_encoded_bytes_unchecked(bytes)
80 }))
81 })
82 .with_context(|| format!("Invalid WTF-8 sequence: {bytes:?}"))
83 }
84 }
85
86 fn local_to_wsl(&self) -> Option<PathBuf>;
89
90 fn multiple_extensions(&self) -> Option<String>;
95
96 #[cfg(not(target_family = "wasm"))]
98 fn try_shell_safe(&self, shell_kind: crate::shell::ShellKind) -> anyhow::Result<String>;
99}
100
101impl<T: AsRef<Path>> PathExt for T {
102 fn compact(&self) -> PathBuf {
103 #[cfg(target_family = "wasm")]
104 {
105 self.as_ref().to_path_buf()
106 }
107 #[cfg(not(target_family = "wasm"))]
108 if cfg!(any(target_os = "linux", target_os = "freebsd")) || cfg!(target_os = "macos") {
109 match self.as_ref().strip_prefix(home_dir().as_path()) {
110 Ok(relative_path) => {
111 let mut shortened_path = PathBuf::new();
112 shortened_path.push("~");
113 shortened_path.push(relative_path);
114 shortened_path
115 }
116 Err(_) => self.as_ref().to_path_buf(),
117 }
118 } else {
119 self.as_ref().to_path_buf()
120 }
121 }
122
123 fn extension_or_hidden_file_name(&self) -> Option<&str> {
124 let path = self.as_ref();
125 let file_name = path.file_name()?.to_str()?;
126 if file_name.starts_with('.') {
127 return file_name.strip_prefix('.');
128 }
129
130 path.extension()
131 .and_then(|e| e.to_str())
132 .or_else(|| path.file_stem()?.to_str())
133 }
134
135 fn local_to_wsl(&self) -> Option<PathBuf> {
136 let mut new_path = std::ffi::OsString::new();
139 for component in self.as_ref().components() {
140 match component {
141 std::path::Component::Prefix(prefix) => {
142 let drive_letter = prefix.as_os_str().to_string_lossy().to_lowercase();
143 let drive_letter = drive_letter.strip_suffix(':')?;
144
145 new_path.push(format!("/mnt/{}", drive_letter));
146 }
147 std::path::Component::RootDir => {}
148 std::path::Component::CurDir => {
149 new_path.push("/.");
150 }
151 std::path::Component::ParentDir => {
152 new_path.push("/..");
153 }
154 std::path::Component::Normal(os_str) => {
155 new_path.push("/");
156 new_path.push(os_str);
157 }
158 }
159 }
160
161 Some(new_path.into())
162 }
163
164 fn multiple_extensions(&self) -> Option<String> {
165 let path = self.as_ref();
166 let file_name = path.file_name()?.to_str()?;
167
168 let parts: Vec<&str> = file_name
169 .split('.')
170 .skip(1)
172 .collect();
173
174 if parts.len() < 2 {
175 return None;
176 }
177
178 Some(parts.into_iter().join("."))
179 }
180
181 #[cfg(not(target_family = "wasm"))]
182 fn try_shell_safe(&self, shell_kind: crate::shell::ShellKind) -> anyhow::Result<String> {
183 use anyhow::Context;
184 let path_str = self
185 .as_ref()
186 .to_str()
187 .with_context(|| "Path contains invalid UTF-8")?;
188 shell_kind
189 .try_quote(path_str)
190 .as_deref()
191 .map(ToOwned::to_owned)
192 .context("Failed to quote path")
193 }
194}
195
196pub fn path_ends_with(base: &Path, suffix: &Path) -> bool {
197 strip_path_suffix(base, suffix).is_some()
198}
199
200pub fn component_matches_ignore_ascii_case(component: &OsStr, name: &str) -> bool {
208 component
209 .to_str()
210 .is_some_and(|s| s.eq_ignore_ascii_case(name))
211}
212
213pub fn strip_path_suffix<'a>(base: &'a Path, suffix: &Path) -> Option<&'a Path> {
214 if let Some(remainder) = base
215 .as_os_str()
216 .as_encoded_bytes()
217 .strip_suffix(suffix.as_os_str().as_encoded_bytes())
218 {
219 if remainder
220 .last()
221 .is_none_or(|last_byte| std::path::is_separator(*last_byte as char))
222 {
223 let os_str = unsafe {
224 OsStr::from_encoded_bytes_unchecked(
225 &remainder[0..remainder.len().saturating_sub(1)],
226 )
227 };
228 return Some(Path::new(os_str));
229 }
230 }
231 None
232}
233
234#[derive(Eq, PartialEq, Hash, Ord, PartialOrd)]
237#[repr(transparent)]
238pub struct SanitizedPath(Path);
239
240impl SanitizedPath {
241 pub fn new<T: AsRef<Path> + ?Sized>(path: &T) -> &Self {
242 #[cfg(not(target_os = "windows"))]
243 return Self::unchecked_new(path.as_ref());
244
245 #[cfg(target_os = "windows")]
246 return Self::unchecked_new(dunce::simplified(path.as_ref()));
247 }
248
249 pub fn unchecked_new<T: AsRef<Path> + ?Sized>(path: &T) -> &Self {
250 unsafe { mem::transmute::<&Path, &Self>(path.as_ref()) }
252 }
253
254 pub fn from_arc(path: Arc<Path>) -> Arc<Self> {
255 #[cfg(not(target_os = "windows"))]
257 return unsafe { mem::transmute::<Arc<Path>, Arc<Self>>(path) };
258
259 #[cfg(target_os = "windows")]
260 {
261 let path = match path.to_str().and_then(|s| s.strip_prefix(r"\\?\UNC\")) {
262 Some(rest) => PathBuf::from(format!(r"\\{rest}")).into(),
263 None => path,
264 };
265 let simplified = dunce::simplified(path.as_ref());
266 if simplified == path.as_ref() {
267 unsafe { mem::transmute::<Arc<Path>, Arc<Self>>(path) }
269 } else {
270 Self::unchecked_new(simplified).into()
271 }
272 }
273 }
274
275 pub fn new_arc<T: AsRef<Path> + ?Sized>(path: &T) -> Arc<Self> {
276 Self::new(path).into()
277 }
278
279 pub fn cast_arc(path: Arc<Self>) -> Arc<Path> {
280 unsafe { mem::transmute::<Arc<Self>, Arc<Path>>(path) }
282 }
283
284 pub fn cast_arc_ref(path: &Arc<Self>) -> &Arc<Path> {
285 unsafe { mem::transmute::<&Arc<Self>, &Arc<Path>>(path) }
287 }
288
289 pub fn starts_with(&self, prefix: &Self) -> bool {
290 self.0.starts_with(&prefix.0)
291 }
292
293 pub fn as_path(&self) -> &Path {
294 &self.0
295 }
296
297 pub fn file_name(&self) -> Option<&std::ffi::OsStr> {
298 self.0.file_name()
299 }
300
301 pub fn extension(&self) -> Option<&std::ffi::OsStr> {
302 self.0.extension()
303 }
304
305 pub fn join<P: AsRef<Path>>(&self, path: P) -> PathBuf {
306 self.0.join(path)
307 }
308
309 pub fn parent(&self) -> Option<&Self> {
310 self.0.parent().map(Self::unchecked_new)
311 }
312
313 pub fn strip_prefix(&self, base: &Self) -> Result<&Path, StripPrefixError> {
314 self.0.strip_prefix(base.as_path())
315 }
316
317 pub fn to_str(&self) -> Option<&str> {
318 self.0.to_str()
319 }
320
321 pub fn to_path_buf(&self) -> PathBuf {
322 self.0.to_path_buf()
323 }
324}
325
326impl std::fmt::Debug for SanitizedPath {
327 fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
328 std::fmt::Debug::fmt(&self.0, formatter)
329 }
330}
331
332impl Display for SanitizedPath {
333 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
334 write!(f, "{}", self.0.display())
335 }
336}
337
338impl From<&SanitizedPath> for Arc<SanitizedPath> {
339 fn from(sanitized_path: &SanitizedPath) -> Self {
340 let path: Arc<Path> = sanitized_path.0.into();
341 unsafe { mem::transmute(path) }
343 }
344}
345
346impl From<&SanitizedPath> for PathBuf {
347 fn from(sanitized_path: &SanitizedPath) -> Self {
348 sanitized_path.as_path().into()
349 }
350}
351
352impl AsRef<Path> for SanitizedPath {
353 fn as_ref(&self) -> &Path {
354 &self.0
355 }
356}
357
358#[derive(Debug, Clone)]
359pub struct RemotePathBuf {
360 style: PathStyle,
361 string: String,
362}
363
364impl RemotePathBuf {
365 pub fn new(string: String, style: PathStyle) -> Self {
366 Self { style, string }
367 }
368
369 pub fn from_str(path: &str, style: PathStyle) -> Self {
370 Self::new(path.to_string(), style)
371 }
372
373 pub fn path_style(&self) -> PathStyle {
374 self.style
375 }
376
377 pub fn to_proto(self) -> String {
378 self.string
379 }
380}
381
382impl Display for RemotePathBuf {
383 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
384 write!(f, "{}", self.string)
385 }
386}
387
388pub fn is_absolute(path_like: &str, path_style: PathStyle) -> bool {
389 path_like.starts_with('/')
390 || path_style == PathStyle::Windows
391 && (path_like.starts_with('\\')
392 || path_like
393 .chars()
394 .next()
395 .is_some_and(|c| c.is_ascii_alphabetic())
396 && path_like[1..]
397 .strip_prefix(':')
398 .is_some_and(|path| path.starts_with('/') || path.starts_with('\\')))
399}
400
401#[derive(Debug, PartialEq)]
402#[non_exhaustive]
403pub struct NormalizeError;
404
405impl Error for NormalizeError {}
406
407impl std::fmt::Display for NormalizeError {
408 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
409 f.write_str("parent reference `..` points outside of base directory")
410 }
411}
412
413pub fn normalize_lexically(path: &Path) -> Result<PathBuf, NormalizeError> {
430 use std::path::Component;
431
432 let mut lexical = PathBuf::new();
433 let mut iter = path.components().peekable();
434
435 let root = match iter.peek() {
439 Some(Component::ParentDir) => return Err(NormalizeError),
440 Some(p @ Component::RootDir) | Some(p @ Component::CurDir) => {
441 lexical.push(p);
442 iter.next();
443 lexical.as_os_str().len()
444 }
445 Some(Component::Prefix(prefix)) => {
446 lexical.push(prefix.as_os_str());
447 iter.next();
448 if let Some(p @ Component::RootDir) = iter.peek() {
449 lexical.push(p);
450 iter.next();
451 }
452 lexical.as_os_str().len()
453 }
454 None => return Ok(PathBuf::new()),
455 Some(Component::Normal(_)) => 0,
456 };
457
458 for component in iter {
459 match component {
460 Component::RootDir => unreachable!(),
461 Component::Prefix(_) => return Err(NormalizeError),
462 Component::CurDir => continue,
463 Component::ParentDir => {
464 if lexical.as_os_str().len() == root {
466 return Err(NormalizeError);
467 } else {
468 lexical.pop();
469 }
470 }
471 Component::Normal(path) => lexical.push(path),
472 }
473 }
474 Ok(lexical)
475}
476
477pub fn insert_subtree(subtrees: &mut Vec<PathBuf>, path: PathBuf) {
486 if subtrees.iter().any(|existing| path.starts_with(existing)) {
487 return;
488 }
489 subtrees.retain(|existing| !existing.starts_with(&path));
490 subtrees.push(path);
491}
492
493pub fn path_within_subtree<'a>(path: &Path, mut subtrees: impl Iterator<Item = &'a Path>) -> bool {
497 subtrees.any(|granted| path.starts_with(granted))
498}
499
500pub const FILE_ROW_COLUMN_DELIMITER: char = ':';
502
503const ROW_COL_CAPTURE_REGEX: &str = r"(?xs)
504 ([^\(]+)\:(?:
505 \((\d+)[,:](\d+)\) # filename:(row,column), filename:(row:column)
506 |
507 \((\d+)\)() # filename:(row)
508 )
509 |
510 ([^\(]+)(?:
511 \((\d+)[,:](\d+)\) # filename(row,column), filename(row:column)
512 |
513 \((\d+)\)() # filename(row)
514 )
515 \:*$
516 |
517 (.+?)(?:
518 \:+(\d+)\:(\d+)\:*$ # filename:row:column
519 |
520 \:+(\d+)\:*()$ # filename:row
521 |
522 \:+()()$
523 )";
524
525#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)]
528pub struct PathWithPosition {
529 pub path: PathBuf,
530 pub row: Option<u32>,
531 pub column: Option<u32>,
533}
534
535impl PathWithPosition {
536 pub fn from_path(path: PathBuf) -> Self {
538 Self {
539 path,
540 row: None,
541 column: None,
542 }
543 }
544
545 pub fn parse_str(s: &str) -> Self {
631 let trimmed = s.trim();
632 let path = Path::new(trimmed);
633 let Some(maybe_file_name_with_row_col) = path.file_name().unwrap_or_default().to_str()
634 else {
635 return Self {
636 path: Path::new(s).to_path_buf(),
637 row: None,
638 column: None,
639 };
640 };
641 if maybe_file_name_with_row_col.is_empty() {
642 return Self {
643 path: Path::new(s).to_path_buf(),
644 row: None,
645 column: None,
646 };
647 }
648
649 static SUFFIX_RE: LazyLock<Regex> =
653 LazyLock::new(|| Regex::new(ROW_COL_CAPTURE_REGEX).unwrap());
654 match SUFFIX_RE
655 .captures(maybe_file_name_with_row_col)
656 .map(|caps| caps.extract())
657 {
658 Some((_, [file_name, maybe_row, maybe_column])) => {
659 let row = maybe_row.parse::<u32>().ok();
660 let column = maybe_column.parse::<u32>().ok();
661
662 let (_, suffix) = trimmed.split_once(file_name).unwrap();
663 let path_without_suffix = &trimmed[..trimmed.len() - suffix.len()];
664
665 Self {
666 path: Path::new(path_without_suffix).to_path_buf(),
667 row,
668 column,
669 }
670 }
671 None => {
672 let delimiter = ':';
676 let mut path_parts = s
677 .rsplitn(3, delimiter)
678 .collect::<Vec<_>>()
679 .into_iter()
680 .rev()
681 .fuse();
682 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();
683 let mut row = None;
684 let mut column = None;
685 if let Some(maybe_row) = path_parts.next() {
686 if let Ok(parsed_row) = maybe_row.parse::<u32>() {
687 row = Some(parsed_row);
688 if let Some(parsed_column) = path_parts
689 .next()
690 .and_then(|maybe_col| maybe_col.parse::<u32>().ok())
691 {
692 column = Some(parsed_column);
693 }
694 } else {
695 path_string.push(delimiter);
696 path_string.push_str(maybe_row);
697 }
698 }
699 for split in path_parts {
700 path_string.push(delimiter);
701 path_string.push_str(split);
702 }
703
704 Self {
705 path: PathBuf::from(path_string),
706 row,
707 column,
708 }
709 }
710 }
711 }
712
713 pub fn map_path<E>(
714 self,
715 mapping: impl FnOnce(PathBuf) -> Result<PathBuf, E>,
716 ) -> Result<PathWithPosition, E> {
717 Ok(PathWithPosition {
718 path: mapping(self.path)?,
719 row: self.row,
720 column: self.column,
721 })
722 }
723
724 pub fn to_string(&self, path_to_string: &dyn Fn(&PathBuf) -> String) -> String {
725 let path_string = path_to_string(&self.path);
726 if let Some(row) = self.row {
727 if let Some(column) = self.column {
728 format!("{path_string}:{row}:{column}")
729 } else {
730 format!("{path_string}:{row}")
731 }
732 } else {
733 path_string
734 }
735 }
736}
737
738#[derive(Clone)]
739pub struct PathMatcher {
740 sources: Vec<(String, RelPathBuf, bool)>,
741 glob: GlobSet,
742 path_style: PathStyle,
743}
744
745impl std::fmt::Debug for PathMatcher {
746 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
747 f.debug_struct("PathMatcher")
748 .field("sources", &self.sources)
749 .field("path_style", &self.path_style)
750 .finish()
751 }
752}
753
754impl PartialEq for PathMatcher {
755 fn eq(&self, other: &Self) -> bool {
756 self.sources.eq(&other.sources)
757 }
758}
759
760impl Eq for PathMatcher {}
761
762impl PathMatcher {
763 pub fn new(
764 globs: impl IntoIterator<Item = impl AsRef<str>>,
765 path_style: PathStyle,
766 ) -> Result<Self, globset::Error> {
767 let globs = globs
768 .into_iter()
769 .map(|as_str| {
770 GlobBuilder::new(as_str.as_ref())
771 .backslash_escape(path_style.is_posix())
772 .build()
773 })
774 .collect::<Result<Vec<_>, _>>()?;
775 Self::from_globs(globs, path_style)
776 }
777
778 pub fn new_lenient(
781 globs: impl IntoIterator<Item = impl AsRef<str>>,
782 path_style: PathStyle,
783 mut on_error: impl FnMut(globset::Error),
784 ) -> Self {
785 let globs = globs
786 .into_iter()
787 .filter_map(|pattern| {
788 match GlobBuilder::new(pattern.as_ref())
789 .backslash_escape(path_style.is_posix())
790 .build()
791 {
792 Ok(glob) => Some(glob),
793 Err(error) => {
794 on_error(error);
795 None
796 }
797 }
798 })
799 .collect();
800 match Self::from_globs(globs, path_style) {
801 Ok(matcher) => matcher,
802 Err(error) => {
803 on_error(error);
804 Self {
805 path_style,
806 ..Self::default()
807 }
808 }
809 }
810 }
811
812 fn from_globs(globs: Vec<Glob>, path_style: PathStyle) -> Result<Self, globset::Error> {
813 let sources = globs
814 .iter()
815 .filter_map(|glob| {
816 let glob = glob.glob();
817 Some((
818 glob.to_string(),
819 RelPath::new(&glob.as_ref(), path_style)
820 .ok()
821 .map(std::borrow::Cow::into_owned)?,
822 glob.ends_with(path_style.separators_ch()),
823 ))
824 })
825 .collect();
826 let mut glob_builder = GlobSetBuilder::new();
827 for single_glob in globs {
828 glob_builder.add(single_glob);
829 }
830 let glob = glob_builder.build()?;
831 Ok(PathMatcher {
832 glob,
833 sources,
834 path_style,
835 })
836 }
837
838 pub fn sources(&self) -> impl Iterator<Item = &str> + Clone {
839 self.sources.iter().map(|(source, ..)| source.as_str())
840 }
841
842 pub fn is_match<P: AsRef<RelPath>>(&self, other: P) -> bool {
843 let other = other.as_ref();
844 if self
845 .sources
846 .iter()
847 .any(|(_, source, _)| other.starts_with(source) || other.ends_with(source))
848 {
849 return true;
850 }
851 let other_path = other.display(self.path_style);
852
853 if self.glob.is_match(&*other_path) {
854 return true;
855 }
856
857 self.glob
858 .is_match(other_path.into_owned() + self.path_style.primary_separator())
859 }
860
861 pub fn is_match_std_path<P: AsRef<Path>>(&self, other: P) -> bool {
862 let other = other.as_ref();
863 if self.sources.iter().any(|(_, source, _)| {
864 other.starts_with(source.as_std_path()) || other.ends_with(source.as_std_path())
865 }) {
866 return true;
867 }
868 self.glob.is_match(other)
869 }
870}
871
872impl Default for PathMatcher {
873 fn default() -> Self {
874 Self {
875 path_style: PathStyle::local(),
876 glob: GlobSet::empty(),
877 sources: vec![],
878 }
879 }
880}
881
882fn compare_numeric_segments<I>(
917 a_iter: &mut std::iter::Peekable<I>,
918 b_iter: &mut std::iter::Peekable<I>,
919) -> Ordering
920where
921 I: Iterator<Item = char>,
922{
923 let mut a_num_str = String::new();
925 let mut b_num_str = String::new();
926
927 while let Some(&c) = a_iter.peek() {
928 if !c.is_ascii_digit() {
929 break;
930 }
931
932 a_num_str.push(c);
933 a_iter.next();
934 }
935
936 while let Some(&c) = b_iter.peek() {
937 if !c.is_ascii_digit() {
938 break;
939 }
940
941 b_num_str.push(c);
942 b_iter.next();
943 }
944
945 match a_num_str.len().cmp(&b_num_str.len()) {
947 Ordering::Equal => {
948 match a_num_str.cmp(&b_num_str) {
950 Ordering::Equal => Ordering::Equal,
951 ordering => ordering,
952 }
953 }
954
955 ordering => {
957 if let (Ok(a_val), Ok(b_val)) = (a_num_str.parse::<u128>(), b_num_str.parse::<u128>()) {
959 match a_val.cmp(&b_val) {
960 Ordering::Equal => ordering, ord => ord,
962 }
963 } else {
964 a_num_str.cmp(&b_num_str)
966 }
967 }
968 }
969}
970
971pub fn natural_sort(a: &str, b: &str) -> Ordering {
993 let mut a_iter = a.chars().peekable();
994 let mut b_iter = b.chars().peekable();
995
996 loop {
997 match (a_iter.peek(), b_iter.peek()) {
998 (None, None) => {
999 return b.cmp(a);
1000 }
1001 (None, _) => return Ordering::Less,
1002 (_, None) => return Ordering::Greater,
1003 (Some(&a_char), Some(&b_char)) => {
1004 if a_char.is_ascii_digit() && b_char.is_ascii_digit() {
1005 match compare_numeric_segments(&mut a_iter, &mut b_iter) {
1006 Ordering::Equal => continue,
1007 ordering => return ordering,
1008 }
1009 } else {
1010 match a_char
1011 .to_ascii_lowercase()
1012 .cmp(&b_char.to_ascii_lowercase())
1013 {
1014 Ordering::Equal => {
1015 a_iter.next();
1016 b_iter.next();
1017 }
1018 ordering => return ordering,
1019 }
1020 }
1021 }
1022 }
1023 }
1024}
1025
1026fn natural_sort_no_tiebreak(a: &str, b: &str) -> Ordering {
1030 if a.eq_ignore_ascii_case(b) {
1031 Ordering::Equal
1032 } else {
1033 natural_sort(a, b)
1034 }
1035}
1036
1037fn stem_and_extension(filename: &str) -> (Option<&str>, Option<&str>) {
1038 if filename.is_empty() {
1039 return (None, None);
1040 }
1041
1042 match filename.rsplit_once('.') {
1043 None => (Some(filename), None),
1045
1046 Some((before, after)) => {
1048 if before.is_empty() {
1052 (Some(filename), None)
1053 } else {
1054 (Some(before), Some(after))
1056 }
1057 }
1058 }
1059}
1060
1061#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
1063pub enum SortOrder {
1064 #[default]
1067 Default,
1068 Upper,
1071 Lower,
1074 Unicode,
1077}
1078
1079#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
1081pub enum SortMode {
1082 #[default]
1084 DirectoriesFirst,
1085 Mixed,
1087 FilesFirst,
1089}
1090
1091fn case_group_key(name: &str, order: SortOrder) -> u8 {
1092 let first = match name.chars().next() {
1093 Some(c) => c,
1094 None => return 0,
1095 };
1096 match order {
1097 SortOrder::Upper => {
1098 if first.is_lowercase() {
1099 1
1100 } else {
1101 0
1102 }
1103 }
1104 SortOrder::Lower => {
1105 if first.is_uppercase() {
1106 1
1107 } else {
1108 0
1109 }
1110 }
1111 _ => 0,
1112 }
1113}
1114
1115fn compare_strings(a: &str, b: &str, order: SortOrder) -> Ordering {
1116 match order {
1117 SortOrder::Unicode => a.cmp(b),
1118 _ => natural_sort(a, b),
1119 }
1120}
1121
1122fn compare_strings_no_tiebreak(a: &str, b: &str, order: SortOrder) -> Ordering {
1123 match order {
1124 SortOrder::Unicode => a.cmp(b),
1125 _ => natural_sort_no_tiebreak(a, b),
1126 }
1127}
1128
1129pub fn compare_rel_paths(
1130 (path_a, a_is_file): (&RelPath, bool),
1131 (path_b, b_is_file): (&RelPath, bool),
1132) -> Ordering {
1133 compare_rel_paths_by(
1134 (path_a, a_is_file),
1135 (path_b, b_is_file),
1136 SortMode::DirectoriesFirst,
1137 SortOrder::Default,
1138 )
1139}
1140
1141pub fn compare_rel_paths_by(
1142 (path_a, a_is_file): (&RelPath, bool),
1143 (path_b, b_is_file): (&RelPath, bool),
1144 mode: SortMode,
1145 order: SortOrder,
1146) -> Ordering {
1147 let needs_final_tiebreak =
1148 mode != SortMode::DirectoriesFirst && !(std::ptr::eq(path_a, path_b) || path_a == path_b);
1149
1150 let mut components_a = path_a.components();
1151 let mut components_b = path_b.components();
1152
1153 loop {
1154 match (components_a.next(), components_b.next()) {
1155 (Some(component_a), Some(component_b)) => {
1156 let a_leaf_file = a_is_file && components_a.rest().is_empty();
1157 let b_leaf_file = b_is_file && components_b.rest().is_empty();
1158
1159 let file_dir_ordering = match mode {
1160 SortMode::DirectoriesFirst => a_leaf_file.cmp(&b_leaf_file),
1161 SortMode::FilesFirst => b_leaf_file.cmp(&a_leaf_file),
1162 SortMode::Mixed => Ordering::Equal,
1163 };
1164
1165 if !file_dir_ordering.is_eq() {
1166 return file_dir_ordering;
1167 }
1168
1169 let (a_stem, a_ext) = a_leaf_file
1170 .then(|| stem_and_extension(component_a))
1171 .unwrap_or_default();
1172 let (b_stem, b_ext) = b_leaf_file
1173 .then(|| stem_and_extension(component_b))
1174 .unwrap_or_default();
1175 let a_key = if a_leaf_file {
1176 a_stem
1177 } else {
1178 Some(component_a)
1179 };
1180 let b_key = if b_leaf_file {
1181 b_stem
1182 } else {
1183 Some(component_b)
1184 };
1185
1186 let ordering = match (a_key, b_key) {
1187 (Some(a), Some(b)) => {
1188 let name_cmp = case_group_key(a, order)
1189 .cmp(&case_group_key(b, order))
1190 .then_with(|| match mode {
1191 SortMode::DirectoriesFirst => compare_strings(a, b, order),
1192 _ => compare_strings_no_tiebreak(a, b, order),
1193 });
1194
1195 let name_cmp = if mode == SortMode::Mixed {
1196 name_cmp.then_with(|| match (a_leaf_file, b_leaf_file) {
1197 (true, false) if a.eq_ignore_ascii_case(b) => Ordering::Greater,
1198 (false, true) if a.eq_ignore_ascii_case(b) => Ordering::Less,
1199 _ => Ordering::Equal,
1200 })
1201 } else {
1202 name_cmp
1203 };
1204
1205 name_cmp.then_with(|| {
1206 if a_leaf_file && b_leaf_file {
1207 match order {
1208 SortOrder::Unicode => {
1209 a_ext.unwrap_or_default().cmp(b_ext.unwrap_or_default())
1210 }
1211 _ => {
1212 let a_ext_str = a_ext.unwrap_or_default().to_lowercase();
1213 let b_ext_str = b_ext.unwrap_or_default().to_lowercase();
1214 a_ext_str.cmp(&b_ext_str)
1215 }
1216 }
1217 } else {
1218 Ordering::Equal
1219 }
1220 })
1221 }
1222 (Some(_), None) => Ordering::Greater,
1223 (None, Some(_)) => Ordering::Less,
1224 (None, None) => Ordering::Equal,
1225 };
1226
1227 if !ordering.is_eq() {
1228 return ordering;
1229 }
1230 }
1231 (Some(_), None) => return Ordering::Greater,
1232 (None, Some(_)) => return Ordering::Less,
1233 (None, None) => {
1234 if needs_final_tiebreak {
1235 return compare_strings(path_a.as_unix_str(), path_b.as_unix_str(), order);
1236 }
1237 return Ordering::Equal;
1238 }
1239 }
1240 }
1241}
1242
1243pub fn compare_paths(
1244 (path_a, a_is_file): (&Path, bool),
1245 (path_b, b_is_file): (&Path, bool),
1246) -> Ordering {
1247 let mut components_a = path_a.components().peekable();
1248 let mut components_b = path_b.components().peekable();
1249
1250 loop {
1251 match (components_a.next(), components_b.next()) {
1252 (Some(component_a), Some(component_b)) => {
1253 let a_is_file = components_a.peek().is_none() && a_is_file;
1254 let b_is_file = components_b.peek().is_none() && b_is_file;
1255
1256 let ordering = a_is_file.cmp(&b_is_file).then_with(|| {
1257 let path_a = Path::new(component_a.as_os_str());
1258 let path_string_a = if a_is_file {
1259 path_a.file_stem()
1260 } else {
1261 path_a.file_name()
1262 }
1263 .map(|s| s.to_string_lossy());
1264
1265 let path_b = Path::new(component_b.as_os_str());
1266 let path_string_b = if b_is_file {
1267 path_b.file_stem()
1268 } else {
1269 path_b.file_name()
1270 }
1271 .map(|s| s.to_string_lossy());
1272
1273 let compare_components = match (path_string_a, path_string_b) {
1274 (Some(a), Some(b)) => natural_sort(&a, &b),
1275 (Some(_), None) => Ordering::Greater,
1276 (None, Some(_)) => Ordering::Less,
1277 (None, None) => Ordering::Equal,
1278 };
1279
1280 compare_components.then_with(|| {
1281 if a_is_file && b_is_file {
1282 let ext_a = path_a.extension().unwrap_or_default();
1283 let ext_b = path_b.extension().unwrap_or_default();
1284 ext_a.cmp(ext_b)
1285 } else {
1286 Ordering::Equal
1287 }
1288 })
1289 });
1290
1291 if !ordering.is_eq() {
1292 return ordering;
1293 }
1294 }
1295 (Some(_), None) => break Ordering::Greater,
1296 (None, Some(_)) => break Ordering::Less,
1297 (None, None) => break Ordering::Equal,
1298 }
1299 }
1300}
1301
1302#[derive(Debug, Clone, PartialEq, Eq)]
1303pub struct WslPath {
1304 pub distro: String,
1305
1306 pub path: std::ffi::OsString,
1310}
1311
1312impl WslPath {
1313 pub fn from_path<P: AsRef<Path>>(path: P) -> Option<WslPath> {
1314 if cfg!(not(target_os = "windows")) {
1315 return None;
1316 }
1317 use std::{
1318 ffi::OsString,
1319 path::{Component, Prefix},
1320 };
1321
1322 let mut components = path.as_ref().components();
1323 let Some(Component::Prefix(prefix)) = components.next() else {
1324 return None;
1325 };
1326 let (server, distro) = match prefix.kind() {
1327 Prefix::UNC(server, distro) => (server, distro),
1328 Prefix::VerbatimUNC(server, distro) => (server, distro),
1329 _ => return None,
1330 };
1331 let Some(Component::RootDir) = components.next() else {
1332 return None;
1333 };
1334
1335 let server_str = server.to_string_lossy();
1336 if server_str == "wsl.localhost" || server_str == "wsl$" {
1337 let mut result = OsString::from("");
1338 for c in components {
1339 use Component::*;
1340 match c {
1341 Prefix(p) => unreachable!("got {p:?}, but already stripped prefix"),
1342 RootDir => unreachable!("got root dir, but already stripped root"),
1343 CurDir => continue,
1344 ParentDir => result.push("/.."),
1345 Normal(s) => {
1346 result.push("/");
1347 result.push(s);
1348 }
1349 }
1350 }
1351 if result.is_empty() {
1352 result.push("/");
1353 }
1354 Some(WslPath {
1355 distro: distro.to_string_lossy().to_string(),
1356 path: result,
1357 })
1358 } else {
1359 None
1360 }
1361 }
1362}
1363
1364pub trait UrlExt {
1365 fn to_file_path_ext(&self, path_style: PathStyle) -> Result<PathBuf, ()>;
1369}
1370
1371impl UrlExt for url::Url {
1372 fn to_file_path_ext(&self, source_path_style: PathStyle) -> Result<PathBuf, ()> {
1374 if let Some(segments) = self.path_segments() {
1375 let host = match self.host() {
1376 None | Some(url::Host::Domain("localhost")) => None,
1377 Some(_) if source_path_style.is_windows() && self.scheme() == "file" => {
1378 self.host_str()
1379 }
1380 _ => return Err(()),
1381 };
1382
1383 let str_len = self.as_str().len();
1384 let estimated_capacity = if source_path_style.is_windows() {
1385 str_len.saturating_sub(self.scheme().len() + 1)
1387 } else {
1388 str_len.saturating_sub(self.scheme().len() + 3)
1390 };
1391 return match source_path_style {
1392 PathStyle::Unix => {
1393 file_url_segments_to_pathbuf_posix(estimated_capacity, host, segments)
1394 }
1395 PathStyle::Windows => {
1396 file_url_segments_to_pathbuf_windows(estimated_capacity, host, segments)
1397 }
1398 };
1399 }
1400
1401 fn file_url_segments_to_pathbuf_posix(
1402 estimated_capacity: usize,
1403 host: Option<&str>,
1404 segments: std::str::Split<'_, char>,
1405 ) -> Result<PathBuf, ()> {
1406 use percent_encoding::percent_decode;
1407
1408 if host.is_some() {
1409 return Err(());
1410 }
1411
1412 let mut bytes = Vec::new();
1413 bytes.try_reserve(estimated_capacity).map_err(|_| ())?;
1414
1415 for segment in segments {
1416 bytes.push(b'/');
1417 bytes.extend(percent_decode(segment.as_bytes()));
1418 }
1419
1420 if bytes.len() > 2
1422 && bytes[bytes.len() - 2].is_ascii_alphabetic()
1423 && matches!(bytes[bytes.len() - 1], b':' | b'|')
1424 {
1425 bytes.push(b'/');
1426 }
1427
1428 let path = String::from_utf8(bytes).map_err(|_| ())?;
1429 debug_assert!(
1430 PathStyle::Unix.is_absolute(&path),
1431 "to_file_path() failed to produce an absolute Path"
1432 );
1433
1434 Ok(PathBuf::from(path))
1435 }
1436
1437 fn file_url_segments_to_pathbuf_windows(
1438 estimated_capacity: usize,
1439 host: Option<&str>,
1440 mut segments: std::str::Split<'_, char>,
1441 ) -> Result<PathBuf, ()> {
1442 use percent_encoding::percent_decode_str;
1443 let mut string = String::new();
1444 string.try_reserve(estimated_capacity).map_err(|_| ())?;
1445 if let Some(host) = host {
1446 string.push_str(r"\\");
1447 string.push_str(host);
1448 } else {
1449 let first = segments.next().ok_or(())?;
1450
1451 match first.len() {
1452 2 => {
1453 if !first.starts_with(|c| char::is_ascii_alphabetic(&c))
1454 || first.as_bytes()[1] != b':'
1455 {
1456 return Err(());
1457 }
1458
1459 string.push_str(first);
1460 }
1461
1462 4 => {
1463 if !first.starts_with(|c| char::is_ascii_alphabetic(&c)) {
1464 return Err(());
1465 }
1466 let bytes = first.as_bytes();
1467 if bytes[1] != b'%'
1468 || bytes[2] != b'3'
1469 || (bytes[3] != b'a' && bytes[3] != b'A')
1470 {
1471 return Err(());
1472 }
1473
1474 string.push_str(&first[0..1]);
1475 string.push(':');
1476 }
1477
1478 _ => return Err(()),
1479 }
1480 };
1481
1482 for segment in segments {
1483 string.push('\\');
1484
1485 match percent_decode_str(segment).decode_utf8() {
1487 Ok(s) => string.push_str(&s),
1488 Err(..) => return Err(()),
1489 }
1490 }
1491 if cfg!(test) {
1493 debug_assert!(
1494 string.len() <= estimated_capacity,
1495 "len: {}, capacity: {}",
1496 string.len(),
1497 estimated_capacity
1498 );
1499 }
1500 debug_assert!(
1501 PathStyle::Windows.is_absolute(&string),
1502 "to_file_path() failed to produce an absolute Path"
1503 );
1504 let path = PathBuf::from(string);
1505 Ok(path)
1506 }
1507 Err(())
1508 }
1509}
1510
1511#[cfg(test)]
1512mod tests {
1513 use super::*;
1514 use util_macros::perf;
1515
1516 #[test]
1517 fn test_lenient_path_matcher() {
1518 for path_style in [PathStyle::Unix, PathStyle::Windows] {
1519 let patterns = ["**/.git", "[", "target/**", "{"];
1520 let mut errors = Vec::new();
1521 let matcher =
1522 PathMatcher::new_lenient(patterns, path_style, |error| errors.push(error));
1523 let expected = PathMatcher::new(["**/.git", "target/**"], path_style).unwrap();
1524 assert_eq!(matcher, expected);
1525 assert_eq!(errors.len(), 2);
1526 assert_eq!(errors[0].glob(), Some("["));
1527 assert_eq!(errors[1].glob(), Some("{"));
1528 for path in ["nested/.git", "src/file.rs", "target/file.rs"] {
1529 let path = RelPath::from_unix_str(path).unwrap();
1530 assert_eq!(matcher.is_match(path), expected.is_match(path));
1531 }
1532 if path_style == PathStyle::local() {
1533 assert!(matcher.is_match(RelPath::from_unix_str("nested/.git").unwrap()));
1534 assert!(matcher.is_match(RelPath::from_unix_str("target/file.rs").unwrap()));
1535 }
1536 assert!(PathMatcher::new(patterns, path_style).is_err());
1537
1538 let mut errors = Vec::new();
1539 let matcher =
1540 PathMatcher::new_lenient(["[", "{"], path_style, |error| errors.push(error));
1541 assert_eq!(errors.len(), 2);
1542 assert_eq!(matcher.path_style, path_style);
1543 assert_eq!(matcher.sources().count(), 0);
1544 assert!(!matcher.is_match(RelPath::from_unix_str("file.rs").unwrap()));
1545 assert!(!matcher.is_match_std_path("file.rs"));
1546
1547 let matcher = PathMatcher::new_lenient([] as [&str; 0], path_style, |_| {
1548 panic!("empty patterns are valid")
1549 });
1550 assert_eq!(matcher.path_style, path_style);
1551 assert_eq!(matcher.sources().count(), 0);
1552 assert!(!matcher.is_match(RelPath::from_unix_str("file.rs").unwrap()));
1553 }
1554 }
1555
1556 #[test]
1557 fn test_lenient_path_matcher_preserves_escaping() {
1558 for path_style in [PathStyle::Unix, PathStyle::Windows] {
1559 let patterns = [r"directory\file.rs", r"literal\*", r"literal\[name]"];
1560 let strict = PathMatcher::new(patterns, path_style).unwrap();
1561 let lenient = PathMatcher::new_lenient(patterns, path_style, |_| {
1562 panic!("valid patterns are preserved")
1563 });
1564 assert_eq!(lenient, strict);
1565 for path in [
1566 "directory/file.rs",
1567 "literal*",
1568 "literal/file.rs",
1569 "literal[name]",
1570 ] {
1571 let path = RelPath::from_unix_str(path).unwrap();
1572 assert_eq!(lenient.is_match(path), strict.is_match(path));
1573 }
1574 }
1575 }
1576
1577 #[test]
1578 fn test_parse_str_treats_paren_suffix_as_position() {
1579 let parsed = PathWithPosition::parse_str("/root/Test (3)");
1583 assert_eq!(parsed.path, PathBuf::from("/root/Test "));
1584 assert_eq!(parsed.row, Some(3));
1585 }
1586
1587 fn rel_path_entry(path: &'static str, is_file: bool) -> (&'static RelPath, bool) {
1588 (RelPath::from_unix_str(path).unwrap(), is_file)
1589 }
1590
1591 fn sorted_rel_paths(
1592 mut paths: Vec<(&'static RelPath, bool)>,
1593 mode: SortMode,
1594 order: SortOrder,
1595 ) -> Vec<(&'static RelPath, bool)> {
1596 paths.sort_by(|&a, &b| compare_rel_paths_by(a, b, mode, order));
1597 paths
1598 }
1599
1600 #[perf]
1601 fn compare_paths_with_dots() {
1602 let mut paths = vec![
1603 (Path::new("test_dirs"), false),
1604 (Path::new("test_dirs/1.46"), false),
1605 (Path::new("test_dirs/1.46/bar_1"), true),
1606 (Path::new("test_dirs/1.46/bar_2"), true),
1607 (Path::new("test_dirs/1.45"), false),
1608 (Path::new("test_dirs/1.45/foo_2"), true),
1609 (Path::new("test_dirs/1.45/foo_1"), true),
1610 ];
1611 paths.sort_by(|&a, &b| compare_paths(a, b));
1612 assert_eq!(
1613 paths,
1614 vec![
1615 (Path::new("test_dirs"), false),
1616 (Path::new("test_dirs/1.45"), false),
1617 (Path::new("test_dirs/1.45/foo_1"), true),
1618 (Path::new("test_dirs/1.45/foo_2"), true),
1619 (Path::new("test_dirs/1.46"), false),
1620 (Path::new("test_dirs/1.46/bar_1"), true),
1621 (Path::new("test_dirs/1.46/bar_2"), true),
1622 ]
1623 );
1624 let mut paths = vec![
1625 (Path::new("root1/one.txt"), true),
1626 (Path::new("root1/one.two.txt"), true),
1627 ];
1628 paths.sort_by(|&a, &b| compare_paths(a, b));
1629 assert_eq!(
1630 paths,
1631 vec![
1632 (Path::new("root1/one.txt"), true),
1633 (Path::new("root1/one.two.txt"), true),
1634 ]
1635 );
1636 }
1637
1638 #[perf]
1639 fn compare_paths_with_same_name_different_extensions() {
1640 let mut paths = vec![
1641 (Path::new("test_dirs/file.rs"), true),
1642 (Path::new("test_dirs/file.txt"), true),
1643 (Path::new("test_dirs/file.md"), true),
1644 (Path::new("test_dirs/file"), true),
1645 (Path::new("test_dirs/file.a"), true),
1646 ];
1647 paths.sort_by(|&a, &b| compare_paths(a, b));
1648 assert_eq!(
1649 paths,
1650 vec![
1651 (Path::new("test_dirs/file"), true),
1652 (Path::new("test_dirs/file.a"), true),
1653 (Path::new("test_dirs/file.md"), true),
1654 (Path::new("test_dirs/file.rs"), true),
1655 (Path::new("test_dirs/file.txt"), true),
1656 ]
1657 );
1658 }
1659
1660 #[perf]
1661 fn compare_paths_case_semi_sensitive() {
1662 let mut paths = vec![
1663 (Path::new("test_DIRS"), false),
1664 (Path::new("test_DIRS/foo_1"), true),
1665 (Path::new("test_DIRS/foo_2"), true),
1666 (Path::new("test_DIRS/bar"), true),
1667 (Path::new("test_DIRS/BAR"), true),
1668 (Path::new("test_dirs"), false),
1669 (Path::new("test_dirs/foo_1"), true),
1670 (Path::new("test_dirs/foo_2"), true),
1671 (Path::new("test_dirs/bar"), true),
1672 (Path::new("test_dirs/BAR"), true),
1673 ];
1674 paths.sort_by(|&a, &b| compare_paths(a, b));
1675 assert_eq!(
1676 paths,
1677 vec![
1678 (Path::new("test_dirs"), false),
1679 (Path::new("test_dirs/bar"), true),
1680 (Path::new("test_dirs/BAR"), true),
1681 (Path::new("test_dirs/foo_1"), true),
1682 (Path::new("test_dirs/foo_2"), true),
1683 (Path::new("test_DIRS"), false),
1684 (Path::new("test_DIRS/bar"), true),
1685 (Path::new("test_DIRS/BAR"), true),
1686 (Path::new("test_DIRS/foo_1"), true),
1687 (Path::new("test_DIRS/foo_2"), true),
1688 ]
1689 );
1690 }
1691
1692 #[perf]
1693 fn compare_paths_mixed_case_numeric_ordering() {
1694 let mut entries = [
1695 (Path::new(".config"), false),
1696 (Path::new("Dir1"), false),
1697 (Path::new("dir01"), false),
1698 (Path::new("dir2"), false),
1699 (Path::new("Dir02"), false),
1700 (Path::new("dir10"), false),
1701 (Path::new("Dir10"), false),
1702 ];
1703
1704 entries.sort_by(|&a, &b| compare_paths(a, b));
1705
1706 let ordered: Vec<&str> = entries
1707 .iter()
1708 .map(|(path, _)| path.to_str().unwrap())
1709 .collect();
1710
1711 assert_eq!(
1712 ordered,
1713 vec![
1714 ".config", "Dir1", "dir01", "dir2", "Dir02", "dir10", "Dir10"
1715 ]
1716 );
1717 }
1718
1719 #[perf]
1720 fn compare_rel_paths_mixed_case_insensitive() {
1721 let mut paths = vec![
1723 (RelPath::from_unix_str("zebra.txt").unwrap(), true),
1724 (RelPath::from_unix_str("Apple").unwrap(), false),
1725 (RelPath::from_unix_str("banana.rs").unwrap(), true),
1726 (RelPath::from_unix_str("Carrot").unwrap(), false),
1727 (RelPath::from_unix_str("aardvark.txt").unwrap(), true),
1728 ];
1729 paths.sort_by(|&a, &b| compare_rel_paths_by(a, b, SortMode::Mixed, SortOrder::Default));
1730 assert_eq!(
1732 paths,
1733 vec![
1734 (RelPath::from_unix_str("aardvark.txt").unwrap(), true),
1735 (RelPath::from_unix_str("Apple").unwrap(), false),
1736 (RelPath::from_unix_str("banana.rs").unwrap(), true),
1737 (RelPath::from_unix_str("Carrot").unwrap(), false),
1738 (RelPath::from_unix_str("zebra.txt").unwrap(), true),
1739 ]
1740 );
1741 }
1742
1743 #[perf]
1744 fn compare_rel_paths_files_first_basic() {
1745 let mut paths = vec![
1747 (RelPath::from_unix_str("zebra.txt").unwrap(), true),
1748 (RelPath::from_unix_str("Apple").unwrap(), false),
1749 (RelPath::from_unix_str("banana.rs").unwrap(), true),
1750 (RelPath::from_unix_str("Carrot").unwrap(), false),
1751 (RelPath::from_unix_str("aardvark.txt").unwrap(), true),
1752 ];
1753 paths
1754 .sort_by(|&a, &b| compare_rel_paths_by(a, b, SortMode::FilesFirst, SortOrder::Default));
1755 assert_eq!(
1757 paths,
1758 vec![
1759 (RelPath::from_unix_str("aardvark.txt").unwrap(), true),
1760 (RelPath::from_unix_str("banana.rs").unwrap(), true),
1761 (RelPath::from_unix_str("zebra.txt").unwrap(), true),
1762 (RelPath::from_unix_str("Apple").unwrap(), false),
1763 (RelPath::from_unix_str("Carrot").unwrap(), false),
1764 ]
1765 );
1766 }
1767
1768 #[perf]
1769 fn compare_rel_paths_files_first_case_insensitive() {
1770 let mut paths = vec![
1772 (RelPath::from_unix_str("Zebra.txt").unwrap(), true),
1773 (RelPath::from_unix_str("apple").unwrap(), false),
1774 (RelPath::from_unix_str("Banana.rs").unwrap(), true),
1775 (RelPath::from_unix_str("carrot").unwrap(), false),
1776 (RelPath::from_unix_str("Aardvark.txt").unwrap(), true),
1777 ];
1778 paths
1779 .sort_by(|&a, &b| compare_rel_paths_by(a, b, SortMode::FilesFirst, SortOrder::Default));
1780 assert_eq!(
1781 paths,
1782 vec![
1783 (RelPath::from_unix_str("Aardvark.txt").unwrap(), true),
1784 (RelPath::from_unix_str("Banana.rs").unwrap(), true),
1785 (RelPath::from_unix_str("Zebra.txt").unwrap(), true),
1786 (RelPath::from_unix_str("apple").unwrap(), false),
1787 (RelPath::from_unix_str("carrot").unwrap(), false),
1788 ]
1789 );
1790 }
1791
1792 #[perf]
1793 fn compare_rel_paths_files_first_numeric() {
1794 let mut paths = vec![
1796 (RelPath::from_unix_str("file10.txt").unwrap(), true),
1797 (RelPath::from_unix_str("dir2").unwrap(), false),
1798 (RelPath::from_unix_str("file2.txt").unwrap(), true),
1799 (RelPath::from_unix_str("dir10").unwrap(), false),
1800 (RelPath::from_unix_str("file1.txt").unwrap(), true),
1801 ];
1802 paths
1803 .sort_by(|&a, &b| compare_rel_paths_by(a, b, SortMode::FilesFirst, SortOrder::Default));
1804 assert_eq!(
1805 paths,
1806 vec![
1807 (RelPath::from_unix_str("file1.txt").unwrap(), true),
1808 (RelPath::from_unix_str("file2.txt").unwrap(), true),
1809 (RelPath::from_unix_str("file10.txt").unwrap(), true),
1810 (RelPath::from_unix_str("dir2").unwrap(), false),
1811 (RelPath::from_unix_str("dir10").unwrap(), false),
1812 ]
1813 );
1814 }
1815
1816 #[perf]
1817 fn compare_rel_paths_mixed_case() {
1818 let mut paths = vec![
1820 (RelPath::from_unix_str("README.md").unwrap(), true),
1821 (RelPath::from_unix_str("readme.txt").unwrap(), true),
1822 (RelPath::from_unix_str("ReadMe.rs").unwrap(), true),
1823 ];
1824 paths.sort_by(|&a, &b| compare_rel_paths_by(a, b, SortMode::Mixed, SortOrder::Default));
1825 assert_eq!(
1827 paths,
1828 vec![
1829 (RelPath::from_unix_str("README.md").unwrap(), true),
1830 (RelPath::from_unix_str("ReadMe.rs").unwrap(), true),
1831 (RelPath::from_unix_str("readme.txt").unwrap(), true),
1832 ]
1833 );
1834 }
1835
1836 #[perf]
1837 fn compare_rel_paths_mixed_files_and_dirs() {
1838 let mut paths = vec![
1840 (RelPath::from_unix_str("file2.txt").unwrap(), true),
1841 (RelPath::from_unix_str("Dir1").unwrap(), false),
1842 (RelPath::from_unix_str("file1.txt").unwrap(), true),
1843 (RelPath::from_unix_str("dir2").unwrap(), false),
1844 ];
1845 paths.sort_by(|&a, &b| compare_rel_paths_by(a, b, SortMode::Mixed, SortOrder::Default));
1846 assert_eq!(
1848 paths,
1849 vec![
1850 (RelPath::from_unix_str("Dir1").unwrap(), false),
1851 (RelPath::from_unix_str("dir2").unwrap(), false),
1852 (RelPath::from_unix_str("file1.txt").unwrap(), true),
1853 (RelPath::from_unix_str("file2.txt").unwrap(), true),
1854 ]
1855 );
1856 }
1857
1858 #[perf]
1859 fn compare_rel_paths_mixed_same_name_different_case_file_and_dir() {
1860 let mut paths = vec![
1861 (RelPath::from_unix_str("Hello.txt").unwrap(), true),
1862 (RelPath::from_unix_str("hello").unwrap(), false),
1863 ];
1864 paths.sort_by(|&a, &b| compare_rel_paths_by(a, b, SortMode::Mixed, SortOrder::Default));
1865 assert_eq!(
1866 paths,
1867 vec![
1868 (RelPath::from_unix_str("hello").unwrap(), false),
1869 (RelPath::from_unix_str("Hello.txt").unwrap(), true),
1870 ]
1871 );
1872
1873 let mut paths = vec![
1874 (RelPath::from_unix_str("hello").unwrap(), false),
1875 (RelPath::from_unix_str("Hello.txt").unwrap(), true),
1876 ];
1877 paths.sort_by(|&a, &b| compare_rel_paths_by(a, b, SortMode::Mixed, SortOrder::Default));
1878 assert_eq!(
1879 paths,
1880 vec![
1881 (RelPath::from_unix_str("hello").unwrap(), false),
1882 (RelPath::from_unix_str("Hello.txt").unwrap(), true),
1883 ]
1884 );
1885 }
1886
1887 #[perf]
1888 fn compare_rel_paths_mixed_with_nested_paths() {
1889 let mut paths = vec![
1891 (RelPath::from_unix_str("src/main.rs").unwrap(), true),
1892 (RelPath::from_unix_str("Cargo.toml").unwrap(), true),
1893 (RelPath::from_unix_str("src").unwrap(), false),
1894 (RelPath::from_unix_str("target").unwrap(), false),
1895 ];
1896 paths.sort_by(|&a, &b| compare_rel_paths_by(a, b, SortMode::Mixed, SortOrder::Default));
1897 assert_eq!(
1898 paths,
1899 vec![
1900 (RelPath::from_unix_str("Cargo.toml").unwrap(), true),
1901 (RelPath::from_unix_str("src").unwrap(), false),
1902 (RelPath::from_unix_str("src/main.rs").unwrap(), true),
1903 (RelPath::from_unix_str("target").unwrap(), false),
1904 ]
1905 );
1906 }
1907
1908 #[perf]
1909 fn compare_rel_paths_files_first_with_nested() {
1910 let mut paths = vec![
1912 (RelPath::from_unix_str("src/lib.rs").unwrap(), true),
1913 (RelPath::from_unix_str("README.md").unwrap(), true),
1914 (RelPath::from_unix_str("src").unwrap(), false),
1915 (RelPath::from_unix_str("tests").unwrap(), false),
1916 ];
1917 paths
1918 .sort_by(|&a, &b| compare_rel_paths_by(a, b, SortMode::FilesFirst, SortOrder::Default));
1919 assert_eq!(
1920 paths,
1921 vec![
1922 (RelPath::from_unix_str("README.md").unwrap(), true),
1923 (RelPath::from_unix_str("src").unwrap(), false),
1924 (RelPath::from_unix_str("src/lib.rs").unwrap(), true),
1925 (RelPath::from_unix_str("tests").unwrap(), false),
1926 ]
1927 );
1928 }
1929
1930 #[perf]
1931 fn compare_rel_paths_mixed_dotfiles() {
1932 let mut paths = vec![
1934 (RelPath::from_unix_str(".gitignore").unwrap(), true),
1935 (RelPath::from_unix_str("README.md").unwrap(), true),
1936 (RelPath::from_unix_str(".github").unwrap(), false),
1937 (RelPath::from_unix_str("src").unwrap(), false),
1938 ];
1939 paths.sort_by(|&a, &b| compare_rel_paths_by(a, b, SortMode::Mixed, SortOrder::Default));
1940 assert_eq!(
1941 paths,
1942 vec![
1943 (RelPath::from_unix_str(".github").unwrap(), false),
1944 (RelPath::from_unix_str(".gitignore").unwrap(), true),
1945 (RelPath::from_unix_str("README.md").unwrap(), true),
1946 (RelPath::from_unix_str("src").unwrap(), false),
1947 ]
1948 );
1949 }
1950
1951 #[perf]
1952 fn compare_rel_paths_files_first_dotfiles() {
1953 let mut paths = vec![
1955 (RelPath::from_unix_str(".gitignore").unwrap(), true),
1956 (RelPath::from_unix_str("README.md").unwrap(), true),
1957 (RelPath::from_unix_str(".github").unwrap(), false),
1958 (RelPath::from_unix_str("src").unwrap(), false),
1959 ];
1960 paths
1961 .sort_by(|&a, &b| compare_rel_paths_by(a, b, SortMode::FilesFirst, SortOrder::Default));
1962 assert_eq!(
1963 paths,
1964 vec![
1965 (RelPath::from_unix_str(".gitignore").unwrap(), true),
1966 (RelPath::from_unix_str("README.md").unwrap(), true),
1967 (RelPath::from_unix_str(".github").unwrap(), false),
1968 (RelPath::from_unix_str("src").unwrap(), false),
1969 ]
1970 );
1971 }
1972
1973 #[perf]
1974 fn compare_rel_paths_mixed_same_stem_different_extension() {
1975 let mut paths = vec![
1977 (RelPath::from_unix_str("file.rs").unwrap(), true),
1978 (RelPath::from_unix_str("file.md").unwrap(), true),
1979 (RelPath::from_unix_str("file.txt").unwrap(), true),
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::from_unix_str("file.md").unwrap(), true),
1986 (RelPath::from_unix_str("file.rs").unwrap(), true),
1987 (RelPath::from_unix_str("file.txt").unwrap(), true),
1988 ]
1989 );
1990 }
1991
1992 #[perf]
1993 fn compare_rel_paths_files_first_same_stem() {
1994 let mut paths = vec![
1996 (RelPath::from_unix_str("main.rs").unwrap(), true),
1997 (RelPath::from_unix_str("main.c").unwrap(), true),
1998 (RelPath::from_unix_str("main").unwrap(), false),
1999 ];
2000 paths
2001 .sort_by(|&a, &b| compare_rel_paths_by(a, b, SortMode::FilesFirst, SortOrder::Default));
2002 assert_eq!(
2003 paths,
2004 vec![
2005 (RelPath::from_unix_str("main.c").unwrap(), true),
2006 (RelPath::from_unix_str("main.rs").unwrap(), true),
2007 (RelPath::from_unix_str("main").unwrap(), false),
2008 ]
2009 );
2010 }
2011
2012 #[perf]
2013 fn compare_rel_paths_mixed_deep_nesting() {
2014 let mut paths = vec![
2016 (RelPath::from_unix_str("a/b/c.txt").unwrap(), true),
2017 (RelPath::from_unix_str("A/B.txt").unwrap(), true),
2018 (RelPath::from_unix_str("a.txt").unwrap(), true),
2019 (RelPath::from_unix_str("A.txt").unwrap(), true),
2020 ];
2021 paths.sort_by(|&a, &b| compare_rel_paths_by(a, b, SortMode::Mixed, SortOrder::Default));
2022 assert_eq!(
2023 paths,
2024 vec![
2025 (RelPath::from_unix_str("a/b/c.txt").unwrap(), true),
2026 (RelPath::from_unix_str("A/B.txt").unwrap(), true),
2027 (RelPath::from_unix_str("a.txt").unwrap(), true),
2028 (RelPath::from_unix_str("A.txt").unwrap(), true),
2029 ]
2030 );
2031 }
2032
2033 #[perf]
2034 fn compare_rel_paths_upper() {
2035 let directories_only_paths = vec![
2036 rel_path_entry("mixedCase", false),
2037 rel_path_entry("Zebra", false),
2038 rel_path_entry("banana", false),
2039 rel_path_entry("ALLCAPS", false),
2040 rel_path_entry("Apple", false),
2041 rel_path_entry("dog", false),
2042 rel_path_entry(".hidden", false),
2043 rel_path_entry("Carrot", false),
2044 ];
2045 assert_eq!(
2046 sorted_rel_paths(
2047 directories_only_paths,
2048 SortMode::DirectoriesFirst,
2049 SortOrder::Upper,
2050 ),
2051 vec![
2052 rel_path_entry(".hidden", false),
2053 rel_path_entry("ALLCAPS", false),
2054 rel_path_entry("Apple", false),
2055 rel_path_entry("Carrot", false),
2056 rel_path_entry("Zebra", false),
2057 rel_path_entry("banana", false),
2058 rel_path_entry("dog", false),
2059 rel_path_entry("mixedCase", false),
2060 ]
2061 );
2062
2063 let file_and_directory_paths = vec![
2064 rel_path_entry("banana", false),
2065 rel_path_entry("Apple.txt", true),
2066 rel_path_entry("dog.md", true),
2067 rel_path_entry("ALLCAPS", false),
2068 rel_path_entry("file1.txt", true),
2069 rel_path_entry("File2.txt", true),
2070 rel_path_entry(".hidden", false),
2071 ];
2072 assert_eq!(
2073 sorted_rel_paths(
2074 file_and_directory_paths.clone(),
2075 SortMode::DirectoriesFirst,
2076 SortOrder::Upper,
2077 ),
2078 vec![
2079 rel_path_entry(".hidden", false),
2080 rel_path_entry("ALLCAPS", false),
2081 rel_path_entry("banana", false),
2082 rel_path_entry("Apple.txt", true),
2083 rel_path_entry("File2.txt", true),
2084 rel_path_entry("dog.md", true),
2085 rel_path_entry("file1.txt", true),
2086 ]
2087 );
2088 assert_eq!(
2089 sorted_rel_paths(
2090 file_and_directory_paths.clone(),
2091 SortMode::Mixed,
2092 SortOrder::Upper,
2093 ),
2094 vec![
2095 rel_path_entry(".hidden", false),
2096 rel_path_entry("ALLCAPS", false),
2097 rel_path_entry("Apple.txt", true),
2098 rel_path_entry("File2.txt", true),
2099 rel_path_entry("banana", false),
2100 rel_path_entry("dog.md", true),
2101 rel_path_entry("file1.txt", true),
2102 ]
2103 );
2104 assert_eq!(
2105 sorted_rel_paths(
2106 file_and_directory_paths,
2107 SortMode::FilesFirst,
2108 SortOrder::Upper,
2109 ),
2110 vec![
2111 rel_path_entry("Apple.txt", true),
2112 rel_path_entry("File2.txt", true),
2113 rel_path_entry("dog.md", true),
2114 rel_path_entry("file1.txt", true),
2115 rel_path_entry(".hidden", false),
2116 rel_path_entry("ALLCAPS", false),
2117 rel_path_entry("banana", false),
2118 ]
2119 );
2120
2121 let natural_sort_paths = vec![
2122 rel_path_entry("file10.txt", true),
2123 rel_path_entry("file1.txt", true),
2124 rel_path_entry("file20.txt", true),
2125 rel_path_entry("file2.txt", true),
2126 ];
2127 assert_eq!(
2128 sorted_rel_paths(natural_sort_paths, SortMode::Mixed, SortOrder::Upper,),
2129 vec![
2130 rel_path_entry("file1.txt", true),
2131 rel_path_entry("file2.txt", true),
2132 rel_path_entry("file10.txt", true),
2133 rel_path_entry("file20.txt", true),
2134 ]
2135 );
2136
2137 let accented_paths = vec![
2138 rel_path_entry("\u{00C9}something.txt", true),
2139 rel_path_entry("zebra.txt", true),
2140 rel_path_entry("Apple.txt", true),
2141 ];
2142 assert_eq!(
2143 sorted_rel_paths(accented_paths, SortMode::Mixed, SortOrder::Upper),
2144 vec![
2145 rel_path_entry("Apple.txt", true),
2146 rel_path_entry("\u{00C9}something.txt", true),
2147 rel_path_entry("zebra.txt", true),
2148 ]
2149 );
2150 }
2151
2152 #[perf]
2153 fn compare_rel_paths_lower() {
2154 let directories_only_paths = vec![
2155 rel_path_entry("mixedCase", false),
2156 rel_path_entry("Zebra", false),
2157 rel_path_entry("banana", false),
2158 rel_path_entry("ALLCAPS", false),
2159 rel_path_entry("Apple", false),
2160 rel_path_entry("dog", false),
2161 rel_path_entry(".hidden", false),
2162 rel_path_entry("Carrot", false),
2163 ];
2164 assert_eq!(
2165 sorted_rel_paths(
2166 directories_only_paths,
2167 SortMode::DirectoriesFirst,
2168 SortOrder::Lower,
2169 ),
2170 vec![
2171 rel_path_entry(".hidden", false),
2172 rel_path_entry("banana", false),
2173 rel_path_entry("dog", false),
2174 rel_path_entry("mixedCase", false),
2175 rel_path_entry("ALLCAPS", false),
2176 rel_path_entry("Apple", false),
2177 rel_path_entry("Carrot", false),
2178 rel_path_entry("Zebra", false),
2179 ]
2180 );
2181
2182 let file_and_directory_paths = vec![
2183 rel_path_entry("banana", false),
2184 rel_path_entry("Apple.txt", true),
2185 rel_path_entry("dog.md", true),
2186 rel_path_entry("ALLCAPS", false),
2187 rel_path_entry("file1.txt", true),
2188 rel_path_entry("File2.txt", true),
2189 rel_path_entry(".hidden", false),
2190 ];
2191 assert_eq!(
2192 sorted_rel_paths(
2193 file_and_directory_paths.clone(),
2194 SortMode::DirectoriesFirst,
2195 SortOrder::Lower,
2196 ),
2197 vec![
2198 rel_path_entry(".hidden", false),
2199 rel_path_entry("banana", false),
2200 rel_path_entry("ALLCAPS", false),
2201 rel_path_entry("dog.md", true),
2202 rel_path_entry("file1.txt", true),
2203 rel_path_entry("Apple.txt", true),
2204 rel_path_entry("File2.txt", true),
2205 ]
2206 );
2207 assert_eq!(
2208 sorted_rel_paths(
2209 file_and_directory_paths.clone(),
2210 SortMode::Mixed,
2211 SortOrder::Lower,
2212 ),
2213 vec![
2214 rel_path_entry(".hidden", false),
2215 rel_path_entry("banana", false),
2216 rel_path_entry("dog.md", true),
2217 rel_path_entry("file1.txt", true),
2218 rel_path_entry("ALLCAPS", false),
2219 rel_path_entry("Apple.txt", true),
2220 rel_path_entry("File2.txt", true),
2221 ]
2222 );
2223 assert_eq!(
2224 sorted_rel_paths(
2225 file_and_directory_paths,
2226 SortMode::FilesFirst,
2227 SortOrder::Lower,
2228 ),
2229 vec![
2230 rel_path_entry("dog.md", true),
2231 rel_path_entry("file1.txt", true),
2232 rel_path_entry("Apple.txt", true),
2233 rel_path_entry("File2.txt", true),
2234 rel_path_entry(".hidden", false),
2235 rel_path_entry("banana", false),
2236 rel_path_entry("ALLCAPS", false),
2237 ]
2238 );
2239 }
2240
2241 #[perf]
2242 fn compare_rel_paths_unicode() {
2243 let directories_only_paths = vec![
2244 rel_path_entry("mixedCase", false),
2245 rel_path_entry("Zebra", false),
2246 rel_path_entry("banana", false),
2247 rel_path_entry("ALLCAPS", false),
2248 rel_path_entry("Apple", false),
2249 rel_path_entry("dog", false),
2250 rel_path_entry(".hidden", false),
2251 rel_path_entry("Carrot", false),
2252 ];
2253 assert_eq!(
2254 sorted_rel_paths(
2255 directories_only_paths,
2256 SortMode::DirectoriesFirst,
2257 SortOrder::Unicode,
2258 ),
2259 vec![
2260 rel_path_entry(".hidden", false),
2261 rel_path_entry("ALLCAPS", false),
2262 rel_path_entry("Apple", false),
2263 rel_path_entry("Carrot", false),
2264 rel_path_entry("Zebra", false),
2265 rel_path_entry("banana", false),
2266 rel_path_entry("dog", false),
2267 rel_path_entry("mixedCase", false),
2268 ]
2269 );
2270
2271 let file_and_directory_paths = vec![
2272 rel_path_entry("banana", false),
2273 rel_path_entry("Apple.txt", true),
2274 rel_path_entry("dog.md", true),
2275 rel_path_entry("ALLCAPS", false),
2276 rel_path_entry("file1.txt", true),
2277 rel_path_entry("File2.txt", true),
2278 rel_path_entry(".hidden", false),
2279 ];
2280 assert_eq!(
2281 sorted_rel_paths(
2282 file_and_directory_paths.clone(),
2283 SortMode::DirectoriesFirst,
2284 SortOrder::Unicode,
2285 ),
2286 vec![
2287 rel_path_entry(".hidden", false),
2288 rel_path_entry("ALLCAPS", false),
2289 rel_path_entry("banana", false),
2290 rel_path_entry("Apple.txt", true),
2291 rel_path_entry("File2.txt", true),
2292 rel_path_entry("dog.md", true),
2293 rel_path_entry("file1.txt", true),
2294 ]
2295 );
2296 assert_eq!(
2297 sorted_rel_paths(
2298 file_and_directory_paths.clone(),
2299 SortMode::Mixed,
2300 SortOrder::Unicode,
2301 ),
2302 vec![
2303 rel_path_entry(".hidden", false),
2304 rel_path_entry("ALLCAPS", false),
2305 rel_path_entry("Apple.txt", true),
2306 rel_path_entry("File2.txt", true),
2307 rel_path_entry("banana", false),
2308 rel_path_entry("dog.md", true),
2309 rel_path_entry("file1.txt", true),
2310 ]
2311 );
2312 assert_eq!(
2313 sorted_rel_paths(
2314 file_and_directory_paths,
2315 SortMode::FilesFirst,
2316 SortOrder::Unicode,
2317 ),
2318 vec![
2319 rel_path_entry("Apple.txt", true),
2320 rel_path_entry("File2.txt", true),
2321 rel_path_entry("dog.md", true),
2322 rel_path_entry("file1.txt", true),
2323 rel_path_entry(".hidden", false),
2324 rel_path_entry("ALLCAPS", false),
2325 rel_path_entry("banana", false),
2326 ]
2327 );
2328
2329 let numeric_paths = vec![
2330 rel_path_entry("file10.txt", true),
2331 rel_path_entry("file1.txt", true),
2332 rel_path_entry("file2.txt", true),
2333 rel_path_entry("file20.txt", true),
2334 ];
2335 assert_eq!(
2336 sorted_rel_paths(numeric_paths, SortMode::Mixed, SortOrder::Unicode,),
2337 vec![
2338 rel_path_entry("file1.txt", true),
2339 rel_path_entry("file10.txt", true),
2340 rel_path_entry("file2.txt", true),
2341 rel_path_entry("file20.txt", true),
2342 ]
2343 );
2344
2345 let accented_paths = vec![
2346 rel_path_entry("\u{00C9}something.txt", true),
2347 rel_path_entry("zebra.txt", true),
2348 rel_path_entry("Apple.txt", true),
2349 ];
2350 assert_eq!(
2351 sorted_rel_paths(accented_paths, SortMode::Mixed, SortOrder::Unicode),
2352 vec![
2353 rel_path_entry("Apple.txt", true),
2354 rel_path_entry("zebra.txt", true),
2355 rel_path_entry("\u{00C9}something.txt", true),
2356 ]
2357 );
2358 }
2359
2360 #[perf]
2361 fn path_with_position_parse_posix_path() {
2362 assert_eq!(
2365 PathWithPosition::parse_str("test_file"),
2366 PathWithPosition {
2367 path: PathBuf::from("test_file"),
2368 row: None,
2369 column: None
2370 }
2371 );
2372
2373 assert_eq!(
2374 PathWithPosition::parse_str("a:bc:.zip:1"),
2375 PathWithPosition {
2376 path: PathBuf::from("a:bc:.zip"),
2377 row: Some(1),
2378 column: None
2379 }
2380 );
2381
2382 assert_eq!(
2383 PathWithPosition::parse_str("one.second.zip:1"),
2384 PathWithPosition {
2385 path: PathBuf::from("one.second.zip"),
2386 row: Some(1),
2387 column: None
2388 }
2389 );
2390
2391 assert_eq!(
2393 PathWithPosition::parse_str("test_file:10:1:"),
2394 PathWithPosition {
2395 path: PathBuf::from("test_file"),
2396 row: Some(10),
2397 column: Some(1)
2398 }
2399 );
2400
2401 assert_eq!(
2402 PathWithPosition::parse_str("test_file.rs:"),
2403 PathWithPosition {
2404 path: PathBuf::from("test_file.rs"),
2405 row: None,
2406 column: None
2407 }
2408 );
2409
2410 assert_eq!(
2411 PathWithPosition::parse_str("test_file.rs:1:"),
2412 PathWithPosition {
2413 path: PathBuf::from("test_file.rs"),
2414 row: Some(1),
2415 column: None
2416 }
2417 );
2418
2419 assert_eq!(
2420 PathWithPosition::parse_str("ab\ncd"),
2421 PathWithPosition {
2422 path: PathBuf::from("ab\ncd"),
2423 row: None,
2424 column: None
2425 }
2426 );
2427
2428 assert_eq!(
2429 PathWithPosition::parse_str("👋\nab"),
2430 PathWithPosition {
2431 path: PathBuf::from("👋\nab"),
2432 row: None,
2433 column: None
2434 }
2435 );
2436
2437 assert_eq!(
2438 PathWithPosition::parse_str("Types.hs:(617,9)-(670,28):"),
2439 PathWithPosition {
2440 path: PathBuf::from("Types.hs"),
2441 row: Some(617),
2442 column: Some(9),
2443 }
2444 );
2445
2446 assert_eq!(
2447 PathWithPosition::parse_str("main (1).log"),
2448 PathWithPosition {
2449 path: PathBuf::from("main (1).log"),
2450 row: None,
2451 column: None
2452 }
2453 );
2454 }
2455
2456 #[perf]
2457 #[cfg(not(target_os = "windows"))]
2458 fn path_with_position_parse_posix_path_with_suffix() {
2459 assert_eq!(
2460 PathWithPosition::parse_str("foo/bar:34:in"),
2461 PathWithPosition {
2462 path: PathBuf::from("foo/bar"),
2463 row: Some(34),
2464 column: None,
2465 }
2466 );
2467 assert_eq!(
2468 PathWithPosition::parse_str("foo/bar.rs:1902:::15:"),
2469 PathWithPosition {
2470 path: PathBuf::from("foo/bar.rs:1902"),
2471 row: Some(15),
2472 column: None
2473 }
2474 );
2475
2476 assert_eq!(
2477 PathWithPosition::parse_str("app-editors:zed-0.143.6:20240710-201212.log:34:"),
2478 PathWithPosition {
2479 path: PathBuf::from("app-editors:zed-0.143.6:20240710-201212.log"),
2480 row: Some(34),
2481 column: None,
2482 }
2483 );
2484
2485 assert_eq!(
2486 PathWithPosition::parse_str("crates/file_finder/src/file_finder.rs:1902:13:"),
2487 PathWithPosition {
2488 path: PathBuf::from("crates/file_finder/src/file_finder.rs"),
2489 row: Some(1902),
2490 column: Some(13),
2491 }
2492 );
2493
2494 assert_eq!(
2495 PathWithPosition::parse_str("crate/utils/src/test:today.log:34"),
2496 PathWithPosition {
2497 path: PathBuf::from("crate/utils/src/test:today.log"),
2498 row: Some(34),
2499 column: None,
2500 }
2501 );
2502 assert_eq!(
2503 PathWithPosition::parse_str("/testing/out/src/file_finder.odin(7:15)"),
2504 PathWithPosition {
2505 path: PathBuf::from("/testing/out/src/file_finder.odin"),
2506 row: Some(7),
2507 column: Some(15),
2508 }
2509 );
2510 }
2511
2512 #[perf]
2513 #[cfg(target_os = "windows")]
2514 fn path_with_position_parse_windows_path() {
2515 assert_eq!(
2516 PathWithPosition::parse_str("crates\\utils\\paths.rs"),
2517 PathWithPosition {
2518 path: PathBuf::from("crates\\utils\\paths.rs"),
2519 row: None,
2520 column: None
2521 }
2522 );
2523
2524 assert_eq!(
2525 PathWithPosition::parse_str("C:\\Users\\someone\\test_file.rs"),
2526 PathWithPosition {
2527 path: PathBuf::from("C:\\Users\\someone\\test_file.rs"),
2528 row: None,
2529 column: None
2530 }
2531 );
2532
2533 assert_eq!(
2534 PathWithPosition::parse_str("C:\\Users\\someone\\main (1).log"),
2535 PathWithPosition {
2536 path: PathBuf::from("C:\\Users\\someone\\main (1).log"),
2537 row: None,
2538 column: None
2539 }
2540 );
2541 }
2542
2543 #[perf]
2544 #[cfg(target_os = "windows")]
2545 fn path_with_position_parse_windows_path_with_suffix() {
2546 assert_eq!(
2547 PathWithPosition::parse_str("crates\\utils\\paths.rs:101"),
2548 PathWithPosition {
2549 path: PathBuf::from("crates\\utils\\paths.rs"),
2550 row: Some(101),
2551 column: None
2552 }
2553 );
2554
2555 assert_eq!(
2556 PathWithPosition::parse_str("\\\\?\\C:\\Users\\someone\\test_file.rs:1:20"),
2557 PathWithPosition {
2558 path: PathBuf::from("\\\\?\\C:\\Users\\someone\\test_file.rs"),
2559 row: Some(1),
2560 column: Some(20)
2561 }
2562 );
2563
2564 assert_eq!(
2565 PathWithPosition::parse_str("C:\\Users\\someone\\test_file.rs(1902,13)"),
2566 PathWithPosition {
2567 path: PathBuf::from("C:\\Users\\someone\\test_file.rs"),
2568 row: Some(1902),
2569 column: Some(13)
2570 }
2571 );
2572
2573 assert_eq!(
2575 PathWithPosition::parse_str("\\\\?\\C:\\Users\\someone\\test_file.rs:1902:13:"),
2576 PathWithPosition {
2577 path: PathBuf::from("\\\\?\\C:\\Users\\someone\\test_file.rs"),
2578 row: Some(1902),
2579 column: Some(13)
2580 }
2581 );
2582
2583 assert_eq!(
2584 PathWithPosition::parse_str("\\\\?\\C:\\Users\\someone\\test_file.rs:1902:13:15:"),
2585 PathWithPosition {
2586 path: PathBuf::from("\\\\?\\C:\\Users\\someone\\test_file.rs:1902"),
2587 row: Some(13),
2588 column: Some(15)
2589 }
2590 );
2591
2592 assert_eq!(
2593 PathWithPosition::parse_str("\\\\?\\C:\\Users\\someone\\test_file.rs:1902:::15:"),
2594 PathWithPosition {
2595 path: PathBuf::from("\\\\?\\C:\\Users\\someone\\test_file.rs:1902"),
2596 row: Some(15),
2597 column: None
2598 }
2599 );
2600
2601 assert_eq!(
2602 PathWithPosition::parse_str("\\\\?\\C:\\Users\\someone\\test_file.rs(1902,13):"),
2603 PathWithPosition {
2604 path: PathBuf::from("\\\\?\\C:\\Users\\someone\\test_file.rs"),
2605 row: Some(1902),
2606 column: Some(13),
2607 }
2608 );
2609
2610 assert_eq!(
2611 PathWithPosition::parse_str("\\\\?\\C:\\Users\\someone\\test_file.rs(1902):"),
2612 PathWithPosition {
2613 path: PathBuf::from("\\\\?\\C:\\Users\\someone\\test_file.rs"),
2614 row: Some(1902),
2615 column: None,
2616 }
2617 );
2618
2619 assert_eq!(
2620 PathWithPosition::parse_str("C:\\Users\\someone\\test_file.rs:1902:13:"),
2621 PathWithPosition {
2622 path: PathBuf::from("C:\\Users\\someone\\test_file.rs"),
2623 row: Some(1902),
2624 column: Some(13),
2625 }
2626 );
2627
2628 assert_eq!(
2629 PathWithPosition::parse_str("C:\\Users\\someone\\test_file.rs(1902,13):"),
2630 PathWithPosition {
2631 path: PathBuf::from("C:\\Users\\someone\\test_file.rs"),
2632 row: Some(1902),
2633 column: Some(13),
2634 }
2635 );
2636
2637 assert_eq!(
2638 PathWithPosition::parse_str("C:\\Users\\someone\\test_file.rs(1902):"),
2639 PathWithPosition {
2640 path: PathBuf::from("C:\\Users\\someone\\test_file.rs"),
2641 row: Some(1902),
2642 column: None,
2643 }
2644 );
2645
2646 assert_eq!(
2647 PathWithPosition::parse_str("crates/utils/paths.rs:101"),
2648 PathWithPosition {
2649 path: PathBuf::from("crates\\utils\\paths.rs"),
2650 row: Some(101),
2651 column: None,
2652 }
2653 );
2654 }
2655
2656 #[perf]
2657 fn test_path_compact() {
2658 let path: PathBuf = [
2659 home_dir().to_string_lossy().into_owned(),
2660 "some_file.txt".to_string(),
2661 ]
2662 .iter()
2663 .collect();
2664 if cfg!(any(target_os = "linux", target_os = "freebsd")) || cfg!(target_os = "macos") {
2665 assert_eq!(path.compact().to_str(), Some("~/some_file.txt"));
2666 } else {
2667 assert_eq!(path.compact().to_str(), path.to_str());
2668 }
2669 }
2670
2671 #[perf]
2672 fn test_extension_or_hidden_file_name() {
2673 let path = Path::new("/a/b/c/file_name.rs");
2675 assert_eq!(path.extension_or_hidden_file_name(), Some("rs"));
2676
2677 let path = Path::new("/a/b/c/file.name.rs");
2679 assert_eq!(path.extension_or_hidden_file_name(), Some("rs"));
2680
2681 let path = Path::new("/a/b/c/long.file.name.rs");
2683 assert_eq!(path.extension_or_hidden_file_name(), Some("rs"));
2684
2685 let path = Path::new("/a/b/c/.gitignore");
2687 assert_eq!(path.extension_or_hidden_file_name(), Some("gitignore"));
2688
2689 let path = Path::new("/a/b/c/.eslintrc.js");
2691 assert_eq!(path.extension_or_hidden_file_name(), Some("eslintrc.js"));
2692 }
2693
2694 #[perf]
2695 #[perf]
2731 #[cfg(target_os = "windows")]
2732 fn test_sanitized_path() {
2733 let path = Path::new("C:\\Users\\someone\\test_file.rs");
2734 let sanitized_path = SanitizedPath::new(path);
2735 assert_eq!(
2736 sanitized_path.to_string(),
2737 "C:\\Users\\someone\\test_file.rs"
2738 );
2739
2740 let path = Path::new("\\\\?\\C:\\Users\\someone\\test_file.rs");
2741 let sanitized_path = SanitizedPath::new(path);
2742 assert_eq!(
2743 sanitized_path.to_string(),
2744 "C:\\Users\\someone\\test_file.rs"
2745 );
2746 }
2747
2748 #[perf]
2749 #[cfg(target_os = "windows")]
2750 fn test_sanitized_path_verbatim_unc() {
2751 let path: Arc<Path> = PathBuf::from("\\\\?\\UNC\\server\\share\\file.txt").into();
2752 let sanitized_path = SanitizedPath::from_arc(path);
2753 assert_eq!(sanitized_path.to_string(), "\\\\server\\share\\file.txt");
2754 }
2755
2756 #[perf]
2757 fn test_compare_numeric_segments() {
2758 fn compare(a: &str, b: &str) -> Ordering {
2760 let mut a_iter = a.chars().peekable();
2761 let mut b_iter = b.chars().peekable();
2762
2763 let result = compare_numeric_segments(&mut a_iter, &mut b_iter);
2764
2765 assert!(
2767 !a_iter.next().is_some_and(|c| c.is_ascii_digit()),
2768 "Iterator a should have consumed all digits"
2769 );
2770 assert!(
2771 !b_iter.next().is_some_and(|c| c.is_ascii_digit()),
2772 "Iterator b should have consumed all digits"
2773 );
2774
2775 result
2776 }
2777
2778 assert_eq!(compare("0", "0"), Ordering::Equal);
2780 assert_eq!(compare("1", "2"), Ordering::Less);
2781 assert_eq!(compare("9", "10"), Ordering::Less);
2782 assert_eq!(compare("10", "9"), Ordering::Greater);
2783 assert_eq!(compare("99", "100"), Ordering::Less);
2784
2785 assert_eq!(compare("0", "00"), Ordering::Less);
2787 assert_eq!(compare("00", "0"), Ordering::Greater);
2788 assert_eq!(compare("01", "1"), Ordering::Greater);
2789 assert_eq!(compare("001", "1"), Ordering::Greater);
2790 assert_eq!(compare("001", "01"), Ordering::Greater);
2791
2792 assert_eq!(compare("000100", "100"), Ordering::Greater);
2794 assert_eq!(compare("100", "0100"), Ordering::Less);
2795 assert_eq!(compare("0100", "00100"), Ordering::Less);
2796
2797 assert_eq!(compare("9999999999", "10000000000"), Ordering::Less);
2799 assert_eq!(
2800 compare(
2801 "340282366920938463463374607431768211455", "340282366920938463463374607431768211456"
2803 ),
2804 Ordering::Less
2805 );
2806 assert_eq!(
2807 compare(
2808 "340282366920938463463374607431768211456", "340282366920938463463374607431768211455"
2810 ),
2811 Ordering::Greater
2812 );
2813
2814 let mut a_iter = "123abc".chars().peekable();
2816 let mut b_iter = "456def".chars().peekable();
2817
2818 compare_numeric_segments(&mut a_iter, &mut b_iter);
2819
2820 assert_eq!(a_iter.collect::<String>(), "abc");
2821 assert_eq!(b_iter.collect::<String>(), "def");
2822 }
2823
2824 #[perf]
2825 fn test_natural_sort() {
2826 assert_eq!(natural_sort("a", "b"), Ordering::Less);
2828 assert_eq!(natural_sort("b", "a"), Ordering::Greater);
2829 assert_eq!(natural_sort("a", "a"), Ordering::Equal);
2830
2831 assert_eq!(natural_sort("a", "A"), Ordering::Less);
2833 assert_eq!(natural_sort("A", "a"), Ordering::Greater);
2834 assert_eq!(natural_sort("aA", "aa"), Ordering::Greater);
2835 assert_eq!(natural_sort("aa", "aA"), Ordering::Less);
2836
2837 assert_eq!(natural_sort("1", "2"), Ordering::Less);
2839 assert_eq!(natural_sort("2", "10"), Ordering::Less);
2840 assert_eq!(natural_sort("02", "10"), Ordering::Less);
2841 assert_eq!(natural_sort("02", "2"), Ordering::Greater);
2842
2843 assert_eq!(natural_sort("a1", "a2"), Ordering::Less);
2845 assert_eq!(natural_sort("a2", "a10"), Ordering::Less);
2846 assert_eq!(natural_sort("a02", "a2"), Ordering::Greater);
2847 assert_eq!(natural_sort("a1b", "a1c"), Ordering::Less);
2848
2849 assert_eq!(natural_sort("1a2", "1a10"), Ordering::Less);
2851 assert_eq!(natural_sort("1a10", "1a2"), Ordering::Greater);
2852 assert_eq!(natural_sort("2a1", "10a1"), Ordering::Less);
2853
2854 assert_eq!(natural_sort("a-1", "a-2"), Ordering::Less);
2856 assert_eq!(natural_sort("a_1", "a_2"), Ordering::Less);
2857 assert_eq!(natural_sort("a.1", "a.2"), Ordering::Less);
2858
2859 assert_eq!(natural_sort("文1", "文2"), Ordering::Less);
2861 assert_eq!(natural_sort("文2", "文10"), Ordering::Less);
2862 assert_eq!(natural_sort("🔤1", "🔤2"), Ordering::Less);
2863
2864 assert_eq!(natural_sort("", ""), Ordering::Equal);
2866 assert_eq!(natural_sort("", "a"), Ordering::Less);
2867 assert_eq!(natural_sort("a", ""), Ordering::Greater);
2868 assert_eq!(natural_sort(" ", " "), Ordering::Less);
2869
2870 assert_eq!(natural_sort("File-1.txt", "File-2.txt"), Ordering::Less);
2872 assert_eq!(natural_sort("File-02.txt", "File-2.txt"), Ordering::Greater);
2873 assert_eq!(natural_sort("File-2.txt", "File-10.txt"), Ordering::Less);
2874 assert_eq!(natural_sort("File_A1", "File_A2"), Ordering::Less);
2875 assert_eq!(natural_sort("File_a1", "File_A1"), Ordering::Less);
2876 }
2877
2878 #[perf]
2879 fn test_compare_paths() {
2880 fn compare(a: &str, is_a_file: bool, b: &str, is_b_file: bool) -> Ordering {
2882 compare_paths((Path::new(a), is_a_file), (Path::new(b), is_b_file))
2883 }
2884
2885 assert_eq!(compare("a", true, "b", true), Ordering::Less);
2887 assert_eq!(compare("b", true, "a", true), Ordering::Greater);
2888 assert_eq!(compare("a", true, "a", true), Ordering::Equal);
2889
2890 assert_eq!(compare("a", true, "a", false), Ordering::Greater);
2892 assert_eq!(compare("a", false, "a", true), Ordering::Less);
2893 assert_eq!(compare("b", false, "a", true), Ordering::Less);
2894
2895 assert_eq!(compare("a.txt", true, "a.md", true), Ordering::Greater);
2897 assert_eq!(compare("a.md", true, "a.txt", true), Ordering::Less);
2898 assert_eq!(compare("a", true, "a.txt", true), Ordering::Less);
2899
2900 assert_eq!(compare("dir/a", true, "dir/b", true), Ordering::Less);
2902 assert_eq!(compare("dir1/a", true, "dir2/a", true), Ordering::Less);
2903 assert_eq!(compare("dir/sub/a", true, "dir/a", true), Ordering::Less);
2904
2905 assert_eq!(
2907 compare("Dir/file", true, "dir/file", true),
2908 Ordering::Greater
2909 );
2910 assert_eq!(
2911 compare("dir/File", true, "dir/file", true),
2912 Ordering::Greater
2913 );
2914 assert_eq!(compare("dir/file", true, "Dir/File", true), Ordering::Less);
2915
2916 assert_eq!(compare(".hidden", true, "visible", true), Ordering::Less);
2918 assert_eq!(compare("_special", true, "normal", true), Ordering::Less);
2919 assert_eq!(compare(".config", false, ".data", false), Ordering::Less);
2920
2921 assert_eq!(
2923 compare("dir1/file", true, "dir2/file", true),
2924 Ordering::Less
2925 );
2926 assert_eq!(
2927 compare("dir2/file", true, "dir10/file", true),
2928 Ordering::Less
2929 );
2930 assert_eq!(
2931 compare("dir02/file", true, "dir2/file", true),
2932 Ordering::Greater
2933 );
2934
2935 assert_eq!(compare("/a", true, "/b", true), Ordering::Less);
2937 assert_eq!(compare("/", false, "/a", true), Ordering::Less);
2938
2939 assert_eq!(
2941 compare("project/src/main.rs", true, "project/src/lib.rs", true),
2942 Ordering::Greater
2943 );
2944 assert_eq!(
2945 compare(
2946 "project/tests/test_1.rs",
2947 true,
2948 "project/tests/test_2.rs",
2949 true
2950 ),
2951 Ordering::Less
2952 );
2953 assert_eq!(
2954 compare(
2955 "project/v1.0.0/README.md",
2956 true,
2957 "project/v1.10.0/README.md",
2958 true
2959 ),
2960 Ordering::Less
2961 );
2962 }
2963
2964 #[perf]
2965 fn test_natural_sort_case_sensitivity() {
2966 std::thread::sleep(std::time::Duration::from_millis(100));
2967 assert_eq!(natural_sort("a", "A"), Ordering::Less);
2969 assert_eq!(natural_sort("A", "a"), Ordering::Greater);
2970 assert_eq!(natural_sort("a", "a"), Ordering::Equal);
2971 assert_eq!(natural_sort("A", "A"), Ordering::Equal);
2972
2973 assert_eq!(natural_sort("aaa", "AAA"), Ordering::Less);
2975 assert_eq!(natural_sort("AAA", "aaa"), Ordering::Greater);
2976 assert_eq!(natural_sort("aAa", "AaA"), Ordering::Less);
2977
2978 assert_eq!(natural_sort("a", "b"), Ordering::Less);
2980 assert_eq!(natural_sort("A", "b"), Ordering::Less);
2981 assert_eq!(natural_sort("a", "B"), Ordering::Less);
2982 }
2983
2984 #[perf]
2985 fn test_natural_sort_with_numbers() {
2986 assert_eq!(natural_sort("file1", "file2"), Ordering::Less);
2988 assert_eq!(natural_sort("file2", "file10"), Ordering::Less);
2989 assert_eq!(natural_sort("file10", "file2"), Ordering::Greater);
2990
2991 assert_eq!(natural_sort("1file", "2file"), Ordering::Less);
2993 assert_eq!(natural_sort("file1text", "file2text"), Ordering::Less);
2994 assert_eq!(natural_sort("text1file", "text2file"), Ordering::Less);
2995
2996 assert_eq!(natural_sort("file1-2", "file1-10"), Ordering::Less);
2998 assert_eq!(natural_sort("2-1file", "10-1file"), Ordering::Less);
2999
3000 assert_eq!(natural_sort("file002", "file2"), Ordering::Greater);
3002 assert_eq!(natural_sort("file002", "file10"), Ordering::Less);
3003
3004 assert_eq!(
3006 natural_sort("file999999999999999999999", "file999999999999999999998"),
3007 Ordering::Greater
3008 );
3009
3010 assert_eq!(
3014 natural_sort(
3015 "file340282366920938463463374607431768211454",
3016 "file340282366920938463463374607431768211455"
3017 ),
3018 Ordering::Less
3019 );
3020
3021 assert_eq!(
3023 natural_sort(
3024 "file340282366920938463463374607431768211456",
3025 "file340282366920938463463374607431768211455"
3026 ),
3027 Ordering::Greater
3028 );
3029
3030 assert_eq!(
3032 natural_sort(
3033 "file3402823669209384634633746074317682114560",
3034 "file340282366920938463463374607431768211455"
3035 ),
3036 Ordering::Greater
3037 );
3038
3039 assert_eq!(
3041 natural_sort(
3042 "file0340282366920938463463374607431768211455",
3043 "file340282366920938463463374607431768211455"
3044 ),
3045 Ordering::Greater
3046 );
3047
3048 assert_eq!(
3050 natural_sort(
3051 "file999999999999999999999999999999999999999999999999",
3052 "file9999999999999999999999999999999999999999999999999"
3053 ),
3054 Ordering::Less
3055 );
3056 }
3057
3058 #[perf]
3059 fn test_natural_sort_case_sensitive() {
3060 assert_eq!(natural_sort("File1", "file2"), Ordering::Less);
3062 assert_eq!(natural_sort("file1", "File2"), Ordering::Less);
3063
3064 assert_eq!(natural_sort("Dir1", "dir01"), Ordering::Less);
3067 assert_eq!(natural_sort("dir2", "Dir02"), Ordering::Less);
3068 assert_eq!(natural_sort("dir2", "dir02"), Ordering::Less);
3069
3070 assert_eq!(natural_sort("dir1", "Dir1"), Ordering::Less);
3073 assert_eq!(natural_sort("dir02", "Dir02"), Ordering::Less);
3074 assert_eq!(natural_sort("dir10", "Dir10"), Ordering::Less);
3075 }
3076
3077 #[perf]
3078 fn test_natural_sort_edge_cases() {
3079 assert_eq!(natural_sort("", ""), Ordering::Equal);
3081 assert_eq!(natural_sort("", "a"), Ordering::Less);
3082 assert_eq!(natural_sort("a", ""), Ordering::Greater);
3083
3084 assert_eq!(natural_sort("file-1", "file_1"), Ordering::Less);
3086 assert_eq!(natural_sort("file.1", "file_1"), Ordering::Less);
3087 assert_eq!(natural_sort("file 1", "file_1"), Ordering::Less);
3088
3089 assert_eq!(natural_sort("file①", "file②"), Ordering::Less);
3092 assert_eq!(natural_sort("file⑩", "file②"), Ordering::Greater);
3094 assert_eq!(natural_sort("file漢", "file字"), Ordering::Greater);
3096
3097 assert_eq!(natural_sort("file-1a", "file-1b"), Ordering::Less);
3099 assert_eq!(natural_sort("file-1.2", "file-1.10"), Ordering::Less);
3100 assert_eq!(natural_sort("file-1.10", "file-1.2"), Ordering::Greater);
3101 }
3102
3103 #[test]
3104 fn test_multiple_extensions() {
3105 let path = Path::new("/a/b/c/file_name");
3107 assert_eq!(path.multiple_extensions(), None);
3108
3109 let path = Path::new("/a/b/c/file_name.tsx");
3111 assert_eq!(path.multiple_extensions(), None);
3112
3113 let path = Path::new("/a/b/c/file_name.stories.tsx");
3115 assert_eq!(path.multiple_extensions(), Some("stories.tsx".to_string()));
3116
3117 let path = Path::new("/a/b/c/long.app.tar.gz");
3119 assert_eq!(path.multiple_extensions(), Some("app.tar.gz".to_string()));
3120 }
3121
3122 #[test]
3123 fn test_strip_path_suffix() {
3124 let base = Path::new("/a/b/c/file_name");
3125 let suffix = Path::new("file_name");
3126 assert_eq!(strip_path_suffix(base, suffix), Some(Path::new("/a/b/c")));
3127
3128 let base = Path::new("/a/b/c/file_name.tsx");
3129 let suffix = Path::new("file_name.tsx");
3130 assert_eq!(strip_path_suffix(base, suffix), Some(Path::new("/a/b/c")));
3131
3132 let base = Path::new("/a/b/c/file_name.stories.tsx");
3133 let suffix = Path::new("c/file_name.stories.tsx");
3134 assert_eq!(strip_path_suffix(base, suffix), Some(Path::new("/a/b")));
3135
3136 let base = Path::new("/a/b/c/long.app.tar.gz");
3137 let suffix = Path::new("b/c/long.app.tar.gz");
3138 assert_eq!(strip_path_suffix(base, suffix), Some(Path::new("/a")));
3139
3140 let base = Path::new("/a/b/c/long.app.tar.gz");
3141 let suffix = Path::new("/a/b/c/long.app.tar.gz");
3142 assert_eq!(strip_path_suffix(base, suffix), Some(Path::new("")));
3143
3144 let base = Path::new("/a/b/c/long.app.tar.gz");
3145 let suffix = Path::new("/a/b/c/no_match.app.tar.gz");
3146 assert_eq!(strip_path_suffix(base, suffix), None);
3147
3148 let base = Path::new("/a/b/c/long.app.tar.gz");
3149 let suffix = Path::new("app.tar.gz");
3150 assert_eq!(strip_path_suffix(base, suffix), None);
3151 }
3152
3153 #[cfg(target_os = "windows")]
3154 #[test]
3155 fn test_wsl_path() {
3156 use super::WslPath;
3157 let path = "/a/b/c";
3158 assert_eq!(WslPath::from_path(&path), None);
3159
3160 let path = r"\\wsl.localhost";
3161 assert_eq!(WslPath::from_path(&path), None);
3162
3163 let path = r"\\wsl.localhost\Distro";
3164 assert_eq!(
3165 WslPath::from_path(&path),
3166 Some(WslPath {
3167 distro: "Distro".to_owned(),
3168 path: "/".into(),
3169 })
3170 );
3171
3172 let path = r"\\wsl.localhost\Distro\blue";
3173 assert_eq!(
3174 WslPath::from_path(&path),
3175 Some(WslPath {
3176 distro: "Distro".to_owned(),
3177 path: "/blue".into()
3178 })
3179 );
3180
3181 let path = r"\\wsl$\archlinux\tomato\.\paprika\..\aubergine.txt";
3182 assert_eq!(
3183 WslPath::from_path(&path),
3184 Some(WslPath {
3185 distro: "archlinux".to_owned(),
3186 path: "/tomato/paprika/../aubergine.txt".into()
3187 })
3188 );
3189
3190 let path = r"\\windows.localhost\Distro\foo";
3191 assert_eq!(WslPath::from_path(&path), None);
3192 }
3193
3194 #[test]
3195 fn test_url_to_file_path_ext_posix_basic() {
3196 use super::UrlExt;
3197
3198 let url = url::Url::parse("file:///home/user/file.txt").unwrap();
3199 assert_eq!(
3200 url.to_file_path_ext(PathStyle::Unix),
3201 Ok(PathBuf::from("/home/user/file.txt"))
3202 );
3203
3204 let url = url::Url::parse("file:///").unwrap();
3205 assert_eq!(
3206 url.to_file_path_ext(PathStyle::Unix),
3207 Ok(PathBuf::from("/"))
3208 );
3209
3210 let url = url::Url::parse("file:///a/b/c/d/e").unwrap();
3211 assert_eq!(
3212 url.to_file_path_ext(PathStyle::Unix),
3213 Ok(PathBuf::from("/a/b/c/d/e"))
3214 );
3215 }
3216
3217 #[test]
3218 fn test_url_to_file_path_ext_posix_percent_encoding() {
3219 use super::UrlExt;
3220
3221 let url = url::Url::parse("file:///home/user/file%20with%20spaces.txt").unwrap();
3222 assert_eq!(
3223 url.to_file_path_ext(PathStyle::Unix),
3224 Ok(PathBuf::from("/home/user/file with spaces.txt"))
3225 );
3226
3227 let url = url::Url::parse("file:///path%2Fwith%2Fencoded%2Fslashes").unwrap();
3228 assert_eq!(
3229 url.to_file_path_ext(PathStyle::Unix),
3230 Ok(PathBuf::from("/path/with/encoded/slashes"))
3231 );
3232
3233 let url = url::Url::parse("file:///special%23chars%3F.txt").unwrap();
3234 assert_eq!(
3235 url.to_file_path_ext(PathStyle::Unix),
3236 Ok(PathBuf::from("/special#chars?.txt"))
3237 );
3238 }
3239
3240 #[test]
3241 fn test_url_to_file_path_ext_posix_localhost() {
3242 use super::UrlExt;
3243
3244 let url = url::Url::parse("file://localhost/home/user/file.txt").unwrap();
3245 assert_eq!(
3246 url.to_file_path_ext(PathStyle::Unix),
3247 Ok(PathBuf::from("/home/user/file.txt"))
3248 );
3249 }
3250
3251 #[test]
3252 fn test_url_to_file_path_ext_posix_rejects_host() {
3253 use super::UrlExt;
3254
3255 let url = url::Url::parse("file://somehost/home/user/file.txt").unwrap();
3256 assert_eq!(url.to_file_path_ext(PathStyle::Unix), Err(()));
3257 }
3258
3259 #[test]
3260 fn test_url_to_file_path_ext_posix_windows_drive_letter() {
3261 use super::UrlExt;
3262
3263 let url = url::Url::parse("file:///C:").unwrap();
3264 assert_eq!(
3265 url.to_file_path_ext(PathStyle::Unix),
3266 Ok(PathBuf::from("/C:/"))
3267 );
3268
3269 let url = url::Url::parse("file:///D|").unwrap();
3270 assert_eq!(
3271 url.to_file_path_ext(PathStyle::Unix),
3272 Ok(PathBuf::from("/D|/"))
3273 );
3274 }
3275
3276 #[test]
3277 fn test_url_to_file_path_ext_windows_basic() {
3278 use super::UrlExt;
3279
3280 let url = url::Url::parse("file:///C:/Users/user/file.txt").unwrap();
3281 assert_eq!(
3282 url.to_file_path_ext(PathStyle::Windows),
3283 Ok(PathBuf::from("C:\\Users\\user\\file.txt"))
3284 );
3285
3286 let url = url::Url::parse("file:///D:/folder/subfolder/file.rs").unwrap();
3287 assert_eq!(
3288 url.to_file_path_ext(PathStyle::Windows),
3289 Ok(PathBuf::from("D:\\folder\\subfolder\\file.rs"))
3290 );
3291
3292 let url = url::Url::parse("file:///C:/").unwrap();
3293 assert_eq!(
3294 url.to_file_path_ext(PathStyle::Windows),
3295 Ok(PathBuf::from("C:\\"))
3296 );
3297 }
3298
3299 #[test]
3300 fn test_url_to_file_path_ext_windows_encoded_drive_letter() {
3301 use super::UrlExt;
3302
3303 let url = url::Url::parse("file:///C%3A/Users/file.txt").unwrap();
3304 assert_eq!(
3305 url.to_file_path_ext(PathStyle::Windows),
3306 Ok(PathBuf::from("C:\\Users\\file.txt"))
3307 );
3308
3309 let url = url::Url::parse("file:///c%3a/Users/file.txt").unwrap();
3310 assert_eq!(
3311 url.to_file_path_ext(PathStyle::Windows),
3312 Ok(PathBuf::from("c:\\Users\\file.txt"))
3313 );
3314
3315 let url = url::Url::parse("file:///D%3A/folder/file.txt").unwrap();
3316 assert_eq!(
3317 url.to_file_path_ext(PathStyle::Windows),
3318 Ok(PathBuf::from("D:\\folder\\file.txt"))
3319 );
3320
3321 let url = url::Url::parse("file:///d%3A/folder/file.txt").unwrap();
3322 assert_eq!(
3323 url.to_file_path_ext(PathStyle::Windows),
3324 Ok(PathBuf::from("d:\\folder\\file.txt"))
3325 );
3326 }
3327
3328 #[test]
3329 fn test_url_to_file_path_ext_windows_unc_path() {
3330 use super::UrlExt;
3331
3332 let url = url::Url::parse("file://server/share/path/file.txt").unwrap();
3333 assert_eq!(
3334 url.to_file_path_ext(PathStyle::Windows),
3335 Ok(PathBuf::from("\\\\server\\share\\path\\file.txt"))
3336 );
3337
3338 let url = url::Url::parse("file://server/share").unwrap();
3339 assert_eq!(
3340 url.to_file_path_ext(PathStyle::Windows),
3341 Ok(PathBuf::from("\\\\server\\share"))
3342 );
3343 }
3344
3345 #[test]
3346 fn test_url_to_file_path_ext_windows_percent_encoding() {
3347 use super::UrlExt;
3348
3349 let url = url::Url::parse("file:///C:/Users/user/file%20with%20spaces.txt").unwrap();
3350 assert_eq!(
3351 url.to_file_path_ext(PathStyle::Windows),
3352 Ok(PathBuf::from("C:\\Users\\user\\file with spaces.txt"))
3353 );
3354
3355 let url = url::Url::parse("file:///C:/special%23chars%3F.txt").unwrap();
3356 assert_eq!(
3357 url.to_file_path_ext(PathStyle::Windows),
3358 Ok(PathBuf::from("C:\\special#chars?.txt"))
3359 );
3360 }
3361
3362 #[test]
3363 fn test_url_to_file_path_ext_windows_invalid_drive() {
3364 use super::UrlExt;
3365
3366 let url = url::Url::parse("file:///1:/path/file.txt").unwrap();
3367 assert_eq!(url.to_file_path_ext(PathStyle::Windows), Err(()));
3368
3369 let url = url::Url::parse("file:///CC:/path/file.txt").unwrap();
3370 assert_eq!(url.to_file_path_ext(PathStyle::Windows), Err(()));
3371
3372 let url = url::Url::parse("file:///C/path/file.txt").unwrap();
3373 assert_eq!(url.to_file_path_ext(PathStyle::Windows), Err(()));
3374
3375 let url = url::Url::parse("file:///invalid").unwrap();
3376 assert_eq!(url.to_file_path_ext(PathStyle::Windows), Err(()));
3377 }
3378
3379 #[test]
3380 fn test_url_to_file_path_ext_non_file_scheme() {
3381 use super::UrlExt;
3382
3383 let url = url::Url::parse("http://example.com/path").unwrap();
3384 assert_eq!(url.to_file_path_ext(PathStyle::Unix), Err(()));
3385 assert_eq!(url.to_file_path_ext(PathStyle::Windows), Err(()));
3386
3387 let url = url::Url::parse("https://example.com/path").unwrap();
3388 assert_eq!(url.to_file_path_ext(PathStyle::Unix), Err(()));
3389 assert_eq!(url.to_file_path_ext(PathStyle::Windows), Err(()));
3390 }
3391
3392 #[test]
3393 fn test_url_to_file_path_ext_windows_localhost() {
3394 use super::UrlExt;
3395
3396 let url = url::Url::parse("file://localhost/C:/Users/file.txt").unwrap();
3397 assert_eq!(
3398 url.to_file_path_ext(PathStyle::Windows),
3399 Ok(PathBuf::from("C:\\Users\\file.txt"))
3400 );
3401 }
3402}