1use globset::{Glob, GlobSet, GlobSetBuilder};
2use itertools::Itertools;
3use regex::Regex;
4use serde::{Deserialize, Serialize};
5use std::cmp::Ordering;
6use std::fmt::{Display, Formatter};
7use std::mem;
8use std::path::StripPrefixError;
9use std::sync::{Arc, OnceLock};
10use std::{
11 ffi::OsStr,
12 path::{Path, PathBuf},
13 sync::LazyLock,
14};
15
16use crate::rel_path::RelPath;
17
18static HOME_DIR: OnceLock<PathBuf> = OnceLock::new();
19
20pub fn home_dir() -> &'static PathBuf {
22 HOME_DIR.get_or_init(|| {
23 if cfg!(any(test, feature = "test-support")) {
24 if cfg!(target_os = "macos") {
25 PathBuf::from("/Users/zed")
26 } else if cfg!(target_os = "windows") {
27 PathBuf::from("C:\\Users\\zed")
28 } else {
29 PathBuf::from("/home/zed")
30 }
31 } else {
32 dirs::home_dir().expect("failed to determine home directory")
33 }
34 })
35}
36
37pub trait PathExt {
38 fn compact(&self) -> PathBuf;
39 fn extension_or_hidden_file_name(&self) -> Option<&str>;
40 fn try_from_bytes<'a>(bytes: &'a [u8]) -> anyhow::Result<Self>
41 where
42 Self: From<&'a Path>,
43 {
44 #[cfg(unix)]
45 {
46 use std::os::unix::prelude::OsStrExt;
47 Ok(Self::from(Path::new(OsStr::from_bytes(bytes))))
48 }
49 #[cfg(windows)]
50 {
51 use anyhow::Context as _;
52 use tendril::fmt::{Format, WTF8};
53 WTF8::validate(bytes)
54 .then(|| {
55 Self::from(Path::new(unsafe {
57 OsStr::from_encoded_bytes_unchecked(bytes)
58 }))
59 })
60 .with_context(|| format!("Invalid WTF-8 sequence: {bytes:?}"))
61 }
62 }
63 fn local_to_wsl(&self) -> Option<PathBuf>;
64 fn multiple_extensions(&self) -> Option<String>;
65}
66
67impl<T: AsRef<Path>> PathExt for T {
68 fn compact(&self) -> PathBuf {
77 if cfg!(any(target_os = "linux", target_os = "freebsd")) || cfg!(target_os = "macos") {
78 match self.as_ref().strip_prefix(home_dir().as_path()) {
79 Ok(relative_path) => {
80 let mut shortened_path = PathBuf::new();
81 shortened_path.push("~");
82 shortened_path.push(relative_path);
83 shortened_path
84 }
85 Err(_) => self.as_ref().to_path_buf(),
86 }
87 } else {
88 self.as_ref().to_path_buf()
89 }
90 }
91
92 fn extension_or_hidden_file_name(&self) -> Option<&str> {
94 let path = self.as_ref();
95 let file_name = path.file_name()?.to_str()?;
96 if file_name.starts_with('.') {
97 return file_name.strip_prefix('.');
98 }
99
100 path.extension()
101 .and_then(|e| e.to_str())
102 .or_else(|| path.file_stem()?.to_str())
103 }
104
105 fn local_to_wsl(&self) -> Option<PathBuf> {
108 let mut new_path = std::ffi::OsString::new();
111 for component in self.as_ref().components() {
112 match component {
113 std::path::Component::Prefix(prefix) => {
114 let drive_letter = prefix.as_os_str().to_string_lossy().to_lowercase();
115 let drive_letter = drive_letter.strip_suffix(':')?;
116
117 new_path.push(format!("/mnt/{}", drive_letter));
118 }
119 std::path::Component::RootDir => {}
120 std::path::Component::CurDir => {
121 new_path.push("/.");
122 }
123 std::path::Component::ParentDir => {
124 new_path.push("/..");
125 }
126 std::path::Component::Normal(os_str) => {
127 new_path.push("/");
128 new_path.push(os_str);
129 }
130 }
131 }
132
133 Some(new_path.into())
134 }
135
136 fn multiple_extensions(&self) -> Option<String> {
141 let path = self.as_ref();
142 let file_name = path.file_name()?.to_str()?;
143
144 let parts: Vec<&str> = file_name
145 .split('.')
146 .skip(1)
148 .collect();
149
150 if parts.len() < 2 {
151 return None;
152 }
153
154 Some(parts.into_iter().join("."))
155 }
156}
157
158#[derive(Eq, PartialEq, Hash, Ord, PartialOrd)]
161#[repr(transparent)]
162pub struct SanitizedPath(Path);
163
164impl SanitizedPath {
165 pub fn new<T: AsRef<Path> + ?Sized>(path: &T) -> &Self {
166 #[cfg(not(target_os = "windows"))]
167 return Self::unchecked_new(path.as_ref());
168
169 #[cfg(target_os = "windows")]
170 return Self::unchecked_new(dunce::simplified(path.as_ref()));
171 }
172
173 pub fn unchecked_new<T: AsRef<Path> + ?Sized>(path: &T) -> &Self {
174 unsafe { mem::transmute::<&Path, &Self>(path.as_ref()) }
176 }
177
178 pub fn from_arc(path: Arc<Path>) -> Arc<Self> {
179 #[cfg(not(target_os = "windows"))]
181 return unsafe { mem::transmute::<Arc<Path>, Arc<Self>>(path) };
182
183 #[cfg(target_os = "windows")]
185 return Self::new(&path).into();
186 }
187
188 pub fn new_arc<T: AsRef<Path> + ?Sized>(path: &T) -> Arc<Self> {
189 Self::new(path).into()
190 }
191
192 pub fn cast_arc(path: Arc<Self>) -> Arc<Path> {
193 unsafe { mem::transmute::<Arc<Self>, Arc<Path>>(path) }
195 }
196
197 pub fn cast_arc_ref(path: &Arc<Self>) -> &Arc<Path> {
198 unsafe { mem::transmute::<&Arc<Self>, &Arc<Path>>(path) }
200 }
201
202 pub fn starts_with(&self, prefix: &Self) -> bool {
203 self.0.starts_with(&prefix.0)
204 }
205
206 pub fn as_path(&self) -> &Path {
207 &self.0
208 }
209
210 pub fn file_name(&self) -> Option<&std::ffi::OsStr> {
211 self.0.file_name()
212 }
213
214 pub fn extension(&self) -> Option<&std::ffi::OsStr> {
215 self.0.extension()
216 }
217
218 pub fn join<P: AsRef<Path>>(&self, path: P) -> PathBuf {
219 self.0.join(path)
220 }
221
222 pub fn parent(&self) -> Option<&Self> {
223 self.0.parent().map(Self::unchecked_new)
224 }
225
226 pub fn strip_prefix(&self, base: &Self) -> Result<&Path, StripPrefixError> {
227 self.0.strip_prefix(base.as_path())
228 }
229
230 pub fn to_str(&self) -> Option<&str> {
231 self.0.to_str()
232 }
233
234 pub fn to_path_buf(&self) -> PathBuf {
235 self.0.to_path_buf()
236 }
237}
238
239impl std::fmt::Debug for SanitizedPath {
240 fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
241 std::fmt::Debug::fmt(&self.0, formatter)
242 }
243}
244
245impl Display for SanitizedPath {
246 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
247 write!(f, "{}", self.0.display())
248 }
249}
250
251impl From<&SanitizedPath> for Arc<SanitizedPath> {
252 fn from(sanitized_path: &SanitizedPath) -> Self {
253 let path: Arc<Path> = sanitized_path.0.into();
254 unsafe { mem::transmute(path) }
256 }
257}
258
259impl From<&SanitizedPath> for PathBuf {
260 fn from(sanitized_path: &SanitizedPath) -> Self {
261 sanitized_path.as_path().into()
262 }
263}
264
265impl AsRef<Path> for SanitizedPath {
266 fn as_ref(&self) -> &Path {
267 &self.0
268 }
269}
270
271#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
272pub enum PathStyle {
273 Posix,
274 Windows,
275}
276
277impl PathStyle {
278 #[cfg(target_os = "windows")]
279 pub const fn local() -> Self {
280 PathStyle::Windows
281 }
282
283 #[cfg(not(target_os = "windows"))]
284 pub const fn local() -> Self {
285 PathStyle::Posix
286 }
287
288 #[inline]
289 pub fn separator(&self) -> &'static str {
290 match self {
291 PathStyle::Posix => "/",
292 PathStyle::Windows => "\\",
293 }
294 }
295
296 pub fn is_windows(&self) -> bool {
297 *self == PathStyle::Windows
298 }
299
300 pub fn join(self, left: impl AsRef<Path>, right: impl AsRef<Path>) -> Option<String> {
301 let right = right.as_ref().to_str()?;
302 if is_absolute(right, self) {
303 return None;
304 }
305 let left = left.as_ref().to_str()?;
306 if left.is_empty() {
307 Some(right.into())
308 } else {
309 Some(format!(
310 "{left}{}{right}",
311 if left.ends_with(self.separator()) {
312 ""
313 } else {
314 self.separator()
315 }
316 ))
317 }
318 }
319
320 pub fn split(self, path_like: &str) -> (Option<&str>, &str) {
321 let Some(pos) = path_like.rfind(self.separator()) else {
322 return (None, path_like);
323 };
324 let filename_start = pos + self.separator().len();
325 (
326 Some(&path_like[..filename_start]),
327 &path_like[filename_start..],
328 )
329 }
330}
331
332#[derive(Debug, Clone)]
333pub struct RemotePathBuf {
334 style: PathStyle,
335 string: String,
336}
337
338impl RemotePathBuf {
339 pub fn new(string: String, style: PathStyle) -> Self {
340 Self { style, string }
341 }
342
343 pub fn from_str(path: &str, style: PathStyle) -> Self {
344 Self::new(path.to_string(), style)
345 }
346
347 pub fn path_style(&self) -> PathStyle {
348 self.style
349 }
350
351 pub fn to_proto(self) -> String {
352 self.string
353 }
354}
355
356impl Display for RemotePathBuf {
357 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
358 write!(f, "{}", self.string)
359 }
360}
361
362pub fn is_absolute(path_like: &str, path_style: PathStyle) -> bool {
363 path_like.starts_with('/')
364 || path_style == PathStyle::Windows
365 && (path_like.starts_with('\\')
366 || path_like
367 .chars()
368 .next()
369 .is_some_and(|c| c.is_ascii_alphabetic())
370 && path_like[1..]
371 .strip_prefix(':')
372 .is_some_and(|path| path.starts_with('/') || path.starts_with('\\')))
373}
374
375pub const FILE_ROW_COLUMN_DELIMITER: char = ':';
377
378const ROW_COL_CAPTURE_REGEX: &str = r"(?xs)
379 ([^\(]+)\:(?:
380 \((\d+)[,:](\d+)\) # filename:(row,column), filename:(row:column)
381 |
382 \((\d+)\)() # filename:(row)
383 )
384 |
385 ([^\(]+)(?:
386 \((\d+)[,:](\d+)\) # filename(row,column), filename(row:column)
387 |
388 \((\d+)\)() # filename(row)
389 )
390 |
391 (.+?)(?:
392 \:+(\d+)\:(\d+)\:*$ # filename:row:column
393 |
394 \:+(\d+)\:*()$ # filename:row
395 |
396 \:+()()$
397 )";
398
399#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)]
402pub struct PathWithPosition {
403 pub path: PathBuf,
404 pub row: Option<u32>,
405 pub column: Option<u32>,
407}
408
409impl PathWithPosition {
410 pub fn from_path(path: PathBuf) -> Self {
412 Self {
413 path,
414 row: None,
415 column: None,
416 }
417 }
418
419 pub fn parse_str(s: &str) -> Self {
505 let trimmed = s.trim();
506 let path = Path::new(trimmed);
507 let maybe_file_name_with_row_col = path.file_name().unwrap_or_default().to_string_lossy();
508 if maybe_file_name_with_row_col.is_empty() {
509 return Self {
510 path: Path::new(s).to_path_buf(),
511 row: None,
512 column: None,
513 };
514 }
515
516 static SUFFIX_RE: LazyLock<Regex> =
520 LazyLock::new(|| Regex::new(ROW_COL_CAPTURE_REGEX).unwrap());
521 match SUFFIX_RE
522 .captures(&maybe_file_name_with_row_col)
523 .map(|caps| caps.extract())
524 {
525 Some((_, [file_name, maybe_row, maybe_column])) => {
526 let row = maybe_row.parse::<u32>().ok();
527 let column = maybe_column.parse::<u32>().ok();
528
529 let suffix_length = maybe_file_name_with_row_col.len() - file_name.len();
530 let path_without_suffix = &trimmed[..trimmed.len() - suffix_length];
531
532 Self {
533 path: Path::new(path_without_suffix).to_path_buf(),
534 row,
535 column,
536 }
537 }
538 None => {
539 let delimiter = ':';
543 let mut path_parts = s
544 .rsplitn(3, delimiter)
545 .collect::<Vec<_>>()
546 .into_iter()
547 .rev()
548 .fuse();
549 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();
550 let mut row = None;
551 let mut column = None;
552 if let Some(maybe_row) = path_parts.next() {
553 if let Ok(parsed_row) = maybe_row.parse::<u32>() {
554 row = Some(parsed_row);
555 if let Some(parsed_column) = path_parts
556 .next()
557 .and_then(|maybe_col| maybe_col.parse::<u32>().ok())
558 {
559 column = Some(parsed_column);
560 }
561 } else {
562 path_string.push(delimiter);
563 path_string.push_str(maybe_row);
564 }
565 }
566 for split in path_parts {
567 path_string.push(delimiter);
568 path_string.push_str(split);
569 }
570
571 Self {
572 path: PathBuf::from(path_string),
573 row,
574 column,
575 }
576 }
577 }
578 }
579
580 pub fn map_path<E>(
581 self,
582 mapping: impl FnOnce(PathBuf) -> Result<PathBuf, E>,
583 ) -> Result<PathWithPosition, E> {
584 Ok(PathWithPosition {
585 path: mapping(self.path)?,
586 row: self.row,
587 column: self.column,
588 })
589 }
590
591 pub fn to_string(&self, path_to_string: impl Fn(&PathBuf) -> String) -> String {
592 let path_string = path_to_string(&self.path);
593 if let Some(row) = self.row {
594 if let Some(column) = self.column {
595 format!("{path_string}:{row}:{column}")
596 } else {
597 format!("{path_string}:{row}")
598 }
599 } else {
600 path_string
601 }
602 }
603}
604
605#[derive(Clone, Debug)]
606pub struct PathMatcher {
607 sources: Vec<String>,
608 glob: GlobSet,
609 path_style: PathStyle,
610}
611
612impl PartialEq for PathMatcher {
619 fn eq(&self, other: &Self) -> bool {
620 self.sources.eq(&other.sources)
621 }
622}
623
624impl Eq for PathMatcher {}
625
626impl PathMatcher {
627 pub fn new(
628 globs: impl IntoIterator<Item = impl AsRef<str>>,
629 path_style: PathStyle,
630 ) -> Result<Self, globset::Error> {
631 let globs = globs
632 .into_iter()
633 .map(|as_str| Glob::new(as_str.as_ref()))
634 .collect::<Result<Vec<_>, _>>()?;
635 let sources = globs.iter().map(|glob| glob.glob().to_owned()).collect();
636 let mut glob_builder = GlobSetBuilder::new();
637 for single_glob in globs {
638 glob_builder.add(single_glob);
639 }
640 let glob = glob_builder.build()?;
641 Ok(PathMatcher {
642 glob,
643 sources,
644 path_style,
645 })
646 }
647
648 pub fn sources(&self) -> &[String] {
649 &self.sources
650 }
651
652 pub fn is_match<P: AsRef<Path>>(&self, other: P) -> bool {
653 let other_path = other.as_ref();
654 self.sources.iter().any(|source| {
655 let as_bytes = other_path.as_os_str().as_encoded_bytes();
656 as_bytes.starts_with(source.as_bytes()) || as_bytes.ends_with(source.as_bytes())
657 }) || self.glob.is_match(other_path)
658 || self.check_with_end_separator(other_path)
659 }
660
661 fn check_with_end_separator(&self, path: &Path) -> bool {
662 let path_str = path.to_string_lossy();
663 let separator = self.path_style.separator();
664 if path_str.ends_with(separator) {
665 false
666 } else {
667 self.glob.is_match(path_str.to_string() + separator)
668 }
669 }
670}
671
672impl Default for PathMatcher {
673 fn default() -> Self {
674 Self {
675 path_style: PathStyle::local(),
676 glob: GlobSet::empty(),
677 sources: vec![],
678 }
679 }
680}
681
682fn compare_chars(a: char, b: char) -> Ordering {
684 match a.to_ascii_lowercase().cmp(&b.to_ascii_lowercase()) {
686 Ordering::Equal => {
687 match (a.is_ascii_lowercase(), b.is_ascii_lowercase()) {
689 (true, false) => Ordering::Less, (false, true) => Ordering::Greater, _ => Ordering::Equal, }
693 }
694 other => other,
695 }
696}
697
698fn compare_numeric_segments<I>(
733 a_iter: &mut std::iter::Peekable<I>,
734 b_iter: &mut std::iter::Peekable<I>,
735) -> Ordering
736where
737 I: Iterator<Item = char>,
738{
739 let mut a_num_str = String::new();
741 let mut b_num_str = String::new();
742
743 while let Some(&c) = a_iter.peek() {
744 if !c.is_ascii_digit() {
745 break;
746 }
747
748 a_num_str.push(c);
749 a_iter.next();
750 }
751
752 while let Some(&c) = b_iter.peek() {
753 if !c.is_ascii_digit() {
754 break;
755 }
756
757 b_num_str.push(c);
758 b_iter.next();
759 }
760
761 match a_num_str.len().cmp(&b_num_str.len()) {
763 Ordering::Equal => {
764 match a_num_str.cmp(&b_num_str) {
766 Ordering::Equal => Ordering::Equal,
767 ordering => ordering,
768 }
769 }
770
771 ordering => {
773 if let (Ok(a_val), Ok(b_val)) = (a_num_str.parse::<u128>(), b_num_str.parse::<u128>()) {
775 match a_val.cmp(&b_val) {
776 Ordering::Equal => ordering, ord => ord,
778 }
779 } else {
780 a_num_str.cmp(&b_num_str)
782 }
783 }
784 }
785}
786
787fn natural_sort(a: &str, b: &str) -> Ordering {
807 let mut a_iter = a.chars().peekable();
808 let mut b_iter = b.chars().peekable();
809
810 loop {
811 match (a_iter.peek(), b_iter.peek()) {
812 (None, None) => return Ordering::Equal,
813 (None, _) => return Ordering::Less,
814 (_, None) => return Ordering::Greater,
815 (Some(&a_char), Some(&b_char)) => {
816 if a_char.is_ascii_digit() && b_char.is_ascii_digit() {
817 match compare_numeric_segments(&mut a_iter, &mut b_iter) {
818 Ordering::Equal => continue,
819 ordering => return ordering,
820 }
821 } else {
822 match compare_chars(a_char, b_char) {
823 Ordering::Equal => {
824 a_iter.next();
825 b_iter.next();
826 }
827 ordering => return ordering,
828 }
829 }
830 }
831 }
832 }
833}
834pub fn compare_rel_paths(
835 (path_a, a_is_file): (&RelPath, bool),
836 (path_b, b_is_file): (&RelPath, bool),
837) -> Ordering {
838 let mut components_a = path_a.components();
839 let mut components_b = path_b.components();
840
841 fn stem_and_extension(filename: &str) -> (Option<&str>, Option<&str>) {
842 if filename.is_empty() {
843 return (None, None);
844 }
845
846 match filename.rsplit_once('.') {
847 None => (Some(filename), None),
849
850 Some((before, after)) => {
852 if before.is_empty() {
856 (Some(filename), None)
857 } else {
858 (Some(before), Some(after))
860 }
861 }
862 }
863 }
864 loop {
865 match (components_a.next(), components_b.next()) {
866 (Some(component_a), Some(component_b)) => {
867 let a_is_file = a_is_file && components_a.rest().is_empty();
868 let b_is_file = b_is_file && components_b.rest().is_empty();
869
870 let ordering = a_is_file.cmp(&b_is_file).then_with(|| {
871 let (a_stem, a_extension) = a_is_file
872 .then(|| stem_and_extension(component_a))
873 .unwrap_or_default();
874 let path_string_a = if a_is_file { a_stem } else { Some(component_a) };
875
876 let (b_stem, b_extension) = b_is_file
877 .then(|| stem_and_extension(component_b))
878 .unwrap_or_default();
879 let path_string_b = if b_is_file { b_stem } else { Some(component_b) };
880
881 let compare_components = match (path_string_a, path_string_b) {
882 (Some(a), Some(b)) => natural_sort(&a, &b),
883 (Some(_), None) => Ordering::Greater,
884 (None, Some(_)) => Ordering::Less,
885 (None, None) => Ordering::Equal,
886 };
887
888 compare_components.then_with(|| {
889 if a_is_file && b_is_file {
890 let ext_a = a_extension.unwrap_or_default();
891 let ext_b = b_extension.unwrap_or_default();
892 ext_a.cmp(ext_b)
893 } else {
894 Ordering::Equal
895 }
896 })
897 });
898
899 if !ordering.is_eq() {
900 return ordering;
901 }
902 }
903 (Some(_), None) => break Ordering::Greater,
904 (None, Some(_)) => break Ordering::Less,
905 (None, None) => break Ordering::Equal,
906 }
907 }
908}
909
910pub fn compare_paths(
911 (path_a, a_is_file): (&Path, bool),
912 (path_b, b_is_file): (&Path, bool),
913) -> Ordering {
914 let mut components_a = path_a.components().peekable();
915 let mut components_b = path_b.components().peekable();
916
917 loop {
918 match (components_a.next(), components_b.next()) {
919 (Some(component_a), Some(component_b)) => {
920 let a_is_file = components_a.peek().is_none() && a_is_file;
921 let b_is_file = components_b.peek().is_none() && b_is_file;
922
923 let ordering = a_is_file.cmp(&b_is_file).then_with(|| {
924 let path_a = Path::new(component_a.as_os_str());
925 let path_string_a = if a_is_file {
926 path_a.file_stem()
927 } else {
928 path_a.file_name()
929 }
930 .map(|s| s.to_string_lossy());
931
932 let path_b = Path::new(component_b.as_os_str());
933 let path_string_b = if b_is_file {
934 path_b.file_stem()
935 } else {
936 path_b.file_name()
937 }
938 .map(|s| s.to_string_lossy());
939
940 let compare_components = match (path_string_a, path_string_b) {
941 (Some(a), Some(b)) => natural_sort(&a, &b),
942 (Some(_), None) => Ordering::Greater,
943 (None, Some(_)) => Ordering::Less,
944 (None, None) => Ordering::Equal,
945 };
946
947 compare_components.then_with(|| {
948 if a_is_file && b_is_file {
949 let ext_a = path_a.extension().unwrap_or_default();
950 let ext_b = path_b.extension().unwrap_or_default();
951 ext_a.cmp(ext_b)
952 } else {
953 Ordering::Equal
954 }
955 })
956 });
957
958 if !ordering.is_eq() {
959 return ordering;
960 }
961 }
962 (Some(_), None) => break Ordering::Greater,
963 (None, Some(_)) => break Ordering::Less,
964 (None, None) => break Ordering::Equal,
965 }
966 }
967}
968
969#[cfg(test)]
970mod tests {
971 use super::*;
972 use util_macros::perf;
973
974 #[perf]
975 fn compare_paths_with_dots() {
976 let mut paths = vec![
977 (Path::new("test_dirs"), false),
978 (Path::new("test_dirs/1.46"), false),
979 (Path::new("test_dirs/1.46/bar_1"), true),
980 (Path::new("test_dirs/1.46/bar_2"), true),
981 (Path::new("test_dirs/1.45"), false),
982 (Path::new("test_dirs/1.45/foo_2"), true),
983 (Path::new("test_dirs/1.45/foo_1"), true),
984 ];
985 paths.sort_by(|&a, &b| compare_paths(a, b));
986 assert_eq!(
987 paths,
988 vec![
989 (Path::new("test_dirs"), false),
990 (Path::new("test_dirs/1.45"), false),
991 (Path::new("test_dirs/1.45/foo_1"), true),
992 (Path::new("test_dirs/1.45/foo_2"), true),
993 (Path::new("test_dirs/1.46"), false),
994 (Path::new("test_dirs/1.46/bar_1"), true),
995 (Path::new("test_dirs/1.46/bar_2"), true),
996 ]
997 );
998 let mut paths = vec![
999 (Path::new("root1/one.txt"), true),
1000 (Path::new("root1/one.two.txt"), true),
1001 ];
1002 paths.sort_by(|&a, &b| compare_paths(a, b));
1003 assert_eq!(
1004 paths,
1005 vec![
1006 (Path::new("root1/one.txt"), true),
1007 (Path::new("root1/one.two.txt"), true),
1008 ]
1009 );
1010 }
1011
1012 #[perf]
1013 fn compare_paths_with_same_name_different_extensions() {
1014 let mut paths = vec![
1015 (Path::new("test_dirs/file.rs"), true),
1016 (Path::new("test_dirs/file.txt"), true),
1017 (Path::new("test_dirs/file.md"), true),
1018 (Path::new("test_dirs/file"), true),
1019 (Path::new("test_dirs/file.a"), true),
1020 ];
1021 paths.sort_by(|&a, &b| compare_paths(a, b));
1022 assert_eq!(
1023 paths,
1024 vec![
1025 (Path::new("test_dirs/file"), true),
1026 (Path::new("test_dirs/file.a"), true),
1027 (Path::new("test_dirs/file.md"), true),
1028 (Path::new("test_dirs/file.rs"), true),
1029 (Path::new("test_dirs/file.txt"), true),
1030 ]
1031 );
1032 }
1033
1034 #[perf]
1035 fn compare_paths_case_semi_sensitive() {
1036 let mut paths = vec![
1037 (Path::new("test_DIRS"), false),
1038 (Path::new("test_DIRS/foo_1"), true),
1039 (Path::new("test_DIRS/foo_2"), true),
1040 (Path::new("test_DIRS/bar"), true),
1041 (Path::new("test_DIRS/BAR"), true),
1042 (Path::new("test_dirs"), false),
1043 (Path::new("test_dirs/foo_1"), true),
1044 (Path::new("test_dirs/foo_2"), true),
1045 (Path::new("test_dirs/bar"), true),
1046 (Path::new("test_dirs/BAR"), true),
1047 ];
1048 paths.sort_by(|&a, &b| compare_paths(a, b));
1049 assert_eq!(
1050 paths,
1051 vec![
1052 (Path::new("test_dirs"), false),
1053 (Path::new("test_dirs/bar"), true),
1054 (Path::new("test_dirs/BAR"), true),
1055 (Path::new("test_dirs/foo_1"), true),
1056 (Path::new("test_dirs/foo_2"), true),
1057 (Path::new("test_DIRS"), false),
1058 (Path::new("test_DIRS/bar"), true),
1059 (Path::new("test_DIRS/BAR"), true),
1060 (Path::new("test_DIRS/foo_1"), true),
1061 (Path::new("test_DIRS/foo_2"), true),
1062 ]
1063 );
1064 }
1065
1066 #[perf]
1067 fn path_with_position_parse_posix_path() {
1068 assert_eq!(
1071 PathWithPosition::parse_str("test_file"),
1072 PathWithPosition {
1073 path: PathBuf::from("test_file"),
1074 row: None,
1075 column: None
1076 }
1077 );
1078
1079 assert_eq!(
1080 PathWithPosition::parse_str("a:bc:.zip:1"),
1081 PathWithPosition {
1082 path: PathBuf::from("a:bc:.zip"),
1083 row: Some(1),
1084 column: None
1085 }
1086 );
1087
1088 assert_eq!(
1089 PathWithPosition::parse_str("one.second.zip:1"),
1090 PathWithPosition {
1091 path: PathBuf::from("one.second.zip"),
1092 row: Some(1),
1093 column: None
1094 }
1095 );
1096
1097 assert_eq!(
1099 PathWithPosition::parse_str("test_file:10:1:"),
1100 PathWithPosition {
1101 path: PathBuf::from("test_file"),
1102 row: Some(10),
1103 column: Some(1)
1104 }
1105 );
1106
1107 assert_eq!(
1108 PathWithPosition::parse_str("test_file.rs:"),
1109 PathWithPosition {
1110 path: PathBuf::from("test_file.rs"),
1111 row: None,
1112 column: None
1113 }
1114 );
1115
1116 assert_eq!(
1117 PathWithPosition::parse_str("test_file.rs:1:"),
1118 PathWithPosition {
1119 path: PathBuf::from("test_file.rs"),
1120 row: Some(1),
1121 column: None
1122 }
1123 );
1124
1125 assert_eq!(
1126 PathWithPosition::parse_str("ab\ncd"),
1127 PathWithPosition {
1128 path: PathBuf::from("ab\ncd"),
1129 row: None,
1130 column: None
1131 }
1132 );
1133
1134 assert_eq!(
1135 PathWithPosition::parse_str("👋\nab"),
1136 PathWithPosition {
1137 path: PathBuf::from("👋\nab"),
1138 row: None,
1139 column: None
1140 }
1141 );
1142
1143 assert_eq!(
1144 PathWithPosition::parse_str("Types.hs:(617,9)-(670,28):"),
1145 PathWithPosition {
1146 path: PathBuf::from("Types.hs"),
1147 row: Some(617),
1148 column: Some(9),
1149 }
1150 );
1151 }
1152
1153 #[perf]
1154 #[cfg(not(target_os = "windows"))]
1155 fn path_with_position_parse_posix_path_with_suffix() {
1156 assert_eq!(
1157 PathWithPosition::parse_str("foo/bar:34:in"),
1158 PathWithPosition {
1159 path: PathBuf::from("foo/bar"),
1160 row: Some(34),
1161 column: None,
1162 }
1163 );
1164 assert_eq!(
1165 PathWithPosition::parse_str("foo/bar.rs:1902:::15:"),
1166 PathWithPosition {
1167 path: PathBuf::from("foo/bar.rs:1902"),
1168 row: Some(15),
1169 column: None
1170 }
1171 );
1172
1173 assert_eq!(
1174 PathWithPosition::parse_str("app-editors:zed-0.143.6:20240710-201212.log:34:"),
1175 PathWithPosition {
1176 path: PathBuf::from("app-editors:zed-0.143.6:20240710-201212.log"),
1177 row: Some(34),
1178 column: None,
1179 }
1180 );
1181
1182 assert_eq!(
1183 PathWithPosition::parse_str("crates/file_finder/src/file_finder.rs:1902:13:"),
1184 PathWithPosition {
1185 path: PathBuf::from("crates/file_finder/src/file_finder.rs"),
1186 row: Some(1902),
1187 column: Some(13),
1188 }
1189 );
1190
1191 assert_eq!(
1192 PathWithPosition::parse_str("crate/utils/src/test:today.log:34"),
1193 PathWithPosition {
1194 path: PathBuf::from("crate/utils/src/test:today.log"),
1195 row: Some(34),
1196 column: None,
1197 }
1198 );
1199 assert_eq!(
1200 PathWithPosition::parse_str("/testing/out/src/file_finder.odin(7:15)"),
1201 PathWithPosition {
1202 path: PathBuf::from("/testing/out/src/file_finder.odin"),
1203 row: Some(7),
1204 column: Some(15),
1205 }
1206 );
1207 }
1208
1209 #[perf]
1210 #[cfg(target_os = "windows")]
1211 fn path_with_position_parse_windows_path() {
1212 assert_eq!(
1213 PathWithPosition::parse_str("crates\\utils\\paths.rs"),
1214 PathWithPosition {
1215 path: PathBuf::from("crates\\utils\\paths.rs"),
1216 row: None,
1217 column: None
1218 }
1219 );
1220
1221 assert_eq!(
1222 PathWithPosition::parse_str("C:\\Users\\someone\\test_file.rs"),
1223 PathWithPosition {
1224 path: PathBuf::from("C:\\Users\\someone\\test_file.rs"),
1225 row: None,
1226 column: None
1227 }
1228 );
1229 }
1230
1231 #[perf]
1232 #[cfg(target_os = "windows")]
1233 fn path_with_position_parse_windows_path_with_suffix() {
1234 assert_eq!(
1235 PathWithPosition::parse_str("crates\\utils\\paths.rs:101"),
1236 PathWithPosition {
1237 path: PathBuf::from("crates\\utils\\paths.rs"),
1238 row: Some(101),
1239 column: None
1240 }
1241 );
1242
1243 assert_eq!(
1244 PathWithPosition::parse_str("\\\\?\\C:\\Users\\someone\\test_file.rs:1:20"),
1245 PathWithPosition {
1246 path: PathBuf::from("\\\\?\\C:\\Users\\someone\\test_file.rs"),
1247 row: Some(1),
1248 column: Some(20)
1249 }
1250 );
1251
1252 assert_eq!(
1253 PathWithPosition::parse_str("C:\\Users\\someone\\test_file.rs(1902,13)"),
1254 PathWithPosition {
1255 path: PathBuf::from("C:\\Users\\someone\\test_file.rs"),
1256 row: Some(1902),
1257 column: Some(13)
1258 }
1259 );
1260
1261 assert_eq!(
1263 PathWithPosition::parse_str("\\\\?\\C:\\Users\\someone\\test_file.rs:1902:13:"),
1264 PathWithPosition {
1265 path: PathBuf::from("\\\\?\\C:\\Users\\someone\\test_file.rs"),
1266 row: Some(1902),
1267 column: Some(13)
1268 }
1269 );
1270
1271 assert_eq!(
1272 PathWithPosition::parse_str("\\\\?\\C:\\Users\\someone\\test_file.rs:1902:13:15:"),
1273 PathWithPosition {
1274 path: PathBuf::from("\\\\?\\C:\\Users\\someone\\test_file.rs:1902"),
1275 row: Some(13),
1276 column: Some(15)
1277 }
1278 );
1279
1280 assert_eq!(
1281 PathWithPosition::parse_str("\\\\?\\C:\\Users\\someone\\test_file.rs:1902:::15:"),
1282 PathWithPosition {
1283 path: PathBuf::from("\\\\?\\C:\\Users\\someone\\test_file.rs:1902"),
1284 row: Some(15),
1285 column: None
1286 }
1287 );
1288
1289 assert_eq!(
1290 PathWithPosition::parse_str("\\\\?\\C:\\Users\\someone\\test_file.rs(1902,13):"),
1291 PathWithPosition {
1292 path: PathBuf::from("\\\\?\\C:\\Users\\someone\\test_file.rs"),
1293 row: Some(1902),
1294 column: Some(13),
1295 }
1296 );
1297
1298 assert_eq!(
1299 PathWithPosition::parse_str("\\\\?\\C:\\Users\\someone\\test_file.rs(1902):"),
1300 PathWithPosition {
1301 path: PathBuf::from("\\\\?\\C:\\Users\\someone\\test_file.rs"),
1302 row: Some(1902),
1303 column: None,
1304 }
1305 );
1306
1307 assert_eq!(
1308 PathWithPosition::parse_str("C:\\Users\\someone\\test_file.rs:1902:13:"),
1309 PathWithPosition {
1310 path: PathBuf::from("C:\\Users\\someone\\test_file.rs"),
1311 row: Some(1902),
1312 column: Some(13),
1313 }
1314 );
1315
1316 assert_eq!(
1317 PathWithPosition::parse_str("C:\\Users\\someone\\test_file.rs(1902,13):"),
1318 PathWithPosition {
1319 path: PathBuf::from("C:\\Users\\someone\\test_file.rs"),
1320 row: Some(1902),
1321 column: Some(13),
1322 }
1323 );
1324
1325 assert_eq!(
1326 PathWithPosition::parse_str("C:\\Users\\someone\\test_file.rs(1902):"),
1327 PathWithPosition {
1328 path: PathBuf::from("C:\\Users\\someone\\test_file.rs"),
1329 row: Some(1902),
1330 column: None,
1331 }
1332 );
1333
1334 assert_eq!(
1335 PathWithPosition::parse_str("crates/utils/paths.rs:101"),
1336 PathWithPosition {
1337 path: PathBuf::from("crates\\utils\\paths.rs"),
1338 row: Some(101),
1339 column: None,
1340 }
1341 );
1342 }
1343
1344 #[perf]
1345 fn test_path_compact() {
1346 let path: PathBuf = [
1347 home_dir().to_string_lossy().into_owned(),
1348 "some_file.txt".to_string(),
1349 ]
1350 .iter()
1351 .collect();
1352 if cfg!(any(target_os = "linux", target_os = "freebsd")) || cfg!(target_os = "macos") {
1353 assert_eq!(path.compact().to_str(), Some("~/some_file.txt"));
1354 } else {
1355 assert_eq!(path.compact().to_str(), path.to_str());
1356 }
1357 }
1358
1359 #[perf]
1360 fn test_extension_or_hidden_file_name() {
1361 let path = Path::new("/a/b/c/file_name.rs");
1363 assert_eq!(path.extension_or_hidden_file_name(), Some("rs"));
1364
1365 let path = Path::new("/a/b/c/file.name.rs");
1367 assert_eq!(path.extension_or_hidden_file_name(), Some("rs"));
1368
1369 let path = Path::new("/a/b/c/long.file.name.rs");
1371 assert_eq!(path.extension_or_hidden_file_name(), Some("rs"));
1372
1373 let path = Path::new("/a/b/c/.gitignore");
1375 assert_eq!(path.extension_or_hidden_file_name(), Some("gitignore"));
1376
1377 let path = Path::new("/a/b/c/.eslintrc.js");
1379 assert_eq!(path.extension_or_hidden_file_name(), Some("eslintrc.js"));
1380 }
1381
1382 #[perf]
1383 fn edge_of_glob() {
1384 let path = Path::new("/work/node_modules");
1385 let path_matcher =
1386 PathMatcher::new(&["**/node_modules/**".to_owned()], PathStyle::Posix).unwrap();
1387 assert!(
1388 path_matcher.is_match(path),
1389 "Path matcher should match {path:?}"
1390 );
1391 }
1392
1393 #[perf]
1394 fn project_search() {
1395 let path = Path::new("/Users/someonetoignore/work/zed/zed.dev/node_modules");
1396 let path_matcher =
1397 PathMatcher::new(&["**/node_modules/**".to_owned()], PathStyle::Posix).unwrap();
1398 assert!(
1399 path_matcher.is_match(path),
1400 "Path matcher should match {path:?}"
1401 );
1402 }
1403
1404 #[perf]
1405 #[cfg(target_os = "windows")]
1406 fn test_sanitized_path() {
1407 let path = Path::new("C:\\Users\\someone\\test_file.rs");
1408 let sanitized_path = SanitizedPath::new(path);
1409 assert_eq!(
1410 sanitized_path.to_string(),
1411 "C:\\Users\\someone\\test_file.rs"
1412 );
1413
1414 let path = Path::new("\\\\?\\C:\\Users\\someone\\test_file.rs");
1415 let sanitized_path = SanitizedPath::new(path);
1416 assert_eq!(
1417 sanitized_path.to_string(),
1418 "C:\\Users\\someone\\test_file.rs"
1419 );
1420 }
1421
1422 #[perf]
1423 fn test_compare_numeric_segments() {
1424 fn compare(a: &str, b: &str) -> Ordering {
1426 let mut a_iter = a.chars().peekable();
1427 let mut b_iter = b.chars().peekable();
1428
1429 let result = compare_numeric_segments(&mut a_iter, &mut b_iter);
1430
1431 assert!(
1433 !a_iter.next().is_some_and(|c| c.is_ascii_digit()),
1434 "Iterator a should have consumed all digits"
1435 );
1436 assert!(
1437 !b_iter.next().is_some_and(|c| c.is_ascii_digit()),
1438 "Iterator b should have consumed all digits"
1439 );
1440
1441 result
1442 }
1443
1444 assert_eq!(compare("0", "0"), Ordering::Equal);
1446 assert_eq!(compare("1", "2"), Ordering::Less);
1447 assert_eq!(compare("9", "10"), Ordering::Less);
1448 assert_eq!(compare("10", "9"), Ordering::Greater);
1449 assert_eq!(compare("99", "100"), Ordering::Less);
1450
1451 assert_eq!(compare("0", "00"), Ordering::Less);
1453 assert_eq!(compare("00", "0"), Ordering::Greater);
1454 assert_eq!(compare("01", "1"), Ordering::Greater);
1455 assert_eq!(compare("001", "1"), Ordering::Greater);
1456 assert_eq!(compare("001", "01"), Ordering::Greater);
1457
1458 assert_eq!(compare("000100", "100"), Ordering::Greater);
1460 assert_eq!(compare("100", "0100"), Ordering::Less);
1461 assert_eq!(compare("0100", "00100"), Ordering::Less);
1462
1463 assert_eq!(compare("9999999999", "10000000000"), Ordering::Less);
1465 assert_eq!(
1466 compare(
1467 "340282366920938463463374607431768211455", "340282366920938463463374607431768211456"
1469 ),
1470 Ordering::Less
1471 );
1472 assert_eq!(
1473 compare(
1474 "340282366920938463463374607431768211456", "340282366920938463463374607431768211455"
1476 ),
1477 Ordering::Greater
1478 );
1479
1480 let mut a_iter = "123abc".chars().peekable();
1482 let mut b_iter = "456def".chars().peekable();
1483
1484 compare_numeric_segments(&mut a_iter, &mut b_iter);
1485
1486 assert_eq!(a_iter.collect::<String>(), "abc");
1487 assert_eq!(b_iter.collect::<String>(), "def");
1488 }
1489
1490 #[perf]
1491 fn test_natural_sort() {
1492 assert_eq!(natural_sort("a", "b"), Ordering::Less);
1494 assert_eq!(natural_sort("b", "a"), Ordering::Greater);
1495 assert_eq!(natural_sort("a", "a"), Ordering::Equal);
1496
1497 assert_eq!(natural_sort("a", "A"), Ordering::Less);
1499 assert_eq!(natural_sort("A", "a"), Ordering::Greater);
1500 assert_eq!(natural_sort("aA", "aa"), Ordering::Greater);
1501 assert_eq!(natural_sort("aa", "aA"), Ordering::Less);
1502
1503 assert_eq!(natural_sort("1", "2"), Ordering::Less);
1505 assert_eq!(natural_sort("2", "10"), Ordering::Less);
1506 assert_eq!(natural_sort("02", "10"), Ordering::Less);
1507 assert_eq!(natural_sort("02", "2"), Ordering::Greater);
1508
1509 assert_eq!(natural_sort("a1", "a2"), Ordering::Less);
1511 assert_eq!(natural_sort("a2", "a10"), Ordering::Less);
1512 assert_eq!(natural_sort("a02", "a2"), Ordering::Greater);
1513 assert_eq!(natural_sort("a1b", "a1c"), Ordering::Less);
1514
1515 assert_eq!(natural_sort("1a2", "1a10"), Ordering::Less);
1517 assert_eq!(natural_sort("1a10", "1a2"), Ordering::Greater);
1518 assert_eq!(natural_sort("2a1", "10a1"), Ordering::Less);
1519
1520 assert_eq!(natural_sort("a-1", "a-2"), Ordering::Less);
1522 assert_eq!(natural_sort("a_1", "a_2"), Ordering::Less);
1523 assert_eq!(natural_sort("a.1", "a.2"), Ordering::Less);
1524
1525 assert_eq!(natural_sort("文1", "文2"), Ordering::Less);
1527 assert_eq!(natural_sort("文2", "文10"), Ordering::Less);
1528 assert_eq!(natural_sort("🔤1", "🔤2"), Ordering::Less);
1529
1530 assert_eq!(natural_sort("", ""), Ordering::Equal);
1532 assert_eq!(natural_sort("", "a"), Ordering::Less);
1533 assert_eq!(natural_sort("a", ""), Ordering::Greater);
1534 assert_eq!(natural_sort(" ", " "), Ordering::Less);
1535
1536 assert_eq!(natural_sort("File-1.txt", "File-2.txt"), Ordering::Less);
1538 assert_eq!(natural_sort("File-02.txt", "File-2.txt"), Ordering::Greater);
1539 assert_eq!(natural_sort("File-2.txt", "File-10.txt"), Ordering::Less);
1540 assert_eq!(natural_sort("File_A1", "File_A2"), Ordering::Less);
1541 assert_eq!(natural_sort("File_a1", "File_A1"), Ordering::Less);
1542 }
1543
1544 #[perf]
1545 fn test_compare_paths() {
1546 fn compare(a: &str, is_a_file: bool, b: &str, is_b_file: bool) -> Ordering {
1548 compare_paths((Path::new(a), is_a_file), (Path::new(b), is_b_file))
1549 }
1550
1551 assert_eq!(compare("a", true, "b", true), Ordering::Less);
1553 assert_eq!(compare("b", true, "a", true), Ordering::Greater);
1554 assert_eq!(compare("a", true, "a", true), Ordering::Equal);
1555
1556 assert_eq!(compare("a", true, "a", false), Ordering::Greater);
1558 assert_eq!(compare("a", false, "a", true), Ordering::Less);
1559 assert_eq!(compare("b", false, "a", true), Ordering::Less);
1560
1561 assert_eq!(compare("a.txt", true, "a.md", true), Ordering::Greater);
1563 assert_eq!(compare("a.md", true, "a.txt", true), Ordering::Less);
1564 assert_eq!(compare("a", true, "a.txt", true), Ordering::Less);
1565
1566 assert_eq!(compare("dir/a", true, "dir/b", true), Ordering::Less);
1568 assert_eq!(compare("dir1/a", true, "dir2/a", true), Ordering::Less);
1569 assert_eq!(compare("dir/sub/a", true, "dir/a", true), Ordering::Less);
1570
1571 assert_eq!(
1573 compare("Dir/file", true, "dir/file", true),
1574 Ordering::Greater
1575 );
1576 assert_eq!(
1577 compare("dir/File", true, "dir/file", true),
1578 Ordering::Greater
1579 );
1580 assert_eq!(compare("dir/file", true, "Dir/File", true), Ordering::Less);
1581
1582 assert_eq!(compare(".hidden", true, "visible", true), Ordering::Less);
1584 assert_eq!(compare("_special", true, "normal", true), Ordering::Less);
1585 assert_eq!(compare(".config", false, ".data", false), Ordering::Less);
1586
1587 assert_eq!(
1589 compare("dir1/file", true, "dir2/file", true),
1590 Ordering::Less
1591 );
1592 assert_eq!(
1593 compare("dir2/file", true, "dir10/file", true),
1594 Ordering::Less
1595 );
1596 assert_eq!(
1597 compare("dir02/file", true, "dir2/file", true),
1598 Ordering::Greater
1599 );
1600
1601 assert_eq!(compare("/a", true, "/b", true), Ordering::Less);
1603 assert_eq!(compare("/", false, "/a", true), Ordering::Less);
1604
1605 assert_eq!(
1607 compare("project/src/main.rs", true, "project/src/lib.rs", true),
1608 Ordering::Greater
1609 );
1610 assert_eq!(
1611 compare(
1612 "project/tests/test_1.rs",
1613 true,
1614 "project/tests/test_2.rs",
1615 true
1616 ),
1617 Ordering::Less
1618 );
1619 assert_eq!(
1620 compare(
1621 "project/v1.0.0/README.md",
1622 true,
1623 "project/v1.10.0/README.md",
1624 true
1625 ),
1626 Ordering::Less
1627 );
1628 }
1629
1630 #[perf]
1631 fn test_natural_sort_case_sensitivity() {
1632 std::thread::sleep(std::time::Duration::from_millis(100));
1633 assert_eq!(natural_sort("a", "A"), Ordering::Less);
1635 assert_eq!(natural_sort("A", "a"), Ordering::Greater);
1636 assert_eq!(natural_sort("a", "a"), Ordering::Equal);
1637 assert_eq!(natural_sort("A", "A"), Ordering::Equal);
1638
1639 assert_eq!(natural_sort("aaa", "AAA"), Ordering::Less);
1641 assert_eq!(natural_sort("AAA", "aaa"), Ordering::Greater);
1642 assert_eq!(natural_sort("aAa", "AaA"), Ordering::Less);
1643
1644 assert_eq!(natural_sort("a", "b"), Ordering::Less);
1646 assert_eq!(natural_sort("A", "b"), Ordering::Less);
1647 assert_eq!(natural_sort("a", "B"), Ordering::Less);
1648 }
1649
1650 #[perf]
1651 fn test_natural_sort_with_numbers() {
1652 assert_eq!(natural_sort("file1", "file2"), Ordering::Less);
1654 assert_eq!(natural_sort("file2", "file10"), Ordering::Less);
1655 assert_eq!(natural_sort("file10", "file2"), Ordering::Greater);
1656
1657 assert_eq!(natural_sort("1file", "2file"), Ordering::Less);
1659 assert_eq!(natural_sort("file1text", "file2text"), Ordering::Less);
1660 assert_eq!(natural_sort("text1file", "text2file"), Ordering::Less);
1661
1662 assert_eq!(natural_sort("file1-2", "file1-10"), Ordering::Less);
1664 assert_eq!(natural_sort("2-1file", "10-1file"), Ordering::Less);
1665
1666 assert_eq!(natural_sort("file002", "file2"), Ordering::Greater);
1668 assert_eq!(natural_sort("file002", "file10"), Ordering::Less);
1669
1670 assert_eq!(
1672 natural_sort("file999999999999999999999", "file999999999999999999998"),
1673 Ordering::Greater
1674 );
1675
1676 assert_eq!(
1680 natural_sort(
1681 "file340282366920938463463374607431768211454",
1682 "file340282366920938463463374607431768211455"
1683 ),
1684 Ordering::Less
1685 );
1686
1687 assert_eq!(
1689 natural_sort(
1690 "file340282366920938463463374607431768211456",
1691 "file340282366920938463463374607431768211455"
1692 ),
1693 Ordering::Greater
1694 );
1695
1696 assert_eq!(
1698 natural_sort(
1699 "file3402823669209384634633746074317682114560",
1700 "file340282366920938463463374607431768211455"
1701 ),
1702 Ordering::Greater
1703 );
1704
1705 assert_eq!(
1707 natural_sort(
1708 "file0340282366920938463463374607431768211455",
1709 "file340282366920938463463374607431768211455"
1710 ),
1711 Ordering::Greater
1712 );
1713
1714 assert_eq!(
1716 natural_sort(
1717 "file999999999999999999999999999999999999999999999999",
1718 "file9999999999999999999999999999999999999999999999999"
1719 ),
1720 Ordering::Less
1721 );
1722
1723 assert_eq!(natural_sort("File1", "file2"), Ordering::Greater);
1725 assert_eq!(natural_sort("file1", "File2"), Ordering::Less);
1726 }
1727
1728 #[perf]
1729 fn test_natural_sort_edge_cases() {
1730 assert_eq!(natural_sort("", ""), Ordering::Equal);
1732 assert_eq!(natural_sort("", "a"), Ordering::Less);
1733 assert_eq!(natural_sort("a", ""), Ordering::Greater);
1734
1735 assert_eq!(natural_sort("file-1", "file_1"), Ordering::Less);
1737 assert_eq!(natural_sort("file.1", "file_1"), Ordering::Less);
1738 assert_eq!(natural_sort("file 1", "file_1"), Ordering::Less);
1739
1740 assert_eq!(natural_sort("file①", "file②"), Ordering::Less);
1743 assert_eq!(natural_sort("file⑩", "file②"), Ordering::Greater);
1745 assert_eq!(natural_sort("file漢", "file字"), Ordering::Greater);
1747
1748 assert_eq!(natural_sort("file-1a", "file-1b"), Ordering::Less);
1750 assert_eq!(natural_sort("file-1.2", "file-1.10"), Ordering::Less);
1751 assert_eq!(natural_sort("file-1.10", "file-1.2"), Ordering::Greater);
1752 }
1753
1754 #[test]
1755 fn test_multiple_extensions() {
1756 let path = Path::new("/a/b/c/file_name");
1758 assert_eq!(path.multiple_extensions(), None);
1759
1760 let path = Path::new("/a/b/c/file_name.tsx");
1762 assert_eq!(path.multiple_extensions(), None);
1763
1764 let path = Path::new("/a/b/c/file_name.stories.tsx");
1766 assert_eq!(path.multiple_extensions(), Some("stories.tsx".to_string()));
1767
1768 let path = Path::new("/a/b/c/long.app.tar.gz");
1770 assert_eq!(path.multiple_extensions(), Some("app.tar.gz".to_string()));
1771 }
1772}