ZIm/crates/util/src/paths.rs
张小白 cff9ae0bbc
Better absolute path handling (#19727)
Closes #19866

This PR supersedes #19228, as #19228 encountered too many merge
conflicts.

After some exploration, I found that for paths with the `\\?\` prefix,
we can safely remove it and consistently use the clean paths in all
cases. Previously, in #19228, I thought we would still need the `\\?\`
prefix for IO operations to handle long paths better. However, this
turns out to be unnecessary because Rust automatically manages this for
us when calling IO-related APIs. For details, refer to Rust's internal
function
[`get_long_path`](017ae1b21f/library/std/src/sys/path/windows.rs (L225-L233)).

Therefore, we can always store and use paths without the `\\?\` prefix.

This PR introduces a `SanitizedPath` structure, which represents a path
stripped of the `\\?\` prefix. To prevent untrimmed paths from being
mistakenly passed into `Worktree`, the type of `Worktree`’s `abs_path`
member variable has been changed to `SanitizedPath`.

Additionally, this PR reverts the changes of #15856 and #18726. After
testing, it appears that the issues those PRs addressed can be resolved
by this PR.

### Existing Issue
To keep the scope of modifications manageable, `Worktree::abs_path` has
retained its current signature as `fn abs_path(&self) -> Arc<Path>`,
rather than returning a `SanitizedPath`. Updating the method to return
`SanitizedPath`—which may better resolve path inconsistencies—would
likely introduce extensive changes similar to those in #19228.

Currently, the limitation is as follows:

```rust
let abs_path: &Arc<Path> = snapshot.abs_path();
let some_non_trimmed_path = Path::new("\\\\?\\C:\\Users\\user\\Desktop\\project"); 
// The caller performs some actions here:
some_non_trimmed_path.strip_prefix(abs_path);  // This fails
some_non_trimmed_path.starts_with(abs_path);   // This fails too
```

The final two lines will fail because `snapshot.abs_path()` returns a
clean path without the `\\?\` prefix. I have identified two relevant
instances that may face this issue:
-
[lsp_store.rs#L3578](0173479d18/crates/project/src/lsp_store.rs (L3578))
-
[worktree.rs#L4338](0173479d18/crates/worktree/src/worktree.rs (L4338))

Switching `Worktree::abs_path` to return `SanitizedPath` would resolve
these issues but would also lead to many code changes.

Any suggestions or feedback on this approach are very welcome.

cc @SomeoneToIgnore 

Release Notes:

- N/A
2024-11-27 20:22:58 +02:00

866 lines
29 KiB
Rust

use std::cmp;
use std::sync::{Arc, OnceLock};
use std::{
ffi::OsStr,
path::{Path, PathBuf},
sync::LazyLock,
};
use globset::{Glob, GlobSet, GlobSetBuilder};
use regex::Regex;
use serde::{Deserialize, Serialize};
use crate::NumericPrefixWithSuffix;
/// Returns the path to the user's home directory.
pub fn home_dir() -> &'static PathBuf {
static HOME_DIR: OnceLock<PathBuf> = OnceLock::new();
HOME_DIR.get_or_init(|| dirs::home_dir().expect("failed to determine home directory"))
}
pub trait PathExt {
fn compact(&self) -> PathBuf;
fn icon_stem_or_suffix(&self) -> Option<&str>;
fn extension_or_hidden_file_name(&self) -> Option<&str>;
fn try_from_bytes<'a>(bytes: &'a [u8]) -> anyhow::Result<Self>
where
Self: From<&'a Path>,
{
#[cfg(unix)]
{
use std::os::unix::prelude::OsStrExt;
Ok(Self::from(Path::new(OsStr::from_bytes(bytes))))
}
#[cfg(windows)]
{
use anyhow::anyhow;
use tendril::fmt::{Format, WTF8};
WTF8::validate(bytes)
.then(|| {
// Safety: bytes are valid WTF-8 sequence.
Self::from(Path::new(unsafe {
OsStr::from_encoded_bytes_unchecked(bytes)
}))
})
.ok_or_else(|| anyhow!("Invalid WTF-8 sequence: {bytes:?}"))
}
}
}
impl<T: AsRef<Path>> PathExt for T {
/// Compacts a given file path by replacing the user's home directory
/// prefix with a tilde (`~`).
///
/// # Returns
///
/// * A `PathBuf` containing the compacted file path. If the input path
/// does not have the user's home directory prefix, or if we are not on
/// Linux or macOS, the original path is returned unchanged.
fn compact(&self) -> PathBuf {
if cfg!(any(target_os = "linux", target_os = "freebsd")) || cfg!(target_os = "macos") {
match self.as_ref().strip_prefix(home_dir().as_path()) {
Ok(relative_path) => {
let mut shortened_path = PathBuf::new();
shortened_path.push("~");
shortened_path.push(relative_path);
shortened_path
}
Err(_) => self.as_ref().to_path_buf(),
}
} else {
self.as_ref().to_path_buf()
}
}
/// Returns either the suffix if available, or the file stem otherwise to determine which file icon to use
fn icon_stem_or_suffix(&self) -> Option<&str> {
let path = self.as_ref();
let file_name = path.file_name()?.to_str()?;
if file_name.starts_with('.') {
return file_name.strip_prefix('.');
}
path.extension()
.and_then(|e| e.to_str())
.or_else(|| path.file_stem()?.to_str())
}
/// Returns a file's extension or, if the file is hidden, its name without the leading dot
fn extension_or_hidden_file_name(&self) -> Option<&str> {
if let Some(extension) = self.as_ref().extension() {
return extension.to_str();
}
self.as_ref().file_name()?.to_str()?.split('.').last()
}
}
/// Due to the issue of UNC paths on Windows, which can cause bugs in various parts of Zed, introducing this `SanitizedPath`
/// leverages Rust's type system to ensure that all paths entering Zed are always "sanitized" by removing the `\\\\?\\` prefix.
/// On non-Windows operating systems, this struct is effectively a no-op.
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct SanitizedPath(Arc<Path>);
impl SanitizedPath {
pub fn starts_with(&self, prefix: &SanitizedPath) -> bool {
self.0.starts_with(&prefix.0)
}
pub fn as_path(&self) -> &Arc<Path> {
&self.0
}
pub fn to_string(&self) -> String {
self.0.to_string_lossy().to_string()
}
}
impl From<SanitizedPath> for Arc<Path> {
fn from(sanitized_path: SanitizedPath) -> Self {
sanitized_path.0
}
}
impl<T: AsRef<Path>> From<T> for SanitizedPath {
#[cfg(not(target_os = "windows"))]
fn from(path: T) -> Self {
let path = path.as_ref();
SanitizedPath(path.into())
}
#[cfg(target_os = "windows")]
fn from(path: T) -> Self {
let path = path.as_ref();
SanitizedPath(dunce::simplified(path).into())
}
}
/// A delimiter to use in `path_query:row_number:column_number` strings parsing.
pub const FILE_ROW_COLUMN_DELIMITER: char = ':';
const ROW_COL_CAPTURE_REGEX: &str = r"(?x)
([^\(]+)(?:
\((\d+),(\d+)\) # filename(row,column)
|
\((\d+)\)() # filename(row)
)
|
(.+?)(?:
\:+(\d+)\:(\d+)\:*$ # filename:row:column
|
\:+(\d+)\:*()$ # filename:row
|
\:*()()$ # filename:
)";
/// A representation of a path-like string with optional row and column numbers.
/// Matching values example: `te`, `test.rs:22`, `te:22:5`, `test.c(22)`, `test.c(22,5)`etc.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)]
pub struct PathWithPosition {
pub path: PathBuf,
pub row: Option<u32>,
// Absent if row is absent.
pub column: Option<u32>,
}
impl PathWithPosition {
/// Returns a PathWithPosition from a path.
pub fn from_path(path: PathBuf) -> Self {
Self {
path,
row: None,
column: None,
}
}
/// Parses a string that possibly has `:row:column` or `(row, column)` suffix.
/// Parenthesis format is used by [MSBuild](https://learn.microsoft.com/en-us/visualstudio/msbuild/msbuild-diagnostic-format-for-tasks) compatible tools
/// Ignores trailing `:`s, so `test.rs:22:` is parsed as `test.rs:22`.
/// If the suffix parsing fails, the whole string is parsed as a path.
///
/// Be mindful that `test_file:10:1:` is a valid posix filename.
/// `PathWithPosition` class assumes that the ending position-like suffix is **not** part of the filename.
///
/// # Examples
///
/// ```
/// # use util::paths::PathWithPosition;
/// # use std::path::PathBuf;
/// assert_eq!(PathWithPosition::parse_str("test_file"), PathWithPosition {
/// path: PathBuf::from("test_file"),
/// row: None,
/// column: None,
/// });
/// assert_eq!(PathWithPosition::parse_str("test_file:10"), PathWithPosition {
/// path: PathBuf::from("test_file"),
/// row: Some(10),
/// column: None,
/// });
/// assert_eq!(PathWithPosition::parse_str("test_file.rs"), PathWithPosition {
/// path: PathBuf::from("test_file.rs"),
/// row: None,
/// column: None,
/// });
/// assert_eq!(PathWithPosition::parse_str("test_file.rs:1"), PathWithPosition {
/// path: PathBuf::from("test_file.rs"),
/// row: Some(1),
/// column: None,
/// });
/// assert_eq!(PathWithPosition::parse_str("test_file.rs:1:2"), PathWithPosition {
/// path: PathBuf::from("test_file.rs"),
/// row: Some(1),
/// column: Some(2),
/// });
/// ```
///
/// # Expected parsing results when encounter ill-formatted inputs.
/// ```
/// # use util::paths::PathWithPosition;
/// # use std::path::PathBuf;
/// assert_eq!(PathWithPosition::parse_str("test_file.rs:a"), PathWithPosition {
/// path: PathBuf::from("test_file.rs:a"),
/// row: None,
/// column: None,
/// });
/// assert_eq!(PathWithPosition::parse_str("test_file.rs:a:b"), PathWithPosition {
/// path: PathBuf::from("test_file.rs:a:b"),
/// row: None,
/// column: None,
/// });
/// assert_eq!(PathWithPosition::parse_str("test_file.rs::"), PathWithPosition {
/// path: PathBuf::from("test_file.rs"),
/// row: None,
/// column: None,
/// });
/// assert_eq!(PathWithPosition::parse_str("test_file.rs::1"), PathWithPosition {
/// path: PathBuf::from("test_file.rs"),
/// row: Some(1),
/// column: None,
/// });
/// assert_eq!(PathWithPosition::parse_str("test_file.rs:1::"), PathWithPosition {
/// path: PathBuf::from("test_file.rs"),
/// row: Some(1),
/// column: None,
/// });
/// assert_eq!(PathWithPosition::parse_str("test_file.rs::1:2"), PathWithPosition {
/// path: PathBuf::from("test_file.rs"),
/// row: Some(1),
/// column: Some(2),
/// });
/// assert_eq!(PathWithPosition::parse_str("test_file.rs:1::2"), PathWithPosition {
/// path: PathBuf::from("test_file.rs:1"),
/// row: Some(2),
/// column: None,
/// });
/// assert_eq!(PathWithPosition::parse_str("test_file.rs:1:2:3"), PathWithPosition {
/// path: PathBuf::from("test_file.rs:1"),
/// row: Some(2),
/// column: Some(3),
/// });
/// ```
pub fn parse_str(s: &str) -> Self {
let trimmed = s.trim();
let path = Path::new(trimmed);
let maybe_file_name_with_row_col = path.file_name().unwrap_or_default().to_string_lossy();
if maybe_file_name_with_row_col.is_empty() {
return Self {
path: Path::new(s).to_path_buf(),
row: None,
column: None,
};
}
// Let's avoid repeated init cost on this. It is subject to thread contention, but
// so far this code isn't called from multiple hot paths. Getting contention here
// in the future seems unlikely.
static SUFFIX_RE: LazyLock<Regex> =
LazyLock::new(|| Regex::new(ROW_COL_CAPTURE_REGEX).unwrap());
match SUFFIX_RE
.captures(&maybe_file_name_with_row_col)
.map(|caps| caps.extract())
{
Some((_, [file_name, maybe_row, maybe_column])) => {
let row = maybe_row.parse::<u32>().ok();
let column = maybe_column.parse::<u32>().ok();
let suffix_length = maybe_file_name_with_row_col.len() - file_name.len();
let path_without_suffix = &trimmed[..trimmed.len() - suffix_length];
Self {
path: Path::new(path_without_suffix).to_path_buf(),
row,
column,
}
}
None => Self {
path: Path::new(s).to_path_buf(),
row: None,
column: None,
},
}
}
pub fn map_path<E>(
self,
mapping: impl FnOnce(PathBuf) -> Result<PathBuf, E>,
) -> Result<PathWithPosition, E> {
Ok(PathWithPosition {
path: mapping(self.path)?,
row: self.row,
column: self.column,
})
}
pub fn to_string(&self, path_to_string: impl Fn(&PathBuf) -> String) -> String {
let path_string = path_to_string(&self.path);
if let Some(row) = self.row {
if let Some(column) = self.column {
format!("{path_string}:{row}:{column}")
} else {
format!("{path_string}:{row}")
}
} else {
path_string
}
}
}
#[derive(Clone, Debug, Default)]
pub struct PathMatcher {
sources: Vec<String>,
glob: GlobSet,
}
// impl std::fmt::Display for PathMatcher {
// fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
// self.sources.fmt(f)
// }
// }
impl PartialEq for PathMatcher {
fn eq(&self, other: &Self) -> bool {
self.sources.eq(&other.sources)
}
}
impl Eq for PathMatcher {}
impl PathMatcher {
pub fn new(globs: &[String]) -> Result<Self, globset::Error> {
let globs = globs
.iter()
.map(|glob| Glob::new(glob))
.collect::<Result<Vec<_>, _>>()?;
let sources = globs.iter().map(|glob| glob.glob().to_owned()).collect();
let mut glob_builder = GlobSetBuilder::new();
for single_glob in globs {
glob_builder.add(single_glob);
}
let glob = glob_builder.build()?;
Ok(PathMatcher { glob, sources })
}
pub fn sources(&self) -> &[String] {
&self.sources
}
pub fn is_match<P: AsRef<Path>>(&self, other: P) -> bool {
let other_path = other.as_ref();
self.sources.iter().any(|source| {
let as_bytes = other_path.as_os_str().as_encoded_bytes();
as_bytes.starts_with(source.as_bytes()) || as_bytes.ends_with(source.as_bytes())
}) || self.glob.is_match(other_path)
|| self.check_with_end_separator(other_path)
}
fn check_with_end_separator(&self, path: &Path) -> bool {
let path_str = path.to_string_lossy();
let separator = std::path::MAIN_SEPARATOR_STR;
if path_str.ends_with(separator) {
false
} else {
self.glob.is_match(path_str.to_string() + separator)
}
}
}
pub fn compare_paths(
(path_a, a_is_file): (&Path, bool),
(path_b, b_is_file): (&Path, bool),
) -> cmp::Ordering {
let mut components_a = path_a.components().peekable();
let mut components_b = path_b.components().peekable();
loop {
match (components_a.next(), components_b.next()) {
(Some(component_a), Some(component_b)) => {
let a_is_file = components_a.peek().is_none() && a_is_file;
let b_is_file = components_b.peek().is_none() && b_is_file;
let ordering = a_is_file.cmp(&b_is_file).then_with(|| {
let path_a = Path::new(component_a.as_os_str());
let path_string_a = if a_is_file {
path_a.file_stem()
} else {
path_a.file_name()
}
.map(|s| s.to_string_lossy());
let num_and_remainder_a = path_string_a
.as_deref()
.map(NumericPrefixWithSuffix::from_numeric_prefixed_str);
let path_b = Path::new(component_b.as_os_str());
let path_string_b = if b_is_file {
path_b.file_stem()
} else {
path_b.file_name()
}
.map(|s| s.to_string_lossy());
let num_and_remainder_b = path_string_b
.as_deref()
.map(NumericPrefixWithSuffix::from_numeric_prefixed_str);
num_and_remainder_a.cmp(&num_and_remainder_b).then_with(|| {
if a_is_file && b_is_file {
let ext_a = path_a.extension().unwrap_or_default();
let ext_b = path_b.extension().unwrap_or_default();
ext_a.cmp(ext_b)
} else {
cmp::Ordering::Equal
}
})
});
if !ordering.is_eq() {
return ordering;
}
}
(Some(_), None) => break cmp::Ordering::Greater,
(None, Some(_)) => break cmp::Ordering::Less,
(None, None) => break cmp::Ordering::Equal,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn compare_paths_with_dots() {
let mut paths = vec![
(Path::new("test_dirs"), false),
(Path::new("test_dirs/1.46"), false),
(Path::new("test_dirs/1.46/bar_1"), true),
(Path::new("test_dirs/1.46/bar_2"), true),
(Path::new("test_dirs/1.45"), false),
(Path::new("test_dirs/1.45/foo_2"), true),
(Path::new("test_dirs/1.45/foo_1"), true),
];
paths.sort_by(|&a, &b| compare_paths(a, b));
assert_eq!(
paths,
vec![
(Path::new("test_dirs"), false),
(Path::new("test_dirs/1.45"), false),
(Path::new("test_dirs/1.45/foo_1"), true),
(Path::new("test_dirs/1.45/foo_2"), true),
(Path::new("test_dirs/1.46"), false),
(Path::new("test_dirs/1.46/bar_1"), true),
(Path::new("test_dirs/1.46/bar_2"), true),
]
);
let mut paths = vec![
(Path::new("root1/one.txt"), true),
(Path::new("root1/one.two.txt"), true),
];
paths.sort_by(|&a, &b| compare_paths(a, b));
assert_eq!(
paths,
vec![
(Path::new("root1/one.txt"), true),
(Path::new("root1/one.two.txt"), true),
]
);
}
#[test]
fn compare_paths_with_same_name_different_extensions() {
let mut paths = vec![
(Path::new("test_dirs/file.rs"), true),
(Path::new("test_dirs/file.txt"), true),
(Path::new("test_dirs/file.md"), true),
(Path::new("test_dirs/file"), true),
(Path::new("test_dirs/file.a"), true),
];
paths.sort_by(|&a, &b| compare_paths(a, b));
assert_eq!(
paths,
vec![
(Path::new("test_dirs/file"), true),
(Path::new("test_dirs/file.a"), true),
(Path::new("test_dirs/file.md"), true),
(Path::new("test_dirs/file.rs"), true),
(Path::new("test_dirs/file.txt"), true),
]
);
}
#[test]
fn compare_paths_case_semi_sensitive() {
let mut paths = vec![
(Path::new("test_DIRS"), false),
(Path::new("test_DIRS/foo_1"), true),
(Path::new("test_DIRS/foo_2"), true),
(Path::new("test_DIRS/bar"), true),
(Path::new("test_DIRS/BAR"), true),
(Path::new("test_dirs"), false),
(Path::new("test_dirs/foo_1"), true),
(Path::new("test_dirs/foo_2"), true),
(Path::new("test_dirs/bar"), true),
(Path::new("test_dirs/BAR"), true),
];
paths.sort_by(|&a, &b| compare_paths(a, b));
assert_eq!(
paths,
vec![
(Path::new("test_dirs"), false),
(Path::new("test_dirs/bar"), true),
(Path::new("test_dirs/BAR"), true),
(Path::new("test_dirs/foo_1"), true),
(Path::new("test_dirs/foo_2"), true),
(Path::new("test_DIRS"), false),
(Path::new("test_DIRS/bar"), true),
(Path::new("test_DIRS/BAR"), true),
(Path::new("test_DIRS/foo_1"), true),
(Path::new("test_DIRS/foo_2"), true),
]
);
}
#[test]
fn path_with_position_parse_posix_path() {
// Test POSIX filename edge cases
// Read more at https://en.wikipedia.org/wiki/Filename
assert_eq!(
PathWithPosition::parse_str(" test_file"),
PathWithPosition {
path: PathBuf::from("test_file"),
row: None,
column: None
}
);
assert_eq!(
PathWithPosition::parse_str("a:bc:.zip:1"),
PathWithPosition {
path: PathBuf::from("a:bc:.zip"),
row: Some(1),
column: None
}
);
assert_eq!(
PathWithPosition::parse_str("one.second.zip:1"),
PathWithPosition {
path: PathBuf::from("one.second.zip"),
row: Some(1),
column: None
}
);
// Trim off trailing `:`s for otherwise valid input.
assert_eq!(
PathWithPosition::parse_str("test_file:10:1:"),
PathWithPosition {
path: PathBuf::from("test_file"),
row: Some(10),
column: Some(1)
}
);
assert_eq!(
PathWithPosition::parse_str("test_file.rs:"),
PathWithPosition {
path: PathBuf::from("test_file.rs"),
row: None,
column: None
}
);
assert_eq!(
PathWithPosition::parse_str("test_file.rs:1:"),
PathWithPosition {
path: PathBuf::from("test_file.rs"),
row: Some(1),
column: None
}
);
}
#[test]
#[cfg(not(target_os = "windows"))]
fn path_with_position_parse_posix_path_with_suffix() {
assert_eq!(
PathWithPosition::parse_str("app-editors:zed-0.143.6:20240710-201212.log:34:"),
PathWithPosition {
path: PathBuf::from("app-editors:zed-0.143.6:20240710-201212.log"),
row: Some(34),
column: None,
}
);
assert_eq!(
PathWithPosition::parse_str("crates/file_finder/src/file_finder.rs:1902:13:"),
PathWithPosition {
path: PathBuf::from("crates/file_finder/src/file_finder.rs"),
row: Some(1902),
column: Some(13),
}
);
assert_eq!(
PathWithPosition::parse_str("crate/utils/src/test:today.log:34"),
PathWithPosition {
path: PathBuf::from("crate/utils/src/test:today.log"),
row: Some(34),
column: None,
}
);
}
#[test]
#[cfg(target_os = "windows")]
fn path_with_position_parse_windows_path() {
assert_eq!(
PathWithPosition::parse_str("crates\\utils\\paths.rs"),
PathWithPosition {
path: PathBuf::from("crates\\utils\\paths.rs"),
row: None,
column: None
}
);
assert_eq!(
PathWithPosition::parse_str("C:\\Users\\someone\\test_file.rs"),
PathWithPosition {
path: PathBuf::from("C:\\Users\\someone\\test_file.rs"),
row: None,
column: None
}
);
}
#[test]
#[cfg(target_os = "windows")]
fn path_with_position_parse_windows_path_with_suffix() {
assert_eq!(
PathWithPosition::parse_str("crates\\utils\\paths.rs:101"),
PathWithPosition {
path: PathBuf::from("crates\\utils\\paths.rs"),
row: Some(101),
column: None
}
);
assert_eq!(
PathWithPosition::parse_str("\\\\?\\C:\\Users\\someone\\test_file.rs:1:20"),
PathWithPosition {
path: PathBuf::from("\\\\?\\C:\\Users\\someone\\test_file.rs"),
row: Some(1),
column: Some(20)
}
);
assert_eq!(
PathWithPosition::parse_str("C:\\Users\\someone\\test_file.rs(1902,13)"),
PathWithPosition {
path: PathBuf::from("C:\\Users\\someone\\test_file.rs"),
row: Some(1902),
column: Some(13)
}
);
// Trim off trailing `:`s for otherwise valid input.
assert_eq!(
PathWithPosition::parse_str("\\\\?\\C:\\Users\\someone\\test_file.rs:1902:13:"),
PathWithPosition {
path: PathBuf::from("\\\\?\\C:\\Users\\someone\\test_file.rs"),
row: Some(1902),
column: Some(13)
}
);
assert_eq!(
PathWithPosition::parse_str("\\\\?\\C:\\Users\\someone\\test_file.rs:1902:13:15:"),
PathWithPosition {
path: PathBuf::from("\\\\?\\C:\\Users\\someone\\test_file.rs:1902"),
row: Some(13),
column: Some(15)
}
);
assert_eq!(
PathWithPosition::parse_str("\\\\?\\C:\\Users\\someone\\test_file.rs:1902:::15:"),
PathWithPosition {
path: PathBuf::from("\\\\?\\C:\\Users\\someone\\test_file.rs:1902"),
row: Some(15),
column: None
}
);
assert_eq!(
PathWithPosition::parse_str("\\\\?\\C:\\Users\\someone\\test_file.rs(1902,13):"),
PathWithPosition {
path: PathBuf::from("\\\\?\\C:\\Users\\someone\\test_file.rs"),
row: Some(1902),
column: Some(13),
}
);
assert_eq!(
PathWithPosition::parse_str("\\\\?\\C:\\Users\\someone\\test_file.rs(1902):"),
PathWithPosition {
path: PathBuf::from("\\\\?\\C:\\Users\\someone\\test_file.rs"),
row: Some(1902),
column: None,
}
);
assert_eq!(
PathWithPosition::parse_str("C:\\Users\\someone\\test_file.rs:1902:13:"),
PathWithPosition {
path: PathBuf::from("C:\\Users\\someone\\test_file.rs"),
row: Some(1902),
column: Some(13),
}
);
assert_eq!(
PathWithPosition::parse_str("C:\\Users\\someone\\test_file.rs(1902,13):"),
PathWithPosition {
path: PathBuf::from("C:\\Users\\someone\\test_file.rs"),
row: Some(1902),
column: Some(13),
}
);
assert_eq!(
PathWithPosition::parse_str("C:\\Users\\someone\\test_file.rs(1902):"),
PathWithPosition {
path: PathBuf::from("C:\\Users\\someone\\test_file.rs"),
row: Some(1902),
column: None,
}
);
assert_eq!(
PathWithPosition::parse_str("crates/utils/paths.rs:101"),
PathWithPosition {
path: PathBuf::from("crates\\utils\\paths.rs"),
row: Some(101),
column: None,
}
);
}
#[test]
fn test_path_compact() {
let path: PathBuf = [
home_dir().to_string_lossy().to_string(),
"some_file.txt".to_string(),
]
.iter()
.collect();
if cfg!(any(target_os = "linux", target_os = "freebsd")) || cfg!(target_os = "macos") {
assert_eq!(path.compact().to_str(), Some("~/some_file.txt"));
} else {
assert_eq!(path.compact().to_str(), path.to_str());
}
}
#[test]
fn test_icon_stem_or_suffix() {
// No dots in name
let path = Path::new("/a/b/c/file_name.rs");
assert_eq!(path.icon_stem_or_suffix(), Some("rs"));
// Single dot in name
let path = Path::new("/a/b/c/file.name.rs");
assert_eq!(path.icon_stem_or_suffix(), Some("rs"));
// No suffix
let path = Path::new("/a/b/c/file");
assert_eq!(path.icon_stem_or_suffix(), Some("file"));
// Multiple dots in name
let path = Path::new("/a/b/c/long.file.name.rs");
assert_eq!(path.icon_stem_or_suffix(), Some("rs"));
// Hidden file, no extension
let path = Path::new("/a/b/c/.gitignore");
assert_eq!(path.icon_stem_or_suffix(), Some("gitignore"));
// Hidden file, with extension
let path = Path::new("/a/b/c/.eslintrc.js");
assert_eq!(path.icon_stem_or_suffix(), Some("eslintrc.js"));
}
#[test]
fn test_extension_or_hidden_file_name() {
// No dots in name
let path = Path::new("/a/b/c/file_name.rs");
assert_eq!(path.extension_or_hidden_file_name(), Some("rs"));
// Single dot in name
let path = Path::new("/a/b/c/file.name.rs");
assert_eq!(path.extension_or_hidden_file_name(), Some("rs"));
// Multiple dots in name
let path = Path::new("/a/b/c/long.file.name.rs");
assert_eq!(path.extension_or_hidden_file_name(), Some("rs"));
// Hidden file, no extension
let path = Path::new("/a/b/c/.gitignore");
assert_eq!(path.extension_or_hidden_file_name(), Some("gitignore"));
// Hidden file, with extension
let path = Path::new("/a/b/c/.eslintrc.js");
assert_eq!(path.extension_or_hidden_file_name(), Some("js"));
}
#[test]
fn edge_of_glob() {
let path = Path::new("/work/node_modules");
let path_matcher = PathMatcher::new(&["**/node_modules/**".to_owned()]).unwrap();
assert!(
path_matcher.is_match(path),
"Path matcher should match {path:?}"
);
}
#[test]
fn project_search() {
let path = Path::new("/Users/someonetoignore/work/zed/zed.dev/node_modules");
let path_matcher = PathMatcher::new(&["**/node_modules/**".to_owned()]).unwrap();
assert!(
path_matcher.is_match(path),
"Path matcher should match {path:?}"
);
}
#[test]
#[cfg(target_os = "windows")]
fn test_sanitized_path() {
let path = Path::new("C:\\Users\\someone\\test_file.rs");
let sanitized_path = SanitizedPath::from(path);
assert_eq!(
sanitized_path.to_string(),
"C:\\Users\\someone\\test_file.rs"
);
let path = Path::new("\\\\?\\C:\\Users\\someone\\test_file.rs");
let sanitized_path = SanitizedPath::from(path);
assert_eq!(
sanitized_path.to_string(),
"C:\\Users\\someone\\test_file.rs"
);
}
}