Skip to content

Commit 46d3972

Browse files
committed
fix(cli-repl): work around Node.js middle-of-line paste bug
1 parent 49fc167 commit 46d3972

File tree

4 files changed

+105
-9
lines changed

4 files changed

+105
-9
lines changed

packages/cli-repl/src/mongosh-repl.spec.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1195,6 +1195,30 @@ describe('MongoshNodeRepl', function () {
11951195
);
11961196
});
11971197
});
1198+
1199+
context('pasting code', function () {
1200+
const pasteStart = '\x1b[200~';
1201+
const pasteEnd = '\x1b[201~';
1202+
const moveLeft = '\x1b[1D';
1203+
1204+
it('can paste code and run it', async function () {
1205+
input.write(`${pasteStart}12 * 34${pasteEnd}\n`);
1206+
await waitEval(bus);
1207+
expect(output).to.include('408');
1208+
});
1209+
1210+
it('can paste code in the middle of a line and run it', async function () {
1211+
input.write(`56${moveLeft}${pasteStart}12 * 34${pasteEnd}\n`);
1212+
await waitEval(bus);
1213+
expect(output).to.include('177152'); // 512 * 346
1214+
});
1215+
1216+
it('can paste code in the middle of multiline input and run it', async function () {
1217+
input.write(`{\n56${moveLeft}${pasteStart}12 * 34${pasteEnd}\n}\n`);
1218+
await waitEval(bus);
1219+
expect(output).to.include('177152'); // 512 * 346
1220+
});
1221+
});
11981222
});
11991223

12001224
context('with somewhat unreachable history file', function () {

packages/cli-repl/src/mongosh-repl.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ import type { FormatOptions } from './format-output';
4747
import { markTime } from './startup-timing';
4848
import type { Context } from 'vm';
4949
import { Script, createContext, runInContext } from 'vm';
50-
import { installPasteSupport } from './repl-paste-support';
50+
import { fixNode60446, installPasteSupport } from './repl-paste-support';
5151
import util from 'util';
5252
import { fixNodeReplCompleterSideEffectHandling } from './node-repl-fix-completer-side-effects';
5353

@@ -278,6 +278,7 @@ class MongoshNodeRepl implements EvaluationListener {
278278
onAsyncSigint: this.onAsyncSigint.bind(this),
279279
...this.nodeReplOptions,
280280
});
281+
fixNode60446(repl);
281282
context = repl.context;
282283
} else {
283284
// https://nodejs.org/api/repl.html#replbuiltinmodules not represented in TS types

packages/cli-repl/src/repl-paste-support.spec.ts

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { start } from 'repl';
33
import type { Readable, Writable } from 'stream';
44
import { PassThrough } from 'stream';
55
import { tick } from '../test/repl-helpers';
6-
import { installPasteSupport } from './repl-paste-support';
6+
import { fixNode60446, installPasteSupport } from './repl-paste-support';
77
import { expect } from 'chai';
88

99
function createTerminalRepl(extraOpts: Partial<ReplOptions> = {}): {
@@ -22,6 +22,7 @@ function createTerminalRepl(extraOpts: Partial<ReplOptions> = {}): {
2222
useColors: false,
2323
...extraOpts,
2424
});
25+
fixNode60446(repl);
2526
return { input, output, repl };
2627
}
2728

@@ -49,9 +50,12 @@ describe('installPasteSupport', function () {
4950
output.read(); // Ignore prompt etc.
5051
input.write('foo\x1b[Dbar'); // ESC[D = 1 character to the left
5152
await tick();
52-
expect(output.read()).to.equal(
53-
'foo\x1B[1D\x1B[1G\x1B[0J> fobo\x1B[6G\x1B[1G\x1B[0J> fobao\x1B[7G\x1B[1G\x1B[0J> fobaro\x1B[8G'
54-
);
53+
// Expected output changed after https://github.com/nodejs/node/pull/59857
54+
// because now characters aren't handled one-by-one anymore.
55+
expect(output.read()).to.be.oneOf([
56+
'foo\x1B[1D\x1B[1G\x1B[0J> fobo\x1B[6G\x1B[1G\x1B[0J> fobao\x1B[7G\x1B[1G\x1B[0J> fobaro\x1B[8G',
57+
'foo\x1B[1Dba\x1B[1G\x1B[0J> fobaro\x1B[8G',
58+
]);
5559
});
5660

5761
it('ignores control characters in the input while pasting', async function () {
@@ -74,9 +78,10 @@ describe('installPasteSupport', function () {
7478
output.read();
7579
input.write('foo\x1b[Dbar');
7680
await tick();
77-
expect(output.read()).to.equal(
78-
'foo\x1B[1D\x1B[1G\x1B[0J> foobarfobo\x1B[12G\x1B[1G\x1B[0J> foobarfobao\x1B[13G\x1B[1G\x1B[0J> foobarfobaro\x1B[14G'
79-
);
81+
expect(output.read()).to.be.oneOf([
82+
'foo\x1B[1D\x1B[1G\x1B[0J> foobarfobo\x1B[12G\x1B[1G\x1B[0J> foobarfobao\x1B[13G\x1B[1G\x1B[0J> foobarfobaro\x1B[14G',
83+
'foo\x1B[1Dba\x1B[1G\x1B[0J> foobarfobaro\x1B[14G',
84+
]);
8085
});
8186

8287
it('allows a few special characters while pasting', async function () {

packages/cli-repl/src/repl-paste-support.ts

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
import type { REPLServer } from 'repl';
1+
import type { Interface as ReadlineInterface } from 'readline';
2+
import { start as replStart, type REPLServer } from 'repl';
3+
import { PassThrough } from 'stream';
24

35
// https://github.com/nodejs/node/blob/d9786109b2a0982677135f0c146f6b591a0e4961/lib/internal/readline/utils.js#L90
46
// https://nodejs.org/api/readline.html#readlineemitkeypresseventsstream-interface
@@ -65,3 +67,67 @@ export function installPasteSupport(repl: REPLServer): string {
6567
});
6668
return onEnd;
6769
}
70+
71+
// Bug: https://github.com/nodejs/node/issues/60446
72+
// Introduced by: https://github.com/nodejs/node/pull/59857
73+
// Fixed by: https://github.com/nodejs/node/pull/60470
74+
function _hasNode60446(): boolean {
75+
const input = new PassThrough();
76+
const output = new PassThrough();
77+
const repl = replStart({ terminal: true, input, output, useColors: false });
78+
repl.input.emit('data', '{}');
79+
repl.input.emit('keypress', '', { name: 'left' });
80+
repl.input.emit('data', 'node');
81+
const { line } = repl;
82+
repl.close();
83+
return line === '{}noed';
84+
}
85+
_hasNode60446();
86+
let hasNode60446: boolean | undefined = undefined;
87+
88+
export function fixNode60446(repl: ReadlineInterface): void {
89+
hasNode60446 ??= _hasNode60446();
90+
if (!hasNode60446) return;
91+
const symbols = [...prototypeChain(repl)].flatMap((proto) =>
92+
Object.getOwnPropertySymbols(proto)
93+
);
94+
const insertStringKey = symbols.find((s) =>
95+
String(s).includes('(_insertString)')
96+
);
97+
const writeToOutputKey = symbols.find((s) =>
98+
String(s).includes('(_writeToOutput)')
99+
);
100+
if (!insertStringKey || !writeToOutputKey) return;
101+
const original = (repl as any)[insertStringKey];
102+
103+
// Monkey-patch in the fix linked above
104+
let fixupOnWriteToOutput: undefined | (() => void);
105+
Object.defineProperty(repl as any, insertStringKey, {
106+
configurable: true,
107+
writable: true,
108+
enumerable: false,
109+
value: function (this: ReadlineInterface, s: string) {
110+
if (!(this as any).isCompletionEnabled) {
111+
const origLine = this.line;
112+
const origCursor = this.cursor;
113+
fixupOnWriteToOutput = () => {
114+
const beg = origLine.slice(0, origCursor);
115+
const end = origLine.slice(origCursor);
116+
(this as any).line = beg + s + end;
117+
};
118+
}
119+
return original.call(this, s);
120+
},
121+
});
122+
const originalWriteToOutput = (repl as any)[writeToOutputKey];
123+
Object.defineProperty(repl as any, writeToOutputKey, {
124+
configurable: true,
125+
writable: true,
126+
enumerable: false,
127+
value: function (this: ReadlineInterface, s: string) {
128+
fixupOnWriteToOutput?.();
129+
fixupOnWriteToOutput = undefined;
130+
return originalWriteToOutput.call(this, s);
131+
},
132+
});
133+
}

0 commit comments

Comments
 (0)