Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

readline: fix issue with newline-less last line #47317

Merged
merged 4 commits into from
Apr 14, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 12 additions & 14 deletions lib/internal/readline/interface.js
Original file line number Diff line number Diff line change
Expand Up @@ -1324,21 +1324,19 @@ class Interface extends InterfaceConstructor {
if (typeof s === 'string' && s) {
// Erase state of previous searches.
lineEnding.lastIndex = 0;
let nextMatch = RegExpPrototypeExec(lineEnding, s);
// 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));
({ lastIndex } = lineEnding);
this[kLine]();
// Restore lastIndex as the call to kLine could have mutated it.
lineEnding.lastIndex = lastIndex;
} while ((nextMatch = RegExpPrototypeExec(lineEnding, s)) !== null);
let nextMatch;
// Keep track of the end of the last match.
let lastIndex = 0;
while ((nextMatch = RegExpPrototypeExec(lineEnding, s)) !== null) {
this[kInsertString](StringPrototypeSlice(s, lastIndex, nextMatch.index));
({ lastIndex } = lineEnding);
this[kLine]();
// Restore lastIndex as the call to kLine could have mutated it.
lineEnding.lastIndex = lastIndex;
}
// This ensures that the last line is written if it doesn't end in a newline.
// Note that the last line may be the first line, in which case this still works.
this[kInsertString](StringPrototypeSlice(s, lastIndex));
}
}
}
Expand Down
7 changes: 7 additions & 0 deletions test/fixtures/repl-load-multiline-no-trailing-newline.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// The lack of a newline at the end of this file is intentional.
const getLunch = () =>
placeOrder('tacos')
.then(eat);

const placeOrder = (order) => Promise.resolve(order);
const eat = (food) => '<nom nom nom>';
24 changes: 24 additions & 0 deletions test/parallel/test-readline-interface-no-trailing-newline.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
'use strict';
const common = require('../common');
const ArrayStream = require('../common/arraystream');
const assert = require('assert');

common.skipIfDumbTerminal();

const readline = require('readline');
const rli = new readline.Interface({
terminal: true,
input: new ArrayStream(),
output: new ArrayStream(),
});

// Minimal reproduction for #47305
const testInput = '{\n}';

let accum = '';

rli.output.write = (data) => accum += data.replace('\r', '');

rli.write(testInput);

assert.strictEqual(accum, testInput);
42 changes: 42 additions & 0 deletions test/parallel/test-repl-load-multiline-no-trailing-newline.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
'use strict';
const common = require('../common');
const ArrayStream = require('../common/arraystream');
const fixtures = require('../common/fixtures');
const assert = require('assert');
const repl = require('repl');

common.skipIfDumbTerminal();

const command = `.load ${fixtures.path('repl-load-multiline-no-trailing-newline.js')}`;
const terminalCode = '\u001b[1G\u001b[0J \u001b[1G';
const terminalCodeRegex = new RegExp(terminalCode.replace(/\[/g, '\\['), 'g');

const expected = `${command}
// The lack of a newline at the end of this file is intentional.
const getLunch = () =>
placeOrder('tacos')
.then(eat);

const placeOrder = (order) => Promise.resolve(order);
const eat = (food) => '<nom nom nom>';
undefined
`;

let accum = '';

const inputStream = new ArrayStream();
const outputStream = new ArrayStream();

outputStream.write = (data) => accum += data.replace('\r', '');

const r = repl.start({
prompt: '',
input: inputStream,
output: outputStream,
terminal: true,
useColors: false
});

r.write(`${command}\n`);
assert.strictEqual(accum.replace(terminalCodeRegex, ''), expected);
r.close();