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