use std::borrow::Cow;
use std::path::{Path, PathBuf};
use std::str::FromStr;
use lsp_types::Uri;
use percent_encoding::AsciiSet;
const ASCII_SET: AsciiSet = percent_encoding::NON_ALPHANUMERIC
.remove(b'-')
.remove(b'.')
.remove(b'_')
.remove(b'~')
.remove(b'/');
pub trait UriExt {
fn to_file_path(&self) -> Option<Cow<'_, Path>>;
fn from_file_path<A: AsRef<Path>>(path: A) -> Option<Uri>;
}
impl UriExt for Uri {
fn to_file_path(&self) -> Option<Cow<'_, Path>> {
let path_str = self.path().as_estr().decode().into_string_lossy();
if path_str.is_empty() {
return None;
}
let path = match path_str {
Cow::Borrowed(ref_) => Cow::Borrowed(Path::new(ref_)),
Cow::Owned(owned) => Cow::Owned(PathBuf::from(owned)),
};
if cfg!(windows) {
let auth_host = self
.authority()
.map(|auth| auth.host().as_str())
.unwrap_or_default();
if auth_host.is_empty() {
let host = path.to_string_lossy();
let host = host.get(1..)?;
return Some(Cow::Owned(PathBuf::from(host)));
}
Some(Cow::Owned(
Path::new(&format!("{auth_host}:"))
.components()
.chain(path.components())
.collect(),
))
} else {
Some(path)
}
}
fn from_file_path<A: AsRef<Path>>(path: A) -> Option<Uri> {
let path = path.as_ref();
let fragment = if path.is_absolute() {
Cow::Borrowed(path)
} else {
match strict_canonicalize(path) {
Ok(path) => Cow::Owned(path),
Err(_) => return None,
}
};
#[cfg(windows)]
let raw_uri = {
format!(
"file:///{}",
percent_encoding::utf8_percent_encode(
&capitalize_drive_letter(&fragment.to_string_lossy().replace('\\', "/")),
&ASCII_SET
)
)
};
#[cfg(not(windows))]
let raw_uri = {
format!(
"file://{}",
percent_encoding::utf8_percent_encode(&fragment.to_string_lossy(), &ASCII_SET)
)
};
Uri::from_str(&raw_uri).ok()
}
}
fn strict_canonicalize<P: AsRef<Path>>(path: P) -> std::io::Result<PathBuf> {
use std::io;
fn impl_(path: PathBuf) -> std::io::Result<PathBuf> {
let head = path
.components()
.next()
.ok_or_else(|| io::Error::other("empty path"))?;
let disk_;
let head = if let std::path::Component::Prefix(prefix) = head {
if let std::path::Prefix::VerbatimDisk(disk) = prefix.kind() {
disk_ = format!("{}:", disk as char);
Path::new(&disk_)
.components()
.next()
.ok_or_else(|| io::Error::other("failed to parse disk component"))?
} else {
head
}
} else {
head
};
Ok(std::iter::once(head)
.chain(path.components().skip(1))
.collect())
}
let canon = std::fs::canonicalize(path)?;
impl_(canon)
}
#[cfg(windows)]
fn capitalize_drive_letter(path: &str) -> String {
if path.len() >= 2 && path.chars().nth(1) == Some(':') {
let mut chars = path.chars();
let drive_letter = chars.next().unwrap().to_ascii_uppercase();
let rest: String = chars.collect();
format!("{drive_letter}{rest}")
} else {
path.to_string()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(not(windows))]
#[test]
fn round_trips_absolute_unix_path() {
let path = Path::new("/tmp/panache/doc.qmd");
let uri = Uri::from_file_path(path).expect("uri");
assert_eq!(uri.as_str(), "file:///tmp/panache/doc.qmd");
let back = uri.to_file_path().expect("path");
assert_eq!(back.as_ref(), path);
}
#[cfg(not(windows))]
#[test]
fn percent_encodes_spaces() {
let path = Path::new("/tmp/my notes/a b.qmd");
let uri = Uri::from_file_path(path).expect("uri");
assert_eq!(uri.as_str(), "file:///tmp/my%20notes/a%20b.qmd");
let back = uri.to_file_path().expect("path");
assert_eq!(back.as_ref(), path);
}
#[test]
fn empty_path_is_none() {
let uri = Uri::from_str("file://").or_else(|_| Uri::from_str("http://example.com"));
if let Ok(uri) = uri {
let _ = uri.to_file_path();
}
}
}