use std::path::{Path, PathBuf};
use std::str::FromStr;
use lsp_types::Uri;
pub(crate) fn to_path(uri: &Uri) -> Option<PathBuf> {
let scheme = uri.scheme()?;
if !scheme.as_str().eq_ignore_ascii_case("file") {
return None;
}
let decoded = uri
.path()
.as_estr()
.decode()
.into_string_lossy()
.into_owned();
Some(from_uri_path(&decoded))
}
#[cfg(windows)]
fn from_uri_path(p: &str) -> PathBuf {
let bytes = p.as_bytes();
let has_drive =
bytes.len() >= 3 && bytes[0] == b'/' && bytes[1].is_ascii_alphabetic() && bytes[2] == b':';
let trimmed = if has_drive { &p[1..] } else { p };
PathBuf::from(trimmed.replace('/', "\\"))
}
#[cfg(not(windows))]
fn from_uri_path(p: &str) -> PathBuf {
PathBuf::from(p)
}
const NON_FILE_ROOT: &str = "fatou-non-file-uri";
pub(crate) fn to_path_or_synthetic(uri: &Uri) -> PathBuf {
to_path(uri).unwrap_or_else(|| {
use std::fmt::Write;
let mut name = String::new();
for &byte in uri.as_str().as_bytes() {
match byte {
b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' => name.push(byte as char),
_ => {
let _ = write!(name, "%{byte:02X}");
}
}
}
name.push_str(".jl");
synthetic_dir().join(name)
})
}
fn synthetic_dir() -> PathBuf {
PathBuf::from(std::path::MAIN_SEPARATOR_STR).join(NON_FILE_ROOT)
}
pub(crate) fn is_synthetic(path: &Path) -> bool {
path.parent() == Some(synthetic_dir().as_path())
}
pub(crate) fn from_path(path: &Path) -> Option<Uri> {
let text = path.to_str()?;
let mut encoded = String::from("file://");
#[cfg(windows)]
let text = {
encoded.push('/');
text.replace('\\', "/")
};
#[cfg(windows)]
let text = text.as_str();
for &byte in text.as_bytes() {
match byte {
b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'.' | b'_' | b'~' | b'/' | b':' => {
encoded.push(byte as char);
}
_ => encoded.push_str(&format!("%{byte:02X}")),
}
}
Uri::from_str(&encoded).ok()
}
#[cfg(test)]
mod tests {
use super::*;
use std::str::FromStr;
#[test]
#[cfg(not(windows))]
fn file_uri_decodes_to_path() {
let uri = Uri::from_str("file:///work/some%20dir/a.jl").unwrap();
assert_eq!(to_path(&uri), Some(PathBuf::from("/work/some dir/a.jl")));
}
#[test]
#[cfg(windows)]
fn drive_letter_uri_decodes_to_path() {
let uri = Uri::from_str("file:///C:/work/a%20b.jl").unwrap();
assert_eq!(to_path(&uri), Some(PathBuf::from("C:\\work\\a b.jl")));
let uri = Uri::from_str("file:///c%3A/work/a.jl").unwrap();
assert_eq!(to_path(&uri), Some(PathBuf::from("c:\\work\\a.jl")));
}
#[test]
#[cfg(windows)]
fn driveless_uri_stays_rooted() {
let uri = Uri::from_str("file:///work/a.jl").unwrap();
assert_eq!(to_path(&uri), Some(PathBuf::from("\\work\\a.jl")));
}
#[test]
fn non_file_uri_has_no_path() {
let uri = Uri::from_str("untitled:Untitled-1").unwrap();
assert_eq!(to_path(&uri), None);
}
#[test]
fn non_file_uris_map_to_distinct_rooted_paths() {
use crate::incremental::normalize_path;
let path = |text: &str| to_path_or_synthetic(&Uri::from_str(text).unwrap());
let first = path("untitled:Untitled-1");
assert_ne!(first, path("untitled:Untitled-2"));
assert_ne!(first, path("vscode-notebook-cell:Untitled-1"));
assert_eq!(first, path("untitled:Untitled-1"), "stable per URI");
assert!(first.has_root(), "{first:?} should be rooted");
assert_eq!(first.components().count(), 3, "root, dir, file: {first:?}");
assert_eq!(first.extension().and_then(|e| e.to_str()), Some("jl"));
let normalized = normalize_path(&first);
assert!(
normalized.ends_with(
first
.strip_prefix(first.components().next().unwrap())
.unwrap()
),
"normalization should keep the root's contents: {normalized:?}"
);
assert!(
!normalized.starts_with(std::env::current_dir().expect("a working directory")),
"a synthetic path must not land in the workspace: {normalized:?}"
);
#[cfg(not(windows))]
assert_eq!(path("file:///work/a.jl"), PathBuf::from("/work/a.jl"));
}
#[test]
fn only_the_minted_shape_counts_as_synthetic() {
assert!(is_synthetic(&to_path_or_synthetic(
&Uri::from_str("untitled:Untitled-1").unwrap()
)));
assert!(!is_synthetic(Path::new("relative")));
assert!(!is_synthetic(
&PathBuf::from(NON_FILE_ROOT).join("notes.jl")
));
#[cfg(not(windows))]
{
assert!(!is_synthetic(Path::new("/work/a.jl")));
assert!(!is_synthetic(Path::new(
"/work/fatou-non-file-uri-notes/a.jl"
)));
assert!(!is_synthetic(Path::new("/work/fatou-non-file-uri/a.jl")));
assert!(!is_synthetic(Path::new(
"/work/proj/fatou-non-file-uri/a.jl"
)));
assert!(!is_synthetic(Path::new("/fatou-non-file-uri/sub/a.jl")));
}
}
#[test]
#[cfg(not(windows))]
fn path_round_trips_through_uri() {
let path = PathBuf::from("/home/x/.julia/packages/A b/src/A b.jl");
let uri = from_path(&path).expect("file uri");
assert!(uri.as_str().contains("%20"), "space should be encoded");
assert_eq!(to_path(&uri), Some(path));
}
}