tinymist_query/
lsp_typst_boundary.rs

1//! Conversions between Typst and LSP types and representations
2
3use tinymist_std::path::PathClean;
4use tinymist_world::vfs::PathResolution;
5
6use crate::prelude::*;
7
8/// An LSP Position encoded by [`PositionEncoding`].
9pub use tinymist_analysis::location::LspPosition;
10/// An LSP range encoded by [`PositionEncoding`].
11pub use tinymist_analysis::location::LspRange;
12
13pub use tinymist_analysis::location::*;
14
15const UNTITLED_ROOT: &str = "/untitled";
16static EMPTY_URL: LazyLock<Url> = LazyLock::new(|| Url::parse("file://").unwrap());
17
18/// Convert a path to a URL.
19pub fn untitled_url(path: &Path) -> anyhow::Result<Url> {
20    Ok(Url::parse(&format!("untitled:{}", path.display()))?)
21}
22
23/// Convert a path to a URL.
24pub fn path_to_url(path: &Path) -> anyhow::Result<Url> {
25    if let Ok(untitled) = path.strip_prefix(UNTITLED_ROOT) {
26        // rust-url will panic on converting an empty path.
27        if untitled == Path::new("nEoViM-BuG") {
28            return Ok(EMPTY_URL.clone());
29        }
30
31        return untitled_url(untitled);
32    }
33
34    url_from_file_path(path)
35}
36
37/// Convert a path resolution to a URL.
38pub fn path_res_to_url(path: PathResolution) -> anyhow::Result<Url> {
39    match path {
40        PathResolution::Rootless(path) => untitled_url(path.as_rooted_path()),
41        PathResolution::Resolved(path) => path_to_url(&path),
42    }
43}
44
45/// Convert a URL to a path.
46pub fn url_to_path(uri: &Url) -> PathBuf {
47    if uri.scheme() == "file" {
48        // typst converts an empty path to `Path::new("/")`, which is undesirable.
49        if !uri.has_host() && uri.path() == "/" {
50            return PathBuf::from("/untitled/nEoViM-BuG");
51        }
52
53        return url_to_file_path(uri);
54    }
55
56    if uri.scheme() == "untitled" {
57        let mut bytes = UNTITLED_ROOT.as_bytes().to_vec();
58
59        // This is rust-url's path_segments, but vscode's untitle doesn't like it.
60        let path = uri.path();
61        let segs = path.strip_prefix('/').unwrap_or(path).split('/');
62        for segment in segs {
63            bytes.push(b'/');
64            bytes.extend(percent_encoding::percent_decode(segment.as_bytes()));
65        }
66
67        return Path::new(String::from_utf8_lossy(&bytes).as_ref()).clean();
68    }
69
70    url_to_file_path(uri)
71}
72
73#[cfg(not(target_arch = "wasm32"))]
74fn url_from_file_path(path: &Path) -> anyhow::Result<Url> {
75    Url::from_file_path(path).or_else(|never| {
76        let _: () = never;
77
78        anyhow::bail!("could not convert path to URI: path: {path:?}",)
79    })
80}
81
82#[cfg(target_arch = "wasm32")]
83fn url_from_file_path(path: &Path) -> anyhow::Result<Url> {
84    // In WASM, create a simple file:// URL
85    let path_str = path.to_string_lossy();
86    let url_str = if path_str.starts_with('/') {
87        format!("file://{}", path_str)
88    } else {
89        format!("file:///{}", path_str)
90    };
91    Url::parse(&url_str).map_err(|e| anyhow::anyhow!("could not convert path to URI: {}", e))
92}
93
94#[cfg(not(target_arch = "wasm32"))]
95fn url_to_file_path(uri: &Url) -> PathBuf {
96    uri.to_file_path()
97        .unwrap_or_else(|_| panic!("could not convert URI to path: URI: {uri:?}",))
98}
99
100#[cfg(target_arch = "wasm32")]
101fn url_to_file_path(uri: &Url) -> PathBuf {
102    // In WASM, manually parse the URL path
103    PathBuf::from(uri.path())
104}
105
106#[cfg(test)]
107mod test {
108    use super::*;
109
110    #[test]
111    fn test_untitled() {
112        let path = Path::new("/untitled/test");
113        let uri = path_to_url(path).unwrap();
114        assert_eq!(uri.scheme(), "untitled");
115        assert_eq!(uri.path(), "test");
116
117        let path = url_to_path(&uri);
118        assert_eq!(path, Path::new("/untitled/test").clean());
119    }
120
121    #[test]
122    fn unnamed_buffer() {
123        // https://github.com/neovim/nvim-lspconfig/pull/2226
124        let uri = EMPTY_URL.clone();
125        let path = url_to_path(&uri);
126        assert_eq!(path, Path::new("/untitled/nEoViM-BuG"));
127
128        let uri2 = path_to_url(&path).unwrap();
129        assert_eq!(EMPTY_URL.clone(), uri2);
130    }
131}