2042 lines · 69.2 KB · Rust
RawDownload
use anyhow::Context;use globset::{Glob, GlobSet, GlobSetBuilder};use itertools::Itertools;use regex::Regex;use serde::{Deserialize, Serialize};use std::cmp::Ordering;use std::error::Error;use std::fmt::{Display, Formatter};use std::mem;use std::path::StripPrefixError;use std::sync::{Arc, OnceLock};use std::{    ffi::OsStr,    path::{Path, PathBuf},    sync::LazyLock,}; use crate::{rel_path::RelPath, shell::ShellKind}; static HOME_DIR: OnceLock<PathBuf> = OnceLock::new(); /// Returns the path to the user's home directory.pub fn home_dir() -> &'static PathBuf {    HOME_DIR.get_or_init(|| {        if cfg!(any(test, feature = "test-support")) {            if cfg!(target_os = "macos") {                PathBuf::from("/Users/zed")            } else if cfg!(target_os = "windows") {                PathBuf::from("C:\\Users\\zed")            } else {                PathBuf::from("/home/zed")            }        } else {            dirs::home_dir().expect("failed to determine home directory")        }    })} pub trait PathExt {    /// 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;     /// 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>;     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 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)                    }))                })                .with_context(|| format!("Invalid WTF-8 sequence: {bytes:?}"))        }    }     /// Converts a local path to one that can be used inside of WSL.    /// Returns `None` if the path cannot be converted into a WSL one (network share).    fn local_to_wsl(&self) -> Option<PathBuf>;     /// Returns a file's "full" joined collection of extensions, in the case where a file does not    /// just have a singular extension but instead has multiple (e.g File.tar.gz, Component.stories.tsx)    ///    /// Will provide back the extensions joined together such as tar.gz or stories.tsx    fn multiple_extensions(&self) -> Option<String>;     /// Try to make a shell-safe representation of the path.    fn try_shell_safe(&self, shell_kind: ShellKind) -> anyhow::Result<String>;} impl<T: AsRef<Path>> PathExt for T {    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()        }    }     fn extension_or_hidden_file_name(&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())    }     fn local_to_wsl(&self) -> Option<PathBuf> {        // quite sketchy to convert this back to path at the end, but a lot of functions only accept paths        // todo: ideally rework them..?        let mut new_path = std::ffi::OsString::new();        for component in self.as_ref().components() {            match component {                std::path::Component::Prefix(prefix) => {                    let drive_letter = prefix.as_os_str().to_string_lossy().to_lowercase();                    let drive_letter = drive_letter.strip_suffix(':')?;                     new_path.push(format!("/mnt/{}", drive_letter));                }                std::path::Component::RootDir => {}                std::path::Component::CurDir => {                    new_path.push("/.");                }                std::path::Component::ParentDir => {                    new_path.push("/..");                }                std::path::Component::Normal(os_str) => {                    new_path.push("/");                    new_path.push(os_str);                }            }        }         Some(new_path.into())    }     fn multiple_extensions(&self) -> Option<String> {        let path = self.as_ref();        let file_name = path.file_name()?.to_str()?;         let parts: Vec<&str> = file_name            .split('.')            // Skip the part with the file name extension            .skip(1)            .collect();         if parts.len() < 2 {            return None;        }         Some(parts.into_iter().join("."))    }     fn try_shell_safe(&self, shell_kind: ShellKind) -> anyhow::Result<String> {        let path_str = self            .as_ref()            .to_str()            .with_context(|| "Path contains invalid UTF-8")?;        shell_kind            .try_quote(path_str)            .as_deref()            .map(ToOwned::to_owned)            .context("Failed to quote path")    }} pub fn path_ends_with(base: &Path, suffix: &Path) -> bool {    strip_path_suffix(base, suffix).is_some()} pub fn strip_path_suffix<'a>(base: &'a Path, suffix: &Path) -> Option<&'a Path> {    if let Some(remainder) = base        .as_os_str()        .as_encoded_bytes()        .strip_suffix(suffix.as_os_str().as_encoded_bytes())    {        if remainder            .last()            .is_none_or(|last_byte| std::path::is_separator(*last_byte as char))        {            let os_str = unsafe {                OsStr::from_encoded_bytes_unchecked(                    &remainder[0..remainder.len().saturating_sub(1)],                )            };            return Some(Path::new(os_str));        }    }    None} /// In memory, this is identical to `Path`. On non-Windows conversions to this type are no-ops. On/// windows, these conversions sanitize UNC paths by removing the `\\\\?\\` prefix.#[derive(Eq, PartialEq, Hash, Ord, PartialOrd)]#[repr(transparent)]pub struct SanitizedPath(Path); impl SanitizedPath {    pub fn new<T: AsRef<Path> + ?Sized>(path: &T) -> &Self {        #[cfg(not(target_os = "windows"))]        return Self::unchecked_new(path.as_ref());         #[cfg(target_os = "windows")]        return Self::unchecked_new(dunce::simplified(path.as_ref()));    }     pub fn unchecked_new<T: AsRef<Path> + ?Sized>(path: &T) -> &Self {        // safe because `Path` and `SanitizedPath` have the same repr and Drop impl        unsafe { mem::transmute::<&Path, &Self>(path.as_ref()) }    }     pub fn from_arc(path: Arc<Path>) -> Arc<Self> {        // safe because `Path` and `SanitizedPath` have the same repr and Drop impl        #[cfg(not(target_os = "windows"))]        return unsafe { mem::transmute::<Arc<Path>, Arc<Self>>(path) };         // TODO: could avoid allocating here if dunce::simplified results in the same path        #[cfg(target_os = "windows")]        return Self::new(&path).into();    }     pub fn new_arc<T: AsRef<Path> + ?Sized>(path: &T) -> Arc<Self> {        Self::new(path).into()    }     pub fn cast_arc(path: Arc<Self>) -> Arc<Path> {        // safe because `Path` and `SanitizedPath` have the same repr and Drop impl        unsafe { mem::transmute::<Arc<Self>, Arc<Path>>(path) }    }     pub fn cast_arc_ref(path: &Arc<Self>) -> &Arc<Path> {        // safe because `Path` and `SanitizedPath` have the same repr and Drop impl        unsafe { mem::transmute::<&Arc<Self>, &Arc<Path>>(path) }    }     pub fn starts_with(&self, prefix: &Self) -> bool {        self.0.starts_with(&prefix.0)    }     pub fn as_path(&self) -> &Path {        &self.0    }     pub fn file_name(&self) -> Option<&std::ffi::OsStr> {        self.0.file_name()    }     pub fn extension(&self) -> Option<&std::ffi::OsStr> {        self.0.extension()    }     pub fn join<P: AsRef<Path>>(&self, path: P) -> PathBuf {        self.0.join(path)    }     pub fn parent(&self) -> Option<&Self> {        self.0.parent().map(Self::unchecked_new)    }     pub fn strip_prefix(&self, base: &Self) -> Result<&Path, StripPrefixError> {        self.0.strip_prefix(base.as_path())    }     pub fn to_str(&self) -> Option<&str> {        self.0.to_str()    }     pub fn to_path_buf(&self) -> PathBuf {        self.0.to_path_buf()    }} impl std::fmt::Debug for SanitizedPath {    fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {        std::fmt::Debug::fmt(&self.0, formatter)    }} impl Display for SanitizedPath {    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {        write!(f, "{}", self.0.display())    }} impl From<&SanitizedPath> for Arc<SanitizedPath> {    fn from(sanitized_path: &SanitizedPath) -> Self {        let path: Arc<Path> = sanitized_path.0.into();        // safe because `Path` and `SanitizedPath` have the same repr and Drop impl        unsafe { mem::transmute(path) }    }} impl From<&SanitizedPath> for PathBuf {    fn from(sanitized_path: &SanitizedPath) -> Self {        sanitized_path.as_path().into()    }} impl AsRef<Path> for SanitizedPath {    fn as_ref(&self) -> &Path {        &self.0    }} #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]pub enum PathStyle {    Posix,    Windows,} impl PathStyle {    #[cfg(target_os = "windows")]    pub const fn local() -> Self {        PathStyle::Windows    }     #[cfg(not(target_os = "windows"))]    pub const fn local() -> Self {        PathStyle::Posix    }     #[inline]    pub fn separator(&self) -> &'static str {        match self {            PathStyle::Posix => "/",            PathStyle::Windows => "\\",        }    }     pub fn is_windows(&self) -> bool {        *self == PathStyle::Windows    }     pub fn join(self, left: impl AsRef<Path>, right: impl AsRef<Path>) -> Option<String> {        let right = right.as_ref().to_str()?;        if is_absolute(right, self) {            return None;        }        let left = left.as_ref().to_str()?;        if left.is_empty() {            Some(right.into())        } else {            Some(format!(                "{left}{}{right}",                if left.ends_with(self.separator()) {                    ""                } else {                    self.separator()                }            ))        }    }     pub fn split(self, path_like: &str) -> (Option<&str>, &str) {        let Some(pos) = path_like.rfind(self.separator()) else {            return (None, path_like);        };        let filename_start = pos + self.separator().len();        (            Some(&path_like[..filename_start]),            &path_like[filename_start..],        )    }} #[derive(Debug, Clone)]pub struct RemotePathBuf {    style: PathStyle,    string: String,} impl RemotePathBuf {    pub fn new(string: String, style: PathStyle) -> Self {        Self { style, string }    }     pub fn from_str(path: &str, style: PathStyle) -> Self {        Self::new(path.to_string(), style)    }     pub fn path_style(&self) -> PathStyle {        self.style    }     pub fn to_proto(self) -> String {        self.string    }} impl Display for RemotePathBuf {    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {        write!(f, "{}", self.string)    }} pub fn is_absolute(path_like: &str, path_style: PathStyle) -> bool {    path_like.starts_with('/')        || path_style == PathStyle::Windows            && (path_like.starts_with('\\')                || path_like                    .chars()                    .next()                    .is_some_and(|c| c.is_ascii_alphabetic())                    && path_like[1..]                        .strip_prefix(':')                        .is_some_and(|path| path.starts_with('/') || path.starts_with('\\')))} #[derive(Debug, PartialEq)]#[non_exhaustive]pub struct NormalizeError; impl Error for NormalizeError {} impl std::fmt::Display for NormalizeError {    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {        f.write_str("parent reference `..` points outside of base directory")    }} /// Copied from stdlib where it's unstable.////// Normalize a path, including `..` without traversing the filesystem.////// Returns an error if normalization would leave leading `..` components.////// <div class="warning">////// This function always resolves `..` to the "lexical" parent./// That is "a/b/../c" will always resolve to `a/c` which can change the meaning of the path./// In particular, `a/c` and `a/b/../c` are distinct on many systems because `b` may be a symbolic link, so its parent isn't `a`.////// </div>////// [`path::absolute`](absolute) is an alternative that preserves `..`./// Or [`Path::canonicalize`] can be used to resolve any `..` by querying the filesystem.pub fn normalize_lexically(path: &Path) -> Result<PathBuf, NormalizeError> {    use std::path::Component;     let mut lexical = PathBuf::new();    let mut iter = path.components().peekable();     // Find the root, if any, and add it to the lexical path.    // Here we treat the Windows path "C:\" as a single "root" even though    // `components` splits it into two: (Prefix, RootDir).    let root = match iter.peek() {        Some(Component::ParentDir) => return Err(NormalizeError),        Some(p @ Component::RootDir) | Some(p @ Component::CurDir) => {            lexical.push(p);            iter.next();            lexical.as_os_str().len()        }        Some(Component::Prefix(prefix)) => {            lexical.push(prefix.as_os_str());            iter.next();            if let Some(p @ Component::RootDir) = iter.peek() {                lexical.push(p);                iter.next();            }            lexical.as_os_str().len()        }        None => return Ok(PathBuf::new()),        Some(Component::Normal(_)) => 0,    };     for component in iter {        match component {            Component::RootDir => unreachable!(),            Component::Prefix(_) => return Err(NormalizeError),            Component::CurDir => continue,            Component::ParentDir => {                // It's an error if ParentDir causes us to go above the "root".                if lexical.as_os_str().len() == root {                    return Err(NormalizeError);                } else {                    lexical.pop();                }            }            Component::Normal(path) => lexical.push(path),        }    }    Ok(lexical)} /// 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"(?xs)    ([^\(]+)\:(?:

Lines 1–500 of 2042.