summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorNayeem Rahman <nayeemrmn99@gmail.com>2024-06-26 23:47:01 +0100
committerGitHub <noreply@github.com>2024-06-26 23:47:01 +0100
commit67dcd6db518446574d3a1e33f4ce536fcdc4fd25 (patch)
tree23d6aa1d867b1efded9c0e638c89513ca492a44f
parent2a2ff96be13047cb50612fde0f12e5f6df374ad3 (diff)
feat(lsp): ts language service scopes (#24345)
-rw-r--r--cli/lsp/code_lens.rs15
-rw-r--r--cli/lsp/config.rs9
-rw-r--r--cli/lsp/documents.rs49
-rw-r--r--cli/lsp/language_server.rs114
-rw-r--r--cli/lsp/resolver.rs24
-rw-r--r--cli/lsp/tsc.rs715
-rw-r--r--cli/tsc/99_main_compiler.js201
-rw-r--r--tests/integration/lsp_tests.rs495
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", &params);
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(&params.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,
&params.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();