diff options
author | Kitson Kelly <me@kitsonkelly.com> | 2021-06-02 20:29:58 +1000 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-06-02 20:29:58 +1000 |
commit | 473713c6210ee11f11b7ae4c83165c4f87ff2d77 (patch) | |
tree | 34d361841ded4b8f0e089615b4afa7a40e37731b /cli | |
parent | 9ae8dbf17334f1cf7ae09abf585d8797f374bdc4 (diff) |
fix(#10815): lsp only responds to formatting for md, json, jsonc (#10816)
Fixes #10815
Diffstat (limited to 'cli')
-rw-r--r-- | cli/file_fetcher.rs | 77 | ||||
-rw-r--r-- | cli/lsp/completions.rs | 18 | ||||
-rw-r--r-- | cli/lsp/diagnostics.rs | 31 | ||||
-rw-r--r-- | cli/lsp/documents.rs | 128 | ||||
-rw-r--r-- | cli/lsp/language_server.rs | 148 | ||||
-rw-r--r-- | cli/lsp/tsc.rs | 37 | ||||
-rw-r--r-- | cli/media_type.rs | 159 | ||||
-rw-r--r-- | cli/tests/integration_tests_lsp.rs | 100 |
8 files changed, 529 insertions, 169 deletions
diff --git a/cli/file_fetcher.rs b/cli/file_fetcher.rs index 7ab2f5cce..4d3048750 100644 --- a/cli/file_fetcher.rs +++ b/cli/file_fetcher.rs @@ -178,33 +178,7 @@ pub fn map_content_type( if let Some(content_type) = maybe_content_type { let mut content_types = content_type.split(';'); let content_type = content_types.next().unwrap(); - let media_type = match content_type.trim().to_lowercase().as_ref() { - "application/typescript" - | "text/typescript" - | "video/vnd.dlna.mpeg-tts" - | "video/mp2t" - | "application/x-typescript" => { - map_js_like_extension(specifier, MediaType::TypeScript) - } - "application/javascript" - | "text/javascript" - | "application/ecmascript" - | "text/ecmascript" - | "application/x-javascript" - | "application/node" => { - map_js_like_extension(specifier, MediaType::JavaScript) - } - "text/jsx" => MediaType::Jsx, - "text/tsx" => MediaType::Tsx, - "application/json" | "text/json" => MediaType::Json, - "application/wasm" => MediaType::Wasm, - // Handle plain and possibly webassembly - "text/plain" | "application/octet-stream" => MediaType::from(specifier), - _ => { - debug!("unknown content type: {}", content_type); - MediaType::Unknown - } - }; + let media_type = MediaType::from_content_type(specifier, content_type); let charset = content_types .map(str::trim) .find_map(|s| s.strip_prefix("charset=")) @@ -216,55 +190,6 @@ pub fn map_content_type( } } -/// Used to augment media types by using the path part of a module specifier to -/// resolve to a more accurate media type. -fn map_js_like_extension( - specifier: &ModuleSpecifier, - default: MediaType, -) -> MediaType { - let path = if specifier.scheme() == "file" { - if let Ok(path) = specifier.to_file_path() { - path - } else { - PathBuf::from(specifier.path()) - } - } else { - PathBuf::from(specifier.path()) - }; - match path.extension() { - None => default, - Some(os_str) => match os_str.to_str() { - None => default, - Some("jsx") => MediaType::Jsx, - Some("tsx") => MediaType::Tsx, - // Because DTS files do not have a separate media type, or a unique - // extension, we have to "guess" at those things that we consider that - // look like TypeScript, and end with `.d.ts` are DTS files. - Some("ts") => { - if default == MediaType::TypeScript { - match path.file_stem() { - None => default, - Some(os_str) => { - if let Some(file_stem) = os_str.to_str() { - if file_stem.ends_with(".d") { - MediaType::Dts - } else { - default - } - } else { - default - } - } - } - } else { - default - } - } - Some(_) => default, - }, - } -} - /// Remove shebangs from the start of source code strings fn strip_shebang(mut value: String) -> String { if value.starts_with("#!") { diff --git a/cli/lsp/completions.rs b/cli/lsp/completions.rs index 95a13d59a..0e78b06e3 100644 --- a/cli/lsp/completions.rs +++ b/cli/lsp/completions.rs @@ -559,6 +559,7 @@ mod tests { use crate::http_cache::HttpCache; use crate::lsp::analysis; use crate::lsp::documents::DocumentCache; + use crate::lsp::documents::LanguageId; use crate::lsp::sources::Sources; use crate::media_type::MediaType; use deno_core::resolve_url; @@ -567,15 +568,15 @@ mod tests { use tempfile::TempDir; fn mock_state_snapshot( - fixtures: &[(&str, &str, i32)], + fixtures: &[(&str, &str, i32, LanguageId)], source_fixtures: &[(&str, &str)], location: &Path, ) -> language_server::StateSnapshot { let mut documents = DocumentCache::default(); - for (specifier, source, version) in fixtures { + for (specifier, source, version, language_id) in fixtures { let specifier = resolve_url(specifier).expect("failed to create specifier"); - documents.open(specifier.clone(), *version, source); + documents.open(specifier.clone(), *version, language_id.clone(), source); let media_type = MediaType::from(&specifier); let parsed_module = analysis::parse_module(&specifier, source, &media_type).unwrap(); @@ -608,7 +609,7 @@ mod tests { } fn setup( - documents: &[(&str, &str, i32)], + documents: &[(&str, &str, i32, LanguageId)], sources: &[(&str, &str)], ) -> language_server::StateSnapshot { let temp_dir = TempDir::new().expect("could not create temp dir"); @@ -885,8 +886,13 @@ mod tests { }; let state_snapshot = setup( &[ - ("file:///a/b/c.ts", "import * as d from \"h\"", 1), - ("file:///a/c.ts", r#""#, 1), + ( + "file:///a/b/c.ts", + "import * as d from \"h\"", + 1, + LanguageId::TypeScript, + ), + ("file:///a/c.ts", r#""#, 1, LanguageId::TypeScript), ], &[("https://deno.land/x/a/b/c.ts", "console.log(1);\n")], ); diff --git a/cli/lsp/diagnostics.rs b/cli/lsp/diagnostics.rs index 9b9035ac5..c069e4666 100644 --- a/cli/lsp/diagnostics.rs +++ b/cli/lsp/diagnostics.rs @@ -222,17 +222,6 @@ impl<'a> From<&'a diagnostics::Position> for lsp::Position { } } -/// Check if diagnostics can be generated for the provided media type. -pub fn is_diagnosable(media_type: MediaType) -> bool { - matches!( - media_type, - MediaType::TypeScript - | MediaType::JavaScript - | MediaType::Tsx - | MediaType::Jsx - ) -} - fn get_diagnostic_message(diagnostic: &diagnostics::Diagnostic) -> String { if let Some(message) = diagnostic.message_text.clone() { message @@ -322,13 +311,16 @@ async fn generate_lint_diagnostics( let mut diagnostics_vec = Vec::new(); if workspace_settings.lint { for specifier in documents.open_specifiers() { + if !documents.is_diagnosable(specifier) { + continue; + } let version = documents.version(specifier); let current_version = collection .lock() .await .get_version(specifier, &DiagnosticSource::DenoLint); let media_type = MediaType::from(specifier); - if version != current_version && is_diagnosable(media_type) { + if version != current_version { if let Ok(Some(source_code)) = documents.content(specifier) { if let Ok(references) = analysis::get_lint_references( specifier, @@ -366,12 +358,15 @@ async fn generate_ts_diagnostics( .open_specifiers() .iter() .filter_map(|&s| { - let version = snapshot.documents.version(s); - let current_version = - collection.get_version(s, &DiagnosticSource::TypeScript); - let media_type = MediaType::from(s); - if version != current_version && is_diagnosable(media_type) { - Some(s.clone()) + if snapshot.documents.is_diagnosable(s) { + let version = snapshot.documents.version(s); + let current_version = + collection.get_version(s, &DiagnosticSource::TypeScript); + if version != current_version { + Some(s.clone()) + } else { + None + } } else { None } diff --git a/cli/lsp/documents.rs b/cli/lsp/documents.rs index bdf80c5a8..048e4bedb 100644 --- a/cli/lsp/documents.rs +++ b/cli/lsp/documents.rs @@ -3,6 +3,9 @@ use super::analysis; use super::text::LineIndex; +use crate::media_type::MediaType; + +use deno_core::error::anyhow; use deno_core::error::custom_error; use deno_core::error::AnyError; use deno_core::error::Context; @@ -10,6 +13,37 @@ use deno_core::ModuleSpecifier; use lspower::lsp::TextDocumentContentChangeEvent; use std::collections::HashMap; use std::ops::Range; +use std::str::FromStr; + +/// A representation of the language id sent from the LSP client, which is used +/// to determine how the document is handled within the language server. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum LanguageId { + JavaScript, + Jsx, + TypeScript, + Tsx, + Json, + JsonC, + Markdown, +} + +impl FromStr for LanguageId { + type Err = AnyError; + + fn from_str(s: &str) -> Result<Self, AnyError> { + match s { + "javascript" => Ok(Self::JavaScript), + "javascriptreact" => Ok(Self::Jsx), + "typescript" => Ok(Self::TypeScript), + "typescriptreact" => Ok(Self::Tsx), + "json" => Ok(Self::Json), + "jsonc" => Ok(Self::JsonC), + "markdown" => Ok(Self::Markdown), + _ => Err(anyhow!("Unsupported language id: {}", s)), + } + } +} #[derive(Debug, PartialEq, Eq)] enum IndexValid { @@ -29,6 +63,7 @@ impl IndexValid { #[derive(Debug, Clone)] pub struct DocumentData { bytes: Option<Vec<u8>>, + language_id: LanguageId, line_index: Option<LineIndex>, specifier: ModuleSpecifier, dependencies: Option<HashMap<String, analysis::Dependency>>, @@ -36,9 +71,15 @@ pub struct DocumentData { } impl DocumentData { - pub fn new(specifier: ModuleSpecifier, version: i32, source: &str) -> Self { + pub fn new( + specifier: ModuleSpecifier, + version: i32, + language_id: LanguageId, + source: &str, + ) -> Self { Self { bytes: Some(source.as_bytes().to_owned()), + language_id, line_index: Some(LineIndex::new(source)), specifier, dependencies: None, @@ -150,6 +191,39 @@ impl DocumentCache { doc.dependencies.clone() } + /// Determines if the specifier should be processed for diagnostics and other + /// related language server features. + pub fn is_diagnosable(&self, specifier: &ModuleSpecifier) -> bool { + if specifier.scheme() != "file" { + // otherwise we look at the media type for the specifier. + matches!( + MediaType::from(specifier), + MediaType::JavaScript + | MediaType::Jsx + | MediaType::TypeScript + | MediaType::Tsx + | MediaType::Dts + ) + } else if let Some(doc_data) = self.docs.get(specifier) { + // if the document is in the document cache, then use the client provided + // language id to determine if the specifier is diagnosable. + matches!( + doc_data.language_id, + LanguageId::JavaScript + | LanguageId::Jsx + | LanguageId::TypeScript + | LanguageId::Tsx + ) + } else { + false + } + } + + /// Determines if the specifier can be processed for formatting. + pub fn is_formattable(&self, specifier: &ModuleSpecifier) -> bool { + self.docs.contains_key(specifier) + } + pub fn len(&self) -> usize { self.docs.len() } @@ -159,10 +233,16 @@ impl DocumentCache { doc.line_index.clone() } - pub fn open(&mut self, specifier: ModuleSpecifier, version: i32, text: &str) { + pub fn open( + &mut self, + specifier: ModuleSpecifier, + version: i32, + language_id: LanguageId, + source: &str, + ) { self.docs.insert( specifier.clone(), - DocumentData::new(specifier, version, text), + DocumentData::new(specifier, version, language_id, source), ); } @@ -219,7 +299,12 @@ mod tests { let mut document_cache = DocumentCache::default(); let specifier = resolve_url("file:///a/b.ts").unwrap(); let missing_specifier = resolve_url("file:///a/c.ts").unwrap(); - document_cache.open(specifier.clone(), 1, "console.log(\"Hello Deno\");\n"); + document_cache.open( + specifier.clone(), + 1, + LanguageId::TypeScript, + "console.log(\"Hello Deno\");\n", + ); assert!(document_cache.contains_key(&specifier)); assert!(!document_cache.contains_key(&missing_specifier)); } @@ -228,7 +313,12 @@ mod tests { fn test_document_cache_change() { let mut document_cache = DocumentCache::default(); let specifier = resolve_url("file:///a/b.ts").unwrap(); - document_cache.open(specifier.clone(), 1, "console.log(\"Hello deno\");\n"); + document_cache.open( + specifier.clone(), + 1, + LanguageId::TypeScript, + "console.log(\"Hello deno\");\n", + ); document_cache .change( &specifier, @@ -259,7 +349,12 @@ mod tests { fn test_document_cache_change_utf16() { let mut document_cache = DocumentCache::default(); let specifier = resolve_url("file:///a/b.ts").unwrap(); - document_cache.open(specifier.clone(), 1, "console.log(\"Hello 🦕\");\n"); + document_cache.open( + specifier.clone(), + 1, + LanguageId::TypeScript, + "console.log(\"Hello 🦕\");\n", + ); document_cache .change( &specifier, @@ -285,4 +380,25 @@ mod tests { .expect("failed to get content"); assert_eq!(actual, Some("console.log(\"Hello Deno\");\n".to_string())); } + + #[test] + fn test_is_diagnosable() { + let mut document_cache = DocumentCache::default(); + let specifier = resolve_url("file:///a/file.ts").unwrap(); + assert!(!document_cache.is_diagnosable(&specifier)); + document_cache.open( + specifier.clone(), + 1, + LanguageId::TypeScript, + "console.log(\"hello world\");\n", + ); + assert!(document_cache.is_diagnosable(&specifier)); + let specifier = + resolve_url("asset:///lib.es2015.symbol.wellknown.d.ts").unwrap(); + assert!(document_cache.is_diagnosable(&specifier)); + let specifier = resolve_url("data:application/typescript;base64,ZXhwb3J0IGNvbnN0IGEgPSAiYSI7CgpleHBvcnQgZW51bSBBIHsKICBBLAogIEIsCiAgQywKfQo=").unwrap(); + assert!(document_cache.is_diagnosable(&specifier)); + let specifier = resolve_url("data:application/json;base64,ZXhwb3J0IGNvbnN0IGEgPSAiYSI7CgpleHBvcnQgZW51bSBBIHsKICBBLAogIEIsCiAgQywKfQo=").unwrap(); + assert!(!document_cache.is_diagnosable(&specifier)); + } } diff --git a/cli/lsp/language_server.rs b/cli/lsp/language_server.rs index 3a751e319..20d5c1ee7 100644 --- a/cli/lsp/language_server.rs +++ b/cli/lsp/language_server.rs @@ -42,6 +42,7 @@ use super::config::SETTINGS_SECTION; use super::diagnostics; use super::diagnostics::DiagnosticSource; use super::documents::DocumentCache; +use super::documents::LanguageId; use super::lsp_custom; use super::performance::Performance; use super::registries; @@ -59,7 +60,6 @@ use crate::config_file::TsConfig; use crate::deno_dir; use crate::import_map::ImportMap; use crate::logger; -use crate::lsp::diagnostics::is_diagnosable; use crate::media_type::MediaType; use crate::tools::fmt::format_file; use crate::tools::fmt::get_typescript_config; @@ -611,17 +611,27 @@ impl Inner { // already managed by the language service return; } + let language_id = match params.text_document.language_id.parse() { + Ok(language_id) => language_id, + Err(err) => { + error!("{}", err); + LanguageId::TypeScript + } + }; self.documents.open( specifier.clone(), params.text_document.version, + language_id, ¶ms.text_document.text, ); - self.analyze_dependencies(&specifier, ¶ms.text_document.text); - self.performance.measure(mark); - if let Err(err) = self.diagnostics_server.update() { - error!("{}", err); + if self.documents.is_diagnosable(&specifier) { + self.analyze_dependencies(&specifier, ¶ms.text_document.text); + if let Err(err) = self.diagnostics_server.update() { + error!("{}", err); + } } + self.performance.measure(mark); } async fn did_change(&mut self, params: DidChangeTextDocumentParams) { @@ -632,15 +642,18 @@ impl Inner { params.text_document.version, params.content_changes, ) { - Ok(Some(source)) => self.analyze_dependencies(&specifier, &source), + Ok(Some(source)) => { + if self.documents.is_diagnosable(&specifier) { + self.analyze_dependencies(&specifier, &source); + if let Err(err) = self.diagnostics_server.update() { + error!("{}", err); + } + } + } Ok(_) => error!("No content returned from change."), Err(err) => error!("{}", err), } self.performance.measure(mark); - - if let Err(err) = self.diagnostics_server.update() { - error!("{}", err); - } } async fn did_close(&mut self, params: DidCloseTextDocumentParams) { @@ -655,10 +668,12 @@ impl Inner { self.documents.close(&specifier); self.navigation_trees.remove(&specifier); - self.performance.measure(mark); - if let Err(err) = self.diagnostics_server.update() { - error!("{}", err); + if self.documents.is_diagnosable(&specifier) { + if let Err(err) = self.diagnostics_server.update() { + error!("{}", err); + } } + self.performance.measure(mark); } async fn did_change_configuration( @@ -751,11 +766,9 @@ impl Inner { params: DocumentSymbolParams, ) -> LspResult<Option<DocumentSymbolResponse>> { let specifier = self.url_map.normalize_url(¶ms.text_document.uri); - if !self.config.specifier_enabled(&specifier) { - return Ok(None); - } - let media_type = MediaType::from(&specifier); - if !is_diagnosable(media_type) { + if !self.documents.is_diagnosable(&specifier) + || !self.config.specifier_enabled(&specifier) + { return Ok(None); } @@ -798,8 +811,11 @@ impl Inner { &self, params: DocumentFormattingParams, ) -> LspResult<Option<Vec<TextEdit>>> { - let mark = self.performance.mark("formatting", Some(¶ms)); let specifier = self.url_map.normalize_url(¶ms.text_document.uri); + if !self.documents.is_formattable(&specifier) { + return Ok(None); + } + let mark = self.performance.mark("formatting", Some(¶ms)); let file_text = self .documents .content(&specifier) @@ -850,7 +866,9 @@ impl Inner { let specifier = self .url_map .normalize_url(¶ms.text_document_position_params.text_document.uri); - if !self.config.specifier_enabled(&specifier) { + if !self.documents.is_diagnosable(&specifier) + || !self.config.specifier_enabled(&specifier) + { return Ok(None); } let mark = self.performance.mark("hover", Some(¶ms)); @@ -891,7 +909,9 @@ impl Inner { params: CodeActionParams, ) -> LspResult<Option<CodeActionResponse>> { let specifier = self.url_map.normalize_url(¶ms.text_document.uri); - if !self.config.specifier_enabled(&specifier) { + if !self.documents.is_diagnosable(&specifier) + || !self.config.specifier_enabled(&specifier) + { return Ok(None); } @@ -1054,7 +1074,8 @@ impl Inner { params: CodeLensParams, ) -> LspResult<Option<Vec<CodeLens>>> { let specifier = self.url_map.normalize_url(¶ms.text_document.uri); - if !self.config.specifier_enabled(&specifier) + if !self.documents.is_diagnosable(&specifier) + || !self.config.specifier_enabled(&specifier) || !self.config.get_workspace_settings().enabled_code_lens() { return Ok(None); @@ -1366,7 +1387,9 @@ impl Inner { let specifier = self .url_map .normalize_url(¶ms.text_document_position_params.text_document.uri); - if !self.config.specifier_enabled(&specifier) { + if !self.documents.is_diagnosable(&specifier) + || !self.config.specifier_enabled(&specifier) + { return Ok(None); } @@ -1416,9 +1439,12 @@ impl Inner { let specifier = self .url_map .normalize_url(¶ms.text_document_position.text_document.uri); - if !self.config.specifier_enabled(&specifier) { + if !self.documents.is_diagnosable(&specifier) + || !self.config.specifier_enabled(&specifier) + { return Ok(None); } + let mark = self.performance.mark("references", Some(¶ms)); let line_index = if let Some(line_index) = self.get_line_index_sync(&specifier) { @@ -1471,9 +1497,12 @@ impl Inner { let specifier = self .url_map .normalize_url(¶ms.text_document_position_params.text_document.uri); - if !self.config.specifier_enabled(&specifier) { + if !self.documents.is_diagnosable(&specifier) + || !self.config.specifier_enabled(&specifier) + { return Ok(None); } + let mark = self.performance.mark("goto_definition", Some(¶ms)); let line_index = if let Some(line_index) = self.get_line_index_sync(&specifier) { @@ -1514,9 +1543,12 @@ impl Inner { let specifier = self .url_map .normalize_url(¶ms.text_document_position.text_document.uri); - if !self.config.specifier_enabled(&specifier) { + if !self.documents.is_diagnosable(&specifier) + || !self.config.specifier_enabled(&specifier) + { return Ok(None); } + let mark = self.performance.mark("completion", Some(¶ms)); // Import specifiers are something wholly internal to Deno, so for // completions, we will use internal logic and if there are completions @@ -1632,9 +1664,12 @@ impl Inner { let specifier = self .url_map .normalize_url(¶ms.text_document_position_params.text_document.uri); - if !self.config.specifier_enabled(&specifier) { + if !self.documents.is_diagnosable(&specifier) + || !self.config.specifier_enabled(&specifier) + { return Ok(None); } + let mark = self.performance.mark("goto_implementation", Some(¶ms)); let line_index = if let Some(line_index) = self.get_line_index_sync(&specifier) { @@ -1680,11 +1715,13 @@ impl Inner { params: FoldingRangeParams, ) -> LspResult<Option<Vec<FoldingRange>>> { let specifier = self.url_map.normalize_url(¶ms.text_document.uri); - if !self.config.specifier_enabled(&specifier) { + if !self.documents.is_diagnosable(&specifier) + || !self.config.specifier_enabled(&specifier) + { return Ok(None); } - let mark = self.performance.mark("folding_range", Some(¶ms)); + let mark = self.performance.mark("folding_range", Some(¶ms)); let line_index = if let Some(line_index) = self.get_line_index_sync(&specifier) { line_index @@ -1737,11 +1774,13 @@ impl Inner { params: CallHierarchyIncomingCallsParams, ) -> LspResult<Option<Vec<CallHierarchyIncomingCall>>> { let specifier = self.url_map.normalize_url(¶ms.item.uri); - if !self.config.specifier_enabled(&specifier) { + if !self.documents.is_diagnosable(&specifier) + || !self.config.specifier_enabled(&specifier) + { return Ok(None); } - let mark = self.performance.mark("incoming_calls", Some(¶ms)); + let mark = self.performance.mark("incoming_calls", Some(¶ms)); let line_index = if let Some(line_index) = self.get_line_index_sync(&specifier) { line_index @@ -1791,11 +1830,13 @@ impl Inner { params: CallHierarchyOutgoingCallsParams, ) -> LspResult<Option<Vec<CallHierarchyOutgoingCall>>> { let specifier = self.url_map.normalize_url(¶ms.item.uri); - if !self.config.specifier_enabled(&specifier) { + if !self.documents.is_diagnosable(&specifier) + || !self.config.specifier_enabled(&specifier) + { return Ok(None); } - let mark = self.performance.mark("outgoing_calls", Some(¶ms)); + let mark = self.performance.mark("outgoing_calls", Some(¶ms)); let line_index = if let Some(line_index) = self.get_line_index_sync(&specifier) { line_index @@ -1848,13 +1889,15 @@ impl Inner { let specifier = self .url_map .normalize_url(¶ms.text_document_position_params.text_document.uri); - if !self.config.specifier_enabled(&specifier) { + if !self.documents.is_diagnosable(&specifier) + || !self.config.specifier_enabled(&specifier) + { return Ok(None); } + let mark = self .performance .mark("prepare_call_hierarchy", Some(¶ms)); - let line_index = if let Some(line_index) = self.get_line_index_sync(&specifier) { line_index @@ -1927,11 +1970,13 @@ impl Inner { let specifier = self .url_map .normalize_url(¶ms.text_document_position.text_document.uri); - if !self.config.specifier_enabled(&specifier) { + if !self.documents.is_diagnosable(&specifier) + || !self.config.specifier_enabled(&specifier) + { return Ok(None); } - let mark = self.performance.mark("rename", Some(¶ms)); + let mark = self.performance.mark("rename", Some(¶ms)); let line_index = if let Some(line_index) = self.get_line_index_sync(&specifier) { line_index @@ -2019,11 +2064,13 @@ impl Inner { params: SelectionRangeParams, ) -> LspResult<Option<Vec<SelectionRange>>> { let specifier = self.url_map.normalize_url(¶ms.text_document.uri); - if !self.config.specifier_enabled(&specifier) { + if !self.documents.is_diagnosable(&specifier) + || !self.config.specifier_enabled(&specifier) + { return Ok(None); } - let mark = self.performance.mark("selection_range", Some(¶ms)); + let mark = self.performance.mark("selection_range", Some(¶ms)); let line_index = if let Some(line_index) = self.get_line_index_sync(&specifier) { line_index @@ -2061,11 +2108,13 @@ impl Inner { params: SemanticTokensParams, ) -> LspResult<Option<SemanticTokensResult>> { let specifier = self.url_map.normalize_url(¶ms.text_document.uri); - if !self.config.specifier_enabled(&specifier) { + if !self.documents.is_diagnosable(&specifier) + || !self.config.specifier_enabled(&specifier) + { return Ok(None); } - let mark = self.performance.mark("semantic_tokens_full", Some(¶ms)); + let mark = self.performance.mark("semantic_tokens_full", Some(¶ms)); let line_index = if let Some(line_index) = self.get_line_index_sync(&specifier) { line_index @@ -2108,13 +2157,15 @@ impl Inner { params: SemanticTokensRangeParams, ) -> LspResult<Option<SemanticTokensRangeResult>> { let specifier = self.url_map.normalize_url(¶ms.text_document.uri); - if !self.config.specifier_enabled(&specifier) { + if !self.documents.is_diagnosable(&specifier) + || !self.config.specifier_enabled(&specifier) + { return Ok(None); } + let mark = self .performance .mark("semantic_tokens_range", Some(¶ms)); - let line_index = if let Some(line_index) = self.get_line_index_sync(&specifier) { line_index @@ -2158,9 +2209,12 @@ impl Inner { let specifier = self .url_map .normalize_url(¶ms.text_document_position_params.text_document.uri); - if !self.config.specifier_enabled(&specifier) { + if !self.documents.is_diagnosable(&specifier) + || !self.config.specifier_enabled(&specifier) + { return Ok(None); } + let mark = self.performance.mark("signature_help", Some(¶ms)); let line_index = if let Some(line_index) = self.get_line_index_sync(&specifier) { @@ -2427,8 +2481,12 @@ impl Inner { &mut self, params: lsp_custom::CacheParams, ) -> LspResult<Option<Value>> { - let mark = self.performance.mark("cache", Some(¶ms)); let referrer = self.url_map.normalize_url(¶ms.referrer.uri); + if !self.documents.is_diagnosable(&referrer) { + return Ok(None); + } + + let mark = self.performance.mark("cache", Some(¶ms)); if !params.uris.is_empty() { for identifier in ¶ms.uris { let specifier = self.url_map.normalize_url(&identifier.uri); diff --git a/cli/lsp/tsc.rs b/cli/lsp/tsc.rs index 00be3e50b..6a476054f 100644 --- a/cli/lsp/tsc.rs +++ b/cli/lsp/tsc.rs @@ -2566,6 +2566,7 @@ mod tests { use crate::http_util::HeadersMap; use crate::lsp::analysis; use crate::lsp::documents::DocumentCache; + use crate::lsp::documents::LanguageId; use crate::lsp::sources::Sources; use crate::lsp::text::LineIndex; use std::path::Path; @@ -2573,14 +2574,14 @@ mod tests { use tempfile::TempDir; fn mock_state_snapshot( - fixtures: &[(&str, &str, i32)], + fixtures: &[(&str, &str, i32, LanguageId)], location: &Path, ) -> StateSnapshot { let mut documents = DocumentCache::default(); - for (specifier, source, version) in fixtures { + for (specifier, source, version, language_id) in fixtures { let specifier = resolve_url(specifier).expect("failed to create specifier"); - documents.open(specifier.clone(), *version, source); + documents.open(specifier.clone(), *version, language_id.clone(), source); let media_type = MediaType::from(&specifier); if let Ok(parsed_module) = analysis::parse_module(&specifier, source, &media_type) @@ -2605,7 +2606,7 @@ mod tests { fn setup( debug: bool, config: Value, - sources: &[(&str, &str, i32)], + sources: &[(&str, &str, i32, LanguageId)], ) -> (JsRuntime, StateSnapshot, PathBuf) { let temp_dir = TempDir::new().expect("could not create temp dir"); let location = temp_dir.path().join("deps"); @@ -2688,7 +2689,12 @@ mod tests { "module": "esnext", "noEmit": true, }), - &[("file:///a.ts", r#"console.log("hello deno");"#, 1)], + &[( + "file:///a.ts", + r#"console.log("hello deno");"#, + 1, + LanguageId::TypeScript, + )], ); let specifier = resolve_url("file:///a.ts").expect("could not resolve url"); let result = request( @@ -2733,7 +2739,12 @@ mod tests { "lib": ["esnext", "dom", "deno.ns"], "noEmit": true, }), - &[("file:///a.ts", r#"console.log(document.location);"#, 1)], + &[( + "file:///a.ts", + r#"console.log(document.location);"#, + 1, + LanguageId::TypeScript, + )], ); let specifier = resolve_url("file:///a.ts").expect("could not resolve url"); let result = request( @@ -2766,6 +2777,7 @@ mod tests { console.log(b); "#, 1, + LanguageId::TypeScript, )], ); let specifier = resolve_url("file:///a.ts").expect("could not resolve url"); @@ -2795,6 +2807,7 @@ mod tests { import { A } from "."; "#, 1, + LanguageId::TypeScript, )], ); let specifier = resolve_url("file:///a.ts").expect("could not resolve url"); @@ -2848,6 +2861,7 @@ mod tests { console.log(b); "#, 1, + LanguageId::TypeScript, )], ); let specifier = resolve_url("file:///a.ts").expect("could not resolve url"); @@ -2884,6 +2898,7 @@ mod tests { import * as test from "#, 1, + LanguageId::TypeScript, )], ); let specifier = resolve_url("file:///a.ts").expect("could not resolve url"); @@ -2941,7 +2956,12 @@ mod tests { "lib": ["deno.ns", "deno.window"], "noEmit": true, }), - &[("file:///a.ts", r#"const url = new URL("b.js", import."#, 1)], + &[( + "file:///a.ts", + r#"const url = new URL("b.js", import."#, + 1, + LanguageId::TypeScript, + )], ); let specifier = resolve_url("file:///a.ts").expect("could not resolve url"); let result = request( @@ -2998,6 +3018,7 @@ mod tests { } "#, 1, + LanguageId::TypeScript, )], ); let cache = HttpCache::new(&location); @@ -3099,7 +3120,7 @@ mod tests { "lib": ["deno.ns", "deno.window"], "noEmit": true, }), - &[("file:///a.ts", fixture, 1)], + &[("file:///a.ts", fixture, 1, LanguageId::TypeScript)], ); let specifier = resolve_url("file:///a.ts").expect("could not resolve url"); let result = request( diff --git a/cli/media_type.rs b/cli/media_type.rs index c83716f67..bfb869c13 100644 --- a/cli/media_type.rs +++ b/cli/media_type.rs @@ -1,5 +1,6 @@ // Copyright 2018-2021 the Deno authors. All rights reserved. MIT license. +use data_url::DataUrl; use deno_core::serde::Serialize; use deno_core::serde::Serializer; use deno_core::ModuleSpecifier; @@ -45,34 +46,40 @@ impl fmt::Display for MediaType { impl<'a> From<&'a Path> for MediaType { fn from(path: &'a Path) -> Self { - MediaType::from_path(path) + Self::from_path(path) } } impl<'a> From<&'a PathBuf> for MediaType { fn from(path: &'a PathBuf) -> Self { - MediaType::from_path(path) + Self::from_path(path) } } impl<'a> From<&'a String> for MediaType { fn from(specifier: &'a String) -> Self { - MediaType::from_path(&PathBuf::from(specifier)) + Self::from_path(&PathBuf::from(specifier)) } } impl<'a> From<&'a ModuleSpecifier> for MediaType { fn from(specifier: &'a ModuleSpecifier) -> Self { - let path = if specifier.scheme() == "file" { - if let Ok(path) = specifier.to_file_path() { - path + if specifier.scheme() != "data" { + let path = if specifier.scheme() == "file" { + if let Ok(path) = specifier.to_file_path() { + path + } else { + PathBuf::from(specifier.path()) + } } else { PathBuf::from(specifier.path()) - } + }; + Self::from_path(&path) + } else if let Ok(data_url) = DataUrl::process(specifier.as_str()) { + Self::from_content_type(specifier, data_url.mime_type().to_string()) } else { - PathBuf::from(specifier.path()) - }; - MediaType::from_path(&path) + Self::Unknown + } } } @@ -83,6 +90,40 @@ impl Default for MediaType { } impl MediaType { + pub fn from_content_type<S: AsRef<str>>( + specifier: &ModuleSpecifier, + content_type: S, + ) -> Self { + match content_type.as_ref().trim().to_lowercase().as_ref() { + "application/typescript" + | "text/typescript" + | "video/vnd.dlna.mpeg-tts" + | "video/mp2t" + | "application/x-typescript" => { + map_js_like_extension(specifier, Self::TypeScript) + } + "application/javascript" + | "text/javascript" + | "application/ecmascript" + | "text/ecmascript" + | "application/x-javascript" + | "application/node" => { + map_js_like_extension(specifier, Self::JavaScript) + } + "text/jsx" => Self::Jsx, + "text/tsx" => Self::Tsx, + "application/json" | "text/json" => Self::Json, + "application/wasm" => Self::Wasm, + // Handle plain and possibly webassembly + "text/plain" | "application/octet-stream" + if specifier.scheme() != "data" => + { + Self::from(specifier) + } + _ => Self::Unknown, + } + } + fn from_path(path: &Path) -> Self { match path.extension() { None => match path.file_name() { @@ -197,6 +238,55 @@ where } } +/// Used to augment media types by using the path part of a module specifier to +/// resolve to a more accurate media type. +fn map_js_like_extension( + specifier: &ModuleSpecifier, + default: MediaType, +) -> MediaType { + let path = if specifier.scheme() == "file" { + if let Ok(path) = specifier.to_file_path() { + path + } else { + PathBuf::from(specifier.path()) + } + } else { + PathBuf::from(specifier.path()) + }; + match path.extension() { + None => default, + Some(os_str) => match os_str.to_str() { + None => default, + Some("jsx") => MediaType::Jsx, + Some("tsx") => MediaType::Tsx, + // Because DTS files do not have a separate media type, or a unique + // extension, we have to "guess" at those things that we consider that + // look like TypeScript, and end with `.d.ts` are DTS files. + Some("ts") => { + if default == MediaType::TypeScript { + match path.file_stem() { + None => default, + Some(os_str) => { + if let Some(file_stem) = os_str.to_str() { + if file_stem.ends_with(".d") { + MediaType::Dts + } else { + default + } + } else { + default + } + } + } + } else { + default + } + } + Some(_) => default, + }, + } +} + #[cfg(test)] mod tests { use super::*; @@ -245,6 +335,9 @@ mod tests { ("https://deno.land/x/mod.ts", MediaType::TypeScript), ("https://deno.land/x/mod.js", MediaType::JavaScript), ("https://deno.land/x/mod.txt", MediaType::Unknown), + ("data:application/typescript;base64,ZXhwb3J0IGNvbnN0IGEgPSAiYSI7CgpleHBvcnQgZW51bSBBIHsKICBBLAogIEIsCiAgQywKfQo=", MediaType::TypeScript), + ("data:application/javascript;base64,ZXhwb3J0IGNvbnN0IGEgPSAiYSI7CgpleHBvcnQgZW51bSBBIHsKICBBLAogIEIsCiAgQywKfQo=", MediaType::JavaScript), + ("data:text/plain;base64,ZXhwb3J0IGNvbnN0IGEgPSAiYSI7CgpleHBvcnQgZW51bSBBIHsKICBBLAogIEIsCiAgQywKfQo=", MediaType::Unknown), ]; for (specifier, expected) in fixtures { @@ -254,6 +347,52 @@ mod tests { } #[test] + fn test_from_content_type() { + let fixtures = vec![ + ( + "https://deno.land/x/mod.ts", + "application/typescript", + MediaType::TypeScript, + ), + ( + "https://deno.land/x/mod.d.ts", + "application/typescript", + MediaType::Dts, + ), + ("https://deno.land/x/mod.tsx", "text/tsx", MediaType::Tsx), + ( + "https://deno.land/x/mod.js", + "application/javascript", + MediaType::JavaScript, + ), + ("https://deno.land/x/mod.jsx", "text/jsx", MediaType::Jsx), + ( + "https://deno.land/x/mod.ts", + "text/plain", + MediaType::TypeScript, + ), + ( + "https://deno.land/x/mod.js", + "text/plain", + MediaType::JavaScript, + ), + ( + "https://deno.land/x/mod.wasm", + "text/plain", + MediaType::Wasm, + ), + ]; + + for (specifier, content_type, expected) in fixtures { + let fixture = deno_core::resolve_url_or_path(specifier).unwrap(); + assert_eq!( + MediaType::from_content_type(&fixture, content_type), + expected + ); + } + } + + #[test] fn test_serialization() { assert_eq!(json!(MediaType::JavaScript), json!(0)); assert_eq!(json!(MediaType::Jsx), json!(1)); diff --git a/cli/tests/integration_tests_lsp.rs b/cli/tests/integration_tests_lsp.rs index e4b963f2b..2fe984bca 100644 --- a/cli/tests/integration_tests_lsp.rs +++ b/cli/tests/integration_tests_lsp.rs @@ -2119,6 +2119,56 @@ fn lsp_format_json() { } #[test] +fn lsp_json_no_diagnostics() { + let mut client = init("initialize_params.json"); + client + .write_notification( + "textDocument/didOpen", + json!({ + "textDocument": { + "uri": "file:///a/file.json", + "languageId": "json", + "version": 1, + "text": "{\"key\":\"value\"}" + } + }), + ) + .unwrap(); + + let (maybe_res, maybe_err) = client + .write_request( + "textDocument/semanticTokens/full", + json!({ + "textDocument": { + "uri": "file:///a/file.json" + } + }), + ) + .unwrap(); + assert!(maybe_err.is_none()); + assert_eq!(maybe_res, Some(json!(null))); + + let (maybe_res, maybe_err) = client + .write_request::<_, _, Value>( + "textDocument/hover", + json!({ + "textDocument": { + "uri": "file:///a/file.json" + }, + "position": { + "line": 0, + "character": 3 + } + }), + ) + .unwrap(); + assert!(maybe_err.is_none()); + assert_eq!(maybe_res, Some(json!(null))); + + shutdown(&mut client); +} + +#[test] fn lsp_format_markdown() { let mut client = init("initialize_params.json"); client @@ -2174,6 +2224,56 @@ fn lsp_format_markdown() { } #[test] +fn lsp_markdown_no_diagnostics() { + let mut client = init("initialize_params.json"); + client + .write_notification( + "textDocument/didOpen", + json!({ + "textDocument": { + "uri": "file:///a/file.md", + "languageId": "markdown", + "version": 1, + "text": "# Hello World" + } + }), + ) + .unwrap(); + + let (maybe_res, maybe_err) = client + .write_request( + "textDocument/semanticTokens/full", + json!({ + "textDocument": { + "uri": "file:///a/file.md" + } + }), + ) + .unwrap(); + assert!(maybe_err.is_none()); + assert_eq!(maybe_res, Some(json!(null))); + + let (maybe_res, maybe_err) = client + .write_request::<_, _, Value>( + "textDocument/hover", + json!({ + "textDocument": { + "uri": "file:///a/file.md" + }, + "position": { + "line": 0, + "character": 3 + } + }), + ) + .unwrap(); + assert!(maybe_err.is_none()); + assert_eq!(maybe_res, Some(json!(null))); + + shutdown(&mut client); +} + +#[test] fn lsp_configuration_did_change() { let _g = http_server(); let mut client = init("initialize_params_did_config_change.json"); |