Skip to content

Commit

Permalink
feat : Support for multiline statements
Browse files Browse the repository at this point in the history
  • Loading branch information
antsmartian authored and devsnek committed Aug 21, 2018
1 parent 106fc9c commit 2ac4bcd
Show file tree
Hide file tree
Showing 3 changed files with 117 additions and 13 deletions.
20 changes: 18 additions & 2 deletions src/io.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,11 @@ class IO {
this.stdin = stdin;
this.stdout = stdout;

this.buffer = '';
this.buffer = ''; // used for capturing the current statement
this.cursor = 0;
this.prefix = '';
this.suffix = '';
this.multilineBuffer = ''; // for buffering the multiline statements

this._paused = false;
this.transformBuffer = transformBuffer;
Expand Down Expand Up @@ -134,7 +135,19 @@ class IO {
this.buffer = '';
this.cursor = 0;
this.history.unshift(b);
this.stdout.write(`${await onLine(b)}\n`);
this.multilineBuffer += b;
// always buffer the line so that when we encounter multi-line
// statements we can execute them as needed.
const code = this.multilineBuffer;
const result = await onLine(code);
// online returns a Symbol when it sees a multi-line statement
if (IO.kNeedsAnotherLine !== result) {
this.stdout.write(`${result}\n`);
this.multilineBuffer = '';
this.setPrefix('> ');
} else {
this.setPrefix('... ');
}
this.unpause();
} else {
this.stdout.write('\n');
Expand Down Expand Up @@ -298,4 +311,7 @@ class IO {
}
}

// Symbol to notify that IO needs an another line
IO.kNeedsAnotherLine = Symbol('IO.kNeedsAnotherLine');

module.exports = IO;
79 changes: 79 additions & 0 deletions src/recoverable.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
'use strict';

const acorn = require('acorn');
const { tokTypes: tt } = acorn;

// If the error is that we've unexpectedly ended the input,
// then let the user try to recover by adding more input.
// Note: `e` (the original exception) is not used by the current implemention,
// but may be needed in the future.
function isRecoverableError(e, code) {
let recoverable = false;

// Determine if the point of the any error raised is at the end of the input.
// There are two cases to consider:
//
// 1. Any error raised after we have encountered the 'eof' token.
// This prevents us from declaring partial tokens (like '2e') as
// recoverable.
//
// 2. Three cases where tokens can legally span lines. This is
// template, comment, and strings with a backslash at the end of
// the line, indicating a continuation. Note that we need to look
// for the specific errors of 'unterminated' kind (not, for example,
// a syntax error in a ${} expression in a template), and the only
// way to do that currently is to look at the message. Should Acorn
// change these messages in the future, this will lead to a test
// failure, indicating that this code needs to be updated.
//
acorn.plugins.replRecoverable = (parser) => {
parser.extend('nextToken', (nextToken) => {
return function() {
Reflect.apply(nextToken, this, []);

if (this.type === tt.eof) recoverable = true;
};
});

parser.extend('raise', (raise) => {
return function(pos, message) {
switch (message) {
case 'Unterminated template':
case 'Unterminated comment':
case 'Unexpected end of input':
recoverable = true;
break;

case 'Unterminated string constant':
const token = this.input.slice(this.lastTokStart, this.pos);
// see https://www.ecma-international.org/ecma-262/#sec-line-terminators
recoverable = /\\(?:\r\n?|\n|\u2028|\u2029)$/.test(token);
}

Reflect.apply(raise, this, [pos, message]);
};
});
};

// For similar reasons as `defaultEval`, wrap expressions starting with a
// curly brace with parenthesis. Note: only the open parenthesis is added
// here as the point is to test for potentially valid but incomplete
// expressions.
if (/^\s*\{/.test(code) && isRecoverableError(e, `(${code}`)) return true;

// Try to parse the code with acorn. If the parse fails, ignore the acorn
// error and return the recoverable status.
try {
acorn.parse(code, { plugins: { replRecoverable: true } });

// Odd case: the underlying JS engine (V8, Chakra) rejected this input
// but Acorn detected no issue. Presume that additional text won't
// address this issue.
return false;
} catch (e) {
return recoverable;
}
}

module.exports = isRecoverableError

31 changes: 20 additions & 11 deletions src/repl.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ const highlight = require('./highlight');
const { processTopLevelAwait } = require('./await');
const { Runtime, mainContextIdPromise } = require('./inspector');
const { strEscape, isIdentifier } = require('./util');
const isRecoverableError = require('./recoverable');

// TODO(devsnek): make more robust
Error.prepareStackTrace = (err, frames) => {
Expand Down Expand Up @@ -105,21 +106,29 @@ Prototype REPL - https://github.com/nodejs/repl`,
const evaluateResult = await this.eval(line, awaited);

if (evaluateResult.exceptionDetails) {
// lets try for recovering
const result = isRecoverableError(evaluateResult.exceptionDetails.exception, line);
if (result) {
return IO.kNeedsAnotherLine;
} else {
// we tried our best - throw error
await this.callFunctionOn(
(err) => {
global.REPL.lastError = err;
},
evaluateResult.exceptionDetails.exception,
);
return inspect(global.REPL.lastError);
}
} else {
await this.callFunctionOn(
(err) => {
global.REPL.lastError = err;
(result) => {
global.REPL.last = result;
},
evaluateResult.exceptionDetails.exception,
evaluateResult.result,
);
return inspect(global.REPL.lastError);
return inspect(global.REPL.last);
}
await this.callFunctionOn(
(result) => {
global.REPL.last = result;
},
evaluateResult.result,
);
return inspect(global.REPL.last);
}

async onAutocomplete(buffer) {
Expand Down

0 comments on commit 2ac4bcd

Please sign in to comment.