1use std::cmp;
2use std::sync::{Arc, OnceLock};
3use std::{
4 ffi::OsStr,
5 path::{Path, PathBuf},
6 sync::LazyLock,
7};
8
9use globset::{Glob, GlobSet, GlobSetBuilder};
10use regex::Regex;
11use serde::{Deserialize, Serialize};
12
13use crate::NumericPrefixWithSuffix;
14
15/// Returns the path to the user's home directory.
16pub fn home_dir() -> &'static PathBuf {
17 static HOME_DIR: OnceLock<PathBuf> = OnceLock::new();
18 HOME_DIR.get_or_init(|| dirs::home_dir().expect("failed to determine home directory"))
19}
20
21pub trait PathExt {
22 fn compact(&self) -> PathBuf;
23 fn icon_stem_or_suffix(&self) -> Option<&str>;
24 fn extension_or_hidden_file_name(&self) -> Option<&str>;
25 fn try_from_bytes<'a>(bytes: &'a [u8]) -> anyhow::Result<Self>
26 where
27 Self: From<&'a Path>,
28 {
29 #[cfg(unix)]
30 {
31 use std::os::unix::prelude::OsStrExt;
32 Ok(Self::from(Path::new(OsStr::from_bytes(bytes))))
33 }
34 #[cfg(windows)]
35 {
36 use anyhow::anyhow;
37 use tendril::fmt::{Format, WTF8};
38 WTF8::validate(bytes)
39 .then(|| {
40 // Safety: bytes are valid WTF-8 sequence.
41 Self::from(Path::new(unsafe {
42 OsStr::from_encoded_bytes_unchecked(bytes)
43 }))
44 })
45 .ok_or_else(|| anyhow!("Invalid WTF-8 sequence: {bytes:?}"))
46 }
47 }
48}
49
50impl<T: AsRef<Path>> PathExt for T {
51 /// Compacts a given file path by replacing the user's home directory
52 /// prefix with a tilde (`~`).
53 ///
54 /// # Returns
55 ///
56 /// * A `PathBuf` containing the compacted file path. If the input path
57 /// does not have the user's home directory prefix, or if we are not on
58 /// Linux or macOS, the original path is returned unchanged.
59 fn compact(&self) -> PathBuf {
60 if cfg!(any(target_os = "linux", target_os = "freebsd")) || cfg!(target_os = "macos") {
61 match self.as_ref().strip_prefix(home_dir().as_path()) {
62 Ok(relative_path) => {
63 let mut shortened_path = PathBuf::new();
64 shortened_path.push("~");
65 shortened_path.push(relative_path);
66 shortened_path
67 }
68 Err(_) => self.as_ref().to_path_buf(),
69 }
70 } else {
71 self.as_ref().to_path_buf()
72 }
73 }
74
75 /// Returns either the suffix if available, or the file stem otherwise to determine which file icon to use
76 fn icon_stem_or_suffix(&self) -> Option<&str> {
77 let path = self.as_ref();
78 let file_name = path.file_name()?.to_str()?;
79 if file_name.starts_with('.') {
80 return file_name.strip_prefix('.');
81 }
82
83 path.extension()
84 .and_then(|e| e.to_str())
85 .or_else(|| path.file_stem()?.to_str())
86 }
87
88 /// Returns a file's extension or, if the file is hidden, its name without the leading dot
89 fn extension_or_hidden_file_name(&self) -> Option<&str> {
90 if let Some(extension) = self.as_ref().extension() {
91 return extension.to_str();
92 }
93
94 self.as_ref().file_name()?.to_str()?.split('.').last()
95 }
96}
97
98/// Due to the issue of UNC paths on Windows, which can cause bugs in various parts of Zed, introducing this `SanitizedPath`
99/// leverages Rust's type system to ensure that all paths entering Zed are always "sanitized" by removing the `\\\\?\\` prefix.
100/// On non-Windows operating systems, this struct is effectively a no-op.
101#[derive(Debug, Clone, PartialEq, Eq, Hash)]
102pub struct SanitizedPath(Arc<Path>);
103
104impl SanitizedPath {
105 pub fn starts_with(&self, prefix: &SanitizedPath) -> bool {
106 self.0.starts_with(&prefix.0)
107 }
108
109 pub fn as_path(&self) -> &Arc<Path> {
110 &self.0
111 }
112
113 pub fn to_string(&self) -> String {
114 self.0.to_string_lossy().to_string()
115 }
116}
117
118impl From<SanitizedPath> for Arc<Path> {
119 fn from(sanitized_path: SanitizedPath) -> Self {
120 sanitized_path.0
121 }
122}
123
124impl<T: AsRef<Path>> From<T> for SanitizedPath {
125 #[cfg(not(target_os = "windows"))]
126 fn from(path: T) -> Self {
127 let path = path.as_ref();
128 SanitizedPath(path.into())
129 }
130
131 #[cfg(target_os = "windows")]
132 fn from(path: T) -> Self {
133 let path = path.as_ref();
134 SanitizedPath(dunce::simplified(path).into())
135 }
136}
137
138/// A delimiter to use in `path_query:row_number:column_number` strings parsing.
139pub const FILE_ROW_COLUMN_DELIMITER: char = ':';
140
141const ROW_COL_CAPTURE_REGEX: &str = r"(?x)
142 ([^\(]+)(?:
143 \((\d+),(\d+)\) # filename(row,column)
144 |
145 \((\d+)\)() # filename(row)
146 )
147 |
148 (.+?)(?:
149 \:+(\d+)\:(\d+)\:*$ # filename:row:column
150 |
151 \:+(\d+)\:*()$ # filename:row
152 |
153 \:*()()$ # filename:
154 )";
155
156/// A representation of a path-like string with optional row and column numbers.
157/// Matching values example: `te`, `test.rs:22`, `te:22:5`, `test.c(22)`, `test.c(22,5)`etc.
158#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)]
159pub struct PathWithPosition {
160 pub path: PathBuf,
161 pub row: Option<u32>,
162 // Absent if row is absent.
163 pub column: Option<u32>,
164}
165
166impl PathWithPosition {
167 /// Returns a PathWithPosition from a path.
168 pub fn from_path(path: PathBuf) -> Self {
169 Self {
170 path,
171 row: None,
172 column: None,
173 }
174 }
175
176 /// Parses a string that possibly has `:row:column` or `(row, column)` suffix.
177 /// Parenthesis format is used by [MSBuild](https://learn.microsoft.com/en-us/visualstudio/msbuild/msbuild-diagnostic-format-for-tasks) compatible tools
178 /// Ignores trailing `:`s, so `test.rs:22:` is parsed as `test.rs:22`.
179 /// If the suffix parsing fails, the whole string is parsed as a path.
180 ///
181 /// Be mindful that `test_file:10:1:` is a valid posix filename.
182 /// `PathWithPosition` class assumes that the ending position-like suffix is **not** part of the filename.
183 ///
184 /// # Examples
185 ///
186 /// ```
187 /// # use util::paths::PathWithPosition;
188 /// # use std::path::PathBuf;
189 /// assert_eq!(PathWithPosition::parse_str("test_file"), PathWithPosition {
190 /// path: PathBuf::from("test_file"),
191 /// row: None,
192 /// column: None,
193 /// });
194 /// assert_eq!(PathWithPosition::parse_str("test_file:10"), PathWithPosition {
195 /// path: PathBuf::from("test_file"),
196 /// row: Some(10),
197 /// column: None,
198 /// });
199 /// assert_eq!(PathWithPosition::parse_str("test_file.rs"), PathWithPosition {
200 /// path: PathBuf::from("test_file.rs"),
201 /// row: None,
202 /// column: None,
203 /// });
204 /// assert_eq!(PathWithPosition::parse_str("test_file.rs:1"), PathWithPosition {
205 /// path: PathBuf::from("test_file.rs"),
206 /// row: Some(1),
207 /// column: None,
208 /// });
209 /// assert_eq!(PathWithPosition::parse_str("test_file.rs:1:2"), PathWithPosition {
210 /// path: PathBuf::from("test_file.rs"),
211 /// row: Some(1),
212 /// column: Some(2),
213 /// });
214 /// ```
215 ///
216 /// # Expected parsing results when encounter ill-formatted inputs.
217 /// ```
218 /// # use util::paths::PathWithPosition;
219 /// # use std::path::PathBuf;
220 /// assert_eq!(PathWithPosition::parse_str("test_file.rs:a"), PathWithPosition {
221 /// path: PathBuf::from("test_file.rs:a"),
222 /// row: None,
223 /// column: None,
224 /// });
225 /// assert_eq!(PathWithPosition::parse_str("test_file.rs:a:b"), PathWithPosition {
226 /// path: PathBuf::from("test_file.rs:a:b"),
227 /// row: None,
228 /// column: None,
229 /// });
230 /// assert_eq!(PathWithPosition::parse_str("test_file.rs::"), PathWithPosition {
231 /// path: PathBuf::from("test_file.rs"),
232 /// row: None,
233 /// column: None,
234 /// });
235 /// assert_eq!(PathWithPosition::parse_str("test_file.rs::1"), PathWithPosition {
236 /// path: PathBuf::from("test_file.rs"),
237 /// row: Some(1),
238 /// column: None,
239 /// });
240 /// assert_eq!(PathWithPosition::parse_str("test_file.rs:1::"), PathWithPosition {
241 /// path: PathBuf::from("test_file.rs"),
242 /// row: Some(1),
243 /// column: None,
244 /// });
245 /// assert_eq!(PathWithPosition::parse_str("test_file.rs::1:2"), PathWithPosition {
246 /// path: PathBuf::from("test_file.rs"),
247 /// row: Some(1),
248 /// column: Some(2),
249 /// });
250 /// assert_eq!(PathWithPosition::parse_str("test_file.rs:1::2"), PathWithPosition {
251 /// path: PathBuf::from("test_file.rs:1"),
252 /// row: Some(2),
253 /// column: None,
254 /// });
255 /// assert_eq!(PathWithPosition::parse_str("test_file.rs:1:2:3"), PathWithPosition {
256 /// path: PathBuf::from("test_file.rs:1"),
257 /// row: Some(2),
258 /// column: Some(3),
259 /// });
260 /// ```
261 pub fn parse_str(s: &str) -> Self {
262 let trimmed = s.trim();
263 let path = Path::new(trimmed);
264 let maybe_file_name_with_row_col = path.file_name().unwrap_or_default().to_string_lossy();
265 if maybe_file_name_with_row_col.is_empty() {
266 return Self {
267 path: Path::new(s).to_path_buf(),
268 row: None,
269 column: None,
270 };
271 }
272
273 // Let's avoid repeated init cost on this. It is subject to thread contention, but
274 // so far this code isn't called from multiple hot paths. Getting contention here
275 // in the future seems unlikely.
276 static SUFFIX_RE: LazyLock<Regex> =
277 LazyLock::new(|| Regex::new(ROW_COL_CAPTURE_REGEX).unwrap());
278 match SUFFIX_RE
279 .captures(&maybe_file_name_with_row_col)
280 .map(|caps| caps.extract())
281 {
282 Some((_, [file_name, maybe_row, maybe_column])) => {
283 let row = maybe_row.parse::<u32>().ok();
284 let column = maybe_column.parse::<u32>().ok();
285
286 let suffix_length = maybe_file_name_with_row_col.len() - file_name.len();
287 let path_without_suffix = &trimmed[..trimmed.len() - suffix_length];
288
289 Self {
290 path: Path::new(path_without_suffix).to_path_buf(),
291 row,
292 column,
293 }
294 }
295 None => Self {
296 path: Path::new(s).to_path_buf(),
297 row: None,
298 column: None,
299 },
300 }
301 }
302
303 pub fn map_path<E>(
304 self,
305 mapping: impl FnOnce(PathBuf) -> Result<PathBuf, E>,
306 ) -> Result<PathWithPosition, E> {
307 Ok(PathWithPosition {
308 path: mapping(self.path)?,
309 row: self.row,
310 column: self.column,
311 })
312 }
313
314 pub fn to_string(&self, path_to_string: impl Fn(&PathBuf) -> String) -> String {
315 let path_string = path_to_string(&self.path);
316 if let Some(row) = self.row {
317 if let Some(column) = self.column {
318 format!("{path_string}:{row}:{column}")
319 } else {
320 format!("{path_string}:{row}")
321 }
322 } else {
323 path_string
324 }
325 }
326}
327
328#[derive(Clone, Debug, Default)]
329pub struct PathMatcher {
330 sources: Vec<String>,
331 glob: GlobSet,
332}
333
334// impl std::fmt::Display for PathMatcher {
335// fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
336// self.sources.fmt(f)
337// }
338// }
339
340impl PartialEq for PathMatcher {
341 fn eq(&self, other: &Self) -> bool {
342 self.sources.eq(&other.sources)
343 }
344}
345
346impl Eq for PathMatcher {}
347
348impl PathMatcher {
349 pub fn new(globs: &[String]) -> Result<Self, globset::Error> {
350 let globs = globs
351 .iter()
352 .map(|glob| Glob::new(glob))
353 .collect::<Result<Vec<_>, _>>()?;
354 let sources = globs.iter().map(|glob| glob.glob().to_owned()).collect();
355 let mut glob_builder = GlobSetBuilder::new();
356 for single_glob in globs {
357 glob_builder.add(single_glob);
358 }
359 let glob = glob_builder.build()?;
360 Ok(PathMatcher { glob, sources })
361 }
362
363 pub fn sources(&self) -> &[String] {
364 &self.sources
365 }
366
367 pub fn is_match<P: AsRef<Path>>(&self, other: P) -> bool {
368 let other_path = other.as_ref();
369 self.sources.iter().any(|source| {
370 let as_bytes = other_path.as_os_str().as_encoded_bytes();
371 as_bytes.starts_with(source.as_bytes()) || as_bytes.ends_with(source.as_bytes())
372 }) || self.glob.is_match(other_path)
373 || self.check_with_end_separator(other_path)
374 }
375
376 fn check_with_end_separator(&self, path: &Path) -> bool {
377 let path_str = path.to_string_lossy();
378 let separator = std::path::MAIN_SEPARATOR_STR;
379 if path_str.ends_with(separator) {
380 false
381 } else {
382 self.glob.is_match(path_str.to_string() + separator)
383 }
384 }
385}
386
387pub fn compare_paths(
388 (path_a, a_is_file): (&Path, bool),
389 (path_b, b_is_file): (&Path, bool),
390) -> cmp::Ordering {
391 let mut components_a = path_a.components().peekable();
392 let mut components_b = path_b.components().peekable();
393 loop {
394 match (components_a.next(), components_b.next()) {
395 (Some(component_a), Some(component_b)) => {
396 let a_is_file = components_a.peek().is_none() && a_is_file;
397 let b_is_file = components_b.peek().is_none() && b_is_file;
398 let ordering = a_is_file.cmp(&b_is_file).then_with(|| {
399 let path_a = Path::new(component_a.as_os_str());
400 let path_string_a = if a_is_file {
401 path_a.file_stem()
402 } else {
403 path_a.file_name()
404 }
405 .map(|s| s.to_string_lossy());
406 let num_and_remainder_a = path_string_a
407 .as_deref()
408 .map(NumericPrefixWithSuffix::from_numeric_prefixed_str);
409
410 let path_b = Path::new(component_b.as_os_str());
411 let path_string_b = if b_is_file {
412 path_b.file_stem()
413 } else {
414 path_b.file_name()
415 }
416 .map(|s| s.to_string_lossy());
417 let num_and_remainder_b = path_string_b
418 .as_deref()
419 .map(NumericPrefixWithSuffix::from_numeric_prefixed_str);
420
421 num_and_remainder_a.cmp(&num_and_remainder_b).then_with(|| {
422 if a_is_file && b_is_file {
423 let ext_a = path_a.extension().unwrap_or_default();
424 let ext_b = path_b.extension().unwrap_or_default();
425 ext_a.cmp(ext_b)
426 } else {
427 cmp::Ordering::Equal
428 }
429 })
430 });
431 if !ordering.is_eq() {
432 return ordering;
433 }
434 }
435 (Some(_), None) => break cmp::Ordering::Greater,
436 (None, Some(_)) => break cmp::Ordering::Less,
437 (None, None) => break cmp::Ordering::Equal,
438 }
439 }
440}
441
442#[cfg(test)]
443mod tests {
444 use super::*;
445
446 #[test]
447 fn compare_paths_with_dots() {
448 let mut paths = vec![
449 (Path::new("test_dirs"), false),
450 (Path::new("test_dirs/1.46"), false),
451 (Path::new("test_dirs/1.46/bar_1"), true),
452 (Path::new("test_dirs/1.46/bar_2"), true),
453 (Path::new("test_dirs/1.45"), false),
454 (Path::new("test_dirs/1.45/foo_2"), true),
455 (Path::new("test_dirs/1.45/foo_1"), true),
456 ];
457 paths.sort_by(|&a, &b| compare_paths(a, b));
458 assert_eq!(
459 paths,
460 vec![
461 (Path::new("test_dirs"), false),
462 (Path::new("test_dirs/1.45"), false),
463 (Path::new("test_dirs/1.45/foo_1"), true),
464 (Path::new("test_dirs/1.45/foo_2"), true),
465 (Path::new("test_dirs/1.46"), false),
466 (Path::new("test_dirs/1.46/bar_1"), true),
467 (Path::new("test_dirs/1.46/bar_2"), true),
468 ]
469 );
470 let mut paths = vec![
471 (Path::new("root1/one.txt"), true),
472 (Path::new("root1/one.two.txt"), true),
473 ];
474 paths.sort_by(|&a, &b| compare_paths(a, b));
475 assert_eq!(
476 paths,
477 vec![
478 (Path::new("root1/one.txt"), true),
479 (Path::new("root1/one.two.txt"), true),
480 ]
481 );
482 }
483
484 #[test]
485 fn compare_paths_with_same_name_different_extensions() {
486 let mut paths = vec![
487 (Path::new("test_dirs/file.rs"), true),
488 (Path::new("test_dirs/file.txt"), true),
489 (Path::new("test_dirs/file.md"), true),
490 (Path::new("test_dirs/file"), true),
491 (Path::new("test_dirs/file.a"), true),
492 ];
493 paths.sort_by(|&a, &b| compare_paths(a, b));
494 assert_eq!(
495 paths,
496 vec![
497 (Path::new("test_dirs/file"), true),
498 (Path::new("test_dirs/file.a"), true),
499 (Path::new("test_dirs/file.md"), true),
500 (Path::new("test_dirs/file.rs"), true),
501 (Path::new("test_dirs/file.txt"), true),
502 ]
503 );
504 }
505
506 #[test]
507 fn compare_paths_case_semi_sensitive() {
508 let mut paths = vec![
509 (Path::new("test_DIRS"), false),
510 (Path::new("test_DIRS/foo_1"), true),
511 (Path::new("test_DIRS/foo_2"), true),
512 (Path::new("test_DIRS/bar"), true),
513 (Path::new("test_DIRS/BAR"), true),
514 (Path::new("test_dirs"), false),
515 (Path::new("test_dirs/foo_1"), true),
516 (Path::new("test_dirs/foo_2"), true),
517 (Path::new("test_dirs/bar"), true),
518 (Path::new("test_dirs/BAR"), true),
519 ];
520 paths.sort_by(|&a, &b| compare_paths(a, b));
521 assert_eq!(
522 paths,
523 vec![
524 (Path::new("test_dirs"), false),
525 (Path::new("test_dirs/bar"), true),
526 (Path::new("test_dirs/BAR"), true),
527 (Path::new("test_dirs/foo_1"), true),
528 (Path::new("test_dirs/foo_2"), true),
529 (Path::new("test_DIRS"), false),
530 (Path::new("test_DIRS/bar"), true),
531 (Path::new("test_DIRS/BAR"), true),
532 (Path::new("test_DIRS/foo_1"), true),
533 (Path::new("test_DIRS/foo_2"), true),
534 ]
535 );
536 }
537
538 #[test]
539 fn path_with_position_parse_posix_path() {
540 // Test POSIX filename edge cases
541 // Read more at https://en.wikipedia.org/wiki/Filename
542 assert_eq!(
543 PathWithPosition::parse_str(" test_file"),
544 PathWithPosition {
545 path: PathBuf::from("test_file"),
546 row: None,
547 column: None
548 }
549 );
550
551 assert_eq!(
552 PathWithPosition::parse_str("a:bc:.zip:1"),
553 PathWithPosition {
554 path: PathBuf::from("a:bc:.zip"),
555 row: Some(1),
556 column: None
557 }
558 );
559
560 assert_eq!(
561 PathWithPosition::parse_str("one.second.zip:1"),
562 PathWithPosition {
563 path: PathBuf::from("one.second.zip"),
564 row: Some(1),
565 column: None
566 }
567 );
568
569 // Trim off trailing `:`s for otherwise valid input.
570 assert_eq!(
571 PathWithPosition::parse_str("test_file:10:1:"),
572 PathWithPosition {
573 path: PathBuf::from("test_file"),
574 row: Some(10),
575 column: Some(1)
576 }
577 );
578
579 assert_eq!(
580 PathWithPosition::parse_str("test_file.rs:"),
581 PathWithPosition {
582 path: PathBuf::from("test_file.rs"),
583 row: None,
584 column: None
585 }
586 );
587
588 assert_eq!(
589 PathWithPosition::parse_str("test_file.rs:1:"),
590 PathWithPosition {
591 path: PathBuf::from("test_file.rs"),
592 row: Some(1),
593 column: None
594 }
595 );
596 }
597
598 #[test]
599 #[cfg(not(target_os = "windows"))]
600 fn path_with_position_parse_posix_path_with_suffix() {
601 assert_eq!(
602 PathWithPosition::parse_str("app-editors:zed-0.143.6:20240710-201212.log:34:"),
603 PathWithPosition {
604 path: PathBuf::from("app-editors:zed-0.143.6:20240710-201212.log"),
605 row: Some(34),
606 column: None,
607 }
608 );
609
610 assert_eq!(
611 PathWithPosition::parse_str("crates/file_finder/src/file_finder.rs:1902:13:"),
612 PathWithPosition {
613 path: PathBuf::from("crates/file_finder/src/file_finder.rs"),
614 row: Some(1902),
615 column: Some(13),
616 }
617 );
618
619 assert_eq!(
620 PathWithPosition::parse_str("crate/utils/src/test:today.log:34"),
621 PathWithPosition {
622 path: PathBuf::from("crate/utils/src/test:today.log"),
623 row: Some(34),
624 column: None,
625 }
626 );
627 }
628
629 #[test]
630 #[cfg(target_os = "windows")]
631 fn path_with_position_parse_windows_path() {
632 assert_eq!(
633 PathWithPosition::parse_str("crates\\utils\\paths.rs"),
634 PathWithPosition {
635 path: PathBuf::from("crates\\utils\\paths.rs"),
636 row: None,
637 column: None
638 }
639 );
640
641 assert_eq!(
642 PathWithPosition::parse_str("C:\\Users\\someone\\test_file.rs"),
643 PathWithPosition {
644 path: PathBuf::from("C:\\Users\\someone\\test_file.rs"),
645 row: None,
646 column: None
647 }
648 );
649 }
650
651 #[test]
652 #[cfg(target_os = "windows")]
653 fn path_with_position_parse_windows_path_with_suffix() {
654 assert_eq!(
655 PathWithPosition::parse_str("crates\\utils\\paths.rs:101"),
656 PathWithPosition {
657 path: PathBuf::from("crates\\utils\\paths.rs"),
658 row: Some(101),
659 column: None
660 }
661 );
662
663 assert_eq!(
664 PathWithPosition::parse_str("\\\\?\\C:\\Users\\someone\\test_file.rs:1:20"),
665 PathWithPosition {
666 path: PathBuf::from("\\\\?\\C:\\Users\\someone\\test_file.rs"),
667 row: Some(1),
668 column: Some(20)
669 }
670 );
671
672 assert_eq!(
673 PathWithPosition::parse_str("C:\\Users\\someone\\test_file.rs(1902,13)"),
674 PathWithPosition {
675 path: PathBuf::from("C:\\Users\\someone\\test_file.rs"),
676 row: Some(1902),
677 column: Some(13)
678 }
679 );
680
681 // Trim off trailing `:`s for otherwise valid input.
682 assert_eq!(
683 PathWithPosition::parse_str("\\\\?\\C:\\Users\\someone\\test_file.rs:1902:13:"),
684 PathWithPosition {
685 path: PathBuf::from("\\\\?\\C:\\Users\\someone\\test_file.rs"),
686 row: Some(1902),
687 column: Some(13)
688 }
689 );
690
691 assert_eq!(
692 PathWithPosition::parse_str("\\\\?\\C:\\Users\\someone\\test_file.rs:1902:13:15:"),
693 PathWithPosition {
694 path: PathBuf::from("\\\\?\\C:\\Users\\someone\\test_file.rs:1902"),
695 row: Some(13),
696 column: Some(15)
697 }
698 );
699
700 assert_eq!(
701 PathWithPosition::parse_str("\\\\?\\C:\\Users\\someone\\test_file.rs:1902:::15:"),
702 PathWithPosition {
703 path: PathBuf::from("\\\\?\\C:\\Users\\someone\\test_file.rs:1902"),
704 row: Some(15),
705 column: None
706 }
707 );
708
709 assert_eq!(
710 PathWithPosition::parse_str("\\\\?\\C:\\Users\\someone\\test_file.rs(1902,13):"),
711 PathWithPosition {
712 path: PathBuf::from("\\\\?\\C:\\Users\\someone\\test_file.rs"),
713 row: Some(1902),
714 column: Some(13),
715 }
716 );
717
718 assert_eq!(
719 PathWithPosition::parse_str("\\\\?\\C:\\Users\\someone\\test_file.rs(1902):"),
720 PathWithPosition {
721 path: PathBuf::from("\\\\?\\C:\\Users\\someone\\test_file.rs"),
722 row: Some(1902),
723 column: None,
724 }
725 );
726
727 assert_eq!(
728 PathWithPosition::parse_str("C:\\Users\\someone\\test_file.rs:1902:13:"),
729 PathWithPosition {
730 path: PathBuf::from("C:\\Users\\someone\\test_file.rs"),
731 row: Some(1902),
732 column: Some(13),
733 }
734 );
735
736 assert_eq!(
737 PathWithPosition::parse_str("C:\\Users\\someone\\test_file.rs(1902,13):"),
738 PathWithPosition {
739 path: PathBuf::from("C:\\Users\\someone\\test_file.rs"),
740 row: Some(1902),
741 column: Some(13),
742 }
743 );
744
745 assert_eq!(
746 PathWithPosition::parse_str("C:\\Users\\someone\\test_file.rs(1902):"),
747 PathWithPosition {
748 path: PathBuf::from("C:\\Users\\someone\\test_file.rs"),
749 row: Some(1902),
750 column: None,
751 }
752 );
753
754 assert_eq!(
755 PathWithPosition::parse_str("crates/utils/paths.rs:101"),
756 PathWithPosition {
757 path: PathBuf::from("crates\\utils\\paths.rs"),
758 row: Some(101),
759 column: None,
760 }
761 );
762 }
763
764 #[test]
765 fn test_path_compact() {
766 let path: PathBuf = [
767 home_dir().to_string_lossy().to_string(),
768 "some_file.txt".to_string(),
769 ]
770 .iter()
771 .collect();
772 if cfg!(any(target_os = "linux", target_os = "freebsd")) || cfg!(target_os = "macos") {
773 assert_eq!(path.compact().to_str(), Some("~/some_file.txt"));
774 } else {
775 assert_eq!(path.compact().to_str(), path.to_str());
776 }
777 }
778
779 #[test]
780 fn test_icon_stem_or_suffix() {
781 // No dots in name
782 let path = Path::new("/a/b/c/file_name.rs");
783 assert_eq!(path.icon_stem_or_suffix(), Some("rs"));
784
785 // Single dot in name
786 let path = Path::new("/a/b/c/file.name.rs");
787 assert_eq!(path.icon_stem_or_suffix(), Some("rs"));
788
789 // No suffix
790 let path = Path::new("/a/b/c/file");
791 assert_eq!(path.icon_stem_or_suffix(), Some("file"));
792
793 // Multiple dots in name
794 let path = Path::new("/a/b/c/long.file.name.rs");
795 assert_eq!(path.icon_stem_or_suffix(), Some("rs"));
796
797 // Hidden file, no extension
798 let path = Path::new("/a/b/c/.gitignore");
799 assert_eq!(path.icon_stem_or_suffix(), Some("gitignore"));
800
801 // Hidden file, with extension
802 let path = Path::new("/a/b/c/.eslintrc.js");
803 assert_eq!(path.icon_stem_or_suffix(), Some("eslintrc.js"));
804 }
805
806 #[test]
807 fn test_extension_or_hidden_file_name() {
808 // No dots in name
809 let path = Path::new("/a/b/c/file_name.rs");
810 assert_eq!(path.extension_or_hidden_file_name(), Some("rs"));
811
812 // Single dot in name
813 let path = Path::new("/a/b/c/file.name.rs");
814 assert_eq!(path.extension_or_hidden_file_name(), Some("rs"));
815
816 // Multiple dots in name
817 let path = Path::new("/a/b/c/long.file.name.rs");
818 assert_eq!(path.extension_or_hidden_file_name(), Some("rs"));
819
820 // Hidden file, no extension
821 let path = Path::new("/a/b/c/.gitignore");
822 assert_eq!(path.extension_or_hidden_file_name(), Some("gitignore"));
823
824 // Hidden file, with extension
825 let path = Path::new("/a/b/c/.eslintrc.js");
826 assert_eq!(path.extension_or_hidden_file_name(), Some("js"));
827 }
828
829 #[test]
830 fn edge_of_glob() {
831 let path = Path::new("/work/node_modules");
832 let path_matcher = PathMatcher::new(&["**/node_modules/**".to_owned()]).unwrap();
833 assert!(
834 path_matcher.is_match(path),
835 "Path matcher should match {path:?}"
836 );
837 }
838
839 #[test]
840 fn project_search() {
841 let path = Path::new("/Users/someonetoignore/work/zed/zed.dev/node_modules");
842 let path_matcher = PathMatcher::new(&["**/node_modules/**".to_owned()]).unwrap();
843 assert!(
844 path_matcher.is_match(path),
845 "Path matcher should match {path:?}"
846 );
847 }
848
849 #[test]
850 #[cfg(target_os = "windows")]
851 fn test_sanitized_path() {
852 let path = Path::new("C:\\Users\\someone\\test_file.rs");
853 let sanitized_path = SanitizedPath::from(path);
854 assert_eq!(
855 sanitized_path.to_string(),
856 "C:\\Users\\someone\\test_file.rs"
857 );
858
859 let path = Path::new("\\\\?\\C:\\Users\\someone\\test_file.rs");
860 let sanitized_path = SanitizedPath::from(path);
861 assert_eq!(
862 sanitized_path.to_string(),
863 "C:\\Users\\someone\\test_file.rs"
864 );
865 }
866}