1use globset::{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 simplified = dunce::simplified(path.as_ref());
262 if simplified == path.as_ref() {
263 unsafe { mem::transmute::<Arc<Path>, Arc<Self>>(path) }
265 } else {
266 Self::unchecked_new(simplified).into()
267 }
268 }
269 }
270
271 pub fn new_arc<T: AsRef<Path> + ?Sized>(path: &T) -> Arc<Self> {
272 Self::new(path).into()
273 }
274
275 pub fn cast_arc(path: Arc<Self>) -> Arc<Path> {
276 unsafe { mem::transmute::<Arc<Self>, Arc<Path>>(path) }
278 }
279
280 pub fn cast_arc_ref(path: &Arc<Self>) -> &Arc<Path> {
281 unsafe { mem::transmute::<&Arc<Self>, &Arc<Path>>(path) }
283 }
284
285 pub fn starts_with(&self, prefix: &Self) -> bool {
286 self.0.starts_with(&prefix.0)
287 }
288
289 pub fn as_path(&self) -> &Path {
290 &self.0
291 }
292
293 pub fn file_name(&self) -> Option<&std::ffi::OsStr> {
294 self.0.file_name()
295 }
296
297 pub fn extension(&self) -> Option<&std::ffi::OsStr> {
298 self.0.extension()
299 }
300
301 pub fn join<P: AsRef<Path>>(&self, path: P) -> PathBuf {
302 self.0.join(path)
303 }
304
305 pub fn parent(&self) -> Option<&Self> {
306 self.0.parent().map(Self::unchecked_new)
307 }
308
309 pub fn strip_prefix(&self, base: &Self) -> Result<&Path, StripPrefixError> {
310 self.0.strip_prefix(base.as_path())
311 }
312
313 pub fn to_str(&self) -> Option<&str> {
314 self.0.to_str()
315 }
316
317 pub fn to_path_buf(&self) -> PathBuf {
318 self.0.to_path_buf()
319 }
320}
321
322impl std::fmt::Debug for SanitizedPath {
323 fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
324 std::fmt::Debug::fmt(&self.0, formatter)
325 }
326}
327
328impl Display for SanitizedPath {
329 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
330 write!(f, "{}", self.0.display())
331 }
332}
333
334impl From<&SanitizedPath> for Arc<SanitizedPath> {
335 fn from(sanitized_path: &SanitizedPath) -> Self {
336 let path: Arc<Path> = sanitized_path.0.into();
337 unsafe { mem::transmute(path) }
339 }
340}
341
342impl From<&SanitizedPath> for PathBuf {
343 fn from(sanitized_path: &SanitizedPath) -> Self {
344 sanitized_path.as_path().into()
345 }
346}
347
348impl AsRef<Path> for SanitizedPath {
349 fn as_ref(&self) -> &Path {
350 &self.0
351 }
352}
353
354#[derive(Debug, Clone)]
355pub struct RemotePathBuf {
356 style: PathStyle,
357 string: String,
358}
359
360impl RemotePathBuf {
361 pub fn new(string: String, style: PathStyle) -> Self {
362 Self { style, string }
363 }
364
365 pub fn from_str(path: &str, style: PathStyle) -> Self {
366 Self::new(path.to_string(), style)
367 }
368
369 pub fn path_style(&self) -> PathStyle {
370 self.style
371 }
372
373 pub fn to_proto(self) -> String {
374 self.string
375 }
376}
377
378impl Display for RemotePathBuf {
379 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
380 write!(f, "{}", self.string)
381 }
382}
383
384pub fn is_absolute(path_like: &str, path_style: PathStyle) -> bool {
385 path_like.starts_with('/')
386 || path_style == PathStyle::Windows
387 && (path_like.starts_with('\\')
388 || path_like
389 .chars()
390 .next()
391 .is_some_and(|c| c.is_ascii_alphabetic())
392 && path_like[1..]
393 .strip_prefix(':')
394 .is_some_and(|path| path.starts_with('/') || path.starts_with('\\')))
395}
396
397#[derive(Debug, PartialEq)]
398#[non_exhaustive]
399pub struct NormalizeError;
400
401impl Error for NormalizeError {}
402
403impl std::fmt::Display for NormalizeError {
404 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
405 f.write_str("parent reference `..` points outside of base directory")
406 }
407}
408
409pub fn normalize_lexically(path: &Path) -> Result<PathBuf, NormalizeError> {
426 use std::path::Component;
427
428 let mut lexical = PathBuf::new();
429 let mut iter = path.components().peekable();
430
431 let root = match iter.peek() {
435 Some(Component::ParentDir) => return Err(NormalizeError),
436 Some(p @ Component::RootDir) | Some(p @ Component::CurDir) => {
437 lexical.push(p);
438 iter.next();
439 lexical.as_os_str().len()
440 }
441 Some(Component::Prefix(prefix)) => {
442 lexical.push(prefix.as_os_str());
443 iter.next();
444 if let Some(p @ Component::RootDir) = iter.peek() {
445 lexical.push(p);
446 iter.next();
447 }
448 lexical.as_os_str().len()
449 }
450 None => return Ok(PathBuf::new()),
451 Some(Component::Normal(_)) => 0,
452 };
453
454 for component in iter {
455 match component {
456 Component::RootDir => unreachable!(),
457 Component::Prefix(_) => return Err(NormalizeError),
458 Component::CurDir => continue,
459 Component::ParentDir => {
460 if lexical.as_os_str().len() == root {
462 return Err(NormalizeError);
463 } else {
464 lexical.pop();
465 }
466 }
467 Component::Normal(path) => lexical.push(path),
468 }
469 }
470 Ok(lexical)
471}
472
473pub fn insert_subtree(subtrees: &mut Vec<PathBuf>, path: PathBuf) {
482 if subtrees.iter().any(|existing| path.starts_with(existing)) {
483 return;
484 }
485 subtrees.retain(|existing| !existing.starts_with(&path));
486 subtrees.push(path);
487}
488
489pub fn path_within_subtree<'a>(path: &Path, mut subtrees: impl Iterator<Item = &'a Path>) -> bool {
493 subtrees.any(|granted| path.starts_with(granted))
494}
495
496pub const FILE_ROW_COLUMN_DELIMITER: char = ':';
498
499const ROW_COL_CAPTURE_REGEX: &str = r"(?xs)
500 ([^\(]+)\:(?:
501 \((\d+)[,:](\d+)\) # filename:(row,column), filename:(row:column)
502 |
503 \((\d+)\)() # filename:(row)
504 )
505 |
506 ([^\(]+)(?:
507 \((\d+)[,:](\d+)\) # filename(row,column), filename(row:column)
508 |
509 \((\d+)\)() # filename(row)
510 )
511 \:*$
512 |
513 (.+?)(?:
514 \:+(\d+)\:(\d+)\:*$ # filename:row:column
515 |
516 \:+(\d+)\:*()$ # filename:row
517 |
518 \:+()()$
519 )";
520
521#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)]
524pub struct PathWithPosition {
525 pub path: PathBuf,
526 pub row: Option<u32>,
527 pub column: Option<u32>,
529}
530
531impl PathWithPosition {
532 pub fn from_path(path: PathBuf) -> Self {
534 Self {
535 path,
536 row: None,
537 column: None,
538 }
539 }
540
541 pub fn parse_str(s: &str) -> Self {
627 let trimmed = s.trim();
628 let path = Path::new(trimmed);
629 let Some(maybe_file_name_with_row_col) = path.file_name().unwrap_or_default().to_str()
630 else {
631 return Self {
632 path: Path::new(s).to_path_buf(),
633 row: None,
634 column: None,
635 };
636 };
637 if maybe_file_name_with_row_col.is_empty() {
638 return Self {
639 path: Path::new(s).to_path_buf(),
640 row: None,
641 column: None,
642 };
643 }
644
645 static SUFFIX_RE: LazyLock<Regex> =
649 LazyLock::new(|| Regex::new(ROW_COL_CAPTURE_REGEX).unwrap());
650 match SUFFIX_RE
651 .captures(maybe_file_name_with_row_col)
652 .map(|caps| caps.extract())
653 {
654 Some((_, [file_name, maybe_row, maybe_column])) => {
655 let row = maybe_row.parse::<u32>().ok();
656 let column = maybe_column.parse::<u32>().ok();
657
658 let (_, suffix) = trimmed.split_once(file_name).unwrap();
659 let path_without_suffix = &trimmed[..trimmed.len() - suffix.len()];
660
661 Self {
662 path: Path::new(path_without_suffix).to_path_buf(),
663 row,
664 column,
665 }
666 }
667 None => {
668 let delimiter = ':';
672 let mut path_parts = s
673 .rsplitn(3, delimiter)
674 .collect::<Vec<_>>()
675 .into_iter()
676 .rev()
677 .fuse();
678 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();
679 let mut row = None;
680 let mut column = None;
681 if let Some(maybe_row) = path_parts.next() {
682 if let Ok(parsed_row) = maybe_row.parse::<u32>() {
683 row = Some(parsed_row);
684 if let Some(parsed_column) = path_parts
685 .next()
686 .and_then(|maybe_col| maybe_col.parse::<u32>().ok())
687 {
688 column = Some(parsed_column);
689 }
690 } else {
691 path_string.push(delimiter);
692 path_string.push_str(maybe_row);
693 }
694 }
695 for split in path_parts {
696 path_string.push(delimiter);
697 path_string.push_str(split);
698 }
699
700 Self {
701 path: PathBuf::from(path_string),
702 row,
703 column,
704 }
705 }
706 }
707 }
708
709 pub fn map_path<E>(
710 self,
711 mapping: impl FnOnce(PathBuf) -> Result<PathBuf, E>,
712 ) -> Result<PathWithPosition, E> {
713 Ok(PathWithPosition {
714 path: mapping(self.path)?,
715 row: self.row,
716 column: self.column,
717 })
718 }
719
720 pub fn to_string(&self, path_to_string: &dyn Fn(&PathBuf) -> String) -> String {
721 let path_string = path_to_string(&self.path);
722 if let Some(row) = self.row {
723 if let Some(column) = self.column {
724 format!("{path_string}:{row}:{column}")
725 } else {
726 format!("{path_string}:{row}")
727 }
728 } else {
729 path_string
730 }
731 }
732}
733
734#[derive(Clone)]
735pub struct PathMatcher {
736 sources: Vec<(String, RelPathBuf, bool)>,
737 glob: GlobSet,
738 path_style: PathStyle,
739}
740
741impl std::fmt::Debug for PathMatcher {
742 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
743 f.debug_struct("PathMatcher")
744 .field("sources", &self.sources)
745 .field("path_style", &self.path_style)
746 .finish()
747 }
748}
749
750impl PartialEq for PathMatcher {
751 fn eq(&self, other: &Self) -> bool {
752 self.sources.eq(&other.sources)
753 }
754}
755
756impl Eq for PathMatcher {}
757
758impl PathMatcher {
759 pub fn new(
760 globs: impl IntoIterator<Item = impl AsRef<str>>,
761 path_style: PathStyle,
762 ) -> Result<Self, globset::Error> {
763 let globs = globs
764 .into_iter()
765 .map(|as_str| {
766 GlobBuilder::new(as_str.as_ref())
767 .backslash_escape(path_style.is_posix())
768 .build()
769 })
770 .collect::<Result<Vec<_>, _>>()?;
771 let sources = globs
772 .iter()
773 .filter_map(|glob| {
774 let glob = glob.glob();
775 Some((
776 glob.to_string(),
777 RelPath::new(&glob.as_ref(), path_style)
778 .ok()
779 .map(std::borrow::Cow::into_owned)?,
780 glob.ends_with(path_style.separators_ch()),
781 ))
782 })
783 .collect();
784 let mut glob_builder = GlobSetBuilder::new();
785 for single_glob in globs {
786 glob_builder.add(single_glob);
787 }
788 let glob = glob_builder.build()?;
789 Ok(PathMatcher {
790 glob,
791 sources,
792 path_style,
793 })
794 }
795
796 pub fn sources(&self) -> impl Iterator<Item = &str> + Clone {
797 self.sources.iter().map(|(source, ..)| source.as_str())
798 }
799
800 pub fn is_match<P: AsRef<RelPath>>(&self, other: P) -> bool {
801 let other = other.as_ref();
802 if self
803 .sources
804 .iter()
805 .any(|(_, source, _)| other.starts_with(source) || other.ends_with(source))
806 {
807 return true;
808 }
809 let other_path = other.display(self.path_style);
810
811 if self.glob.is_match(&*other_path) {
812 return true;
813 }
814
815 self.glob
816 .is_match(other_path.into_owned() + self.path_style.primary_separator())
817 }
818
819 pub fn is_match_std_path<P: AsRef<Path>>(&self, other: P) -> bool {
820 let other = other.as_ref();
821 if self.sources.iter().any(|(_, source, _)| {
822 other.starts_with(source.as_std_path()) || other.ends_with(source.as_std_path())
823 }) {
824 return true;
825 }
826 self.glob.is_match(other)
827 }
828}
829
830impl Default for PathMatcher {
831 fn default() -> Self {
832 Self {
833 path_style: PathStyle::local(),
834 glob: GlobSet::empty(),
835 sources: vec![],
836 }
837 }
838}
839
840fn compare_numeric_segments<I>(
875 a_iter: &mut std::iter::Peekable<I>,
876 b_iter: &mut std::iter::Peekable<I>,
877) -> Ordering
878where
879 I: Iterator<Item = char>,
880{
881 let mut a_num_str = String::new();
883 let mut b_num_str = String::new();
884
885 while let Some(&c) = a_iter.peek() {
886 if !c.is_ascii_digit() {
887 break;
888 }
889
890 a_num_str.push(c);
891 a_iter.next();
892 }
893
894 while let Some(&c) = b_iter.peek() {
895 if !c.is_ascii_digit() {
896 break;
897 }
898
899 b_num_str.push(c);
900 b_iter.next();
901 }
902
903 match a_num_str.len().cmp(&b_num_str.len()) {
905 Ordering::Equal => {
906 match a_num_str.cmp(&b_num_str) {
908 Ordering::Equal => Ordering::Equal,
909 ordering => ordering,
910 }
911 }
912
913 ordering => {
915 if let (Ok(a_val), Ok(b_val)) = (a_num_str.parse::<u128>(), b_num_str.parse::<u128>()) {
917 match a_val.cmp(&b_val) {
918 Ordering::Equal => ordering, ord => ord,
920 }
921 } else {
922 a_num_str.cmp(&b_num_str)
924 }
925 }
926 }
927}
928
929pub fn natural_sort(a: &str, b: &str) -> Ordering {
951 let mut a_iter = a.chars().peekable();
952 let mut b_iter = b.chars().peekable();
953
954 loop {
955 match (a_iter.peek(), b_iter.peek()) {
956 (None, None) => {
957 return b.cmp(a);
958 }
959 (None, _) => return Ordering::Less,
960 (_, None) => return Ordering::Greater,
961 (Some(&a_char), Some(&b_char)) => {
962 if a_char.is_ascii_digit() && b_char.is_ascii_digit() {
963 match compare_numeric_segments(&mut a_iter, &mut b_iter) {
964 Ordering::Equal => continue,
965 ordering => return ordering,
966 }
967 } else {
968 match a_char
969 .to_ascii_lowercase()
970 .cmp(&b_char.to_ascii_lowercase())
971 {
972 Ordering::Equal => {
973 a_iter.next();
974 b_iter.next();
975 }
976 ordering => return ordering,
977 }
978 }
979 }
980 }
981 }
982}
983
984fn natural_sort_no_tiebreak(a: &str, b: &str) -> Ordering {
988 if a.eq_ignore_ascii_case(b) {
989 Ordering::Equal
990 } else {
991 natural_sort(a, b)
992 }
993}
994
995fn stem_and_extension(filename: &str) -> (Option<&str>, Option<&str>) {
996 if filename.is_empty() {
997 return (None, None);
998 }
999
1000 match filename.rsplit_once('.') {
1001 None => (Some(filename), None),
1003
1004 Some((before, after)) => {
1006 if before.is_empty() {
1010 (Some(filename), None)
1011 } else {
1012 (Some(before), Some(after))
1014 }
1015 }
1016 }
1017}
1018
1019#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
1021pub enum SortOrder {
1022 #[default]
1025 Default,
1026 Upper,
1029 Lower,
1032 Unicode,
1035}
1036
1037#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
1039pub enum SortMode {
1040 #[default]
1042 DirectoriesFirst,
1043 Mixed,
1045 FilesFirst,
1047}
1048
1049fn case_group_key(name: &str, order: SortOrder) -> u8 {
1050 let first = match name.chars().next() {
1051 Some(c) => c,
1052 None => return 0,
1053 };
1054 match order {
1055 SortOrder::Upper => {
1056 if first.is_lowercase() {
1057 1
1058 } else {
1059 0
1060 }
1061 }
1062 SortOrder::Lower => {
1063 if first.is_uppercase() {
1064 1
1065 } else {
1066 0
1067 }
1068 }
1069 _ => 0,
1070 }
1071}
1072
1073fn compare_strings(a: &str, b: &str, order: SortOrder) -> Ordering {
1074 match order {
1075 SortOrder::Unicode => a.cmp(b),
1076 _ => natural_sort(a, b),
1077 }
1078}
1079
1080fn compare_strings_no_tiebreak(a: &str, b: &str, order: SortOrder) -> Ordering {
1081 match order {
1082 SortOrder::Unicode => a.cmp(b),
1083 _ => natural_sort_no_tiebreak(a, b),
1084 }
1085}
1086
1087pub fn compare_rel_paths(
1088 (path_a, a_is_file): (&RelPath, bool),
1089 (path_b, b_is_file): (&RelPath, bool),
1090) -> Ordering {
1091 compare_rel_paths_by(
1092 (path_a, a_is_file),
1093 (path_b, b_is_file),
1094 SortMode::DirectoriesFirst,
1095 SortOrder::Default,
1096 )
1097}
1098
1099pub fn compare_rel_paths_by(
1100 (path_a, a_is_file): (&RelPath, bool),
1101 (path_b, b_is_file): (&RelPath, bool),
1102 mode: SortMode,
1103 order: SortOrder,
1104) -> Ordering {
1105 let needs_final_tiebreak =
1106 mode != SortMode::DirectoriesFirst && !(std::ptr::eq(path_a, path_b) || path_a == path_b);
1107
1108 let mut components_a = path_a.components();
1109 let mut components_b = path_b.components();
1110
1111 loop {
1112 match (components_a.next(), components_b.next()) {
1113 (Some(component_a), Some(component_b)) => {
1114 let a_leaf_file = a_is_file && components_a.rest().is_empty();
1115 let b_leaf_file = b_is_file && components_b.rest().is_empty();
1116
1117 let file_dir_ordering = match mode {
1118 SortMode::DirectoriesFirst => a_leaf_file.cmp(&b_leaf_file),
1119 SortMode::FilesFirst => b_leaf_file.cmp(&a_leaf_file),
1120 SortMode::Mixed => Ordering::Equal,
1121 };
1122
1123 if !file_dir_ordering.is_eq() {
1124 return file_dir_ordering;
1125 }
1126
1127 let (a_stem, a_ext) = a_leaf_file
1128 .then(|| stem_and_extension(component_a))
1129 .unwrap_or_default();
1130 let (b_stem, b_ext) = b_leaf_file
1131 .then(|| stem_and_extension(component_b))
1132 .unwrap_or_default();
1133 let a_key = if a_leaf_file {
1134 a_stem
1135 } else {
1136 Some(component_a)
1137 };
1138 let b_key = if b_leaf_file {
1139 b_stem
1140 } else {
1141 Some(component_b)
1142 };
1143
1144 let ordering = match (a_key, b_key) {
1145 (Some(a), Some(b)) => {
1146 let name_cmp = case_group_key(a, order)
1147 .cmp(&case_group_key(b, order))
1148 .then_with(|| match mode {
1149 SortMode::DirectoriesFirst => compare_strings(a, b, order),
1150 _ => compare_strings_no_tiebreak(a, b, order),
1151 });
1152
1153 let name_cmp = if mode == SortMode::Mixed {
1154 name_cmp.then_with(|| match (a_leaf_file, b_leaf_file) {
1155 (true, false) if a.eq_ignore_ascii_case(b) => Ordering::Greater,
1156 (false, true) if a.eq_ignore_ascii_case(b) => Ordering::Less,
1157 _ => Ordering::Equal,
1158 })
1159 } else {
1160 name_cmp
1161 };
1162
1163 name_cmp.then_with(|| {
1164 if a_leaf_file && b_leaf_file {
1165 match order {
1166 SortOrder::Unicode => {
1167 a_ext.unwrap_or_default().cmp(b_ext.unwrap_or_default())
1168 }
1169 _ => {
1170 let a_ext_str = a_ext.unwrap_or_default().to_lowercase();
1171 let b_ext_str = b_ext.unwrap_or_default().to_lowercase();
1172 a_ext_str.cmp(&b_ext_str)
1173 }
1174 }
1175 } else {
1176 Ordering::Equal
1177 }
1178 })
1179 }
1180 (Some(_), None) => Ordering::Greater,
1181 (None, Some(_)) => Ordering::Less,
1182 (None, None) => Ordering::Equal,
1183 };
1184
1185 if !ordering.is_eq() {
1186 return ordering;
1187 }
1188 }
1189 (Some(_), None) => return Ordering::Greater,
1190 (None, Some(_)) => return Ordering::Less,
1191 (None, None) => {
1192 if needs_final_tiebreak {
1193 return compare_strings(path_a.as_unix_str(), path_b.as_unix_str(), order);
1194 }
1195 return Ordering::Equal;
1196 }
1197 }
1198 }
1199}
1200
1201pub fn compare_paths(
1202 (path_a, a_is_file): (&Path, bool),
1203 (path_b, b_is_file): (&Path, bool),
1204) -> Ordering {
1205 let mut components_a = path_a.components().peekable();
1206 let mut components_b = path_b.components().peekable();
1207
1208 loop {
1209 match (components_a.next(), components_b.next()) {
1210 (Some(component_a), Some(component_b)) => {
1211 let a_is_file = components_a.peek().is_none() && a_is_file;
1212 let b_is_file = components_b.peek().is_none() && b_is_file;
1213
1214 let ordering = a_is_file.cmp(&b_is_file).then_with(|| {
1215 let path_a = Path::new(component_a.as_os_str());
1216 let path_string_a = if a_is_file {
1217 path_a.file_stem()
1218 } else {
1219 path_a.file_name()
1220 }
1221 .map(|s| s.to_string_lossy());
1222
1223 let path_b = Path::new(component_b.as_os_str());
1224 let path_string_b = if b_is_file {
1225 path_b.file_stem()
1226 } else {
1227 path_b.file_name()
1228 }
1229 .map(|s| s.to_string_lossy());
1230
1231 let compare_components = match (path_string_a, path_string_b) {
1232 (Some(a), Some(b)) => natural_sort(&a, &b),
1233 (Some(_), None) => Ordering::Greater,
1234 (None, Some(_)) => Ordering::Less,
1235 (None, None) => Ordering::Equal,
1236 };
1237
1238 compare_components.then_with(|| {
1239 if a_is_file && b_is_file {
1240 let ext_a = path_a.extension().unwrap_or_default();
1241 let ext_b = path_b.extension().unwrap_or_default();
1242 ext_a.cmp(ext_b)
1243 } else {
1244 Ordering::Equal
1245 }
1246 })
1247 });
1248
1249 if !ordering.is_eq() {
1250 return ordering;
1251 }
1252 }
1253 (Some(_), None) => break Ordering::Greater,
1254 (None, Some(_)) => break Ordering::Less,
1255 (None, None) => break Ordering::Equal,
1256 }
1257 }
1258}
1259
1260#[derive(Debug, Clone, PartialEq, Eq)]
1261pub struct WslPath {
1262 pub distro: String,
1263
1264 pub path: std::ffi::OsString,
1268}
1269
1270impl WslPath {
1271 pub fn from_path<P: AsRef<Path>>(path: P) -> Option<WslPath> {
1272 if cfg!(not(target_os = "windows")) {
1273 return None;
1274 }
1275 use std::{
1276 ffi::OsString,
1277 path::{Component, Prefix},
1278 };
1279
1280 let mut components = path.as_ref().components();
1281 let Some(Component::Prefix(prefix)) = components.next() else {
1282 return None;
1283 };
1284 let (server, distro) = match prefix.kind() {
1285 Prefix::UNC(server, distro) => (server, distro),
1286 Prefix::VerbatimUNC(server, distro) => (server, distro),
1287 _ => return None,
1288 };
1289 let Some(Component::RootDir) = components.next() else {
1290 return None;
1291 };
1292
1293 let server_str = server.to_string_lossy();
1294 if server_str == "wsl.localhost" || server_str == "wsl$" {
1295 let mut result = OsString::from("");
1296 for c in components {
1297 use Component::*;
1298 match c {
1299 Prefix(p) => unreachable!("got {p:?}, but already stripped prefix"),
1300 RootDir => unreachable!("got root dir, but already stripped root"),
1301 CurDir => continue,
1302 ParentDir => result.push("/.."),
1303 Normal(s) => {
1304 result.push("/");
1305 result.push(s);
1306 }
1307 }
1308 }
1309 if result.is_empty() {
1310 result.push("/");
1311 }
1312 Some(WslPath {
1313 distro: distro.to_string_lossy().to_string(),
1314 path: result,
1315 })
1316 } else {
1317 None
1318 }
1319 }
1320}
1321
1322pub trait UrlExt {
1323 fn to_file_path_ext(&self, path_style: PathStyle) -> Result<PathBuf, ()>;
1327}
1328
1329impl UrlExt for url::Url {
1330 fn to_file_path_ext(&self, source_path_style: PathStyle) -> Result<PathBuf, ()> {
1332 if let Some(segments) = self.path_segments() {
1333 let host = match self.host() {
1334 None | Some(url::Host::Domain("localhost")) => None,
1335 Some(_) if source_path_style.is_windows() && self.scheme() == "file" => {
1336 self.host_str()
1337 }
1338 _ => return Err(()),
1339 };
1340
1341 let str_len = self.as_str().len();
1342 let estimated_capacity = if source_path_style.is_windows() {
1343 str_len.saturating_sub(self.scheme().len() + 1)
1345 } else {
1346 str_len.saturating_sub(self.scheme().len() + 3)
1348 };
1349 return match source_path_style {
1350 PathStyle::Unix => {
1351 file_url_segments_to_pathbuf_posix(estimated_capacity, host, segments)
1352 }
1353 PathStyle::Windows => {
1354 file_url_segments_to_pathbuf_windows(estimated_capacity, host, segments)
1355 }
1356 };
1357 }
1358
1359 fn file_url_segments_to_pathbuf_posix(
1360 estimated_capacity: usize,
1361 host: Option<&str>,
1362 segments: std::str::Split<'_, char>,
1363 ) -> Result<PathBuf, ()> {
1364 use percent_encoding::percent_decode;
1365
1366 if host.is_some() {
1367 return Err(());
1368 }
1369
1370 let mut bytes = Vec::new();
1371 bytes.try_reserve(estimated_capacity).map_err(|_| ())?;
1372
1373 for segment in segments {
1374 bytes.push(b'/');
1375 bytes.extend(percent_decode(segment.as_bytes()));
1376 }
1377
1378 if bytes.len() > 2
1380 && bytes[bytes.len() - 2].is_ascii_alphabetic()
1381 && matches!(bytes[bytes.len() - 1], b':' | b'|')
1382 {
1383 bytes.push(b'/');
1384 }
1385
1386 let path = String::from_utf8(bytes).map_err(|_| ())?;
1387 debug_assert!(
1388 PathStyle::Unix.is_absolute(&path),
1389 "to_file_path() failed to produce an absolute Path"
1390 );
1391
1392 Ok(PathBuf::from(path))
1393 }
1394
1395 fn file_url_segments_to_pathbuf_windows(
1396 estimated_capacity: usize,
1397 host: Option<&str>,
1398 mut segments: std::str::Split<'_, char>,
1399 ) -> Result<PathBuf, ()> {
1400 use percent_encoding::percent_decode_str;
1401 let mut string = String::new();
1402 string.try_reserve(estimated_capacity).map_err(|_| ())?;
1403 if let Some(host) = host {
1404 string.push_str(r"\\");
1405 string.push_str(host);
1406 } else {
1407 let first = segments.next().ok_or(())?;
1408
1409 match first.len() {
1410 2 => {
1411 if !first.starts_with(|c| char::is_ascii_alphabetic(&c))
1412 || first.as_bytes()[1] != b':'
1413 {
1414 return Err(());
1415 }
1416
1417 string.push_str(first);
1418 }
1419
1420 4 => {
1421 if !first.starts_with(|c| char::is_ascii_alphabetic(&c)) {
1422 return Err(());
1423 }
1424 let bytes = first.as_bytes();
1425 if bytes[1] != b'%'
1426 || bytes[2] != b'3'
1427 || (bytes[3] != b'a' && bytes[3] != b'A')
1428 {
1429 return Err(());
1430 }
1431
1432 string.push_str(&first[0..1]);
1433 string.push(':');
1434 }
1435
1436 _ => return Err(()),
1437 }
1438 };
1439
1440 for segment in segments {
1441 string.push('\\');
1442
1443 match percent_decode_str(segment).decode_utf8() {
1445 Ok(s) => string.push_str(&s),
1446 Err(..) => return Err(()),
1447 }
1448 }
1449 if cfg!(test) {
1451 debug_assert!(
1452 string.len() <= estimated_capacity,
1453 "len: {}, capacity: {}",
1454 string.len(),
1455 estimated_capacity
1456 );
1457 }
1458 debug_assert!(
1459 PathStyle::Windows.is_absolute(&string),
1460 "to_file_path() failed to produce an absolute Path"
1461 );
1462 let path = PathBuf::from(string);
1463 Ok(path)
1464 }
1465 Err(())
1466 }
1467}
1468
1469#[cfg(test)]
1470mod tests {
1471 use super::*;
1472 use util_macros::perf;
1473
1474 #[test]
1475 fn test_parse_str_treats_paren_suffix_as_position() {
1476 let parsed = PathWithPosition::parse_str("/root/Test (3)");
1480 assert_eq!(parsed.path, PathBuf::from("/root/Test "));
1481 assert_eq!(parsed.row, Some(3));
1482 }
1483
1484 fn rel_path_entry(path: &'static str, is_file: bool) -> (&'static RelPath, bool) {
1485 (RelPath::from_unix_str(path).unwrap(), is_file)
1486 }
1487
1488 fn sorted_rel_paths(
1489 mut paths: Vec<(&'static RelPath, bool)>,
1490 mode: SortMode,
1491 order: SortOrder,
1492 ) -> Vec<(&'static RelPath, bool)> {
1493 paths.sort_by(|&a, &b| compare_rel_paths_by(a, b, mode, order));
1494 paths
1495 }
1496
1497 #[perf]
1498 fn compare_paths_with_dots() {
1499 let mut paths = vec![
1500 (Path::new("test_dirs"), false),
1501 (Path::new("test_dirs/1.46"), false),
1502 (Path::new("test_dirs/1.46/bar_1"), true),
1503 (Path::new("test_dirs/1.46/bar_2"), true),
1504 (Path::new("test_dirs/1.45"), false),
1505 (Path::new("test_dirs/1.45/foo_2"), true),
1506 (Path::new("test_dirs/1.45/foo_1"), true),
1507 ];
1508 paths.sort_by(|&a, &b| compare_paths(a, b));
1509 assert_eq!(
1510 paths,
1511 vec![
1512 (Path::new("test_dirs"), false),
1513 (Path::new("test_dirs/1.45"), false),
1514 (Path::new("test_dirs/1.45/foo_1"), true),
1515 (Path::new("test_dirs/1.45/foo_2"), true),
1516 (Path::new("test_dirs/1.46"), false),
1517 (Path::new("test_dirs/1.46/bar_1"), true),
1518 (Path::new("test_dirs/1.46/bar_2"), true),
1519 ]
1520 );
1521 let mut paths = vec![
1522 (Path::new("root1/one.txt"), true),
1523 (Path::new("root1/one.two.txt"), true),
1524 ];
1525 paths.sort_by(|&a, &b| compare_paths(a, b));
1526 assert_eq!(
1527 paths,
1528 vec![
1529 (Path::new("root1/one.txt"), true),
1530 (Path::new("root1/one.two.txt"), true),
1531 ]
1532 );
1533 }
1534
1535 #[perf]
1536 fn compare_paths_with_same_name_different_extensions() {
1537 let mut paths = vec![
1538 (Path::new("test_dirs/file.rs"), true),
1539 (Path::new("test_dirs/file.txt"), true),
1540 (Path::new("test_dirs/file.md"), true),
1541 (Path::new("test_dirs/file"), true),
1542 (Path::new("test_dirs/file.a"), true),
1543 ];
1544 paths.sort_by(|&a, &b| compare_paths(a, b));
1545 assert_eq!(
1546 paths,
1547 vec![
1548 (Path::new("test_dirs/file"), true),
1549 (Path::new("test_dirs/file.a"), true),
1550 (Path::new("test_dirs/file.md"), true),
1551 (Path::new("test_dirs/file.rs"), true),
1552 (Path::new("test_dirs/file.txt"), true),
1553 ]
1554 );
1555 }
1556
1557 #[perf]
1558 fn compare_paths_case_semi_sensitive() {
1559 let mut paths = vec![
1560 (Path::new("test_DIRS"), false),
1561 (Path::new("test_DIRS/foo_1"), true),
1562 (Path::new("test_DIRS/foo_2"), true),
1563 (Path::new("test_DIRS/bar"), true),
1564 (Path::new("test_DIRS/BAR"), true),
1565 (Path::new("test_dirs"), false),
1566 (Path::new("test_dirs/foo_1"), true),
1567 (Path::new("test_dirs/foo_2"), true),
1568 (Path::new("test_dirs/bar"), true),
1569 (Path::new("test_dirs/BAR"), true),
1570 ];
1571 paths.sort_by(|&a, &b| compare_paths(a, b));
1572 assert_eq!(
1573 paths,
1574 vec![
1575 (Path::new("test_dirs"), false),
1576 (Path::new("test_dirs/bar"), true),
1577 (Path::new("test_dirs/BAR"), true),
1578 (Path::new("test_dirs/foo_1"), true),
1579 (Path::new("test_dirs/foo_2"), true),
1580 (Path::new("test_DIRS"), false),
1581 (Path::new("test_DIRS/bar"), true),
1582 (Path::new("test_DIRS/BAR"), true),
1583 (Path::new("test_DIRS/foo_1"), true),
1584 (Path::new("test_DIRS/foo_2"), true),
1585 ]
1586 );
1587 }
1588
1589 #[perf]
1590 fn compare_paths_mixed_case_numeric_ordering() {
1591 let mut entries = [
1592 (Path::new(".config"), false),
1593 (Path::new("Dir1"), false),
1594 (Path::new("dir01"), false),
1595 (Path::new("dir2"), false),
1596 (Path::new("Dir02"), false),
1597 (Path::new("dir10"), false),
1598 (Path::new("Dir10"), false),
1599 ];
1600
1601 entries.sort_by(|&a, &b| compare_paths(a, b));
1602
1603 let ordered: Vec<&str> = entries
1604 .iter()
1605 .map(|(path, _)| path.to_str().unwrap())
1606 .collect();
1607
1608 assert_eq!(
1609 ordered,
1610 vec![
1611 ".config", "Dir1", "dir01", "dir2", "Dir02", "dir10", "Dir10"
1612 ]
1613 );
1614 }
1615
1616 #[perf]
1617 fn compare_rel_paths_mixed_case_insensitive() {
1618 let mut paths = vec![
1620 (RelPath::from_unix_str("zebra.txt").unwrap(), true),
1621 (RelPath::from_unix_str("Apple").unwrap(), false),
1622 (RelPath::from_unix_str("banana.rs").unwrap(), true),
1623 (RelPath::from_unix_str("Carrot").unwrap(), false),
1624 (RelPath::from_unix_str("aardvark.txt").unwrap(), true),
1625 ];
1626 paths.sort_by(|&a, &b| compare_rel_paths_by(a, b, SortMode::Mixed, SortOrder::Default));
1627 assert_eq!(
1629 paths,
1630 vec![
1631 (RelPath::from_unix_str("aardvark.txt").unwrap(), true),
1632 (RelPath::from_unix_str("Apple").unwrap(), false),
1633 (RelPath::from_unix_str("banana.rs").unwrap(), true),
1634 (RelPath::from_unix_str("Carrot").unwrap(), false),
1635 (RelPath::from_unix_str("zebra.txt").unwrap(), true),
1636 ]
1637 );
1638 }
1639
1640 #[perf]
1641 fn compare_rel_paths_files_first_basic() {
1642 let mut paths = vec![
1644 (RelPath::from_unix_str("zebra.txt").unwrap(), true),
1645 (RelPath::from_unix_str("Apple").unwrap(), false),
1646 (RelPath::from_unix_str("banana.rs").unwrap(), true),
1647 (RelPath::from_unix_str("Carrot").unwrap(), false),
1648 (RelPath::from_unix_str("aardvark.txt").unwrap(), true),
1649 ];
1650 paths
1651 .sort_by(|&a, &b| compare_rel_paths_by(a, b, SortMode::FilesFirst, SortOrder::Default));
1652 assert_eq!(
1654 paths,
1655 vec![
1656 (RelPath::from_unix_str("aardvark.txt").unwrap(), true),
1657 (RelPath::from_unix_str("banana.rs").unwrap(), true),
1658 (RelPath::from_unix_str("zebra.txt").unwrap(), true),
1659 (RelPath::from_unix_str("Apple").unwrap(), false),
1660 (RelPath::from_unix_str("Carrot").unwrap(), false),
1661 ]
1662 );
1663 }
1664
1665 #[perf]
1666 fn compare_rel_paths_files_first_case_insensitive() {
1667 let mut paths = vec![
1669 (RelPath::from_unix_str("Zebra.txt").unwrap(), true),
1670 (RelPath::from_unix_str("apple").unwrap(), false),
1671 (RelPath::from_unix_str("Banana.rs").unwrap(), true),
1672 (RelPath::from_unix_str("carrot").unwrap(), false),
1673 (RelPath::from_unix_str("Aardvark.txt").unwrap(), true),
1674 ];
1675 paths
1676 .sort_by(|&a, &b| compare_rel_paths_by(a, b, SortMode::FilesFirst, SortOrder::Default));
1677 assert_eq!(
1678 paths,
1679 vec![
1680 (RelPath::from_unix_str("Aardvark.txt").unwrap(), true),
1681 (RelPath::from_unix_str("Banana.rs").unwrap(), true),
1682 (RelPath::from_unix_str("Zebra.txt").unwrap(), true),
1683 (RelPath::from_unix_str("apple").unwrap(), false),
1684 (RelPath::from_unix_str("carrot").unwrap(), false),
1685 ]
1686 );
1687 }
1688
1689 #[perf]
1690 fn compare_rel_paths_files_first_numeric() {
1691 let mut paths = vec![
1693 (RelPath::from_unix_str("file10.txt").unwrap(), true),
1694 (RelPath::from_unix_str("dir2").unwrap(), false),
1695 (RelPath::from_unix_str("file2.txt").unwrap(), true),
1696 (RelPath::from_unix_str("dir10").unwrap(), false),
1697 (RelPath::from_unix_str("file1.txt").unwrap(), true),
1698 ];
1699 paths
1700 .sort_by(|&a, &b| compare_rel_paths_by(a, b, SortMode::FilesFirst, SortOrder::Default));
1701 assert_eq!(
1702 paths,
1703 vec![
1704 (RelPath::from_unix_str("file1.txt").unwrap(), true),
1705 (RelPath::from_unix_str("file2.txt").unwrap(), true),
1706 (RelPath::from_unix_str("file10.txt").unwrap(), true),
1707 (RelPath::from_unix_str("dir2").unwrap(), false),
1708 (RelPath::from_unix_str("dir10").unwrap(), false),
1709 ]
1710 );
1711 }
1712
1713 #[perf]
1714 fn compare_rel_paths_mixed_case() {
1715 let mut paths = vec![
1717 (RelPath::from_unix_str("README.md").unwrap(), true),
1718 (RelPath::from_unix_str("readme.txt").unwrap(), true),
1719 (RelPath::from_unix_str("ReadMe.rs").unwrap(), true),
1720 ];
1721 paths.sort_by(|&a, &b| compare_rel_paths_by(a, b, SortMode::Mixed, SortOrder::Default));
1722 assert_eq!(
1724 paths,
1725 vec![
1726 (RelPath::from_unix_str("README.md").unwrap(), true),
1727 (RelPath::from_unix_str("ReadMe.rs").unwrap(), true),
1728 (RelPath::from_unix_str("readme.txt").unwrap(), true),
1729 ]
1730 );
1731 }
1732
1733 #[perf]
1734 fn compare_rel_paths_mixed_files_and_dirs() {
1735 let mut paths = vec![
1737 (RelPath::from_unix_str("file2.txt").unwrap(), true),
1738 (RelPath::from_unix_str("Dir1").unwrap(), false),
1739 (RelPath::from_unix_str("file1.txt").unwrap(), true),
1740 (RelPath::from_unix_str("dir2").unwrap(), false),
1741 ];
1742 paths.sort_by(|&a, &b| compare_rel_paths_by(a, b, SortMode::Mixed, SortOrder::Default));
1743 assert_eq!(
1745 paths,
1746 vec![
1747 (RelPath::from_unix_str("Dir1").unwrap(), false),
1748 (RelPath::from_unix_str("dir2").unwrap(), false),
1749 (RelPath::from_unix_str("file1.txt").unwrap(), true),
1750 (RelPath::from_unix_str("file2.txt").unwrap(), true),
1751 ]
1752 );
1753 }
1754
1755 #[perf]
1756 fn compare_rel_paths_mixed_same_name_different_case_file_and_dir() {
1757 let mut paths = vec![
1758 (RelPath::from_unix_str("Hello.txt").unwrap(), true),
1759 (RelPath::from_unix_str("hello").unwrap(), false),
1760 ];
1761 paths.sort_by(|&a, &b| compare_rel_paths_by(a, b, SortMode::Mixed, SortOrder::Default));
1762 assert_eq!(
1763 paths,
1764 vec![
1765 (RelPath::from_unix_str("hello").unwrap(), false),
1766 (RelPath::from_unix_str("Hello.txt").unwrap(), true),
1767 ]
1768 );
1769
1770 let mut paths = vec![
1771 (RelPath::from_unix_str("hello").unwrap(), false),
1772 (RelPath::from_unix_str("Hello.txt").unwrap(), true),
1773 ];
1774 paths.sort_by(|&a, &b| compare_rel_paths_by(a, b, SortMode::Mixed, SortOrder::Default));
1775 assert_eq!(
1776 paths,
1777 vec![
1778 (RelPath::from_unix_str("hello").unwrap(), false),
1779 (RelPath::from_unix_str("Hello.txt").unwrap(), true),
1780 ]
1781 );
1782 }
1783
1784 #[perf]
1785 fn compare_rel_paths_mixed_with_nested_paths() {
1786 let mut paths = vec![
1788 (RelPath::from_unix_str("src/main.rs").unwrap(), true),
1789 (RelPath::from_unix_str("Cargo.toml").unwrap(), true),
1790 (RelPath::from_unix_str("src").unwrap(), false),
1791 (RelPath::from_unix_str("target").unwrap(), false),
1792 ];
1793 paths.sort_by(|&a, &b| compare_rel_paths_by(a, b, SortMode::Mixed, SortOrder::Default));
1794 assert_eq!(
1795 paths,
1796 vec![
1797 (RelPath::from_unix_str("Cargo.toml").unwrap(), true),
1798 (RelPath::from_unix_str("src").unwrap(), false),
1799 (RelPath::from_unix_str("src/main.rs").unwrap(), true),
1800 (RelPath::from_unix_str("target").unwrap(), false),
1801 ]
1802 );
1803 }
1804
1805 #[perf]
1806 fn compare_rel_paths_files_first_with_nested() {
1807 let mut paths = vec![
1809 (RelPath::from_unix_str("src/lib.rs").unwrap(), true),
1810 (RelPath::from_unix_str("README.md").unwrap(), true),
1811 (RelPath::from_unix_str("src").unwrap(), false),
1812 (RelPath::from_unix_str("tests").unwrap(), false),
1813 ];
1814 paths
1815 .sort_by(|&a, &b| compare_rel_paths_by(a, b, SortMode::FilesFirst, SortOrder::Default));
1816 assert_eq!(
1817 paths,
1818 vec![
1819 (RelPath::from_unix_str("README.md").unwrap(), true),
1820 (RelPath::from_unix_str("src").unwrap(), false),
1821 (RelPath::from_unix_str("src/lib.rs").unwrap(), true),
1822 (RelPath::from_unix_str("tests").unwrap(), false),
1823 ]
1824 );
1825 }
1826
1827 #[perf]
1828 fn compare_rel_paths_mixed_dotfiles() {
1829 let mut paths = vec![
1831 (RelPath::from_unix_str(".gitignore").unwrap(), true),
1832 (RelPath::from_unix_str("README.md").unwrap(), true),
1833 (RelPath::from_unix_str(".github").unwrap(), false),
1834 (RelPath::from_unix_str("src").unwrap(), false),
1835 ];
1836 paths.sort_by(|&a, &b| compare_rel_paths_by(a, b, SortMode::Mixed, SortOrder::Default));
1837 assert_eq!(
1838 paths,
1839 vec![
1840 (RelPath::from_unix_str(".github").unwrap(), false),
1841 (RelPath::from_unix_str(".gitignore").unwrap(), true),
1842 (RelPath::from_unix_str("README.md").unwrap(), true),
1843 (RelPath::from_unix_str("src").unwrap(), false),
1844 ]
1845 );
1846 }
1847
1848 #[perf]
1849 fn compare_rel_paths_files_first_dotfiles() {
1850 let mut paths = vec![
1852 (RelPath::from_unix_str(".gitignore").unwrap(), true),
1853 (RelPath::from_unix_str("README.md").unwrap(), true),
1854 (RelPath::from_unix_str(".github").unwrap(), false),
1855 (RelPath::from_unix_str("src").unwrap(), false),
1856 ];
1857 paths
1858 .sort_by(|&a, &b| compare_rel_paths_by(a, b, SortMode::FilesFirst, SortOrder::Default));
1859 assert_eq!(
1860 paths,
1861 vec![
1862 (RelPath::from_unix_str(".gitignore").unwrap(), true),
1863 (RelPath::from_unix_str("README.md").unwrap(), true),
1864 (RelPath::from_unix_str(".github").unwrap(), false),
1865 (RelPath::from_unix_str("src").unwrap(), false),
1866 ]
1867 );
1868 }
1869
1870 #[perf]
1871 fn compare_rel_paths_mixed_same_stem_different_extension() {
1872 let mut paths = vec![
1874 (RelPath::from_unix_str("file.rs").unwrap(), true),
1875 (RelPath::from_unix_str("file.md").unwrap(), true),
1876 (RelPath::from_unix_str("file.txt").unwrap(), true),
1877 ];
1878 paths.sort_by(|&a, &b| compare_rel_paths_by(a, b, SortMode::Mixed, SortOrder::Default));
1879 assert_eq!(
1880 paths,
1881 vec![
1882 (RelPath::from_unix_str("file.md").unwrap(), true),
1883 (RelPath::from_unix_str("file.rs").unwrap(), true),
1884 (RelPath::from_unix_str("file.txt").unwrap(), true),
1885 ]
1886 );
1887 }
1888
1889 #[perf]
1890 fn compare_rel_paths_files_first_same_stem() {
1891 let mut paths = vec![
1893 (RelPath::from_unix_str("main.rs").unwrap(), true),
1894 (RelPath::from_unix_str("main.c").unwrap(), true),
1895 (RelPath::from_unix_str("main").unwrap(), false),
1896 ];
1897 paths
1898 .sort_by(|&a, &b| compare_rel_paths_by(a, b, SortMode::FilesFirst, SortOrder::Default));
1899 assert_eq!(
1900 paths,
1901 vec![
1902 (RelPath::from_unix_str("main.c").unwrap(), true),
1903 (RelPath::from_unix_str("main.rs").unwrap(), true),
1904 (RelPath::from_unix_str("main").unwrap(), false),
1905 ]
1906 );
1907 }
1908
1909 #[perf]
1910 fn compare_rel_paths_mixed_deep_nesting() {
1911 let mut paths = vec![
1913 (RelPath::from_unix_str("a/b/c.txt").unwrap(), true),
1914 (RelPath::from_unix_str("A/B.txt").unwrap(), true),
1915 (RelPath::from_unix_str("a.txt").unwrap(), true),
1916 (RelPath::from_unix_str("A.txt").unwrap(), true),
1917 ];
1918 paths.sort_by(|&a, &b| compare_rel_paths_by(a, b, SortMode::Mixed, SortOrder::Default));
1919 assert_eq!(
1920 paths,
1921 vec![
1922 (RelPath::from_unix_str("a/b/c.txt").unwrap(), true),
1923 (RelPath::from_unix_str("A/B.txt").unwrap(), true),
1924 (RelPath::from_unix_str("a.txt").unwrap(), true),
1925 (RelPath::from_unix_str("A.txt").unwrap(), true),
1926 ]
1927 );
1928 }
1929
1930 #[perf]
1931 fn compare_rel_paths_upper() {
1932 let directories_only_paths = vec![
1933 rel_path_entry("mixedCase", false),
1934 rel_path_entry("Zebra", false),
1935 rel_path_entry("banana", false),
1936 rel_path_entry("ALLCAPS", false),
1937 rel_path_entry("Apple", false),
1938 rel_path_entry("dog", false),
1939 rel_path_entry(".hidden", false),
1940 rel_path_entry("Carrot", false),
1941 ];
1942 assert_eq!(
1943 sorted_rel_paths(
1944 directories_only_paths,
1945 SortMode::DirectoriesFirst,
1946 SortOrder::Upper,
1947 ),
1948 vec![
1949 rel_path_entry(".hidden", false),
1950 rel_path_entry("ALLCAPS", false),
1951 rel_path_entry("Apple", false),
1952 rel_path_entry("Carrot", false),
1953 rel_path_entry("Zebra", false),
1954 rel_path_entry("banana", false),
1955 rel_path_entry("dog", false),
1956 rel_path_entry("mixedCase", false),
1957 ]
1958 );
1959
1960 let file_and_directory_paths = vec![
1961 rel_path_entry("banana", false),
1962 rel_path_entry("Apple.txt", true),
1963 rel_path_entry("dog.md", true),
1964 rel_path_entry("ALLCAPS", false),
1965 rel_path_entry("file1.txt", true),
1966 rel_path_entry("File2.txt", true),
1967 rel_path_entry(".hidden", false),
1968 ];
1969 assert_eq!(
1970 sorted_rel_paths(
1971 file_and_directory_paths.clone(),
1972 SortMode::DirectoriesFirst,
1973 SortOrder::Upper,
1974 ),
1975 vec![
1976 rel_path_entry(".hidden", false),
1977 rel_path_entry("ALLCAPS", false),
1978 rel_path_entry("banana", false),
1979 rel_path_entry("Apple.txt", true),
1980 rel_path_entry("File2.txt", true),
1981 rel_path_entry("dog.md", true),
1982 rel_path_entry("file1.txt", true),
1983 ]
1984 );
1985 assert_eq!(
1986 sorted_rel_paths(
1987 file_and_directory_paths.clone(),
1988 SortMode::Mixed,
1989 SortOrder::Upper,
1990 ),
1991 vec![
1992 rel_path_entry(".hidden", false),
1993 rel_path_entry("ALLCAPS", false),
1994 rel_path_entry("Apple.txt", true),
1995 rel_path_entry("File2.txt", true),
1996 rel_path_entry("banana", false),
1997 rel_path_entry("dog.md", true),
1998 rel_path_entry("file1.txt", true),
1999 ]
2000 );
2001 assert_eq!(
2002 sorted_rel_paths(
2003 file_and_directory_paths,
2004 SortMode::FilesFirst,
2005 SortOrder::Upper,
2006 ),
2007 vec![
2008 rel_path_entry("Apple.txt", true),
2009 rel_path_entry("File2.txt", true),
2010 rel_path_entry("dog.md", true),
2011 rel_path_entry("file1.txt", true),
2012 rel_path_entry(".hidden", false),
2013 rel_path_entry("ALLCAPS", false),
2014 rel_path_entry("banana", false),
2015 ]
2016 );
2017
2018 let natural_sort_paths = vec![
2019 rel_path_entry("file10.txt", true),
2020 rel_path_entry("file1.txt", true),
2021 rel_path_entry("file20.txt", true),
2022 rel_path_entry("file2.txt", true),
2023 ];
2024 assert_eq!(
2025 sorted_rel_paths(natural_sort_paths, SortMode::Mixed, SortOrder::Upper,),
2026 vec![
2027 rel_path_entry("file1.txt", true),
2028 rel_path_entry("file2.txt", true),
2029 rel_path_entry("file10.txt", true),
2030 rel_path_entry("file20.txt", true),
2031 ]
2032 );
2033
2034 let accented_paths = vec![
2035 rel_path_entry("\u{00C9}something.txt", true),
2036 rel_path_entry("zebra.txt", true),
2037 rel_path_entry("Apple.txt", true),
2038 ];
2039 assert_eq!(
2040 sorted_rel_paths(accented_paths, SortMode::Mixed, SortOrder::Upper),
2041 vec![
2042 rel_path_entry("Apple.txt", true),
2043 rel_path_entry("\u{00C9}something.txt", true),
2044 rel_path_entry("zebra.txt", true),
2045 ]
2046 );
2047 }
2048
2049 #[perf]
2050 fn compare_rel_paths_lower() {
2051 let directories_only_paths = vec![
2052 rel_path_entry("mixedCase", false),
2053 rel_path_entry("Zebra", false),
2054 rel_path_entry("banana", false),
2055 rel_path_entry("ALLCAPS", false),
2056 rel_path_entry("Apple", false),
2057 rel_path_entry("dog", false),
2058 rel_path_entry(".hidden", false),
2059 rel_path_entry("Carrot", false),
2060 ];
2061 assert_eq!(
2062 sorted_rel_paths(
2063 directories_only_paths,
2064 SortMode::DirectoriesFirst,
2065 SortOrder::Lower,
2066 ),
2067 vec![
2068 rel_path_entry(".hidden", false),
2069 rel_path_entry("banana", false),
2070 rel_path_entry("dog", false),
2071 rel_path_entry("mixedCase", false),
2072 rel_path_entry("ALLCAPS", false),
2073 rel_path_entry("Apple", false),
2074 rel_path_entry("Carrot", false),
2075 rel_path_entry("Zebra", false),
2076 ]
2077 );
2078
2079 let file_and_directory_paths = vec![
2080 rel_path_entry("banana", false),
2081 rel_path_entry("Apple.txt", true),
2082 rel_path_entry("dog.md", true),
2083 rel_path_entry("ALLCAPS", false),
2084 rel_path_entry("file1.txt", true),
2085 rel_path_entry("File2.txt", true),
2086 rel_path_entry(".hidden", false),
2087 ];
2088 assert_eq!(
2089 sorted_rel_paths(
2090 file_and_directory_paths.clone(),
2091 SortMode::DirectoriesFirst,
2092 SortOrder::Lower,
2093 ),
2094 vec![
2095 rel_path_entry(".hidden", false),
2096 rel_path_entry("banana", false),
2097 rel_path_entry("ALLCAPS", false),
2098 rel_path_entry("dog.md", true),
2099 rel_path_entry("file1.txt", true),
2100 rel_path_entry("Apple.txt", true),
2101 rel_path_entry("File2.txt", true),
2102 ]
2103 );
2104 assert_eq!(
2105 sorted_rel_paths(
2106 file_and_directory_paths.clone(),
2107 SortMode::Mixed,
2108 SortOrder::Lower,
2109 ),
2110 vec![
2111 rel_path_entry(".hidden", false),
2112 rel_path_entry("banana", false),
2113 rel_path_entry("dog.md", true),
2114 rel_path_entry("file1.txt", true),
2115 rel_path_entry("ALLCAPS", false),
2116 rel_path_entry("Apple.txt", true),
2117 rel_path_entry("File2.txt", true),
2118 ]
2119 );
2120 assert_eq!(
2121 sorted_rel_paths(
2122 file_and_directory_paths,
2123 SortMode::FilesFirst,
2124 SortOrder::Lower,
2125 ),
2126 vec![
2127 rel_path_entry("dog.md", true),
2128 rel_path_entry("file1.txt", true),
2129 rel_path_entry("Apple.txt", true),
2130 rel_path_entry("File2.txt", true),
2131 rel_path_entry(".hidden", false),
2132 rel_path_entry("banana", false),
2133 rel_path_entry("ALLCAPS", false),
2134 ]
2135 );
2136 }
2137
2138 #[perf]
2139 fn compare_rel_paths_unicode() {
2140 let directories_only_paths = vec![
2141 rel_path_entry("mixedCase", false),
2142 rel_path_entry("Zebra", false),
2143 rel_path_entry("banana", false),
2144 rel_path_entry("ALLCAPS", false),
2145 rel_path_entry("Apple", false),
2146 rel_path_entry("dog", false),
2147 rel_path_entry(".hidden", false),
2148 rel_path_entry("Carrot", false),
2149 ];
2150 assert_eq!(
2151 sorted_rel_paths(
2152 directories_only_paths,
2153 SortMode::DirectoriesFirst,
2154 SortOrder::Unicode,
2155 ),
2156 vec![
2157 rel_path_entry(".hidden", false),
2158 rel_path_entry("ALLCAPS", false),
2159 rel_path_entry("Apple", false),
2160 rel_path_entry("Carrot", false),
2161 rel_path_entry("Zebra", false),
2162 rel_path_entry("banana", false),
2163 rel_path_entry("dog", false),
2164 rel_path_entry("mixedCase", false),
2165 ]
2166 );
2167
2168 let file_and_directory_paths = vec![
2169 rel_path_entry("banana", false),
2170 rel_path_entry("Apple.txt", true),
2171 rel_path_entry("dog.md", true),
2172 rel_path_entry("ALLCAPS", false),
2173 rel_path_entry("file1.txt", true),
2174 rel_path_entry("File2.txt", true),
2175 rel_path_entry(".hidden", false),
2176 ];
2177 assert_eq!(
2178 sorted_rel_paths(
2179 file_and_directory_paths.clone(),
2180 SortMode::DirectoriesFirst,
2181 SortOrder::Unicode,
2182 ),
2183 vec![
2184 rel_path_entry(".hidden", false),
2185 rel_path_entry("ALLCAPS", false),
2186 rel_path_entry("banana", false),
2187 rel_path_entry("Apple.txt", true),
2188 rel_path_entry("File2.txt", true),
2189 rel_path_entry("dog.md", true),
2190 rel_path_entry("file1.txt", true),
2191 ]
2192 );
2193 assert_eq!(
2194 sorted_rel_paths(
2195 file_and_directory_paths.clone(),
2196 SortMode::Mixed,
2197 SortOrder::Unicode,
2198 ),
2199 vec![
2200 rel_path_entry(".hidden", false),
2201 rel_path_entry("ALLCAPS", false),
2202 rel_path_entry("Apple.txt", true),
2203 rel_path_entry("File2.txt", true),
2204 rel_path_entry("banana", false),
2205 rel_path_entry("dog.md", true),
2206 rel_path_entry("file1.txt", true),
2207 ]
2208 );
2209 assert_eq!(
2210 sorted_rel_paths(
2211 file_and_directory_paths,
2212 SortMode::FilesFirst,
2213 SortOrder::Unicode,
2214 ),
2215 vec![
2216 rel_path_entry("Apple.txt", true),
2217 rel_path_entry("File2.txt", true),
2218 rel_path_entry("dog.md", true),
2219 rel_path_entry("file1.txt", true),
2220 rel_path_entry(".hidden", false),
2221 rel_path_entry("ALLCAPS", false),
2222 rel_path_entry("banana", false),
2223 ]
2224 );
2225
2226 let numeric_paths = vec![
2227 rel_path_entry("file10.txt", true),
2228 rel_path_entry("file1.txt", true),
2229 rel_path_entry("file2.txt", true),
2230 rel_path_entry("file20.txt", true),
2231 ];
2232 assert_eq!(
2233 sorted_rel_paths(numeric_paths, SortMode::Mixed, SortOrder::Unicode,),
2234 vec![
2235 rel_path_entry("file1.txt", true),
2236 rel_path_entry("file10.txt", true),
2237 rel_path_entry("file2.txt", true),
2238 rel_path_entry("file20.txt", true),
2239 ]
2240 );
2241
2242 let accented_paths = vec![
2243 rel_path_entry("\u{00C9}something.txt", true),
2244 rel_path_entry("zebra.txt", true),
2245 rel_path_entry("Apple.txt", true),
2246 ];
2247 assert_eq!(
2248 sorted_rel_paths(accented_paths, SortMode::Mixed, SortOrder::Unicode),
2249 vec![
2250 rel_path_entry("Apple.txt", true),
2251 rel_path_entry("zebra.txt", true),
2252 rel_path_entry("\u{00C9}something.txt", true),
2253 ]
2254 );
2255 }
2256
2257 #[perf]
2258 fn path_with_position_parse_posix_path() {
2259 assert_eq!(
2262 PathWithPosition::parse_str("test_file"),
2263 PathWithPosition {
2264 path: PathBuf::from("test_file"),
2265 row: None,
2266 column: None
2267 }
2268 );
2269
2270 assert_eq!(
2271 PathWithPosition::parse_str("a:bc:.zip:1"),
2272 PathWithPosition {
2273 path: PathBuf::from("a:bc:.zip"),
2274 row: Some(1),
2275 column: None
2276 }
2277 );
2278
2279 assert_eq!(
2280 PathWithPosition::parse_str("one.second.zip:1"),
2281 PathWithPosition {
2282 path: PathBuf::from("one.second.zip"),
2283 row: Some(1),
2284 column: None
2285 }
2286 );
2287
2288 assert_eq!(
2290 PathWithPosition::parse_str("test_file:10:1:"),
2291 PathWithPosition {
2292 path: PathBuf::from("test_file"),
2293 row: Some(10),
2294 column: Some(1)
2295 }
2296 );
2297
2298 assert_eq!(
2299 PathWithPosition::parse_str("test_file.rs:"),
2300 PathWithPosition {
2301 path: PathBuf::from("test_file.rs"),
2302 row: None,
2303 column: None
2304 }
2305 );
2306
2307 assert_eq!(
2308 PathWithPosition::parse_str("test_file.rs:1:"),
2309 PathWithPosition {
2310 path: PathBuf::from("test_file.rs"),
2311 row: Some(1),
2312 column: None
2313 }
2314 );
2315
2316 assert_eq!(
2317 PathWithPosition::parse_str("ab\ncd"),
2318 PathWithPosition {
2319 path: PathBuf::from("ab\ncd"),
2320 row: None,
2321 column: None
2322 }
2323 );
2324
2325 assert_eq!(
2326 PathWithPosition::parse_str("👋\nab"),
2327 PathWithPosition {
2328 path: PathBuf::from("👋\nab"),
2329 row: None,
2330 column: None
2331 }
2332 );
2333
2334 assert_eq!(
2335 PathWithPosition::parse_str("Types.hs:(617,9)-(670,28):"),
2336 PathWithPosition {
2337 path: PathBuf::from("Types.hs"),
2338 row: Some(617),
2339 column: Some(9),
2340 }
2341 );
2342
2343 assert_eq!(
2344 PathWithPosition::parse_str("main (1).log"),
2345 PathWithPosition {
2346 path: PathBuf::from("main (1).log"),
2347 row: None,
2348 column: None
2349 }
2350 );
2351 }
2352
2353 #[perf]
2354 #[cfg(not(target_os = "windows"))]
2355 fn path_with_position_parse_posix_path_with_suffix() {
2356 assert_eq!(
2357 PathWithPosition::parse_str("foo/bar:34:in"),
2358 PathWithPosition {
2359 path: PathBuf::from("foo/bar"),
2360 row: Some(34),
2361 column: None,
2362 }
2363 );
2364 assert_eq!(
2365 PathWithPosition::parse_str("foo/bar.rs:1902:::15:"),
2366 PathWithPosition {
2367 path: PathBuf::from("foo/bar.rs:1902"),
2368 row: Some(15),
2369 column: None
2370 }
2371 );
2372
2373 assert_eq!(
2374 PathWithPosition::parse_str("app-editors:zed-0.143.6:20240710-201212.log:34:"),
2375 PathWithPosition {
2376 path: PathBuf::from("app-editors:zed-0.143.6:20240710-201212.log"),
2377 row: Some(34),
2378 column: None,
2379 }
2380 );
2381
2382 assert_eq!(
2383 PathWithPosition::parse_str("crates/file_finder/src/file_finder.rs:1902:13:"),
2384 PathWithPosition {
2385 path: PathBuf::from("crates/file_finder/src/file_finder.rs"),
2386 row: Some(1902),
2387 column: Some(13),
2388 }
2389 );
2390
2391 assert_eq!(
2392 PathWithPosition::parse_str("crate/utils/src/test:today.log:34"),
2393 PathWithPosition {
2394 path: PathBuf::from("crate/utils/src/test:today.log"),
2395 row: Some(34),
2396 column: None,
2397 }
2398 );
2399 assert_eq!(
2400 PathWithPosition::parse_str("/testing/out/src/file_finder.odin(7:15)"),
2401 PathWithPosition {
2402 path: PathBuf::from("/testing/out/src/file_finder.odin"),
2403 row: Some(7),
2404 column: Some(15),
2405 }
2406 );
2407 }
2408
2409 #[perf]
2410 #[cfg(target_os = "windows")]
2411 fn path_with_position_parse_windows_path() {
2412 assert_eq!(
2413 PathWithPosition::parse_str("crates\\utils\\paths.rs"),
2414 PathWithPosition {
2415 path: PathBuf::from("crates\\utils\\paths.rs"),
2416 row: None,
2417 column: None
2418 }
2419 );
2420
2421 assert_eq!(
2422 PathWithPosition::parse_str("C:\\Users\\someone\\test_file.rs"),
2423 PathWithPosition {
2424 path: PathBuf::from("C:\\Users\\someone\\test_file.rs"),
2425 row: None,
2426 column: None
2427 }
2428 );
2429
2430 assert_eq!(
2431 PathWithPosition::parse_str("C:\\Users\\someone\\main (1).log"),
2432 PathWithPosition {
2433 path: PathBuf::from("C:\\Users\\someone\\main (1).log"),
2434 row: None,
2435 column: None
2436 }
2437 );
2438 }
2439
2440 #[perf]
2441 #[cfg(target_os = "windows")]
2442 fn path_with_position_parse_windows_path_with_suffix() {
2443 assert_eq!(
2444 PathWithPosition::parse_str("crates\\utils\\paths.rs:101"),
2445 PathWithPosition {
2446 path: PathBuf::from("crates\\utils\\paths.rs"),
2447 row: Some(101),
2448 column: None
2449 }
2450 );
2451
2452 assert_eq!(
2453 PathWithPosition::parse_str("\\\\?\\C:\\Users\\someone\\test_file.rs:1:20"),
2454 PathWithPosition {
2455 path: PathBuf::from("\\\\?\\C:\\Users\\someone\\test_file.rs"),
2456 row: Some(1),
2457 column: Some(20)
2458 }
2459 );
2460
2461 assert_eq!(
2462 PathWithPosition::parse_str("C:\\Users\\someone\\test_file.rs(1902,13)"),
2463 PathWithPosition {
2464 path: PathBuf::from("C:\\Users\\someone\\test_file.rs"),
2465 row: Some(1902),
2466 column: Some(13)
2467 }
2468 );
2469
2470 assert_eq!(
2472 PathWithPosition::parse_str("\\\\?\\C:\\Users\\someone\\test_file.rs:1902:13:"),
2473 PathWithPosition {
2474 path: PathBuf::from("\\\\?\\C:\\Users\\someone\\test_file.rs"),
2475 row: Some(1902),
2476 column: Some(13)
2477 }
2478 );
2479
2480 assert_eq!(
2481 PathWithPosition::parse_str("\\\\?\\C:\\Users\\someone\\test_file.rs:1902:13:15:"),
2482 PathWithPosition {
2483 path: PathBuf::from("\\\\?\\C:\\Users\\someone\\test_file.rs:1902"),
2484 row: Some(13),
2485 column: Some(15)
2486 }
2487 );
2488
2489 assert_eq!(
2490 PathWithPosition::parse_str("\\\\?\\C:\\Users\\someone\\test_file.rs:1902:::15:"),
2491 PathWithPosition {
2492 path: PathBuf::from("\\\\?\\C:\\Users\\someone\\test_file.rs:1902"),
2493 row: Some(15),
2494 column: None
2495 }
2496 );
2497
2498 assert_eq!(
2499 PathWithPosition::parse_str("\\\\?\\C:\\Users\\someone\\test_file.rs(1902,13):"),
2500 PathWithPosition {
2501 path: PathBuf::from("\\\\?\\C:\\Users\\someone\\test_file.rs"),
2502 row: Some(1902),
2503 column: Some(13),
2504 }
2505 );
2506
2507 assert_eq!(
2508 PathWithPosition::parse_str("\\\\?\\C:\\Users\\someone\\test_file.rs(1902):"),
2509 PathWithPosition {
2510 path: PathBuf::from("\\\\?\\C:\\Users\\someone\\test_file.rs"),
2511 row: Some(1902),
2512 column: None,
2513 }
2514 );
2515
2516 assert_eq!(
2517 PathWithPosition::parse_str("C:\\Users\\someone\\test_file.rs:1902:13:"),
2518 PathWithPosition {
2519 path: PathBuf::from("C:\\Users\\someone\\test_file.rs"),
2520 row: Some(1902),
2521 column: Some(13),
2522 }
2523 );
2524
2525 assert_eq!(
2526 PathWithPosition::parse_str("C:\\Users\\someone\\test_file.rs(1902,13):"),
2527 PathWithPosition {
2528 path: PathBuf::from("C:\\Users\\someone\\test_file.rs"),
2529 row: Some(1902),
2530 column: Some(13),
2531 }
2532 );
2533
2534 assert_eq!(
2535 PathWithPosition::parse_str("C:\\Users\\someone\\test_file.rs(1902):"),
2536 PathWithPosition {
2537 path: PathBuf::from("C:\\Users\\someone\\test_file.rs"),
2538 row: Some(1902),
2539 column: None,
2540 }
2541 );
2542
2543 assert_eq!(
2544 PathWithPosition::parse_str("crates/utils/paths.rs:101"),
2545 PathWithPosition {
2546 path: PathBuf::from("crates\\utils\\paths.rs"),
2547 row: Some(101),
2548 column: None,
2549 }
2550 );
2551 }
2552
2553 #[perf]
2554 fn test_path_compact() {
2555 let path: PathBuf = [
2556 home_dir().to_string_lossy().into_owned(),
2557 "some_file.txt".to_string(),
2558 ]
2559 .iter()
2560 .collect();
2561 if cfg!(any(target_os = "linux", target_os = "freebsd")) || cfg!(target_os = "macos") {
2562 assert_eq!(path.compact().to_str(), Some("~/some_file.txt"));
2563 } else {
2564 assert_eq!(path.compact().to_str(), path.to_str());
2565 }
2566 }
2567
2568 #[perf]
2569 fn test_extension_or_hidden_file_name() {
2570 let path = Path::new("/a/b/c/file_name.rs");
2572 assert_eq!(path.extension_or_hidden_file_name(), Some("rs"));
2573
2574 let path = Path::new("/a/b/c/file.name.rs");
2576 assert_eq!(path.extension_or_hidden_file_name(), Some("rs"));
2577
2578 let path = Path::new("/a/b/c/long.file.name.rs");
2580 assert_eq!(path.extension_or_hidden_file_name(), Some("rs"));
2581
2582 let path = Path::new("/a/b/c/.gitignore");
2584 assert_eq!(path.extension_or_hidden_file_name(), Some("gitignore"));
2585
2586 let path = Path::new("/a/b/c/.eslintrc.js");
2588 assert_eq!(path.extension_or_hidden_file_name(), Some("eslintrc.js"));
2589 }
2590
2591 #[perf]
2592 #[perf]
2628 #[cfg(target_os = "windows")]
2629 fn test_sanitized_path() {
2630 let path = Path::new("C:\\Users\\someone\\test_file.rs");
2631 let sanitized_path = SanitizedPath::new(path);
2632 assert_eq!(
2633 sanitized_path.to_string(),
2634 "C:\\Users\\someone\\test_file.rs"
2635 );
2636
2637 let path = Path::new("\\\\?\\C:\\Users\\someone\\test_file.rs");
2638 let sanitized_path = SanitizedPath::new(path);
2639 assert_eq!(
2640 sanitized_path.to_string(),
2641 "C:\\Users\\someone\\test_file.rs"
2642 );
2643 }
2644
2645 #[perf]
2646 fn test_compare_numeric_segments() {
2647 fn compare(a: &str, b: &str) -> Ordering {
2649 let mut a_iter = a.chars().peekable();
2650 let mut b_iter = b.chars().peekable();
2651
2652 let result = compare_numeric_segments(&mut a_iter, &mut b_iter);
2653
2654 assert!(
2656 !a_iter.next().is_some_and(|c| c.is_ascii_digit()),
2657 "Iterator a should have consumed all digits"
2658 );
2659 assert!(
2660 !b_iter.next().is_some_and(|c| c.is_ascii_digit()),
2661 "Iterator b should have consumed all digits"
2662 );
2663
2664 result
2665 }
2666
2667 assert_eq!(compare("0", "0"), Ordering::Equal);
2669 assert_eq!(compare("1", "2"), Ordering::Less);
2670 assert_eq!(compare("9", "10"), Ordering::Less);
2671 assert_eq!(compare("10", "9"), Ordering::Greater);
2672 assert_eq!(compare("99", "100"), Ordering::Less);
2673
2674 assert_eq!(compare("0", "00"), Ordering::Less);
2676 assert_eq!(compare("00", "0"), Ordering::Greater);
2677 assert_eq!(compare("01", "1"), Ordering::Greater);
2678 assert_eq!(compare("001", "1"), Ordering::Greater);
2679 assert_eq!(compare("001", "01"), Ordering::Greater);
2680
2681 assert_eq!(compare("000100", "100"), Ordering::Greater);
2683 assert_eq!(compare("100", "0100"), Ordering::Less);
2684 assert_eq!(compare("0100", "00100"), Ordering::Less);
2685
2686 assert_eq!(compare("9999999999", "10000000000"), Ordering::Less);
2688 assert_eq!(
2689 compare(
2690 "340282366920938463463374607431768211455", "340282366920938463463374607431768211456"
2692 ),
2693 Ordering::Less
2694 );
2695 assert_eq!(
2696 compare(
2697 "340282366920938463463374607431768211456", "340282366920938463463374607431768211455"
2699 ),
2700 Ordering::Greater
2701 );
2702
2703 let mut a_iter = "123abc".chars().peekable();
2705 let mut b_iter = "456def".chars().peekable();
2706
2707 compare_numeric_segments(&mut a_iter, &mut b_iter);
2708
2709 assert_eq!(a_iter.collect::<String>(), "abc");
2710 assert_eq!(b_iter.collect::<String>(), "def");
2711 }
2712
2713 #[perf]
2714 fn test_natural_sort() {
2715 assert_eq!(natural_sort("a", "b"), Ordering::Less);
2717 assert_eq!(natural_sort("b", "a"), Ordering::Greater);
2718 assert_eq!(natural_sort("a", "a"), Ordering::Equal);
2719
2720 assert_eq!(natural_sort("a", "A"), Ordering::Less);
2722 assert_eq!(natural_sort("A", "a"), Ordering::Greater);
2723 assert_eq!(natural_sort("aA", "aa"), Ordering::Greater);
2724 assert_eq!(natural_sort("aa", "aA"), Ordering::Less);
2725
2726 assert_eq!(natural_sort("1", "2"), Ordering::Less);
2728 assert_eq!(natural_sort("2", "10"), Ordering::Less);
2729 assert_eq!(natural_sort("02", "10"), Ordering::Less);
2730 assert_eq!(natural_sort("02", "2"), Ordering::Greater);
2731
2732 assert_eq!(natural_sort("a1", "a2"), Ordering::Less);
2734 assert_eq!(natural_sort("a2", "a10"), Ordering::Less);
2735 assert_eq!(natural_sort("a02", "a2"), Ordering::Greater);
2736 assert_eq!(natural_sort("a1b", "a1c"), Ordering::Less);
2737
2738 assert_eq!(natural_sort("1a2", "1a10"), Ordering::Less);
2740 assert_eq!(natural_sort("1a10", "1a2"), Ordering::Greater);
2741 assert_eq!(natural_sort("2a1", "10a1"), Ordering::Less);
2742
2743 assert_eq!(natural_sort("a-1", "a-2"), Ordering::Less);
2745 assert_eq!(natural_sort("a_1", "a_2"), Ordering::Less);
2746 assert_eq!(natural_sort("a.1", "a.2"), Ordering::Less);
2747
2748 assert_eq!(natural_sort("文1", "文2"), Ordering::Less);
2750 assert_eq!(natural_sort("文2", "文10"), Ordering::Less);
2751 assert_eq!(natural_sort("🔤1", "🔤2"), Ordering::Less);
2752
2753 assert_eq!(natural_sort("", ""), Ordering::Equal);
2755 assert_eq!(natural_sort("", "a"), Ordering::Less);
2756 assert_eq!(natural_sort("a", ""), Ordering::Greater);
2757 assert_eq!(natural_sort(" ", " "), Ordering::Less);
2758
2759 assert_eq!(natural_sort("File-1.txt", "File-2.txt"), Ordering::Less);
2761 assert_eq!(natural_sort("File-02.txt", "File-2.txt"), Ordering::Greater);
2762 assert_eq!(natural_sort("File-2.txt", "File-10.txt"), Ordering::Less);
2763 assert_eq!(natural_sort("File_A1", "File_A2"), Ordering::Less);
2764 assert_eq!(natural_sort("File_a1", "File_A1"), Ordering::Less);
2765 }
2766
2767 #[perf]
2768 fn test_compare_paths() {
2769 fn compare(a: &str, is_a_file: bool, b: &str, is_b_file: bool) -> Ordering {
2771 compare_paths((Path::new(a), is_a_file), (Path::new(b), is_b_file))
2772 }
2773
2774 assert_eq!(compare("a", true, "b", true), Ordering::Less);
2776 assert_eq!(compare("b", true, "a", true), Ordering::Greater);
2777 assert_eq!(compare("a", true, "a", true), Ordering::Equal);
2778
2779 assert_eq!(compare("a", true, "a", false), Ordering::Greater);
2781 assert_eq!(compare("a", false, "a", true), Ordering::Less);
2782 assert_eq!(compare("b", false, "a", true), Ordering::Less);
2783
2784 assert_eq!(compare("a.txt", true, "a.md", true), Ordering::Greater);
2786 assert_eq!(compare("a.md", true, "a.txt", true), Ordering::Less);
2787 assert_eq!(compare("a", true, "a.txt", true), Ordering::Less);
2788
2789 assert_eq!(compare("dir/a", true, "dir/b", true), Ordering::Less);
2791 assert_eq!(compare("dir1/a", true, "dir2/a", true), Ordering::Less);
2792 assert_eq!(compare("dir/sub/a", true, "dir/a", true), Ordering::Less);
2793
2794 assert_eq!(
2796 compare("Dir/file", true, "dir/file", true),
2797 Ordering::Greater
2798 );
2799 assert_eq!(
2800 compare("dir/File", true, "dir/file", true),
2801 Ordering::Greater
2802 );
2803 assert_eq!(compare("dir/file", true, "Dir/File", true), Ordering::Less);
2804
2805 assert_eq!(compare(".hidden", true, "visible", true), Ordering::Less);
2807 assert_eq!(compare("_special", true, "normal", true), Ordering::Less);
2808 assert_eq!(compare(".config", false, ".data", false), Ordering::Less);
2809
2810 assert_eq!(
2812 compare("dir1/file", true, "dir2/file", true),
2813 Ordering::Less
2814 );
2815 assert_eq!(
2816 compare("dir2/file", true, "dir10/file", true),
2817 Ordering::Less
2818 );
2819 assert_eq!(
2820 compare("dir02/file", true, "dir2/file", true),
2821 Ordering::Greater
2822 );
2823
2824 assert_eq!(compare("/a", true, "/b", true), Ordering::Less);
2826 assert_eq!(compare("/", false, "/a", true), Ordering::Less);
2827
2828 assert_eq!(
2830 compare("project/src/main.rs", true, "project/src/lib.rs", true),
2831 Ordering::Greater
2832 );
2833 assert_eq!(
2834 compare(
2835 "project/tests/test_1.rs",
2836 true,
2837 "project/tests/test_2.rs",
2838 true
2839 ),
2840 Ordering::Less
2841 );
2842 assert_eq!(
2843 compare(
2844 "project/v1.0.0/README.md",
2845 true,
2846 "project/v1.10.0/README.md",
2847 true
2848 ),
2849 Ordering::Less
2850 );
2851 }
2852
2853 #[perf]
2854 fn test_natural_sort_case_sensitivity() {
2855 std::thread::sleep(std::time::Duration::from_millis(100));
2856 assert_eq!(natural_sort("a", "A"), Ordering::Less);
2858 assert_eq!(natural_sort("A", "a"), Ordering::Greater);
2859 assert_eq!(natural_sort("a", "a"), Ordering::Equal);
2860 assert_eq!(natural_sort("A", "A"), Ordering::Equal);
2861
2862 assert_eq!(natural_sort("aaa", "AAA"), Ordering::Less);
2864 assert_eq!(natural_sort("AAA", "aaa"), Ordering::Greater);
2865 assert_eq!(natural_sort("aAa", "AaA"), Ordering::Less);
2866
2867 assert_eq!(natural_sort("a", "b"), Ordering::Less);
2869 assert_eq!(natural_sort("A", "b"), Ordering::Less);
2870 assert_eq!(natural_sort("a", "B"), Ordering::Less);
2871 }
2872
2873 #[perf]
2874 fn test_natural_sort_with_numbers() {
2875 assert_eq!(natural_sort("file1", "file2"), Ordering::Less);
2877 assert_eq!(natural_sort("file2", "file10"), Ordering::Less);
2878 assert_eq!(natural_sort("file10", "file2"), Ordering::Greater);
2879
2880 assert_eq!(natural_sort("1file", "2file"), Ordering::Less);
2882 assert_eq!(natural_sort("file1text", "file2text"), Ordering::Less);
2883 assert_eq!(natural_sort("text1file", "text2file"), Ordering::Less);
2884
2885 assert_eq!(natural_sort("file1-2", "file1-10"), Ordering::Less);
2887 assert_eq!(natural_sort("2-1file", "10-1file"), Ordering::Less);
2888
2889 assert_eq!(natural_sort("file002", "file2"), Ordering::Greater);
2891 assert_eq!(natural_sort("file002", "file10"), Ordering::Less);
2892
2893 assert_eq!(
2895 natural_sort("file999999999999999999999", "file999999999999999999998"),
2896 Ordering::Greater
2897 );
2898
2899 assert_eq!(
2903 natural_sort(
2904 "file340282366920938463463374607431768211454",
2905 "file340282366920938463463374607431768211455"
2906 ),
2907 Ordering::Less
2908 );
2909
2910 assert_eq!(
2912 natural_sort(
2913 "file340282366920938463463374607431768211456",
2914 "file340282366920938463463374607431768211455"
2915 ),
2916 Ordering::Greater
2917 );
2918
2919 assert_eq!(
2921 natural_sort(
2922 "file3402823669209384634633746074317682114560",
2923 "file340282366920938463463374607431768211455"
2924 ),
2925 Ordering::Greater
2926 );
2927
2928 assert_eq!(
2930 natural_sort(
2931 "file0340282366920938463463374607431768211455",
2932 "file340282366920938463463374607431768211455"
2933 ),
2934 Ordering::Greater
2935 );
2936
2937 assert_eq!(
2939 natural_sort(
2940 "file999999999999999999999999999999999999999999999999",
2941 "file9999999999999999999999999999999999999999999999999"
2942 ),
2943 Ordering::Less
2944 );
2945 }
2946
2947 #[perf]
2948 fn test_natural_sort_case_sensitive() {
2949 assert_eq!(natural_sort("File1", "file2"), Ordering::Less);
2951 assert_eq!(natural_sort("file1", "File2"), Ordering::Less);
2952
2953 assert_eq!(natural_sort("Dir1", "dir01"), Ordering::Less);
2956 assert_eq!(natural_sort("dir2", "Dir02"), Ordering::Less);
2957 assert_eq!(natural_sort("dir2", "dir02"), Ordering::Less);
2958
2959 assert_eq!(natural_sort("dir1", "Dir1"), Ordering::Less);
2962 assert_eq!(natural_sort("dir02", "Dir02"), Ordering::Less);
2963 assert_eq!(natural_sort("dir10", "Dir10"), Ordering::Less);
2964 }
2965
2966 #[perf]
2967 fn test_natural_sort_edge_cases() {
2968 assert_eq!(natural_sort("", ""), Ordering::Equal);
2970 assert_eq!(natural_sort("", "a"), Ordering::Less);
2971 assert_eq!(natural_sort("a", ""), Ordering::Greater);
2972
2973 assert_eq!(natural_sort("file-1", "file_1"), Ordering::Less);
2975 assert_eq!(natural_sort("file.1", "file_1"), Ordering::Less);
2976 assert_eq!(natural_sort("file 1", "file_1"), Ordering::Less);
2977
2978 assert_eq!(natural_sort("file①", "file②"), Ordering::Less);
2981 assert_eq!(natural_sort("file⑩", "file②"), Ordering::Greater);
2983 assert_eq!(natural_sort("file漢", "file字"), Ordering::Greater);
2985
2986 assert_eq!(natural_sort("file-1a", "file-1b"), Ordering::Less);
2988 assert_eq!(natural_sort("file-1.2", "file-1.10"), Ordering::Less);
2989 assert_eq!(natural_sort("file-1.10", "file-1.2"), Ordering::Greater);
2990 }
2991
2992 #[test]
2993 fn test_multiple_extensions() {
2994 let path = Path::new("/a/b/c/file_name");
2996 assert_eq!(path.multiple_extensions(), None);
2997
2998 let path = Path::new("/a/b/c/file_name.tsx");
3000 assert_eq!(path.multiple_extensions(), None);
3001
3002 let path = Path::new("/a/b/c/file_name.stories.tsx");
3004 assert_eq!(path.multiple_extensions(), Some("stories.tsx".to_string()));
3005
3006 let path = Path::new("/a/b/c/long.app.tar.gz");
3008 assert_eq!(path.multiple_extensions(), Some("app.tar.gz".to_string()));
3009 }
3010
3011 #[test]
3012 fn test_strip_path_suffix() {
3013 let base = Path::new("/a/b/c/file_name");
3014 let suffix = Path::new("file_name");
3015 assert_eq!(strip_path_suffix(base, suffix), Some(Path::new("/a/b/c")));
3016
3017 let base = Path::new("/a/b/c/file_name.tsx");
3018 let suffix = Path::new("file_name.tsx");
3019 assert_eq!(strip_path_suffix(base, suffix), Some(Path::new("/a/b/c")));
3020
3021 let base = Path::new("/a/b/c/file_name.stories.tsx");
3022 let suffix = Path::new("c/file_name.stories.tsx");
3023 assert_eq!(strip_path_suffix(base, suffix), Some(Path::new("/a/b")));
3024
3025 let base = Path::new("/a/b/c/long.app.tar.gz");
3026 let suffix = Path::new("b/c/long.app.tar.gz");
3027 assert_eq!(strip_path_suffix(base, suffix), Some(Path::new("/a")));
3028
3029 let base = Path::new("/a/b/c/long.app.tar.gz");
3030 let suffix = Path::new("/a/b/c/long.app.tar.gz");
3031 assert_eq!(strip_path_suffix(base, suffix), Some(Path::new("")));
3032
3033 let base = Path::new("/a/b/c/long.app.tar.gz");
3034 let suffix = Path::new("/a/b/c/no_match.app.tar.gz");
3035 assert_eq!(strip_path_suffix(base, suffix), None);
3036
3037 let base = Path::new("/a/b/c/long.app.tar.gz");
3038 let suffix = Path::new("app.tar.gz");
3039 assert_eq!(strip_path_suffix(base, suffix), None);
3040 }
3041
3042 #[cfg(target_os = "windows")]
3043 #[test]
3044 fn test_wsl_path() {
3045 use super::WslPath;
3046 let path = "/a/b/c";
3047 assert_eq!(WslPath::from_path(&path), None);
3048
3049 let path = r"\\wsl.localhost";
3050 assert_eq!(WslPath::from_path(&path), None);
3051
3052 let path = r"\\wsl.localhost\Distro";
3053 assert_eq!(
3054 WslPath::from_path(&path),
3055 Some(WslPath {
3056 distro: "Distro".to_owned(),
3057 path: "/".into(),
3058 })
3059 );
3060
3061 let path = r"\\wsl.localhost\Distro\blue";
3062 assert_eq!(
3063 WslPath::from_path(&path),
3064 Some(WslPath {
3065 distro: "Distro".to_owned(),
3066 path: "/blue".into()
3067 })
3068 );
3069
3070 let path = r"\\wsl$\archlinux\tomato\.\paprika\..\aubergine.txt";
3071 assert_eq!(
3072 WslPath::from_path(&path),
3073 Some(WslPath {
3074 distro: "archlinux".to_owned(),
3075 path: "/tomato/paprika/../aubergine.txt".into()
3076 })
3077 );
3078
3079 let path = r"\\windows.localhost\Distro\foo";
3080 assert_eq!(WslPath::from_path(&path), None);
3081 }
3082
3083 #[test]
3084 fn test_url_to_file_path_ext_posix_basic() {
3085 use super::UrlExt;
3086
3087 let url = url::Url::parse("file:///home/user/file.txt").unwrap();
3088 assert_eq!(
3089 url.to_file_path_ext(PathStyle::Unix),
3090 Ok(PathBuf::from("/home/user/file.txt"))
3091 );
3092
3093 let url = url::Url::parse("file:///").unwrap();
3094 assert_eq!(
3095 url.to_file_path_ext(PathStyle::Unix),
3096 Ok(PathBuf::from("/"))
3097 );
3098
3099 let url = url::Url::parse("file:///a/b/c/d/e").unwrap();
3100 assert_eq!(
3101 url.to_file_path_ext(PathStyle::Unix),
3102 Ok(PathBuf::from("/a/b/c/d/e"))
3103 );
3104 }
3105
3106 #[test]
3107 fn test_url_to_file_path_ext_posix_percent_encoding() {
3108 use super::UrlExt;
3109
3110 let url = url::Url::parse("file:///home/user/file%20with%20spaces.txt").unwrap();
3111 assert_eq!(
3112 url.to_file_path_ext(PathStyle::Unix),
3113 Ok(PathBuf::from("/home/user/file with spaces.txt"))
3114 );
3115
3116 let url = url::Url::parse("file:///path%2Fwith%2Fencoded%2Fslashes").unwrap();
3117 assert_eq!(
3118 url.to_file_path_ext(PathStyle::Unix),
3119 Ok(PathBuf::from("/path/with/encoded/slashes"))
3120 );
3121
3122 let url = url::Url::parse("file:///special%23chars%3F.txt").unwrap();
3123 assert_eq!(
3124 url.to_file_path_ext(PathStyle::Unix),
3125 Ok(PathBuf::from("/special#chars?.txt"))
3126 );
3127 }
3128
3129 #[test]
3130 fn test_url_to_file_path_ext_posix_localhost() {
3131 use super::UrlExt;
3132
3133 let url = url::Url::parse("file://localhost/home/user/file.txt").unwrap();
3134 assert_eq!(
3135 url.to_file_path_ext(PathStyle::Unix),
3136 Ok(PathBuf::from("/home/user/file.txt"))
3137 );
3138 }
3139
3140 #[test]
3141 fn test_url_to_file_path_ext_posix_rejects_host() {
3142 use super::UrlExt;
3143
3144 let url = url::Url::parse("file://somehost/home/user/file.txt").unwrap();
3145 assert_eq!(url.to_file_path_ext(PathStyle::Unix), Err(()));
3146 }
3147
3148 #[test]
3149 fn test_url_to_file_path_ext_posix_windows_drive_letter() {
3150 use super::UrlExt;
3151
3152 let url = url::Url::parse("file:///C:").unwrap();
3153 assert_eq!(
3154 url.to_file_path_ext(PathStyle::Unix),
3155 Ok(PathBuf::from("/C:/"))
3156 );
3157
3158 let url = url::Url::parse("file:///D|").unwrap();
3159 assert_eq!(
3160 url.to_file_path_ext(PathStyle::Unix),
3161 Ok(PathBuf::from("/D|/"))
3162 );
3163 }
3164
3165 #[test]
3166 fn test_url_to_file_path_ext_windows_basic() {
3167 use super::UrlExt;
3168
3169 let url = url::Url::parse("file:///C:/Users/user/file.txt").unwrap();
3170 assert_eq!(
3171 url.to_file_path_ext(PathStyle::Windows),
3172 Ok(PathBuf::from("C:\\Users\\user\\file.txt"))
3173 );
3174
3175 let url = url::Url::parse("file:///D:/folder/subfolder/file.rs").unwrap();
3176 assert_eq!(
3177 url.to_file_path_ext(PathStyle::Windows),
3178 Ok(PathBuf::from("D:\\folder\\subfolder\\file.rs"))
3179 );
3180
3181 let url = url::Url::parse("file:///C:/").unwrap();
3182 assert_eq!(
3183 url.to_file_path_ext(PathStyle::Windows),
3184 Ok(PathBuf::from("C:\\"))
3185 );
3186 }
3187
3188 #[test]
3189 fn test_url_to_file_path_ext_windows_encoded_drive_letter() {
3190 use super::UrlExt;
3191
3192 let url = url::Url::parse("file:///C%3A/Users/file.txt").unwrap();
3193 assert_eq!(
3194 url.to_file_path_ext(PathStyle::Windows),
3195 Ok(PathBuf::from("C:\\Users\\file.txt"))
3196 );
3197
3198 let url = url::Url::parse("file:///c%3a/Users/file.txt").unwrap();
3199 assert_eq!(
3200 url.to_file_path_ext(PathStyle::Windows),
3201 Ok(PathBuf::from("c:\\Users\\file.txt"))
3202 );
3203
3204 let url = url::Url::parse("file:///D%3A/folder/file.txt").unwrap();
3205 assert_eq!(
3206 url.to_file_path_ext(PathStyle::Windows),
3207 Ok(PathBuf::from("D:\\folder\\file.txt"))
3208 );
3209
3210 let url = url::Url::parse("file:///d%3A/folder/file.txt").unwrap();
3211 assert_eq!(
3212 url.to_file_path_ext(PathStyle::Windows),
3213 Ok(PathBuf::from("d:\\folder\\file.txt"))
3214 );
3215 }
3216
3217 #[test]
3218 fn test_url_to_file_path_ext_windows_unc_path() {
3219 use super::UrlExt;
3220
3221 let url = url::Url::parse("file://server/share/path/file.txt").unwrap();
3222 assert_eq!(
3223 url.to_file_path_ext(PathStyle::Windows),
3224 Ok(PathBuf::from("\\\\server\\share\\path\\file.txt"))
3225 );
3226
3227 let url = url::Url::parse("file://server/share").unwrap();
3228 assert_eq!(
3229 url.to_file_path_ext(PathStyle::Windows),
3230 Ok(PathBuf::from("\\\\server\\share"))
3231 );
3232 }
3233
3234 #[test]
3235 fn test_url_to_file_path_ext_windows_percent_encoding() {
3236 use super::UrlExt;
3237
3238 let url = url::Url::parse("file:///C:/Users/user/file%20with%20spaces.txt").unwrap();
3239 assert_eq!(
3240 url.to_file_path_ext(PathStyle::Windows),
3241 Ok(PathBuf::from("C:\\Users\\user\\file with spaces.txt"))
3242 );
3243
3244 let url = url::Url::parse("file:///C:/special%23chars%3F.txt").unwrap();
3245 assert_eq!(
3246 url.to_file_path_ext(PathStyle::Windows),
3247 Ok(PathBuf::from("C:\\special#chars?.txt"))
3248 );
3249 }
3250
3251 #[test]
3252 fn test_url_to_file_path_ext_windows_invalid_drive() {
3253 use super::UrlExt;
3254
3255 let url = url::Url::parse("file:///1:/path/file.txt").unwrap();
3256 assert_eq!(url.to_file_path_ext(PathStyle::Windows), Err(()));
3257
3258 let url = url::Url::parse("file:///CC:/path/file.txt").unwrap();
3259 assert_eq!(url.to_file_path_ext(PathStyle::Windows), Err(()));
3260
3261 let url = url::Url::parse("file:///C/path/file.txt").unwrap();
3262 assert_eq!(url.to_file_path_ext(PathStyle::Windows), Err(()));
3263
3264 let url = url::Url::parse("file:///invalid").unwrap();
3265 assert_eq!(url.to_file_path_ext(PathStyle::Windows), Err(()));
3266 }
3267
3268 #[test]
3269 fn test_url_to_file_path_ext_non_file_scheme() {
3270 use super::UrlExt;
3271
3272 let url = url::Url::parse("http://example.com/path").unwrap();
3273 assert_eq!(url.to_file_path_ext(PathStyle::Unix), Err(()));
3274 assert_eq!(url.to_file_path_ext(PathStyle::Windows), Err(()));
3275
3276 let url = url::Url::parse("https://example.com/path").unwrap();
3277 assert_eq!(url.to_file_path_ext(PathStyle::Unix), Err(()));
3278 assert_eq!(url.to_file_path_ext(PathStyle::Windows), Err(()));
3279 }
3280
3281 #[test]
3282 fn test_url_to_file_path_ext_windows_localhost() {
3283 use super::UrlExt;
3284
3285 let url = url::Url::parse("file://localhost/C:/Users/file.txt").unwrap();
3286 assert_eq!(
3287 url.to_file_path_ext(PathStyle::Windows),
3288 Ok(PathBuf::from("C:\\Users\\file.txt"))
3289 );
3290 }
3291}