mirror of
https://github.com/zed-industries/zed.git
synced 2026-08-24 08:24:58 +00:00
This is necessary to remove some `util` dependencies from crates, as well as better sharing for our projects. This also includes the WIP AbsPath abstraction as well as some bug fixes from internal tooling. Release Notes: - N/A or Added/Fixed/Improved ...
286 lines
7.7 KiB
Rust
286 lines
7.7 KiB
Rust
use std::{
|
|
borrow::{Borrow, Cow},
|
|
fmt, io,
|
|
ops::Deref,
|
|
path::{Path, PathBuf},
|
|
rc::Rc,
|
|
sync::Arc,
|
|
};
|
|
|
|
use anyhow::Context;
|
|
|
|
use crate::{PathStyle, rel_path::RelPath};
|
|
|
|
// An absolute path on the user's local filesystem.
|
|
// Requires paths to be valid utf-8
|
|
#[derive(PartialEq, Eq, Hash, Debug, PartialOrd, Ord)]
|
|
#[repr(transparent)]
|
|
pub struct AbsPath(Path);
|
|
|
|
impl AbsPath {
|
|
pub fn new(path: &Path) -> anyhow::Result<&Self> {
|
|
if !path.is_absolute() {
|
|
return Err(anyhow::anyhow!("Path is not absolute: {:?}", path));
|
|
}
|
|
if path.to_str().is_none() {
|
|
return Err(anyhow::anyhow!("Path is not valid utf-8: {:?}", path));
|
|
}
|
|
Ok(Self::new_unchecked(path))
|
|
}
|
|
|
|
fn new_unchecked(path: &Path) -> &Self {
|
|
// SAFETY: `AbsPath` is a `repr(transparent)` wrapper around `Path`.
|
|
unsafe { &*(path as *const Path as *const Self) }
|
|
}
|
|
|
|
pub fn to_abs_path_buf(&self) -> AbsPathBuf {
|
|
AbsPathBuf(self.0.to_owned())
|
|
}
|
|
|
|
pub fn join(&self, name: impl AsRef<str>) -> AbsPathBuf {
|
|
AbsPathBuf(self.0.join(name.as_ref()))
|
|
}
|
|
|
|
pub fn join_rel_path(&self, relative_path: &RelPath) -> AbsPathBuf {
|
|
AbsPathBuf(self.0.join(relative_path.as_std_path()))
|
|
}
|
|
|
|
pub fn parent(&self) -> Option<&AbsPath> {
|
|
let parent = self.0.parent()?;
|
|
Some(AbsPath::new_unchecked(parent))
|
|
}
|
|
|
|
pub fn starts_with(&self, other: &AbsPath) -> bool {
|
|
self.0.starts_with(&other.0)
|
|
}
|
|
|
|
pub fn ends_with(&self, other: &RelPath) -> bool {
|
|
self.0.ends_with(other.as_std_path())
|
|
}
|
|
|
|
pub fn is_descendant_of(&self, ancestor: &Self) -> bool {
|
|
if self == ancestor {
|
|
return false;
|
|
}
|
|
self.starts_with(ancestor)
|
|
}
|
|
|
|
pub fn file_name(&self) -> Option<&str> {
|
|
self.0.file_name()?.to_str()
|
|
}
|
|
|
|
pub fn display(&self) -> impl fmt::Display + '_ {
|
|
self.0.display()
|
|
}
|
|
|
|
pub fn as_std_path(&self) -> &Path {
|
|
&self.0
|
|
}
|
|
|
|
pub fn as_str(&self) -> &str {
|
|
self.0
|
|
.to_str()
|
|
.expect("valid UTF-8 enforced in constructor")
|
|
}
|
|
|
|
pub fn ancestors(&self) -> impl Iterator<Item = &AbsPath> {
|
|
self.0.ancestors().map(|p| AbsPath::new_unchecked(p))
|
|
}
|
|
|
|
pub fn strip_prefix<'a>(&'a self, prefix: &AbsPath) -> Option<Cow<'a, RelPath>> {
|
|
let prefix = self.0.strip_prefix(&prefix.0).ok()?;
|
|
RelPath::new(prefix, PathStyle::local()).ok()
|
|
}
|
|
}
|
|
|
|
impl ToOwned for AbsPath {
|
|
type Owned = AbsPathBuf;
|
|
|
|
fn to_owned(&self) -> Self::Owned {
|
|
self.to_abs_path_buf()
|
|
}
|
|
}
|
|
|
|
impl AsRef<Path> for AbsPath {
|
|
fn as_ref(&self) -> &Path {
|
|
&self.0
|
|
}
|
|
}
|
|
|
|
impl AsRef<AbsPath> for AbsPath {
|
|
fn as_ref(&self) -> &AbsPath {
|
|
self
|
|
}
|
|
}
|
|
|
|
impl From<&AbsPath> for Arc<AbsPath> {
|
|
fn from(path: &AbsPath) -> Self {
|
|
let arc: Arc<Path> = Arc::from(&path.0);
|
|
// SAFETY: `AbsPath` is a `repr(transparent)` wrapper around `Path`.
|
|
unsafe { Arc::from_raw(Arc::into_raw(arc) as *const AbsPath) }
|
|
}
|
|
}
|
|
|
|
impl From<&AbsPath> for Rc<AbsPath> {
|
|
fn from(path: &AbsPath) -> Self {
|
|
let arc: Rc<Path> = Rc::from(&path.0);
|
|
// SAFETY: `AbsPath` is a `repr(transparent)` wrapper around `Path`.
|
|
unsafe { Rc::from_raw(Rc::into_raw(arc) as *const AbsPath) }
|
|
}
|
|
}
|
|
|
|
// An absolute path on the user's local filesystem.
|
|
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
|
pub struct AbsPathBuf(PathBuf);
|
|
|
|
impl AbsPathBuf {
|
|
pub fn new(path: impl Into<PathBuf>) -> anyhow::Result<Self> {
|
|
let path = path.into();
|
|
if let Err(e) = AbsPath::new(&path) {
|
|
return Err(e);
|
|
}
|
|
Ok(Self(path))
|
|
}
|
|
|
|
pub fn home_dir() -> anyhow::Result<Self> {
|
|
let home = std::env::home_dir().context("no home dir available")?;
|
|
Self::new(home)
|
|
}
|
|
|
|
/// Resolves `path` to its canonical on-disk spelling: symlinks and `..`
|
|
/// are resolved, relative input is anchored on the current directory, and
|
|
/// on case-insensitive filesystems each component takes the casing stored
|
|
/// on disk. Unlike [`std::fs::canonicalize`], the result never uses
|
|
/// Windows extended-length (`\\?\`) syntax, which chokes tools the path
|
|
/// is later handed to (e.g. `git`).
|
|
///
|
|
/// Paths act as identity in several places (lock keys, watch-target
|
|
/// comparisons, persisted repository records), so canonicalize a path
|
|
/// where it enters the system whenever it may have been spelled by a user
|
|
/// or an external tool.
|
|
pub fn canonicalize(path: impl AsRef<Path>) -> io::Result<Self> {
|
|
let canonical = dunce::canonicalize(path.as_ref())?;
|
|
Self::new(canonical).map_err(io::Error::other)
|
|
}
|
|
|
|
pub fn push(&mut self, name: &str) {
|
|
self.0.push(name);
|
|
}
|
|
|
|
#[cfg(any(test, feature = "test-support"))]
|
|
pub fn new_test(path: &'static str) -> Self {
|
|
if cfg!(windows) {
|
|
Self::new(format!("C:{path}")).unwrap()
|
|
} else {
|
|
Self::new(path).unwrap()
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(any(test, feature = "test-support"))]
|
|
pub fn abs_path(path: &str) -> AbsPathBuf {
|
|
if cfg!(windows) {
|
|
AbsPathBuf::new(format!("C:{path}")).unwrap()
|
|
} else {
|
|
AbsPathBuf::new(path).unwrap()
|
|
}
|
|
}
|
|
|
|
impl Deref for AbsPathBuf {
|
|
type Target = AbsPath;
|
|
|
|
fn deref(&self) -> &Self::Target {
|
|
AbsPath::new_unchecked(&self.0)
|
|
}
|
|
}
|
|
|
|
impl Borrow<AbsPath> for AbsPathBuf {
|
|
fn borrow(&self) -> &AbsPath {
|
|
self
|
|
}
|
|
}
|
|
|
|
impl AsRef<Path> for AbsPathBuf {
|
|
fn as_ref(&self) -> &Path {
|
|
self.0.as_ref()
|
|
}
|
|
}
|
|
|
|
impl AsRef<AbsPath> for AbsPathBuf {
|
|
fn as_ref(&self) -> &AbsPath {
|
|
self
|
|
}
|
|
}
|
|
|
|
impl From<AbsPathBuf> for PathBuf {
|
|
fn from(path: AbsPathBuf) -> PathBuf {
|
|
path.0
|
|
}
|
|
}
|
|
|
|
impl fmt::Display for AbsPathBuf {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
self.as_str().fmt(f)
|
|
}
|
|
}
|
|
|
|
impl PartialEq<AbsPath> for AbsPathBuf {
|
|
fn eq(&self, other: &AbsPath) -> bool {
|
|
**self == *other
|
|
}
|
|
}
|
|
|
|
impl PartialEq<AbsPathBuf> for AbsPath {
|
|
fn eq(&self, other: &AbsPathBuf) -> bool {
|
|
*self == **other
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_canonicalize_restores_on_disk_spelling() {
|
|
let temp = tempfile::tempdir().unwrap();
|
|
let root = dunce::canonicalize(temp.path()).unwrap();
|
|
let dir = root.join("CamelCase");
|
|
std::fs::create_dir(&dir).unwrap();
|
|
|
|
let canonical = AbsPathBuf::canonicalize(&dir).unwrap();
|
|
assert_eq!(canonical.as_std_path(), dir);
|
|
assert!(
|
|
!canonical.as_str().starts_with(r"\\?\"),
|
|
"canonical paths must not use Windows extended-length syntax: {canonical}"
|
|
);
|
|
|
|
// A differently-cased spelling addresses the same directory only on a
|
|
// case-insensitive filesystem; when it does, canonicalization must
|
|
// restore the stored casing.
|
|
let lowercased = root.join("camelcase");
|
|
if std::fs::metadata(&lowercased).is_ok() {
|
|
assert_eq!(
|
|
AbsPathBuf::canonicalize(&lowercased).unwrap().as_std_path(),
|
|
dir,
|
|
"canonicalization should restore the on-disk casing"
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_new_test_normalizes_rooted_paths() {
|
|
if cfg!(windows) {
|
|
assert_eq!(AbsPathBuf::new_test("/").as_str(), "C:/");
|
|
assert_eq!(
|
|
AbsPathBuf::new_test("/test/project").as_str(),
|
|
"C:/test/project"
|
|
);
|
|
} else {
|
|
assert_eq!(AbsPathBuf::new_test("/").as_str(), "/");
|
|
assert_eq!(
|
|
AbsPathBuf::new_test("/test/project").as_str(),
|
|
"/test/project"
|
|
);
|
|
}
|
|
}
|
|
}
|