// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license. use super::analysis; use super::document_source::DocumentSource; use super::text::LineIndex; use super::tsc; use deno_ast::MediaType; use deno_core::error::custom_error; use deno_core::error::AnyError; use deno_core::ModuleSpecifier; use lspower::lsp; use std::collections::HashMap; use std::collections::HashSet; use std::ops::Range; use std::str::FromStr; use std::sync::Arc; /// 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, Copy)] pub enum LanguageId { JavaScript, Jsx, TypeScript, Tsx, Json, JsonC, Markdown, Unknown, } impl FromStr for LanguageId { type Err = AnyError; fn from_str(s: &str) -> Result { match s { "javascript" => Ok(Self::JavaScript), "javascriptreact" => Ok(Self::Jsx), "jsx" => Ok(Self::Jsx), "typescript" => Ok(Self::TypeScript), "typescriptreact" => Ok(Self::Tsx), "tsx" => Ok(Self::Tsx), "json" => Ok(Self::Json), "jsonc" => Ok(Self::JsonC), "markdown" => Ok(Self::Markdown), _ => Ok(Self::Unknown), } } } impl<'a> From<&'a LanguageId> for MediaType { fn from(id: &'a LanguageId) -> MediaType { match id { LanguageId::JavaScript => MediaType::JavaScript, LanguageId::Json => MediaType::Json, LanguageId::JsonC => MediaType::Json, LanguageId::Jsx => MediaType::Jsx, LanguageId::Markdown => MediaType::Unknown, LanguageId::Tsx => MediaType::Tsx, LanguageId::TypeScript => MediaType::TypeScript, LanguageId::Unknown => MediaType::Unknown, } } } #[derive(Debug, PartialEq, Eq)] enum IndexValid { All, UpTo(u32), } impl IndexValid { fn covers(&self, line: u32) -> bool { match *self { IndexValid::UpTo(to) => to > line, IndexValid::All => true, } } } #[derive(Debug, Clone)] pub struct DocumentData { source: DocumentSource, dependencies: Option>, dependency_ranges: Option, pub(crate) language_id: LanguageId, maybe_navigation_tree: Option, specifier: ModuleSpecifier, version: Option, } impl DocumentData { pub fn new( specifier: ModuleSpecifier, version: i32, language_id: LanguageId, source_text: Arc, ) -> Self { let line_index = LineIndex::new(&source_text); Self { source: DocumentSource::new( &specifier, MediaType::from(&language_id), source_text, line_index, ), dependencies: None, dependency_ranges: None, language_id, maybe_navigation_tree: None, specifier, version: Some(version), } } pub fn apply_content_changes( &mut self, content_changes: Vec, ) -> Result<(), AnyError> { let mut content = self.source.text_info().text_str().to_string(); let mut line_index = self.source.line_index().clone(); let mut index_valid = IndexValid::All; for change in content_changes { if let Some(range) = change.range { if !index_valid.covers(range.start.line) { line_index = LineIndex::new(&content); } index_valid = IndexValid::UpTo(range.start.line); let range = line_index.get_text_range(range)?; content.replace_range(Range::::from(range), &change.text); } else { content = change.text; index_valid = IndexValid::UpTo(0); } } let line_index = if index_valid == IndexValid::All { line_index } else { LineIndex::new(&content) }; self.source = DocumentSource::new( &self.specifier, MediaType::from(&self.language_id), Arc::new(content), line_index, ); self.maybe_navigation_tree = None; Ok(()) } pub fn source(&self) -> &DocumentSource { &self.source } /// Determines if a position within the document is within a dependency range /// and if so, returns the range with the text of the specifier. fn is_specifier_position( &self, position: &lsp::Position, ) -> Option { let import_ranges = self.dependency_ranges.as_ref()?; import_ranges.contains(position) } } #[derive(Debug, Clone, Default)] pub struct DocumentCache { dependents_graph: HashMap>, pub(crate) docs: HashMap, } impl DocumentCache { /// Calculate a graph of dependents and set it on the structure. fn calculate_dependents(&mut self) { let mut dependents_graph: HashMap< ModuleSpecifier, HashSet, > = HashMap::new(); for (specifier, data) in &self.docs { if let Some(dependencies) = &data.dependencies { for dependency in dependencies.values() { if let Some(analysis::ResolvedDependency::Resolved(dep_specifier)) = &dependency.maybe_code { dependents_graph .entry(dep_specifier.clone()) .or_default() .insert(specifier.clone()); } if let Some(analysis::ResolvedDependency::Resolved(dep_specifier)) = &dependency.maybe_type { dependents_graph .entry(dep_specifier.clone()) .or_default() .insert(specifier.clone()); } } } } self.dependents_graph = dependents_graph; } pub fn change( &mut self, specifier: &ModuleSpecifier, version: i32, content_changes: Vec, ) -> Result<(), AnyError> { if !self.contains_key(specifier) { return Err(custom_error( "NotFound", format!( "The specifier (\"{}\") does not exist in the document cache.", specifier ), )); } let doc = self.docs.get_mut(specifier).unwrap(); doc.apply_content_changes(content_changes)?; doc.version = Some(version); Ok(()) } pub fn close(&mut self, specifier: &ModuleSpecifier) { self.docs.remove(specifier); self.calculate_dependents(); } pub fn contains_key(&self, specifier: &ModuleSpecifier) -> bool { self.docs.contains_key(specifier) } pub fn get(&self, specifier: &ModuleSpecifier) -> Option<&DocumentData> { self.docs.get(specifier) } pub fn content(&self, specifier: &ModuleSpecifier) -> Option> { self .docs .get(specifier) .map(|d| d.source().text_info().text()) } // For a given specifier, get all open documents which directly or indirectly // depend upon the specifier. pub fn dependents( &self, specifier: &ModuleSpecifier, ) -> Vec { let mut dependents = HashSet::new(); self.recurse_dependents(specifier, &mut dependents); dependents.into_iter().collect() } pub fn dependencies( &self, specifier: &ModuleSpecifier, ) -> Option> { self .docs .get(specifier) .map(|doc| doc.dependencies.clone()) .flatten() } pub fn get_navigation_tree( &self, specifier: &ModuleSpecifier, ) -> Option { self .docs .get(specifier) .map(|doc| doc.maybe_navigation_tree.clone()) .flatten() } /// 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) } /// Determines if the position in the document is within a range of a module /// specifier, returning the text range if true. pub fn is_specifier_position( &self, specifier: &ModuleSpecifier, position: &lsp::Position, ) -> Option { let document = self.docs.get(specifier)?; document.is_specifier_position(position) } pub fn len(&self) -> usize { self.docs.len() } pub fn line_index(&self, specifier: &ModuleSpecifier) -> Option { self .docs .get(specifier) .map(|d| d.source().line_index().clone()) } pub fn open( &mut self, specifier: ModuleSpecifier, version: i32, language_id: LanguageId, source: Arc, ) { self.docs.insert( specifier.clone(), DocumentData::new(specifier, version, language_id, source), ); } pub fn open_specifiers(&self) -> Vec<&ModuleSpecifier> { self .docs .iter() .filter_map(|(key, data)| { if data.version.is_some() { Some(key) } else { None } }) .collect() } fn recurse_dependents( &self, specifier: &ModuleSpecifier, dependents: &mut HashSet, ) { if let Some(deps) = self.dependents_graph.get(specifier) { for dep in deps { if !dependents.contains(dep) { dependents.insert(dep.clone()); self.recurse_dependents(dep, dependents); } } } } pub fn set_dependencies( &mut self, specifier: &ModuleSpecifier, maybe_dependencies: Option>, maybe_dependency_ranges: Option, ) -> Result<(), AnyError> { if let Some(doc) = self.docs.get_mut(specifier) { doc.dependencies = maybe_dependencies; doc.dependency_ranges = maybe_dependency_ranges; self.calculate_dependents(); Ok(()) } else { Err(custom_error( "NotFound", format!( "The specifier (\"{}\") does not exist in the document cache.", specifier ), )) } } pub fn set_navigation_tree( &mut self, specifier: &ModuleSpecifier, navigation_tree: tsc::NavigationTree, ) -> Result<(), AnyError> { if let Some(doc) = self.docs.get_mut(specifier) { doc.maybe_navigation_tree = Some(navigation_tree); Ok(()) } else { Err(custom_error( "NotFound", format!( "The specifier (\"{}\") does not exist in the document cache.", specifier ), )) } } pub fn specifiers(&self) -> Vec { self.docs.keys().cloned().collect() } pub fn version(&self, specifier: &ModuleSpecifier) -> Option { self.docs.get(specifier).and_then(|doc| doc.version) } } #[cfg(test)] mod tests { use super::*; use deno_core::resolve_url; use lspower::lsp; #[test] fn test_language_id() { assert_eq!( "javascript".parse::().unwrap(), LanguageId::JavaScript ); assert_eq!( "javascriptreact".parse::().unwrap(), LanguageId::Jsx ); assert_eq!("jsx".parse::().unwrap(), LanguageId::Jsx); assert_eq!( "typescript".parse::().unwrap(), LanguageId::TypeScript ); assert_eq!( "typescriptreact".parse::().unwrap(), LanguageId::Tsx ); assert_eq!("tsx".parse::().unwrap(), LanguageId::Tsx); assert_eq!("json".parse::().unwrap(), LanguageId::Json); assert_eq!("jsonc".parse::().unwrap(), LanguageId::JsonC); assert_eq!( "markdown".parse::().unwrap(), LanguageId::Markdown ); assert_eq!("rust".parse::().unwrap(), LanguageId::Unknown); } #[test] fn test_document_cache_contains() { 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, LanguageId::TypeScript, Arc::new("console.log(\"Hello Deno\");\n".to_string()), ); assert!(document_cache.contains_key(&specifier)); assert!(!document_cache.contains_key(&missing_specifier)); } #[test] 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, LanguageId::TypeScript, Arc::new("console.log(\"Hello deno\");\n".to_string()), ); document_cache .change( &specifier, 2, vec![lsp::TextDocumentContentChangeEvent { range: Some(lsp::Range { start: lsp::Position { line: 0, character: 19, }, end: lsp::Position { line: 0, character: 20, }, }), range_length: Some(1), text: "D".to_string(), }], ) .expect("failed to make changes"); let actual = document_cache .content(&specifier) .expect("failed to get content") .to_string(); assert_eq!(actual, "console.log(\"Hello Deno\");\n"); } #[test] 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, LanguageId::TypeScript, Arc::new("console.log(\"Hello 🦕\");\n".to_string()), ); document_cache .change( &specifier, 2, vec![lsp::TextDocumentContentChangeEvent { range: Some(lsp::Range { start: lsp::Position { line: 0, character: 19, }, end: lsp::Position { line: 0, character: 21, }, }), range_length: Some(2), text: "Deno".to_string(), }], ) .expect("failed to make changes"); let actual = document_cache .content(&specifier) .expect("failed to get content") .to_string(); assert_eq!(actual, "console.log(\"Hello Deno\");\n"); } #[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, "typescript".parse().unwrap(), Arc::new("console.log(\"hello world\");\n".to_string()), ); assert!(document_cache.is_diagnosable(&specifier)); let specifier = resolve_url("file:///a/file.rs").unwrap(); document_cache.open( specifier.clone(), 1, "rust".parse().unwrap(), Arc::new("pub mod a;".to_string()), ); 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)); } }