diff --git a/doc/api/repl.md b/doc/api/repl.md index 4703022d3b5ea4..625f3c22d24255 100644 --- a/doc/api/repl.md +++ b/doc/api/repl.md @@ -21,9 +21,11 @@ result. Input and output may be from `stdin` and `stdout`, respectively, or may be connected to any Node.js [stream][]. Instances of [`repl.REPLServer`][] support automatic completion of inputs, -simplistic Emacs-style line editing, multi-line inputs, ANSI-styled output, -saving and restoring current REPL session state, error recovery, and -customizable evaluation functions. +completion preview, simplistic Emacs-style line editing, multi-line inputs, +[ZSH][] like reverse-i-search, ANSI-styled output, saving and restoring current +REPL session state, error recovery, and customizable evaluation functions. +Terminals that do not support ANSI-styles and Emacs-style line editing +automatically fall back to a limited feature set. ### Commands and Special Keys @@ -232,6 +234,24 @@ undefined undefined ``` +### Reverse-i-search + + +The REPL supports bi-directional reverse-i-search similar to [ZSH][]. It is +triggered with ` + R` to search backwards and ` + S` to search +forwards. + +Duplicated history entires will be skipped. + +Entries are accepted as soon as any button is pressed that doesn't correspond +with the reverse search. Cancelling is possible by pressing `escape` or +` + C`. + +Changing the direction immediately searches for the next entry in the expected +direction from the current position on. + ### Custom Evaluation Functions When a new [`repl.REPLServer`][] is created, a custom evaluation function may be @@ -695,6 +715,7 @@ a `net.Server` and `net.Socket` instance, see: For an example of running a REPL instance over [curl(1)][], see: . +[ZSH]: https://en.wikipedia.org/wiki/Z_shell [`'uncaughtException'`]: process.html#process_event_uncaughtexception [`--experimental-repl-await`]: cli.html#cli_experimental_repl_await [`ERR_DOMAIN_CANNOT_SET_UNCAUGHT_EXCEPTION_CAPTURE`]: errors.html#errors_err_domain_cannot_set_uncaught_exception_capture diff --git a/lib/internal/repl/utils.js b/lib/internal/repl/utils.js index 906046c40c5ede..e6383e90e1046e 100644 --- a/lib/internal/repl/utils.js +++ b/lib/internal/repl/utils.js @@ -24,6 +24,7 @@ const { const { clearLine, + clearScreenDown, cursorTo, moveCursor, } = require('readline'); @@ -42,7 +43,13 @@ const inspectOptions = { compact: true, breakLength: Infinity }; -const inspectedOptions = inspect(inspectOptions, { colors: false }); +// Specify options that might change the output in a way that it's not a valid +// stringified object anymore. +const inspectedOptions = inspect(inspectOptions, { + depth: 1, + colors: false, + showHidden: false +}); // If the error is that we've unexpectedly ended the input, // then let the user try to recover by adding more input. @@ -393,8 +400,242 @@ function setupPreview(repl, contextSymbol, bufferSymbol, active) { return { showPreview, clearPreview }; } +function setupReverseSearch(repl) { + // Simple terminals can't use reverse search. + if (process.env.TERM === 'dumb') { + return { reverseSearch() { return false; } }; + } + + const alreadyMatched = new Set(); + const labels = { + r: 'bck-i-search: ', + s: 'fwd-i-search: ' + }; + let isInReverseSearch = false; + let historyIndex = -1; + let input = ''; + let cursor = -1; + let dir = 'r'; + let lastMatch = -1; + let lastCursor = -1; + let promptPos; + + function checkAndSetDirectionKey(keyName) { + if (!labels[keyName]) { + return false; + } + if (dir !== keyName) { + // Reset the already matched set in case the direction is changed. That + // way it's possible to find those entries again. + alreadyMatched.clear(); + } + dir = keyName; + return true; + } + + function goToNextHistoryIndex() { + // Ignore this entry for further searches and continue to the next + // history entry. + alreadyMatched.add(repl.history[historyIndex]); + historyIndex += dir === 'r' ? 1 : -1; + cursor = -1; + } + + function search() { + // Just print an empty line in case the user removed the search parameter. + if (input === '') { + print(repl.line, `${labels[dir]}_`); + return; + } + // Fix the bounds in case the direction has changed in the meanwhile. + if (dir === 'r') { + if (historyIndex < 0) { + historyIndex = 0; + } + } else if (historyIndex >= repl.history.length) { + historyIndex = repl.history.length - 1; + } + // Check the history entries until a match is found. + while (historyIndex >= 0 && historyIndex < repl.history.length) { + let entry = repl.history[historyIndex]; + // Visualize all potential matches only once. + if (alreadyMatched.has(entry)) { + historyIndex += dir === 'r' ? 1 : -1; + continue; + } + // Match the next entry either from the start or from the end, depending + // on the current direction. + if (dir === 'r') { + // Update the cursor in case it's necessary. + if (cursor === -1) { + cursor = entry.length; + } + cursor = entry.lastIndexOf(input, cursor - 1); + } else { + cursor = entry.indexOf(input, cursor + 1); + } + // Match not found. + if (cursor === -1) { + goToNextHistoryIndex(); + // Match found. + } else { + if (repl.useColors) { + const start = entry.slice(0, cursor); + const end = entry.slice(cursor + input.length); + entry = `${start}\x1B[4m${input}\x1B[24m${end}`; + } + print(entry, `${labels[dir]}${input}_`, cursor); + lastMatch = historyIndex; + lastCursor = cursor; + // Explicitly go to the next history item in case no further matches are + // possible with the current entry. + if ((dir === 'r' && cursor === 0) || + (dir === 's' && entry.length === cursor + input.length)) { + goToNextHistoryIndex(); + } + return; + } + } + print(repl.line, `failed-${labels[dir]}${input}_`); + } + + function print(outputLine, inputLine, cursor = repl.cursor) { + // TODO(BridgeAR): Resizing the terminal window hides the overlay. To fix + // that, readline must be aware of this information. It's probably best to + // add a couple of properties to readline that allow to do the following: + // 1. Add arbitrary data to the end of the current line while not counting + // towards the line. This would be useful for the completion previews. + // 2. Add arbitrary extra lines that do not count towards the regular line. + // This would be useful for both, the input preview and the reverse + // search. It might be combined with the first part? + // 3. Add arbitrary input that is "on top" of the current line. That is + // useful for the reverse search. + // 4. To trigger the line refresh, functions should be used to pass through + // the information. Alternatively, getters and setters could be used. + // That might even be more elegant. + // The data would then be accounted for when calling `_refreshLine()`. + // This function would then look similar to: + // repl.overlay(outputLine); + // repl.addTrailingLine(inputLine); + // repl.setCursor(cursor); + // More potential improvements: use something similar to stream.cork(). + // Multiple cursor moves on the same tick could be prevented in case all + // writes from the same tick are combined and the cursor is moved at the + // tick end instead of after each operation. + let rows = 0; + if (lastMatch !== -1) { + const line = repl.history[lastMatch].slice(0, lastCursor); + rows = repl._getDisplayPos(`${repl._prompt}${line}`).rows; + cursorTo(repl.output, promptPos.cols); + } else if (isInReverseSearch && repl.line !== '') { + rows = repl._getCursorPos().rows; + cursorTo(repl.output, promptPos.cols); + } + if (rows !== 0) + moveCursor(repl.output, 0, -rows); + + if (isInReverseSearch) { + clearScreenDown(repl.output); + repl.output.write(`${outputLine}\n${inputLine}`); + } else { + repl.output.write(`\n${inputLine}`); + } + + lastMatch = -1; + + // To know exactly how many rows we have to move the cursor back we need the + // cursor rows, the output rows and the input rows. + const prompt = repl._prompt; + const cursorLine = `${prompt}${outputLine.slice(0, cursor)}`; + const cursorPos = repl._getDisplayPos(cursorLine); + const outputPos = repl._getDisplayPos(`${prompt}${outputLine}`); + const inputPos = repl._getDisplayPos(inputLine); + const inputRows = inputPos.rows - (inputPos.cols === 0 ? 1 : 0); + + rows = -1 - inputRows - (outputPos.rows - cursorPos.rows); + + moveCursor(repl.output, 0, rows); + cursorTo(repl.output, cursorPos.cols); + } + + function reset(string) { + isInReverseSearch = string !== undefined; + + // In case the reverse search ends and a history entry is found, reset the + // line to the found entry. + if (!isInReverseSearch) { + if (lastMatch !== -1) { + repl.line = repl.history[lastMatch]; + repl.cursor = lastCursor; + repl.historyIndex = lastMatch; + } + + lastMatch = -1; + + // Clear screen and write the current repl.line before exiting. + cursorTo(repl.output, promptPos.cols); + if (promptPos.rows !== 0) + moveCursor(repl.output, 0, promptPos.rows); + clearScreenDown(repl.output); + if (repl.line !== '') { + repl.output.write(repl.line); + if (repl.line.length !== repl.cursor) { + const { cols, rows } = repl._getCursorPos(); + cursorTo(repl.output, cols); + if (rows !== 0) + moveCursor(repl.output, 0, rows); + } + } + } + + input = string || ''; + cursor = -1; + historyIndex = repl.historyIndex; + alreadyMatched.clear(); + } + + function reverseSearch(string, key) { + if (!isInReverseSearch) { + if (key.ctrl && checkAndSetDirectionKey(key.name)) { + historyIndex = repl.historyIndex; + promptPos = repl._getDisplayPos(`${repl._prompt}`); + print(repl.line, `${labels[dir]}_`); + isInReverseSearch = true; + } + } else if (key.ctrl && checkAndSetDirectionKey(key.name)) { + search(); + } else if (key.name === 'backspace' || + (key.ctrl && (key.name === 'h' || key.name === 'w'))) { + reset(input.slice(0, input.length - 1)); + search(); + // Special handle + c and escape. Those should only cancel the + // reverse search. The original line is visible afterwards again. + } else if ((key.ctrl && key.name === 'c') || key.name === 'escape') { + lastMatch = -1; + reset(); + return true; + // End search in case either enter is pressed or if any non-reverse-search + // key (combination) is pressed. + } else if (key.ctrl || + key.meta || + key.name === 'return' || + key.name === 'enter' || + typeof string !== 'string' || + string === '') { + reset(); + } else { + reset(`${input}${string}`); + search(); + } + return isInReverseSearch; + } + + return { reverseSearch }; +} + module.exports = { isRecoverableError, kStandaloneREPL: Symbol('kStandaloneREPL'), - setupPreview + setupPreview, + setupReverseSearch }; diff --git a/lib/repl.js b/lib/repl.js index 181556a2316c2d..d9efb8c5ece523 100644 --- a/lib/repl.js +++ b/lib/repl.js @@ -102,6 +102,7 @@ const { isRecoverableError, kStandaloneREPL, setupPreview, + setupReverseSearch, } = require('internal/repl/utils'); const { getOwnNonIndexProperties, @@ -810,6 +811,8 @@ function REPLServer(prompt, } }); + const { reverseSearch } = setupReverseSearch(this); + const { clearPreview, showPreview @@ -835,8 +838,10 @@ function REPLServer(prompt, self.clearLine(); } clearPreview(); - ttyWrite(d, key); - showPreview(); + if (!reverseSearch(d, key)) { + ttyWrite(d, key); + showPreview(); + } return; } @@ -1081,6 +1086,9 @@ REPLServer.prototype.complete = function() { this.completer.apply(this, arguments); }; +// TODO: Native module names should be auto-resolved. +// That improves the auto completion. + // Provide a list of completions for the given leading text. This is // given to the readline interface for handling tab completion. // diff --git a/test/parallel/test-repl-history-navigation.js b/test/parallel/test-repl-history-navigation.js index 766b3f0424711d..8de2b49b0ea5a6 100644 --- a/test/parallel/test-repl-history-navigation.js +++ b/test/parallel/test-repl-history-navigation.js @@ -340,7 +340,7 @@ function runTest() { const output = chunk.toString(); if (!opts.showEscapeCodes && - output.charCodeAt(0) === 27 || /^[\r\n]+$/.test(output)) { + (output[0] === '\x1B' || /^[\r\n]+$/.test(output))) { return next(); } diff --git a/test/parallel/test-repl-reverse-search.js b/test/parallel/test-repl-reverse-search.js new file mode 100644 index 00000000000000..a7c736300188de --- /dev/null +++ b/test/parallel/test-repl-reverse-search.js @@ -0,0 +1,363 @@ +'use strict'; + +// Flags: --expose-internals + +const common = require('../common'); +const stream = require('stream'); +const REPL = require('internal/repl'); +const assert = require('assert'); +const fs = require('fs'); +const path = require('path'); +const { inspect } = require('util'); + +common.allowGlobals('aaaa'); + +const tmpdir = require('../common/tmpdir'); +tmpdir.refresh(); + +const defaultHistoryPath = path.join(tmpdir.path, '.node_repl_history'); + +// Create an input stream specialized for testing an array of actions +class ActionStream extends stream.Stream { + run(data) { + const _iter = data[Symbol.iterator](); + const doAction = () => { + const next = _iter.next(); + if (next.done) { + // Close the repl. Note that it must have a clean prompt to do so. + this.emit('keypress', '', { ctrl: true, name: 'd' }); + return; + } + const action = next.value; + + if (typeof action === 'object') { + this.emit('keypress', '', action); + } else { + this.emit('data', `${action}`); + } + setImmediate(doAction); + }; + doAction(); + } + resume() {} + pause() {} +} +ActionStream.prototype.readable = true; + +// Mock keys +const ENTER = { name: 'enter' }; +const UP = { name: 'up' }; +const DOWN = { name: 'down' }; +const BACKSPACE = { name: 'backspace' }; +const SEARCH_BACKWARDS = { name: 'r', ctrl: true }; +const SEARCH_FORWARDS = { name: 's', ctrl: true }; +const ESCAPE = { name: 'escape' }; +const CTRL_C = { name: 'c', ctrl: true }; +const DELETE_WORD_LEFT = { name: 'w', ctrl: true }; + +const prompt = '> '; + +// TODO(BridgeAR): Add tests for lines that exceed the maximum columns. +const tests = [ + { // Creates few history to navigate for + env: { NODE_REPL_HISTORY: defaultHistoryPath }, + test: [ + 'console.log("foo")', ENTER, + 'ab = "aaaa"', ENTER, + 'repl.repl.historyIndex', ENTER, + 'console.log("foo")', ENTER, + 'let ba = 9', ENTER, + 'ab = "aaaa"', ENTER, + '555 - 909', ENTER, + '{key : {key2 :[] }}', ENTER, + 'Array(100).fill(1)', ENTER + ], + expected: [], + clean: false + }, + { + env: { NODE_REPL_HISTORY: defaultHistoryPath }, + showEscapeCodes: true, + checkTotal: true, + useColors: true, + test: [ + '7', // 1 + SEARCH_FORWARDS, + SEARCH_FORWARDS, // 3 + 'a', + SEARCH_BACKWARDS, // 5 + SEARCH_FORWARDS, + SEARCH_BACKWARDS, // 7 + 'a', + BACKSPACE, // 9 + DELETE_WORD_LEFT, + 'aa', // 11 + SEARCH_BACKWARDS, + SEARCH_BACKWARDS, // 13 + SEARCH_BACKWARDS, + SEARCH_BACKWARDS, // 15 + SEARCH_FORWARDS, + ESCAPE, // 17 + ENTER + ], + // A = Cursor n up + // B = Cursor n down + // C = Cursor n forward + // D = Cursor n back + // G = Cursor to column n + // J = Erase in screen; 0 = right; 1 = left; 2 = total + // K = Erase in line; 0 = right; 1 = left; 2 = total + expected: [ + // 0. Start + '\x1B[1G', '\x1B[0J', + prompt, '\x1B[3G', + // 1. '7' + '7', + // 2. SEARCH FORWARDS + '\nfwd-i-search: _', '\x1B[1A', '\x1B[4G', + // 3. SEARCH FORWARDS + '\x1B[3G', '\x1B[0J', + '7\nfwd-i-search: _', '\x1B[1A', '\x1B[4G', + // 4. 'a' + '\x1B[3G', '\x1B[0J', + '7\nfailed-fwd-i-search: a_', '\x1B[1A', '\x1B[4G', + // 5. SEARCH BACKWARDS + '\x1B[3G', '\x1B[0J', + 'Arr\x1B[4ma\x1B[24my(100).fill(1)\nbck-i-search: a_', + '\x1B[1A', '\x1B[6G', + // 6. SEARCH FORWARDS + '\x1B[3G', '\x1B[0J', + '7\nfailed-fwd-i-search: a_', '\x1B[1A', '\x1B[4G', + // 7. SEARCH BACKWARDS + '\x1B[3G', '\x1B[0J', + 'Arr\x1B[4ma\x1B[24my(100).fill(1)\nbck-i-search: a_', + '\x1B[1A', '\x1B[6G', + // 8. 'a' + '\x1B[3G', '\x1B[0J', + 'ab = "aa\x1B[4maa\x1B[24m"\nbck-i-search: aa_', + '\x1B[1A', '\x1B[11G', + // 9. BACKSPACE + '\x1B[3G', '\x1B[0J', + 'Arr\x1B[4ma\x1B[24my(100).fill(1)\nbck-i-search: a_', + '\x1B[1A', '\x1B[6G', + // 10. DELETE WORD LEFT (works as backspace) + '\x1B[3G', '\x1B[0J', + '7\nbck-i-search: _', '\x1B[1A', '\x1B[4G', + // 11. 'a' + '\x1B[3G', '\x1B[0J', + 'Arr\x1B[4ma\x1B[24my(100).fill(1)\nbck-i-search: a_', + '\x1B[1A', '\x1B[6G', + // 11. 'aa' - continued + '\x1B[3G', '\x1B[0J', + 'ab = "aa\x1B[4maa\x1B[24m"\nbck-i-search: aa_', + '\x1B[1A', '\x1B[11G', + // 12. SEARCH BACKWARDS + '\x1B[3G', '\x1B[0J', + 'ab = "a\x1B[4maa\x1B[24ma"\nbck-i-search: aa_', + '\x1B[1A', '\x1B[10G', + // 13. SEARCH BACKWARDS + '\x1B[3G', '\x1B[0J', + 'ab = "\x1B[4maa\x1B[24maa"\nbck-i-search: aa_', + '\x1B[1A', '\x1B[9G', + // 14. SEARCH BACKWARDS + '\x1B[3G', '\x1B[0J', + '7\nfailed-bck-i-search: aa_', '\x1B[1A', '\x1B[4G', + // 15. SEARCH BACKWARDS + '\x1B[3G', '\x1B[0J', + '7\nfailed-bck-i-search: aa_', '\x1B[1A', '\x1B[4G', + // 16. SEARCH FORWARDS + '\x1B[3G', '\x1B[0J', + 'ab = "\x1B[4maa\x1B[24maa"\nfwd-i-search: aa_', + '\x1B[1A', '\x1B[9G', + // 17. ESCAPE + '\x1B[3G', '\x1B[0J', + '7', + // 18. ENTER + '\r\n', + '\x1B[33m7\x1B[39m\n', + '\x1B[1G', '\x1B[0J', + prompt, + '\x1B[3G', + '\r\n' + ], + clean: false + }, + { + env: { NODE_REPL_HISTORY: defaultHistoryPath }, + showEscapeCodes: true, + skip: !process.features.inspector, + checkTotal: true, + useColors: false, + test: [ + 'fu', // 1 + SEARCH_BACKWARDS, + '}', // 3 + SEARCH_BACKWARDS, + CTRL_C, // 5 + CTRL_C, + '1+1', // 7 + ENTER, + SEARCH_BACKWARDS, // 9 + '+', + '\r', // 11 + '2', + SEARCH_BACKWARDS, // 13 + 're', + UP, // 15 + DOWN, + SEARCH_FORWARDS, // 17 + '\n' + ], + expected: [ + '\x1B[1G', '\x1B[0J', + prompt, '\x1B[3G', + 'f', 'u', ' // nction', + '\x1B[5G', '\x1B[0K', + '\nbck-i-search: _', '\x1B[1A', '\x1B[5G', + '\x1B[3G', '\x1B[0J', + '{key : {key2 :[] }}\nbck-i-search: }_', '\x1B[1A', '\x1B[21G', + '\x1B[3G', '\x1B[0J', + '{key : {key2 :[] }}\nbck-i-search: }_', '\x1B[1A', '\x1B[20G', + '\x1B[3G', '\x1B[0J', + 'fu', + '\r\n', + '\x1B[1G', '\x1B[0J', + prompt, '\x1B[3G', + '1', '+', '1', '\n// 2', '\x1B[1C\x1B[1A', + '\x1B[1B', '\x1B[2K', '\x1B[1A', + '\r\n', + '2\n', + '\x1B[1G', '\x1B[0J', + prompt, '\x1B[3G', + '\nbck-i-search: _', '\x1B[1A', + '\x1B[3G', '\x1B[0J', + '1+1\nbck-i-search: +_', '\x1B[1A', '\x1B[4G', + '\x1B[3G', '\x1B[0J', + '1+1', '\x1B[4G', + '\x1B[2C', + '\r\n', + '2\n', + '\x1B[1G', '\x1B[0J', + prompt, '\x1B[3G', + '2', '\n// 2', '\x1B[1D\x1B[1A', + '\x1B[1B', '\x1B[2K', '\x1B[1A', + '\nbck-i-search: _', '\x1B[1A', '\x1B[4G', + '\x1B[3G', '\x1B[0J', + 'Array(100).fill(1)\nbck-i-search: r_', '\x1B[1A', '\x1B[5G', + '\x1B[3G', '\x1B[0J', + 'repl.repl.historyIndex\nbck-i-search: re_', '\x1B[1A', '\x1B[8G', + '\x1B[3G', '\x1B[0J', + 'repl.repl.historyIndex', '\x1B[8G', + '\x1B[1G', '\x1B[0J', + `${prompt}ab = "aaaa"`, '\x1B[14G', + '\x1B[1G', '\x1B[0J', + `${prompt}repl.repl.historyIndex`, '\x1B[25G', '\n// -1', + '\x1B[19C\x1B[1A', + '\x1B[1B', '\x1B[2K', '\x1B[1A', + '\nfwd-i-search: _', '\x1B[1A', '\x1B[25G', + '\x1B[3G', '\x1B[0J', + 'repl.repl.historyIndex', + '\r\n', + '-1\n', + '\x1B[1G', '\x1B[0J', + prompt, '\x1B[3G', + '\r\n' + ], + clean: false + } +]; +const numtests = tests.length; + +const runTestWrap = common.mustCall(runTest, numtests); + +function cleanupTmpFile() { + try { + // Write over the file, clearing any history + fs.writeFileSync(defaultHistoryPath, ''); + } catch (err) { + if (err.code === 'ENOENT') return true; + throw err; + } + return true; +} + +function runTest() { + const opts = tests.shift(); + if (!opts) return; // All done + + const { expected, skip } = opts; + + // Test unsupported on platform. + if (skip) { + setImmediate(runTestWrap, true); + return; + } + + const lastChunks = []; + let i = 0; + + REPL.createInternalRepl(opts.env, { + input: new ActionStream(), + output: new stream.Writable({ + write(chunk, _, next) { + const output = chunk.toString(); + + if (!opts.showEscapeCodes && + (output[0] === '\x1B' || /^[\r\n]+$/.test(output))) { + return next(); + } + + lastChunks.push(output); + + if (expected.length) { + try { + if (!opts.checkTotal) + assert.strictEqual(output, expected[i]); + } catch (e) { + console.error(`Failed test # ${numtests - tests.length}`); + console.error('Last outputs: ' + inspect(lastChunks, { + breakLength: 5, colors: true + })); + throw e; + } + i++; + } + + next(); + } + }), + completer: opts.completer, + prompt, + useColors: opts.useColors || false, + terminal: true + }, function(err, repl) { + if (err) { + console.error(`Failed test # ${numtests - tests.length}`); + throw err; + } + + repl.once('close', () => { + if (opts.clean) + cleanupTmpFile(); + + if (opts.checkTotal) { + assert.deepStrictEqual(lastChunks, expected); + } else if (expected.length !== 0) { + throw new Error(`Failed test # ${numtests - tests.length}`); + } + + setImmediate(runTestWrap, true); + }); + + if (opts.columns) { + Object.defineProperty(repl, 'columns', { + value: opts.columns, + enumerable: true + }); + } + repl.inputStream.run(opts.test); + }); +} + +// run the tests +runTest(); diff --git a/test/pseudo-tty/repl-dumb-tty.js b/test/pseudo-tty/repl-dumb-tty.js index 1a3a24299821fe..8c9b93a9f31ccd 100644 --- a/test/pseudo-tty/repl-dumb-tty.js +++ b/test/pseudo-tty/repl-dumb-tty.js @@ -7,25 +7,30 @@ const repl = require('repl'); const ArrayStream = require('../common/arraystream'); repl.start('> '); -process.stdin.push('console.log("foo")\n'); -process.stdin.push('1 + 2\n'); +process.stdin.push('conso'); // No completion preview. +process.stdin.push('le.log("foo")\n'); +process.stdin.push('1 + 2'); // No input preview. +process.stdin.push('\n'); process.stdin.push('"str"\n'); process.stdin.push('console.dir({ a: 1 })\n'); process.stdin.push('{ a: 1 }\n'); process.stdin.push('\n'); process.stdin.push('.exit\n'); -// Verify Control+D support. +// Verify + D support. { const stream = new ArrayStream(); - const replServer = repl.start({ + const replServer = new repl.REPLServer({ prompt: '> ', terminal: true, input: stream, - output: stream, + output: process.stdout, useColors: false }); replServer.on('close', common.mustCall()); + // Verify that + R or + C does not trigger the reverse search. + replServer.write(null, { ctrl: true, name: 'r' }); + replServer.write(null, { ctrl: true, name: 's' }); replServer.write(null, { ctrl: true, name: 'd' }); } diff --git a/test/pseudo-tty/repl-dumb-tty.out b/test/pseudo-tty/repl-dumb-tty.out index 69eb4e5da6313e..3304faff0a4f4f 100644 --- a/test/pseudo-tty/repl-dumb-tty.out +++ b/test/pseudo-tty/repl-dumb-tty.out @@ -12,3 +12,4 @@ undefined { a: 1 } > > .exit +>