diff --git a/lib/repl.js b/lib/repl.js index 17aab1c409beca..b5b25fb534f980 100644 --- a/lib/repl.js +++ b/lib/repl.js @@ -76,6 +76,7 @@ const { StringPrototypeCharAt, StringPrototypeEndsWith, StringPrototypeIncludes, + StringPrototypeLastIndexOf, StringPrototypeRepeat, StringPrototypeSlice, StringPrototypeStartsWith, @@ -802,7 +803,31 @@ class REPLServer extends Interface { } // Check REPL keywords and empty lines against a trimmed line input. - const trimmedCmd = StringPrototypeTrim(cmd); + let trimmedCmd = StringPrototypeTrim(cmd); + + // TTY multiline input is stored as one editable line by readline. Check + // the newest physical line so dot-commands work from a continuation + // prompt, while keeping the previous lines buffered. + if (self.terminal && StringPrototypeIncludes(cmd, '\n')) { + const lastNewlineIndex = StringPrototypeLastIndexOf(cmd, '\n'); + const commandLine = StringPrototypeSlice(cmd, lastNewlineIndex + 1); + const trimmedCommandLine = StringPrototypeTrim(commandLine); + + if (StringPrototypeCharAt(trimmedCommandLine, 0) === '.' && + StringPrototypeCharAt(trimmedCommandLine, 1) !== '.' && + NumberIsNaN(NumberParseFloat(trimmedCommandLine))) { + const matches = RegExpPrototypeExec(/^\.([^\s]+)\s*(.*)$/, trimmedCommandLine); + const keyword = matches?.[1]; + + if (self.commands[keyword]) { + if (!self[kBufferedCommandSymbol]) { + self[kBufferedCommandSymbol] = + StringPrototypeSlice(cmd, 0, lastNewlineIndex + 1); + } + trimmedCmd = trimmedCommandLine; + } + } + } // Check to see if a REPL keyword was used. If it returns true, // display next prompt and return. diff --git a/test/parallel/test-repl-multiline.js b/test/parallel/test-repl-multiline.js index 6aecb670114484..8c531e7d178a3d 100644 --- a/test/parallel/test-repl-multiline.js +++ b/test/parallel/test-repl-multiline.js @@ -4,6 +4,8 @@ const assert = require('assert'); const { startNewREPLServer } = require('../common/repl'); const input = ['const foo = {', '};', 'foo']; +const dotCommandSyntaxError = + /Uncaught SyntaxError: Unexpected token '\.'/; function run({ useColors }) { const { replServer, output } = startNewREPLServer({ useColors }); @@ -27,3 +29,90 @@ function run({ useColors }) { run({ useColors: true }); run({ useColors: false }); + +function runDotCommandAfterRecoverable(command, validate) { + const { replServer, output } = startNewREPLServer(); + + replServer.on('exit', common.mustCall()); + replServer.write('function a() {\n'); + replServer.write(`${command}\n`); + + assert.doesNotMatch(output.accumulator, dotCommandSyntaxError); + validate(replServer, output); + + replServer.close(); +} + +runDotCommandAfterRecoverable('.break', (replServer, output) => { + replServer.write('1 + 1\n'); + assert.doesNotMatch(output.accumulator, dotCommandSyntaxError); + assert.match(output.accumulator, /2\n/); +}); + +runDotCommandAfterRecoverable('.help', (replServer, output) => { + assert.match(output.accumulator, /\.break\s+Sometimes you get stuck/); + assert.match(output.accumulator, /\.help\s+Print this help message/); + + replServer.write('.break\n'); + replServer.write('1 + 1\n'); + + assert.doesNotMatch(output.accumulator, dotCommandSyntaxError); + assert.match(output.accumulator, /2\n/); +}); + +function runBufferedDotCommandAfterRecoverable(command, validate) { + const { replServer, input, output } = + startNewREPLServer({ terminal: false }); + + replServer.on('exit', common.mustCall()); + input.run(['function a() {', command]); + + assert.doesNotMatch(output.accumulator, dotCommandSyntaxError); + validate(input, output); + + replServer.close(); +} + +runBufferedDotCommandAfterRecoverable('.break', (input, output) => { + input.run(['1 + 1']); + assert.doesNotMatch(output.accumulator, dotCommandSyntaxError); + assert.match(output.accumulator, /2\n/); +}); + +runBufferedDotCommandAfterRecoverable('.help', (input, output) => { + assert.match(output.accumulator, /\.break\s+Sometimes you get stuck/); + assert.match(output.accumulator, /\.help\s+Print this help message/); + + input.run(['.break', '1 + 1']); + + assert.doesNotMatch(output.accumulator, dotCommandSyntaxError); + assert.match(output.accumulator, /2\n/); +}); + +{ + const { replServer, output } = startNewREPLServer(); + let exited = false; + + replServer.on('exit', common.mustCall(() => { + exited = true; + })); + replServer.write('function a() {\n'); + replServer.write('.exit\n'); + + assert.strictEqual(exited, true); + assert.doesNotMatch(output.accumulator, dotCommandSyntaxError); +} + +{ + const { replServer, input, output } = + startNewREPLServer({ terminal: false }); + let exited = false; + + replServer.on('exit', common.mustCall(() => { + exited = true; + })); + input.run(['function a() {', '.exit']); + + assert.strictEqual(exited, true); + assert.doesNotMatch(output.accumulator, dotCommandSyntaxError); +}