summaryrefslogtreecommitdiff
path: root/cli/js/repl.ts
diff options
context:
space:
mode:
Diffstat (limited to 'cli/js/repl.ts')
-rw-r--r--cli/js/repl.ts197
1 files changed, 197 insertions, 0 deletions
diff --git a/cli/js/repl.ts b/cli/js/repl.ts
new file mode 100644
index 000000000..966e809e8
--- /dev/null
+++ b/cli/js/repl.ts
@@ -0,0 +1,197 @@
+// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
+import { close } from "./files.ts";
+import { exit } from "./os.ts";
+import { window } from "./window.ts";
+import { core } from "./core.ts";
+import { formatError } from "./format_error.ts";
+import { stringifyArgs } from "./console.ts";
+import * as dispatch from "./dispatch.ts";
+import { sendSync, sendAsync } from "./dispatch_json.ts";
+
+const { console } = window;
+
+/**
+ * REPL logging.
+ * In favor of console.log to avoid unwanted indentation
+ */
+function replLog(...args: unknown[]): void {
+ core.print(stringifyArgs(args) + "\n");
+}
+
+/**
+ * REPL logging for errors.
+ * In favor of console.error to avoid unwanted indentation
+ */
+function replError(...args: unknown[]): void {
+ core.print(stringifyArgs(args) + "\n", true);
+}
+
+const helpMsg = [
+ "_ Get last evaluation result",
+ "_error Get last thrown error",
+ "exit Exit the REPL",
+ "help Print this help message"
+].join("\n");
+
+const replCommands = {
+ exit: {
+ get(): void {
+ exit(0);
+ }
+ },
+ help: {
+ get(): string {
+ return helpMsg;
+ }
+ }
+};
+
+function startRepl(historyFile: string): number {
+ return sendSync(dispatch.OP_REPL_START, { historyFile });
+}
+
+// @internal
+export async function readline(rid: number, prompt: string): Promise<string> {
+ return sendAsync(dispatch.OP_REPL_READLINE, { rid, prompt });
+}
+
+// Error messages that allow users to continue input
+// instead of throwing an error to REPL
+// ref: https://github.com/v8/v8/blob/master/src/message-template.h
+// TODO(kevinkassimo): this list might not be comprehensive
+const recoverableErrorMessages = [
+ "Unexpected end of input", // { or [ or (
+ "Missing initializer in const declaration", // const a
+ "Missing catch or finally after try", // try {}
+ "missing ) after argument list", // console.log(1
+ "Unterminated template literal" // `template
+ // TODO(kevinkassimo): need a parser to handling errors such as:
+ // "Missing } in template expression" // `${ or `${ a 123 }`
+];
+
+function isRecoverableError(e: Error): boolean {
+ return recoverableErrorMessages.includes(e.message);
+}
+
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+type Value = any;
+
+let lastEvalResult: Value = undefined;
+let lastThrownError: Value = undefined;
+
+// Evaluate code.
+// Returns true if code is consumed (no error/irrecoverable error).
+// Returns false if error is recoverable
+function evaluate(code: string): boolean {
+ const [result, errInfo] = core.evalContext(code);
+ if (!errInfo) {
+ lastEvalResult = result;
+ replLog(result);
+ } else if (errInfo.isCompileError && isRecoverableError(errInfo.thrown)) {
+ // Recoverable compiler error
+ return false; // don't consume code.
+ } else {
+ lastThrownError = errInfo.thrown;
+ if (errInfo.isNativeError) {
+ const formattedError = formatError(
+ core.errorToJSON(errInfo.thrown as Error)
+ );
+ replError(formattedError);
+ } else {
+ replError("Thrown:", errInfo.thrown);
+ }
+ }
+ return true;
+}
+
+// @internal
+export async function replLoop(): Promise<void> {
+ Object.defineProperties(window, replCommands);
+
+ const historyFile = "deno_history.txt";
+ const rid = startRepl(historyFile);
+
+ const quitRepl = (exitCode: number): void => {
+ // Special handling in case user calls deno.close(3).
+ try {
+ close(rid); // close signals Drop on REPL and saves history.
+ } catch {}
+ exit(exitCode);
+ };
+
+ // Configure window._ to give the last evaluation result.
+ Object.defineProperty(window, "_", {
+ configurable: true,
+ get: (): Value => lastEvalResult,
+ set: (value: Value): Value => {
+ Object.defineProperty(window, "_", {
+ value: value,
+ writable: true,
+ enumerable: true,
+ configurable: true
+ });
+ console.log("Last evaluation result is no longer saved to _.");
+ }
+ });
+
+ // Configure window._error to give the last thrown error.
+ Object.defineProperty(window, "_error", {
+ configurable: true,
+ get: (): Value => lastThrownError,
+ set: (value: Value): Value => {
+ Object.defineProperty(window, "_error", {
+ value: value,
+ writable: true,
+ enumerable: true,
+ configurable: true
+ });
+ console.log("Last thrown error is no longer saved to _error.");
+ }
+ });
+
+ while (true) {
+ let code = "";
+ // Top level read
+ try {
+ code = await readline(rid, "> ");
+ if (code.trim() === "") {
+ continue;
+ }
+ } catch (err) {
+ if (err.message === "EOF") {
+ quitRepl(0);
+ } else {
+ // If interrupted, don't print error.
+ if (err.message !== "Interrupted") {
+ // e.g. this happens when we have deno.close(3).
+ // We want to display the problem.
+ const formattedError = formatError(core.errorToJSON(err));
+ replError(formattedError);
+ }
+ // Quit REPL anyways.
+ quitRepl(1);
+ }
+ }
+ // Start continued read
+ while (!evaluate(code)) {
+ code += "\n";
+ try {
+ code += await readline(rid, " ");
+ } catch (err) {
+ // If interrupted on continued read,
+ // abort this read instead of quitting.
+ if (err.message === "Interrupted") {
+ break;
+ } else if (err.message === "EOF") {
+ quitRepl(0);
+ } else {
+ // e.g. this happens when we have deno.close(3).
+ // We want to display the problem.
+ const formattedError = formatError(core.errorToJSON(err));
+ replError(formattedError);
+ quitRepl(1);
+ }
+ }
+ }
+ }
+}