summaryrefslogtreecommitdiff
path: root/cli/tsc/99_main_compiler.js
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 /cli/tsc/99_main_compiler.js
parent2a2ff96be13047cb50612fde0f12e5f6df374ad3 (diff)
feat(lsp): ts language service scopes (#24345)
Diffstat (limited to 'cli/tsc/99_main_compiler.js')
-rw-r--r--cli/tsc/99_main_compiler.js201
1 files changed, 140 insertions, 61 deletions
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);
}