From 34a9ddff091950aee9d89915cd13944259e7d346 Mon Sep 17 00:00:00 2001 From: Kitson Kelly Date: Fri, 29 Oct 2021 10:56:01 +1100 Subject: [PATCH] refactor(lsp): use deno_graph and single document struct (#12535) Closes #12473 --- Cargo.lock | 13 +- cli/Cargo.toml | 4 +- cli/http_cache.rs | 1 - cli/lsp/analysis.rs | 727 +-------------- cli/lsp/code_lens.rs | 102 ++- cli/lsp/completions.rs | 60 +- cli/lsp/diagnostics.rs | 258 ++++-- cli/lsp/document_source.rs | 75 -- cli/lsp/documents.rs | 1329 ++++++++++++++++++++-------- cli/lsp/language_server.rs | 632 ++++++------- cli/lsp/mod.rs | 3 +- cli/lsp/registries.rs | 19 +- cli/lsp/resolver.rs | 33 + cli/lsp/sources.rs | 797 ----------------- cli/lsp/tsc.rs | 294 +++--- cli/lsp/urls.rs | 4 +- cli/tests/integration/lsp_tests.rs | 32 +- 17 files changed, 1673 insertions(+), 2710 deletions(-) delete mode 100644 cli/lsp/document_source.rs create mode 100644 cli/lsp/resolver.rs delete mode 100644 cli/lsp/sources.rs diff --git a/Cargo.lock b/Cargo.lock index 8eaece96f8..7324466c47 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -781,9 +781,9 @@ dependencies = [ [[package]] name = "deno_doc" -version = "0.17.1" +version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96b60f0447a17ed4fc413bd7abad29e7d31e76f13422813f2e7978b0c4bdca7a" +checksum = "81744eb79bda23580020b2df68a150197312b54c47e11160a65add0534c03ec5" dependencies = [ "cfg-if 1.0.0", "deno_ast", @@ -826,9 +826,9 @@ dependencies = [ [[package]] name = "deno_graph" -version = "0.8.2" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3d7197f490abd2a1e7d1edc3a1c4db1f4e294d7067dbbf65c4714932c7c3c70" +checksum = "d7513c22ec28dd1a4eeb82a99fc543c92d7ee2f1a146a0dd6bea7427020eb863" dependencies = [ "anyhow", "cfg-if 1.0.0", @@ -836,7 +836,7 @@ dependencies = [ "deno_ast", "futures", "lazy_static", - "parking_lot_core", + "parking_lot", "regex", "ring", "serde", @@ -1887,6 +1887,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "716d3d89f35ac6a34fd0eed635395f4c3b76fa889338a4632e5231a8684216bd" dependencies = [ "cfg-if 1.0.0", + "js-sys", + "wasm-bindgen", + "web-sys", ] [[package]] diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 3c316f9b0f..86d35a0e11 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -41,8 +41,8 @@ winres = "0.1.11" [dependencies] deno_ast = { version = "0.4.1", features = ["bundler", "codegen", "dep_graph", "module_specifier", "proposal", "react", "sourcemap", "transforms", "typescript", "view", "visit"] } deno_core = { version = "0.105.0", path = "../core" } -deno_doc = "0.17.1" -deno_graph = "0.8.2" +deno_doc = "0.18.0" +deno_graph = "0.9.1" deno_lint = { version = "0.18.1", features = ["docs"] } deno_runtime = { version = "0.31.0", path = "../runtime" } deno_tls = { version = "0.10.0", path = "../ext/tls" } diff --git a/cli/http_cache.rs b/cli/http_cache.rs index 9a6a89b563..9f76364dea 100644 --- a/cli/http_cache.rs +++ b/cli/http_cache.rs @@ -91,7 +91,6 @@ impl Metadata { Ok(()) } - #[cfg(test)] pub fn read(cache_filename: &Path) -> Result { let metadata_filename = Metadata::filename(cache_filename); let metadata = fs::read_to_string(metadata_filename)?; diff --git a/cli/lsp/analysis.rs b/cli/lsp/analysis.rs index 643de5c56f..7091316891 100644 --- a/cli/lsp/analysis.rs +++ b/cli/lsp/analysis.rs @@ -3,23 +3,10 @@ use super::language_server; use super::tsc; -use crate::ast; -use crate::ast::Location; use crate::config_file::LintConfig; -use crate::lsp::documents::DocumentData; use crate::tools::lint::create_linter; use crate::tools::lint::get_configured_rules; -use deno_ast::swc::ast as swc_ast; -use deno_ast::swc::common::comments::Comment; -use deno_ast::swc::common::BytePos; -use deno_ast::swc::common::Span; -use deno_ast::swc::common::DUMMY_SP; -use deno_ast::swc::visit::Node; -use deno_ast::swc::visit::Visit; -use deno_ast::swc::visit::VisitWith; -use deno_ast::Diagnostic; -use deno_ast::MediaType; use deno_ast::SourceTextInfo; use deno_core::error::anyhow; use deno_core::error::custom_error; @@ -27,17 +14,13 @@ use deno_core::error::AnyError; use deno_core::serde::Deserialize; use deno_core::serde_json; use deno_core::serde_json::json; -use deno_core::url; -use deno_core::ModuleResolutionError; use deno_core::ModuleSpecifier; -use import_map::ImportMap; use lspower::lsp; use lspower::lsp::Position; use lspower::lsp::Range; use regex::Regex; use std::cmp::Ordering; use std::collections::HashMap; -use std::fmt; lazy_static::lazy_static! { /// Diagnostic error codes which actually are the same, and so when grouping @@ -84,56 +67,6 @@ lazy_static::lazy_static! { const SUPPORTED_EXTENSIONS: &[&str] = &[".ts", ".tsx", ".js", ".jsx", ".mjs"]; -// TODO(@kitsonk) remove after deno_graph migration -#[derive(Debug, Clone, Eq, PartialEq)] -enum TypeScriptReference { - Path(String), - Types(String), -} - -fn match_to_span(comment: &Comment, m: ®ex::Match) -> Span { - Span { - lo: comment.span.lo + BytePos((m.start() + 1) as u32), - hi: comment.span.lo + BytePos((m.end() + 1) as u32), - ctxt: comment.span.ctxt, - } -} - -// TODO(@kitsonk) remove after deno_graph migration -fn parse_deno_types(comment: &Comment) -> Option<(String, Span)> { - let captures = DENO_TYPES_RE.captures(&comment.text)?; - if let Some(m) = captures.get(1) { - Some((m.as_str().to_string(), match_to_span(comment, &m))) - } else if let Some(m) = captures.get(2) { - Some((m.as_str().to_string(), match_to_span(comment, &m))) - } else { - unreachable!(); - } -} - -// TODO(@kitsonk) remove after deno_graph migration -fn parse_ts_reference( - comment: &Comment, -) -> Option<(TypeScriptReference, Span)> { - if !TRIPLE_SLASH_REFERENCE_RE.is_match(&comment.text) { - None - } else if let Some(captures) = PATH_REFERENCE_RE.captures(&comment.text) { - let m = captures.get(1).unwrap(); - Some(( - TypeScriptReference::Path(m.as_str().to_string()), - match_to_span(comment, &m), - )) - } else { - TYPES_REFERENCE_RE.captures(&comment.text).map(|captures| { - let m = captures.get(1).unwrap(); - ( - TypeScriptReference::Types(m.as_str().to_string()), - match_to_span(comment, &m), - ) - }) - } -} - /// Category of self-generated diagnostic messages (those not coming from) /// TypeScript. #[derive(Debug, PartialEq, Eq)] @@ -219,266 +152,6 @@ pub fn get_lint_references( ) } -#[derive(Debug, Default, Clone, PartialEq, Eq)] -pub struct Dependency { - pub is_dynamic: bool, - pub maybe_code: Option, - pub maybe_code_specifier_range: Option, - pub maybe_type: Option, - pub maybe_type_specifier_range: Option, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum ResolvedDependencyErr { - InvalidDowngrade, - InvalidLocalImport, - InvalidSpecifier(ModuleResolutionError), - Missing, -} - -impl ResolvedDependencyErr { - pub fn as_code(&self) -> lsp::NumberOrString { - match self { - Self::InvalidDowngrade => { - lsp::NumberOrString::String("invalid-downgrade".to_string()) - } - Self::InvalidLocalImport => { - lsp::NumberOrString::String("invalid-local-import".to_string()) - } - Self::InvalidSpecifier(error) => match error { - ModuleResolutionError::ImportPrefixMissing(_, _) => { - lsp::NumberOrString::String("import-prefix-missing".to_string()) - } - ModuleResolutionError::InvalidBaseUrl(_) => { - lsp::NumberOrString::String("invalid-base-url".to_string()) - } - ModuleResolutionError::InvalidPath(_) => { - lsp::NumberOrString::String("invalid-path".to_string()) - } - ModuleResolutionError::InvalidUrl(_) => { - lsp::NumberOrString::String("invalid-url".to_string()) - } - }, - Self::Missing => lsp::NumberOrString::String("missing".to_string()), - } - } -} - -impl fmt::Display for ResolvedDependencyErr { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match self { - Self::InvalidDowngrade => { - write!(f, "HTTPS modules cannot import HTTP modules.") - } - Self::InvalidLocalImport => { - write!(f, "Remote modules cannot import local modules.") - } - Self::InvalidSpecifier(err) => write!(f, "{}", err), - Self::Missing => write!(f, "The module is unexpectedly missing."), - } - } -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum ResolvedDependency { - Resolved(ModuleSpecifier), - Err(ResolvedDependencyErr), -} - -impl ResolvedDependency { - pub fn as_hover_text(&self) -> String { - match self { - Self::Resolved(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() - ), - }, - Self::Err(_) => "_[errored]_".to_string(), - } - } -} - -pub fn resolve_import( - specifier: &str, - referrer: &ModuleSpecifier, - maybe_import_map: &Option, -) -> ResolvedDependency { - let maybe_mapped = if let Some(import_map) = maybe_import_map { - import_map.resolve(specifier, referrer.as_str()).ok() - } else { - None - }; - let remapped = maybe_mapped.is_some(); - let specifier = if let Some(remapped) = maybe_mapped { - remapped - } else { - match deno_core::resolve_import(specifier, referrer.as_str()) { - Ok(resolved) => resolved, - Err(err) => { - return ResolvedDependency::Err( - ResolvedDependencyErr::InvalidSpecifier(err), - ) - } - } - }; - let referrer_scheme = referrer.scheme(); - let specifier_scheme = specifier.scheme(); - if referrer_scheme == "https" && specifier_scheme == "http" { - return ResolvedDependency::Err(ResolvedDependencyErr::InvalidDowngrade); - } - if (referrer_scheme == "https" || referrer_scheme == "http") - && !(specifier_scheme == "https" || specifier_scheme == "http") - && !remapped - { - return ResolvedDependency::Err(ResolvedDependencyErr::InvalidLocalImport); - } - - ResolvedDependency::Resolved(specifier) -} - -pub fn parse_module( - specifier: &ModuleSpecifier, - source: SourceTextInfo, - media_type: MediaType, -) -> Result { - deno_ast::parse_module(deno_ast::ParseParams { - specifier: specifier.as_str().to_string(), - source, - media_type, - // capture the tokens for linting and formatting - capture_tokens: true, - maybe_syntax: None, - scope_analysis: true, // for deno_lint - }) -} - -// TODO(@kitsonk) a lot of this logic is duplicated in module_graph.rs in -// Module::parse() and should be refactored out to a common function. -pub fn analyze_dependencies( - specifier: &ModuleSpecifier, - media_type: MediaType, - parsed_source: &deno_ast::ParsedSource, - maybe_import_map: &Option, -) -> (HashMap, Option) { - let mut maybe_type = None; - let mut dependencies = HashMap::::new(); - - // Parse leading comments for supported triple slash references. - for comment in parsed_source.get_leading_comments().iter() { - if let Some((ts_reference, span)) = parse_ts_reference(comment) { - let loc = parsed_source.source().line_and_column_index(span.lo); - match ts_reference { - TypeScriptReference::Path(import) => { - let dep = dependencies.entry(import.clone()).or_default(); - let resolved_import = - resolve_import(&import, specifier, maybe_import_map); - dep.maybe_code = Some(resolved_import); - dep.maybe_code_specifier_range = Some(Range { - start: Position { - line: loc.line_index as u32, - character: loc.column_index as u32, - }, - end: Position { - line: loc.line_index as u32, - character: (loc.column_index + import.chars().count() + 2) as u32, - }, - }); - } - TypeScriptReference::Types(import) => { - let resolved_import = - resolve_import(&import, specifier, maybe_import_map); - if media_type == MediaType::JavaScript || media_type == MediaType::Jsx - { - maybe_type = Some(resolved_import.clone()); - } - let dep = dependencies.entry(import.clone()).or_default(); - dep.maybe_type = Some(resolved_import); - dep.maybe_type_specifier_range = Some(Range { - start: Position { - line: loc.line_index as u32, - character: loc.column_index as u32, - }, - end: Position { - line: loc.line_index as u32, - character: (loc.column_index + import.chars().count() + 2) as u32, - }, - }); - } - } - } - } - - // Parse ES and type only imports - let descriptors = deno_graph::analyze_dependencies(parsed_source); - for desc in descriptors.into_iter().filter(|desc| { - desc.kind != deno_ast::swc::dep_graph::DependencyKind::Require - }) { - let resolved_import = - resolve_import(&desc.specifier, specifier, maybe_import_map); - - let maybe_resolved_type_dependency = - // Check for `@deno-types` pragmas that affect the import - if let Some(comment) = desc.leading_comments.last() { - parse_deno_types(comment).as_ref().map(|(deno_types, span)| { - ( - resolve_import(deno_types, specifier, maybe_import_map), - deno_types.clone(), - parsed_source.source().line_and_column_index(span.lo) - ) - }) - } else { - None - }; - - let dep = dependencies.entry(desc.specifier.to_string()).or_default(); - dep.is_dynamic = desc.is_dynamic; - let start = parsed_source - .source() - .line_and_column_index(desc.specifier_span.lo); - let end = parsed_source - .source() - .line_and_column_index(desc.specifier_span.hi); - let range = Range { - start: Position { - line: start.line_index as u32, - character: start.column_index as u32, - }, - end: Position { - line: end.line_index as u32, - character: end.column_index as u32, - }, - }; - dep.maybe_code_specifier_range = Some(range); - dep.maybe_code = Some(resolved_import); - if dep.maybe_type.is_none() { - if let Some((resolved_dependency, specifier, loc)) = - maybe_resolved_type_dependency - { - dep.maybe_type_specifier_range = Some(Range { - start: Position { - line: loc.line_index as u32, - // +1 to skip quote - character: (loc.column_index + 1) as u32, - }, - end: Position { - line: loc.line_index as u32, - // +1 to skip quote - character: (loc.column_index + 1 + specifier.chars().count()) - as u32, - }, - }); - dep.maybe_type = Some(resolved_dependency); - } - } - } - - (dependencies, maybe_type) -} - fn code_as_string(code: &Option) -> String { match code { Some(lsp::NumberOrString::String(str)) => str.clone(), @@ -494,21 +167,16 @@ fn check_specifier( specifier: &str, referrer: &ModuleSpecifier, snapshot: &language_server::StateSnapshot, - maybe_import_map: &Option, ) -> Option { for ext in SUPPORTED_EXTENSIONS { let specifier_with_ext = format!("{}{}", specifier, ext); - if let ResolvedDependency::Resolved(resolved_specifier) = - resolve_import(&specifier_with_ext, referrer, maybe_import_map) + if snapshot + .documents + .contains_import(&specifier_with_ext, referrer) { - if snapshot.documents.contains_key(&resolved_specifier) - || snapshot.sources.contains_key(&resolved_specifier) - { - return Some(specifier_with_ext); - } + return Some(specifier_with_ext); } } - None } @@ -531,12 +199,9 @@ pub(crate) fn fix_ts_import_changes( .get(1) .ok_or_else(|| anyhow!("Missing capture."))? .as_str(); - if let Some(new_specifier) = check_specifier( - specifier, - referrer, - &snapshot, - &language_server.maybe_import_map, - ) { + if let Some(new_specifier) = + check_specifier(specifier, referrer, &snapshot) + { let new_text = text_change.new_text.replace(specifier, &new_specifier); text_changes.push(tsc::TextChange { @@ -582,12 +247,9 @@ fn fix_ts_import_action( .ok_or_else(|| anyhow!("Missing capture."))? .as_str(); let snapshot = language_server.snapshot()?; - if let Some(new_specifier) = check_specifier( - specifier, - referrer, - &snapshot, - &language_server.maybe_import_map, - ) { + if let Some(new_specifier) = + check_specifier(specifier, referrer, &snapshot) + { let description = action.description.replace(specifier, &new_specifier); let changes = action .changes @@ -755,8 +417,9 @@ impl CodeActionCollection { pub(crate) fn add_deno_lint_ignore_action( &mut self, specifier: &ModuleSpecifier, - document: Option<&DocumentData>, diagnostic: &lsp::Diagnostic, + maybe_text_info: Option, + maybe_parsed_source: Option, ) -> Result<(), AnyError> { let code = diagnostic .code @@ -767,11 +430,8 @@ impl CodeActionCollection { }) .unwrap(); - let document_source = document.map(|d| d.source()); - - let line_content = document_source.map(|d| { - d.text_info() - .line_text(diagnostic.range.start.line as usize) + let line_content = maybe_text_info.map(|ti| { + ti.line_text(diagnostic.range.start.line as usize) .to_string() }); @@ -814,9 +474,7 @@ impl CodeActionCollection { .push(CodeActionKind::DenoLint(ignore_error_action)); // Disable a lint error for the entire file. - let parsed_source = - document_source.and_then(|d| d.module().and_then(|r| r.as_ref().ok())); - let maybe_ignore_comment = parsed_source.and_then(|ps| { + let maybe_ignore_comment = maybe_parsed_source.clone().and_then(|ps| { // Note: we can use ps.get_leading_comments() but it doesn't // work when shebang is present at the top of the file. ps.comments().get_vec().iter().find_map(|c| { @@ -847,7 +505,7 @@ impl CodeActionCollection { if let Some(ignore_comment) = maybe_ignore_comment { new_text = format!(" {}", code); // Get the end position of the comment. - let line = parsed_source + let line = maybe_parsed_source .unwrap() .source() .line_and_column_index(ignore_comment.span.hi()); @@ -1099,157 +757,9 @@ fn prepend_whitespace(content: String, line_content: Option) -> String { } } -/// Get LSP range from the provided start and end locations. -fn get_range_from_location( - start: &ast::Location, - end: &ast::Location, -) -> lsp::Range { - lsp::Range { - start: lsp::Position { - line: (start.line - 1) as u32, - character: start.col as u32, - }, - end: lsp::Position { - line: (end.line - 1) as u32, - character: end.col as u32, - }, - } -} - -/// Narrow the range to only include the text of the specifier, excluding the -/// quotes. -fn narrow_range(range: lsp::Range) -> lsp::Range { - lsp::Range { - start: lsp::Position { - line: range.start.line, - character: range.start.character + 1, - }, - end: lsp::Position { - line: range.end.line, - character: range.end.character - 1, - }, - } -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct DependencyRange { - /// The LSP Range is inclusive of the quotes around the specifier. - pub range: lsp::Range, - /// The text of the specifier within the document. - pub specifier: String, -} - -impl DependencyRange { - /// Determine if the position is within the range - fn within(&self, position: &lsp::Position) -> bool { - (position.line > self.range.start.line - || position.line == self.range.start.line - && position.character >= self.range.start.character) - && (position.line < self.range.end.line - || position.line == self.range.end.line - && position.character <= self.range.end.character) - } -} - -#[derive(Debug, Default, Clone)] -pub struct DependencyRanges(Vec); - -impl DependencyRanges { - pub fn contains(&self, position: &lsp::Position) -> Option { - self.0.iter().find(|r| r.within(position)).cloned() - } -} - -struct DependencyRangeCollector<'a> { - import_ranges: DependencyRanges, - parsed_source: &'a deno_ast::ParsedSource, -} - -impl<'a> DependencyRangeCollector<'a> { - pub fn new(parsed_source: &'a deno_ast::ParsedSource) -> Self { - Self { - import_ranges: DependencyRanges::default(), - parsed_source, - } - } - - pub fn take(self) -> DependencyRanges { - self.import_ranges - } -} - -impl<'a> Visit for DependencyRangeCollector<'a> { - fn visit_import_decl( - &mut self, - node: &swc_ast::ImportDecl, - _parent: &dyn Node, - ) { - let start = Location::from_pos(self.parsed_source, node.src.span.lo); - let end = Location::from_pos(self.parsed_source, node.src.span.hi); - self.import_ranges.0.push(DependencyRange { - range: narrow_range(get_range_from_location(&start, &end)), - specifier: node.src.value.to_string(), - }); - } - - fn visit_named_export( - &mut self, - node: &swc_ast::NamedExport, - _parent: &dyn Node, - ) { - if let Some(src) = &node.src { - let start = Location::from_pos(self.parsed_source, src.span.lo); - let end = Location::from_pos(self.parsed_source, src.span.hi); - self.import_ranges.0.push(DependencyRange { - range: narrow_range(get_range_from_location(&start, &end)), - specifier: src.value.to_string(), - }); - } - } - - fn visit_export_all( - &mut self, - node: &swc_ast::ExportAll, - _parent: &dyn Node, - ) { - let start = Location::from_pos(self.parsed_source, node.src.span.lo); - let end = Location::from_pos(self.parsed_source, node.src.span.hi); - self.import_ranges.0.push(DependencyRange { - range: narrow_range(get_range_from_location(&start, &end)), - specifier: node.src.value.to_string(), - }); - } - - fn visit_ts_import_type( - &mut self, - node: &swc_ast::TsImportType, - _parent: &dyn Node, - ) { - let start = Location::from_pos(self.parsed_source, node.arg.span.lo); - let end = Location::from_pos(self.parsed_source, node.arg.span.hi); - self.import_ranges.0.push(DependencyRange { - range: narrow_range(get_range_from_location(&start, &end)), - specifier: node.arg.value.to_string(), - }); - } -} - -/// Analyze a document for import ranges, which then can be used to identify if -/// a particular position within the document as inside an import range. -pub fn analyze_dependency_ranges( - parsed_source: &deno_ast::ParsedSource, -) -> Result { - let mut collector = DependencyRangeCollector::new(parsed_source); - parsed_source - .module() - .visit_with(&swc_ast::Invalid { span: DUMMY_SP }, &mut collector); - Ok(collector.take()) -} - #[cfg(test)] mod tests { use super::*; - use deno_core::resolve_url; #[test] fn test_reference_to_diagnostic() { @@ -1338,209 +848,4 @@ mod tests { } ); } - - #[test] - fn test_get_lint_references() { - let specifier = resolve_url("file:///a.ts").expect("bad specifier"); - let source = "const foo = 42;"; - let parsed_module = parse_module( - &specifier, - SourceTextInfo::from_string(source.to_string()), - MediaType::TypeScript, - ) - .unwrap(); - let actual = get_lint_references(&parsed_module, None).unwrap(); - - assert_eq!( - actual, - vec![Reference { - category: Category::Lint { - message: "`foo` is never used".to_string(), - code: "no-unused-vars".to_string(), - hint: Some( - "If this is intentional, prefix it with an underscore like `_foo`" - .to_string() - ), - }, - range: Range { - start: Position { - line: 0, - character: 6, - }, - end: Position { - line: 0, - character: 9, - } - } - }] - ); - } - - #[test] - fn test_analyze_dependencies() { - let specifier = resolve_url("file:///a.ts").expect("bad specifier"); - let source = r#"import { - Application, - Context, - Router, - Status, - } from "https://deno.land/x/oak@v6.3.2/mod.ts"; - - import type { Component } from "https://esm.sh/preact"; - import { h, Fragment } from "https://esm.sh/preact"; - - // @deno-types="https://deno.land/x/types/react/index.d.ts"; - import React from "https://cdn.skypack.dev/react"; - "#; - let parsed_module = parse_module( - &specifier, - SourceTextInfo::from_string(source.to_string()), - MediaType::TypeScript, - ) - .unwrap(); - let (actual, maybe_type) = analyze_dependencies( - &specifier, - MediaType::TypeScript, - &parsed_module, - &None, - ); - assert!(maybe_type.is_none()); - assert_eq!(actual.len(), 3); - assert_eq!( - actual.get("https://cdn.skypack.dev/react").cloned(), - Some(Dependency { - is_dynamic: false, - maybe_code: Some(ResolvedDependency::Resolved( - resolve_url("https://cdn.skypack.dev/react").unwrap() - )), - maybe_type: Some(ResolvedDependency::Resolved( - resolve_url("https://deno.land/x/types/react/index.d.ts").unwrap() - )), - maybe_code_specifier_range: Some(Range { - start: Position { - line: 11, - character: 22, - }, - end: Position { - line: 11, - character: 53, - } - }), - maybe_type_specifier_range: Some(Range { - start: Position { - line: 10, - character: 20, - }, - end: Position { - line: 10, - character: 62, - } - }) - }) - ); - assert_eq!( - actual.get("https://deno.land/x/oak@v6.3.2/mod.ts").cloned(), - Some(Dependency { - is_dynamic: false, - maybe_code: Some(ResolvedDependency::Resolved( - resolve_url("https://deno.land/x/oak@v6.3.2/mod.ts").unwrap() - )), - maybe_type: None, - maybe_code_specifier_range: Some(Range { - start: Position { - line: 5, - character: 11, - }, - end: Position { - line: 5, - character: 50, - } - }), - maybe_type_specifier_range: None, - }) - ); - assert_eq!( - actual.get("https://esm.sh/preact").cloned(), - Some(Dependency { - is_dynamic: false, - maybe_code: Some(ResolvedDependency::Resolved( - resolve_url("https://esm.sh/preact").unwrap() - )), - maybe_type: None, - maybe_code_specifier_range: Some(Range { - start: Position { - line: 8, - character: 32 - }, - end: Position { - line: 8, - character: 55 - } - }), - maybe_type_specifier_range: None, - }), - ); - } - - #[test] - fn test_analyze_dependency_ranges() { - let specifier = resolve_url("file:///a.ts").unwrap(); - let source = - "import * as a from \"./b.ts\";\nexport * as a from \"./c.ts\";\n"; - let media_type = MediaType::TypeScript; - let parsed_module = parse_module( - &specifier, - SourceTextInfo::from_string(source.to_string()), - media_type, - ) - .unwrap(); - let result = analyze_dependency_ranges(&parsed_module); - assert!(result.is_ok()); - let actual = result.unwrap(); - assert_eq!( - actual.contains(&lsp::Position { - line: 0, - character: 0, - }), - None - ); - assert_eq!( - actual.contains(&lsp::Position { - line: 0, - character: 22, - }), - Some(DependencyRange { - range: lsp::Range { - start: lsp::Position { - line: 0, - character: 20, - }, - end: lsp::Position { - line: 0, - character: 26, - }, - }, - specifier: "./b.ts".to_string(), - }) - ); - assert_eq!( - actual.contains(&lsp::Position { - line: 1, - character: 22, - }), - Some(DependencyRange { - range: lsp::Range { - start: lsp::Position { - line: 1, - character: 20, - }, - end: lsp::Position { - line: 1, - character: 26, - }, - }, - specifier: "./c.ts".to_string(), - }) - ); - } } diff --git a/cli/lsp/code_lens.rs b/cli/lsp/code_lens.rs index 6755f50d56..97ad8f260d 100644 --- a/cli/lsp/code_lens.rs +++ b/cli/lsp/code_lens.rs @@ -25,6 +25,7 @@ use regex::Regex; use std::cell::RefCell; use std::collections::HashSet; use std::rc::Rc; +use std::sync::Arc; lazy_static::lazy_static! { static ref ABSTRACT_MODIFIER: Regex = Regex::new(r"\babstract\b").unwrap(); @@ -61,18 +62,15 @@ fn span_to_range(span: &Span, parsed_source: &ParsedSource) -> lsp::Range { } } -struct DenoTestCollector<'a> { +struct DenoTestCollector { code_lenses: Vec, - parsed_source: &'a ParsedSource, + parsed_source: ParsedSource, specifier: ModuleSpecifier, test_vars: HashSet, } -impl<'a> DenoTestCollector<'a> { - pub fn new( - specifier: ModuleSpecifier, - parsed_source: &'a ParsedSource, - ) -> Self { +impl DenoTestCollector { + pub fn new(specifier: ModuleSpecifier, parsed_source: ParsedSource) -> Self { Self { code_lenses: Vec::new(), parsed_source, @@ -82,7 +80,7 @@ impl<'a> DenoTestCollector<'a> { } fn add_code_lens>(&mut self, name: N, span: &Span) { - let range = span_to_range(span, self.parsed_source); + let range = span_to_range(span, &self.parsed_source); self.code_lenses.push(lsp::CodeLens { range, command: Some(lsp::Command { @@ -130,7 +128,7 @@ impl<'a> DenoTestCollector<'a> { } } -impl<'a> Visit for DenoTestCollector<'a> { +impl Visit for DenoTestCollector { fn visit_call_expr(&mut self, node: &ast::CallExpr, _parent: &dyn Node) { if let ast::ExprOrSuper::Expr(callee_expr) = &node.callee { match callee_expr.as_ref() { @@ -238,7 +236,7 @@ async fn resolve_implementation_code_lens( let implementation_specifier = resolve_url(&implementation.document_span.file_name)?; let implementation_location = - implementation.to_location(&line_index, language_server); + implementation.to_location(line_index.clone(), language_server); if !(implementation_specifier == data.specifier && implementation_location.range.start == code_lens.range.start) { @@ -311,7 +309,7 @@ async fn resolve_references_code_lens( resolve_url(&reference.document_span.file_name)?; let line_index = language_server.get_line_index(reference_specifier).await?; - locations.push(reference.to_location(&line_index, language_server)); + locations.push(reference.to_location(line_index, language_server)); } let command = if !locations.is_empty() { let title = if locations.len() > 1 { @@ -372,9 +370,9 @@ pub(crate) async fn resolve_code_lens( pub(crate) async fn collect( specifier: &ModuleSpecifier, - parsed_source: Option<&ParsedSource>, + parsed_source: Option, config: &Config, - line_index: &LineIndex, + line_index: Arc, navigation_tree: &NavigationTree, ) -> Result, AnyError> { let mut code_lenses = collect_test(specifier, parsed_source, config)?; @@ -393,13 +391,13 @@ pub(crate) async fn collect( fn collect_test( specifier: &ModuleSpecifier, - parsed_source: Option<&ParsedSource>, + parsed_source: Option, config: &Config, ) -> Result, AnyError> { if config.specifier_code_lens_test(specifier) { if let Some(parsed_source) = parsed_source { let mut collector = - DenoTestCollector::new(specifier.clone(), parsed_source); + DenoTestCollector::new(specifier.clone(), parsed_source.clone()); parsed_source.module().visit_with( &ast::Invalid { span: deno_ast::swc::common::DUMMY_SP, @@ -416,7 +414,7 @@ fn collect_test( async fn collect_tsc( specifier: &ModuleSpecifier, workspace_settings: &WorkspaceSettings, - line_index: &LineIndex, + line_index: Arc, navigation_tree: &NavigationTree, ) -> Result, AnyError> { let code_lenses = Rc::new(RefCell::new(Vec::new())); @@ -428,7 +426,11 @@ async fn collect_tsc( let source = CodeLensSource::Implementations; match i.kind { tsc::ScriptElementKind::InterfaceElement => { - code_lenses.push(i.to_code_lens(line_index, specifier, &source)); + code_lenses.push(i.to_code_lens( + line_index.clone(), + specifier, + &source, + )); } tsc::ScriptElementKind::ClassElement | tsc::ScriptElementKind::MemberFunctionElement @@ -436,7 +438,11 @@ async fn collect_tsc( | tsc::ScriptElementKind::MemberGetAccessorElement | tsc::ScriptElementKind::MemberSetAccessorElement => { if ABSTRACT_MODIFIER.is_match(&i.kind_modifiers) { - code_lenses.push(i.to_code_lens(line_index, specifier, &source)); + code_lenses.push(i.to_code_lens( + line_index.clone(), + specifier, + &source, + )); } } _ => (), @@ -448,31 +454,51 @@ async fn collect_tsc( let source = CodeLensSource::References; if let Some(parent) = &mp { if parent.kind == tsc::ScriptElementKind::EnumElement { - code_lenses.push(i.to_code_lens(line_index, specifier, &source)); + code_lenses.push(i.to_code_lens( + line_index.clone(), + specifier, + &source, + )); } } match i.kind { tsc::ScriptElementKind::FunctionElement => { if workspace_settings.code_lens.references_all_functions { - code_lenses.push(i.to_code_lens(line_index, specifier, &source)); + code_lenses.push(i.to_code_lens( + line_index.clone(), + specifier, + &source, + )); } } tsc::ScriptElementKind::ConstElement | tsc::ScriptElementKind::LetElement | tsc::ScriptElementKind::VariableElement => { if EXPORT_MODIFIER.is_match(&i.kind_modifiers) { - code_lenses.push(i.to_code_lens(line_index, specifier, &source)); + code_lenses.push(i.to_code_lens( + line_index.clone(), + specifier, + &source, + )); } } tsc::ScriptElementKind::ClassElement => { if i.text != "" { - code_lenses.push(i.to_code_lens(line_index, specifier, &source)); + code_lenses.push(i.to_code_lens( + line_index.clone(), + specifier, + &source, + )); } } tsc::ScriptElementKind::InterfaceElement | tsc::ScriptElementKind::TypeElement | tsc::ScriptElementKind::EnumElement => { - code_lenses.push(i.to_code_lens(line_index, specifier, &source)); + code_lenses.push(i.to_code_lens( + line_index.clone(), + specifier, + &source, + )); } tsc::ScriptElementKind::LocalFunctionElement | tsc::ScriptElementKind::MemberGetAccessorElement @@ -485,8 +511,11 @@ async fn collect_tsc( tsc::ScriptElementKind::ClassElement | tsc::ScriptElementKind::InterfaceElement | tsc::ScriptElementKind::TypeElement => { - code_lenses - .push(i.to_code_lens(line_index, specifier, &source)); + code_lenses.push(i.to_code_lens( + line_index.clone(), + specifier, + &source, + )); } _ => (), } @@ -510,21 +539,28 @@ mod tests { #[test] fn test_deno_test_collector() { let specifier = resolve_url("https://deno.land/x/mod.ts").unwrap(); - let source = r#" + let source = Arc::new( + r#" Deno.test({ name: "test a", fn() {} }); Deno.test("test b", function anotherTest() {}); - "#; - let parsed_module = crate::lsp::analysis::parse_module( - &specifier, - SourceTextInfo::from_string(source.to_string()), - MediaType::TypeScript, - ) + "# + .to_string(), + ); + let parsed_module = deno_ast::parse_module(deno_ast::ParseParams { + specifier: specifier.to_string(), + source: SourceTextInfo::new(source), + media_type: MediaType::TypeScript, + capture_tokens: true, + scope_analysis: true, + maybe_syntax: None, + }) .unwrap(); - let mut collector = DenoTestCollector::new(specifier, &parsed_module); + let mut collector = + DenoTestCollector::new(specifier, parsed_module.clone()); parsed_module.module().visit_with( &ast::Invalid { span: deno_ast::swc::common::DUMMY_SP, diff --git a/cli/lsp/completions.rs b/cli/lsp/completions.rs index 2fa526f6f9..4931b55608 100644 --- a/cli/lsp/completions.rs +++ b/cli/lsp/completions.rs @@ -1,6 +1,5 @@ // Copyright 2018-2021 the Deno authors. All rights reserved. MIT license. -use super::analysis; use super::language_server; use super::lsp_custom; use super::tsc; @@ -85,21 +84,34 @@ async fn check_auto_config_registry( } } +/// Ranges from the graph for specifiers include the leading and trailing quote, +/// which we want to ignore when replacing text. +fn to_narrow_lsp_range(range: &deno_graph::Range) -> lsp::Range { + lsp::Range { + start: lsp::Position { + line: range.start.line as u32, + character: (range.start.character + 1) as u32, + }, + end: lsp::Position { + line: range.end.line as u32, + character: (range.end.character - 1) as u32, + }, + } +} + /// Given a specifier, a position, and a snapshot, optionally return a /// completion response, which will be valid import completions for the specific /// context. -pub async fn get_import_completions( +pub(crate) async fn get_import_completions( specifier: &ModuleSpecifier, position: &lsp::Position, state_snapshot: &language_server::StateSnapshot, client: lspower::Client, ) -> Option { - let analysis::DependencyRange { - range, - specifier: text, - } = state_snapshot + let (text, _, range) = state_snapshot .documents - .is_specifier_position(specifier, position)?; + .get_maybe_dependency(specifier, position)?; + let range = to_narrow_lsp_range(&range); // completions for local relative modules if text.starts_with("./") || text.starts_with("../") { Some(lsp::CompletionResponse::List(lsp::CompletionList { @@ -258,7 +270,7 @@ fn get_workspace_completions( range: &lsp::Range, state_snapshot: &language_server::StateSnapshot, ) -> Vec { - let workspace_specifiers = state_snapshot.sources.specifiers(); + let workspace_specifiers = state_snapshot.documents.specifiers(false, true); let specifier_strings = get_relative_specifiers(specifier, workspace_specifiers); specifier_strings @@ -399,11 +411,8 @@ fn relative_specifier( mod tests { use super::*; use crate::http_cache::HttpCache; - use crate::lsp::analysis; - use crate::lsp::documents::DocumentCache; + use crate::lsp::documents::Documents; use crate::lsp::documents::LanguageId; - use crate::lsp::sources::Sources; - use deno_ast::MediaType; use deno_core::resolve_url; use std::collections::HashMap; use std::path::Path; @@ -415,37 +424,17 @@ mod tests { source_fixtures: &[(&str, &str)], location: &Path, ) -> language_server::StateSnapshot { - let mut documents = DocumentCache::default(); + let documents = Documents::new(location); for (specifier, source, version, language_id) in fixtures { let specifier = resolve_url(specifier).expect("failed to create specifier"); documents.open( specifier.clone(), *version, - *language_id, + language_id.clone(), Arc::new(source.to_string()), ); - let media_type = MediaType::from(&specifier); - let parsed_module = documents - .get(&specifier) - .unwrap() - .source() - .module() - .map(|r| r.as_ref()) - .unwrap() - .unwrap(); - let (deps, _) = analysis::analyze_dependencies( - &specifier, - media_type, - parsed_module, - &None, - ); - let dep_ranges = analysis::analyze_dependency_ranges(parsed_module).ok(); - documents - .set_dependencies(&specifier, Some(deps), dep_ranges) - .unwrap(); } - let sources = Sources::new(location); let http_cache = HttpCache::new(location); for (specifier, source) in source_fixtures { let specifier = @@ -454,13 +443,12 @@ mod tests { .set(&specifier, HashMap::default(), source.as_bytes()) .expect("could not cache file"); assert!( - sources.get_source(&specifier).is_some(), + documents.content(&specifier).is_some(), "source could not be setup" ); } language_server::StateSnapshot { documents, - sources, ..Default::default() } } diff --git a/cli/lsp/diagnostics.rs b/cli/lsp/diagnostics.rs index 4bae048c0e..698fcd7441 100644 --- a/cli/lsp/diagnostics.rs +++ b/cli/lsp/diagnostics.rs @@ -1,15 +1,14 @@ // Copyright 2018-2021 the Deno authors. All rights reserved. MIT license. use super::analysis; -use super::documents::DocumentCache; +use super::documents; +use super::documents::Documents; use super::language_server; -use super::sources::Sources; use super::tsc; use crate::diagnostics; use crate::tokio_util::create_basic_runtime; -use analysis::ResolvedDependency; use deno_core::error::anyhow; use deno_core::error::AnyError; use deno_core::resolve_url; @@ -90,21 +89,13 @@ impl DiagnosticCollection { } } -#[derive(Debug)] +#[derive(Debug, Default)] pub(crate) struct DiagnosticsServer { channel: Option>, collection: Arc>, } impl DiagnosticsServer { - pub(crate) fn new() -> Self { - let collection = Arc::new(Mutex::new(DiagnosticCollection::default())); - Self { - channel: None, - collection, - } - } - pub(crate) async fn get( &self, specifier: &ModuleSpecifier, @@ -318,24 +309,17 @@ async fn generate_lint_diagnostics( tokio::task::spawn(async move { 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); + for specifier in documents.specifiers(true, true) { + let version = documents.lsp_version(&specifier); let current_version = collection .lock() .await - .get_version(specifier, &DiagnosticSource::DenoLint); + .get_version(&specifier, &DiagnosticSource::DenoLint); if version != current_version { - let module = documents - .get(specifier) - .map(|d| d.source().module()) - .flatten(); - let diagnostics = match module { - Some(Ok(module)) => { + let diagnostics = match documents.parsed_source(&specifier) { + Some(Ok(parsed_source)) => { if let Ok(references) = analysis::get_lint_references( - module, + &parsed_source, maybe_lint_config.as_ref(), ) { references @@ -372,18 +356,14 @@ async fn generate_ts_diagnostics( let collection = collection.lock().await; snapshot .documents - .open_specifiers() + .specifiers(true, true) .iter() - .filter_map(|&s| { - 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 - } + .filter_map(|s| { + let version = snapshot.documents.lsp_version(s); + let current_version = + collection.get_version(s, &DiagnosticSource::TypeScript); + if version != current_version { + Some(s.clone()) } else { None } @@ -396,7 +376,7 @@ async fn generate_ts_diagnostics( ts_server.request(snapshot.clone(), req).await?; for (specifier_str, ts_diagnostics) in ts_diagnostics_map { let specifier = resolve_url(&specifier_str)?; - let version = snapshot.documents.version(&specifier); + let version = snapshot.documents.lsp_version(&specifier); diagnostics_vec.push(( specifier, version, @@ -407,61 +387,76 @@ async fn generate_ts_diagnostics( Ok(diagnostics_vec) } +fn resolution_error_as_code( + err: &deno_graph::ResolutionError, +) -> lsp::NumberOrString { + use deno_graph::ResolutionError; + use deno_graph::SpecifierError; + + match err { + ResolutionError::InvalidDowngrade(_, _) => { + lsp::NumberOrString::String("invalid-downgrade".to_string()) + } + ResolutionError::InvalidLocalImport(_, _) => { + lsp::NumberOrString::String("invalid-local-import".to_string()) + } + ResolutionError::InvalidSpecifier(err, _) => match err { + SpecifierError::ImportPrefixMissing(_, _) => { + lsp::NumberOrString::String("import-prefix-missing".to_string()) + } + SpecifierError::InvalidUrl(_) => { + lsp::NumberOrString::String("invalid-url".to_string()) + } + }, + ResolutionError::ResolverError(_, _, _) => { + lsp::NumberOrString::String("resolver-error".to_string()) + } + } +} + fn diagnose_dependency( diagnostics: &mut Vec, - documents: &DocumentCache, - sources: &Sources, - maybe_dependency: &Option, - maybe_range: &Option, + documents: &Documents, + resolved: &deno_graph::Resolved, ) { - if let (Some(dep), Some(range)) = (maybe_dependency, *maybe_range) { - match dep { - analysis::ResolvedDependency::Err(err) => { - diagnostics.push(lsp::Diagnostic { - range, - severity: Some(lsp::DiagnosticSeverity::Error), - code: Some(err.as_code()), - code_description: None, - source: Some("deno".to_string()), - message: err.to_string(), - related_information: None, - tags: None, - data: None, - }) - } - analysis::ResolvedDependency::Resolved(specifier) => { - if !(documents.contains_key(specifier) - || sources.contains_key(specifier)) - { - let (code, message) = match specifier.scheme() { - "file" => (Some(lsp::NumberOrString::String("no-local".to_string())), format!("Unable to load a local module: \"{}\".\n Please check the file path.", specifier)), - "data" => (Some(lsp::NumberOrString::String("no-cache-data".to_string())), "Uncached data URL.".to_string()), + match resolved { + Some(Ok((specifier, range))) => { + if !documents.contains_specifier(specifier) { + let (code, message) = match specifier.scheme() { + "file" => (Some(lsp::NumberOrString::String("no-local".to_string())), format!("Unable to load a local module: \"{}\".\n Please check the file path.", specifier)), + "data" => (Some(lsp::NumberOrString::String("no-cache-data".to_string())), "Uncached data URL.".to_string()), "blob" => (Some(lsp::NumberOrString::String("no-cache-blob".to_string())), "Uncached blob URL.".to_string()), _ => (Some(lsp::NumberOrString::String("no-cache".to_string())), format!("Uncached or missing remote URL: \"{}\".", specifier)), - }; - diagnostics.push(lsp::Diagnostic { - range, - severity: Some(lsp::DiagnosticSeverity::Error), - code, - source: Some("deno".to_string()), - message, - data: Some(json!({ "specifier": specifier })), - ..Default::default() - }); - } else if sources.contains_key(specifier) { - if let Some(message) = sources.get_maybe_warning(specifier) { - diagnostics.push(lsp::Diagnostic { - range, - severity: Some(lsp::DiagnosticSeverity::Warning), - code: Some(lsp::NumberOrString::String("deno-warn".to_string())), - source: Some("deno".to_string()), - message, - ..Default::default() - }) - } - } + }; + diagnostics.push(lsp::Diagnostic { + range: documents::to_lsp_range(range), + severity: Some(lsp::DiagnosticSeverity::Error), + code, + source: Some("deno".to_string()), + message, + data: Some(json!({ "specifier": specifier })), + ..Default::default() + }); + } else if let Some(message) = documents.maybe_warning(specifier) { + diagnostics.push(lsp::Diagnostic { + range: documents::to_lsp_range(range), + severity: Some(lsp::DiagnosticSeverity::Warning), + code: Some(lsp::NumberOrString::String("deno-warn".to_string())), + source: Some("deno".to_string()), + message, + ..Default::default() + }) } } + Some(Err(err)) => diagnostics.push(lsp::Diagnostic { + range: documents::to_lsp_range(err.range()), + severity: Some(lsp::DiagnosticSeverity::Error), + code: Some(resolution_error_as_code(err)), + source: Some("deno".to_string()), + message: err.to_string(), + ..Default::default() + }), + _ => (), } } @@ -473,36 +468,31 @@ async fn generate_deps_diagnostics( ) -> Result { let config = snapshot.config.clone(); let documents = snapshot.documents.clone(); - let sources = snapshot.sources.clone(); tokio::task::spawn(async move { let mut diagnostics_vec = Vec::new(); - for specifier in documents.open_specifiers() { - if !config.specifier_enabled(specifier) { + for specifier in documents.specifiers(true, true) { + if !config.specifier_enabled(&specifier) { continue; } - let version = documents.version(specifier); + let version = documents.lsp_version(&specifier); let current_version = collection .lock() .await - .get_version(specifier, &DiagnosticSource::Deno); + .get_version(&specifier, &DiagnosticSource::Deno); if version != current_version { let mut diagnostics = Vec::new(); - if let Some(dependencies) = documents.dependencies(specifier) { + if let Some(dependencies) = documents.dependencies(&specifier) { for (_, dependency) in dependencies { diagnose_dependency( &mut diagnostics, &documents, - &sources, &dependency.maybe_code, - &dependency.maybe_code_specifier_range, ); diagnose_dependency( &mut diagnostics, &documents, - &sources, &dependency.maybe_type, - &dependency.maybe_type_specifier_range, ); } } @@ -544,7 +534,7 @@ async fn publish_diagnostics( .extend(collection.get(&specifier, DiagnosticSource::Deno).cloned()); } let uri = specifier.clone(); - let version = snapshot.documents.version(&specifier); + let version = snapshot.documents.lsp_version(&specifier); client.publish_diagnostics(uri, diagnostics, version).await; } } @@ -627,3 +617,79 @@ async fn update_diagnostics( tokio::join!(lint, ts, deps); snapshot.performance.measure(mark); } + +#[cfg(test)] +mod tests { + use super::*; + use crate::lsp::config::ConfigSnapshot; + use crate::lsp::config::Settings; + use crate::lsp::config::WorkspaceSettings; + use crate::lsp::documents::LanguageId; + use crate::lsp::language_server::StateSnapshot; + use std::path::Path; + use std::path::PathBuf; + use tempfile::TempDir; + + fn mock_state_snapshot( + fixtures: &[(&str, &str, i32, LanguageId)], + location: &Path, + ) -> StateSnapshot { + let documents = Documents::new(location); + for (specifier, source, version, language_id) in fixtures { + let specifier = + resolve_url(specifier).expect("failed to create specifier"); + documents.open( + specifier.clone(), + *version, + language_id.clone(), + Arc::new(source.to_string()), + ); + } + let config = ConfigSnapshot { + settings: Settings { + workspace: WorkspaceSettings { + enable: true, + lint: true, + ..Default::default() + }, + ..Default::default() + }, + ..Default::default() + }; + StateSnapshot { + config, + documents, + ..Default::default() + } + } + + fn setup( + sources: &[(&str, &str, i32, LanguageId)], + ) -> (StateSnapshot, Arc>, PathBuf) { + let temp_dir = TempDir::new().expect("could not create temp dir"); + let location = temp_dir.path().join("deps"); + let state_snapshot = mock_state_snapshot(sources, &location); + let collection = Arc::new(Mutex::new(DiagnosticCollection::default())); + (state_snapshot, collection, location) + } + + #[tokio::test] + async fn test_generate_lint_diagnostics() { + let (snapshot, collection, _) = setup(&[( + "file:///a.ts", + r#"import * as b from "./b.ts"; + +let a = "a"; +console.log(a); +"#, + 1, + LanguageId::TypeScript, + )]); + let result = generate_lint_diagnostics(&snapshot, collection).await; + assert!(result.is_ok()); + let diagnostics = result.unwrap(); + assert_eq!(diagnostics.len(), 1); + let (_, _, diagnostics) = &diagnostics[0]; + assert_eq!(diagnostics.len(), 2); + } +} diff --git a/cli/lsp/document_source.rs b/cli/lsp/document_source.rs deleted file mode 100644 index c2bef884eb..0000000000 --- a/cli/lsp/document_source.rs +++ /dev/null @@ -1,75 +0,0 @@ -use deno_ast::Diagnostic; -use deno_ast::MediaType; -use deno_ast::ParsedSource; -use deno_ast::SourceTextInfo; -use deno_core::ModuleSpecifier; -use once_cell::sync::OnceCell; -use std::sync::Arc; - -use super::analysis; -use super::text::LineIndex; - -#[derive(Debug)] -struct DocumentSourceInner { - specifier: ModuleSpecifier, - media_type: MediaType, - text_info: SourceTextInfo, - parsed_module: OnceCell>, - line_index: LineIndex, -} - -/// Immutable information about a document. -#[derive(Debug, Clone)] -pub struct DocumentSource { - inner: Arc, -} - -impl DocumentSource { - pub fn new( - specifier: &ModuleSpecifier, - media_type: MediaType, - text: Arc, - line_index: LineIndex, - ) -> Self { - Self { - inner: Arc::new(DocumentSourceInner { - specifier: specifier.clone(), - media_type, - text_info: SourceTextInfo::new(text), - parsed_module: OnceCell::new(), - line_index, - }), - } - } - - pub fn text_info(&self) -> &SourceTextInfo { - &self.inner.text_info - } - - pub fn line_index(&self) -> &LineIndex { - &self.inner.line_index - } - - pub fn module(&self) -> Option<&Result> { - let is_parsable = matches!( - self.inner.media_type, - MediaType::JavaScript - | MediaType::Jsx - | MediaType::TypeScript - | MediaType::Tsx - | MediaType::Dts, - ); - if is_parsable { - // lazily parse the module - Some(self.inner.parsed_module.get_or_init(|| { - analysis::parse_module( - &self.inner.specifier, - self.inner.text_info.clone(), - self.inner.media_type, - ) - })) - } else { - None - } - } -} diff --git a/cli/lsp/documents.rs b/cli/lsp/documents.rs index 3855150e71..640a48f145 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; -/// 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 { +lazy_static::lazy_static! { + static ref JS_HEADERS: HashMap = ([ + ("content-type".to_string(), "application/javascript".to_string()) + ]).iter().cloned().collect(); + static ref JSX_HEADERS: HashMap = ([ + ("content-type".to_string(), "text/jsx".to_string()) + ]).iter().cloned().collect(); + static ref TS_HEADERS: HashMap = ([ + ("content-type".to_string(), "application/typescript".to_string()) + ]).iter().cloned().collect(); + static ref TSX_HEADERS: HashMap = ([ + ("content-type".to_string(), "text/tsx".to_string()) + ]).iter().cloned().collect(); +} + +/// 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, + media_type: MediaType, + ) -> Result { + 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> { + 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 { 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>, - dependency_ranges: Option, - pub(crate) language_id: LanguageId, - maybe_navigation_tree: Option, +#[derive(Debug)] +pub(crate) struct Document { + line_index: Arc, + maybe_language_id: Option, + maybe_lsp_version: Option, + maybe_module: + Option>, + maybe_navigation_tree: Option>, + maybe_warning: Option, + source: SourceTextInfo, specifier: ModuleSpecifier, - version: Option, + version: String, } -impl DocumentData { - pub fn new( +impl Document { + fn new( specifier: ModuleSpecifier, - version: i32, - language_id: LanguageId, - source_text: Arc, + version: String, + maybe_headers: Option<&HashMap>, + content: Arc, + 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, + version: i32, + changes: Vec, + 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,224 +204,656 @@ 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 { - let import_ranges = self.dependency_ranges.as_ref()?; - import_ranges.contains(position) + fn content(&self) -> Arc { + 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, + 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>, - pub(crate) docs: HashMap, +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>, + dependents: &mut HashSet, +) { + 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>, + /// A map of documents that can either be "open" in the language server, or + /// just present on disk. + docs: HashMap, + /// The optional import map that should be used when resolving dependencies. + maybe_import_map: Option, + redirects: HashMap, +} + +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 { + 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, - > = 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> = + 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 { + 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, + 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).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(); - } - - 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 - ) + 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 } } - /// Determines if the specifier can be processed for formatting. - pub fn is_formattable(&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) } - /// 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, + fn content(&mut self, specifier: &ModuleSpecifier) -> Option> { + self.get(specifier).map(|d| d.content()) + } + + fn dependencies( + &mut self, + specifier: &ModuleSpecifier, + ) -> Option> { + 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(), + ) + } + + fn dependents( + &mut self, + specifier: &ModuleSpecifier, + ) -> Vec { + if self.dirty { + self.calculate_dependents(); + self.dirty = false; + } + let mut dependents = HashSet::new(); + if let Some(specifier) = self.resolve_specifier(specifier) { + recurse_dependents(&specifier, &self.dependents_map, &mut dependents); + dependents.into_iter().collect() + } else { + vec![] + } + } + + 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, position: &lsp::Position, - ) -> Option { - let document = self.docs.get(specifier)?; - document.is_specifier_position(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 len(&self) -> usize { - self.docs.len() + fn get_navigation_tree( + &mut self, + specifier: &ModuleSpecifier, + ) -> Option> { + self + .get(specifier) + .map(|d| d.maybe_navigation_tree.clone()) + .flatten() } - pub fn line_index(&self, specifier: &ModuleSpecifier) -> Option { + fn get_path(&self, specifier: &ModuleSpecifier) -> Option { + 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 + } + } + + fn is_formattable(&mut self, specifier: &ModuleSpecifier) -> bool { + // currently any document that is open in the language server is formattable + self.is_open(specifier) + } + + 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 + } + } + + 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 + } + } + + fn line_index( + &mut self, + specifier: &ModuleSpecifier, + ) -> Option> { + let specifier = self.resolve_specifier(specifier)?; + self.docs.get(&specifier).map(|doc| doc.line_index.clone()) + } + + fn lsp_version(&self, specifier: &ModuleSpecifier) -> Option { 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 { + self + .get(specifier) + .map(|d| d.maybe_warning.clone()) + .flatten() + } + + fn open( &mut self, specifier: ModuleSpecifier, version: i32, language_id: LanguageId, - source: Arc, + content: Arc, ) { - 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> { + self + .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, + referrer: &ModuleSpecifier, + ) -> Option>> { + 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 { + 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() + }) + .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 resolve_remote_specifier( + &self, + specifier: &ModuleSpecifier, + redirect_limit: usize, + ) -> Option { + 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 + } + } + + fn resolve_specifier( + &mut self, + specifier: &ModuleSpecifier, + ) -> Option { + 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 { + let redirect = self.resolve_remote_specifier(specifier, 10)?; + self.redirects.insert(specifier.clone(), redirect.clone()); + Some(redirect) + } + } + + fn set_import_map( + &mut self, + maybe_import_map: Option>, + ) { + // 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: Arc, + ) -> Result<(), AnyError> { + 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 { self .docs .iter() - .filter_map(|(key, data)| { - if data.version.is_some() { - Some(key) + .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 } @@ -359,221 +861,292 @@ impl DocumentCache { .collect() } - fn recurse_dependents( + fn text_info( + &mut self, + specifier: &ModuleSpecifier, + ) -> Option { + self.get(specifier).map(|d| d.source.clone()) + } + + fn version(&mut self, specifier: &ModuleSpecifier) -> Option { + 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>); + +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, - dependents: &mut HashSet, + version: i32, + changes: Vec, + ) -> 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> { + self.0.lock().content(specifier) + } + + /// Return an optional vector of dependencies for a given specifier. + pub fn dependencies( + &self, + specifier: &ModuleSpecifier, + ) -> Option> { + 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 { + 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> { + 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> { + 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 { + self.0.lock().lsp_version(specifier) + } + + /// Return a warning header for a given specifier, if present. + pub fn maybe_warning(&self, specifier: &ModuleSpecifier) -> Option { + 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, ) { - 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); - } - } - } + self.0.lock().open(specifier, version, language_id, content) } - pub fn set_dependencies( - &mut self, + /// Return the parsed source or the module graph error for a given specifier. + pub fn parsed_source( + &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 - ), - )) - } + ) -> Option> { + 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, + referrer: &ModuleSpecifier, + ) -> Option>> { + 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>, + ) { + self.0.lock().set_import_map(maybe_import_map); + } + + /// 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) + } + + /// Set a navigation tree that is associated with the provided specifier. pub fn set_navigation_tree( - &mut self, + &self, specifier: &ModuleSpecifier, - navigation_tree: tsc::NavigationTree, + navigation_tree: Arc, ) -> 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 - ), - )) - } + self + .0 + .lock() + .set_navigation_tree(specifier, navigation_tree) } - pub fn specifiers(&self) -> Vec { - self.docs.keys().cloned().collect() + /// 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 { + self.0.lock().specifiers(open_only, diagnosable_only) } - pub fn version(&self, specifier: &ModuleSpecifier) -> Option { - self.docs.get(specifier).and_then(|doc| doc.version) + /// Return the current text info for a given specifier. + pub fn text_info( + &self, + specifier: &ModuleSpecifier, + ) -> Option { + self.0.lock().text_info(specifier) + } + + /// Return the version of a document in the document cache. + pub fn version(&self, specifier: &ModuleSpecifier) -> Option { + 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::().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); + 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( + 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(), + ); + documents.open( specifier.clone(), 1, - LanguageId::TypeScript, - Arc::new("console.log(\"Hello Deno\");\n".to_string()), + "javascript".parse().unwrap(), + content, ); - assert!(document_cache.contains_key(&specifier)); - assert!(!document_cache.contains_key(&missing_specifier)); + assert!(documents.is_formattable(&specifier)); + assert!(documents.is_diagnosable(&specifier)); + assert!(documents.line_index(&specifier).is_some()); } #[test] - fn test_document_cache_change() { - 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 deno\");\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: 20, + line: 1, + character: 13, }, }), - range_length: Some(1), - text: "D".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_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()), + .unwrap(); + assert_eq!( + documents.content(&specifier).unwrap().as_str(), + r#"import * as b from "./b.ts"; +console.log(b, "hello deno"); +"# ); - 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)); } } diff --git a/cli/lsp/language_server.rs b/cli/lsp/language_server.rs index 13fcafec15..b56093c7bf 100644 --- a/cli/lsp/language_server.rs +++ b/cli/lsp/language_server.rs @@ -1,5 +1,6 @@ // Copyright 2018-2021 the Deno authors. All rights reserved. MIT license. +use deno_ast::MediaType; use deno_core::error::anyhow; use deno_core::error::AnyError; use deno_core::resolve_url; @@ -23,12 +24,10 @@ use std::sync::atomic::Ordering; use std::sync::Arc; use tokio::fs; -use super::analysis; use super::analysis::fix_ts_import_changes; use super::analysis::ts_changes_to_edit; use super::analysis::CodeActionCollection; use super::analysis::CodeActionData; -use super::analysis::ResolvedDependency; use super::cache::CacheServer; use super::capabilities; use super::code_lens; @@ -38,14 +37,15 @@ use super::config::ConfigSnapshot; use super::config::SETTINGS_SECTION; use super::diagnostics; use super::diagnostics::DiagnosticSource; -use super::documents::DocumentCache; +use super::documents::to_hover_text; +use super::documents::to_lsp_range; +use super::documents::Documents; use super::documents::LanguageId; use super::lsp_custom; use super::parent_process_checker; use super::performance::Performance; use super::refactor; use super::registries; -use super::sources::Sources; use super::text; use super::text::LineIndex; use super::tsc; @@ -65,22 +65,21 @@ use crate::tools::fmt::format_file; use crate::tools::fmt::format_parsed_source; pub const REGISTRIES_PATH: &str = "registries"; -const SOURCES_PATH: &str = "deps"; +const CACHE_PATH: &str = "deps"; #[derive(Debug, Clone)] pub struct LanguageServer(Arc>); #[derive(Debug, Clone, Default)] -pub struct StateSnapshot { +pub(crate) struct StateSnapshot { pub assets: Assets, pub config: ConfigSnapshot, - pub documents: DocumentCache, + pub documents: Documents, pub maybe_lint_config: Option, pub maybe_fmt_config: Option, pub maybe_config_uri: Option, pub module_registries: registries::ModuleRegistry, pub performance: Performance, - pub sources: Sources, pub url_map: urls::LspUrlMap, } @@ -94,8 +93,9 @@ pub(crate) struct Inner { /// Configuration information. pub(crate) config: Config, diagnostics_server: diagnostics::DiagnosticsServer, - /// The "in-memory" documents in the editor which can be updated and changed. - documents: DocumentCache, + /// The collection of documents that the server is currently handling, either + /// on disk or "open" within the client. + documents: Documents, /// Handles module registries, which allow discovery of modules module_registries: registries::ModuleRegistry, /// The path to the module registries cache @@ -121,8 +121,6 @@ pub(crate) struct Inner { maybe_import_map_uri: Option, /// A collection of measurements which instrument that performance of the LSP. performance: Performance, - /// Cached sources that are read-only. - sources: Sources, /// A memoized version of fixable diagnostic codes retrieved from TypeScript. ts_fixable_diagnostics: Vec, /// An abstraction that handles interactions with TypeScript. @@ -145,19 +143,17 @@ impl Inner { let module_registries_location = dir.root.join(REGISTRIES_PATH); let module_registries = registries::ModuleRegistry::new(&module_registries_location); - let sources_location = dir.root.join(SOURCES_PATH); - let sources = Sources::new(&sources_location); + let location = dir.root.join(CACHE_PATH); + let documents = Documents::new(&location); let ts_server = Arc::new(TsServer::new()); - let performance = Performance::default(); - let diagnostics_server = diagnostics::DiagnosticsServer::new(); let config = Config::new(client.clone()); Self { assets: Default::default(), client, config, - diagnostics_server, - documents: Default::default(), + diagnostics_server: Default::default(), + documents, maybe_cache_path: None, maybe_lint_config: None, maybe_fmt_config: None, @@ -168,69 +164,20 @@ impl Inner { maybe_import_map_uri: None, module_registries, module_registries_location, - performance, - sources, + performance: Default::default(), ts_fixable_diagnostics: Default::default(), ts_server, url_map: Default::default(), } } - /// Analyzes dependencies of a document that has been opened in the editor and - /// sets the dependencies property on the document. - fn analyze_dependencies(&mut self, specifier: &ModuleSpecifier) { - if let Some(Ok(parsed_module)) = self - .documents - .get(specifier) - .map(|d| d.source().module()) - .flatten() - { - let (mut deps, _) = analysis::analyze_dependencies( - specifier, - parsed_module.media_type(), - parsed_module, - &self.maybe_import_map, - ); - for (_, dep) in deps.iter_mut() { - if dep.maybe_type.is_none() { - if let Some(ResolvedDependency::Resolved(resolved)) = &dep.maybe_code - { - dep.maybe_type = self.sources.get_maybe_types(resolved); - } - } - } - let dep_ranges = analysis::analyze_dependency_ranges(parsed_module).ok(); - if let Err(err) = - self - .documents - .set_dependencies(specifier, Some(deps), dep_ranges) - { - error!("{}", err); - } - } - } - - /// 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 specifiers = self - .documents - .docs - .keys() - .map(ToOwned::to_owned) - .collect::>(); - for specifier in specifiers { - self.analyze_dependencies(&specifier); - } - } - /// Searches assets, open documents and external sources for a line_index, /// which might be performed asynchronously, hydrating in memory caches for /// subsequent requests. pub(crate) async fn get_line_index( &mut self, specifier: ModuleSpecifier, - ) -> Result { + ) -> Result, AnyError> { let mark = self .performance .mark("get_line_index", Some(json!({ "specifier": specifier }))); @@ -242,8 +189,6 @@ impl Inner { } } else if let Some(line_index) = self.documents.line_index(&specifier) { Ok(line_index) - } else if let Some(line_index) = self.sources.get_line_index(&specifier) { - Ok(line_index) } else { Err(anyhow!("Unable to find line index for: {}", specifier)) }; @@ -256,7 +201,7 @@ impl Inner { pub fn get_line_index_sync( &self, specifier: &ModuleSpecifier, - ) -> Option { + ) -> Option> { let mark = self.performance.mark( "get_line_index_sync", Some(json!({ "specifier": specifier })), @@ -268,12 +213,7 @@ impl Inner { None } } else { - let documents = &self.documents; - if documents.contains_key(specifier) { - documents.line_index(specifier) - } else { - self.sources.get_line_index(specifier) - } + self.documents.line_index(specifier) }; self.performance.measure(mark); maybe_line_index @@ -294,31 +234,23 @@ impl Inner { .assets .get(specifier) .map(|o| o.clone().map(|a| a.text))? - } else if self.documents.contains_key(specifier) { - self.documents.content(specifier) } else { - self.sources.get_source(specifier) + self.documents.content(specifier) } } pub(crate) async fn get_navigation_tree( &mut self, specifier: &ModuleSpecifier, - ) -> Result { + ) -> Result, AnyError> { let mark = self.performance.mark( "get_navigation_tree", Some(json!({ "specifier": specifier })), ); let maybe_navigation_tree = if specifier.scheme() == "asset" { - self - .assets - .get(specifier) - .map(|o| o.clone().map(|a| a.maybe_navigation_tree).flatten()) - .flatten() - } else if self.documents.contains_key(specifier) { - self.documents.get_navigation_tree(specifier) + self.assets.get_navigation_tree(specifier) } else { - self.sources.get_navigation_tree(specifier) + self.documents.get_navigation_tree(specifier) }; let navigation_tree = if let Some(navigation_tree) = maybe_navigation_tree { navigation_tree @@ -330,17 +262,14 @@ impl Inner { tsc::RequestMethod::GetNavigationTree(specifier.clone()), ) .await?; + let navigation_tree = Arc::new(navigation_tree); if specifier.scheme() == "asset" { self .assets .set_navigation_tree(specifier, navigation_tree.clone())?; - } else if self.documents.contains_key(specifier) { - self - .documents - .set_navigation_tree(specifier, navigation_tree.clone())?; } else { self - .sources + .documents .set_navigation_tree(specifier, navigation_tree.clone())?; } navigation_tree @@ -394,6 +323,21 @@ impl Inner { Ok(None) } + fn is_diagnosable(&self, specifier: &ModuleSpecifier) -> bool { + if specifier.scheme() == "asset" { + matches!( + MediaType::from(specifier), + MediaType::JavaScript + | MediaType::Jsx + | MediaType::TypeScript + | MediaType::Tsx + | MediaType::Dts + ) + } else { + self.documents.is_diagnosable(specifier) + } + } + fn merge_user_tsconfig( &mut self, tsconfig: &mut TsConfig, @@ -424,7 +368,6 @@ impl Inner { maybe_config_uri: self.maybe_config_uri.clone(), module_registries: self.module_registries.clone(), performance: self.performance.clone(), - sources: self.sources.clone(), url_map: self.url_map.clone(), }) } @@ -479,8 +422,7 @@ impl Inner { self.module_registries = registries::ModuleRegistry::new(&module_registries_location); self.module_registries_location = module_registries_location; - let sources_location = dir.root.join(SOURCES_PATH); - self.sources = Sources::new(&sources_location); + self.documents.set_location(dir.root.join(CACHE_PATH)); self.maybe_cache_path = maybe_cache_path; } Ok(()) @@ -542,9 +484,9 @@ impl Inner { 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.clone()); - self.sources.set_import_map(Some(import_map)); + self.documents.set_import_map(Some(Arc::new(import_map))); } else { - self.sources.set_import_map(None); + self.documents.set_import_map(None); self.maybe_import_map = None; } self.performance.measure(mark); @@ -651,9 +593,9 @@ impl Inner { pub(crate) fn document_version( &self, - specifier: ModuleSpecifier, + specifier: &ModuleSpecifier, ) -> Option { - self.documents.version(&specifier) + self.documents.lsp_version(specifier) } async fn get_asset( @@ -833,15 +775,15 @@ impl Inner { params.text_document.language_id, params.text_document.uri ); } + let content = Arc::new(params.text_document.text); self.documents.open( specifier.clone(), params.text_document.version, - language_id, - Arc::new(params.text_document.text), + params.text_document.language_id.parse().unwrap(), + content, ); - if self.documents.is_diagnosable(&specifier) { - self.analyze_dependencies(&specifier); + if self.is_diagnosable(&specifier) { self .diagnostics_server .invalidate(self.documents.dependents(&specifier)) @@ -850,6 +792,7 @@ impl Inner { error!("{}", err); } } + self.performance.measure(mark); } @@ -862,8 +805,7 @@ impl Inner { params.content_changes, ) { Ok(()) => { - if self.documents.is_diagnosable(&specifier) { - self.analyze_dependencies(&specifier); + if self.is_diagnosable(&specifier) { self .diagnostics_server .invalidate(self.documents.dependents(&specifier)) @@ -887,15 +829,14 @@ impl Inner { return; } let specifier = self.url_map.normalize_url(¶ms.text_document.uri); - let is_diagnosable = self.documents.is_diagnosable(&specifier); - if is_diagnosable { + if let Err(err) = self.documents.close(&specifier) { + error!("{}", err); + } + if self.is_diagnosable(&specifier) { let mut specifiers = self.documents.dependents(&specifier); specifiers.push(specifier.clone()); self.diagnostics_server.invalidate(specifiers).await; - } - self.documents.close(&specifier); - if is_diagnosable { if let Err(err) = self.diagnostics_server.update() { error!("{}", err); } @@ -998,7 +939,6 @@ impl Inner { } } 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); @@ -1012,7 +952,7 @@ impl Inner { params: DocumentSymbolParams, ) -> LspResult> { let specifier = self.url_map.normalize_url(¶ms.text_document.uri); - if !self.documents.is_diagnosable(&specifier) + if !self.is_diagnosable(&specifier) || !self.config.specifier_enabled(&specifier) { return Ok(None); @@ -1020,15 +960,15 @@ impl Inner { let mark = self.performance.mark("document_symbol", Some(¶ms)); - let line_index = - if let Some(line_index) = self.get_line_index_sync(&specifier) { - line_index - } else { - return Err(LspError::invalid_params(format!( + let line_index = self.get_line_index_sync(&specifier).map_or_else( + || { + Err(LspError::invalid_params(format!( "An unexpected specifier ({}) was provided.", specifier - ))); - }; + ))) + }, + Ok, + )?; let req = tsc::RequestMethod::GetNavigationTree(specifier); let navigation_tree: tsc::NavigationTree = self @@ -1043,7 +983,8 @@ impl Inner { let response = if let Some(child_items) = navigation_tree.child_items { let mut document_symbols = Vec::::new(); for item in child_items { - item.collect_document_symbols(&line_index, &mut document_symbols); + item + .collect_document_symbols(line_index.clone(), &mut document_symbols); } Some(DocumentSymbolResponse::Nested(document_symbols)) } else { @@ -1062,11 +1003,6 @@ impl Inner { return Ok(None); } let mark = self.performance.mark("formatting", Some(¶ms)); - let document_data = self.documents.get(&specifier).ok_or_else(|| { - LspError::invalid_params( - "The specified file could not be found in memory.", - ) - })?; let file_path = if let Ok(file_path) = params.text_document.uri.to_file_path() { file_path @@ -1080,28 +1016,35 @@ impl Inner { Default::default() }; - let source = document_data.source().clone(); + let content = self.documents.content(&specifier).map_or_else( + || { + Err(LspError::invalid_params( + "The specified file could not be found in memory.", + )) + }, + Ok, + )?; + let line_index = self.documents.line_index(&specifier).unwrap(); + let maybe_parsed_source = self.documents.parsed_source(&specifier); + let text_edits = tokio::task::spawn_blocking(move || { - let format_result = match source.module() { - Some(Ok(parsed_module)) => { - format_parsed_source(parsed_module, fmt_options) + let format_result = match maybe_parsed_source { + Some(Ok(parsed_source)) => { + format_parsed_source(&parsed_source, fmt_options) } Some(Err(err)) => Err(err.to_string()), None => { // it's not a js/ts file, so attempt to format its contents - format_file(&file_path, source.text_info().text_str(), fmt_options) + format_file(&file_path, content.as_str(), fmt_options) } }; match format_result { - Ok(new_text) => { - let line_index = source.line_index(); - Some(text::get_edits( - source.text_info().text_str(), - &new_text, - line_index, - )) - } + Ok(new_text) => Some(text::get_edits( + content.as_str(), + &new_text, + line_index.as_ref(), + )), Err(err) => { // TODO(lucacasonato): handle error properly warn!("Format error: {}", err); @@ -1129,72 +1072,51 @@ impl Inner { let specifier = self .url_map .normalize_url(¶ms.text_document_position_params.text_document.uri); - if !self.documents.is_diagnosable(&specifier) + if !self.is_diagnosable(&specifier) || !self.config.specifier_enabled(&specifier) { return Ok(None); } let mark = self.performance.mark("hover", Some(¶ms)); - let hover = if let Some(dependency_range) = - self.documents.is_specifier_position( + let hover = if let Some((_, dep, range)) = + self.documents.get_maybe_dependency( &specifier, ¶ms.text_document_position_params.position, ) { - if let Some(dependencies) = &self.documents.dependencies(&specifier) { - if let Some(dep) = dependencies.get(&dependency_range.specifier) { - let value = match (&dep.maybe_code, &dep.maybe_type) { - (Some(code_dep), Some(type_dep)) => { - format!( - "**Resolved Dependency**\n\n**Code**: {}\n\n**Types**: {}\n", - code_dep.as_hover_text(), - type_dep.as_hover_text() - ) - } - (Some(code_dep), None) => { - format!( - "**Resolved Dependency**\n\n**Code**: {}\n", - code_dep.as_hover_text() - ) - } - (None, Some(type_dep)) => { - format!( - "**Resolved Dependency**\n\n**Types**: {}\n", - type_dep.as_hover_text() - ) - } - (None, None) => { - error!( - "Unexpected state hovering on dependency. Dependency \"{}\" in \"{}\" not found.", - dependency_range.specifier, - specifier - ); - "".to_string() - } - }; - Some(Hover { - contents: HoverContents::Markup(MarkupContent { - kind: MarkupKind::Markdown, - value, - }), - range: Some(dependency_range.range), - }) - } else { - None - } - } else { - None - } + let value = match (&dep.maybe_code, &dep.maybe_type) { + (Some(code_dep), Some(type_dep)) => format!( + "**Resolved Dependency**\n\n**Code**: {}\n\n**Types**: {}\n", + to_hover_text(code_dep), + to_hover_text(type_dep) + ), + (Some(code_dep), None) => format!( + "**Resolved Dependency**\n\n**Code**: {}\n", + to_hover_text(code_dep) + ), + (None, Some(type_dep)) => format!( + "**Resolved Dependency**\n\n**Types**: {}\n", + to_hover_text(type_dep) + ), + (None, None) => unreachable!("{}", json!(params)), + }; + Some(Hover { + contents: HoverContents::Markup(MarkupContent { + kind: MarkupKind::Markdown, + value, + }), + range: Some(to_lsp_range(&range)), + }) } else { - let line_index = - if let Some(line_index) = self.get_line_index_sync(&specifier) { - line_index - } else { - return Err(LspError::invalid_params(format!( + let line_index = self.get_line_index_sync(&specifier).map_or_else( + || { + Err(LspError::invalid_params(format!( "An unexpected specifier ({}) was provided.", specifier - ))); - }; + ))) + }, + Ok, + )?; let req = tsc::RequestMethod::GetQuickInfo(( specifier, line_index.offset_tsc(params.text_document_position_params.position)?, @@ -1207,7 +1129,7 @@ impl Inner { error!("Unable to get quick info: {}", err); LspError::internal_error() })?; - maybe_quick_info.map(|qi| qi.to_hover(&line_index)) + maybe_quick_info.map(|qi| qi.to_hover(line_index)) }; self.performance.measure(mark); Ok(hover) @@ -1218,7 +1140,7 @@ impl Inner { params: CodeActionParams, ) -> LspResult> { let specifier = self.url_map.normalize_url(¶ms.text_document.uri); - if !self.documents.is_diagnosable(&specifier) + if !self.is_diagnosable(&specifier) || !self.config.specifier_enabled(&specifier) { return Ok(None); @@ -1315,8 +1237,13 @@ impl Inner { Some("deno-lint") => code_actions .add_deno_lint_ignore_action( &specifier, - self.documents.get(&specifier), diagnostic, + self.documents.text_info(&specifier), + self + .documents + .parsed_source(&specifier) + .map(|r| r.ok()) + .flatten(), ) .map_err(|err| { error!("Unable to fix lint error: {}", err); @@ -1485,7 +1412,7 @@ impl Inner { params: CodeLensParams, ) -> LspResult>> { let specifier = self.url_map.normalize_url(¶ms.text_document.uri); - if !self.documents.is_diagnosable(&specifier) + if !self.is_diagnosable(&specifier) || !self.config.specifier_enabled(&specifier) || !(self.config.get_workspace_settings().enabled_code_lens() || self.config.specifier_code_lens_test(&specifier)) @@ -1499,25 +1426,25 @@ impl Inner { error!("Error getting code lenses for \"{}\": {}", specifier, err); LspError::internal_error() })?; - let parsed_module = self + let parsed_source = self .documents - .get(&specifier) - .map(|d| d.source().module()) - .flatten() - .map(|m| m.as_ref().ok()) + .parsed_source(&specifier) + .map(|r| r.ok()) .flatten(); - let line_index = self.get_line_index_sync(&specifier).ok_or_else(|| { - error!( - "Error getting code lenses for \"{}\": Missing line index", - specifier - ); - LspError::internal_error() - })?; + let line_index = self.get_line_index_sync(&specifier).map_or_else( + || { + Err(LspError::invalid_params(format!( + "An unexpected specifier ({}) was provided.", + specifier + ))) + }, + Ok, + )?; let code_lenses = code_lens::collect( &specifier, - parsed_module, + parsed_source, &self.config, - &line_index, + line_index, &navigation_tree, ) .await @@ -1558,22 +1485,22 @@ impl Inner { let specifier = self .url_map .normalize_url(¶ms.text_document_position_params.text_document.uri); - if !self.documents.is_diagnosable(&specifier) + if !self.is_diagnosable(&specifier) || !self.config.specifier_enabled(&specifier) { return Ok(None); } let mark = self.performance.mark("document_highlight", Some(¶ms)); - let line_index = - if let Some(line_index) = self.get_line_index_sync(&specifier) { - line_index - } else { - return Err(LspError::invalid_params(format!( + let line_index = self.get_line_index_sync(&specifier).map_or_else( + || { + Err(LspError::invalid_params(format!( "An unexpected specifier ({}) was provided.", specifier - ))); - }; + ))) + }, + Ok, + )?; let files_to_search = vec![specifier.clone()]; let req = tsc::RequestMethod::GetDocumentHighlights(( specifier, @@ -1592,7 +1519,7 @@ impl Inner { if let Some(document_highlights) = maybe_document_highlights { let result = document_highlights .into_iter() - .map(|dh| dh.to_highlight(&line_index)) + .map(|dh| dh.to_highlight(line_index.clone())) .flatten() .collect(); self.performance.measure(mark); @@ -1610,22 +1537,22 @@ impl Inner { let specifier = self .url_map .normalize_url(¶ms.text_document_position.text_document.uri); - if !self.documents.is_diagnosable(&specifier) + if !self.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) { - line_index - } else { - return Err(LspError::invalid_params(format!( + let line_index = self.get_line_index_sync(&specifier).map_or_else( + || { + Err(LspError::invalid_params(format!( "An unexpected specifier ({}) was provided.", specifier - ))); - }; + ))) + }, + Ok, + )?; let req = tsc::RequestMethod::GetReferences(( specifier, line_index.offset_tsc(params.text_document_position.position)?, @@ -1650,7 +1577,7 @@ impl Inner { // TODO(lucacasonato): handle error correctly let line_index = self.get_line_index(reference_specifier).await.unwrap(); - results.push(reference.to_location(&line_index, self)); + results.push(reference.to_location(line_index, self)); } self.performance.measure(mark); @@ -1668,22 +1595,22 @@ impl Inner { let specifier = self .url_map .normalize_url(¶ms.text_document_position_params.text_document.uri); - if !self.documents.is_diagnosable(&specifier) + if !self.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) { - line_index - } else { - return Err(LspError::invalid_params(format!( + let line_index = self.get_line_index_sync(&specifier).map_or_else( + || { + Err(LspError::invalid_params(format!( "An unexpected specifier ({}) was provided.", specifier - ))); - }; + ))) + }, + Ok, + )?; let req = tsc::RequestMethod::GetDefinition(( specifier, line_index.offset_tsc(params.text_document_position_params.position)?, @@ -1698,7 +1625,7 @@ impl Inner { })?; if let Some(definition) = maybe_definition { - let results = definition.to_definition(&line_index, self).await; + let results = definition.to_definition(line_index, self).await; self.performance.measure(mark); Ok(results) } else { @@ -1714,7 +1641,7 @@ impl Inner { let specifier = self .url_map .normalize_url(¶ms.text_document_position.text_document.uri); - if !self.documents.is_diagnosable(&specifier) + if !self.is_diagnosable(&specifier) || !self.config.specifier_enabled(&specifier) { return Ok(None); @@ -1735,15 +1662,15 @@ impl Inner { { Some(response) } else { - let line_index = - if let Some(line_index) = self.get_line_index_sync(&specifier) { - line_index - } else { - return Err(LspError::invalid_params(format!( + let line_index = self.get_line_index_sync(&specifier).map_or_else( + || { + Err(LspError::invalid_params(format!( "An unexpected specifier ({}) was provided.", specifier - ))); - }; + ))) + }, + Ok, + )?; let trigger_character = if let Some(context) = ¶ms.context { context.trigger_character.clone() } else { @@ -1777,7 +1704,7 @@ impl Inner { if let Some(completions) = maybe_completion_info { let results = completions.as_completion_response( - &line_index, + line_index, &self.config.get_workspace_settings().suggest, &specifier, position, @@ -1839,22 +1766,22 @@ impl Inner { let specifier = self .url_map .normalize_url(¶ms.text_document_position_params.text_document.uri); - if !self.documents.is_diagnosable(&specifier) + if !self.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) { - line_index - } else { - return Err(LspError::invalid_params(format!( + let line_index = self.get_line_index_sync(&specifier).map_or_else( + || { + Err(LspError::invalid_params(format!( "An unexpected specifier ({}) was provided.", specifier - ))); - }; + ))) + }, + Ok, + )?; let req = tsc::RequestMethod::GetImplementation(( specifier, @@ -1872,7 +1799,9 @@ impl Inner { let result = if let Some(implementations) = maybe_implementations { let mut links = Vec::new(); for implementation in implementations { - if let Some(link) = implementation.to_link(&line_index, self).await { + if let Some(link) = + implementation.to_link(line_index.clone(), self).await + { links.push(link) } } @@ -1890,22 +1819,22 @@ impl Inner { params: FoldingRangeParams, ) -> LspResult>> { let specifier = self.url_map.normalize_url(¶ms.text_document.uri); - if !self.documents.is_diagnosable(&specifier) + if !self.is_diagnosable(&specifier) || !self.config.specifier_enabled(&specifier) { return Ok(None); } 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 - } else { - return Err(LspError::invalid_params(format!( + let line_index = self.get_line_index_sync(&specifier).map_or_else( + || { + Err(LspError::invalid_params(format!( "An unexpected specifier ({}) was provided.", specifier - ))); - }; + ))) + }, + Ok, + )?; let req = tsc::RequestMethod::GetOutliningSpans(specifier.clone()); let outlining_spans: Vec = self @@ -1930,7 +1859,7 @@ impl Inner { .iter() .map(|span| { span.to_folding_range( - &line_index, + line_index.clone(), text_content.as_str().as_bytes(), self.config.client_capabilities.line_folding_only, ) @@ -1949,22 +1878,22 @@ impl Inner { params: CallHierarchyIncomingCallsParams, ) -> LspResult>> { let specifier = self.url_map.normalize_url(¶ms.item.uri); - if !self.documents.is_diagnosable(&specifier) + if !self.is_diagnosable(&specifier) || !self.config.specifier_enabled(&specifier) { return Ok(None); } 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 - } else { - return Err(LspError::invalid_params(format!( + let line_index = self.get_line_index_sync(&specifier).map_or_else( + || { + Err(LspError::invalid_params(format!( "An unexpected specifier ({}) was provided.", specifier - ))); - }; + ))) + }, + Ok, + )?; let req = tsc::RequestMethod::ProvideCallHierarchyIncomingCalls(( specifier.clone(), @@ -2005,22 +1934,22 @@ impl Inner { params: CallHierarchyOutgoingCallsParams, ) -> LspResult>> { let specifier = self.url_map.normalize_url(¶ms.item.uri); - if !self.documents.is_diagnosable(&specifier) + if !self.is_diagnosable(&specifier) || !self.config.specifier_enabled(&specifier) { return Ok(None); } 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 - } else { - return Err(LspError::invalid_params(format!( + let line_index = self.get_line_index_sync(&specifier).map_or_else( + || { + Err(LspError::invalid_params(format!( "An unexpected specifier ({}) was provided.", specifier - ))); - }; + ))) + }, + Ok, + )?; let req = tsc::RequestMethod::ProvideCallHierarchyOutgoingCalls(( specifier.clone(), @@ -2044,7 +1973,7 @@ impl Inner { for item in outgoing_calls.iter() { if let Some(resolved) = item .try_resolve_call_hierarchy_outgoing_call( - &line_index, + line_index.clone(), self, maybe_root_path_owned.as_deref(), ) @@ -2064,7 +1993,7 @@ impl Inner { let specifier = self .url_map .normalize_url(¶ms.text_document_position_params.text_document.uri); - if !self.documents.is_diagnosable(&specifier) + if !self.is_diagnosable(&specifier) || !self.config.specifier_enabled(&specifier) { return Ok(None); @@ -2073,15 +2002,15 @@ impl Inner { 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 - } else { - return Err(LspError::invalid_params(format!( + let line_index = self.get_line_index_sync(&specifier).map_or_else( + || { + Err(LspError::invalid_params(format!( "An unexpected specifier ({}) was provided.", specifier - ))); - }; + ))) + }, + Ok, + )?; let req = tsc::RequestMethod::PrepareCallHierarchy(( specifier.clone(), @@ -2145,22 +2074,22 @@ impl Inner { let specifier = self .url_map .normalize_url(¶ms.text_document_position.text_document.uri); - if !self.documents.is_diagnosable(&specifier) + if !self.is_diagnosable(&specifier) || !self.config.specifier_enabled(&specifier) { return Ok(None); } let mark = self.performance.mark("rename", Some(¶ms)); - let line_index = - if let Some(line_index) = self.get_line_index_sync(&specifier) { - line_index - } else { - return Err(LspError::invalid_params(format!( + let line_index = self.get_line_index_sync(&specifier).map_or_else( + || { + Err(LspError::invalid_params(format!( "An unexpected specifier ({}) was provided.", specifier - ))); - }; + ))) + }, + Ok, + )?; let req = tsc::RequestMethod::FindRenameLocations { specifier, @@ -2240,22 +2169,22 @@ impl Inner { params: SelectionRangeParams, ) -> LspResult>> { let specifier = self.url_map.normalize_url(¶ms.text_document.uri); - if !self.documents.is_diagnosable(&specifier) + if !self.is_diagnosable(&specifier) || !self.config.specifier_enabled(&specifier) { return Ok(None); } 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 - } else { - return Err(LspError::invalid_params(format!( + let line_index = self.get_line_index_sync(&specifier).map_or_else( + || { + Err(LspError::invalid_params(format!( "An unexpected specifier ({}) was provided.", specifier - ))); - }; + ))) + }, + Ok, + )?; let mut selection_ranges = Vec::::new(); for position in params.positions { @@ -2273,7 +2202,8 @@ impl Inner { LspError::invalid_request() })?; - selection_ranges.push(selection_range.to_selection_range(&line_index)); + selection_ranges + .push(selection_range.to_selection_range(line_index.clone())); } self.performance.measure(mark); Ok(Some(selection_ranges)) @@ -2284,22 +2214,22 @@ impl Inner { params: SemanticTokensParams, ) -> LspResult> { let specifier = self.url_map.normalize_url(¶ms.text_document.uri); - if !self.documents.is_diagnosable(&specifier) + if !self.is_diagnosable(&specifier) || !self.config.specifier_enabled(&specifier) { return Ok(None); } 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 - } else { - return Err(LspError::invalid_params(format!( + let line_index = self.get_line_index_sync(&specifier).map_or_else( + || { + Err(LspError::invalid_params(format!( "An unexpected specifier ({}) was provided.", specifier - ))); - }; + ))) + }, + Ok, + )?; let req = tsc::RequestMethod::GetEncodedSemanticClassifications(( specifier.clone(), @@ -2318,7 +2248,7 @@ impl Inner { })?; let semantic_tokens: SemanticTokens = - semantic_classification.to_semantic_tokens(&line_index); + semantic_classification.to_semantic_tokens(line_index); let response = if !semantic_tokens.data.is_empty() { Some(SemanticTokensResult::Tokens(semantic_tokens)) } else { @@ -2333,7 +2263,7 @@ impl Inner { params: SemanticTokensRangeParams, ) -> LspResult> { let specifier = self.url_map.normalize_url(¶ms.text_document.uri); - if !self.documents.is_diagnosable(&specifier) + if !self.is_diagnosable(&specifier) || !self.config.specifier_enabled(&specifier) { return Ok(None); @@ -2342,15 +2272,15 @@ impl Inner { 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 - } else { - return Err(LspError::invalid_params(format!( + let line_index = self.get_line_index_sync(&specifier).map_or_else( + || { + Err(LspError::invalid_params(format!( "An unexpected specifier ({}) was provided.", specifier - ))); - }; + ))) + }, + Ok, + )?; let start = line_index.offset_tsc(params.range.start)?; let length = line_index.offset_tsc(params.range.end)? - start; @@ -2368,7 +2298,7 @@ impl Inner { })?; let semantic_tokens: SemanticTokens = - semantic_classification.to_semantic_tokens(&line_index); + semantic_classification.to_semantic_tokens(line_index); let response = if !semantic_tokens.data.is_empty() { Some(SemanticTokensRangeResult::Tokens(semantic_tokens)) } else { @@ -2385,22 +2315,22 @@ impl Inner { let specifier = self .url_map .normalize_url(¶ms.text_document_position_params.text_document.uri); - if !self.documents.is_diagnosable(&specifier) + if !self.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) { - line_index - } else { - return Err(LspError::invalid_params(format!( + let line_index = self.get_line_index_sync(&specifier).map_or_else( + || { + Err(LspError::invalid_params(format!( "An unexpected specifier ({}) was provided.", specifier - ))); - }; + ))) + }, + Ok, + )?; let options = if let Some(context) = params.context { tsc::SignatureHelpItemsOptions { trigger_reason: Some(tsc::SignatureHelpTriggerReason { @@ -2658,7 +2588,7 @@ impl Inner { params: lsp_custom::CacheParams, ) -> LspResult> { let referrer = self.url_map.normalize_url(¶ms.referrer.uri); - if !self.documents.is_diagnosable(&referrer) { + if !self.is_diagnosable(&referrer) { return Ok(None); } @@ -2689,10 +2619,7 @@ impl Inner { // now that we have dependencies loaded, we need to re-analyze them and // invalidate some diagnostics - if self.documents.contains_key(&referrer) { - self.analyze_dependencies(&referrer); - self.diagnostics_server.invalidate(vec![referrer]).await; - } + self.diagnostics_server.invalidate(vec![referrer]).await; self.diagnostics_server.update().map_err(|err| { error!("{}", err); @@ -2731,12 +2658,15 @@ impl Inner { .performance .mark("virtual_text_document", Some(¶ms)); let specifier = self.url_map.normalize_url(¶ms.text_document.uri); + info!( + "virtual_text_document\n{}\nspecifier: {}", + json!(params), + specifier + ); let contents = if specifier.as_str() == "deno:/status.md" { let mut contents = String::new(); - let mut documents_specifiers = self.documents.specifiers(); + let mut documents_specifiers = self.documents.specifiers(false, false); documents_specifiers.sort(); - let mut sources_specifiers = self.sources.specifiers(); - sources_specifiers.sort(); let measures = self.performance.to_vec(); let workspace_settings = self.config.get_workspace_settings(); @@ -2757,12 +2687,6 @@ impl Inner { - -
Sources in memory: {} - - - {} - -
- -
Performance measures: {} - {} @@ -2770,18 +2694,12 @@ impl Inner {
"#, serde_json::to_string_pretty(&workspace_settings).unwrap(), - self.documents.len(), + documents_specifiers.len(), documents_specifiers .into_iter() .map(|s| s.to_string()) .collect::>() .join("\n - "), - self.sources.len(), - sources_specifiers - .into_iter() - .map(|s| s.to_string()) - .collect::>() - .join("\n - "), measures.len(), measures .iter() @@ -2815,7 +2733,7 @@ impl Inner { } } _ => { - if let Some(source) = self.sources.get_source(&specifier) { + if let Some(source) = self.documents.content(&specifier) { Some(source.to_string()) } else { error!("The cached source was not found: {}", specifier); diff --git a/cli/lsp/mod.rs b/cli/lsp/mod.rs index e6cc882089..725ca07b30 100644 --- a/cli/lsp/mod.rs +++ b/cli/lsp/mod.rs @@ -11,7 +11,6 @@ mod code_lens; mod completions; mod config; mod diagnostics; -mod document_source; mod documents; pub(crate) mod language_server; mod lsp_custom; @@ -20,8 +19,8 @@ mod path_to_regex; mod performance; mod refactor; mod registries; +mod resolver; mod semantic_tokens; -mod sources; mod text; mod tsc; mod urls; diff --git a/cli/lsp/registries.rs b/cli/lsp/registries.rs index c288666d51..504ff27381 100644 --- a/cli/lsp/registries.rs +++ b/cli/lsp/registries.rs @@ -424,7 +424,7 @@ impl ModuleRegistry { /// For a string specifier from the client, provide a set of completions, if /// any, for the specifier. - pub async fn get_completions( + pub(crate) async fn get_completions( &self, current_specifier: &str, offset: usize, @@ -520,8 +520,8 @@ impl ModuleRegistry { )); let command = if key.name == last_key_name && !state_snapshot - .sources - .contains_key(&item_specifier) + .documents + .contains_specifier(&item_specifier) { Some(lsp::Command { title: "".to_string(), @@ -610,8 +610,8 @@ impl ModuleRegistry { ); let command = if k.name == last_key_name && !state_snapshot - .sources - .contains_key(&item_specifier) + .documents + .contains_specifier(&item_specifier) { Some(lsp::Command { title: "".to_string(), @@ -761,16 +761,14 @@ impl ModuleRegistry { #[cfg(test)] mod tests { use super::*; - use crate::lsp::documents::DocumentCache; - use crate::lsp::sources::Sources; + use crate::lsp::documents::Documents; use tempfile::TempDir; fn mock_state_snapshot( source_fixtures: &[(&str, &str)], location: &Path, ) -> language_server::StateSnapshot { - let documents = DocumentCache::default(); - let sources = Sources::new(location); + let documents = Documents::new(location); let http_cache = HttpCache::new(location); for (specifier, source) in source_fixtures { let specifier = @@ -779,13 +777,12 @@ mod tests { .set(&specifier, HashMap::default(), source.as_bytes()) .expect("could not cache file"); assert!( - sources.get_source(&specifier).is_some(), + documents.content(&specifier).is_some(), "source could not be setup" ); } language_server::StateSnapshot { documents, - sources, ..Default::default() } } diff --git a/cli/lsp/resolver.rs b/cli/lsp/resolver.rs new file mode 100644 index 0000000000..4f768b697f --- /dev/null +++ b/cli/lsp/resolver.rs @@ -0,0 +1,33 @@ +// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license. + +use deno_core::error::AnyError; +use deno_core::ModuleSpecifier; +use deno_graph::source::Resolver; +use import_map::ImportMap; +use std::sync::Arc; + +#[derive(Debug)] +pub(crate) struct ImportMapResolver(Arc); + +impl ImportMapResolver { + pub fn new(import_map: Arc) -> Self { + Self(import_map) + } + + pub fn as_resolver(&self) -> &dyn Resolver { + self + } +} + +impl Resolver for ImportMapResolver { + fn resolve( + &self, + specifier: &str, + referrer: &ModuleSpecifier, + ) -> Result { + self + .0 + .resolve(specifier, referrer.as_str()) + .map_err(|err| err.into()) + } +} diff --git a/cli/lsp/sources.rs b/cli/lsp/sources.rs deleted file mode 100644 index 69de6d976c..0000000000 --- a/cli/lsp/sources.rs +++ /dev/null @@ -1,797 +0,0 @@ -// 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 super::urls::INVALID_SPECIFIER; - -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_core::error::anyhow; -use deno_core::error::AnyError; -use deno_core::parking_lot::Mutex; -use deno_core::serde_json; -use deno_core::ModuleSpecifier; -use import_map::ImportMap; -use std::collections::HashMap; -use std::fs; -use std::path::Path; -use std::path::PathBuf; -use std::sync::Arc; -use std::time::SystemTime; -use tsc::NavigationTree; - -fn get_remote_headers( - cache_filename: &Path, -) -> Option> { - let metadata_path = http_cache::Metadata::filename(cache_filename); - let metadata_str = fs::read_to_string(metadata_path).ok()?; - let metadata: http_cache::Metadata = - serde_json::from_str(&metadata_str).ok()?; - Some(metadata.headers) -} - -fn resolve_remote_specifier( - specifier: &ModuleSpecifier, - http_cache: &HttpCache, - redirect_limit: isize, -) -> Option { - let cache_filename = http_cache.get_cache_filename(specifier)?; - if redirect_limit >= 0 && cache_filename.is_file() { - let headers = get_remote_headers(&cache_filename)?; - if let Some(location) = headers.get("location") { - let redirect = - deno_core::resolve_import(location, specifier.as_str()).ok()?; - resolve_remote_specifier(&redirect, http_cache, redirect_limit - 1) - } else { - Some(specifier.clone()) - } - } else { - None - } -} - -fn resolve_specifier( - specifier: &ModuleSpecifier, - redirects: &mut HashMap, - http_cache: &HttpCache, -) -> Option { - let scheme = specifier.scheme(); - if !SUPPORTED_SCHEMES.contains(&scheme) { - return None; - } - - if scheme == "data" { - Some(specifier.clone()) - } else if scheme == "file" { - let path = specifier.to_file_path().ok()?; - if path.is_file() { - Some(specifier.clone()) - } else { - None - } - } else if let Some(specifier) = redirects.get(specifier) { - Some(specifier.clone()) - } else { - let redirect = resolve_remote_specifier(specifier, http_cache, 10)?; - redirects.insert(specifier.clone(), redirect.clone()); - Some(redirect) - } -} - -#[derive(Debug, Clone)] -struct Metadata { - dependencies: Option>, - length_utf16: usize, - maybe_navigation_tree: Option, - maybe_types: Option, - maybe_warning: Option, - media_type: MediaType, - source: DocumentSource, - specifier: ModuleSpecifier, - version: String, -} - -impl Default for Metadata { - fn default() -> Self { - Self { - dependencies: None, - length_utf16: 0, - maybe_navigation_tree: None, - maybe_types: None, - maybe_warning: None, - media_type: MediaType::default(), - source: DocumentSource::new( - &INVALID_SPECIFIER, - MediaType::default(), - Arc::new(String::default()), - LineIndex::default(), - ), - specifier: INVALID_SPECIFIER.clone(), - version: String::default(), - } - } -} - -impl Metadata { - fn new( - specifier: &ModuleSpecifier, - source: Arc, - version: &str, - media_type: MediaType, - maybe_warning: Option, - maybe_import_map: &Option, - ) -> Self { - let line_index = LineIndex::new(&source); - let document_source = - DocumentSource::new(specifier, media_type, source, line_index); - let (dependencies, maybe_types) = - if let Some(Ok(parsed_module)) = document_source.module() { - let (deps, maybe_types) = analysis::analyze_dependencies( - specifier, - media_type, - parsed_module, - maybe_import_map, - ); - (Some(deps), maybe_types) - } else { - (None, None) - }; - - Self { - dependencies, - length_utf16: document_source - .text_info() - .text_str() - .encode_utf16() - .count(), - maybe_navigation_tree: None, - maybe_types, - maybe_warning, - media_type: media_type.to_owned(), - source: document_source, - specifier: specifier.clone(), - version: version.to_string(), - } - } - - fn refresh(&mut self, maybe_import_map: &Option) { - let (dependencies, maybe_types) = - if let Some(Ok(parsed_module)) = self.source.module() { - 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)] -struct Inner { - http_cache: HttpCache, - maybe_import_map: Option, - metadata: HashMap, - redirects: HashMap, - remotes: HashMap, -} - -#[derive(Debug, Clone, Default)] -pub struct Sources(Arc>); - -impl Sources { - pub fn new(location: &Path) -> Self { - Self(Arc::new(Mutex::new(Inner::new(location)))) - } - - pub fn contains_key(&self, specifier: &ModuleSpecifier) -> bool { - self.0.lock().contains_key(specifier) - } - - pub fn get_line_index( - &self, - specifier: &ModuleSpecifier, - ) -> Option { - self.0.lock().get_line_index(specifier) - } - - pub fn get_maybe_types( - &self, - specifier: &ModuleSpecifier, - ) -> Option { - self.0.lock().get_maybe_types(specifier) - } - - pub fn get_maybe_warning( - &self, - specifier: &ModuleSpecifier, - ) -> Option { - self.0.lock().get_maybe_warning(specifier) - } - - pub fn get_media_type( - &self, - specifier: &ModuleSpecifier, - ) -> Option { - self.0.lock().get_media_type(specifier) - } - - pub fn get_navigation_tree( - &self, - specifier: &ModuleSpecifier, - ) -> Option { - self.0.lock().get_navigation_tree(specifier) - } - - pub fn get_script_version( - &self, - specifier: &ModuleSpecifier, - ) -> Option { - self.0.lock().get_script_version(specifier) - } - - pub fn get_source(&self, specifier: &ModuleSpecifier) -> Option> { - self.0.lock().get_source(specifier) - } - - pub fn len(&self) -> usize { - self.0.lock().metadata.len() - } - - pub fn resolve_import( - &self, - specifier: &str, - referrer: &ModuleSpecifier, - ) -> Option<(ModuleSpecifier, MediaType)> { - self.0.lock().resolve_import(specifier, referrer) - } - - pub fn specifiers(&self) -> Vec { - 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, - navigation_tree: tsc::NavigationTree, - ) -> Result<(), AnyError> { - self - .0 - .lock() - .set_navigation_tree(specifier, navigation_tree) - } -} - -impl Inner { - fn new(location: &Path) -> Self { - Self { - http_cache: HttpCache::new(location), - ..Default::default() - } - } - - fn calculate_script_version( - &mut self, - specifier: &ModuleSpecifier, - ) -> Option { - 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(format!("{}", n.as_millis())) - } else { - Some("1".to_string()) - } - } else { - Some("1".to_string()) - } - } - - fn contains_key(&mut self, specifier: &ModuleSpecifier) -> bool { - if let Some(specifier) = - resolve_specifier(specifier, &mut self.redirects, &self.http_cache) - { - if self.get_metadata(&specifier).is_some() { - return true; - } - } - false - } - - fn get_line_index( - &mut self, - specifier: &ModuleSpecifier, - ) -> Option { - let specifier = - resolve_specifier(specifier, &mut self.redirects, &self.http_cache)?; - let metadata = self.get_metadata(&specifier)?; - Some(metadata.source.line_index().clone()) - } - - fn get_maybe_types( - &mut self, - specifier: &ModuleSpecifier, - ) -> Option { - let specifier = - resolve_specifier(specifier, &mut self.redirects, &self.http_cache)?; - let metadata = self.get_metadata(&specifier)?; - metadata.maybe_types - } - - fn get_maybe_warning( - &mut self, - specifier: &ModuleSpecifier, - ) -> Option { - let metadata = self.get_metadata(specifier)?; - metadata.maybe_warning - } - - fn get_media_type( - &mut self, - specifier: &ModuleSpecifier, - ) -> Option { - let specifier = - resolve_specifier(specifier, &mut self.redirects, &self.http_cache)?; - let metadata = self.get_metadata(&specifier)?; - Some(metadata.media_type) - } - - fn get_metadata(&mut self, specifier: &ModuleSpecifier) -> Option { - if let Some(metadata) = self.metadata.get(specifier).cloned() { - if metadata.version == self.calculate_script_version(specifier)? { - return Some(metadata); - } - } - - let version = self.calculate_script_version(specifier)?; - let path = self.get_path(specifier)?; - let bytes = fs::read(path).ok()?; - let scheme = specifier.scheme(); - let (source, media_type, maybe_types, maybe_warning) = if scheme == "file" { - let maybe_charset = - Some(text_encoding::detect_charset(&bytes).to_string()); - let source = get_source_from_bytes(bytes, maybe_charset).ok()?; - (source, MediaType::from(specifier), None, None) - } else { - let cache_filename = self.http_cache.get_cache_filename(specifier)?; - let headers = get_remote_headers(&cache_filename)?; - let maybe_content_type = headers.get("content-type").cloned(); - let (media_type, maybe_charset) = - map_content_type(specifier, maybe_content_type); - let source = get_source_from_bytes(bytes, maybe_charset).ok()?; - let maybe_types = headers.get("x-typescript-types").map(|s| { - analysis::resolve_import(s, specifier, &self.maybe_import_map) - }); - let maybe_warning = headers.get("x-deno-warning").cloned(); - (source, media_type, maybe_types, maybe_warning) - }; - let mut metadata = Metadata::new( - specifier, - Arc::new(source), - &version, - media_type, - maybe_warning, - &self.maybe_import_map, - ); - if maybe_types.is_some() { - metadata.maybe_types = maybe_types; - } - self.metadata.insert(specifier.clone(), metadata.clone()); - Some(metadata) - } - - fn get_navigation_tree( - &mut self, - specifier: &ModuleSpecifier, - ) -> Option { - let specifier = - resolve_specifier(specifier, &mut self.redirects, &self.http_cache)?; - let metadata = self.get_metadata(&specifier)?; - metadata.maybe_navigation_tree - } - - fn get_path(&mut self, specifier: &ModuleSpecifier) -> Option { - if specifier.scheme() == "file" { - specifier.to_file_path().ok() - } else if let Some(path) = self.remotes.get(specifier) { - Some(path.clone()) - } else { - let path = self.http_cache.get_cache_filename(specifier)?; - if path.is_file() { - self.remotes.insert(specifier.clone(), path.clone()); - Some(path) - } else { - None - } - } - } - - fn get_script_version( - &mut self, - specifier: &ModuleSpecifier, - ) -> Option { - let specifier = - resolve_specifier(specifier, &mut self.redirects, &self.http_cache)?; - let metadata = self.get_metadata(&specifier)?; - Some(metadata.version) - } - - fn get_source(&mut self, specifier: &ModuleSpecifier) -> Option> { - let specifier = - resolve_specifier(specifier, &mut self.redirects, &self.http_cache)?; - let metadata = self.get_metadata(&specifier)?; - Some(metadata.source.text_info().text()) - } - - fn resolution_result( - &mut self, - specifier: &ModuleSpecifier, - ) -> Option<(ModuleSpecifier, MediaType)> { - let specifier = - resolve_specifier(specifier, &mut self.redirects, &self.http_cache)?; - let media_type = if let Some(metadata) = self.metadata.get(&specifier) { - metadata.media_type - } else { - MediaType::from(&specifier) - }; - Some((specifier, media_type)) - } - - fn resolve_import( - &mut self, - specifier: &str, - referrer: &ModuleSpecifier, - ) -> Option<(ModuleSpecifier, MediaType)> { - let referrer = - resolve_specifier(referrer, &mut self.redirects, &self.http_cache)?; - let metadata = self.get_metadata(&referrer)?; - let dependencies = &metadata.dependencies?; - let dependency = dependencies.get(specifier)?; - if let Some(type_dependency) = &dependency.maybe_type { - if let analysis::ResolvedDependency::Resolved(resolved_specifier) = - type_dependency - { - // even if we have a module in the maybe_types slot, it doesn't mean - // that it is the actual module we should be using based on headers, - // so we check here and update properly. - if let Some(type_dependency) = self.get_maybe_types(resolved_specifier) - { - self.set_maybe_type(specifier, &referrer, &type_dependency); - if let analysis::ResolvedDependency::Resolved(type_specifier) = - type_dependency - { - self.resolution_result(&type_specifier) - } else { - self.resolution_result(resolved_specifier) - } - } else { - self.resolution_result(resolved_specifier) - } - } else { - None - } - } else { - let code_dependency = &dependency.maybe_code.clone()?; - if let analysis::ResolvedDependency::Resolved(resolved_specifier) = - code_dependency - { - if let Some(type_dependency) = self.get_maybe_types(resolved_specifier) - { - self.set_maybe_type(specifier, &referrer, &type_dependency); - if let analysis::ResolvedDependency::Resolved(type_specifier) = - type_dependency - { - self.resolution_result(&type_specifier) - } else { - self.resolution_result(resolved_specifier) - } - } else { - self.resolution_result(resolved_specifier) - } - } else { - None - } - } - } - - 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, - referrer: &ModuleSpecifier, - dependency: &analysis::ResolvedDependency, - ) { - if let Some(metadata) = self.metadata.get_mut(referrer) { - if let Some(dependencies) = &mut metadata.dependencies { - if let Some(dep) = dependencies.get_mut(specifier) { - dep.maybe_type = Some(dependency.clone()); - } - } - } - } - - fn set_navigation_tree( - &mut self, - specifier: &ModuleSpecifier, - navigation_tree: NavigationTree, - ) -> Result<(), AnyError> { - let mut metadata = self - .metadata - .get_mut(specifier) - .ok_or_else(|| anyhow!("Specifier not found {}"))?; - metadata.maybe_navigation_tree = Some(navigation_tree); - Ok(()) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use deno_core::resolve_path; - use deno_core::resolve_url; - use deno_core::serde_json::json; - use tempfile::TempDir; - - fn setup() -> (Sources, PathBuf) { - let temp_dir = TempDir::new().expect("could not create temp dir"); - let location = temp_dir.path().join("deps"); - let sources = Sources::new(&location); - (sources, location) - } - - #[test] - fn test_sources_get_script_version() { - let (sources, _) = setup(); - let tests = test_util::testdata_path(); - let specifier = - resolve_path(&tests.join("001_hello.js").to_string_lossy()).unwrap(); - let actual = sources.get_script_version(&specifier); - assert!(actual.is_some()); - } - - #[test] - fn test_sources_get_text() { - let (sources, _) = setup(); - let tests = test_util::testdata_path(); - let specifier = - resolve_path(&tests.join("001_hello.js").to_string_lossy()).unwrap(); - let actual = sources.get_source(&specifier); - assert!(actual.is_some()); - let actual = actual.unwrap().to_string(); - assert_eq!(actual, "console.log(\"Hello World\");\n"); - } - - #[test] - fn test_resolve_dependency_types() { - let (sources, location) = setup(); - let cache = HttpCache::new(&location); - let specifier_dep = resolve_url("https://deno.land/x/mod.ts").unwrap(); - cache - .set( - &specifier_dep, - Default::default(), - b"export * from \"https://deno.land/x/lib.js\";", - ) - .unwrap(); - let specifier_code = resolve_url("https://deno.land/x/lib.js").unwrap(); - let mut headers_code = HashMap::new(); - headers_code - .insert("x-typescript-types".to_string(), "./lib.d.ts".to_string()); - cache - .set(&specifier_code, headers_code, b"export const a = 1;") - .unwrap(); - let specifier_type = resolve_url("https://deno.land/x/lib.d.ts").unwrap(); - cache - .set( - &specifier_type, - Default::default(), - b"export const a: number;", - ) - .unwrap(); - let actual = - sources.resolve_import("https://deno.land/x/lib.js", &specifier_dep); - assert_eq!(actual, Some((specifier_type, MediaType::Dts))) - } - - #[test] - /// This is a regression test for https://github.com/denoland/deno/issues/10031 - fn test_resolve_dependency_import_types() { - let (sources, location) = setup(); - let cache = HttpCache::new(&location); - let specifier_dep = resolve_url("https://deno.land/x/mod.ts").unwrap(); - cache - .set( - &specifier_dep, - Default::default(), - b"import type { A } from \"https://deno.land/x/lib.js\";\nconst a: A = { a: \"a\" };", - ) - .unwrap(); - let specifier_code = resolve_url("https://deno.land/x/lib.js").unwrap(); - let mut headers_code = HashMap::new(); - headers_code - .insert("x-typescript-types".to_string(), "./lib.d.ts".to_string()); - cache - .set(&specifier_code, headers_code, b"export const a = 1;") - .unwrap(); - let specifier_type = resolve_url("https://deno.land/x/lib.d.ts").unwrap(); - cache - .set( - &specifier_type, - Default::default(), - b"export const a: number;\nexport interface A { a: number; }\n", - ) - .unwrap(); - let actual = - sources.resolve_import("https://deno.land/x/lib.js", &specifier_dep); - assert_eq!(actual, Some((specifier_type, MediaType::Dts))) - } - - #[test] - fn test_warning_header() { - let (sources, location) = setup(); - let cache = HttpCache::new(&location); - let specifier = resolve_url("https://deno.land/x/lib.js").unwrap(); - let mut headers = HashMap::new(); - headers.insert( - "x-deno-warning".to_string(), - "this is a warning".to_string(), - ); - cache - .set(&specifier, headers, b"export const a = 1;") - .unwrap(); - let actual = sources.get_maybe_warning(&specifier); - assert_eq!(actual, Some("this is a warning".to_string())); - } - - #[test] - fn test_resolve_dependency_evil_redirect() { - let (sources, location) = setup(); - let cache = HttpCache::new(&location); - let evil_specifier = resolve_url("https://deno.land/x/evil.ts").unwrap(); - let mut evil_headers = HashMap::new(); - evil_headers - .insert("location".to_string(), "file:///etc/passwd".to_string()); - cache.set(&evil_specifier, evil_headers, b"").unwrap(); - let remote_specifier = resolve_url("https://deno.land/x/mod.ts").unwrap(); - cache - .set( - &remote_specifier, - Default::default(), - b"export * from \"./evil.ts\";", - ) - .unwrap(); - let actual = sources.resolve_import("./evil.ts", &remote_specifier); - 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(); - let specifier = - resolve_url("foo://a/b/c.ts").expect("could not create specifier"); - let sources = sources.0.lock(); - let mut redirects = sources.redirects.clone(); - let http_cache = sources.http_cache.clone(); - let actual = resolve_specifier(&specifier, &mut redirects, &http_cache); - assert!(actual.is_none()); - } -} diff --git a/cli/lsp/tsc.rs b/cli/lsp/tsc.rs index 9899fad724..8f3db6131e 100644 --- a/cli/lsp/tsc.rs +++ b/cli/lsp/tsc.rs @@ -1,7 +1,5 @@ // Copyright 2018-2021 the Deno authors. All rights reserved. MIT license. -use super::analysis::ResolvedDependency; -use super::analysis::ResolvedDependencyErr; use super::code_lens; use super::config; use super::language_server; @@ -22,7 +20,6 @@ use crate::tokio_util::create_basic_runtime; use crate::tsc; use crate::tsc::ResolveArgs; -use deno_ast::MediaType; use deno_core::error::anyhow; use deno_core::error::custom_error; use deno_core::error::AnyError; @@ -106,7 +103,7 @@ impl TsServer { Self(tx) } - pub async fn request( + pub(crate) async fn request( &self, snapshot: StateSnapshot, req: RequestMethod, @@ -128,8 +125,8 @@ impl TsServer { pub struct AssetDocument { pub text: Arc, pub length: usize, - pub line_index: LineIndex, - pub maybe_navigation_tree: Option, + pub line_index: Arc, + pub maybe_navigation_tree: Option>, } impl AssetDocument { @@ -138,7 +135,7 @@ impl AssetDocument { Self { text: Arc::new(text.to_string()), length: text.encode_utf16().count(), - line_index: LineIndex::new(text), + line_index: Arc::new(LineIndex::new(text)), maybe_navigation_tree: None, } } @@ -179,10 +176,18 @@ impl Assets { self.0.insert(k, v) } + pub fn get_navigation_tree( + &self, + specifier: &ModuleSpecifier, + ) -> Option> { + let doc = self.0.get(specifier).map(|v| v.as_ref()).flatten()?; + doc.maybe_navigation_tree.as_ref().cloned() + } + pub fn set_navigation_tree( &mut self, specifier: &ModuleSpecifier, - navigation_tree: NavigationTree, + navigation_tree: Arc, ) -> Result<(), AnyError> { let maybe_doc = self .0 @@ -199,7 +204,7 @@ impl Assets { /// Optionally returns an internal asset, first checking for any static assets /// in Rust, then checking any previously retrieved static assets from the /// isolate, and then finally, the tsc isolate itself. -pub async fn get_asset( +pub(crate) async fn get_asset( specifier: &ModuleSpecifier, ts_server: &TsServer, state_snapshot: StateSnapshot, @@ -498,7 +503,7 @@ pub struct TextSpan { } impl TextSpan { - pub fn to_range(&self, line_index: &LineIndex) -> lsp::Range { + pub fn to_range(&self, line_index: Arc) -> lsp::Range { lsp::Range { start: line_index.position_tsc(self.start.into()), end: line_index.position_tsc(TextSize::from(self.start + self.length)), @@ -532,7 +537,7 @@ pub struct QuickInfo { } impl QuickInfo { - pub fn to_hover(&self, line_index: &LineIndex) -> lsp::Hover { + pub fn to_hover(&self, line_index: Arc) -> lsp::Hover { let mut contents = Vec::::new(); if let Some(display_string) = self .display_parts @@ -585,7 +590,7 @@ pub struct DocumentSpan { impl DocumentSpan { pub(crate) async fn to_link( &self, - line_index: &LineIndex, + line_index: Arc, language_server: &mut language_server::Inner, ) -> Option { let target_specifier = normalize_specifier(&self.file_name).ok()?; @@ -600,13 +605,13 @@ impl DocumentSpan { let (target_range, target_selection_range) = if let Some(context_span) = &self.context_span { ( - context_span.to_range(&target_line_index), - self.text_span.to_range(&target_line_index), + context_span.to_range(target_line_index.clone()), + self.text_span.to_range(target_line_index), ) } else { ( - self.text_span.to_range(&target_line_index), - self.text_span.to_range(&target_line_index), + self.text_span.to_range(target_line_index.clone()), + self.text_span.to_range(target_line_index), ) }; let origin_selection_range = @@ -642,7 +647,7 @@ pub struct NavigationTree { impl NavigationTree { pub fn to_code_lens( &self, - line_index: &LineIndex, + line_index: Arc, specifier: &ModuleSpecifier, source: &code_lens::CodeLensSource, ) -> lsp::CodeLens { @@ -666,7 +671,7 @@ impl NavigationTree { pub fn collect_document_symbols( &self, - line_index: &LineIndex, + line_index: Arc, document_symbols: &mut Vec, ) -> bool { let mut should_include = self.should_include_entry(); @@ -692,8 +697,8 @@ impl NavigationTree { }) .any(|child_range| range.intersect(child_range).is_some()); if should_traverse_child { - let included_child = - child.collect_document_symbols(line_index, &mut symbol_children); + let included_child = child + .collect_document_symbols(line_index.clone(), &mut symbol_children); should_include = should_include || included_child; } } @@ -727,8 +732,8 @@ impl NavigationTree { document_symbols.push(lsp::DocumentSymbol { name: self.text.clone(), kind: self.kind.clone().into(), - range: span.to_range(line_index), - selection_range: selection_span.to_range(line_index), + range: span.to_range(line_index.clone()), + selection_range: selection_span.to_range(line_index.clone()), tags, children, detail: None, @@ -786,7 +791,7 @@ pub struct ImplementationLocation { impl ImplementationLocation { pub(crate) fn to_location( &self, - line_index: &LineIndex, + line_index: Arc, language_server: &mut language_server::Inner, ) -> lsp::Location { let specifier = normalize_specifier(&self.document_span.file_name) @@ -803,7 +808,7 @@ impl ImplementationLocation { pub(crate) async fn to_link( &self, - line_index: &LineIndex, + line_index: Arc, language_server: &mut language_server::Inner, ) -> Option { self @@ -846,7 +851,7 @@ impl RenameLocations { lsp::TextDocumentEdit { text_document: lsp::OptionalVersionedTextDocumentIdentifier { uri: uri.clone(), - version: language_server.document_version(specifier.clone()), + version: language_server.document_version(&specifier), }, edits: Vec::>::new(), @@ -860,7 +865,7 @@ impl RenameLocations { range: location .document_span .text_span - .to_range(&language_server.get_line_index(specifier.clone()).await?), + .to_range(language_server.get_line_index(specifier.clone()).await?), new_text: new_name.to_string(), })); } @@ -919,14 +924,16 @@ pub struct DefinitionInfoAndBoundSpan { impl DefinitionInfoAndBoundSpan { pub(crate) async fn to_definition( &self, - line_index: &LineIndex, + line_index: Arc, language_server: &mut language_server::Inner, ) -> Option { if let Some(definitions) = &self.definitions { let mut location_links = Vec::::new(); for di in definitions { - if let Some(link) = - di.document_span.to_link(line_index, language_server).await + if let Some(link) = di + .document_span + .to_link(line_index.clone(), language_server) + .await { location_links.push(link); } @@ -948,13 +955,13 @@ pub struct DocumentHighlights { impl DocumentHighlights { pub fn to_highlight( &self, - line_index: &LineIndex, + line_index: Arc, ) -> Vec { self .highlight_spans .iter() .map(|hs| lsp::DocumentHighlight { - range: hs.text_span.to_range(line_index), + range: hs.text_span.to_range(line_index.clone()), kind: match hs.kind { HighlightSpanKind::WrittenReference => { Some(lsp::DocumentHighlightKind::Write) @@ -976,7 +983,7 @@ pub struct TextChange { impl TextChange { pub fn as_text_edit( &self, - line_index: &LineIndex, + line_index: Arc, ) -> lsp::OneOf { lsp::OneOf::Left(lsp::TextEdit { range: self.span.to_range(line_index), @@ -1004,12 +1011,12 @@ impl FileTextChanges { let edits = self .text_changes .iter() - .map(|tc| tc.as_text_edit(&line_index)) + .map(|tc| tc.as_text_edit(line_index.clone())) .collect(); Ok(lsp::TextDocumentEdit { text_document: lsp::OptionalVersionedTextDocumentIdentifier { uri: specifier.clone(), - version: language_server.document_version(specifier), + version: language_server.document_version(&specifier), }, edits, }) @@ -1024,7 +1031,7 @@ impl FileTextChanges { let line_index = if !self.is_new_file.unwrap_or(false) { language_server.get_line_index(specifier.clone()).await? } else { - LineIndex::new("") + Arc::new(LineIndex::new("")) }; if self.is_new_file.unwrap_or(false) { @@ -1043,12 +1050,12 @@ impl FileTextChanges { let edits = self .text_changes .iter() - .map(|tc| tc.as_text_edit(&line_index)) + .map(|tc| tc.as_text_edit(line_index.clone())) .collect(); ops.push(lsp::DocumentChangeOperation::Edit(lsp::TextDocumentEdit { text_document: lsp::OptionalVersionedTextDocumentIdentifier { uri: specifier.clone(), - version: language_server.document_version(specifier), + version: language_server.document_version(&specifier), }, edits, })); @@ -1066,7 +1073,7 @@ pub struct Classifications { impl Classifications { pub fn to_semantic_tokens( &self, - line_index: &LineIndex, + line_index: Arc, ) -> lsp::SemanticTokens { let token_count = self.spans.len() / 3; let mut builder = SemanticTokensBuilder::new(); @@ -1299,7 +1306,7 @@ pub struct ReferenceEntry { impl ReferenceEntry { pub(crate) fn to_location( &self, - line_index: &LineIndex, + line_index: Arc, language_server: &mut language_server::Inner, ) -> lsp::Location { let specifier = normalize_specifier(&self.document_span.file_name) @@ -1342,7 +1349,7 @@ impl CallHierarchyItem { .ok()?; Some(self.to_call_hierarchy_item( - &target_line_index, + target_line_index, language_server, maybe_root_path, )) @@ -1350,7 +1357,7 @@ impl CallHierarchyItem { pub(crate) fn to_call_hierarchy_item( &self, - line_index: &LineIndex, + line_index: Arc, language_server: &mut language_server::Inner, maybe_root_path: Option<&Path>, ) -> lsp::CallHierarchyItem { @@ -1410,7 +1417,7 @@ impl CallHierarchyItem { uri, detail: Some(detail), kind: self.kind.clone().into(), - range: self.span.to_range(line_index), + range: self.span.to_range(line_index.clone()), selection_range: self.selection_span.to_range(line_index), data: None, } @@ -1444,14 +1451,14 @@ impl CallHierarchyIncomingCall { Some(lsp::CallHierarchyIncomingCall { from: self.from.to_call_hierarchy_item( - &target_line_index, + target_line_index.clone(), language_server, maybe_root_path, ), from_ranges: self .from_spans .iter() - .map(|span| span.to_range(&target_line_index)) + .map(|span| span.to_range(target_line_index.clone())) .collect(), }) } @@ -1467,7 +1474,7 @@ pub struct CallHierarchyOutgoingCall { impl CallHierarchyOutgoingCall { pub(crate) async fn try_resolve_call_hierarchy_outgoing_call( &self, - line_index: &LineIndex, + line_index: Arc, language_server: &mut language_server::Inner, maybe_root_path: Option<&Path>, ) -> Option { @@ -1479,14 +1486,14 @@ impl CallHierarchyOutgoingCall { Some(lsp::CallHierarchyOutgoingCall { to: self.to.to_call_hierarchy_item( - &target_line_index, + target_line_index, language_server, maybe_root_path, ), from_ranges: self .from_spans .iter() - .map(|span| span.to_range(line_index)) + .map(|span| span.to_range(line_index.clone())) .collect(), }) } @@ -1560,7 +1567,7 @@ pub struct CompletionInfo { impl CompletionInfo { pub fn as_completion_response( &self, - line_index: &LineIndex, + line_index: Arc, settings: &config::CompletionSettings, specifier: &ModuleSpecifier, position: u32, @@ -1569,8 +1576,13 @@ impl CompletionInfo { .entries .iter() .map(|entry| { - entry - .as_completion_item(line_index, self, settings, specifier, position) + entry.as_completion_item( + line_index.clone(), + self, + settings, + specifier, + position, + ) }) .collect(); let is_incomplete = self @@ -1711,7 +1723,7 @@ impl CompletionEntry { pub fn as_completion_item( &self, - line_index: &LineIndex, + line_index: Arc, info: &CompletionInfo, settings: &config::CompletionSettings, specifier: &ModuleSpecifier, @@ -1837,11 +1849,11 @@ const FOLD_END_PAIR_CHARACTERS: &[u8] = &[b'}', b']', b')', b'`']; impl OutliningSpan { pub fn to_folding_range( &self, - line_index: &LineIndex, + line_index: Arc, content: &[u8], line_folding_only: bool, ) -> lsp::FoldingRange { - let range = self.text_span.to_range(line_index); + let range = self.text_span.to_range(line_index.clone()); lsp::FoldingRange { start_line: range.start.line, start_character: if line_folding_only { @@ -1867,7 +1879,7 @@ impl OutliningSpan { fn adjust_folding_end_line( &self, range: &lsp::Range, - line_index: &LineIndex, + line_index: Arc, content: &[u8], line_folding_only: bool, ) -> u32 { @@ -1991,10 +2003,10 @@ pub struct SelectionRange { impl SelectionRange { pub fn to_selection_range( &self, - line_index: &LineIndex, + line_index: Arc, ) -> lsp::SelectionRange { lsp::SelectionRange { - range: self.text_span.to_range(line_index), + range: self.text_span.to_range(line_index.clone()), parent: self.parent.as_ref().map(|parent_selection| { Box::new(parent_selection.to_selection_range(line_index)) }), @@ -2065,19 +2077,13 @@ fn cache_snapshot( .snapshots .contains_key(&(specifier.clone(), version.clone().into())) { - let content = if state.state_snapshot.documents.contains_key(specifier) { - state - .state_snapshot - .documents - .content(specifier) - .ok_or_else(|| { - anyhow!("Specifier unexpectedly doesn't have content: {}", specifier) - })? - } else { - state.state_snapshot.sources.get_source(specifier).ok_or_else(|| { - anyhow!("Specifier (\"{}\") is not an in memory document or on disk resource.", specifier) - })? - }; + let content = state + .state_snapshot + .documents + .content(specifier) + .ok_or_else(|| { + anyhow!("Specifier unexpectedly doesn't have content: {}", specifier) + })?; state .snapshots .insert((specifier.clone(), version.into()), content.to_string()); @@ -2140,7 +2146,10 @@ fn op_exists(state: &mut State, args: SpecifierArgs) -> Result { .performance .mark("op_exists", Some(&args)); let specifier = state.normalize_specifier(args.specifier)?; - let result = state.state_snapshot.sources.contains_key(&specifier); + let result = state + .state_snapshot + .documents + .contains_specifier(&specifier); state.state_snapshot.performance.measure(mark); Ok(result) } @@ -2268,7 +2277,7 @@ fn op_load( .performance .mark("op_load", Some(&args)); let specifier = state.normalize_specifier(args.specifier)?; - let result = state.state_snapshot.sources.get_source(&specifier); + let result = state.state_snapshot.documents.content(&specifier); state.state_snapshot.performance.measure(mark); Ok(result.map(|t| t.to_string())) } @@ -2281,89 +2290,33 @@ fn op_resolve( .state_snapshot .performance .mark("op_resolve", Some(&args)); - let mut resolved = Vec::new(); let referrer = state.normalize_specifier(&args.base)?; - let sources = &mut state.state_snapshot.sources; - if state.state_snapshot.documents.contains_key(&referrer) { - if let Some(dependencies) = - state.state_snapshot.documents.dependencies(&referrer) - { - for specifier in &args.specifiers { - if specifier.starts_with("asset:///") { - resolved.push(Some(( - specifier.clone(), - MediaType::from(specifier).as_ts_extension().into(), - ))) - } else if let Some(dependency) = dependencies.get(specifier) { - let resolved_import = - if let Some(resolved_import) = &dependency.maybe_type { - resolved_import.clone() - } else if let Some(resolved_import) = &dependency.maybe_code { - resolved_import.clone() - } else { - ResolvedDependency::Err(ResolvedDependencyErr::Missing) - }; - if let ResolvedDependency::Resolved(resolved_specifier) = - resolved_import - { - if state - .state_snapshot - .documents - .contains_key(&resolved_specifier) - { - let media_type = MediaType::from(&resolved_specifier); - resolved.push(Some(( - resolved_specifier.to_string(), - media_type.as_ts_extension().into(), - ))); - } else if sources.contains_key(&resolved_specifier) { - let media_type = if let Some(media_type) = - sources.get_media_type(&resolved_specifier) - { - media_type - } else { - MediaType::from(&resolved_specifier) - }; - resolved.push(Some(( - resolved_specifier.to_string(), - media_type.as_ts_extension().into(), - ))); - } else { - resolved.push(None); - } - } else { - resolved.push(None); - } - } - } - } - } else if sources.contains_key(&referrer) { - for specifier in &args.specifiers { - if let Some((resolved_specifier, media_type)) = - sources.resolve_import(specifier, &referrer) - { - resolved.push(Some(( - resolved_specifier.to_string(), - media_type.as_ts_extension().into(), - ))); - } else { - resolved.push(None); - } - } + let result = if let Some(resolved) = state + .state_snapshot + .documents + .resolve(args.specifiers, &referrer) + { + Ok( + resolved + .into_iter() + .map(|o| { + o.map(|(s, mt)| (s.to_string(), mt.as_ts_extension().to_string())) + }) + .collect(), + ) } else { - state.state_snapshot.performance.measure(mark); - return Err(custom_error( + Err(custom_error( "NotFound", format!( - "the referring ({}) specifier is unexpectedly missing", - referrer + "Error resolving. Referring specifier \"{}\" was not found.", + args.base ), - )); - } + )) + }; state.state_snapshot.performance.measure(mark); - Ok(resolved) + result } fn op_respond(state: &mut State, args: Response) -> Result { @@ -2375,15 +2328,7 @@ fn op_script_names( state: &mut State, _args: Value, ) -> Result, AnyError> { - Ok( - state - .state_snapshot - .documents - .open_specifiers() - .into_iter() - .cloned() - .collect(), - ) + Ok(state.state_snapshot.documents.specifiers(true, true)) } #[derive(Debug, Deserialize, Serialize)] @@ -2407,17 +2352,8 @@ fn op_script_version( } else { Ok(None) } - } else if let Some(version) = - state.state_snapshot.documents.version(&specifier) - { - Ok(Some(version.to_string())) } else { - let sources = &mut state.state_snapshot.sources; - if let Some(version) = sources.get_script_version(&specifier) { - Ok(Some(version)) - } else { - Ok(None) - } + Ok(state.state_snapshot.documents.version(&specifier)) }; state.state_snapshot.performance.measure(mark); @@ -2868,7 +2804,7 @@ impl RequestMethod { } /// Send a request into a runtime and return the JSON value of the response. -pub fn request( +pub(crate) fn request( runtime: &mut JsRuntime, state_snapshot: StateSnapshot, method: RequestMethod, @@ -2908,10 +2844,8 @@ mod tests { use super::*; use crate::http_cache::HttpCache; use crate::http_util::HeadersMap; - use crate::lsp::analysis; - use crate::lsp::documents::DocumentCache; + use crate::lsp::documents::Documents; use crate::lsp::documents::LanguageId; - use crate::lsp::sources::Sources; use crate::lsp::text::LineIndex; use std::path::Path; use std::path::PathBuf; @@ -2921,37 +2855,19 @@ mod tests { fixtures: &[(&str, &str, i32, LanguageId)], location: &Path, ) -> StateSnapshot { - let mut documents = DocumentCache::default(); + let documents = Documents::new(location); for (specifier, source, version, language_id) in fixtures { let specifier = resolve_url(specifier).expect("failed to create specifier"); documents.open( specifier.clone(), *version, - *language_id, + language_id.clone(), Arc::new(source.to_string()), ); - let media_type = MediaType::from(&specifier); - if let Some(Ok(parsed_module)) = - documents.get(&specifier).unwrap().source().module() - { - let (deps, _) = analysis::analyze_dependencies( - &specifier, - media_type, - parsed_module, - &None, - ); - let dep_ranges = - analysis::analyze_dependency_ranges(parsed_module).ok(); - documents - .set_dependencies(&specifier, Some(deps), dep_ranges) - .unwrap(); - } } - let sources = Sources::new(location); StateSnapshot { documents, - sources, ..Default::default() } } diff --git a/cli/lsp/urls.rs b/cli/lsp/urls.rs index 8b47911f6e..19f53f2972 100644 --- a/cli/lsp/urls.rs +++ b/cli/lsp/urls.rs @@ -92,7 +92,9 @@ impl LspUrlMap { let url = if specifier.scheme() == "file" { specifier.clone() } else { - let specifier_str = if specifier.scheme() == "data" { + let specifier_str = if specifier.scheme() == "asset" { + format!("deno:asset{}", specifier.path()) + } else if specifier.scheme() == "data" { let data_url = DataUrl::process(specifier.as_str()) .map_err(|e| uri_error(format!("{:?}", e)))?; let mime = data_url.mime_type(); diff --git a/cli/tests/integration/lsp_tests.rs b/cli/tests/integration/lsp_tests.rs index 3126eb31c0..ecfe5d334d 100644 --- a/cli/tests/integration/lsp_tests.rs +++ b/cli/tests/integration/lsp_tests.rs @@ -431,7 +431,7 @@ fn lsp_hover_asset() { "deno/virtualTextDocument", json!({ "textDocument": { - "uri": "deno:/asset//lib.deno.shared_globals.d.ts" + "uri": "deno:asset/lib.deno.shared_globals.d.ts" } }), ) @@ -442,7 +442,7 @@ fn lsp_hover_asset() { "textDocument/hover", json!({ "textDocument": { - "uri": "deno:/asset//lib.es2015.symbol.wellknown.d.ts" + "uri": "deno:asset/lib.es2015.symbol.wellknown.d.ts" }, "position": { "line": 109, @@ -919,11 +919,11 @@ fn lsp_hover_dependency() { "range": { "start": { "line": 0, - "character": 20 + "character": 19 }, "end":{ "line": 0, - "character": 61 + "character": 62 } } })) @@ -953,11 +953,11 @@ fn lsp_hover_dependency() { "range": { "start": { "line": 3, - "character": 20 + "character": 19 }, "end":{ "line": 3, - "character": 66 + "character": 67 } } })) @@ -987,11 +987,11 @@ fn lsp_hover_dependency() { "range": { "start": { "line": 4, - "character": 20 + "character": 19 }, "end":{ "line": 4, - "character": 56 + "character": 57 } } })) @@ -1021,11 +1021,11 @@ fn lsp_hover_dependency() { "range": { "start": { "line": 5, - "character": 20 + "character": 19 }, "end":{ "line": 5, - "character": 131 + "character": 132 } } })) @@ -1055,11 +1055,11 @@ fn lsp_hover_dependency() { "range": { "start": { "line": 6, - "character": 20 + "character": 19 }, "end":{ "line": 6, - "character": 32 + "character": 33 } } })) @@ -1771,7 +1771,7 @@ fn lsp_code_lens_non_doc_nav_tree() { "deno/virtualTextDocument", json!({ "textDocument": { - "uri": "deno:/asset//lib.deno.shared_globals.d.ts" + "uri": "deno:asset/lib.deno.shared_globals.d.ts" } }), ) @@ -1783,7 +1783,7 @@ fn lsp_code_lens_non_doc_nav_tree() { "textDocument/codeLens", json!({ "textDocument": { - "uri": "deno:/asset//lib.deno.shared_globals.d.ts" + "uri": "deno:asset/lib.deno.shared_globals.d.ts" } }), ) @@ -2714,11 +2714,11 @@ fn lsp_cache_location() { "range": { "start": { "line": 0, - "character": 20 + "character": 19 }, "end":{ "line": 0, - "character": 61 + "character": 62 } } }))