paths.rs

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