Skip to main content

aether_lspd/
uri.rs

1//! URI↔path conversion utilities for LSP file URIs.
2
3use lsp_types::Uri;
4use std::path::{Path, PathBuf};
5
6#[derive(Debug, thiserror::Error)]
7#[error("Invalid path '{}': {reason}", path.display())]
8pub struct UriError {
9    path: PathBuf,
10    reason: String,
11}
12
13/// Convert a file path to an LSP `file://` URI.
14///
15/// Relative paths are resolved against the current working directory.
16pub fn path_to_uri(path: &Path) -> Result<Uri, UriError> {
17    let absolute =
18        if path.is_absolute() { path.to_path_buf() } else { std::env::current_dir().unwrap_or_default().join(path) };
19    // Canonicalize to resolve symlinks (e.g. /var → /private/var on macOS).
20    // This ensures URIs match what language servers like rust-analyzer produce.
21    let absolute = absolute.canonicalize().unwrap_or(absolute);
22
23    #[cfg(windows)]
24    let uri_str = {
25        let path_str = absolute.to_string_lossy().replace('\\', "/");
26        format!("file:///{}", path_str)
27    };
28
29    #[cfg(not(windows))]
30    let uri_str = format!("file://{}", absolute.display());
31
32    uri_str.parse::<Uri>().map_err(|e| UriError { path: absolute, reason: e.to_string() })
33}
34
35/// Convert an LSP `file://` URI to a file path string.
36pub fn uri_to_path(uri: &Uri) -> String {
37    let uri_str = uri.as_str();
38    if let Some(path) = uri_str.strip_prefix("file://") {
39        // Handle Windows paths (file:///C:/...)
40        if path.starts_with('/') && path.len() > 2 && path.chars().nth(2) == Some(':') {
41            path[1..].to_string()
42        } else {
43            path.to_string()
44        }
45    } else {
46        uri_str.to_string()
47    }
48}
49
50#[cfg(test)]
51mod tests {
52    use super::*;
53    use std::path::Path;
54
55    #[test]
56    fn test_path_to_uri_absolute() {
57        let uri = path_to_uri(Path::new("/src/main.rs")).unwrap();
58        assert_eq!(uri.as_str(), "file:///src/main.rs");
59    }
60
61    #[test]
62    fn test_uri_to_path_basic() {
63        let uri: Uri = "file:///src/main.rs".parse().unwrap();
64        assert_eq!(uri_to_path(&uri), "/src/main.rs");
65    }
66
67    #[test]
68    fn test_roundtrip() {
69        let path = Path::new("/home/user/project/src/lib.rs");
70        let uri = path_to_uri(path).unwrap();
71        let back = uri_to_path(&uri);
72        assert_eq!(back, path.to_str().unwrap());
73    }
74}