diff options
Diffstat (limited to 'cli/lsp/urls.rs')
-rw-r--r-- | cli/lsp/urls.rs | 174 |
1 files changed, 174 insertions, 0 deletions
diff --git a/cli/lsp/urls.rs b/cli/lsp/urls.rs new file mode 100644 index 000000000..a4af7d7b9 --- /dev/null +++ b/cli/lsp/urls.rs @@ -0,0 +1,174 @@ +// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license. + +use crate::file_fetcher::map_content_type; +use crate::media_type::MediaType; + +use deno_core::error::AnyError; +use deno_core::url::Url; +use deno_core::ModuleSpecifier; +use std::collections::HashMap; + +/// This is a partial guess as how URLs get encoded from the LSP. We want to +/// encode URLs sent to the LSP in the same way that they would be encoded back +/// so that we have the same return encoding. +const LSP_ENCODING: &percent_encoding::AsciiSet = &percent_encoding::CONTROLS + .add(b':') + .add(b';') + .add(b'=') + .add(b'@') + .add(b'[') + .add(b'\\') + .add(b']') + .add(b'^') + .add(b'|'); + +fn hash_data_specifier(specifier: &ModuleSpecifier) -> String { + let mut file_name_str = specifier.path().to_string(); + if let Some(query) = specifier.query() { + file_name_str.push('?'); + file_name_str.push_str(query); + } + crate::checksum::gen(&[file_name_str.as_bytes()]) +} + +fn data_url_media_type(specifier: &ModuleSpecifier) -> MediaType { + let path = specifier.path(); + let mut parts = path.splitn(2, ','); + let media_type_part = + percent_encoding::percent_decode_str(parts.next().unwrap()) + .decode_utf8_lossy(); + let (media_type, _) = + map_content_type(specifier, Some(media_type_part.into())); + media_type +} + +/// A bi-directional map of URLs sent to the LSP client and internal module +/// specifiers. We need to map internal specifiers into `deno:` schema URLs +/// to allow the Deno language server to manage these as virtual documents. +#[derive(Debug, Default)] +pub struct LspUrlMap { + specifier_to_url: HashMap<ModuleSpecifier, Url>, + url_to_specifier: HashMap<Url, ModuleSpecifier>, +} + +impl LspUrlMap { + fn put(&mut self, specifier: ModuleSpecifier, url: Url) { + self.specifier_to_url.insert(specifier.clone(), url.clone()); + self.url_to_specifier.insert(url, specifier); + } + + fn get_url(&self, specifier: &ModuleSpecifier) -> Option<&Url> { + self.specifier_to_url.get(specifier) + } + + fn get_specifier(&self, url: &Url) -> Option<&ModuleSpecifier> { + self.url_to_specifier.get(url) + } + + /// Normalize a specifier that is used internally within Deno (or tsc) to a + /// URL that can be handled as a "virtual" document by an LSP client. + pub fn normalize_specifier( + &mut self, + specifier: &ModuleSpecifier, + ) -> Result<Url, AnyError> { + if let Some(url) = self.get_url(specifier) { + Ok(url.clone()) + } else { + let url = if specifier.scheme() == "file" { + specifier.clone() + } else { + let specifier_str = if specifier.scheme() == "data" { + let media_type = data_url_media_type(specifier); + let extension = if media_type == MediaType::Unknown { + "" + } else { + media_type.as_ts_extension() + }; + format!( + "deno:/{}/data_url{}", + hash_data_specifier(specifier), + extension + ) + } else { + let path = specifier.as_str().replacen("://", "/", 1); + let path = percent_encoding::utf8_percent_encode(&path, LSP_ENCODING); + format!("deno:/{}", path) + }; + let url = Url::parse(&specifier_str)?; + self.put(specifier.clone(), url.clone()); + url + }; + Ok(url) + } + } + + /// Normalize URLs from the client, where "virtual" `deno:///` URLs are + /// converted into proper module specifiers. + pub fn normalize_url(&self, url: &Url) -> ModuleSpecifier { + if let Some(specifier) = self.get_specifier(url) { + specifier.clone() + } else { + url.clone() + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use deno_core::resolve_url; + + #[test] + fn test_hash_data_specifier() { + let fixture = resolve_url("data:application/typescript;base64,ZXhwb3J0IGNvbnN0IGEgPSAiYSI7CgpleHBvcnQgZW51bSBBIHsKICBBLAogIEIsCiAgQywKfQo=").unwrap(); + let actual = hash_data_specifier(&fixture); + assert_eq!( + actual, + "c21c7fc382b2b0553dc0864aa81a3acacfb7b3d1285ab5ae76da6abec213fb37" + ); + } + + #[test] + fn test_data_url_media_type() { + let fixture = resolve_url("data:application/typescript;base64,ZXhwb3J0IGNvbnN0IGEgPSAiYSI7CgpleHBvcnQgZW51bSBBIHsKICBBLAogIEIsCiAgQywKfQo=").unwrap(); + let actual = data_url_media_type(&fixture); + assert_eq!(actual, MediaType::TypeScript); + + let fixture = resolve_url("data:application/javascript;base64,ZXhwb3J0IGNvbnN0IGEgPSAiYSI7CgpleHBvcnQgZW51bSBBIHsKICBBLAogIEIsCiAgQywKfQo=").unwrap(); + let actual = data_url_media_type(&fixture); + assert_eq!(actual, MediaType::JavaScript); + + let fixture = resolve_url("data:text/plain;base64,ZXhwb3J0IGNvbnN0IGEgPSAiYSI7CgpleHBvcnQgZW51bSBBIHsKICBBLAogIEIsCiAgQywKfQo=").unwrap(); + let actual = data_url_media_type(&fixture); + assert_eq!(actual, MediaType::Unknown); + } + + #[test] + fn test_lsp_url_map() { + let mut map = LspUrlMap::default(); + let fixture = resolve_url("https://deno.land/x/pkg@1.0.0/mod.ts").unwrap(); + let actual_url = map + .normalize_specifier(&fixture) + .expect("could not handle specifier"); + let expected_url = + Url::parse("deno:/https/deno.land/x/pkg%401.0.0/mod.ts").unwrap(); + assert_eq!(actual_url, expected_url); + + let actual_specifier = map.normalize_url(&actual_url); + assert_eq!(actual_specifier, fixture); + } + + #[test] + fn test_lsp_url_map_data() { + let mut map = LspUrlMap::default(); + let fixture = resolve_url("data:application/typescript;base64,ZXhwb3J0IGNvbnN0IGEgPSAiYSI7CgpleHBvcnQgZW51bSBBIHsKICBBLAogIEIsCiAgQywKfQo=").unwrap(); + let actual_url = map + .normalize_specifier(&fixture) + .expect("could not handle specifier"); + let expected_url = Url::parse("deno:/c21c7fc382b2b0553dc0864aa81a3acacfb7b3d1285ab5ae76da6abec213fb37/data_url.ts").unwrap(); + assert_eq!(actual_url, expected_url); + + let actual_specifier = map.normalize_url(&actual_url); + assert_eq!(actual_specifier, fixture); + } +} |