diff --git a/lib/internal/readline/callbacks.js b/lib/internal/readline/callbacks.js index 8ac7dd23439e2e..ae7cf0c07dda0b 100644 --- a/lib/internal/readline/callbacks.js +++ b/lib/internal/readline/callbacks.js @@ -5,13 +5,12 @@ const { } = primordials; const { - codes + codes: { + ERR_INVALID_ARG_VALUE, + ERR_INVALID_CURSOR_POS, + }, } = require('internal/errors'); -const { - ERR_INVALID_ARG_VALUE, - ERR_INVALID_CURSOR_POS, -} = codes; const { validateCallback, } = require('internal/validators'); @@ -20,10 +19,10 @@ const { } = require('internal/readline/utils'); const { + kClearLine, + kClearScreenDown, kClearToLineBeginning, kClearToLineEnd, - kClearLine, - kClearScreenDown } = CSI; diff --git a/lib/internal/readline/interface.js b/lib/internal/readline/interface.js index 19e0eecd7074db..7484bed50ca192 100644 --- a/lib/internal/readline/interface.js +++ b/lib/internal/readline/interface.js @@ -155,7 +155,7 @@ function InterfaceConstructor(input, output, completer, terminal) { ); } } - + if (signal) { validateAbortSignal(signal, 'options.signal'); } diff --git a/lib/internal/readline/promises.js b/lib/internal/readline/promises.js index b9ff638dc807d8..c58a1eb81024dc 100644 --- a/lib/internal/readline/promises.js +++ b/lib/internal/readline/promises.js @@ -28,9 +28,8 @@ const { /** - * moves the cursor to the x and y coordinate on the given stream + * Moves the cursor to the x and y coordinate on the given stream. */ - function cursorTo(stream, x, y = undefined) { if (NumberIsNaN(x)) return PromiseReject(new ERR_INVALID_ARG_VALUE('x', x)); if (NumberIsNaN(y)) return PromiseReject(new ERR_INVALID_ARG_VALUE('y', y)); @@ -46,9 +45,8 @@ function cursorTo(stream, x, y = undefined) { } /** - * moves the cursor relative to its current location + * Moves the cursor relative to its current location. */ - function moveCursor(stream, dx, dy) { if (stream == null || !(dx || dy)) { return PromiseResolve(); @@ -72,14 +70,13 @@ function moveCursor(stream, dx, dy) { } /** - * clears the current line the cursor is on: + * Clears the current line the cursor is on: * -1 for left of the cursor * +1 for right of the cursor * 0 for the entire line */ - function clearLine(stream, dir) { - if (stream === null || stream === undefined) { + if (stream == null) { return PromiseResolve(); } @@ -89,11 +86,10 @@ function clearLine(stream, dir) { } /** - * clears the screen from the current position of the cursor down + * Clears the screen from the current position of the cursor down. */ - function clearScreenDown(stream) { - if (stream === null || stream === undefined) { + if (stream == null) { return PromiseResolve(); } diff --git a/lib/readline.js b/lib/readline.js index b3e35b8ee41891..0fecc3ae48504b 100644 --- a/lib/readline.js +++ b/lib/readline.js @@ -34,6 +34,7 @@ const { ObjectDefineProperties, ObjectSetPrototypeOf, Promise, + PromiseReject, StringPrototypeSlice, } = primordials; @@ -124,16 +125,11 @@ Interface.prototype.question = function(query, options, cb) { if (options.signal) { if (options.signal.aborted) { - this[kQuestionCancel](); return; } - options.signal.addEventListener( - 'abort', - () => { - this[kQuestionCancel](); - }, - { once: true } - ); + options.signal.addEventListener('abort', () => { + this[kQuestionCancel](); + }, { once: true }); } if (typeof cb === 'function') { FunctionPrototypeCall(_Interface.prototype.question, this, @@ -381,7 +377,8 @@ function _ttyWriteDumb(s, key) { if (key.name === 'escape') return; - if (this[kSawReturnAt] && key.name !== 'enter') this[kSawReturnAt] = 0; + if (this[kSawReturnAt] && key.name !== 'enter') + this[kSawReturnAt] = 0; if (key.ctrl) { if (key.name === 'c') { @@ -400,17 +397,15 @@ function _ttyWriteDumb(s, key) { } switch (key.name) { - case 'return': // Carriage return, i.e. \r + case 'return': // Carriage return, i.e. \r this[kSawReturnAt] = DateNow(); this[kLine](); break; case 'enter': // When key interval > crlfDelay - if ( - this[kSawReturnAt] === 0 || - DateNow() - this[kSawReturnAt] > this.crlfDelay - ) { + if (this[kSawReturnAt] === 0 || + DateNow() - this[kSawReturnAt] > this.crlfDelay) { this[kLine](); } this[kSawReturnAt] = 0; diff --git a/lib/readline/promises.js b/lib/readline/promises.js index 5ccf9da58522df..17a4eaad74c34c 100644 --- a/lib/readline/promises.js +++ b/lib/readline/promises.js @@ -21,6 +21,10 @@ const { } = require('internal/errors'); class Interface extends _Interface { + // eslint-disable-next-line no-useless-constructor + constructor(input, output, completer, terminal) { + super(input, output, completer, terminal); + } question(query, options = {}) { return new Promise((resolve, reject) => { if (options.signal) { diff --git a/test/common/index.js b/test/common/index.js index f0d8c72d4a1017..83b0c0b7bb025c 100644 --- a/test/common/index.js +++ b/test/common/index.js @@ -387,10 +387,25 @@ function _mustCallInner(fn, criteria = 1, field) { mustCallChecks.push(context); - return function() { + const _return = function() { // eslint-disable-line func-style context.actual++; return fn.apply(this, arguments); }; + Object.defineProperties(_return, { + name: { + value: fn.name, + writable: false, + enumerable: false, + configurable: true, + }, + length: { + value: fn.length, + writable: false, + enumerable: false, + configurable: true, + }, + }); + return _return; } function hasMultiLocalhost() { diff --git a/test/parallel/test-readline-promises-tab-complete.js b/test/parallel/test-readline-promises-tab-complete.js new file mode 100644 index 00000000000000..e57cfe94337383 --- /dev/null +++ b/test/parallel/test-readline-promises-tab-complete.js @@ -0,0 +1,116 @@ +'use strict'; + +// Flags: --expose-internals + +const common = require('../common'); +const readline = require('readline/promises'); +const assert = require('assert'); +const { EventEmitter } = require('events'); +const { getStringWidth } = require('internal/util/inspect'); + +common.skipIfDumbTerminal(); + +// This test verifies that the tab completion supports unicode and the writes +// are limited to the minimum. +[ + 'あ', + '𐐷', + '🐕' +].forEach((char) => { + [true, false].forEach((lineBreak) => { + [ + (line) => [ + ['First group', '', + `${char}${'a'.repeat(10)}`, + `${char}${'b'.repeat(10)}`, + char.repeat(11), + ], + line + ], + + async (line) => [ + ['First group', '', + `${char}${'a'.repeat(10)}`, + `${char}${'b'.repeat(10)}`, + char.repeat(11), + ], + line + ], + ].forEach((completer) => { + + let output = ''; + const width = getStringWidth(char) - 1; + + class FakeInput extends EventEmitter { + columns = ((width + 1) * 10 + (lineBreak ? 0 : 10)) * 3 + + write = common.mustCall((data) => { + output += data; + }, 6) + + resume() {} + pause() {} + end() {} + } + + const fi = new FakeInput(); + const rli = new readline.Interface({ + input: fi, + output: fi, + terminal: true, + completer: common.mustCallAtLeast(completer), + }); + + const last = '\r\nFirst group\r\n\r\n' + + `${char}${'a'.repeat(10)}${' '.repeat(2 + width * 10)}` + + `${char}${'b'.repeat(10)}` + + (lineBreak ? '\r\n' : ' '.repeat(2 + width * 10)) + + `${char.repeat(11)}\r\n` + + `\r\n\u001b[1G\u001b[0J> ${char}\u001b[${4 + width}G`; + + const expectations = [char, '', last]; + + rli.on('line', common.mustNotCall()); + for (const character of `${char}\t\t`) { + fi.emit('data', character); + queueMicrotask(() => { + assert.strictEqual(output, expectations.shift()); + output = ''; + }); + } + rli.close(); + }); + }); +}); + +{ + let output = ''; + class FakeInput extends EventEmitter { + columns = 80 + + write = common.mustCall((data) => { + output += data; + }, 1) + + resume() {} + pause() {} + end() {} + } + + const fi = new FakeInput(); + const rli = new readline.Interface({ + input: fi, + output: fi, + terminal: true, + completer: + common.mustCallAtLeast(() => Promise.reject(new Error('message'))), + }); + + rli.on('line', common.mustNotCall()); + fi.emit('data', '\t'); + queueMicrotask(() => { + assert.match(output, /^Tab completion error: Error: message/); + output = ''; + }); + rli.close(); +} diff --git a/test/parallel/test-readline-tab-complete.js b/test/parallel/test-readline-tab-complete.js index 7ccdef116492b7..c283d446f9af28 100644 --- a/test/parallel/test-readline-tab-complete.js +++ b/test/parallel/test-readline-tab-complete.js @@ -31,15 +31,15 @@ common.skipIfDumbTerminal(); const width = getStringWidth(char) - 1; class FakeInput extends EventEmitter { - columns = ((width + 1) * 10 + (lineBreak ? 0 : 10)) * 3 + columns = ((width + 1) * 10 + (lineBreak ? 0 : 10)) * 3 - write = common.mustCall((data) => { - output += data; - }, 6) + write = common.mustCall((data) => { + output += data; + }, 6) - resume() {} - pause() {} - end() {} + resume() {} + pause() {} + end() {} } const fi = new FakeInput(); @@ -47,7 +47,7 @@ common.skipIfDumbTerminal(); input: fi, output: fi, terminal: true, - completer: completer + completer: common.mustCallAtLeast(completer), }); const last = '\r\nFirst group\r\n\r\n' + @@ -68,3 +68,35 @@ common.skipIfDumbTerminal(); rli.close(); }); }); + +{ + let output = ''; + class FakeInput extends EventEmitter { + columns = 80 + + write = common.mustCall((data) => { + output += data; + }, 1) + + resume() {} + pause() {} + end() {} + } + + const fi = new FakeInput(); + const rli = new readline.Interface({ + input: fi, + output: fi, + terminal: true, + completer: + common.mustCallAtLeast((_, cb) => cb(new Error('message'))), + }); + + rli.on('line', common.mustNotCall()); + fi.emit('data', '\t'); + queueMicrotask(() => { + assert.match(output, /^Tab completion error: Error: message/); + output = ''; + }); + rli.close(); +}