1use lsp_types::Uri;
4use std::path::{Path, PathBuf};
5use url::Url;
6
7#[derive(Debug, thiserror::Error)]
8#[error("Invalid path '{}': {reason}", path.display())]
9pub struct UriError {
10 path: PathBuf,
11 reason: String,
12}
13
14pub fn path_to_uri(path: &Path) -> Result<Uri, UriError> {
18 let absolute =
19 if path.is_absolute() { path.to_path_buf() } else { std::env::current_dir().unwrap_or_default().join(path) };
20 let absolute = absolute.canonicalize().unwrap_or(absolute);
23
24 let url = Url::from_file_path(&absolute)
25 .map_err(|()| UriError { path: absolute.clone(), reason: "not an absolute file path".to_string() })?;
26
27 url.as_str().parse::<Uri>().map_err(|e| UriError { path: absolute, reason: e.to_string() })
28}
29
30pub fn uri_to_path(uri: &Uri) -> String {
34 let uri_str = uri.as_str();
35 Url::parse(uri_str)
36 .ok()
37 .and_then(|url| url.to_file_path().ok())
38 .map_or_else(|| uri_str.to_string(), |path| path.to_string_lossy().into_owned())
39}
40
41#[cfg(test)]
42mod tests {
43 use super::*;
44 use std::path::Path;
45
46 #[test]
47 fn test_path_to_uri_absolute() {
48 let uri = path_to_uri(Path::new("/src/main.rs")).unwrap();
49 assert_eq!(uri.as_str(), "file:///src/main.rs");
50 }
51
52 #[test]
53 fn test_uri_to_path_basic() {
54 let uri: Uri = "file:///src/main.rs".parse().unwrap();
55 assert_eq!(uri_to_path(&uri), "/src/main.rs");
56 }
57
58 #[test]
59 fn test_roundtrip() {
60 let path = Path::new("/home/user/project/src/lib.rs");
61 let uri = path_to_uri(path).unwrap();
62 let back = uri_to_path(&uri);
63 assert_eq!(back, path.to_str().unwrap());
64 }
65
66 #[test]
67 fn path_to_uri_percent_encodes_reserved_characters() {
68 let cases = [
69 ("/src/my file.rs", "/src/my%20file.rs"),
70 ("/src/a#b.rs", "/src/a%23b.rs"),
71 ("/src/a?b.rs", "/src/a%3Fb.rs"),
72 ("/src/100%.rs", "/src/100%25.rs"),
73 ("/src/café.rs", "/src/caf%C3%A9.rs"),
74 ];
75
76 for (path, expected_uri_path) in cases {
77 let uri = path_to_uri(Path::new(path)).unwrap();
78 assert_eq!(uri.as_str(), format!("file://{expected_uri_path}"), "path: {path}");
79 }
80 }
81
82 #[test]
83 fn uri_to_path_decodes_percent_encoding() {
84 let cases = [
85 ("file:///src/my%20file.rs", "/src/my file.rs"),
86 ("file:///src/a%23b.rs", "/src/a#b.rs"),
87 ("file:///src/caf%C3%A9.rs", "/src/café.rs"),
88 ];
89
90 for (uri_str, expected_path) in cases {
91 let uri: Uri = uri_str.parse().unwrap();
92 assert_eq!(uri_to_path(&uri), expected_path, "uri: {uri_str}");
93 }
94 }
95
96 #[test]
97 fn roundtrips_paths_containing_reserved_characters() {
98 let path = "/home/user/my project/a#b.rs";
99 let uri = path_to_uri(Path::new(path)).unwrap();
100 assert!(uri.as_str().contains('%'), "expected percent-encoding in {}", uri.as_str());
101 assert_eq!(uri_to_path(&uri), path);
102 }
103
104 #[test]
105 fn uri_to_path_passes_non_file_uris_through_unchanged() {
106 let uri: Uri = "https://example.com/file.rs".parse().unwrap();
107 assert_eq!(uri_to_path(&uri), "https://example.com/file.rs");
108 }
109}