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