1
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2024-12-25 16:49:18 -05:00

feat(lsp): ts language service scopes (#24345)

This commit is contained in:
Nayeem Rahman 2024-06-26 23:47:01 +01:00 committed by GitHub
parent 2a2ff96be1
commit 67dcd6db51
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 1265 additions and 357 deletions

View file

@ -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()

View file

@ -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());
}
}

View file

@ -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;
}

View file

@ -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| {

View file

@ -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()
}

File diff suppressed because it is too large Load diff

View file

@ -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);
}

View file

@ -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",
@ -12431,6 +12430,500 @@ fn lsp_deno_json_scopes_vendor_dirs() {
client.shutdown();
}
#[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();