-
Notifications
You must be signed in to change notification settings - Fork 69
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(cli-repl): add support for bracketed paste in REPL MONGOSH-1909 (#…
…2328) Bracketed paste allows us to receive a copy-pasted piece of mongosh as a single block, rather than interpreting it line-by-line. For now, this requires some monkey-patching of Node.js internals, so a follow-up ticket will include work to upstream support for this into Node.js core.
- Loading branch information
Showing
12 changed files
with
272 additions
and
16 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,91 @@ | ||
import type { ReplOptions, REPLServer } from 'repl'; | ||
import { start } from 'repl'; | ||
import type { Readable, Writable } from 'stream'; | ||
import { PassThrough } from 'stream'; | ||
import { tick } from '../test/repl-helpers'; | ||
import { installPasteSupport } from './repl-paste-support'; | ||
import { expect } from 'chai'; | ||
|
||
function createTerminalRepl(extraOpts: Partial<ReplOptions> = {}): { | ||
input: Writable; | ||
output: Readable; | ||
repl: REPLServer; | ||
} { | ||
const input = new PassThrough(); | ||
const output = new PassThrough({ encoding: 'utf8' }); | ||
|
||
const repl = start({ | ||
input: input, | ||
output: output, | ||
prompt: '> ', | ||
terminal: true, | ||
useColors: false, | ||
...extraOpts, | ||
}); | ||
return { input, output, repl }; | ||
} | ||
|
||
describe('installPasteSupport', function () { | ||
it('does nothing for non-terminal REPL instances', async function () { | ||
const { repl, output } = createTerminalRepl({ terminal: false }); | ||
const onFinish = installPasteSupport(repl); | ||
await tick(); | ||
expect(output.read()).to.equal('> '); | ||
expect(onFinish).to.equal(''); | ||
}); | ||
|
||
it('prints a control character sequence that indicates support for bracketed paste', async function () { | ||
const { repl, output } = createTerminalRepl(); | ||
const onFinish = installPasteSupport(repl); | ||
await tick(); | ||
expect(output.read()).to.include('\x1B[?2004h'); | ||
expect(onFinish).to.include('\x1B[?2004l'); | ||
}); | ||
|
||
it('echoes back control characters in the input by default', async function () { | ||
const { repl, input, output } = createTerminalRepl(); | ||
installPasteSupport(repl); | ||
await tick(); | ||
output.read(); // Ignore prompt etc. | ||
input.write('foo\x1b[Dbar'); // ESC[D = 1 character to the left | ||
await tick(); | ||
expect(output.read()).to.equal( | ||
'foo\x1B[1D\x1B[1G\x1B[0J> fobo\x1B[6G\x1B[1G\x1B[0J> fobao\x1B[7G\x1B[1G\x1B[0J> fobaro\x1B[8G' | ||
); | ||
}); | ||
|
||
it('ignores control characters in the input while pasting', async function () { | ||
const { repl, input, output } = createTerminalRepl(); | ||
installPasteSupport(repl); | ||
await tick(); | ||
output.read(); // Ignore prompt etc. | ||
input.write('\x1b[200~foo\x1b[Dbar\x1b[201~'); // ESC[D = 1 character to the left | ||
await tick(); | ||
expect(output.read()).to.equal('foobar'); | ||
}); | ||
|
||
it('resets to accepting control characters in the input after pasting', async function () { | ||
const { repl, input, output } = createTerminalRepl(); | ||
installPasteSupport(repl); | ||
await tick(); | ||
output.read(); | ||
input.write('\x1b[200~foo\x1b[Dbar\x1b[201~'); // ESC[D = 1 character to the left | ||
await tick(); | ||
output.read(); | ||
input.write('foo\x1b[Dbar'); | ||
await tick(); | ||
expect(output.read()).to.equal( | ||
'foo\x1B[1D\x1B[1G\x1B[0J> foobarfobo\x1B[12G\x1B[1G\x1B[0J> foobarfobao\x1B[13G\x1B[1G\x1B[0J> foobarfobaro\x1B[14G' | ||
); | ||
}); | ||
|
||
it('allows a few special characters while pasting', async function () { | ||
const { repl, input, output } = createTerminalRepl(); | ||
installPasteSupport(repl); | ||
await tick(); | ||
output.read(); | ||
input.write('\x1b[200~12*34\n_*_\n\x1b[201~'); | ||
await tick(); | ||
expect(output.read()).to.include((12 * 34) ** 2); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
import type { REPLServer } from 'repl'; | ||
|
||
// https://github.com/nodejs/node/blob/d9786109b2a0982677135f0c146f6b591a0e4961/lib/internal/readline/utils.js#L90 | ||
// https://nodejs.org/api/readline.html#readlineemitkeypresseventsstream-interface | ||
export type KeypressKey = { | ||
sequence: string | null; | ||
name: string | undefined; | ||
ctrl: boolean; | ||
meta: boolean; | ||
shift: boolean; | ||
code?: string; | ||
}; | ||
|
||
function* prototypeChain(obj: unknown): Iterable<unknown> { | ||
if (!obj) return; | ||
yield obj; | ||
yield* prototypeChain(Object.getPrototypeOf(obj)); | ||
} | ||
|
||
export function installPasteSupport(repl: REPLServer): string { | ||
if (!repl.terminal || process.env.TERM === 'dumb') return ''; // No paste needed in non-terminal environments | ||
|
||
// TODO(MONGOSH-1911): Upstream as much of this into Node.js core as possible, | ||
// both because of the value to the wider community but also because this is | ||
// messing with Node.js REPL internals to a very unfortunate degree. | ||
repl.output.write('\x1b[?2004h'); // Indicate support for paste mode | ||
const onEnd = '\x1b[?2004l'; // End of support for paste mode | ||
// Find the symbol used for the (internal) _ttyWrite method of readline.Interface | ||
// https://github.com/nodejs/node/blob/d9786109b2a0982677135f0c146f6b591a0e4961/lib/internal/readline/interface.js#L1056 | ||
const ttyWriteKey = [...prototypeChain(repl)] | ||
.flatMap((proto) => Object.getOwnPropertySymbols(proto)) | ||
.find((s) => String(s).includes('(_ttyWrite)')); | ||
if (!ttyWriteKey) | ||
throw new Error('Could not find _ttyWrite key on readline instance'); | ||
repl.input.on('keypress', (s: string, key: KeypressKey) => { | ||
if (key.name === 'paste-start') { | ||
if (Object.prototype.hasOwnProperty.call(repl, ttyWriteKey)) | ||
throw new Error( | ||
'Unexpected existing own _ttyWrite key on readline instance' | ||
); | ||
const origTtyWrite = (repl as any)[ttyWriteKey]; | ||
Object.defineProperty(repl as any, ttyWriteKey, { | ||
value: function (s: string, key: KeypressKey) { | ||
if (key.ctrl || key.meta || key.code) { | ||
// Special character or escape code sequence, ignore while pasting | ||
return; | ||
} | ||
if ( | ||
key.name && | ||
key.name !== key.sequence?.toLowerCase() && | ||
!['tab', 'return', 'enter', 'space'].includes(key.name) | ||
) { | ||
// Special character or escape code sequence, ignore while pasting | ||
return; | ||
} | ||
return origTtyWrite.call(this, s, key); | ||
}, | ||
enumerable: false, | ||
writable: true, | ||
configurable: true, | ||
}); | ||
} else if (key.name === 'paste-end') { | ||
delete (repl as any)[ttyWriteKey]; | ||
} | ||
}); | ||
return onEnd; | ||
} |
Oops, something went wrong.