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: add readline.promises #28407

Closed
wants to merge 8 commits into from
Closed
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
472 changes: 472 additions & 0 deletions doc/api/readline.md

Large diffs are not rendered by default.

1,120 changes: 1,120 additions & 0 deletions lib/internal/readline/interface.js

Large diffs are not rendered by default.

72 changes: 72 additions & 0 deletions lib/internal/readline/promises.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
'use strict';
const { Object } = primordials;
const EventEmitter = require('events');
const {
Interface: CallbackInterface,
initializeInterface,
onTabComplete,
normalizeInterfaceArguments
} = require('internal/readline/interface');


class Interface extends EventEmitter {
constructor(input, output, completer, terminal) {
super();
const opts = normalizeInterfaceArguments(
input, output, completer, terminal);
initializeInterface(this, opts);
}

question(query) {
let resolve;
const promise = new Promise((res) => {
resolve = res;
});

if (!this._questionCallback) {
this._oldPrompt = this._prompt;
this.setPrompt(query);
this._questionCallback = resolve;
}

this.prompt();
return promise;
}

async _tabComplete(lastKeypressWasTab) {
this.pause();

try {
const line = this.line.slice(0, this.cursor);
const results = await this.completer(line);
onTabComplete(null, results, this, lastKeypressWasTab);
} catch (err) {
onTabComplete(err, null, this, lastKeypressWasTab);
}
}
}

// Copy the rest of the callback interface over to this interface.
Object.keys(CallbackInterface.prototype).forEach((keyName) => {
if (Interface.prototype[keyName] === undefined)
Interface.prototype[keyName] = CallbackInterface.prototype[keyName];
});

Object.defineProperty(Interface.prototype, 'columns', {
configurable: true,
enumerable: true,
get() {
return this.output && this.output.columns ? this.output.columns : Infinity;
}
});

Interface.prototype[Symbol.asyncIterator] =
CallbackInterface.prototype[Symbol.asyncIterator];


function createInterface(input, output, completer, terminal) {
return new Interface(input, output, completer, terminal);
}


module.exports = { createInterface, Interface };
163 changes: 156 additions & 7 deletions lib/internal/readline.js → lib/internal/readline/utils.js
Original file line number Diff line number Diff line change
@@ -1,22 +1,32 @@
'use strict';

// Regex used for ansi escape code splitting
const { ERR_INVALID_CURSOR_POS } = require('internal/errors').codes;
const { clearTimeout, setTimeout } = require('timers');

// Regex used for ansi escape code splitting.
// Adopted from https://github.com/chalk/ansi-regex/blob/master/index.js
// License: MIT, authors: @sindresorhus, Qix-, and arjunmehta
// Matches all ansi escape code sequences in a string
// Matches all ansi escape code sequences in a string.
/* eslint-disable no-control-regex */
const ansi =
/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g;
/* eslint-enable no-control-regex */

// GNU readline library - keyseq-timeout is 500ms (default)
const kEscapeCodeTimeout = 500;

const kKeypressDecoder = Symbol('keypress-decoder');
const kEscapeDecoder = Symbol('escape-decoder');
const kUTF16SurrogateThreshold = 0x10000; // 2 ** 16
const kEscape = '\x1b';

let StringDecoder; // Lazy loaded.
let getStringWidth;
let isFullWidthCodePoint;

function CSI(strings, ...args) {
let ret = `${kEscape}[`;
for (var n = 0; n < strings.length; n++) {
for (let n = 0; n < strings.length; n++) {
ret += strings[n];
if (n < args.length)
ret += args[n];
Expand Down Expand Up @@ -60,10 +70,10 @@ if (internalBinding('config').hasIntl) {

str = stripVTControlCharacters(String(str));

for (var i = 0; i < str.length; i++) {
for (let i = 0; i < str.length; i++) {
const code = str.codePointAt(i);

if (code >= 0x10000) { // surrogates
if (code >= kUTF16SurrogateThreshold) { // Surrogates.
i++;
}

Expand Down Expand Up @@ -430,10 +440,149 @@ function* emitKeys(stream) {
}
}


function moveCursor(stream, dx, dy) {
// Moves the cursor relative to its current location.
if (stream == null)
return;

if (dx < 0)
stream.write(CSI`${-dx}D`);
else if (dx > 0)
stream.write(CSI`${dx}C`);

if (dy < 0)
stream.write(CSI`${-dy}A`);
else if (dy > 0)
stream.write(CSI`${dy}B`);
}


function clearScreenDown(stream) {
// Clears the screen from the current position of the cursor down.
if (stream == null)
return;

stream.write(CSI.kClearScreenDown);
}


function clearLine(stream, dir) {
// 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.
if (stream == null)
return;

if (dir < 0)
stream.write(CSI.kClearToBeginning); // Clear to the beginning of the line.
else if (dir > 0)
stream.write(CSI.kClearToEnd); // Clear to the end of the line.
else
stream.write(CSI.kClearLine); // Clear the entire line.
}


function cursorTo(stream, x, y) {
// Moves the cursor to the x and y coordinate on the given stream.
if (stream == null)
return;

if (typeof x !== 'number' && typeof y !== 'number')
return;

if (typeof x !== 'number')
throw new ERR_INVALID_CURSOR_POS();

if (typeof y !== 'number')
stream.write(CSI`${x + 1}G`);
else
stream.write(CSI`${y + 1};${x + 1}H`);
}


function emitKeypressEvents(stream, iface) {
// Accepts a readable stream instance and makes it emit "keypress" events.
if (stream[kKeypressDecoder])
return;

if (StringDecoder === undefined)
StringDecoder = require('string_decoder').StringDecoder;

stream[kKeypressDecoder] = new StringDecoder('utf8');
stream[kEscapeDecoder] = emitKeys(stream);
stream[kEscapeDecoder].next();

const escapeCodeTimeout = () => stream[kEscapeDecoder].next('');
let timeoutId;

function onData(b) {
if (stream.listenerCount('keypress') > 0) {
const r = stream[kKeypressDecoder].write(b);

if (r) {
clearTimeout(timeoutId);

if (iface)
iface._sawKeyPress = r.length === 1;

for (let i = 0; i < r.length; i++) {
if (r[i] === '\t' && typeof r[i + 1] === 'string' && iface)
iface.isCompletionEnabled = false;

try {
stream[kEscapeDecoder].next(r[i]);
// Escape letter at the tail position.
if (r[i] === kEscape && i + 1 === r.length) {
timeoutId = setTimeout(
escapeCodeTimeout,
iface ? iface.escapeCodeTimeout : kEscapeCodeTimeout
);
}
} catch (err) {
// If the generator throws (it could happen in the `keypress`
// event), restart it.
stream[kEscapeDecoder] = emitKeys(stream);
stream[kEscapeDecoder].next();
throw err;
} finally {
if (iface)
iface.isCompletionEnabled = true;
}
}
}
} else {
// Nobody is watching anyway.
stream.removeListener('data', onData);
stream.on('newListener', onNewListener);
}
}

function onNewListener(event) {
if (event === 'keypress') {
stream.on('data', onData);
stream.removeListener('newListener', onNewListener);
}
}

if (stream.listenerCount('keypress') > 0)
stream.on('data', onData);
else
stream.on('newListener', onNewListener);
}


module.exports = {
emitKeys,
clearLine,
clearScreenDown,
cursorTo,
emitKeypressEvents,
getStringWidth,
isFullWidthCodePoint,
kEscapeCodeTimeout,
kUTF16SurrogateThreshold,
moveCursor,
stripVTControlCharacters,
CSI
CSI // CSI is only exported for testing purposes.
};
Loading