diff options
Diffstat (limited to 'cli/lsp/documents.rs')
-rw-r--r-- | cli/lsp/documents.rs | 1287 |
1 files changed, 930 insertions, 357 deletions
diff --git a/cli/lsp/documents.rs b/cli/lsp/documents.rs index 3855150e7..640a48f14 100644 --- a/cli/lsp/documents.rs +++ b/cli/lsp/documents.rs @@ -1,25 +1,75 @@ // Copyright 2018-2021 the Deno authors. All rights reserved. MIT license. -use super::analysis; -use super::document_source::DocumentSource; +use super::resolver::ImportMapResolver; use super::text::LineIndex; use super::tsc; +use crate::file_fetcher::get_source_from_bytes; +use crate::file_fetcher::map_content_type; +use crate::file_fetcher::SUPPORTED_SCHEMES; +use crate::http_cache; +use crate::http_cache::HttpCache; +use crate::text_encoding; + use deno_ast::MediaType; +use deno_ast::SourceTextInfo; use deno_core::error::custom_error; use deno_core::error::AnyError; +use deno_core::parking_lot::Mutex; +use deno_core::url; use deno_core::ModuleSpecifier; use lspower::lsp; use std::collections::HashMap; use std::collections::HashSet; +use std::fs; use std::ops::Range; +use std::path::Path; +use std::path::PathBuf; use std::str::FromStr; use std::sync::Arc; +use std::time::SystemTime; + +lazy_static::lazy_static! { + static ref JS_HEADERS: HashMap<String, String> = ([ + ("content-type".to_string(), "application/javascript".to_string()) + ]).iter().cloned().collect(); + static ref JSX_HEADERS: HashMap<String, String> = ([ + ("content-type".to_string(), "text/jsx".to_string()) + ]).iter().cloned().collect(); + static ref TS_HEADERS: HashMap<String, String> = ([ + ("content-type".to_string(), "application/typescript".to_string()) + ]).iter().cloned().collect(); + static ref TSX_HEADERS: HashMap<String, String> = ([ + ("content-type".to_string(), "text/tsx".to_string()) + ]).iter().cloned().collect(); +} -/// 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 { +/// The default parser from `deno_graph` does not include the configuration +/// options we require here, and so implementing an empty struct that provides +/// the trait. +#[derive(Debug, Default)] +struct SourceParser {} + +impl deno_graph::SourceParser for SourceParser { + fn parse_module( + &self, + specifier: &ModuleSpecifier, + source: Arc<String>, + media_type: MediaType, + ) -> Result<deno_ast::ParsedSource, deno_ast::Diagnostic> { + deno_ast::parse_module(deno_ast::ParseParams { + specifier: specifier.to_string(), + source: SourceTextInfo::new(source), + media_type, + capture_tokens: true, + scope_analysis: true, + maybe_syntax: None, + }) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) enum LanguageId { JavaScript, Jsx, TypeScript, @@ -30,17 +80,34 @@ pub enum LanguageId { Unknown, } +impl LanguageId { + fn as_headers(&self) -> Option<&HashMap<String, String>> { + match self { + Self::JavaScript => Some(&JS_HEADERS), + Self::Jsx => Some(&JSX_HEADERS), + Self::TypeScript => Some(&TS_HEADERS), + Self::Tsx => Some(&TSX_HEADERS), + _ => None, + } + } + + fn is_diagnosable(&self) -> bool { + matches!( + self, + Self::JavaScript | Self::Jsx | Self::TypeScript | Self::Tsx + ) + } +} + impl FromStr for LanguageId { type Err = AnyError; fn from_str(s: &str) -> Result<Self, Self::Err> { match s { "javascript" => Ok(Self::JavaScript), - "javascriptreact" => Ok(Self::Jsx), - "jsx" => Ok(Self::Jsx), + "javascriptreact" | "jsx" => Ok(Self::Jsx), "typescript" => Ok(Self::TypeScript), - "typescriptreact" => Ok(Self::Tsx), - "tsx" => Ok(Self::Tsx), + "typescriptreact" | "tsx" => Ok(Self::Tsx), "json" => Ok(Self::Json), "jsonc" => Ok(Self::JsonC), "markdown" => Ok(Self::Markdown), @@ -49,21 +116,6 @@ impl FromStr for LanguageId { } } -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, @@ -79,52 +131,70 @@ impl IndexValid { } } -#[derive(Debug, Clone)] -pub struct DocumentData { - source: DocumentSource, - dependencies: Option<HashMap<String, analysis::Dependency>>, - dependency_ranges: Option<analysis::DependencyRanges>, - pub(crate) language_id: LanguageId, - maybe_navigation_tree: Option<tsc::NavigationTree>, +#[derive(Debug)] +pub(crate) struct Document { + line_index: Arc<LineIndex>, + maybe_language_id: Option<LanguageId>, + maybe_lsp_version: Option<i32>, + maybe_module: + Option<Result<deno_graph::Module, deno_graph::ModuleGraphError>>, + maybe_navigation_tree: Option<Arc<tsc::NavigationTree>>, + maybe_warning: Option<String>, + source: SourceTextInfo, specifier: ModuleSpecifier, - version: Option<i32>, + version: String, } -impl DocumentData { - pub fn new( +impl Document { + fn new( specifier: ModuleSpecifier, - version: i32, - language_id: LanguageId, - source_text: Arc<String>, + version: String, + maybe_headers: Option<&HashMap<String, String>>, + content: Arc<String>, + maybe_resolver: Option<&dyn deno_graph::source::Resolver>, ) -> Self { - let line_index = LineIndex::new(&source_text); + let maybe_warning = maybe_headers + .map(|h| h.get("x-deno-warning").cloned()) + .flatten(); + let parser = SourceParser::default(); + // we only ever do `Document::new` on on disk resources that are supposed to + // be diagnosable, unlike `Document::open`, so it is safe to unconditionally + // parse the module. + let maybe_module = Some(deno_graph::parse_module( + &specifier, + maybe_headers, + content.clone(), + maybe_resolver, + Some(&parser), + )); + let source = SourceTextInfo::new(content); + let line_index = Arc::new(LineIndex::new(source.text_str())); Self { - source: DocumentSource::new( - &specifier, - MediaType::from(&language_id), - source_text, - line_index, - ), - dependencies: None, - dependency_ranges: None, - language_id, + line_index, + maybe_language_id: None, + maybe_lsp_version: None, + maybe_module, maybe_navigation_tree: None, + maybe_warning, + source, specifier, - version: Some(version), + version, } } - pub fn apply_content_changes( + fn change( &mut self, - content_changes: Vec<lsp::TextDocumentContentChangeEvent>, + version: i32, + changes: Vec<lsp::TextDocumentContentChangeEvent>, + maybe_resolver: Option<&dyn deno_graph::source::Resolver>, ) -> Result<(), AnyError> { - let mut content = self.source.text_info().text_str().to_string(); - let mut line_index = self.source.line_index().clone(); + let mut content = self.source.text_str().to_string(); + let mut line_index = self.line_index.clone(); let mut index_valid = IndexValid::All; - for change in content_changes { + for change in changes { if let Some(range) = change.range { if !index_valid.covers(range.start.line) { - line_index = LineIndex::new(&content); + line_index = Arc::new(LineIndex::new(&content)); } index_valid = IndexValid::UpTo(range.start.line); let range = line_index.get_text_range(range)?; @@ -134,446 +204,949 @@ impl DocumentData { index_valid = IndexValid::UpTo(0); } } - let line_index = if index_valid == IndexValid::All { + let content = Arc::new(content); + if self + .maybe_language_id + .as_ref() + .map(|li| li.is_diagnosable()) + .unwrap_or(false) + { + let maybe_headers = self + .maybe_language_id + .as_ref() + .map(|li| li.as_headers()) + .flatten(); + let parser = SourceParser::default(); + self.maybe_module = Some(deno_graph::parse_module( + &self.specifier, + maybe_headers, + content.clone(), + maybe_resolver, + Some(&parser), + )); + } else { + self.maybe_module = None; + } + self.source = SourceTextInfo::new(content); + self.line_index = if index_valid == IndexValid::All { line_index } else { - LineIndex::new(&content) + Arc::new(LineIndex::new(self.source.text_str())) }; - self.source = DocumentSource::new( - &self.specifier, - MediaType::from(&self.language_id), - Arc::new(content), - line_index, - ); + self.maybe_lsp_version = Some(version); self.maybe_navigation_tree = None; Ok(()) } - pub fn source(&self) -> &DocumentSource { - &self.source + fn close(&mut self) { + self.maybe_lsp_version = None; + self.maybe_language_id = None; } - /// 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<analysis::DependencyRange> { - let import_ranges = self.dependency_ranges.as_ref()?; - import_ranges.contains(position) + fn content(&self) -> Arc<String> { + self.source.text() + } + + fn is_diagnosable(&self) -> bool { + matches!( + self.media_type(), + MediaType::JavaScript + | MediaType::Jsx + | MediaType::TypeScript + | MediaType::Tsx + | MediaType::Dts + ) + } + + fn is_open(&self) -> bool { + self.maybe_lsp_version.is_some() + } + + fn media_type(&self) -> MediaType { + if let Some(Ok(module)) = &self.maybe_module { + module.media_type + } else { + MediaType::from(&self.specifier) + } + } + + fn open( + specifier: ModuleSpecifier, + version: i32, + language_id: LanguageId, + content: Arc<String>, + maybe_resolver: Option<&dyn deno_graph::source::Resolver>, + ) -> Self { + let maybe_headers = language_id.as_headers(); + let parser = SourceParser::default(); + let maybe_module = if language_id.is_diagnosable() { + Some(deno_graph::parse_module( + &specifier, + maybe_headers, + content.clone(), + maybe_resolver, + Some(&parser), + )) + } else { + None + }; + let source = SourceTextInfo::new(content); + let line_index = Arc::new(LineIndex::new(source.text_str())); + Self { + line_index, + maybe_language_id: Some(language_id), + maybe_lsp_version: Some(version), + maybe_module, + maybe_navigation_tree: None, + maybe_warning: None, + source, + specifier, + version: "1".to_string(), + } } } -#[derive(Debug, Clone, Default)] -pub struct DocumentCache { - dependents_graph: HashMap<ModuleSpecifier, HashSet<ModuleSpecifier>>, - pub(crate) docs: HashMap<ModuleSpecifier, DocumentData>, +pub(crate) fn to_hover_text( + result: &Result< + (ModuleSpecifier, deno_graph::Range), + deno_graph::ResolutionError, + >, +) -> String { + match result { + Ok((specifier, _)) => match specifier.scheme() { + "data" => "_(a data url)_".to_string(), + "blob" => "_(a blob url)_".to_string(), + _ => format!( + "{}​{}", + specifier[..url::Position::AfterScheme].to_string(), + specifier[url::Position::AfterScheme..].to_string() + ), + }, + Err(_) => "_[errored]_".to_string(), + } } -impl DocumentCache { - /// Calculate a graph of dependents and set it on the structure. +pub(crate) fn to_lsp_range(range: &deno_graph::Range) -> lsp::Range { + lsp::Range { + start: lsp::Position { + line: range.start.line as u32, + character: range.start.character as u32, + }, + end: lsp::Position { + line: range.end.line as u32, + character: range.end.character as u32, + }, + } +} + +/// Recurse and collect specifiers that appear in the dependent map. +fn recurse_dependents( + specifier: &ModuleSpecifier, + map: &HashMap<ModuleSpecifier, HashSet<ModuleSpecifier>>, + dependents: &mut HashSet<ModuleSpecifier>, +) { + if let Some(deps) = map.get(specifier) { + for dep in deps { + if !dependents.contains(dep) { + dependents.insert(dep.clone()); + recurse_dependents(dep, map, dependents); + } + } + } +} + +#[derive(Debug, Default)] +struct Inner { + /// The DENO_DIR that the documents looks for non-file based modules. + cache: HttpCache, + /// A flag that indicates that stated data is potentially invalid and needs to + /// be recalculated before being considered valid. + dirty: bool, + /// A map where the key is a specifier and the value is a set of specifiers + /// that depend on the key. + dependents_map: HashMap<ModuleSpecifier, HashSet<ModuleSpecifier>>, + /// A map of documents that can either be "open" in the language server, or + /// just present on disk. + docs: HashMap<ModuleSpecifier, Document>, + /// The optional import map that should be used when resolving dependencies. + maybe_import_map: Option<ImportMapResolver>, + redirects: HashMap<ModuleSpecifier, ModuleSpecifier>, +} + +impl Inner { + fn new(location: &Path) -> Self { + Self { + cache: HttpCache::new(location), + dirty: true, + dependents_map: HashMap::default(), + docs: HashMap::default(), + maybe_import_map: None, + redirects: HashMap::default(), + } + } + + /// Adds a document by reading the document from the file system. + fn add(&mut self, specifier: ModuleSpecifier) -> Option<Document> { + let version = self.calculate_version(&specifier)?; + let path = self.get_path(&specifier)?; + let bytes = fs::read(path).ok()?; + let doc = if specifier.scheme() == "file" { + let maybe_charset = + Some(text_encoding::detect_charset(&bytes).to_string()); + let content = Arc::new(get_source_from_bytes(bytes, maybe_charset).ok()?); + Document::new( + specifier.clone(), + version, + None, + content, + self.maybe_import_map.as_ref().map(|r| r.as_resolver()), + ) + } else { + let cache_filename = self.cache.get_cache_filename(&specifier)?; + let metadata = http_cache::Metadata::read(&cache_filename).ok()?; + let maybe_content_type = metadata.headers.get("content-type").cloned(); + let maybe_headers = Some(&metadata.headers); + let (_, maybe_charset) = map_content_type(&specifier, maybe_content_type); + let content = Arc::new(get_source_from_bytes(bytes, maybe_charset).ok()?); + Document::new( + specifier.clone(), + version, + maybe_headers, + content, + self.maybe_import_map.as_ref().map(|r| r.as_resolver()), + ) + }; + self.dirty = true; + self.docs.insert(specifier, doc) + } + + /// Iterate through the documents, building a map where the key is a unique + /// document and the value is a set of specifiers that depend on that + /// document. fn calculate_dependents(&mut self) { - let mut dependents_graph: HashMap< - ModuleSpecifier, - HashSet<ModuleSpecifier>, - > = 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()) + let mut dependents_map: HashMap<ModuleSpecifier, HashSet<ModuleSpecifier>> = + HashMap::new(); + for (specifier, doc) in &self.docs { + if let Some(Ok(module)) = &doc.maybe_module { + for dependency in module.dependencies.values() { + if let Some(dep) = dependency.get_code() { + dependents_map + .entry(dep.clone()) .or_default() .insert(specifier.clone()); } - if let Some(analysis::ResolvedDependency::Resolved(dep_specifier)) = - &dependency.maybe_type - { - dependents_graph - .entry(dep_specifier.clone()) + if let Some(dep) = dependency.get_type() { + dependents_map + .entry(dep.clone()) .or_default() .insert(specifier.clone()); } } + if let Some((_, Some(Ok((dep, _))))) = &module.maybe_types_dependency { + dependents_map + .entry(dep.clone()) + .or_default() + .insert(specifier.clone()); + } } } - self.dependents_graph = dependents_graph; + self.dependents_map = dependents_map; } - pub fn change( + fn calculate_version(&self, specifier: &ModuleSpecifier) -> Option<String> { + let path = self.get_path(specifier)?; + let metadata = fs::metadata(path).ok()?; + if let Ok(modified) = metadata.modified() { + if let Ok(n) = modified.duration_since(SystemTime::UNIX_EPOCH) { + Some(n.as_millis().to_string()) + } else { + Some("1".to_string()) + } + } else { + Some("1".to_string()) + } + } + + fn change( &mut self, specifier: &ModuleSpecifier, version: i32, - content_changes: Vec<lsp::TextDocumentContentChangeEvent>, + changes: Vec<lsp::TextDocumentContentChangeEvent>, ) -> 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).map_or_else( + || { + Err(custom_error( + "NotFound", + format!("The specifier \"{}\" was not found.", specifier), + )) + }, + Ok, + )?; + self.dirty = true; + doc.change( + version, + changes, + self.maybe_import_map.as_ref().map(|r| r.as_resolver()), + ) + } - let doc = self.docs.get_mut(specifier).unwrap(); - doc.apply_content_changes(content_changes)?; - doc.version = Some(version); + fn close(&mut self, specifier: &ModuleSpecifier) -> Result<(), AnyError> { + let doc = self.docs.get_mut(specifier).map_or_else( + || { + Err(custom_error( + "NotFound", + format!("The specifier \"{}\" was not found.", specifier), + )) + }, + Ok, + )?; + doc.close(); + self.dirty = true; Ok(()) } - pub fn close(&mut self, specifier: &ModuleSpecifier) { - self.docs.remove(specifier); - self.calculate_dependents(); + fn contains_import( + &mut self, + specifier: &str, + referrer: &ModuleSpecifier, + ) -> bool { + let maybe_resolver = + self.maybe_import_map.as_ref().map(|im| im.as_resolver()); + let maybe_specifier = if let Some(resolver) = maybe_resolver { + resolver.resolve(specifier, referrer).ok() + } else { + deno_core::resolve_import(specifier, referrer.as_str()).ok() + }; + if let Some(import_specifier) = maybe_specifier { + self.contains_specifier(&import_specifier) + } else { + false + } } - pub fn contains_key(&self, specifier: &ModuleSpecifier) -> bool { - self.docs.contains_key(specifier) + fn contains_specifier(&mut self, specifier: &ModuleSpecifier) -> bool { + let specifier = self + .resolve_specifier(specifier) + .unwrap_or_else(|| specifier.clone()); + if !self.is_valid(&specifier) { + self.add(specifier.clone()); + } + self.docs.contains_key(&specifier) } - pub fn get(&self, specifier: &ModuleSpecifier) -> Option<&DocumentData> { - self.docs.get(specifier) + fn content(&mut self, specifier: &ModuleSpecifier) -> Option<Arc<String>> { + self.get(specifier).map(|d| d.content()) } - pub fn content(&self, specifier: &ModuleSpecifier) -> Option<Arc<String>> { - self - .docs - .get(specifier) - .map(|d| d.source().text_info().text()) + fn dependencies( + &mut self, + specifier: &ModuleSpecifier, + ) -> Option<Vec<(String, deno_graph::Dependency)>> { + let doc = self.get(specifier)?; + let module = doc.maybe_module.as_ref()?.as_ref().ok()?; + Some( + module + .dependencies + .iter() + .map(|(s, d)| (s.clone(), d.clone())) + .collect(), + ) } - // For a given specifier, get all open documents which directly or indirectly - // depend upon the specifier. - pub fn dependents( - &self, + fn dependents( + &mut self, specifier: &ModuleSpecifier, ) -> Vec<ModuleSpecifier> { + if self.dirty { + self.calculate_dependents(); + self.dirty = false; + } let mut dependents = HashSet::new(); - self.recurse_dependents(specifier, &mut dependents); - dependents.into_iter().collect() + if let Some(specifier) = self.resolve_specifier(specifier) { + recurse_dependents(&specifier, &self.dependents_map, &mut dependents); + dependents.into_iter().collect() + } else { + vec![] + } } - pub fn dependencies( - &self, + fn get(&mut self, specifier: &ModuleSpecifier) -> Option<&Document> { + let specifier = self.resolve_specifier(specifier)?; + if !self.is_valid(&specifier) { + self.add(specifier.clone()); + } + self.docs.get(&specifier) + } + + fn get_maybe_dependency( + &mut self, specifier: &ModuleSpecifier, - ) -> Option<HashMap<String, analysis::Dependency>> { - self - .docs - .get(specifier) - .map(|doc| doc.dependencies.clone()) - .flatten() + position: &lsp::Position, + ) -> Option<(String, deno_graph::Dependency, deno_graph::Range)> { + let doc = self.get(specifier)?; + let module = doc.maybe_module.as_ref()?.as_ref().ok()?; + let position = deno_graph::Position { + line: position.line as usize, + character: position.character as usize, + }; + module.dependencies.iter().find_map(|(s, dep)| { + dep + .includes(&position) + .map(|r| (s.clone(), dep.clone(), r.clone())) + }) } - pub fn get_navigation_tree( - &self, + fn get_navigation_tree( + &mut self, specifier: &ModuleSpecifier, - ) -> Option<tsc::NavigationTree> { + ) -> Option<Arc<tsc::NavigationTree>> { self - .docs .get(specifier) - .map(|doc| doc.maybe_navigation_tree.clone()) + .map(|d| d.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 - ) + fn get_path(&self, specifier: &ModuleSpecifier) -> Option<PathBuf> { + if specifier.scheme() == "file" { + specifier.to_file_path().ok() + } else { + let path = self.cache.get_cache_filename(specifier)?; + if path.is_file() { + Some(path) + } else { + None + } + } + } + + fn is_diagnosable(&mut self, specifier: &ModuleSpecifier) -> bool { + if let Some(doc) = self.get(specifier) { + doc.is_diagnosable() } else { false } } - /// Determines if the specifier can be processed for formatting. - pub fn is_formattable(&self, specifier: &ModuleSpecifier) -> bool { - self.docs.contains_key(specifier) + fn is_formattable(&mut self, specifier: &ModuleSpecifier) -> bool { + // currently any document that is open in the language server is formattable + self.is_open(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<analysis::DependencyRange> { - let document = self.docs.get(specifier)?; - document.is_specifier_position(position) + fn is_open(&mut self, specifier: &ModuleSpecifier) -> bool { + let specifier = self + .resolve_specifier(specifier) + .unwrap_or_else(|| specifier.clone()); + // this does not use `self.get` since that lazily adds documents, and we + // only care about documents already in the cache. + if let Some(doc) = self.docs.get(&specifier) { + doc.is_open() + } else { + false + } } - pub fn len(&self) -> usize { - self.docs.len() + fn is_valid(&mut self, specifier: &ModuleSpecifier) -> bool { + if self.is_open(specifier) { + true + } else if let Some(specifier) = self.resolve_specifier(specifier) { + self.docs.get(&specifier).map(|d| d.version.clone()) + == self.calculate_version(&specifier) + } else { + // even though it isn't valid, it just can't exist, so we will say it is + // valid + true + } } - pub fn line_index(&self, specifier: &ModuleSpecifier) -> Option<LineIndex> { + fn line_index( + &mut self, + specifier: &ModuleSpecifier, + ) -> Option<Arc<LineIndex>> { + let specifier = self.resolve_specifier(specifier)?; + self.docs.get(&specifier).map(|doc| doc.line_index.clone()) + } + + fn lsp_version(&self, specifier: &ModuleSpecifier) -> Option<i32> { self .docs .get(specifier) - .map(|d| d.source().line_index().clone()) + .map(|doc| doc.maybe_lsp_version) + .flatten() } - pub fn open( + fn maybe_warning(&mut self, specifier: &ModuleSpecifier) -> Option<String> { + self + .get(specifier) + .map(|d| d.maybe_warning.clone()) + .flatten() + } + + fn open( &mut self, specifier: ModuleSpecifier, version: i32, language_id: LanguageId, - source: Arc<String>, + content: Arc<String>, ) { - self.docs.insert( + let document_data = Document::open( specifier.clone(), - DocumentData::new(specifier, version, language_id, source), + version, + language_id, + content, + self.maybe_import_map.as_ref().map(|r| r.as_resolver()), ); + self.docs.insert(specifier, document_data); + self.dirty = true; } - pub fn open_specifiers(&self) -> Vec<&ModuleSpecifier> { + fn parsed_source( + &mut self, + specifier: &ModuleSpecifier, + ) -> Option<Result<deno_ast::ParsedSource, deno_graph::ModuleGraphError>> { self - .docs - .iter() - .filter_map(|(key, data)| { - if data.version.is_some() { - Some(key) + .get(specifier) + .map(|doc| { + doc.maybe_module.as_ref().map(|r| { + r.as_ref() + .map(|m| m.parsed_source.clone()) + .map_err(|err| err.clone()) + }) + }) + .flatten() + } + + fn resolve( + &mut self, + specifiers: Vec<String>, + referrer: &ModuleSpecifier, + ) -> Option<Vec<Option<(ModuleSpecifier, MediaType)>>> { + let doc = self.get(referrer)?; + let mut results = Vec::new(); + if let Some(Ok(module)) = &doc.maybe_module { + let dependencies = module.dependencies.clone(); + for specifier in specifiers { + if specifier.starts_with("asset:") { + if let Ok(specifier) = ModuleSpecifier::parse(&specifier) { + let media_type = MediaType::from(&specifier); + results.push(Some((specifier, media_type))); + } else { + results.push(None); + } + } else if let Some(dep) = dependencies.get(&specifier) { + if let Some(Ok((specifier, _))) = &dep.maybe_type { + results.push(self.resolve_dependency(specifier)); + } else if let Some(Ok((specifier, _))) = &dep.maybe_code { + results.push(self.resolve_dependency(specifier)); + } else { + results.push(None); + } } else { - None + results.push(None); } + } + } + Some(results) + } + + fn resolve_dependency( + &mut self, + specifier: &ModuleSpecifier, + ) -> Option<(ModuleSpecifier, MediaType)> { + let doc = self.get(specifier)?; + let maybe_module = + doc.maybe_module.as_ref().map(|r| r.as_ref().ok()).flatten(); + let maybe_types_dependency = maybe_module + .map(|m| { + m.maybe_types_dependency + .as_ref() + .map(|(_, o)| o.as_ref().map(|r| r.as_ref().ok()).flatten()) + .flatten() }) - .collect() + .flatten() + .cloned(); + if let Some((specifier, _)) = maybe_types_dependency { + self.resolve_dependency(&specifier) + } else { + let media_type = doc.media_type(); + Some((specifier.clone(), media_type)) + } } - fn recurse_dependents( + fn resolve_remote_specifier( &self, specifier: &ModuleSpecifier, - dependents: &mut HashSet<ModuleSpecifier>, - ) { - 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); - } + redirect_limit: usize, + ) -> Option<ModuleSpecifier> { + let cache_filename = self.cache.get_cache_filename(specifier)?; + if redirect_limit > 0 && cache_filename.is_file() { + let headers = http_cache::Metadata::read(&cache_filename) + .ok() + .map(|m| m.headers)?; + if let Some(location) = headers.get("location") { + let redirect = + deno_core::resolve_import(location, specifier.as_str()).ok()?; + self.resolve_remote_specifier(&redirect, redirect_limit - 1) + } else { + Some(specifier.clone()) } + } else { + None } } - pub fn set_dependencies( + fn resolve_specifier( &mut self, specifier: &ModuleSpecifier, - maybe_dependencies: Option<HashMap<String, analysis::Dependency>>, - maybe_dependency_ranges: Option<analysis::DependencyRanges>, - ) -> 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(()) + ) -> Option<ModuleSpecifier> { + let scheme = specifier.scheme(); + if !SUPPORTED_SCHEMES.contains(&scheme) { + return None; + } + + if scheme == "data" || scheme == "blob" || scheme == "file" { + Some(specifier.clone()) + } else if let Some(specifier) = self.redirects.get(specifier) { + Some(specifier.clone()) } else { - Err(custom_error( - "NotFound", - format!( - "The specifier (\"{}\") does not exist in the document cache.", - specifier - ), - )) + let redirect = self.resolve_remote_specifier(specifier, 10)?; + self.redirects.insert(specifier.clone(), redirect.clone()); + Some(redirect) } } - pub fn set_navigation_tree( + fn set_import_map( + &mut self, + maybe_import_map: Option<Arc<import_map::ImportMap>>, + ) { + // TODO update resolved dependencies? + self.maybe_import_map = maybe_import_map.map(ImportMapResolver::new); + self.dirty = true; + } + + fn set_location(&mut self, location: PathBuf) { + // TODO update resolved dependencies? + self.cache = HttpCache::new(&location); + self.dirty = true; + } + + fn set_navigation_tree( &mut self, specifier: &ModuleSpecifier, - navigation_tree: tsc::NavigationTree, + navigation_tree: Arc<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 - ), - )) - } + let doc = self.docs.get_mut(specifier).ok_or_else(|| { + custom_error("NotFound", format!("Specifier not found {}", specifier)) + })?; + doc.maybe_navigation_tree = Some(navigation_tree); + Ok(()) + } + + fn specifiers( + &self, + open_only: bool, + diagnosable_only: bool, + ) -> Vec<ModuleSpecifier> { + self + .docs + .iter() + .filter_map(|(specifier, doc)| { + let open = open_only && doc.is_open(); + let diagnosable = diagnosable_only && doc.is_diagnosable(); + if (!open_only || open) && (!diagnosable_only || diagnosable) { + Some(specifier.clone()) + } else { + None + } + }) + .collect() + } + + fn text_info( + &mut self, + specifier: &ModuleSpecifier, + ) -> Option<SourceTextInfo> { + self.get(specifier).map(|d| d.source.clone()) + } + + fn version(&mut self, specifier: &ModuleSpecifier) -> Option<String> { + self.get(specifier).map(|d| { + d.maybe_lsp_version + .map_or_else(|| d.version.clone(), |v| v.to_string()) + }) + } +} + +#[derive(Debug, Clone, Default)] +pub(crate) struct Documents(Arc<Mutex<Inner>>); + +impl Documents { + pub fn new(location: &Path) -> Self { + Self(Arc::new(Mutex::new(Inner::new(location)))) + } + + /// Apply language server content changes to an open document. + pub fn change( + &self, + specifier: &ModuleSpecifier, + version: i32, + changes: Vec<lsp::TextDocumentContentChangeEvent>, + ) -> Result<(), AnyError> { + self.0.lock().change(specifier, version, changes) + } + + /// Close an open document, this essentially clears any editor state that is + /// being held, and the document store will revert to the file system if + /// information about the document is required. + pub fn close(&self, specifier: &ModuleSpecifier) -> Result<(), AnyError> { + self.0.lock().close(specifier) + } + + /// Return `true` if the provided specifier can be resolved to a document, + /// otherwise `false`. + pub fn contains_import( + &self, + specifier: &str, + referrer: &ModuleSpecifier, + ) -> bool { + self.0.lock().contains_import(specifier, referrer) + } + + /// Return `true` if the specifier can be resolved to a document. + pub fn contains_specifier(&self, specifier: &ModuleSpecifier) -> bool { + self.0.lock().contains_specifier(specifier) + } + + /// If the specifier can be resolved to a document, return its current + /// content, otherwise none. + pub fn content(&self, specifier: &ModuleSpecifier) -> Option<Arc<String>> { + self.0.lock().content(specifier) + } + + /// Return an optional vector of dependencies for a given specifier. + pub fn dependencies( + &self, + specifier: &ModuleSpecifier, + ) -> Option<Vec<(String, deno_graph::Dependency)>> { + self.0.lock().dependencies(specifier) + } + + /// Return an array of specifiers, if any, that are dependent upon the + /// supplied specifier. This is used to determine invalidation of diagnostics + /// when a module has been changed. + pub fn dependents( + &self, + specifier: &ModuleSpecifier, + ) -> Vec<ModuleSpecifier> { + self.0.lock().dependents(specifier) + } + + /// If the supplied position is within a dependency range, return the resolved + /// string specifier for the dependency, the resolved dependency and the range + /// in the source document of the specifier. + pub fn get_maybe_dependency( + &self, + specifier: &ModuleSpecifier, + position: &lsp::Position, + ) -> Option<(String, deno_graph::Dependency, deno_graph::Range)> { + self.0.lock().get_maybe_dependency(specifier, position) + } + + /// Get a reference to the navigation tree stored for a given specifier, if + /// any. + pub fn get_navigation_tree( + &self, + specifier: &ModuleSpecifier, + ) -> Option<Arc<tsc::NavigationTree>> { + self.0.lock().get_navigation_tree(specifier) + } + + /// Indicates that a specifier is able to be diagnosed by the language server + pub fn is_diagnosable(&self, specifier: &ModuleSpecifier) -> bool { + self.0.lock().is_diagnosable(specifier) + } + + /// Indicates that a specifier is formattable. + pub fn is_formattable(&self, specifier: &ModuleSpecifier) -> bool { + self.0.lock().is_formattable(specifier) + } + + /// Return a reference to the line index for a given specifiers, if any. + pub fn line_index( + &self, + specifier: &ModuleSpecifier, + ) -> Option<Arc<LineIndex>> { + self.0.lock().line_index(specifier) + } + + /// Return the current language server client version for a given specifier, + /// if any. + pub fn lsp_version(&self, specifier: &ModuleSpecifier) -> Option<i32> { + self.0.lock().lsp_version(specifier) + } + + /// Return a warning header for a given specifier, if present. + pub fn maybe_warning(&self, specifier: &ModuleSpecifier) -> Option<String> { + self.0.lock().maybe_warning(specifier) + } + + /// "Open" a document from the perspective of the editor, meaning that + /// requests for information from the document will come from the in-memory + /// representation received from the language server client, versus reading + /// information from the disk. + pub fn open( + &self, + specifier: ModuleSpecifier, + version: i32, + language_id: LanguageId, + content: Arc<String>, + ) { + self.0.lock().open(specifier, version, language_id, content) + } + + /// Return the parsed source or the module graph error for a given specifier. + pub fn parsed_source( + &self, + specifier: &ModuleSpecifier, + ) -> Option<Result<deno_ast::ParsedSource, deno_graph::ModuleGraphError>> { + self.0.lock().parsed_source(specifier) + } + + /// For a given set of string specifiers, resolve each one from the graph, + /// for a given referrer. This is used to provide resolution information to + /// tsc when type checking. + pub fn resolve( + &self, + specifiers: Vec<String>, + referrer: &ModuleSpecifier, + ) -> Option<Vec<Option<(ModuleSpecifier, MediaType)>>> { + self.0.lock().resolve(specifiers, referrer) + } + + /// Set the optional import map for the document cache. + pub fn set_import_map( + &self, + maybe_import_map: Option<Arc<import_map::ImportMap>>, + ) { + self.0.lock().set_import_map(maybe_import_map); } - pub fn specifiers(&self) -> Vec<ModuleSpecifier> { - self.docs.keys().cloned().collect() + /// Update the location of the on disk cache for the document store. + pub fn set_location(&self, location: PathBuf) { + self.0.lock().set_location(location) } - pub fn version(&self, specifier: &ModuleSpecifier) -> Option<i32> { - self.docs.get(specifier).and_then(|doc| doc.version) + /// Set a navigation tree that is associated with the provided specifier. + pub fn set_navigation_tree( + &self, + specifier: &ModuleSpecifier, + navigation_tree: Arc<tsc::NavigationTree>, + ) -> Result<(), AnyError> { + self + .0 + .lock() + .set_navigation_tree(specifier, navigation_tree) + } + + /// Return a vector of specifiers that are contained in the document store, + /// where `open_only` flag would provide only those documents currently open + /// in the editor and `diagnosable_only` would provide only those documents + /// that the language server can provide diagnostics for. + pub fn specifiers( + &self, + open_only: bool, + diagnosable_only: bool, + ) -> Vec<ModuleSpecifier> { + self.0.lock().specifiers(open_only, diagnosable_only) + } + + /// Return the current text info for a given specifier. + pub fn text_info( + &self, + specifier: &ModuleSpecifier, + ) -> Option<SourceTextInfo> { + self.0.lock().text_info(specifier) + } + + /// Return the version of a document in the document cache. + pub fn version(&self, specifier: &ModuleSpecifier) -> Option<String> { + self.0.lock().version(specifier) } } #[cfg(test)] mod tests { use super::*; - use deno_core::resolve_url; - use lspower::lsp; + use tempfile::TempDir; - #[test] - fn test_language_id() { - assert_eq!( - "javascript".parse::<LanguageId>().unwrap(), - LanguageId::JavaScript - ); - assert_eq!( - "javascriptreact".parse::<LanguageId>().unwrap(), - LanguageId::Jsx - ); - assert_eq!("jsx".parse::<LanguageId>().unwrap(), LanguageId::Jsx); - assert_eq!( - "typescript".parse::<LanguageId>().unwrap(), - LanguageId::TypeScript - ); - assert_eq!( - "typescriptreact".parse::<LanguageId>().unwrap(), - LanguageId::Tsx - ); - assert_eq!("tsx".parse::<LanguageId>().unwrap(), LanguageId::Tsx); - assert_eq!("json".parse::<LanguageId>().unwrap(), LanguageId::Json); - assert_eq!("jsonc".parse::<LanguageId>().unwrap(), LanguageId::JsonC); - assert_eq!( - "markdown".parse::<LanguageId>().unwrap(), - LanguageId::Markdown - ); - assert_eq!("rust".parse::<LanguageId>().unwrap(), LanguageId::Unknown); + fn setup() -> (Documents, PathBuf) { + let temp_dir = TempDir::new().unwrap(); + let location = temp_dir.path().join("deps"); + let documents = Documents::new(&location); + (documents, location) } #[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()), + fn test_documents_open() { + let (documents, _) = setup(); + let specifier = ModuleSpecifier::parse("file:///a.ts").unwrap(); + let content = Arc::new( + r#"import * as b from "./b.ts"; +console.log(b); +"# + .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( + documents.open( specifier.clone(), 1, - LanguageId::TypeScript, - Arc::new("console.log(\"Hello deno\");\n".to_string()), + "javascript".parse().unwrap(), + content, ); - 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"); + assert!(documents.is_formattable(&specifier)); + assert!(documents.is_diagnosable(&specifier)); + assert!(documents.line_index(&specifier).is_some()); } #[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( + fn test_documents_change() { + let (documents, _) = setup(); + let specifier = ModuleSpecifier::parse("file:///a.ts").unwrap(); + let content = Arc::new( + r#"import * as b from "./b.ts"; +console.log(b); +"# + .to_string(), + ); + documents.open( specifier.clone(), 1, - LanguageId::TypeScript, - Arc::new("console.log(\"Hello 🦕\");\n".to_string()), + "javascript".parse().unwrap(), + content, ); - document_cache + documents .change( &specifier, 2, vec![lsp::TextDocumentContentChangeEvent { range: Some(lsp::Range { start: lsp::Position { - line: 0, - character: 19, + line: 1, + character: 13, }, end: lsp::Position { - line: 0, - character: 21, + line: 1, + character: 13, }, }), - range_length: Some(2), - text: "Deno".to_string(), + range_length: None, + text: r#", "hello 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()), + .unwrap(); + assert_eq!( + documents.content(&specifier).unwrap().as_str(), + r#"import * as b from "./b.ts"; +console.log(b, "hello deno"); +"# ); - 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)); } } |