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<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}