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