summaryrefslogtreecommitdiff
path: root/cli/tsc/99_main_compiler.js
diff options
context:
space:
mode:
authorKitson Kelly <me@kitsonkelly.com>2020-12-07 21:46:39 +1100
committerGitHub <noreply@github.com>2020-12-07 21:46:39 +1100
commit301d3e4b6849d24154ac2d65c00a9b30223d000e (patch)
treeab3bc074493e6c9be8d1875233bc141bdc0da3b4 /cli/tsc/99_main_compiler.js
parentc8e9b2654ec0d54c77bb3f49fa31c3986203d517 (diff)
feat: add mvp language server (#8515)
Resolves #8400
Diffstat (limited to 'cli/tsc/99_main_compiler.js')
-rw-r--r--cli/tsc/99_main_compiler.js254
1 files changed, 241 insertions, 13 deletions
diff --git a/cli/tsc/99_main_compiler.js b/cli/tsc/99_main_compiler.js
index bb8458c93..f379d6bae 100644
--- a/cli/tsc/99_main_compiler.js
+++ b/cli/tsc/99_main_compiler.js
@@ -1,5 +1,7 @@
// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license.
+// @ts-check
+/// <reference path="./compiler.d.ts" />
// deno-lint-ignore-file no-undef
// This module is the entry point for "compiler" isolate, ie. the one
@@ -11,6 +13,7 @@
delete Object.prototype.__proto__;
((window) => {
+ /** @type {DenoCore} */
const core = window.Deno.core;
let logDebug = false;
@@ -25,7 +28,9 @@ delete Object.prototype.__proto__;
function debug(...args) {
if (logDebug) {
- const stringifiedArgs = args.map((arg) => JSON.stringify(arg)).join(" ");
+ const stringifiedArgs = args.map((arg) =>
+ typeof arg === "string" ? arg : JSON.stringify(arg)
+ ).join(" ");
core.print(`DEBUG ${logSource} - ${stringifiedArgs}\n`);
}
}
@@ -86,6 +91,7 @@ delete Object.prototype.__proto__;
/** @param {ts.Diagnostic[]} diagnostics */
function fromTypeScriptDiagnostic(diagnostics) {
return diagnostics.map(({ relatedInformation: ri, source, ...diag }) => {
+ /** @type {any} */
const value = fromRelatedInformation(diag);
value.relatedInformation = ri
? ri.map(fromRelatedInformation)
@@ -106,7 +112,7 @@ delete Object.prototype.__proto__;
* Deno, as they provide misleading or incorrect information. */
const IGNORED_DIAGNOSTICS = [
// TS1208: All files must be modules when the '--isolatedModules' flag is
- // provided. We can ignore because we guarantuee that all files are
+ // provided. We can ignore because we guarantee that all files are
// modules.
1208,
// TS1375: 'await' expressions are only allowed at the top level of a file
@@ -148,10 +154,72 @@ delete Object.prototype.__proto__;
target: ts.ScriptTarget.ESNext,
};
+ class ScriptSnapshot {
+ /** @type {string} */
+ specifier;
+ /** @type {string} */
+ version;
+ /**
+ * @param {string} specifier
+ * @param {string} version
+ */
+ constructor(specifier, version) {
+ this.specifier = specifier;
+ this.version = version;
+ }
+ /**
+ * @param {number} start
+ * @param {number} end
+ * @returns {string}
+ */
+ getText(start, end) {
+ const { specifier, version } = this;
+ debug(
+ `snapshot.getText(${start}, ${end}) specifier: ${specifier} version: ${version}`,
+ );
+ return core.jsonOpSync("op_get_text", { specifier, version, start, end });
+ }
+ /**
+ * @returns {number}
+ */
+ getLength() {
+ const { specifier, version } = this;
+ debug(`snapshot.getLength() specifier: ${specifier} version: ${version}`);
+ return core.jsonOpSync("op_get_length", { specifier, version });
+ }
+ /**
+ * @param {ScriptSnapshot} oldSnapshot
+ * @returns {ts.TextChangeRange | undefined}
+ */
+ getChangeRange(oldSnapshot) {
+ const { specifier, version } = this;
+ const { version: oldVersion } = oldSnapshot;
+ const oldLength = oldSnapshot.getLength();
+ debug(
+ `snapshot.getLength() specifier: ${specifier} oldVersion: ${oldVersion} version: ${version}`,
+ );
+ return core.jsonOpSync(
+ "op_get_change_range",
+ { specifier, oldLength, oldVersion, version },
+ );
+ }
+ dispose() {
+ const { specifier, version } = this;
+ debug(`snapshot.dispose() specifier: ${specifier} version: ${version}`);
+ core.jsonOpSync("op_dispose", { specifier, version });
+ }
+ }
+
+ /** @type {ts.CompilerOptions} */
+ let compilationSettings = {};
+
+ /** @type {ts.LanguageService} */
+ let languageService;
+
/** An object literal of the incremental compiler host, which provides the
* specific "bindings" to the Deno environment that tsc needs to work.
*
- * @type {ts.CompilerHost} */
+ * @type {ts.CompilerHost & ts.LanguageServiceHost} */
const host = {
fileExists(fileName) {
debug(`host.fileExists("${fileName}")`);
@@ -231,21 +299,73 @@ delete Object.prototype.__proto__;
debug(`host.resolveModuleNames()`);
debug(` base: ${base}`);
debug(` specifiers: ${specifiers.join(", ")}`);
- /** @type {Array<[string, ts.Extension]>} */
+ /** @type {Array<[string, ts.Extension] | undefined>} */
const resolved = core.jsonOpSync("op_resolve", {
specifiers,
base,
});
- const r = resolved.map(([resolvedFileName, extension]) => ({
- resolvedFileName,
- extension,
- isExternalLibraryImport: false,
- }));
- return r;
+ if (resolved) {
+ const result = resolved.map((item) => {
+ if (item) {
+ const [resolvedFileName, extension] = item;
+ return {
+ resolvedFileName,
+ extension,
+ isExternalLibraryImport: false,
+ };
+ }
+ return undefined;
+ });
+ result.length = specifiers.length;
+ return result;
+ } else {
+ return new Array(specifiers.length);
+ }
},
createHash(data) {
return core.jsonOpSync("op_create_hash", { data }).hash;
},
+
+ // LanguageServiceHost
+ getCompilationSettings() {
+ debug("host.getCompilationSettings()");
+ return compilationSettings;
+ },
+ getScriptFileNames() {
+ debug("host.getScriptFileNames()");
+ return core.jsonOpSync("op_script_names", undefined);
+ },
+ getScriptVersion(specifier) {
+ debug(`host.getScriptVersion("${specifier}")`);
+ const sourceFile = sourceFileCache.get(specifier);
+ if (sourceFile) {
+ return sourceFile.version ?? "1";
+ }
+ return core.jsonOpSync("op_script_version", { specifier });
+ },
+ getScriptSnapshot(specifier) {
+ debug(`host.getScriptSnapshot("${specifier}")`);
+ const sourceFile = sourceFileCache.get(specifier);
+ if (sourceFile) {
+ return {
+ getText(start, end) {
+ return sourceFile.text.substring(start, end);
+ },
+ getLength() {
+ return sourceFile.text.length;
+ },
+ getChangeRange() {
+ return undefined;
+ },
+ };
+ }
+ /** @type {string | undefined} */
+ const version = core.jsonOpSync("op_script_version", { specifier });
+ if (version != null) {
+ return new ScriptSnapshot(specifier, version);
+ }
+ return undefined;
+ },
};
/** @type {Array<[string, number]>} */
@@ -254,10 +374,13 @@ delete Object.prototype.__proto__;
function performanceStart() {
stats.length = 0;
- statsStart = new Date();
+ statsStart = Date.now();
ts.performance.enable();
}
+ /**
+ * @param {{ program: ts.Program | ts.EmitAndSemanticDiagnosticsBuilderProgram, fileCount?: number }} options
+ */
function performanceProgram({ program, fileCount }) {
if (program) {
if ("getProgram" in program) {
@@ -286,7 +409,7 @@ delete Object.prototype.__proto__;
}
function performanceEnd() {
- const duration = new Date() - statsStart;
+ const duration = Date.now() - statsStart;
stats.push(["Compile time", duration]);
return stats;
}
@@ -308,7 +431,7 @@ delete Object.prototype.__proto__;
debug(config);
const { options, errors: configFileParsingDiagnostics } = ts
- .convertCompilerOptionsFromJson(config, "", "tsconfig.json");
+ .convertCompilerOptionsFromJson(config, "");
// The `allowNonTsExtensions` is a "hidden" compiler option used in VSCode
// which is not allowed to be passed in JSON, we need it to allow special
// URLs which Deno supports. So we need to either ignore the diagnostic, or
@@ -340,6 +463,106 @@ delete Object.prototype.__proto__;
debug("<<< exec stop");
}
+ /**
+ * @param {number} id
+ * @param {any} data
+ */
+ function respond(id, data = null) {
+ core.jsonOpSync("op_respond", { id, data });
+ }
+
+ /**
+ * @param {LanguageServerRequest} request
+ */
+ function serverRequest({ id, ...request }) {
+ debug(`serverRequest()`, { id, ...request });
+ switch (request.method) {
+ case "configure": {
+ const { options, errors } = ts
+ .convertCompilerOptionsFromJson(request.compilerOptions, "");
+ Object.assign(options, { allowNonTsExtensions: true });
+ if (errors.length) {
+ debug(ts.formatDiagnostics(errors, host));
+ }
+ compilationSettings = options;
+ return respond(id, true);
+ }
+ case "getSemanticDiagnostics": {
+ const diagnostics = languageService.getSemanticDiagnostics(
+ request.specifier,
+ ).filter(({ code }) => !IGNORED_DIAGNOSTICS.includes(code));
+ return respond(id, fromTypeScriptDiagnostic(diagnostics));
+ }
+ case "getSuggestionDiagnostics": {
+ const diagnostics = languageService.getSuggestionDiagnostics(
+ request.specifier,
+ ).filter(({ code }) => !IGNORED_DIAGNOSTICS.includes(code));
+ return respond(id, fromTypeScriptDiagnostic(diagnostics));
+ }
+ case "getSyntacticDiagnostics": {
+ const diagnostics = languageService.getSyntacticDiagnostics(
+ request.specifier,
+ ).filter(({ code }) => !IGNORED_DIAGNOSTICS.includes(code));
+ return respond(id, fromTypeScriptDiagnostic(diagnostics));
+ }
+ case "getQuickInfo": {
+ return respond(
+ id,
+ languageService.getQuickInfoAtPosition(
+ request.specifier,
+ request.position,
+ ),
+ );
+ }
+ case "getDocumentHighlights": {
+ return respond(
+ id,
+ languageService.getDocumentHighlights(
+ request.specifier,
+ request.position,
+ request.filesToSearch,
+ ),
+ );
+ }
+ case "getReferences": {
+ return respond(
+ id,
+ languageService.getReferencesAtPosition(
+ request.specifier,
+ request.position,
+ ),
+ );
+ }
+ case "getDefinition": {
+ return respond(
+ id,
+ languageService.getDefinitionAndBoundSpan(
+ request.specifier,
+ request.position,
+ ),
+ );
+ }
+ default:
+ throw new TypeError(
+ // @ts-ignore exhausted case statement sets type to never
+ `Invalid request method for request: "${request.method}" (${id})`,
+ );
+ }
+ }
+
+ /** @param {{ debug: boolean; }} init */
+ function serverInit({ debug: debugFlag }) {
+ if (hasStarted) {
+ throw new Error("The language server has already been initialized.");
+ }
+ hasStarted = true;
+ languageService = ts.createLanguageService(host);
+ core.ops();
+ core.registerErrorClass("Error", Error);
+ setLogDebug(debugFlag, "TSLS");
+ debug("serverInit()");
+ }
+
let hasStarted = false;
/** Startup the runtime environment, setting various flags.
@@ -391,4 +614,9 @@ delete Object.prototype.__proto__;
// checking TypeScript.
globalThis.startup = startup;
globalThis.exec = exec;
+
+ // exposes the functions that are called when the compiler is used as a
+ // language service.
+ globalThis.serverInit = serverInit;
+ globalThis.serverRequest = serverRequest;
})(this);