Files
galaxy/crates/galaxy_util/src/local_or_remote_path.rs
T

204 lines
7.6 KiB
Rust

use std::path::{Path, PathBuf};
use serde::{Deserialize, Serialize};
use crate::remote_path::RemotePath;
use crate::standardized_path::StandardizedPath;
/// Uniquely identifies where a file lives — either on the local filesystem
/// or on a remote host. Used across both the buffer model and the
/// editor/view layers as the canonical file-identity type.
#[derive(Clone, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
pub enum LocalOrRemotePath {
/// File on the local filesystem.
Local(PathBuf),
/// File on a remote host, identified by host + path.
Remote(RemotePath),
}
impl LocalOrRemotePath {
/// Returns `true` if this is a `Local` location.
pub fn is_local(&self) -> bool {
matches!(self, LocalOrRemotePath::Local(_))
}
/// Returns `true` if this is a `Remote` location.
pub fn is_remote(&self) -> bool {
matches!(self, LocalOrRemotePath::Remote(_))
}
/// Returns the standardized path component of the location, regardless of where it lives.
pub fn path_component(&self) -> StandardizedPath {
match self {
LocalOrRemotePath::Local(path) => StandardizedPath::from_local_absolute_unchecked(path),
LocalOrRemotePath::Remote(remote) => remote.path.clone(),
}
}
/// Returns the file name component for display (e.g. tab titles).
pub fn display_name(&self) -> &str {
match self {
LocalOrRemotePath::Local(path) => path
.file_name()
.and_then(|n| n.to_str())
.unwrap_or_default(),
LocalOrRemotePath::Remote(remote) => remote.path.file_name().unwrap_or_default(),
}
}
/// Returns a displayable path string.
pub fn display_path(&self) -> String {
match self {
LocalOrRemotePath::Local(path) => path.to_string_lossy().to_string(),
LocalOrRemotePath::Remote(remote) => {
format!("{}", remote.path)
}
}
}
/// Returns this location's parent, preserving remote host identity.
pub fn parent(&self) -> Option<LocalOrRemotePath> {
match self {
LocalOrRemotePath::Local(path) => path
.parent()
.map(|parent| LocalOrRemotePath::Local(parent.to_path_buf())),
LocalOrRemotePath::Remote(remote) => remote.path.parent().map(|parent| {
LocalOrRemotePath::Remote(RemotePath::new(remote.host_id.clone(), parent))
}),
}
}
/// Returns the file name component, regardless of where the path lives.
pub fn file_name(&self) -> Option<&str> {
match self {
LocalOrRemotePath::Local(path) => path.file_name().and_then(|name| name.to_str()),
LocalOrRemotePath::Remote(remote) => remote.path.file_name(),
}
}
/// Returns whether this location starts with `base`.
///
/// Remote locations only compare as ancestors when they are on the same
/// host. This prevents `/repo` on one host from matching `/repo` on another.
pub fn starts_with(&self, base: &LocalOrRemotePath) -> bool {
match (self, base) {
(LocalOrRemotePath::Local(path), LocalOrRemotePath::Local(base)) => {
path.starts_with(base)
}
(LocalOrRemotePath::Remote(path), LocalOrRemotePath::Remote(base)) => {
path.host_id == base.host_id && path.path.starts_with(&base.path)
}
_ => false,
}
}
/// Returns the local path if this is a `Local` location, `None` for `Remote`.
/// Callers that only work with local files (LSP, save-to-disk, reveal-in-finder)
/// should use this to gate their behavior.
pub fn to_local_path(&self) -> Option<&Path> {
match self {
LocalOrRemotePath::Local(path) => Some(path.as_path()),
LocalOrRemotePath::Remote(_) => None,
}
}
/// Returns the remote path if this is a `Remote` location, `None` for `Local`.
/// Callers that only work with remote files should use this to gate their
/// behavior.
pub fn as_remote(&self) -> Option<&RemotePath> {
match self {
LocalOrRemotePath::Local(_) => None,
LocalOrRemotePath::Remote(remote) => Some(remote),
}
}
/// Joins a (typically repo-relative) segment onto this location, preserving
/// the host.
///
/// Accepts a `&str` rather than a `&Path` so that no caller is forced to
/// construct a local-filesystem path type when working with paths that
/// may originate from a remote host.
///
/// For `Local`, this delegates to `PathBuf::join` and yields a new local
/// path. For `Remote`, the host id is carried through and only the
/// path component is extended.
///
/// Note: if `segment` is itself absolute, the standard `Path::join`
/// replacement semantics apply (the joined result is `segment`), so
/// callers that already hold an absolute path from a wire decode will
/// get back the absolute path unchanged — modulo host preservation on
/// the remote side.
pub fn join(&self, segment: &str) -> LocalOrRemotePath {
match self {
LocalOrRemotePath::Local(path) => LocalOrRemotePath::Local(path.join(segment)),
LocalOrRemotePath::Remote(remote) => {
let joined = remote.path.join(segment);
LocalOrRemotePath::Remote(RemotePath::new(remote.host_id.clone(), joined))
}
}
}
/// If `file` shares this location's host and starts with this location's
/// path, returns the relative remainder as a `String`. Returns `None`
/// when the hosts differ or when `file` is not under this location.
///
/// Returns a `String` (rather than `PathBuf`) so that callers do not
/// implicitly assume the relative remainder lives on the local
/// filesystem — remote paths may use a different encoding than the host
/// the client is running on.
///
/// Use this when you want to compute a repo-relative path from an
/// absolute file path without silently dropping the host id (as
/// `path_component().strip_prefix(...)` would).
pub fn strip_repo_prefix(&self, file: &LocalOrRemotePath) -> Option<String> {
match (self, file) {
(LocalOrRemotePath::Local(repo), LocalOrRemotePath::Local(f)) => f
.strip_prefix(repo)
.ok()
.map(|p| p.to_string_lossy().into_owned()),
(LocalOrRemotePath::Remote(repo), LocalOrRemotePath::Remote(f))
if repo.host_id == f.host_id =>
{
f.path.strip_prefix(&repo.path).map(str::to_owned)
}
_ => None,
}
}
}
impl From<PathBuf> for LocalOrRemotePath {
fn from(path: PathBuf) -> Self {
LocalOrRemotePath::Local(path)
}
}
impl From<RemotePath> for LocalOrRemotePath {
fn from(remote: RemotePath) -> Self {
LocalOrRemotePath::Remote(remote)
}
}
impl TryFrom<LocalOrRemotePath> for PathBuf {
type Error = RemotePath;
fn try_from(location: LocalOrRemotePath) -> Result<Self, Self::Error> {
match location {
LocalOrRemotePath::Local(path) => Ok(path),
LocalOrRemotePath::Remote(remote) => Err(remote),
}
}
}
impl TryFrom<&LocalOrRemotePath> for PathBuf {
type Error = ();
fn try_from(location: &LocalOrRemotePath) -> Result<Self, Self::Error> {
match location {
LocalOrRemotePath::Local(path) => Ok(path.clone()),
LocalOrRemotePath::Remote(_) => Err(()),
}
}
}
#[cfg(test)]
#[path = "local_or_remote_path_tests.rs"]
mod tests;