repl: Add editor mode support · nodejs/node@b779eb4

@@ -223,6 +223,7 @@ function REPLServer(prompt,

223223

self.underscoreAssigned = false;

224224

self.last = undefined;

225225

self.breakEvalOnSigint = !!breakEvalOnSigint;

226+

self.editorMode = false;

226227227228

self._inTemplateLiteral = false;

228229

@@ -394,7 +395,12 @@ function REPLServer(prompt,

394395

// Figure out which "complete" function to use.

395396

self.completer = (typeof options.completer === 'function')

396397

? options.completer

397-

: complete;

398+

: completer;

399+400+

function completer(text, cb) {

401+

complete.call(self, text, self.editorMode

402+

? self.completeOnEditorMode(cb) : cb);

403+

}

398404399405

Interface.call(this, {

400406

input: self.inputStream,

@@ -428,9 +434,11 @@ function REPLServer(prompt,

428434

});

429435430436

var sawSIGINT = false;

437+

var sawCtrlD = false;

431438

self.on('SIGINT', function() {

432439

var empty = self.line.length === 0;

433440

self.clearLine();

441+

self.turnOffEditorMode();

434442435443

if (!(self.bufferedCommand && self.bufferedCommand.length > 0) && empty) {

436444

if (sawSIGINT) {

@@ -454,6 +462,11 @@ function REPLServer(prompt,

454462

debug('line %j', cmd);

455463

sawSIGINT = false;

456464465+

if (self.editorMode) {

466+

self.bufferedCommand += cmd + '\n';

467+

return;

468+

}

469+457470

// leading whitespaces in template literals should not be trimmed.

458471

if (self._inTemplateLiteral) {

459472

self._inTemplateLiteral = false;

@@ -499,7 +512,8 @@ function REPLServer(prompt,

499512500513

// If error was SyntaxError and not JSON.parse error

501514

if (e) {

502-

if (e instanceof Recoverable && !self.lineParser.shouldFail) {

515+

if (e instanceof Recoverable && !self.lineParser.shouldFail &&

516+

!sawCtrlD) {

503517

// Start buffering data like that:

504518

// {

505519

// ... x: 1

@@ -515,6 +529,7 @@ function REPLServer(prompt,

515529

// Clear buffer if no SyntaxErrors

516530

self.lineParser.reset();

517531

self.bufferedCommand = '';

532+

sawCtrlD = false;

518533519534

// If we got any output - print it (if no error)

520535

if (!e &&

@@ -555,9 +570,55 @@ function REPLServer(prompt,

555570

});

556571557572

self.on('SIGCONT', function() {

558-

self.displayPrompt(true);

573+

if (self.editorMode) {

574+

self.outputStream.write(`${self._initialPrompt}.editor\n`);

575+

self.outputStream.write(

576+

'// Entering editor mode (^D to finish, ^C to cancel)\n');

577+

self.outputStream.write(`${self.bufferedCommand}\n`);

578+

self.prompt(true);

579+

} else {

580+

self.displayPrompt(true);

581+

}

559582

});

560583584+

// Wrap readline tty to enable editor mode

585+

const ttyWrite = self._ttyWrite.bind(self);

586+

self._ttyWrite = (d, key) => {

587+

if (!self.editorMode || !self.terminal) {

588+

ttyWrite(d, key);

589+

return;

590+

}

591+592+

// editor mode

593+

if (key.ctrl && !key.shift) {

594+

switch (key.name) {

595+

case 'd': // End editor mode

596+

self.turnOffEditorMode();

597+

sawCtrlD = true;

598+

ttyWrite(d, { name: 'return' });

599+

break;

600+

case 'n': // Override next history item

601+

case 'p': // Override previous history item

602+

break;

603+

default:

604+

ttyWrite(d, key);

605+

}

606+

} else {

607+

switch (key.name) {

608+

case 'up': // Override previous history item

609+

case 'down': // Override next history item

610+

break;

611+

case 'tab':

612+

// prevent double tab behavior

613+

self._previousKey = null;

614+

ttyWrite(d, key);

615+

break;

616+

default:

617+

ttyWrite(d, key);

618+

}

619+

}

620+

};

621+561622

self.displayPrompt();

562623

}

563624

inherits(REPLServer, Interface);

@@ -680,6 +741,12 @@ REPLServer.prototype.setPrompt = function setPrompt(prompt) {

680741

REPLServer.super_.prototype.setPrompt.call(this, prompt);

681742

};

682743744+

REPLServer.prototype.turnOffEditorMode = function() {

745+

this.editorMode = false;

746+

this.setPrompt(this._initialPrompt);

747+

};

748+749+683750

// A stream to push an array into a REPL

684751

// used in REPLServer.complete

685752

function ArrayStream() {

@@ -987,6 +1054,39 @@ function complete(line, callback) {

9871054

}

9881055

}

98910561057+

function longestCommonPrefix(arr = []) {

1058+

const cnt = arr.length;

1059+

if (cnt === 0) return '';

1060+

if (cnt === 1) return arr[0];

1061+1062+

const first = arr[0];

1063+

// complexity: O(m * n)

1064+

for (let m = 0; m < first.length; m++) {

1065+

const c = first[m];

1066+

for (let n = 1; n < cnt; n++) {

1067+

const entry = arr[n];

1068+

if (m >= entry.length || c !== entry[m]) {

1069+

return first.substring(0, m);

1070+

}

1071+

}

1072+

}

1073+

return first;

1074+

}

1075+1076+

REPLServer.prototype.completeOnEditorMode = (callback) => (err, results) => {

1077+

if (err) return callback(err);

1078+1079+

const [completions, completeOn = ''] = results;

1080+

const prefixLength = completeOn.length;

1081+1082+

if (prefixLength === 0) return callback(null, [[], completeOn]);

1083+1084+

const isNotEmpty = (v) => v.length > 0;

1085+

const trimCompleteOnPrefix = (v) => v.substring(prefixLength);

1086+

const data = completions.filter(isNotEmpty).map(trimCompleteOnPrefix);

1087+1088+

callback(null, [[`${completeOn}${longestCommonPrefix(data)}`], completeOn]);

1089+

};

99010909911091

/**

9921092

* Used to parse and execute the Node REPL commands.

@@ -1189,6 +1289,17 @@ function defineDefaultCommands(repl) {

11891289

this.displayPrompt();

11901290

}

11911291

});

1292+1293+

repl.defineCommand('editor', {

1294+

help: 'Entering editor mode (^D to finish, ^C to cancel)',

1295+

action() {

1296+

if (!this.terminal) return;

1297+

this.editorMode = true;

1298+

REPLServer.super_.prototype.setPrompt.call(this, '');

1299+

this.outputStream.write(

1300+

'// Entering editor mode (^D to finish, ^C to cancel)\n');

1301+

}

1302+

});

11921303

}

1193130411941305

function regexpEscape(s) {