diff --git a/lib/internal/readline/interface.js b/lib/internal/readline/interface.js index 45524612aea37b..75843fe96251ec 100644 --- a/lib/internal/readline/interface.js +++ b/lib/internal/readline/interface.js @@ -20,6 +20,7 @@ const { MathMaxApply, NumberIsFinite, ObjectSetPrototypeOf, + RegExp, RegExpPrototypeExec, StringPrototypeCodePointAt, StringPrototypeEndsWith, @@ -72,7 +73,7 @@ const kHistorySize = 30; const kMaxUndoRedoStackSize = 2048; const kMincrlfDelay = 100; // \r\n, \n, or \r followed by something other than \n -const lineEnding = /\r?\n|\r(?!\n)/g; +const lineEndingPattern = '\r?\n|\r(?!\n)'; const kLineObjectStream = Symbol('line object stream'); const kQuestionCancel = Symbol('kQuestionCancel'); @@ -585,6 +586,7 @@ class Interface extends InterfaceConstructor { } // Run test() on the new string chunk, not on the entire line buffer. + const lineEnding = new RegExp(lineEndingPattern, 'g'); let newPartContainsEnding = RegExpPrototypeExec(lineEnding, string); if (newPartContainsEnding !== null) { if (this[kLine_buffer]) { @@ -1322,18 +1324,24 @@ class Interface extends InterfaceConstructor { // falls through default: if (typeof s === 'string' && s) { + /** + * Use Regular Expression scoped to this block, as lastIndex and the state for RegExpPrototypeExec + * will be overwritten if the same RegEx instance is reused in recursive function calls. + */ + const lineEnding = new RegExp(lineEndingPattern, 'g'); let nextMatch = RegExpPrototypeExec(lineEnding, s); - if (nextMatch !== null) { - this[kInsertString](StringPrototypeSlice(s, 0, nextMatch.index)); - let { lastIndex } = lineEnding; - while ((nextMatch = RegExpPrototypeExec(lineEnding, s)) !== null) { - this[kLine](); + + // If no line endings are found, just insert the string as is + if (nextMatch === null) { + this[kInsertString](s); + } else { + // Keep track of the end of the last match + let lastIndex = 0; + do { this[kInsertString](StringPrototypeSlice(s, lastIndex, nextMatch.index)); + this[kLine](); ({ lastIndex } = lineEnding); - } - if (lastIndex === s.length) this[kLine](); - } else { - this[kInsertString](s); + } while ((nextMatch = RegExpPrototypeExec(lineEnding, s)) !== null); } } } diff --git a/lib/repl.js b/lib/repl.js index 3875858871ebfb..638fb04689c3d0 100644 --- a/lib/repl.js +++ b/lib/repl.js @@ -861,6 +861,7 @@ function REPLServer(prompt, // code alignment const matches = self._sawKeyPress ? RegExpPrototypeExec(/^\s+/, cmd) : null; + // Preserve indentation in editorMode if (matches) { const prefix = matches[0]; self.write(prefix); diff --git a/test/parallel/test-repl-load-multiline-function.js b/test/parallel/test-repl-load-multiline-function.js new file mode 100644 index 00000000000000..055cd1cf2fd954 --- /dev/null +++ b/test/parallel/test-repl-load-multiline-function.js @@ -0,0 +1,47 @@ +'use strict'; +const common = require('../common'); +const ArrayStream = require('../common/arraystream'); +const assert = require('assert'); +const join = require('path').join; +const fs = require('fs'); + +common.skipIfDumbTerminal(); + +const tmpdir = require('../common/tmpdir'); +tmpdir.refresh(); + +const terminalCode = '(\u001b[1G\u001b[0J \u001b[1G)'; +const terminalCodeRegex = new RegExp(terminalCode.replace(/\[/g, '\\['), 'g'); + +const repl = require('repl'); + +const inputStream = new ArrayStream(); +const outputStream = new ArrayStream(); + +const r = repl.start({ + prompt: '', + input: inputStream, + output: outputStream, + terminal: true, + useColors: false +}); + +const testFile = 'function a(b) {\n return b }\na(1)\n'; +const testFileName = join(tmpdir.path, 'foo.js'); +fs.writeFileSync(testFileName, testFile); + +const command = `.load ${testFileName}\n`; +let accum = ''; +outputStream.write = (data) => accum += data.replace('\r', ''); + + +r.write(command); + +const expected = command + +'function a(b) {\n' + +' return b }\n' + +'a(1)\n' + +'\n' + +'1\n'; +assert.strictEqual(accum.replace(terminalCodeRegex, ''), expected); +r.close();