diff options
author | Nayeem Rahman <nayeemrmn99@gmail.com> | 2024-06-26 23:47:01 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-06-26 23:47:01 +0100 |
commit | 67dcd6db518446574d3a1e33f4ce536fcdc4fd25 (patch) | |
tree | 23d6aa1d867b1efded9c0e638c89513ca492a44f | |
parent | 2a2ff96be13047cb50612fde0f12e5f6df374ad3 (diff) |
feat(lsp): ts language service scopes (#24345)
-rw-r--r-- | cli/lsp/code_lens.rs | 15 | ||||
-rw-r--r-- | cli/lsp/config.rs | 9 | ||||
-rw-r--r-- | cli/lsp/documents.rs | 49 | ||||
-rw-r--r-- | cli/lsp/language_server.rs | 114 | ||||
-rw-r--r-- | cli/lsp/resolver.rs | 24 | ||||
-rw-r--r-- | cli/lsp/tsc.rs | 715 | ||||
-rw-r--r-- | cli/tsc/99_main_compiler.js | 201 | ||||
-rw-r--r-- | tests/integration/lsp_tests.rs | 495 |
8 files changed, 1265 insertions, 357 deletions
diff --git a/cli/lsp/code_lens.rs b/cli/lsp/code_lens.rs index 21daf0ac4..2996103be 100644 --- a/cli/lsp/code_lens.rs +++ b/cli/lsp/code_lens.rs @@ -1,5 +1,7 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. +use crate::lsp::logging::lsp_warn; + use super::analysis::source_range_to_lsp_range; use super::config::CodeLensSettings; use super::language_server; @@ -27,6 +29,7 @@ use std::cell::RefCell; use std::collections::HashSet; use std::rc::Rc; use std::sync::Arc; +use tower_lsp::jsonrpc::Error as LspError; use tower_lsp::lsp_types as lsp; static ABSTRACT_MODIFIER: Lazy<Regex> = lazy_regex!(r"\babstract\b"); @@ -260,7 +263,11 @@ async fn resolve_implementation_code_lens( data.specifier.clone(), line_index.offset_tsc(code_lens.range.start)?, ) - .await?; + .await + .map_err(|err| { + lsp_warn!("{err}"); + LspError::internal_error() + })?; if let Some(implementations) = maybe_implementations { let mut locations = Vec::new(); for implementation in implementations { @@ -357,7 +364,11 @@ async fn resolve_references_code_lens( data.specifier.clone(), line_index.offset_tsc(code_lens.range.start)?, ) - .await?; + .await + .map_err(|err| { + lsp_warn!("Unable to find references: {err}"); + LspError::internal_error() + })?; let locations = get_locations(maybe_referenced_symbols, language_server)?; let title = if locations.len() == 1 { "1 reference".to_string() diff --git a/cli/lsp/config.rs b/cli/lsp/config.rs index 4f96d45a4..8238ae510 100644 --- a/cli/lsp/config.rs +++ b/cli/lsp/config.rs @@ -1055,7 +1055,6 @@ impl Default for LspTsConfig { "esModuleInterop": true, "experimentalDecorators": false, "isolatedModules": true, - "jsx": "react", "lib": ["deno.ns", "deno.window", "deno.unstable"], "module": "esnext", "moduleDetection": "force", @@ -1569,16 +1568,10 @@ impl ConfigData { #[derive(Clone, Debug, Default)] pub struct ConfigTree { - first_folder: Option<ModuleSpecifier>, scopes: Arc<BTreeMap<ModuleSpecifier, Arc<ConfigData>>>, } impl ConfigTree { - pub fn root_ts_config(&self) -> Arc<LspTsConfig> { - let root_data = self.first_folder.as_ref().and_then(|s| self.scopes.get(s)); - root_data.map(|d| d.ts_config.clone()).unwrap_or_default() - } - pub fn scope_for_specifier( &self, specifier: &ModuleSpecifier, @@ -1773,7 +1766,6 @@ impl ConfigTree { ); } } - self.first_folder.clone_from(&settings.first_folder); self.scopes = Arc::new(scopes); } @@ -1790,7 +1782,6 @@ impl ConfigTree { ) .await, ); - self.first_folder = Some(scope.clone()); self.scopes = Arc::new([(scope, data)].into_iter().collect()); } } diff --git a/cli/lsp/documents.rs b/cli/lsp/documents.rs index 93fcfe808..29308e9f7 100644 --- a/cli/lsp/documents.rs +++ b/cli/lsp/documents.rs @@ -144,6 +144,20 @@ impl AssetOrDocument { } } + pub fn file_referrer(&self) -> Option<&ModuleSpecifier> { + match self { + AssetOrDocument::Asset(_) => None, + AssetOrDocument::Document(doc) => doc.file_referrer(), + } + } + + pub fn scope(&self) -> Option<&ModuleSpecifier> { + match self { + AssetOrDocument::Asset(_) => None, + AssetOrDocument::Document(doc) => doc.scope(), + } + } + pub fn maybe_semantic_tokens(&self) -> Option<lsp::SemanticTokens> { match self { AssetOrDocument::Asset(_) => None, @@ -605,6 +619,13 @@ impl Document { self.file_referrer.as_ref() } + pub fn scope(&self) -> Option<&ModuleSpecifier> { + self + .file_referrer + .as_ref() + .and_then(|r| self.config.tree.scope_for_specifier(r)) + } + pub fn content(&self) -> &Arc<str> { &self.text } @@ -926,9 +947,9 @@ pub struct Documents { /// The npm package requirements found in npm specifiers. npm_reqs_by_scope: Arc<BTreeMap<Option<ModuleSpecifier>, BTreeSet<PackageReq>>>, - /// Gets if any document had a node: specifier such that a @types/node package - /// should be injected. - has_injected_types_node_package: bool, + /// Config scopes that contain a node: specifier such that a @types/node + /// package should be injected. + scopes_with_node_specifier: Arc<HashSet<Option<ModuleSpecifier>>>, } impl Documents { @@ -1122,10 +1143,10 @@ impl Documents { self.npm_reqs_by_scope.clone() } - /// Returns if a @types/node package was injected into the npm - /// resolver based on the state of the documents. - pub fn has_injected_types_node_package(&self) -> bool { - self.has_injected_types_node_package + pub fn scopes_with_node_specifier( + &self, + ) -> &Arc<HashSet<Option<ModuleSpecifier>>> { + &self.scopes_with_node_specifier } /// Return a document for the specifier. @@ -1346,20 +1367,18 @@ impl Documents { /// document. fn calculate_npm_reqs_if_dirty(&mut self) { let mut npm_reqs_by_scope: BTreeMap<_, BTreeSet<_>> = Default::default(); - let mut scopes_with_node_builtin_specifier = HashSet::new(); + let mut scopes_with_specifier = HashSet::new(); let is_fs_docs_dirty = self.file_system_docs.set_dirty(false); if !is_fs_docs_dirty && !self.dirty { return; } let mut visit_doc = |doc: &Arc<Document>| { - let scope = doc - .file_referrer() - .and_then(|r| self.config.tree.scope_for_specifier(r)); + let scope = doc.scope(); let reqs = npm_reqs_by_scope.entry(scope.cloned()).or_default(); for dependency in doc.dependencies().values() { if let Some(dep) = dependency.get_code() { if dep.scheme() == "node" { - scopes_with_node_builtin_specifier.insert(scope.cloned()); + scopes_with_specifier.insert(scope.cloned()); } if let Ok(reference) = NpmPackageReqReference::from_specifier(dep) { reqs.insert(reference.into_inner().req); @@ -1402,15 +1421,15 @@ impl Documents { // Ensure a @types/node package exists when any module uses a node: specifier. // Unlike on the command line, here we just add @types/node to the npm package // requirements since this won't end up in the lockfile. - for scope in scopes_with_node_builtin_specifier { - let reqs = npm_reqs_by_scope.entry(scope).or_default(); + for scope in &scopes_with_specifier { + let reqs = npm_reqs_by_scope.entry(scope.clone()).or_default(); if !reqs.iter().any(|r| r.name == "@types/node") { - self.has_injected_types_node_package = true; reqs.insert(PackageReq::from_str("@types/node").unwrap()); } } self.npm_reqs_by_scope = Arc::new(npm_reqs_by_scope); + self.scopes_with_node_specifier = Arc::new(scopes_with_specifier); self.dirty = false; } diff --git a/cli/lsp/language_server.rs b/cli/lsp/language_server.rs index a921584c2..7d8213a24 100644 --- a/cli/lsp/language_server.rs +++ b/cli/lsp/language_server.rs @@ -16,6 +16,7 @@ use deno_graph::Resolution; use deno_runtime::deno_tls::rustls::RootCertStore; use deno_runtime::deno_tls::RootCertStoreProvider; use deno_semver::jsr::JsrPackageReqReference; +use indexmap::Equivalent; use indexmap::IndexSet; use log::error; use serde::Deserialize; @@ -570,7 +571,11 @@ impl Inner { } else { let navigation_tree: tsc::NavigationTree = self .ts_server - .get_navigation_tree(self.snapshot(), specifier.clone()) + .get_navigation_tree( + self.snapshot(), + specifier.clone(), + asset_or_doc.scope().cloned(), + ) .await?; let navigation_tree = Arc::new(navigation_tree); match asset_or_doc { @@ -1065,8 +1070,8 @@ impl Inner { params.text_document.text.into(), file_referrer, ); - self.project_changed([(document.specifier(), ChangeKind::Opened)], false); if document.is_diagnosable() { + self.project_changed([(document.specifier(), ChangeKind::Opened)], false); self.refresh_npm_specifiers().await; self.diagnostics_server.invalidate(&[specifier]); self.send_diagnostics_update(); @@ -1087,11 +1092,21 @@ impl Inner { ) { Ok(document) => { if document.is_diagnosable() { + let old_scopes_with_node_specifier = + self.documents.scopes_with_node_specifier().clone(); + self.refresh_npm_specifiers().await; + let mut config_changed = false; + if !self + .documents + .scopes_with_node_specifier() + .equivalent(&old_scopes_with_node_specifier) + { + config_changed = true; + } self.project_changed( [(document.specifier(), ChangeKind::Modified)], - false, + config_changed, ); - self.refresh_npm_specifiers().await; self.diagnostics_server.invalidate(&[specifier]); self.send_diagnostics_update(); self.send_testing_update(); @@ -1399,7 +1414,7 @@ impl Inner { let mark = self.performance.mark_with_args("lsp.hover", ¶ms); let asset_or_doc = self.get_asset_or_document(&specifier)?; - let file_referrer = asset_or_doc.document().and_then(|d| d.file_referrer()); + let file_referrer = asset_or_doc.file_referrer(); let hover = if let Some((_, dep, range)) = asset_or_doc .get_maybe_dependency(¶ms.text_document_position_params.position) { @@ -1459,7 +1474,12 @@ impl Inner { line_index.offset_tsc(params.text_document_position_params.position)?; let maybe_quick_info = self .ts_server - .get_quick_info(self.snapshot(), specifier.clone(), position) + .get_quick_info( + self.snapshot(), + specifier.clone(), + position, + asset_or_doc.scope().cloned(), + ) .await?; maybe_quick_info.map(|qi| qi.to_hover(line_index, self)) }; @@ -1588,6 +1608,7 @@ impl Inner { &self.config, &specifier, ), + asset_or_doc.scope().cloned(), ) .await; for action in actions { @@ -1682,6 +1703,7 @@ impl Inner { )), params.context.trigger_kind, only, + asset_or_doc.scope().cloned(), ) .await?; let mut refactor_actions = Vec::<CodeAction>::new(); @@ -1732,6 +1754,10 @@ impl Inner { error!("Unable to decode code action data: {:#}", err); LspError::invalid_params("The CodeAction's data is invalid.") })?; + let scope = self + .get_asset_or_document(&code_action_data.specifier) + .ok() + .and_then(|d| d.scope().cloned()); let combined_code_actions = self .ts_server .get_combined_code_fix( @@ -1747,6 +1773,7 @@ impl Inner { &self.config, &code_action_data.specifier, ), + scope, ) .await?; if combined_code_actions.commands.is_some() { @@ -1801,6 +1828,7 @@ impl Inner { &self.config, &action_data.specifier, )), + asset_or_doc.scope().cloned(), ) .await?; code_action.edit = refactor_edit_info.to_workspace_edit(self)?; @@ -1944,6 +1972,7 @@ impl Inner { specifier, line_index.offset_tsc(params.text_document_position_params.position)?, files_to_search, + asset_or_doc.scope().cloned(), ) .await?; @@ -1984,7 +2013,11 @@ impl Inner { specifier.clone(), line_index.offset_tsc(params.text_document_position.position)?, ) - .await?; + .await + .map_err(|err| { + lsp_warn!("Unable to find references: {err}"); + LspError::internal_error() + })?; if let Some(symbols) = maybe_referenced_symbols { let mut results = Vec::new(); @@ -2037,6 +2070,7 @@ impl Inner { self.snapshot(), specifier, line_index.offset_tsc(params.text_document_position_params.position)?, + asset_or_doc.scope().cloned(), ) .await?; @@ -2075,6 +2109,7 @@ impl Inner { self.snapshot(), specifier, line_index.offset_tsc(params.text_document_position_params.position)?, + asset_or_doc.scope().cloned(), ) .await?; @@ -2123,10 +2158,7 @@ impl Inner { .map(|s| s.suggest.include_completions_for_import_statements) .unwrap_or(true) { - let file_referrer = asset_or_doc - .document() - .and_then(|d| d.file_referrer()) - .unwrap_or(&specifier); + let file_referrer = asset_or_doc.file_referrer().unwrap_or(&specifier); response = completions::get_import_completions( &specifier, ¶ms.text_document_position.position, @@ -2158,6 +2190,7 @@ impl Inner { }; let position = line_index.offset_tsc(params.text_document_position.position)?; + let scope = asset_or_doc.scope(); let maybe_completion_info = self .ts_server .get_completions( @@ -2178,6 +2211,7 @@ impl Inner { .fmt_options_for_specifier(&specifier) .options) .into(), + scope.cloned(), ) .await; @@ -2219,6 +2253,10 @@ impl Inner { })?; if let Some(data) = &data.tsc { let specifier = &data.specifier; + let scope = self + .get_asset_or_document(specifier) + .ok() + .and_then(|d| d.scope().cloned()); let result = self .ts_server .get_completion_details( @@ -2240,6 +2278,7 @@ impl Inner { ), ..data.into() }, + scope, ) .await; match result { @@ -2309,7 +2348,11 @@ impl Inner { specifier, line_index.offset_tsc(params.text_document_position_params.position)?, ) - .await?; + .await + .map_err(|err| { + lsp_warn!("{:#}", err); + LspError::internal_error() + })?; let result = if let Some(implementations) = maybe_implementations { let mut links = Vec::new(); @@ -2347,7 +2390,11 @@ impl Inner { let outlining_spans = self .ts_server - .get_outlining_spans(self.snapshot(), specifier) + .get_outlining_spans( + self.snapshot(), + specifier, + asset_or_doc.scope().cloned(), + ) .await?; let response = if !outlining_spans.is_empty() { @@ -2396,7 +2443,11 @@ impl Inner { specifier, line_index.offset_tsc(params.item.selection_range.start)?, ) - .await?; + .await + .map_err(|err| { + lsp_warn!("{:#}", err); + LspError::internal_error() + })?; let maybe_root_path_owned = self .config @@ -2440,6 +2491,7 @@ impl Inner { self.snapshot(), specifier, line_index.offset_tsc(params.item.selection_range.start)?, + asset_or_doc.scope().cloned(), ) .await?; @@ -2487,6 +2539,7 @@ impl Inner { self.snapshot(), specifier, line_index.offset_tsc(params.text_document_position_params.position)?, + asset_or_doc.scope().cloned(), ) .await?; @@ -2549,7 +2602,11 @@ impl Inner { specifier, line_index.offset_tsc(params.text_document_position.position)?, ) - .await?; + .await + .map_err(|err| { + lsp_warn!("{:#}", err); + LspError::internal_error() + })?; if let Some(locations) = maybe_locations { let rename_locations = tsc::RenameLocations { locations }; @@ -2594,6 +2651,7 @@ impl Inner { self.snapshot(), specifier.clone(), line_index.offset_tsc(position)?, + asset_or_doc.scope().cloned(), ) .await?; @@ -2637,6 +2695,7 @@ impl Inner { self.snapshot(), specifier, 0..line_index.text_content_length_utf16().into(), + asset_or_doc.scope().cloned(), ) .await?; @@ -2692,6 +2751,7 @@ impl Inner { specifier, line_index.offset_tsc(params.range.start)? ..line_index.offset_tsc(params.range.end)?, + asset_or_doc.scope().cloned(), ) .await?; @@ -2744,6 +2804,7 @@ impl Inner { specifier, line_index.offset_tsc(params.text_document_position_params.position)?, options, + asset_or_doc.scope().cloned(), ) .await?; @@ -2799,7 +2860,11 @@ impl Inner { ..Default::default() }, ) - .await?, + .await + .map_err(|err| { + lsp_warn!("{:#}", err); + LspError::internal_error() + })?, ); } file_text_changes_to_workspace_edit(&changes, self) @@ -2822,7 +2887,11 @@ impl Inner { file: None, }, ) - .await?; + .await + .map_err(|err| { + error!("{:#}", err); + LspError::invalid_request() + })?; let maybe_symbol_information = if navigate_to_items.is_empty() { None @@ -2849,7 +2918,15 @@ impl Inner { self.ts_server.project_changed( self.snapshot(), modified_scripts, - config_changed, + config_changed.then(|| { + self + .config + .tree + .data_by_scope() + .iter() + .map(|(s, d)| (s.clone(), d.ts_config.clone())) + .collect() + }), ); } @@ -3582,6 +3659,7 @@ impl Inner { &self.config, &specifier, ), + asset_or_doc.scope().cloned(), ) .await?; let maybe_inlay_hints = maybe_inlay_hints.map(|hints| { diff --git a/cli/lsp/resolver.rs b/cli/lsp/resolver.rs index 45d44032f..2568dc3a2 100644 --- a/cli/lsp/resolver.rs +++ b/cli/lsp/resolver.rs @@ -271,20 +271,20 @@ impl LspResolver { pub fn graph_imports_by_referrer( &self, + file_referrer: &ModuleSpecifier, ) -> IndexMap<&ModuleSpecifier, Vec<&ModuleSpecifier>> { - self - .by_scope + let resolver = self.get_scope_resolver(Some(file_referrer)); + resolver + .graph_imports .iter() - .flat_map(|(_, r)| { - r.graph_imports.iter().map(|(s, i)| { - ( - s, - i.dependencies - .values() - .flat_map(|d| d.get_type().or_else(|| d.get_code())) - .collect(), - ) - }) + .map(|(s, i)| { + ( + s, + i.dependencies + .values() + .flat_map(|d| d.get_type().or_else(|| d.get_code())) + .collect(), + ) }) .collect() } diff --git a/cli/lsp/tsc.rs b/cli/lsp/tsc.rs index 5659decbf..3239ba700 100644 --- a/cli/lsp/tsc.rs +++ b/cli/lsp/tsc.rs @@ -3,6 +3,7 @@ use super::analysis::CodeActionData; use super::code_lens; use super::config; +use super::config::LspTsConfig; use super::documents::AssetOrDocument; use super::documents::Document; use super::documents::DocumentsFilter; @@ -34,6 +35,8 @@ use crate::util::v8::convert; use deno_core::convert::Smi; use deno_core::convert::ToV8; use deno_core::error::StdAnyError; +use deno_core::futures::stream::FuturesOrdered; +use deno_core::futures::StreamExt; use deno_runtime::fs_util::specifier_to_file_path; use dashmap::DashMap; @@ -60,6 +63,8 @@ use deno_core::PollEventLoopOptions; use deno_core::RuntimeOptions; use deno_runtime::inspector_server::InspectorServer; use deno_runtime::tokio_util::create_basic_runtime; +use indexmap::IndexMap; +use indexmap::IndexSet; use lazy_regex::lazy_regex; use log::error; use once_cell::sync::Lazy; @@ -67,9 +72,9 @@ use regex::Captures; use regex::Regex; use serde_repr::Deserialize_repr; use serde_repr::Serialize_repr; -use std::borrow::Cow; use std::cell::RefCell; use std::cmp; +use std::collections::BTreeMap; use std::collections::HashMap; use std::collections::HashSet; use std::convert::Infallible; @@ -108,6 +113,7 @@ const FILE_EXTENSION_KIND_MODIFIERS: &[&str] = type Request = ( TscRequest, + Option<ModuleSpecifier>, Arc<StateSnapshot>, oneshot::Sender<Result<String, AnyError>>, CancellationToken, @@ -270,11 +276,12 @@ impl Serialize for ChangeKind { } } -#[derive(Debug, PartialEq)] +#[derive(Debug)] +#[cfg_attr(test, derive(Serialize))] pub struct PendingChange { pub modified_scripts: Vec<(String, ChangeKind)>, pub project_version: usize, - pub config_changed: bool, + pub new_configs_by_scope: Option<BTreeMap<ModuleSpecifier, Arc<LspTsConfig>>>, } impl<'a> ToV8<'a> for PendingChange { @@ -297,12 +304,24 @@ impl<'a> ToV8<'a> for PendingChange { }; let project_version = v8::Integer::new_from_unsigned(scope, self.project_version as u32).into(); - let config_changed = v8::Boolean::new(scope, self.config_changed).into(); + let new_configs_by_scope = + if let Some(new_configs_by_scope) = self.new_configs_by_scope { + serde_v8::to_v8( + scope, + new_configs_by_scope.into_iter().collect::<Vec<_>>(), + ) + .unwrap_or_else(|err| { + lsp_warn!("Couldn't serialize ts configs: {err}"); + v8::null(scope).into() + }) + } else { + v8::null(scope).into() + }; Ok( v8::Array::new_with_elements( scope, - &[modified_scripts, project_version, config_changed], + &[modified_scripts, project_version, new_configs_by_scope], ) .into(), ) @@ -314,11 +333,13 @@ impl PendingChange { &mut self, new_version: usize, modified_scripts: Vec<(String, ChangeKind)>, - config_changed: bool, + new_configs_by_scope: Option<BTreeMap<ModuleSpecifier, Arc<LspTsConfig>>>, ) { use ChangeKind::*; self.project_version = self.project_version.max(new_version); - self.config_changed |= config_changed; + if let Some(new_configs_by_scope) = new_configs_by_scope { + self.new_configs_by_scope = Some(new_configs_by_scope); + } for (spec, new) in modified_scripts { if let Some((_, current)) = self.modified_scripts.iter_mut().find(|(s, _)| s == &spec) @@ -408,7 +429,7 @@ impl TsServer { &self, snapshot: Arc<StateSnapshot>, modified_scripts: impl IntoIterator<Item = (&'a ModuleSpecifier, ChangeKind)>, - config_changed: bool, + new_configs_by_scope: Option<BTreeMap<ModuleSpecifier, Arc<LspTsConfig>>>, ) { let modified_scripts = modified_scripts .into_iter() @@ -419,14 +440,14 @@ impl TsServer { pending_change.coalesce( snapshot.project_version, modified_scripts, - config_changed, + new_configs_by_scope, ); } pending => { let pending_change = PendingChange { modified_scripts, project_version: snapshot.project_version, - config_changed, + new_configs_by_scope, }; *pending = Some(pending_change); } @@ -438,36 +459,69 @@ impl TsServer { snapshot: Arc<StateSnapshot>, specifiers: Vec<ModuleSpecifier>, token: CancellationToken, - ) -> Result<HashMap<String, Vec<crate::tsc::Diagnostic>>, AnyError> { - let req = TscRequest::GetDiagnostics(( - specifiers - .into_iter() - .map(|s| self.specifier_map.denormalize(&s)) - .collect::<Vec<String>>(), - snapshot.project_version, - )); - let raw_diagnostics = self.request_with_cancellation::<HashMap<String, Vec<crate::tsc::Diagnostic>>>(snapshot, req, token).await?; - let mut diagnostics_map = HashMap::with_capacity(raw_diagnostics.len()); - for (mut specifier, mut diagnostics) in raw_diagnostics { - specifier = self.specifier_map.normalize(&specifier)?.to_string(); - for diagnostic in &mut diagnostics { - normalize_diagnostic(diagnostic, &self.specifier_map)?; + ) -> Result<IndexMap<String, Vec<crate::tsc::Diagnostic>>, AnyError> { + let mut diagnostics_map = IndexMap::with_capacity(specifiers.len()); + let mut specifiers_by_scope = BTreeMap::new(); + for specifier in specifiers { + let scope = if specifier.scheme() == "file" { + snapshot + .config + .tree + .scope_for_specifier(&specifier) + .cloned() + } else { + snapshot + .documents + .get(&specifier) + .and_then(|d| d.scope().cloned()) + }; + let specifiers = specifiers_by_scope.entry(scope).or_insert(vec![]); + specifiers.push(self.specifier_map.denormalize(&specifier)); + } + let mut results = FuturesOrdered::new(); + for (scope, specifiers) in specifiers_by_scope { + let req = + TscRequest::GetDiagnostics((specifiers, snapshot.project_version)); + results.push_back(self.request_with_cancellation::<IndexMap<String, Vec<crate::tsc::Diagnostic>>>(snapshot.clone(), req, scope, token.clone())); + } + while let Some(raw_diagnostics) = results.next().await { + let raw_diagnostics = raw_diagnostics + .inspect_err(|err| { + if !token.is_cancelled() { + lsp_warn!("Error generating TypeScript diagnostics: {err}"); + } + }) + .unwrap_or_default(); + for (mut specifier, mut diagnostics) in raw_diagnostics { + specifier = self.specifier_map.normalize(&specifier)?.to_string(); + for diagnostic in &mut diagnostics { + normalize_diagnostic(diagnostic, &self.specifier_map)?; + } + diagnostics_map.insert(specifier, diagnostics); } - diagnostics_map.insert(specifier, diagnostics); } Ok(diagnostics_map) } pub async fn cleanup_semantic_cache(&self, snapshot: Arc<StateSnapshot>) { - let req = TscRequest::CleanupSemanticCache; - self - .request::<()>(snapshot, req) - .await - .map_err(|err| { - log::error!("Failed to request to tsserver {}", err); - LspError::invalid_request() - }) - .ok(); + for scope in snapshot + .config + .tree + .data_by_scope() + .keys() + .map(Some) + .chain(std::iter::once(None)) + { + let req = TscRequest::CleanupSemanticCache; + self + .request::<()>(snapshot.clone(), req, scope.cloned()) + .await + .map_err(|err| { + log::error!("Failed to request to tsserver {}", err); + LspError::invalid_request() + }) + .ok(); + } } pub async fn find_references( @@ -475,35 +529,60 @@ impl TsServer { snapshot: Arc<StateSnapshot>, specifier: ModuleSpecifier, position: u32, - ) -> Result<Option<Vec<ReferencedSymbol>>, LspError> { + ) -> Result<Option<Vec<ReferencedSymbol>>, AnyError> { let req = TscRequest::FindReferences(( self.specifier_map.denormalize(&specifier), position, )); - self - .request::<Option<Vec<ReferencedSymbol>>>(snapshot, req) - .await - .and_then(|mut symbols| { - for symbol in symbols.iter_mut().flatten() { - symbol.normalize(&self.specifier_map)?; - } - Ok(symbols) - }) - .map_err(|err| { - log::error!("Unable to get references from TypeScript: {}", err); - LspError::internal_error() - }) + let mut results = FuturesOrdered::new(); + for scope in snapshot + .config + .tree + .data_by_scope() + .keys() + .map(Some) + .chain(std::iter::once(None)) + { + results.push_back(self.request::<Option<Vec<ReferencedSymbol>>>( + snapshot.clone(), + req.clone(), + scope.cloned(), + )); + } + let mut all_symbols = IndexSet::new(); + while let Some(symbols) = results.next().await { + let symbols = symbols + .inspect_err(|err| { + let err = err.to_string(); + if !err.contains("Could not find source file") { + lsp_warn!("Unable to get references from TypeScript: {err}"); + } + }) + .unwrap_or_default(); + let Some(mut symbols) = symbols else { + continue; + }; + for symbol in &mut symbols { + symbol.normalize(&self.specifier_map)?; + } + all_symbols.extend(symbols); + } + if all_symbols.is_empty() { + return Ok(None); + } + Ok(Some(all_symbols.into_iter().collect())) } pub async fn get_navigation_tree( &self, snapshot: Arc<StateSnapshot>, specifier: ModuleSpecifier, + scope: Option<ModuleSpecifier>, ) -> Result<NavigationTree, AnyError> { let req = TscRequest::GetNavigationTree((self .specifier_map .denormalize(&specifier),)); - self.request(snapshot, req).await + self.request(snapshot, req, scope).await } pub async fn get_supported_code_fixes( @@ -511,7 +590,7 @@ impl TsServer { snapshot: Arc<StateSnapshot>, ) -> Result<Vec<String>, LspError> { let req = TscRequest::GetSupportedCodeFixes; - self.request(snapshot, req).await.map_err(|err| { + self.request(snapshot, req, None).await.map_err(|err| { log::error!("Unable to get fixable diagnostics: {}", err); LspError::internal_error() }) @@ -522,17 +601,19 @@ impl TsServer { snapshot: Arc<StateSnapshot>, specifier: ModuleSpecifier, position: u32, + scope: Option<ModuleSpecifier>, ) -> Result<Option<QuickInfo>, LspError> { let req = TscRequest::GetQuickInfoAtPosition(( self.specifier_map.denormalize(&specifier), position, )); - self.request(snapshot, req).await.map_err(|err| { + self.request(snapshot, req, scope).await.map_err(|err| { log::error!("Unable to get quick info: {}", err); LspError::internal_error() }) } + #[allow(clippy::too_many_arguments)] pub async fn get_code_fixes( &self, snapshot: Arc<StateSnapshot>, @@ -541,6 +622,7 @@ impl TsServer { codes: Vec<i32>, format_code_settings: FormatCodeSettings, preferences: UserPreferences, + scope: Option<ModuleSpecifier>, ) -> Vec<CodeFixAction> { let req = TscRequest::GetCodeFixesAtPosition(Box::new(( self.specifier_map.denormalize(&specifier), @@ -551,7 +633,7 @@ impl TsServer { preferences, ))); let result = self - .request::<Vec<CodeFixAction>>(snapshot, req) + .request::<Vec<CodeFixAction>>(snapshot, req, scope) .await .and_then(|mut actions| { for action in &mut actions { @@ -572,6 +654,7 @@ impl TsServer { } } + #[allow(clippy::too_many_arguments)] pub async fn get_applicable_refactors( &self, snapshot: Arc<StateSnapshot>, @@ -580,6 +663,7 @@ impl TsServer { preferences: Option<UserPreferences>, trigger_kind: Option<lsp::CodeActionTriggerKind>, only: String, + scope: Option<ModuleSpecifier>, ) -> Result<Vec<ApplicableRefactorInfo>, LspError> { let trigger_kind = trigger_kind.map(|reason| match reason { lsp::CodeActionTriggerKind::INVOKED => "invoked", @@ -593,7 +677,7 @@ impl TsServer { trigger_kind, only, ))); - self.request(snapshot, req).await.map_err(|err| { + self.request(snapshot, req, scope).await.map_err(|err| { log::error!("Failed to request to tsserver {}", err); LspError::invalid_request() }) @@ -605,6 +689,7 @@ impl TsServer { code_action_data: &CodeActionData, format_code_settings: FormatCodeSettings, preferences: UserPreferences, + scope: Option<ModuleSpecifier>, ) -> Result<CombinedCodeActions, LspError> { let req = TscRequest::GetCombinedCodeFix(Box::new(( CombinedCodeFixScope { @@ -616,7 +701,7 @@ impl TsServer { preferences, ))); self - .request::<CombinedCodeActions>(snapshot, req) + .request::<CombinedCodeActions>(snapshot, req, scope) .await .and_then(|mut actions| { actions.normalize(&self.specifier_map)?; @@ -638,6 +723,7 @@ impl TsServer { refactor_name: String, action_name: String, preferences: Option<UserPreferences>, + scope: Option<ModuleSpecifier>, ) -> Result<RefactorEditInfo, LspError> { let req = TscRequest::GetEditsForRefactor(Box::new(( self.specifier_map.denormalize(&specifier), @@ -648,7 +734,7 @@ impl TsServer { preferences, ))); self - .request::<RefactorEditInfo>(snapshot, req) + .request::<RefactorEditInfo>(snapshot, req, scope) .await .and_then(|mut info| { info.normalize(&self.specifier_map)?; @@ -667,30 +753,47 @@ impl TsServer { new_specifier: ModuleSpecifier, format_code_settings: FormatCodeSettings, user_preferences: UserPreferences, - ) -> Result<Vec<FileTextChanges>, LspError> { + ) -> Result<Vec<FileTextChanges>, AnyError> { let req = TscRequest::GetEditsForFileRename(Box::new(( self.specifier_map.denormalize(&old_specifier), self.specifier_map.denormalize(&new_specifier), format_code_settings, user_preferences, ))); - self - .request::<Vec<FileTextChanges>>(snapshot, req) - .await - .and_then(|mut changes| { - for changes in &mut changes { - changes.normalize(&self.specifier_map)?; - for text_changes in &mut changes.text_changes { - text_changes.new_text = - to_percent_decoded_str(&text_changes.new_text); - } + let mut results = FuturesOrdered::new(); + for scope in snapshot + .config + .tree + .data_by_scope() + .keys() + .map(Some) + .chain(std::iter::once(None)) + { + results.push_back(self.request::<Vec<FileTextChanges>>( + snapshot.clone(), + req.clone(), + scope.cloned(), + )); + } + let mut all_changes = IndexSet::new(); + while let Some(changes) = results.next().await { + let mut changes = changes + .inspect_err(|err| { + lsp_warn!( + "Unable to get edits for file rename from TypeScript: {err}" + ); + }) + .unwrap_or_default(); + for changes in &mut changes { + changes.normalize(&self.specifier_map)?; + for text_changes in &mut changes.text_changes { + text_changes.new_text = + to_percent_decoded_str(&text_changes.new_text); } - Ok(changes) - }) - .map_err(|err| { - log::error!("Failed to request to tsserver {}", err); - LspError::invalid_request() - }) + } + all_changes.extend(changes); + } + Ok(all_changes.into_iter().collect()) } pub async fn get_document_highlights( @@ -699,6 +802,7 @@ impl TsServer { specifier: ModuleSpecifier, position: u32, files_to_search: Vec<ModuleSpecifier>, + scope: Option<ModuleSpecifier>, ) -> Result<Option<Vec<DocumentHighlights>>, LspError> { let req = TscRequest::GetDocumentHighlights(Box::new(( self.specifier_map.denormalize(&specifier), @@ -708,7 +812,7 @@ impl TsServer { .map(|s| self.specifier_map.denormalize(&s)) .collect::<Vec<_>>(), ))); - self.request(snapshot, req).await.map_err(|err| { + self.request(snapshot, req, scope).await.map_err(|err| { log::error!("Unable to get document highlights from TypeScript: {}", err); LspError::internal_error() }) @@ -719,13 +823,14 @@ impl TsServer { snapshot: Arc<StateSnapshot>, specifier: ModuleSpecifier, position: u32, + scope: Option<ModuleSpecifier>, ) -> Result<Option<DefinitionInfoAndBoundSpan>, LspError> { let req = TscRequest::GetDefinitionAndBoundSpan(( self.specifier_map.denormalize(&specifier), position, )); self - .request::<Option<DefinitionInfoAndBoundSpan>>(snapshot, req) + .request::<Option<DefinitionInfoAndBoundSpan>>(snapshot, req, scope) .await .and_then(|mut info| { if let Some(info) = &mut info { @@ -744,13 +849,14 @@ impl TsServer { snapshot: Arc<StateSnapshot>, specifier: ModuleSpecifier, position: u32, + scope: Option<ModuleSpecifier>, ) -> Result<Option<Vec<DefinitionInfo>>, LspError> { let req = TscRequest::GetTypeDefinitionAtPosition(( self.specifier_map.denormalize(&specifier), position, )); self - .request::<Option<Vec<DefinitionInfo>>>(snapshot, req) + .request::<Option<Vec<DefinitionInfo>>>(snapshot, req, scope) .await .and_then(|mut infos| { for info in infos.iter_mut().flatten() { @@ -771,6 +877,7 @@ impl TsServer { position: u32, options: GetCompletionsAtPositionOptions, format_code_settings: FormatCodeSettings, + scope: Option<ModuleSpecifier>, ) -> Option<CompletionInfo> { let req = TscRequest::GetCompletionsAtPosition(Box::new(( self.specifier_map.denormalize(&specifier), @@ -778,7 +885,7 @@ impl TsServer { options, format_code_settings, ))); - match self.request(snapshot, req).await { + match self.request(snapshot, req, scope).await { Ok(maybe_info) => maybe_info, Err(err) => { log::error!("Unable to get completion info from TypeScript: {:#}", err); @@ -791,6 +898,7 @@ impl TsServer { &self, snapshot: Arc<StateSnapshot>, args: GetCompletionDetailsArgs, + scope: Option<ModuleSpecifier>, ) -> Result<Option<CompletionEntryDetails>, AnyError> { let req = TscRequest::GetCompletionEntryDetails(Box::new(( self.specifier_map.denormalize(&args.specifier), @@ -802,7 +910,7 @@ impl TsServer { args.data, ))); self - .request::<Option<CompletionEntryDetails>>(snapshot, req) + .request::<Option<CompletionEntryDetails>>(snapshot, req, scope) .await .and_then(|mut details| { if let Some(details) = &mut details { @@ -817,35 +925,60 @@ impl TsServer { snapshot: Arc<StateSnapshot>, specifier: ModuleSpecifier, position: u32, - ) -> Result<Option<Vec<ImplementationLocation>>, LspError> { + ) -> Result<Option<Vec<ImplementationLocation>>, AnyError> { let req = TscRequest::GetImplementationAtPosition(( self.specifier_map.denormalize(&specifier), position, )); - self - .request::<Option<Vec<ImplementationLocation>>>(snapshot, req) - .await - .and_then(|mut locations| { - for location in locations.iter_mut().flatten() { - location.normalize(&self.specifier_map)?; - } - Ok(locations) - }) - .map_err(|err| { - log::error!("Failed to request to tsserver {}", err); - LspError::invalid_request() - }) + let mut results = FuturesOrdered::new(); + for scope in snapshot + .config + .tree + .data_by_scope() + .keys() + .map(Some) + .chain(std::iter::once(None)) + { + results.push_back(self.request::<Option<Vec<ImplementationLocation>>>( + snapshot.clone(), + req.clone(), + scope.cloned(), + )); + } + let mut all_locations = IndexSet::new(); + while let Some(locations) = results.next().await { + let locations = locations + .inspect_err(|err| { + let err = err.to_string(); + if !err.contains("Could not find source file") { + lsp_warn!("Unable to get implementations from TypeScript: {err}"); + } + }) + .unwrap_or_default(); + let Some(mut locations) = locations else { + continue; + }; + for location in &mut locations { + location.normalize(&self.specifier_map)?; + } + all_locations.extend(locations); + } + if all_locations.is_empty() { + return Ok(None); + } + Ok(Some(all_locations.into_iter().collect())) } pub async fn get_outlining_spans( &self, snapshot: Arc<StateSnapshot>, specifier: ModuleSpecifier, + scope: Option<ModuleSpecifier>, ) -> Result<Vec<OutliningSpan>, LspError> { let req = TscRequest::GetOutliningSpans((self .specifier_map .denormalize(&specifier),)); - self.request(snapshot, req).await.map_err(|err| { + self.request(snapshot, req, scope).await.map_err(|err| { log::error!("Failed to request to tsserver {}", err); LspError::invalid_request() }) @@ -856,24 +989,42 @@ impl TsServer { snapshot: Arc<StateSnapshot>, specifier: ModuleSpecifier, position: u32, - ) -> Result<Vec<CallHierarchyIncomingCall>, LspError> { + ) -> Result<Vec<CallHierarchyIncomingCall>, AnyError> { let req = TscRequest::ProvideCallHierarchyIncomingCalls(( self.specifier_map.denormalize(&specifier), position, )); - self - .request::<Vec<CallHierarchyIncomingCall>>(snapshot, req) - .await - .and_then(|mut calls| { - for call in &mut calls { - call.normalize(&self.specifier_map)?; - } - Ok(calls) - }) - .map_err(|err| { - log::error!("Failed to request to tsserver {}", err); - LspError::invalid_request() - }) + let mut results = FuturesOrdered::new(); + for scope in snapshot + .config + .tree + .data_by_scope() + .keys() + .map(Some) + .chain(std::iter::once(None)) + { + results.push_back(self.request::<Vec<CallHierarchyIncomingCall>>( + snapshot.clone(), + req.clone(), + scope.cloned(), + )); + } + let mut all_calls = IndexSet::new(); + while let Some(calls) = results.next().await { + let mut calls = calls + .inspect_err(|err| { + let err = err.to_string(); + if !err.contains("Could not find source file") { + lsp_warn!("Unable to get incoming calls from TypeScript: {err}"); + } + }) + .unwrap_or_default(); + for call in &mut calls { + call.normalize(&self.specifier_map)?; + } + all_calls.extend(calls) + } + Ok(all_calls.into_iter().collect()) } pub async fn provide_call_hierarchy_outgoing_calls( @@ -881,13 +1032,14 @@ impl TsServer { snapshot: Arc<StateSnapshot>, specifier: ModuleSpecifier, position: u32, + scope: Option<ModuleSpecifier>, ) -> Result<Vec<CallHierarchyOutgoingCall>, LspError> { let req = TscRequest::ProvideCallHierarchyOutgoingCalls(( self.specifier_map.denormalize(&specifier), position, )); self - .request::<Vec<CallHierarchyOutgoingCall>>(snapshot, req) + .request::<Vec<CallHierarchyOutgoingCall>>(snapshot, req, scope) .await .and_then(|mut calls| { for call in &mut calls { @@ -906,13 +1058,14 @@ impl TsServer { snapshot: Arc<StateSnapshot>, specifier: ModuleSpecifier, position: u32, + scope: Option<ModuleSpecifier>, ) -> Result<Option<OneOrMany<CallHierarchyItem>>, LspError> { let req = TscRequest::PrepareCallHierarchy(( self.specifier_map.denormalize(&specifier), position, )); self - .request::<Option<OneOrMany<CallHierarchyItem>>>(snapshot, req) + .request::<Option<OneOrMany<CallHierarchyItem>>>(snapshot, req, scope) .await .and_then(|mut items| { match &mut items { @@ -939,7 +1092,7 @@ impl TsServer { snapshot: Arc<StateSnapshot>, specifier: ModuleSpecifier, position: u32, - ) -> Result<Option<Vec<RenameLocation>>, LspError> { + ) -> Result<Option<Vec<RenameLocation>>, AnyError> { let req = TscRequest::FindRenameLocations(( self.specifier_map.denormalize(&specifier), position, @@ -947,19 +1100,43 @@ impl TsServer { false, false, )); - self - .request::<Option<Vec<RenameLocation>>>(snapshot, req) - .await - .and_then(|mut locations| { - for location in locations.iter_mut().flatten() { - location.normalize(&self.specifier_map)?; - } - Ok(locations) - }) - .map_err(|err| { - log::error!("Failed to request to tsserver {}", err); - LspError::invalid_request() - }) + let mut results = FuturesOrdered::new(); + for scope in snapshot + .config + .tree + .data_by_scope() + .keys() + .map(Some) + .chain(std::iter::once(None)) + { + results.push_back(self.request::<Option<Vec<RenameLocation>>>( + snapshot.clone(), + req.clone(), + scope.cloned(), + )); + } + let mut all_locations = IndexSet::new(); + while let Some(locations) = results.next().await { + let locations = locations + .inspect_err(|err| { + let err = err.to_string(); + if !err.contains("Could not find source file") { + lsp_warn!("Unable to get rename locations from TypeScript: {err}"); + } + }) + .unwrap_or_default(); + let Some(mut locations) = locations else { + continue; + }; + for symbol in &mut locations { + symbol.normalize(&self.specifier_map)?; + } + all_locations.extend(locations); + } + if all_locations.is_empty() { + return Ok(None); + } + Ok(Some(all_locations.into_iter().collect())) } pub async fn get_smart_selection_range( @@ -967,12 +1144,13 @@ impl TsServer { snapshot: Arc<StateSnapshot>, specifier: ModuleSpecifier, position: u32, + scope: Option<ModuleSpecifier>, ) -> Result<SelectionRange, LspError> { let req = TscRequest::GetSmartSelectionRange(( self.specifier_map.denormalize(&specifier), position, )); - self.request(snapshot, req).await.map_err(|err| { + self.request(snapshot, req, scope).await.map_err(|err| { log::error!("Failed to request to tsserver {}", err); LspError::invalid_request() }) @@ -983,6 +1161,7 @@ impl TsServer { snapshot: Arc<StateSnapshot>, specifier: ModuleSpecifier, range: Range<u32>, + scope: Option<ModuleSpecifier>, ) -> Result<Classifications, LspError> { let req = TscRequest::GetEncodedSemanticClassifications(( self.specifier_map.denormalize(&specifier), @@ -992,7 +1171,7 @@ impl TsServer { }, "2020", )); - self.request(snapshot, req).await.map_err(|err| { + self.request(snapshot, req, scope).await.map_err(|err| { log::error!("Failed to request to tsserver {}", err); LspError::invalid_request() }) @@ -1004,13 +1183,14 @@ impl TsServer { specifier: ModuleSpecifier, position: u32, options: SignatureHelpItemsOptions, + scope: Option<ModuleSpecifier>, ) -> Result<Option<SignatureHelpItems>, LspError> { let req = TscRequest::GetSignatureHelpItems(( self.specifier_map.denormalize(&specifier), position, options, )); - self.request(snapshot, req).await.map_err(|err| { + self.request(snapshot, req, scope).await.map_err(|err| { log::error!("Failed to request to tsserver: {}", err); LspError::invalid_request() }) @@ -1020,7 +1200,7 @@ impl TsServer { &self, snapshot: Arc<StateSnapshot>, args: GetNavigateToItemsArgs, - ) -> Result<Vec<NavigateToItem>, LspError> { + ) -> Result<Vec<NavigateToItem>, AnyError> { let req = TscRequest::GetNavigateToItems(( args.search, args.max_result_count, @@ -1029,19 +1209,34 @@ impl TsServer { Err(_) => f, }), )); - self - .request::<Vec<NavigateToItem>>(snapshot, req) - .await - .and_then(|mut items| { - for items in &mut items { - items.normalize(&self.specifier_map)?; - } - Ok(items) - }) - .map_err(|err| { - log::error!("Failed request to tsserver: {}", err); - LspError::invalid_request() - }) + let mut results = FuturesOrdered::new(); + for scope in snapshot + .config + .tree + .data_by_scope() + .keys() + .map(Some) + .chain(std::iter::once(None)) + { + results.push_back(self.request::<Vec<NavigateToItem>>( + snapshot.clone(), + req.clone(), + scope.cloned(), + )); + } + let mut all_items = IndexSet::new(); + while let Some(items) = results.next().await { + let mut items = items + .inspect_err(|err| { + lsp_warn!("Unable to get 'navigate to' items from TypeScript: {err}"); + }) + .unwrap_or_default(); + for item in &mut items { + item.normalize(&self.specifier_map)?; + } + all_items.extend(items) + } + Ok(all_items.into_iter().collect()) } pub async fn provide_inlay_hints( @@ -1050,13 +1245,14 @@ impl TsServer { specifier: ModuleSpecifier, text_span: TextSpan, user_preferences: UserPreferences, + scope: Option<ModuleSpecifier>, ) -> Result<Option<Vec<InlayHint>>, LspError> { let req = TscRequest::ProvideInlayHints(( self.specifier_map.denormalize(&specifier), text_span, user_preferences, )); - self.request(snapshot, req).await.map_err(|err| { + self.request(snapshot, req, scope).await.map_err(|err| { log::error!("Unable to get inlay hints: {}", err); LspError::internal_error() }) @@ -1066,6 +1262,7 @@ impl TsServer { &self, snapshot: Arc<StateSnapshot>, req: TscRequest, + scope: Option<ModuleSpecifier>, ) -> Result<R, AnyError> where R: de::DeserializeOwned, @@ -1074,7 +1271,7 @@ impl TsServer { .performance .mark(format!("tsc.request.{}", req.method())); let r = self - .request_with_cancellation(snapshot, req, Default::default()) + .request_with_cancellation(snapshot, req, scope, Default::default()) .await; self.performance.measure(mark); r @@ -1084,6 +1281,7 @@ impl TsServer { &self, snapshot: Arc<StateSnapshot>, req: TscRequest, + scope: Option<ModuleSpecifier>, token: CancellationToken, ) -> Result<R, AnyError> where @@ -1106,7 +1304,7 @@ impl TsServer { if self .sender - .send((req, snapshot, tx, token.clone(), change)) + .send((req, scope, snapshot, tx, token.clone(), change)) .is_err() { return Err(anyhow!("failed to send request to tsc thread")); @@ -1261,7 +1459,7 @@ async fn get_isolate_assets( state_snapshot: Arc<StateSnapshot>, ) -> Vec<AssetDocument> { let req = TscRequest::GetAssets; - let res: Value = ts_server.request(state_snapshot, req).await.unwrap(); + let res: Value = ts_server.request(state_snapshot, req, None).await.unwrap(); let response_assets = match res { Value::Array(value) => value, _ => unreachable!(), @@ -1401,7 +1599,7 @@ pub enum OneOrMany<T> { } /// Aligns with ts.ScriptElementKind -#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)] +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, Hash)] pub enum ScriptElementKind { #[serde(rename = "")] Unknown, @@ -1591,7 +1789,7 @@ impl From<ScriptElementKind> for lsp::SymbolKind { } } -#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)] +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, Hash)] #[serde(rename_all = "camelCase")] pub struct TextSpan { pub start: u32, @@ -1812,7 +2010,7 @@ impl QuickInfo { } } -#[derive(Debug, Clone, Deserialize, Serialize)] +#[derive(Debug, Eq, PartialEq, Hash, Clone, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct DocumentSpan { text_span: TextSpan, @@ -1920,7 +2118,7 @@ pub enum MatchKind { CamelCase, } -#[derive(Debug, Clone, Deserialize)] +#[derive(Debug, Clone, Eq, PartialEq, Hash, Deserialize)] #[serde(rename_all = "camelCase")] pub struct NavigateToItem { name: String, @@ -2186,7 +2384,7 @@ impl NavigationTree { } } -#[derive(Debug, Deserialize)] +#[derive(Debug, Eq, PartialEq, Hash, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ImplementationLocation { #[serde(flatten)] @@ -2234,7 +2432,7 @@ impl ImplementationLocation { } } -#[derive(Debug, Deserialize)] +#[derive(Debug, Eq, PartialEq, Hash, Deserialize)] #[serde(rename_all = "camelCase")] pub struct RenameLocation { #[serde(flatten)] @@ -2264,10 +2462,7 @@ impl RenameLocations { new_name: &str, language_server: &language_server::Inner, ) -> Result<lsp::WorkspaceEdit, AnyError> { - let mut text_document_edit_map: HashMap< - LspClientUrl, - lsp::TextDocumentEdit, - > = HashMap::new(); + let mut text_document_edit_map = IndexMap::new(); let mut includes_non_files = false; for location in self.locations.iter() { let specifier = resolve_url(&location.document_span.file_name)?; @@ -2344,7 +2539,7 @@ pub struct HighlightSpan { kind: HighlightSpanKind, } -#[derive(Debug, Deserialize, Serialize)] +#[derive(Debug, Eq, PartialEq, Hash, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct DefinitionInfo { // kind: ScriptElementKind, @@ -2433,7 +2628,7 @@ impl DocumentHighlights { } } -#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)] +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, Hash)] #[serde(rename_all = "camelCase")] pub struct TextChange { pub span: TextSpan, @@ -2459,7 +2654,7 @@ impl TextChange { } } -#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)] +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, Hash)] #[serde(rename_all = "camelCase")] pub struct FileTextChanges { pub file_name: String, @@ -2846,7 +3041,7 @@ impl CombinedCodeActions { } } -#[derive(Debug, Deserialize, Serialize)] +#[derive(Debug, Eq, PartialEq, Hash, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct ReferencedSymbol { pub definition: ReferencedSymbolDefinitionInfo, @@ -2866,7 +3061,7 @@ impl ReferencedSymbol { } } -#[derive(Debug, Deserialize, Serialize)] +#[derive(Debug, Eq, PartialEq, Hash, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct ReferencedSymbolDefinitionInfo { #[serde(flatten)] @@ -2883,7 +3078,7 @@ impl ReferencedSymbolDefinitionInfo { } } -#[derive(Debug, Deserialize, Serialize)] +#[derive(Debug, Eq, PartialEq, Hash, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct ReferencedSymbolEntry { #[serde(default)] @@ -2902,7 +3097,7 @@ impl ReferencedSymbolEntry { } } -#[derive(Debug, Deserialize, Serialize)] +#[derive(Debug, Eq, PartialEq, Hash, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct ReferenceEntry { // is_write_access: bool, @@ -2941,7 +3136,7 @@ impl ReferenceEntry { } } -#[derive(Debug, Deserialize)] +#[derive(Debug, Eq, PartialEq, Hash, Deserialize)] #[serde(rename_all = "camelCase")] pub struct CallHierarchyItem { name: String, @@ -3058,7 +3253,7 @@ impl CallHierarchyItem { } } -#[derive(Debug, Deserialize)] +#[derive(Debug, Eq, PartialEq, Hash, Deserialize)] #[serde(rename_all = "camelCase")] pub struct CallHierarchyIncomingCall { from: CallHierarchyItem, @@ -3098,7 +3293,7 @@ impl CallHierarchyIncomingCall { } } -#[derive(Debug, Deserialize)] +#[derive(Debug, Eq, PartialEq, Hash, Deserialize)] #[serde(rename_all = "camelCase")] pub struct CallHierarchyOutgoingCall { to: CallHierarchyItem, @@ -4166,6 +4361,7 @@ fn op_resolve( struct TscRequestArray { request: TscRequest, + scope: Option<String>, id: Smi<usize>, change: convert::OptionNull<PendingChange>, } @@ -4185,12 +4381,18 @@ impl<'a> ToV8<'a> for TscRequestArray { .v8_string(scope) .into(); let args = args.unwrap_or_else(|| v8::Array::new(scope, 0).into()); + let scope_url = serde_v8::to_v8(scope, self.scope) + .map_err(AnyError::from) + .map_err(StdAnyError::from)?; let change = self.change.to_v8(scope).unwrap_infallible(); Ok( - v8::Array::new_with_elements(scope, &[id, method_name, args, change]) - .into(), + v8::Array::new_with_elements( + scope, + &[id, method_name, args, scope_url, change], + ) + .into(), ) } } @@ -4206,7 +4408,7 @@ async fn op_poll_requests( state.pending_requests.take().unwrap() }; - let Some((request, snapshot, response_tx, token, change)) = + let Some((request, scope, snapshot, response_tx, token, change)) = pending_requests.recv().await else { return None.into(); @@ -4227,6 +4429,7 @@ async fn op_poll_requests( Some(TscRequestArray { request, + scope: scope.map(|s| s.into()), id: Smi(id), change: change.into(), }) @@ -4283,36 +4486,58 @@ fn op_respond( } } +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +struct ScriptNames { + unscoped: IndexSet<String>, + by_scope: BTreeMap<ModuleSpecifier, IndexSet<String>>, +} + #[op2] #[serde] -fn op_script_names(state: &mut OpState) -> Vec<String> { +fn op_script_names(state: &mut OpState) -> ScriptNames { let state = state.borrow_mut::<State>(); let mark = state.performance.mark("tsc.op.op_script_names"); - let mut seen = HashSet::new(); - let mut result = Vec::new(); + let mut result = ScriptNames { + unscoped: IndexSet::new(), + by_scope: BTreeMap::from_iter( + state + .state_snapshot + .config + .tree + .data_by_scope() + .keys() + .map(|s| (s.clone(), IndexSet::new())), + ), + }; - if state - .state_snapshot - .documents - .has_injected_types_node_package() - { - // ensure this is first so it resolves the node types first - let specifier = "asset:///node_types.d.ts"; - result.push(specifier.to_string()); - seen.insert(Cow::Borrowed(specifier)); + let scopes_with_node_specifier = + state.state_snapshot.documents.scopes_with_node_specifier(); + if scopes_with_node_specifier.contains(&None) { + result + .unscoped + .insert("asset:///node_types.d.ts".to_string()); + } + for (scope, script_names) in &mut result.by_scope { + if scopes_with_node_specifier.contains(&Some(scope.clone())) { + script_names.insert("asset:///node_types.d.ts".to_string()); + } } // inject these next because they're global - for (referrer, specifiers) in - state.state_snapshot.resolver.graph_imports_by_referrer() - { - for specifier in specifiers { - if seen.insert(Cow::Borrowed(specifier.as_str())) { - result.push(specifier.to_string()); + for (scope, script_names) in &mut result.by_scope { + for (referrer, specifiers) in state + .state_snapshot + .resolver + .graph_imports_by_referrer(scope) + { + for specifier in specifiers { + script_names.insert(specifier.to_string()); + state + .root_referrers + .entry(specifier.clone()) + .or_insert(referrer.clone()); } - state - .root_referrers - .insert(specifier.clone(), referrer.clone()); } } @@ -4324,9 +4549,11 @@ fn op_script_names(state: &mut OpState) -> Vec<String> { for doc in &docs { let specifier = doc.specifier(); let is_open = doc.is_open(); - if seen.insert(Cow::Borrowed(specifier.as_str())) - && (is_open || specifier.scheme() == "file") - { + if is_open || specifier.scheme() == "file" { + let script_names = doc + .scope() + .and_then(|s| result.by_scope.get_mut(s)) + .unwrap_or(&mut result.unscoped); let types_specifier = (|| { let documents = &state.state_snapshot.documents; let types = doc.maybe_types_dependency().maybe_specifier()?; @@ -4341,25 +4568,29 @@ fn op_script_names(state: &mut OpState) -> Vec<String> { // If there is a types dep, use that as the root instead. But if the doc // is open, include both as roots. if let Some(types_specifier) = &types_specifier { - if seen.insert(Cow::Owned(types_specifier.to_string())) { - result.push(types_specifier.to_string()); - } + script_names.insert(types_specifier.to_string()); } if types_specifier.is_none() || is_open { - result.push(specifier.to_string()); + script_names.insert(specifier.to_string()); } } } - let r = result - .into_iter() - .map(|s| match ModuleSpecifier::parse(&s) { - Ok(s) => state.specifier_map.denormalize(&s), - Err(_) => s, - }) - .collect(); + for script_names in result + .by_scope + .values_mut() + .chain(std::iter::once(&mut result.unscoped)) + { + *script_names = std::mem::take(script_names) + .into_iter() + .map(|s| match ModuleSpecifier::parse(&s) { + Ok(s) => state.specifier_map.denormalize(&s), + Err(_) => s, + }) + .collect(); + } state.performance.measure(mark); - r + result } #[op2] @@ -4376,16 +4607,6 @@ fn op_script_version( Ok(r) } -#[op2] -#[serde] -fn op_ts_config(state: &mut OpState) -> serde_json::Value { - let state = state.borrow_mut::<State>(); - let mark = state.performance.mark("tsc.op.op_ts_config"); - let r = json!(state.state_snapshot.config.tree.root_ts_config()); - state.performance.measure(mark); - r -} - #[op2(fast)] #[number] fn op_project_version(state: &mut OpState) -> usize { @@ -4522,7 +4743,6 @@ deno_core::extension!(deno_tsc, op_respond, op_script_names, op_script_version, - op_ts_config, op_project_version, op_poll_requests, ], @@ -4541,7 +4761,7 @@ deno_core::extension!(deno_tsc, }, ); -#[derive(Debug, Deserialize_repr, Serialize_repr)] +#[derive(Debug, Clone, Deserialize_repr, Serialize_repr)] #[repr(u32)] pub enum CompletionTriggerKind { Invoked = 1, @@ -4566,7 +4786,7 @@ pub type QuotePreference = config::QuoteStyle; pub type ImportModuleSpecifierPreference = config::ImportModuleSpecifier; -#[derive(Debug, Serialize)] +#[derive(Debug, Clone, Serialize)] #[serde(rename_all = "kebab-case")] #[allow(dead_code)] pub enum ImportModuleSpecifierEnding { @@ -4576,7 +4796,7 @@ pub enum ImportModuleSpecifierEnding { Js, } -#[derive(Debug, Serialize)] +#[derive(Debug, Clone, Serialize)] #[serde(rename_all = "kebab-case")] #[allow(dead_code)] pub enum IncludeInlayParameterNameHints { @@ -4597,7 +4817,7 @@ impl From<&config::InlayHintsParamNamesEnabled> } } -#[derive(Debug, Serialize)] +#[derive(Debug, Clone, Serialize)] #[serde(rename_all = "kebab-case")] #[allow(dead_code)] pub enum IncludePackageJsonAutoImports { @@ -4608,7 +4828,7 @@ pub enum IncludePackageJsonAutoImports { pub type JsxAttributeCompletionStyle = config::JsxAttributeCompletionStyle; -#[derive(Debug, Default, Serialize)] +#[derive(Debug, Default, Clone, Serialize)] #[serde(rename_all = "camelCase")] pub struct GetCompletionsAtPositionOptions { #[serde(flatten)] @@ -4619,7 +4839,7 @@ pub struct GetCompletionsAtPositionOptions { pub trigger_kind: Option<CompletionTriggerKind>, } -#[derive(Debug, Default, Serialize)] +#[derive(Debug, Default, Clone, Serialize)] #[serde(rename_all = "camelCase")] pub struct UserPreferences { #[serde(skip_serializing_if = "Option::is_none")] @@ -4807,14 +5027,14 @@ impl UserPreferences { } } -#[derive(Debug, Serialize)] +#[derive(Debug, Clone, Serialize)] #[serde(rename_all = "camelCase")] pub struct SignatureHelpItemsOptions { #[serde(skip_serializing_if = "Option::is_none")] pub trigger_reason: Option<SignatureHelpTriggerReason>, } -#[derive(Debug, Serialize)] +#[derive(Debug, Clone, Serialize)] pub enum SignatureHelpTriggerKind { #[serde(rename = "characterTyped")] CharacterTyped, @@ -4837,7 +5057,7 @@ impl From<lsp::SignatureHelpTriggerKind> for SignatureHelpTriggerKind { } } -#[derive(Debug, Serialize)] +#[derive(Debug, Clone, Serialize)] #[serde(rename_all = "camelCase")] pub struct SignatureHelpTriggerReason { pub kind: SignatureHelpTriggerKind, @@ -4882,7 +5102,7 @@ pub struct GetNavigateToItemsArgs { pub file: Option<String>, } -#[derive(Serialize, Clone, Copy)] +#[derive(Debug, Serialize, Clone, Copy)] pub struct TscTextRange { pos: u32, end: u32, @@ -4897,7 +5117,7 @@ impl From<Range<u32>> for TscTextRange { } } -#[derive(Serialize, Clone)] +#[derive(Debug, Serialize, Clone)] #[serde(rename_all = "camelCase")] pub struct CombinedCodeFixScope { r#type: &'static str, @@ -4907,7 +5127,7 @@ pub struct CombinedCodeFixScope { #[derive(Serialize, Clone, Copy)] pub struct JsNull; -#[derive(Serialize)] +#[derive(Debug, Clone, Serialize)] pub enum TscRequest { GetDiagnostics((Vec<String>, usize)), GetAssets, @@ -5226,6 +5446,19 @@ mod tests { let performance = Arc::new(Performance::default()); let ts_server = TsServer::new(performance); ts_server.start(None).unwrap(); + ts_server.project_changed( + snapshot.clone(), + [], + Some( + snapshot + .config + .tree + .data_by_scope() + .iter() + .map(|(s, d)| (s.clone(), d.ts_config.clone())) + .collect(), + ), + ); (ts_server, snapshot, cache) } @@ -5654,7 +5887,7 @@ mod tests { ts_server.project_changed( snapshot.clone(), [(&specifier_dep, ChangeKind::Opened)], - false, + None, ); let specifier = resolve_url("file:///a.ts").unwrap(); let diagnostics = ts_server @@ -5739,6 +5972,7 @@ mod tests { trigger_kind: None, }, Default::default(), + Some(ModuleSpecifier::parse("file:///").unwrap()), ) .await .unwrap(); @@ -5755,6 +5989,7 @@ mod tests { preferences: None, data: None, }, + Some(ModuleSpecifier::parse("file:///").unwrap()), ) .await .unwrap() @@ -5897,6 +6132,7 @@ mod tests { ..Default::default() }, FormatCodeSettings::from(&fmt_options_config), + Some(ModuleSpecifier::parse("file:///").unwrap()), ) .await .unwrap(); @@ -5922,6 +6158,7 @@ mod tests { }), data: entry.data.clone(), }, + Some(ModuleSpecifier::parse("file:///").unwrap()), ) .await .unwrap() @@ -6080,7 +6317,7 @@ mod tests { fn change<S: AsRef<str>>( project_version: usize, scripts: impl IntoIterator<Item = (S, ChangeKind)>, - config_changed: bool, + new_configs_by_scope: Option<BTreeMap<ModuleSpecifier, Arc<LspTsConfig>>>, ) -> PendingChange { PendingChange { project_version, @@ -6088,20 +6325,20 @@ mod tests { .into_iter() .map(|(s, c)| (s.as_ref().into(), c)) .collect(), - config_changed, + new_configs_by_scope, } } let cases = [ ( // start - change(1, [("file:///a.ts", Closed)], false), + change(1, [("file:///a.ts", Closed)], None), // new - change(2, Some(("file:///b.ts", Opened)), false), + change(2, Some(("file:///b.ts", Opened)), None), // expected change( 2, [("file:///a.ts", Closed), ("file:///b.ts", Opened)], - false, + None, ), ), ( @@ -6109,48 +6346,48 @@ mod tests { change( 1, [("file:///a.ts", Closed), ("file:///b.ts", Opened)], - false, + None, ), // new change( 2, // a gets closed then reopened, b gets opened then closed [("file:///a.ts", Opened), ("file:///b.ts", Closed)], - false, + None, ), // expected change( 2, [("file:///a.ts", Opened), ("file:///b.ts", Closed)], - false, + None, ), ), ( change( 1, [("file:///a.ts", Opened), ("file:///b.ts", Modified)], - false, + None, ), // new change( 2, // a gets opened then modified, b gets modified then closed [("file:///a.ts", Opened), ("file:///b.ts", Closed)], - false, + None, ), // expected change( 2, [("file:///a.ts", Opened), ("file:///b.ts", Closed)], - false, + None, ), ), ]; for (start, new, expected) in cases { let mut pending = start; - pending.coalesce(new.project_version, new.modified_scripts, false); - assert_eq!(pending, expected); + pending.coalesce(new.project_version, new.modified_scripts, None); + assert_eq!(json!(pending), json!(expected)); } } } diff --git a/cli/tsc/99_main_compiler.js b/cli/tsc/99_main_compiler.js index 16e8f1ee9..3e37070a9 100644 --- a/cli/tsc/99_main_compiler.js +++ b/cli/tsc/99_main_compiler.js @@ -161,9 +161,6 @@ delete Object.prototype.__proto__; /** @type {Map<string, number>} */ const sourceRefCounts = new Map(); - /** @type {string[]=} */ - let scriptFileNamesCache; - /** @type {Map<string, string>} */ const scriptVersionCache = new Map(); @@ -172,14 +169,15 @@ delete Object.prototype.__proto__; const isCjsCache = new SpecifierIsCjsCache(); - /** @type {ts.CompilerOptions | null} */ - let tsConfigCache = null; - /** @type {number | null} */ let projectVersionCache = null; + /** @type {string | null} */ let lastRequestMethod = null; + /** @type {string | null} */ + let lastRequestScope = null; + const ChangeKind = { Opened: 0, Modified: 1, @@ -542,8 +540,19 @@ delete Object.prototype.__proto__; } } - /** @type {ts.LanguageService & { [k:string]: any }} */ - let languageService; + /** @typedef {{ + * ls: ts.LanguageService & { [k:string]: any }, + * compilerOptions: ts.CompilerOptions, + * }} LanguageServiceEntry */ + /** @type {{ unscoped: LanguageServiceEntry, byScope: Map<string, LanguageServiceEntry> }} */ + const languageServiceEntries = { + // @ts-ignore Will be set later. + unscoped: null, + byScope: new Map(), + }; + + /** @type {{ unscoped: string[], byScope: Map<string, string[]> } | null} */ + let scriptNamesCache = null; /** An object literal of the incremental compiler host, which provides the * specific "bindings" to the Deno environment that tsc needs to work. @@ -785,32 +794,24 @@ delete Object.prototype.__proto__; if (logDebug) { debug("host.getCompilationSettings()"); } - if (tsConfigCache) { - return tsConfigCache; - } - const tsConfig = normalizeConfig(ops.op_ts_config()); - const { options, errors } = ts - .convertCompilerOptionsFromJson(tsConfig, ""); - Object.assign(options, { - allowNonTsExtensions: true, - allowImportingTsExtensions: true, - }); - if (errors.length > 0 && logDebug) { - debug(ts.formatDiagnostics(errors, host)); - } - tsConfigCache = options; - return options; + return (lastRequestScope + ? languageServiceEntries.byScope.get(lastRequestScope)?.compilerOptions + : null) ?? languageServiceEntries.unscoped.compilerOptions; }, getScriptFileNames() { if (logDebug) { debug("host.getScriptFileNames()"); } - // tsc requests the script file names multiple times even though it can't - // possibly have changed, so we will memoize it on a per request basis. - if (scriptFileNamesCache) { - return scriptFileNamesCache; + if (!scriptNamesCache) { + const { unscoped, byScope } = ops.op_script_names(); + scriptNamesCache = { + unscoped, + byScope: new Map(Object.entries(byScope)), + }; } - return scriptFileNamesCache = ops.op_script_names(); + return (lastRequestScope + ? scriptNamesCache.byScope.get(lastRequestScope) + : null) ?? scriptNamesCache.unscoped; }, getScriptVersion(specifier) { if (logDebug) { @@ -953,7 +954,7 @@ delete Object.prototype.__proto__; } } - /** @param {Record<string, string>} config */ + /** @param {Record<string, unknown>} config */ function normalizeConfig(config) { // the typescript compiler doesn't know about the precompile // transform at the moment, so just tell it we're using react-jsx @@ -966,6 +967,21 @@ delete Object.prototype.__proto__; return config; } + /** @param {Record<string, unknown>} config */ + function lspTsConfigToCompilerOptions(config) { + const normalizedConfig = normalizeConfig(config); + const { options, errors } = ts + .convertCompilerOptionsFromJson(normalizedConfig, ""); + Object.assign(options, { + allowNonTsExtensions: true, + allowImportingTsExtensions: true, + }); + if (errors.length > 0 && logDebug) { + debug(ts.formatDiagnostics(errors, host)); + } + return options; + } + /** The API that is called by Rust when executing a request. * @param {Request} request */ @@ -1079,7 +1095,7 @@ delete Object.prototype.__proto__; /** * @param {number} _id * @param {any} data - * @param {any | null} error + * @param {string | null} error */ // TODO(bartlomieju): this feels needlessly generic, both type chcking // and language server use it with inefficient serialization. Id is not used @@ -1088,19 +1104,19 @@ delete Object.prototype.__proto__; if (error) { ops.op_respond( "error", - "stack" in error ? error.stack.toString() : error.toString(), + error, ); } else { ops.op_respond(JSON.stringify(data), ""); } } - /** @typedef {[[string, number][], number, boolean] } PendingChange */ + /** @typedef {[[string, number][], number, [string, any][]] } PendingChange */ /** * @template T * @typedef {T | null} Option<T> */ - /** @returns {Promise<[number, string, any[], Option<PendingChange>] | null>} */ + /** @returns {Promise<[number, string, any[], string | null, Option<PendingChange>] | null>} */ async function pollRequests() { return await ops.op_poll_requests(); } @@ -1113,7 +1129,30 @@ delete Object.prototype.__proto__; throw new Error("The language server has already been initialized."); } hasStarted = true; - languageService = ts.createLanguageService(host, documentRegistry); + languageServiceEntries.unscoped = { + ls: ts.createLanguageService( + host, + documentRegistry, + ), + compilerOptions: lspTsConfigToCompilerOptions({ + "allowJs": true, + "esModuleInterop": true, + "experimentalDecorators": false, + "isolatedModules": true, + "lib": ["deno.ns", "deno.window", "deno.unstable"], + "module": "esnext", + "moduleDetection": "force", + "noEmit": true, + "resolveJsonModule": true, + "strict": true, + "target": "esnext", + "useDefineForClassFields": true, + "useUnknownInCatchVariables": false, + "jsx": "react", + "jsxFactory": "React.createElement", + "jsxFragmentFactory": "React.Fragment", + }), + }; setLogDebug(enableDebugLogging, "TSLS"); debug("serverInit()"); @@ -1123,39 +1162,68 @@ delete Object.prototype.__proto__; break; } try { - serverRequest(request[0], request[1], request[2], request[3]); - } catch (err) { - const reqString = "[" + request.map((v) => - JSON.stringify(v) - ).join(", ") + "]"; - error( - `Error occurred processing request ${reqString} : ${ - "stack" in err ? err.stack : err - }`, + serverRequest( + request[0], + request[1], + request[2], + request[3], + request[4], ); + } catch (err) { + error(`Internal error occurred processing request: ${err}`); } } } /** + * @param {any} error + * @param {any[] | null} args + */ + function formatErrorWithArgs(error, args) { + let errorString = "stack" in error + ? error.stack.toString() + : error.toString(); + if (args) { + errorString += `\nFor request: [${ + args.map((v) => JSON.stringify(v)).join(", ") + }]`; + } + return errorString; + } + + /** * @param {number} id * @param {string} method * @param {any[]} args + * @param {string | null} scope * @param {PendingChange | null} maybeChange */ - function serverRequest(id, method, args, maybeChange) { + function serverRequest(id, method, args, scope, maybeChange) { if (logDebug) { - debug(`serverRequest()`, id, method, args, maybeChange); + debug(`serverRequest()`, id, method, args, scope, maybeChange); } - lastRequestMethod = method; if (maybeChange !== null) { const changedScripts = maybeChange[0]; const newProjectVersion = maybeChange[1]; - const configChanged = maybeChange[2]; - - if (configChanged) { - tsConfigCache = null; + const newConfigsByScope = maybeChange[2]; + if (newConfigsByScope) { isNodeSourceFileCache.clear(); + /** @type { typeof languageServiceEntries.byScope } */ + const newByScope = new Map(); + for (const [scope, config] of newConfigsByScope) { + lastRequestScope = scope; + const oldEntry = languageServiceEntries.byScope.get(scope); + const ls = oldEntry + ? oldEntry.ls + : ts.createLanguageService(host, documentRegistry); + const compilerOptions = lspTsConfigToCompilerOptions(config); + newByScope.set(scope, { ls, compilerOptions }); + languageServiceEntries.byScope.delete(scope); + } + for (const oldEntry of languageServiceEntries.byScope.values()) { + oldEntry.ls.dispose(); + } + languageServiceEntries.byScope = newByScope; } projectVersionCache = newProjectVersion; @@ -1172,10 +1240,15 @@ delete Object.prototype.__proto__; sourceTextCache.delete(script); } - if (configChanged || opened || closed) { - scriptFileNamesCache = undefined; + if (newConfigsByScope || opened || closed) { + scriptNamesCache = null; } } + + lastRequestMethod = method; + lastRequestScope = scope; + const ls = (scope ? languageServiceEntries.byScope.get(scope)?.ls : null) ?? + languageServiceEntries.unscoped.ls; switch (method) { case "$getSupportedCodeFixes": { return respond( @@ -1200,9 +1273,9 @@ delete Object.prototype.__proto__; const diagnosticMap = {}; for (const specifier of args[0]) { diagnosticMap[specifier] = fromTypeScriptDiagnostics([ - ...languageService.getSemanticDiagnostics(specifier), - ...languageService.getSuggestionDiagnostics(specifier), - ...languageService.getSyntacticDiagnostics(specifier), + ...ls.getSemanticDiagnostics(specifier), + ...ls.getSuggestionDiagnostics(specifier), + ...ls.getSyntacticDiagnostics(specifier), ].filter(({ code }) => !IGNORED_DIAGNOSTICS.includes(code))); } return respond(id, diagnosticMap); @@ -1210,25 +1283,31 @@ delete Object.prototype.__proto__; if ( !isCancellationError(e) ) { - respond(id, {}, e); - throw e; + return respond( + id, + {}, + formatErrorWithArgs(e, [id, method, args, scope, maybeChange]), + ); } return respond(id, {}); } } default: - if (typeof languageService[method] === "function") { + if (typeof ls[method] === "function") { // The `getCompletionEntryDetails()` method returns null if the // `source` is `null` for whatever reason. It must be `undefined`. if (method == "getCompletionEntryDetails") { args[4] ??= undefined; } try { - return respond(id, languageService[method](...args)); + return respond(id, ls[method](...args)); } catch (e) { if (!isCancellationError(e)) { - respond(id, null, e); - throw e; + return respond( + id, + null, + formatErrorWithArgs(e, [id, method, args, scope, maybeChange]), + ); } return respond(id); } diff --git a/tests/integration/lsp_tests.rs b/tests/integration/lsp_tests.rs index c8089a503..bfb15ecb9 100644 --- a/tests/integration/lsp_tests.rs +++ b/tests/integration/lsp_tests.rs @@ -9603,7 +9603,6 @@ fn lsp_performance() { "tsc.op.op_is_node_file", "tsc.op.op_load", "tsc.op.op_script_names", - "tsc.op.op_ts_config", "tsc.request.$getAssets", "tsc.request.$getSupportedCodeFixes", "tsc.request.getQuickInfoAtPosition", @@ -12432,6 +12431,500 @@ fn lsp_deno_json_scopes_vendor_dirs() { } #[test] +fn lsp_deno_json_scopes_ts_config() { + let context = TestContextBuilder::new().use_temp_cwd().build(); + let temp_dir = context.temp_dir(); + temp_dir.create_dir_all("project1"); + temp_dir.create_dir_all("project2"); + temp_dir.write("project1/deno.json", json!({}).to_string()); + temp_dir.write( + "project2/deno.json", + json!({ + "compilerOptions": { + "lib": ["deno.worker"], + }, + }) + .to_string(), + ); + let mut client = context.new_lsp_command().build(); + client.initialize_default(); + client.did_open(json!({ + "textDocument": { + "uri": temp_dir.uri().join("project1/file.ts").unwrap(), + "languageId": "typescript", + "version": 1, + "text": "Window;\nWorkerGlobalScope;\n", + }, + })); + let diagnostics = client.did_open(json!({ + "textDocument": { + "uri": temp_dir.uri().join("project2/file.ts").unwrap(), + "languageId": "typescript", + "version": 1, + "text": "Window;\nWorkerGlobalScope;\n", + }, + })); + assert_eq!( + json!(diagnostics.all_messages()), + json!([ + { + "uri": temp_dir.uri().join("project2/file.ts").unwrap(), + "version": 1, + "diagnostics": [ + { + "range": { + "start": { "line": 0, "character": 0 }, + "end": { "line": 0, "character": 6 }, + }, + "severity": 1, + "code": 2304, + "source": "deno-ts", + "message": "Cannot find name 'Window'.", + }, + ], + }, + { + "uri": temp_dir.uri().join("project1/file.ts").unwrap(), + "version": 1, + "diagnostics": [ + { + "range": { + "start": { "line": 1, "character": 0 }, + "end": { "line": 1, "character": 17 }, + }, + "severity": 1, + "code": 2304, + "source": "deno-ts", + "message": "Cannot find name 'WorkerGlobalScope'.", + }, + ], + } + ]), + ); + client.shutdown(); +} + +#[test] +fn lsp_deno_json_scopes_declaration_files() { + let context = TestContextBuilder::new().use_temp_cwd().build(); + let temp_dir = context.temp_dir(); + temp_dir.create_dir_all("project1"); + temp_dir.create_dir_all("project2"); + temp_dir.write("project1/deno.json", json!({}).to_string()); + temp_dir.write("project2/deno.json", json!({}).to_string()); + temp_dir.write("project1/foo.d.ts", "declare type Foo = number;\n"); + temp_dir.write("project2/bar.d.ts", "declare type Bar = number;\n"); + let mut client = context.new_lsp_command().build(); + client.initialize_default(); + client.did_open(json!({ + "textDocument": { + "uri": temp_dir.uri().join("project1/file.ts").unwrap(), + "languageId": "typescript", + "version": 1, + "text": "export const foo: Foo = 1;\nexport const bar: Bar = 1;\n", + }, + })); + let diagnostics = client.did_open(json!({ + "textDocument": { + "uri": temp_dir.uri().join("project2/file.ts").unwrap(), + "languageId": "typescript", + "version": 1, + "text": "export const foo: Foo = 1;\nexport const bar: Bar = 1;\n", + }, + })); + assert_eq!( + json!(diagnostics.all_messages()), + json!([ + { + "uri": temp_dir.uri().join("project2/file.ts").unwrap(), + "version": 1, + "diagnostics": [ + { + "range": { + "start": { "line": 0, "character": 18 }, + "end": { "line": 0, "character": 21 }, + }, + "severity": 1, + "code": 2304, + "source": "deno-ts", + "message": "Cannot find name 'Foo'.", + }, + ], + }, + { + "uri": temp_dir.uri().join("project1/file.ts").unwrap(), + "version": 1, + "diagnostics": [ + { + "range": { + "start": { "line": 1, "character": 18 }, + "end": { "line": 1, "character": 21 }, + }, + "severity": 1, + "code": 2304, + "source": "deno-ts", + "message": "Cannot find name 'Bar'.", + }, + ], + } + ]), + ); + client.shutdown(); +} + +#[test] +fn lsp_deno_json_scopes_find_references() { + let context = TestContextBuilder::new().use_temp_cwd().build(); + let temp_dir = context.temp_dir(); + temp_dir.create_dir_all("project1"); + temp_dir.create_dir_all("project2"); + temp_dir.write("project1/deno.json", json!({}).to_string()); + temp_dir.write("project2/deno.json", json!({}).to_string()); + let file1 = source_file( + temp_dir.path().join("project1/file.ts"), + "export const foo = 1;\n", + ); + let file2 = source_file( + temp_dir.path().join("project2/file.ts"), + "export { foo } from \"../project1/file.ts\";\n", + ); + let mut client = context.new_lsp_command().build(); + client.initialize_default(); + let res = client.write_request( + "textDocument/references", + json!({ + "textDocument": file1.identifier(), + "position": file1.range_of("foo").start, + "context": { + "includeDeclaration": true, + }, + }), + ); + assert_eq!( + res, + json!([ + { + "uri": file1.uri(), + "range": file1.range_of("foo"), + }, + { + "uri": file2.uri(), + "range": file2.range_of("foo"), + }, + ]), + ); + client.shutdown(); +} + +#[test] +fn lsp_deno_json_scopes_file_rename_import_edits() { + let context = TestContextBuilder::new().use_temp_cwd().build(); + let temp_dir = context.temp_dir(); + temp_dir.create_dir_all("project1"); + temp_dir.create_dir_all("project2"); + temp_dir.write("project1/deno.json", json!({}).to_string()); + temp_dir.write("project2/deno.json", json!({}).to_string()); + let file1 = source_file(temp_dir.path().join("project1/file.ts"), ""); + let file2 = source_file( + temp_dir.path().join("project2/file.ts"), + "import \"../project1/file.ts\";\n", + ); + let mut client = context.new_lsp_command().build(); + client.initialize_default(); + let res = client.write_request( + "workspace/willRenameFiles", + json!({ + "files": [ + { + "oldUri": file1.uri(), + "newUri": file1.uri().join("file_renamed.ts").unwrap(), + }, + ], + }), + ); + assert_eq!( + res, + json!({ + "documentChanges": [ + { + "textDocument": { + "uri": file2.uri(), + "version": null, + }, + "edits": [ + { + "range": file2.range_of("../project1/file.ts"), + "newText": "../project1/file_renamed.ts", + }, + ], + }, + ], + }), + ); + client.shutdown(); +} + +#[test] +fn lsp_deno_json_scopes_goto_implementations() { + let context = TestContextBuilder::new().use_temp_cwd().build(); + let temp_dir = context.temp_dir(); + temp_dir.create_dir_all("project1"); + temp_dir.create_dir_all("project2"); + temp_dir.write("project1/deno.json", json!({}).to_string()); + temp_dir.write("project2/deno.json", json!({}).to_string()); + let file1 = source_file( + temp_dir.path().join("project1/file.ts"), + "export interface Foo {}\n", + ); + let file2 = source_file( + temp_dir.path().join("project2/file.ts"), + r#" + import type { Foo } from "../project1/file.ts"; + export class SomeFoo implements Foo {} + "#, + ); + let mut client = context.new_lsp_command().build(); + client.initialize_default(); + let res = client.write_request( + "textDocument/implementation", + json!({ + "textDocument": file1.identifier(), + "position": file1.range_of("Foo").start, + }), + ); + assert_eq!( + res, + json!([ + { + "targetUri": file2.uri(), + "targetRange": file2.range_of("export class SomeFoo implements Foo {}"), + "targetSelectionRange": file2.range_of("SomeFoo"), + }, + ]), + ); + client.shutdown(); +} + +#[test] +fn lsp_deno_json_scopes_call_hierarchy() { + let context = TestContextBuilder::new().use_temp_cwd().build(); + let temp_dir = context.temp_dir(); + temp_dir.create_dir_all("project1"); + temp_dir.create_dir_all("project2"); + temp_dir.create_dir_all("project3"); + temp_dir.write("project1/deno.json", json!({}).to_string()); + temp_dir.write("project2/deno.json", json!({}).to_string()); + temp_dir.write("project3/deno.json", json!({}).to_string()); + let file1 = source_file( + temp_dir.path().join("project1/file.ts"), + r#" + export function foo() {} + "#, + ); + let file2 = source_file( + temp_dir.path().join("project2/file.ts"), + r#" + import { foo } from "../project1/file.ts"; + export function bar() { + foo(); + } + "#, + ); + let file3 = source_file( + temp_dir.path().join("project3/file.ts"), + r#" + import { bar } from "../project2/file.ts"; + bar(); + "#, + ); + let mut client = context.new_lsp_command().build(); + client.initialize_default(); + let res = client.write_request( + "textDocument/prepareCallHierarchy", + json!({ + "textDocument": file2.identifier(), + "position": file2.range_of("bar").start, + }), + ); + assert_eq!( + &res, + &json!([ + { + "name": "bar", + "kind": 12, + "detail": "", + "uri": file2.uri(), + "range": { + "start": { "line": 2, "character": 6 }, + "end": { "line": 4, "character": 7 }, + }, + "selectionRange": file2.range_of("bar"), + }, + ]), + ); + let item = res.as_array().unwrap().first().unwrap(); + let res = client + .write_request("callHierarchy/incomingCalls", json!({ "item": item })); + assert_eq!( + res, + json!([ + { + "from": { + "name": "file.ts", + "kind": 2, + "detail": "project3", + "uri": file3.uri(), + "range": { + "start": { "line": 1, "character": 6 }, + "end": { "line": 3, "character": 4 }, + }, + "selectionRange": { + "start": { "line": 0, "character": 0 }, + "end": { "line": 0, "character": 0 }, + }, + }, + "fromRanges": [ + { + "start": { "line": 2, "character": 6 }, + "end": { "line": 2, "character": 9 }, + }, + ], + }, + ]), + ); + let res = client + .write_request("callHierarchy/outgoingCalls", json!({ "item": item })); + assert_eq!( + res, + json!([ + { + "to": { + "name": "foo", + "kind": 12, + "detail": "", + "uri": file1.uri(), + "range": file1.range_of("export function foo() {}"), + "selectionRange": file1.range_of("foo"), + }, + "fromRanges": [ + { + "start": { "line": 3, "character": 8 }, + "end": { "line": 3, "character": 11 }, + }, + ], + }, + ]), + ); + client.shutdown(); +} + +#[test] +fn lsp_deno_json_scopes_rename_symbol() { + let context = TestContextBuilder::new().use_temp_cwd().build(); + let temp_dir = context.temp_dir(); + temp_dir.create_dir_all("project1"); + temp_dir.create_dir_all("project2"); + temp_dir.write("project1/deno.json", json!({}).to_string()); + temp_dir.write("project2/deno.json", json!({}).to_string()); + let file1 = source_file( + temp_dir.path().join("project1/file.ts"), + "export const foo = 1;\n", + ); + let file2 = source_file( + temp_dir.path().join("project2/file.ts"), + "export { foo } from \"../project1/file.ts\";\n", + ); + let mut client = context.new_lsp_command().build(); + client.initialize_default(); + let res = client.write_request( + "textDocument/rename", + json!({ + "textDocument": file1.identifier(), + "position": file1.range_of("foo").start, + "newName": "bar", + }), + ); + assert_eq!( + res, + json!({ + "documentChanges": [ + { + "textDocument": { + "uri": file1.uri(), + "version": null, + }, + "edits": [ + { + "range": file1.range_of("foo"), + "newText": "bar", + }, + ], + }, + { + "textDocument": { + "uri": file2.uri(), + "version": null, + }, + "edits": [ + { + "range": file2.range_of("foo"), + "newText": "bar", + }, + ], + }, + ], + }), + ); + client.shutdown(); +} + +#[test] +fn lsp_deno_json_scopes_search_symbol() { + let context = TestContextBuilder::new().use_temp_cwd().build(); + let temp_dir = context.temp_dir(); + temp_dir.create_dir_all("project1"); + temp_dir.create_dir_all("project2"); + temp_dir.write("project1/deno.json", json!({}).to_string()); + temp_dir.write("project2/deno.json", json!({}).to_string()); + let file1 = source_file( + temp_dir.path().join("project1/file.ts"), + "export const someSymbol1 = 1;\n", + ); + let file2 = source_file( + temp_dir.path().join("project2/file.ts"), + "export const someSymbol2 = 2;\n", + ); + let mut client = context.new_lsp_command().build(); + client.initialize_default(); + let res = + client.write_request("workspace/symbol", json!({ "query": "someSymbol" })); + assert_eq!( + res, + json!([ + { + "name": "someSymbol1", + "kind": 13, + "location": { + "uri": file1.uri(), + "range": file1.range_of("someSymbol1 = 1"), + }, + "containerName": "", + }, + { + "name": "someSymbol2", + "kind": 13, + "location": { + "uri": file2.uri(), + "range": file2.range_of("someSymbol2 = 2"), + }, + "containerName": "", + }, + ]), + ); + client.shutdown(); +} + +#[test] fn lsp_deno_json_workspace_fmt_config() { let context = TestContextBuilder::new().use_temp_cwd().build(); let temp_dir = context.temp_dir(); |