You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
xplr/src/path.rs

500 lines
14 KiB
Rust

use crate::dirs;
use anyhow::{bail, Result};
use lazy_static::lazy_static;
use serde::{Deserialize, Serialize};
pub use snailquote::escape;
use std::path::{Component, Path, PathBuf};
lazy_static! {
pub static ref HOME: Option<PathBuf> = dirs::home_dir();
}
// Stolen from https://github.com/Manishearth/pathdiff/blob/master/src/lib.rs
pub fn diff<P, B>(path: P, base: B) -> Result<PathBuf>
where
P: AsRef<Path>,
B: AsRef<Path>,
{
let path = path.as_ref();
let base = base.as_ref();
if path.is_absolute() != base.is_absolute() {
if path.is_absolute() {
Ok(PathBuf::from(path))
} else {
let path = path.to_string_lossy();
bail!("{path}: is not absolute")
}
} else {
let mut ita = path.components();
let mut itb = base.components();
let mut comps: Vec<Component> = vec![];
loop {
match (ita.next(), itb.next()) {
(None, None) => break,
(Some(a), None) => {
comps.push(a);
comps.extend(ita.by_ref());
break;
}
(None, _) => comps.push(Component::ParentDir),
(Some(a), Some(b)) if comps.is_empty() && a == b => (),
(Some(a), Some(Component::CurDir)) => comps.push(a),
(Some(_), Some(Component::ParentDir)) => {
let path = path.to_string_lossy();
let base = base.to_string_lossy();
bail!("{base} is not a parent of {path}")
}
(Some(a), Some(_)) => {
comps.push(Component::ParentDir);
for _ in itb {
comps.push(Component::ParentDir);
}
comps.push(a);
comps.extend(ita.by_ref());
break;
}
}
}
Ok(comps.iter().map(|c| c.as_os_str()).collect())
}
}
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
pub struct RelativityConfig<B: AsRef<Path>> {
base: Option<B>,
with_prefix_dots: Option<bool>,
without_suffix_dots: Option<bool>,
}
impl<B: AsRef<Path>> RelativityConfig<B> {
pub fn with_base(mut self, base: B) -> Self {
self.base = Some(base);
self
}
pub fn with_prefix_dots(mut self) -> Self {
self.with_prefix_dots = Some(true);
self
}
pub fn without_suffix_dots(mut self) -> Self {
self.without_suffix_dots = Some(true);
self
}
}
pub fn relative_to<P, B>(
path: P,
config: Option<&RelativityConfig<B>>,
) -> Result<PathBuf>
where
P: AsRef<Path>,
B: AsRef<Path>,
{
let path = path.as_ref();
let base = match config.and_then(|c| c.base.as_ref()) {
Some(base) => PathBuf::from(base.as_ref()),
None => std::env::current_dir()?,
};
let diff = diff(path, base)?;
let relative = if diff.to_str() == Some("") {
".".into()
} else {
diff
};
let relative = if config.and_then(|c| c.with_prefix_dots).unwrap_or(false)
&& !relative.starts_with(".")
&& !relative.starts_with("..")
{
PathBuf::from(".").join(relative)
} else {
relative
};
let relative = if !config.and_then(|c| c.without_suffix_dots).unwrap_or(false) {
relative
} else if relative.ends_with(".") {
match (path.parent(), path.file_name()) {
(Some(_), Some(name)) => PathBuf::from("..").join(name),
(_, _) => relative,
}
} else if relative.ends_with("..") {
match (path.parent(), path.file_name()) {
(Some(parent), Some(name)) => {
if parent.parent().is_some() {
relative.join("..").join(name)
} else {
// always prefer absolute path if it's a child of the root directory
// to guarantee that the basename is included
path.into()
}
}
(_, _) => relative,
}
} else {
relative
};
Ok(relative)
}
pub fn shorten<P, B>(path: P, config: Option<&RelativityConfig<B>>) -> Result<String>
where
P: AsRef<Path>,
B: AsRef<Path>,
{
let path = path.as_ref();
let pathstring = path.to_string_lossy().to_string();
let relative = relative_to(path, config)?;
let relative = relative.to_string_lossy().to_string();
let fromhome = HOME
.as_ref()
.and_then(|h| {
path.strip_prefix(h).ok().map(|p| {
if p.to_str() == Some("") {
"~".into()
} else {
PathBuf::from("~").join(p).to_string_lossy().to_string()
}
})
})
.unwrap_or(pathstring);
if relative.len() < fromhome.len() {
Ok(relative)
} else {
Ok(fromhome)
}
}
#[cfg(test)]
mod tests {
use super::*;
type Config<'a> = Option<&'a RelativityConfig<String>>;
const NONE: Config = Config::None;
fn default<'a>() -> RelativityConfig<&'a str> {
Default::default()
}
#[test]
fn test_relative_to_pwd() {
let path = std::env::current_dir().unwrap();
let relative = relative_to(&path, NONE).unwrap();
assert_eq!(relative, PathBuf::from("."));
let relative = relative_to(&path, Some(&default().with_prefix_dots())).unwrap();
assert_eq!(relative, PathBuf::from("."));
let relative =
relative_to(&path, Some(&default().without_suffix_dots())).unwrap();
assert_eq!(
relative,
PathBuf::from("..").join(path.file_name().unwrap())
);
let relative = relative_to(
&path,
Some(&default().with_prefix_dots().without_suffix_dots()),
)
.unwrap();
assert_eq!(
relative,
PathBuf::from("..").join(path.file_name().unwrap())
);
}
#[test]
fn test_relative_to_parent() {
let path = std::env::current_dir().unwrap().join("docs");
let parent = path.parent().unwrap();
let base = default().with_base(path.to_str().unwrap());
let relative = relative_to(parent, Some(&base)).unwrap();
assert_eq!(relative, PathBuf::from(".."));
let relative =
relative_to(parent, Some(&base.clone().with_prefix_dots())).unwrap();
assert_eq!(relative, PathBuf::from(".."));
let relative =
relative_to(parent, Some(&base.clone().without_suffix_dots())).unwrap();
assert_eq!(
relative,
PathBuf::from("../..").join(parent.file_name().unwrap())
);
let relative = relative_to(
parent,
Some(&base.clone().with_prefix_dots().without_suffix_dots()),
)
.unwrap();
assert_eq!(
relative,
PathBuf::from("../..").join(parent.file_name().unwrap())
);
}
#[test]
fn test_relative_to_file() {
let path = std::env::current_dir().unwrap().join("foo").join("bar");
let relative = relative_to(&path, NONE).unwrap();
assert_eq!(relative, PathBuf::from("foo/bar"));
let relative = relative_to(&path, Some(&default().with_prefix_dots())).unwrap();
assert_eq!(relative, PathBuf::from("./foo/bar"));
let relative = relative_to(
&path,
Some(&default().with_prefix_dots().without_suffix_dots()),
)
.unwrap();
assert_eq!(relative, PathBuf::from("./foo/bar"));
}
#[test]
fn test_relative_to_root() {
let relative = relative_to("/foo", Some(&default().with_base("/"))).unwrap();
assert_eq!(relative, PathBuf::from("foo"));
let relative = relative_to(
"/foo",
Some(
&default()
.with_base("/")
.with_prefix_dots()
.without_suffix_dots(),
),
)
.unwrap();
assert_eq!(relative, PathBuf::from("./foo"));
let relative = relative_to("/", Some(&default().with_base("/"))).unwrap();
assert_eq!(relative, PathBuf::from("."));
let relative = relative_to(
"/",
Some(
&default()
.with_base("/")
.with_prefix_dots()
.without_suffix_dots(),
),
)
.unwrap();
assert_eq!(relative, PathBuf::from("."));
let relative = relative_to("/", Some(&default().with_base("/foo"))).unwrap();
assert_eq!(relative, PathBuf::from(".."));
let relative = relative_to(
"/",
Some(
&default()
.with_base("/foo")
.with_prefix_dots()
.without_suffix_dots(),
),
)
.unwrap();
assert_eq!(relative, PathBuf::from(".."));
}
#[test]
fn test_relative_to_base() {
let path = "/some/directory";
let base = "/another/foo/bar";
let relative = relative_to(path, Some(&default().with_base(base))).unwrap();
assert_eq!(relative, PathBuf::from("../../../some/directory"));
let relative = relative_to(
path,
Some(
&default()
.with_base(base)
.with_prefix_dots()
.without_suffix_dots(),
),
)
.unwrap();
assert_eq!(relative, PathBuf::from("../../../some/directory"));
}
#[test]
fn test_shorten_home() {
let path = HOME.as_ref().unwrap();
let res = shorten(path, NONE).unwrap();
assert_eq!(res, "~");
let res = shorten(
path,
Some(&default().with_prefix_dots().without_suffix_dots()),
)
.unwrap();
assert_eq!(res, "~");
let res = shorten(
path,
Some(&default().with_prefix_dots().without_suffix_dots()),
)
.unwrap();
assert_eq!(res, "~");
let res = shorten(path.join("foo"), NONE).unwrap();
assert_eq!(res, "~/foo");
let res = shorten(
path.join("foo"),
Some(&default().with_prefix_dots().without_suffix_dots()),
)
.unwrap();
assert_eq!(res, "~/foo");
let res = shorten(format!("{}foo", path.to_string_lossy()), NONE).unwrap();
assert_ne!(res, "~/foo");
assert_eq!(res, format!("{}foo", path.to_string_lossy()));
}
#[test]
fn test_shorten_base() {
let path = "/present/working/directory";
let base = "/present/foo/bar";
let res = shorten(path, Some(&default().with_base(base))).unwrap();
assert_eq!(res, "../../working/directory");
let res = shorten(
path,
Some(
&default()
.with_base(base)
.with_prefix_dots()
.without_suffix_dots(),
),
)
.unwrap();
assert_eq!(res, "../../working/directory");
}
#[test]
fn test_shorten_pwd() {
let path = "/present/working/directory";
let res = shorten(path, Some(&default().with_base(path))).unwrap();
assert_eq!(res, ".");
let res = shorten(
path,
Some(
&default()
.with_base(path)
.with_prefix_dots()
.without_suffix_dots(),
),
)
.unwrap();
assert_eq!(res, "../directory");
}
#[test]
fn test_shorten_parent() {
let path = "/present/working";
let base = "/present/working/directory";
let res = shorten(path, Some(&default().with_base(base))).unwrap();
assert_eq!(res, "..");
let res = shorten(
path,
Some(
&default()
.with_base(base)
.with_prefix_dots()
.without_suffix_dots(),
),
)
.unwrap();
assert_eq!(res, "../../working");
}
#[test]
fn test_shorten_root() {
let res = shorten("/", Some(&default().with_base("/"))).unwrap();
assert_eq!(res, "/");
let res = shorten(
"/",
Some(
&default()
.with_base("/")
.with_prefix_dots()
.without_suffix_dots(),
),
)
.unwrap();
assert_eq!(res, "/");
let res = shorten("/foo", Some(&default().with_base("/"))).unwrap();
assert_eq!(res, "foo");
let res = shorten(
"/foo",
Some(
&default()
.with_base("/")
.with_prefix_dots()
.without_suffix_dots(),
),
)
.unwrap();
assert_eq!(res, "/foo");
let res = shorten(
"/",
Some(
&default()
.with_base("/foo")
.with_prefix_dots()
.without_suffix_dots(),
),
)
.unwrap();
assert_eq!(res, "/");
}
#[test]
fn test_path_escape() {
let text = "foo".to_string();
assert_eq!(escape(&text), "foo");
let text = "foo bar".to_string();
assert_eq!(escape(&text), "'foo bar'");
let text = "foo\nbar".to_string();
assert_eq!(escape(&text), "\"foo\\nbar\"");
let text = "foo$bar".to_string();
assert_eq!(escape(&text), "'foo$bar'");
let text = "foo'$\n'bar".to_string();
assert_eq!(escape(&text), "\"foo'\\$\\n'bar\"");
let text = "a'b\"c".to_string();
assert_eq!(escape(&text), "\"a'b\\\"c\"");
}
}