diff --git a/cli/lsp/diagnostics.rs b/cli/lsp/diagnostics.rs index 18cff6ea7d..a7051ddf1b 100644 --- a/cli/lsp/diagnostics.rs +++ b/cli/lsp/diagnostics.rs @@ -122,11 +122,16 @@ impl DiagnosticsServer { pub(crate) async fn invalidate(&self, specifiers: Vec) { let mut collection = self.collection.lock().await; - for specifier in specifiers { - collection.versions.remove(&specifier); + for specifier in &specifiers { + collection.versions.remove(specifier); } } + pub(crate) async fn invalidate_all(&self) { + let mut collection = self.collection.lock().await; + collection.versions.clear(); + } + pub(crate) fn start( &mut self, language_server: Arc>, diff --git a/cli/lsp/documents.rs b/cli/lsp/documents.rs index 00a4aa1565..911e30389d 100644 --- a/cli/lsp/documents.rs +++ b/cli/lsp/documents.rs @@ -81,7 +81,7 @@ pub struct DocumentData { bytes: Option>, dependencies: Option>, dependency_ranges: Option, - language_id: LanguageId, + pub(crate) language_id: LanguageId, line_index: Option, maybe_navigation_tree: Option, specifier: ModuleSpecifier, @@ -180,7 +180,7 @@ impl DocumentData { #[derive(Debug, Clone, Default)] pub struct DocumentCache { dependents_graph: HashMap>, - pub docs: HashMap, + pub(crate) docs: HashMap, } impl DocumentCache { @@ -272,16 +272,29 @@ impl DocumentCache { &self, specifier: &ModuleSpecifier, ) -> Option> { - let doc = self.docs.get(specifier)?; - doc.dependencies.clone() + self + .docs + .get(specifier) + .map(|doc| doc.dependencies.clone()) + .flatten() + } + + pub fn get_language_id( + &self, + specifier: &ModuleSpecifier, + ) -> Option { + self.docs.get(specifier).map(|doc| doc.language_id.clone()) } pub fn get_navigation_tree( &self, specifier: &ModuleSpecifier, ) -> Option { - let doc = self.docs.get(specifier)?; - doc.maybe_navigation_tree.clone() + self + .docs + .get(specifier) + .map(|doc| doc.maybe_navigation_tree.clone()) + .flatten() } /// Determines if the specifier should be processed for diagnostics and other diff --git a/cli/lsp/language_server.rs b/cli/lsp/language_server.rs index 7f80864663..25b588ae6b 100644 --- a/cli/lsp/language_server.rs +++ b/cli/lsp/language_server.rs @@ -163,15 +163,15 @@ impl Inner { fn analyze_dependencies( &mut self, specifier: &ModuleSpecifier, + media_type: &MediaType, source: &str, ) { - let media_type = MediaType::from(specifier); if let Ok(parsed_module) = - analysis::parse_module(specifier, source, &media_type) + analysis::parse_module(specifier, source, media_type) { let (mut deps, _) = analysis::analyze_dependencies( specifier, - &media_type, + media_type, &parsed_module, &self.maybe_import_map, ); @@ -194,6 +194,24 @@ impl Inner { } } + /// Analyzes all dependencies for all documents that have been opened in the + /// editor and sets the dependencies property on the documents. + fn analyze_dependencies_all(&mut self) { + let docs: Vec<(ModuleSpecifier, String, MediaType)> = self + .documents + .docs + .iter() + .filter_map(|(s, doc)| { + let source = doc.content().ok().flatten()?; + let media_type = MediaType::from(&doc.language_id); + Some((s.clone(), source, media_type)) + }) + .collect(); + for (specifier, source, media_type) in docs { + self.analyze_dependencies(&specifier, &media_type, &source); + } + } + /// Searches assets, open documents and external sources for a line_index, /// which might be performed asynchronously, hydrating in memory caches for /// subsequent requests. @@ -445,8 +463,10 @@ impl Inner { let import_map = ImportMap::from_json(&import_map_url.to_string(), &import_map_json)?; self.maybe_import_map_uri = Some(import_map_url); - self.maybe_import_map = Some(import_map); + self.maybe_import_map = Some(import_map.clone()); + self.sources.set_import_map(Some(import_map)); } else { + self.sources.set_import_map(None); self.maybe_import_map = None; } self.performance.measure(mark); @@ -694,6 +714,7 @@ impl Inner { LanguageId::TypeScript } }; + let media_type = MediaType::from(&language_id); self.documents.open( specifier.clone(), params.text_document.version, @@ -702,7 +723,11 @@ impl Inner { ); if self.documents.is_diagnosable(&specifier) { - self.analyze_dependencies(&specifier, ¶ms.text_document.text); + self.analyze_dependencies( + &specifier, + &media_type, + ¶ms.text_document.text, + ); self .diagnostics_server .invalidate(self.documents.dependents(&specifier)) @@ -724,7 +749,10 @@ impl Inner { ) { Ok(Some(source)) => { if self.documents.is_diagnosable(&specifier) { - self.analyze_dependencies(&specifier, &source); + let media_type = MediaType::from( + &self.documents.get_language_id(&specifier).unwrap(), + ); + self.analyze_dependencies(&specifier, &media_type, &source); self .diagnostics_server .invalidate(self.documents.dependents(&specifier)) @@ -825,12 +853,14 @@ impl Inner { let mark = self .performance .mark("did_change_watched_files", Some(¶ms)); + let mut touched = false; // if the current import map has changed, we need to reload it if let Some(import_map_uri) = &self.maybe_import_map_uri { if params.changes.iter().any(|fe| *import_map_uri == fe.uri) { if let Err(err) = self.update_import_map().await { self.client.show_message(MessageType::Warning, err).await; } + touched = true; } } // if the current tsconfig has changed, we need to reload it @@ -839,6 +869,14 @@ impl Inner { if let Err(err) = self.update_tsconfig().await { self.client.show_message(MessageType::Warning, err).await; } + touched = true; + } + } + if touched { + self.analyze_dependencies_all(); + self.diagnostics_server.invalidate_all().await; + if let Err(err) = self.diagnostics_server.update() { + error!("Cannot update diagnostics: {}", err); } } self.performance.measure(mark); @@ -2392,7 +2430,9 @@ impl Inner { // invalidate some diagnostics if self.documents.contains_key(&referrer) { if let Some(source) = self.documents.content(&referrer).unwrap() { - self.analyze_dependencies(&referrer, &source); + let media_type = + MediaType::from(&self.documents.get_language_id(&referrer).unwrap()); + self.analyze_dependencies(&referrer, &media_type, &source); } self.diagnostics_server.invalidate(vec![referrer]).await; } diff --git a/cli/lsp/sources.rs b/cli/lsp/sources.rs index 1b7d6f3fa6..62c76260c5 100644 --- a/cli/lsp/sources.rs +++ b/cli/lsp/sources.rs @@ -3,6 +3,7 @@ use super::analysis; use super::text::LineIndex; use super::tsc; +use super::urls::INVALID_SPECIFIER; use crate::config_file::ConfigFile; use crate::file_fetcher::get_source_from_bytes; @@ -105,7 +106,7 @@ fn resolve_specifier( } } -#[derive(Debug, Clone, Default)] +#[derive(Debug, Clone)] struct Metadata { dependencies: Option>, length_utf16: usize, @@ -115,9 +116,27 @@ struct Metadata { maybe_warning: Option, media_type: MediaType, source: String, + specifier: ModuleSpecifier, version: String, } +impl Default for Metadata { + fn default() -> Self { + Self { + dependencies: None, + length_utf16: 0, + line_index: LineIndex::default(), + maybe_navigation_tree: None, + maybe_types: None, + maybe_warning: None, + media_type: MediaType::default(), + source: String::default(), + specifier: INVALID_SPECIFIER.clone(), + version: String::default(), + } + } +} + impl Metadata { fn new( specifier: &ModuleSpecifier, @@ -151,9 +170,28 @@ impl Metadata { maybe_warning, media_type: media_type.to_owned(), source: source.to_string(), + specifier: specifier.clone(), version: version.to_string(), } } + + fn refresh(&mut self, maybe_import_map: &Option) { + let (dependencies, maybe_types) = if let Ok(parsed_module) = + analysis::parse_module(&self.specifier, &self.source, &self.media_type) + { + let (deps, maybe_types) = analysis::analyze_dependencies( + &self.specifier, + &self.media_type, + &parsed_module, + maybe_import_map, + ); + (Some(deps), maybe_types) + } else { + (None, None) + }; + self.dependencies = dependencies; + self.maybe_types = maybe_types; + } } #[derive(Debug, Clone, Default)] @@ -239,6 +277,10 @@ impl Sources { self.0.lock().metadata.keys().cloned().collect() } + pub fn set_import_map(&self, maybe_import_map: Option) { + self.0.lock().set_import_map(maybe_import_map) + } + pub fn set_navigation_tree( &self, specifier: &ModuleSpecifier, @@ -483,6 +525,13 @@ impl Inner { } } + fn set_import_map(&mut self, maybe_import_map: Option) { + for (_, metadata) in self.metadata.iter_mut() { + metadata.refresh(&maybe_import_map); + } + self.maybe_import_map = maybe_import_map; + } + fn set_maybe_type( &mut self, specifier: &str, @@ -517,6 +566,7 @@ mod tests { use super::*; use deno_core::resolve_path; use deno_core::resolve_url; + use deno_core::serde_json::json; use std::env; use tempfile::TempDir; @@ -654,6 +704,102 @@ mod tests { assert_eq!(actual, None); } + #[test] + fn test_resolve_with_import_map() { + let (sources, location) = setup(); + let import_map_json = json!({ + "imports": { + "mylib": "https://deno.land/x/myLib/index.js" + } + }); + let import_map = ImportMap::from_json( + "https://deno.land/x/", + &import_map_json.to_string(), + ) + .unwrap(); + sources.set_import_map(Some(import_map)); + let cache = HttpCache::new(&location); + let mylib_specifier = + resolve_url("https://deno.land/x/myLib/index.js").unwrap(); + let mut mylib_headers_map = HashMap::new(); + mylib_headers_map.insert( + "content-type".to_string(), + "application/javascript".to_string(), + ); + cache + .set( + &mylib_specifier, + mylib_headers_map, + b"export const a = \"a\";\n", + ) + .unwrap(); + let referrer = resolve_url("https://deno.land/x/mod.ts").unwrap(); + cache + .set( + &referrer, + Default::default(), + b"export { a } from \"mylib\";", + ) + .unwrap(); + let actual = sources.resolve_import("mylib", &referrer); + assert_eq!(actual, Some((mylib_specifier, MediaType::JavaScript))); + } + + #[test] + fn test_update_import_map() { + let (sources, location) = setup(); + let import_map_json = json!({ + "imports": { + "otherlib": "https://deno.land/x/otherlib/index.js" + } + }); + let import_map = ImportMap::from_json( + "https://deno.land/x/", + &import_map_json.to_string(), + ) + .unwrap(); + sources.set_import_map(Some(import_map)); + let cache = HttpCache::new(&location); + let mylib_specifier = + resolve_url("https://deno.land/x/myLib/index.js").unwrap(); + let mut mylib_headers_map = HashMap::new(); + mylib_headers_map.insert( + "content-type".to_string(), + "application/javascript".to_string(), + ); + cache + .set( + &mylib_specifier, + mylib_headers_map, + b"export const a = \"a\";\n", + ) + .unwrap(); + let referrer = resolve_url("https://deno.land/x/mod.ts").unwrap(); + cache + .set( + &referrer, + Default::default(), + b"export { a } from \"mylib\";", + ) + .unwrap(); + let actual = sources.resolve_import("mylib", &referrer); + assert_eq!(actual, None); + let import_map_json = json!({ + "imports": { + "otherlib": "https://deno.land/x/otherlib/index.js", + "mylib": "https://deno.land/x/myLib/index.js" + } + }); + let import_map = ImportMap::from_json( + "https://deno.land/x/", + &import_map_json.to_string(), + ) + .unwrap(); + sources.set_import_map(Some(import_map)); + let actual = sources.resolve_import("mylib", &referrer); + assert_eq!(actual, Some((mylib_specifier, MediaType::JavaScript))); + } + #[test] fn test_sources_resolve_specifier_non_supported_schema() { let (sources, _) = setup(); diff --git a/cli/lsp/tsc.rs b/cli/lsp/tsc.rs index aed1dda9de..3f52beb6b9 100644 --- a/cli/lsp/tsc.rs +++ b/cli/lsp/tsc.rs @@ -10,6 +10,7 @@ use super::semantic_tokens::SemanticTokensBuilder; use super::semantic_tokens::TsTokenEncodingConsts; use super::text; use super::text::LineIndex; +use super::urls::INVALID_SPECIFIER; use crate::config_file::TsConfig; use crate::media_type::MediaType; @@ -577,7 +578,7 @@ impl DocumentSpan { line_index: &LineIndex, language_server: &mut language_server::Inner, ) -> Option { - let target_specifier = normalize_specifier(&self.file_name).unwrap(); + let target_specifier = normalize_specifier(&self.file_name).ok()?; let target_line_index = language_server .get_line_index(target_specifier.clone()) .await @@ -585,7 +586,7 @@ impl DocumentSpan { let target_uri = language_server .url_map .normalize_specifier(&target_specifier) - .unwrap(); + .ok()?; let (target_range, target_selection_range) = if let Some(context_span) = &self.context_span { ( @@ -778,11 +779,12 @@ impl ImplementationLocation { line_index: &LineIndex, language_server: &mut language_server::Inner, ) -> lsp::Location { - let specifier = normalize_specifier(&self.document_span.file_name).unwrap(); + let specifier = normalize_specifier(&self.document_span.file_name) + .unwrap_or_else(|_| ModuleSpecifier::parse("deno://invalid").unwrap()); let uri = language_server .url_map .normalize_specifier(&specifier) - .unwrap(); + .unwrap_or_else(|_| ModuleSpecifier::parse("deno://invalid").unwrap()); lsp::Location { uri, range: self.document_span.text_span.to_range(line_index), @@ -1107,11 +1109,12 @@ impl ReferenceEntry { line_index: &LineIndex, language_server: &mut language_server::Inner, ) -> lsp::Location { - let specifier = normalize_specifier(&self.document_span.file_name).unwrap(); + let specifier = normalize_specifier(&self.document_span.file_name) + .unwrap_or_else(|_| INVALID_SPECIFIER.clone()); let uri = language_server .url_map .normalize_specifier(&specifier) - .unwrap(); + .unwrap_or_else(|_| INVALID_SPECIFIER.clone()); lsp::Location { uri, range: self.document_span.text_span.to_range(line_index), @@ -1139,7 +1142,7 @@ impl CallHierarchyItem { language_server: &mut language_server::Inner, maybe_root_path: Option<&Path>, ) -> Option { - let target_specifier = normalize_specifier(&self.file).unwrap(); + let target_specifier = normalize_specifier(&self.file).ok()?; let target_line_index = language_server .get_line_index(target_specifier) .await @@ -1158,11 +1161,12 @@ impl CallHierarchyItem { language_server: &mut language_server::Inner, maybe_root_path: Option<&Path>, ) -> lsp::CallHierarchyItem { - let target_specifier = normalize_specifier(&self.file).unwrap(); + let target_specifier = normalize_specifier(&self.file) + .unwrap_or_else(|_| INVALID_SPECIFIER.clone()); let uri = language_server .url_map .normalize_specifier(&target_specifier) - .unwrap(); + .unwrap_or_else(|_| INVALID_SPECIFIER.clone()); let use_file_name = self.is_source_file_item(); let maybe_file_path = if uri.scheme() == "file" { @@ -1239,7 +1243,7 @@ impl CallHierarchyIncomingCall { language_server: &mut language_server::Inner, maybe_root_path: Option<&Path>, ) -> Option { - let target_specifier = normalize_specifier(&self.from.file).unwrap(); + let target_specifier = normalize_specifier(&self.from.file).ok()?; let target_line_index = language_server .get_line_index(target_specifier) .await @@ -1274,7 +1278,7 @@ impl CallHierarchyOutgoingCall { language_server: &mut language_server::Inner, maybe_root_path: Option<&Path>, ) -> Option { - let target_specifier = normalize_specifier(&self.to.file).unwrap(); + let target_specifier = normalize_specifier(&self.to.file).ok()?; let target_line_index = language_server .get_line_index(target_specifier) .await diff --git a/cli/lsp/urls.rs b/cli/lsp/urls.rs index 49049b5351..52876157ae 100644 --- a/cli/lsp/urls.rs +++ b/cli/lsp/urls.rs @@ -11,6 +11,12 @@ use deno_core::url::Url; use deno_core::ModuleSpecifier; use std::collections::HashMap; +lazy_static::lazy_static! { + /// Used in situations where a default URL needs to be used where otherwise a + /// panic is undesired. + pub(crate) static ref INVALID_SPECIFIER: ModuleSpecifier = ModuleSpecifier::parse("deno://invalid").unwrap(); +} + /// Matches the `encodeURIComponent()` encoding from JavaScript, which matches /// the component percent encoding set. /// diff --git a/cli/tests/integration/lsp_tests.rs b/cli/tests/integration/lsp_tests.rs index 81eb64b7a1..7f4f4e5910 100644 --- a/cli/tests/integration/lsp_tests.rs +++ b/cli/tests/integration/lsp_tests.rs @@ -231,6 +231,91 @@ fn lsp_triple_slash_types() { shutdown(&mut client); } +#[test] +fn lsp_import_map() { + let temp_dir = TempDir::new().expect("could not create temp dir"); + let mut params: lsp::InitializeParams = + serde_json::from_value(load_fixture("initialize_params.json")).unwrap(); + let import_map = + serde_json::to_vec_pretty(&load_fixture("import-map.json")).unwrap(); + fs::write(temp_dir.path().join("import-map.json"), import_map).unwrap(); + fs::create_dir(temp_dir.path().join("lib")).unwrap(); + fs::write( + temp_dir.path().join("lib").join("b.ts"), + r#"export const b = "b";"#, + ) + .unwrap(); + + params.root_uri = Some(Url::from_file_path(temp_dir.path()).unwrap()); + if let Some(Value::Object(mut map)) = params.initialization_options { + map.insert("importMap".to_string(), json!("import-map.json")); + params.initialization_options = Some(Value::Object(map)); + } + + let deno_exe = deno_exe_path(); + let mut client = LspClient::new(&deno_exe).unwrap(); + client + .write_request::<_, _, Value>("initialize", params) + .unwrap(); + + client.write_notification("initialized", json!({})).unwrap(); + let uri = Url::from_file_path(temp_dir.path().join("a.ts")).unwrap(); + + let diagnostics = did_open( + &mut client, + json!({ + "textDocument": { + "uri": uri, + "languageId": "typescript", + "version": 1, + "text": "import { b } from \"/~/b.ts\";\n\nconsole.log(b);\n" + } + }), + ); + + let diagnostics = diagnostics.into_iter().flat_map(|x| x.diagnostics); + assert_eq!(diagnostics.count(), 0); + + let (maybe_res, maybe_err) = client + .write_request::<_, _, Value>( + "textDocument/hover", + json!({ + "textDocument": { + "uri": uri + }, + "position": { + "line": 2, + "character": 12 + } + }), + ) + .unwrap(); + assert!(maybe_err.is_none()); + assert_eq!( + maybe_res, + Some(json!({ + "contents": [ + { + "language": "typescript", + "value":"(alias) const b: \"b\"\nimport b" + }, + "" + ], + "range": { + "start": { + "line": 2, + "character": 12 + }, + "end": { + "line": 2, + "character": 13 + } + } + })) + ); + shutdown(&mut client); +} + #[test] fn lsp_hover() { let mut client = init("initialize_params.json"); diff --git a/cli/tests/lsp/import-map.json b/cli/tests/lsp/import-map.json new file mode 100644 index 0000000000..75d5d08491 --- /dev/null +++ b/cli/tests/lsp/import-map.json @@ -0,0 +1,5 @@ +{ + "imports": { + "/~/": "./lib/" + } +}