Skip to content

Commit 760d048

Browse files
committed
fixup! readline: add Promise-based API
1 parent ff61371 commit 760d048

8 files changed

Lines changed: 198 additions & 41 deletions

File tree

lib/internal/readline/callbacks.js

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,12 @@ const {
55
} = primordials;
66

77
const {
8-
codes
8+
codes: {
9+
ERR_INVALID_ARG_VALUE,
10+
ERR_INVALID_CURSOR_POS,
11+
},
912
} = require('internal/errors');
1013

11-
const {
12-
ERR_INVALID_ARG_VALUE,
13-
ERR_INVALID_CURSOR_POS,
14-
} = codes;
1514
const {
1615
validateCallback,
1716
} = require('internal/validators');
@@ -20,10 +19,10 @@ const {
2019
} = require('internal/readline/utils');
2120

2221
const {
22+
kClearLine,
23+
kClearScreenDown,
2324
kClearToLineBeginning,
2425
kClearToLineEnd,
25-
kClearLine,
26-
kClearScreenDown
2726
} = CSI;
2827

2928

lib/internal/readline/interface.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ function InterfaceConstructor(input, output, completer, terminal) {
155155
);
156156
}
157157
}
158-
158+
159159
if (signal) {
160160
validateAbortSignal(signal, 'options.signal');
161161
}

lib/internal/readline/promises.js

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,8 @@ const {
2828

2929

3030
/**
31-
* moves the cursor to the x and y coordinate on the given stream
31+
* Moves the cursor to the x and y coordinate on the given stream.
3232
*/
33-
3433
function cursorTo(stream, x, y = undefined) {
3534
if (NumberIsNaN(x)) return PromiseReject(new ERR_INVALID_ARG_VALUE('x', x));
3635
if (NumberIsNaN(y)) return PromiseReject(new ERR_INVALID_ARG_VALUE('y', y));
@@ -46,9 +45,8 @@ function cursorTo(stream, x, y = undefined) {
4645
}
4746

4847
/**
49-
* moves the cursor relative to its current location
48+
* Moves the cursor relative to its current location.
5049
*/
51-
5250
function moveCursor(stream, dx, dy) {
5351
if (stream == null || !(dx || dy)) {
5452
return PromiseResolve();
@@ -72,14 +70,13 @@ function moveCursor(stream, dx, dy) {
7270
}
7371

7472
/**
75-
* clears the current line the cursor is on:
73+
* Clears the current line the cursor is on:
7674
* -1 for left of the cursor
7775
* +1 for right of the cursor
7876
* 0 for the entire line
7977
*/
80-
8178
function clearLine(stream, dir) {
82-
if (stream === null || stream === undefined) {
79+
if (stream == null) {
8380
return PromiseResolve();
8481
}
8582

@@ -89,11 +86,10 @@ function clearLine(stream, dir) {
8986
}
9087

9188
/**
92-
* clears the screen from the current position of the cursor down
89+
* Clears the screen from the current position of the cursor down.
9390
*/
94-
9591
function clearScreenDown(stream) {
96-
if (stream === null || stream === undefined) {
92+
if (stream == null) {
9793
return PromiseResolve();
9894
}
9995

lib/readline.js

Lines changed: 9 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ const {
3434
ObjectDefineProperties,
3535
ObjectSetPrototypeOf,
3636
Promise,
37+
PromiseReject,
3738
StringPrototypeSlice,
3839
} = primordials;
3940

@@ -124,16 +125,11 @@ Interface.prototype.question = function(query, options, cb) {
124125

125126
if (options.signal) {
126127
if (options.signal.aborted) {
127-
this[kQuestionCancel]();
128128
return;
129129
}
130-
options.signal.addEventListener(
131-
'abort',
132-
() => {
133-
this[kQuestionCancel]();
134-
},
135-
{ once: true }
136-
);
130+
options.signal.addEventListener('abort', () => {
131+
this[kQuestionCancel]();
132+
}, { once: true });
137133
}
138134
if (typeof cb === 'function') {
139135
FunctionPrototypeCall(_Interface.prototype.question, this,
@@ -381,7 +377,8 @@ function _ttyWriteDumb(s, key) {
381377

382378
if (key.name === 'escape') return;
383379

384-
if (this[kSawReturnAt] && key.name !== 'enter') this[kSawReturnAt] = 0;
380+
if (this[kSawReturnAt] && key.name !== 'enter')
381+
this[kSawReturnAt] = 0;
385382

386383
if (key.ctrl) {
387384
if (key.name === 'c') {
@@ -400,17 +397,15 @@ function _ttyWriteDumb(s, key) {
400397
}
401398

402399
switch (key.name) {
403-
case 'return': // Carriage return, i.e. \r
400+
case 'return': // Carriage return, i.e. \r
404401
this[kSawReturnAt] = DateNow();
405402
this[kLine]();
406403
break;
407404

408405
case 'enter':
409406
// When key interval > crlfDelay
410-
if (
411-
this[kSawReturnAt] === 0 ||
412-
DateNow() - this[kSawReturnAt] > this.crlfDelay
413-
) {
407+
if (this[kSawReturnAt] === 0 ||
408+
DateNow() - this[kSawReturnAt] > this.crlfDelay) {
414409
this[kLine]();
415410
}
416411
this[kSawReturnAt] = 0;

lib/readline/promises.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@ const {
2121
} = require('internal/errors');
2222

2323
class Interface extends _Interface {
24+
// eslint-disable-next-line no-useless-constructor
25+
constructor(input, output, completer, terminal) {
26+
super(input, output, completer, terminal);
27+
}
2428
question(query, options = {}) {
2529
return new Promise((resolve, reject) => {
2630
if (options.signal) {

test/common/index.js

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -387,10 +387,25 @@ function _mustCallInner(fn, criteria = 1, field) {
387387

388388
mustCallChecks.push(context);
389389

390-
return function() {
390+
const _return = function() { // eslint-disable-line func-style
391391
context.actual++;
392392
return fn.apply(this, arguments);
393393
};
394+
Object.defineProperties(_return, {
395+
name: {
396+
value: fn.name,
397+
writable: false,
398+
enumerable: false,
399+
configurable: true,
400+
},
401+
length: {
402+
value: fn.length,
403+
writable: false,
404+
enumerable: false,
405+
configurable: true,
406+
},
407+
});
408+
return _return;
394409
}
395410

396411
function hasMultiLocalhost() {
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
'use strict';
2+
3+
// Flags: --expose-internals
4+
5+
const common = require('../common');
6+
const readline = require('readline/promises');
7+
const assert = require('assert');
8+
const { EventEmitter } = require('events');
9+
const { getStringWidth } = require('internal/util/inspect');
10+
11+
common.skipIfDumbTerminal();
12+
13+
// This test verifies that the tab completion supports unicode and the writes
14+
// are limited to the minimum.
15+
[
16+
'あ',
17+
'𐐷',
18+
'🐕'
19+
].forEach((char) => {
20+
[true, false].forEach((lineBreak) => {
21+
[
22+
(line) => [
23+
['First group', '',
24+
`${char}${'a'.repeat(10)}`,
25+
`${char}${'b'.repeat(10)}`,
26+
char.repeat(11),
27+
],
28+
line
29+
],
30+
31+
async (line) => [
32+
['First group', '',
33+
`${char}${'a'.repeat(10)}`,
34+
`${char}${'b'.repeat(10)}`,
35+
char.repeat(11),
36+
],
37+
line
38+
],
39+
].forEach((completer) => {
40+
41+
let output = '';
42+
const width = getStringWidth(char) - 1;
43+
44+
class FakeInput extends EventEmitter {
45+
columns = ((width + 1) * 10 + (lineBreak ? 0 : 10)) * 3
46+
47+
write = common.mustCall((data) => {
48+
output += data;
49+
}, 6)
50+
51+
resume() {}
52+
pause() {}
53+
end() {}
54+
}
55+
56+
const fi = new FakeInput();
57+
const rli = new readline.Interface({
58+
input: fi,
59+
output: fi,
60+
terminal: true,
61+
completer: common.mustCallAtLeast(completer),
62+
});
63+
64+
const last = '\r\nFirst group\r\n\r\n' +
65+
`${char}${'a'.repeat(10)}${' '.repeat(2 + width * 10)}` +
66+
`${char}${'b'.repeat(10)}` +
67+
(lineBreak ? '\r\n' : ' '.repeat(2 + width * 10)) +
68+
`${char.repeat(11)}\r\n` +
69+
`\r\n\u001b[1G\u001b[0J> ${char}\u001b[${4 + width}G`;
70+
71+
const expectations = [char, '', last];
72+
73+
rli.on('line', common.mustNotCall());
74+
for (const character of `${char}\t\t`) {
75+
fi.emit('data', character);
76+
queueMicrotask(() => {
77+
assert.strictEqual(output, expectations.shift());
78+
output = '';
79+
});
80+
}
81+
rli.close();
82+
});
83+
});
84+
});
85+
86+
{
87+
let output = '';
88+
class FakeInput extends EventEmitter {
89+
columns = 80
90+
91+
write = common.mustCall((data) => {
92+
output += data;
93+
}, 1)
94+
95+
resume() {}
96+
pause() {}
97+
end() {}
98+
}
99+
100+
const fi = new FakeInput();
101+
const rli = new readline.Interface({
102+
input: fi,
103+
output: fi,
104+
terminal: true,
105+
completer:
106+
common.mustCallAtLeast(() => Promise.reject(new Error('message'))),
107+
});
108+
109+
rli.on('line', common.mustNotCall());
110+
fi.emit('data', '\t');
111+
queueMicrotask(() => {
112+
assert.match(output, /^Tab completion error: Error: message/);
113+
output = '';
114+
});
115+
rli.close();
116+
}

test/parallel/test-readline-tab-complete.js

Lines changed: 40 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -31,23 +31,23 @@ common.skipIfDumbTerminal();
3131
const width = getStringWidth(char) - 1;
3232

3333
class FakeInput extends EventEmitter {
34-
columns = ((width + 1) * 10 + (lineBreak ? 0 : 10)) * 3
34+
columns = ((width + 1) * 10 + (lineBreak ? 0 : 10)) * 3
3535

36-
write = common.mustCall((data) => {
37-
output += data;
38-
}, 6)
36+
write = common.mustCall((data) => {
37+
output += data;
38+
}, 6)
3939

40-
resume() {}
41-
pause() {}
42-
end() {}
40+
resume() {}
41+
pause() {}
42+
end() {}
4343
}
4444

4545
const fi = new FakeInput();
4646
const rli = new readline.Interface({
4747
input: fi,
4848
output: fi,
4949
terminal: true,
50-
completer: completer
50+
completer: common.mustCallAtLeast(completer),
5151
});
5252

5353
const last = '\r\nFirst group\r\n\r\n' +
@@ -68,3 +68,35 @@ common.skipIfDumbTerminal();
6868
rli.close();
6969
});
7070
});
71+
72+
{
73+
let output = '';
74+
class FakeInput extends EventEmitter {
75+
columns = 80
76+
77+
write = common.mustCall((data) => {
78+
output += data;
79+
}, 1)
80+
81+
resume() {}
82+
pause() {}
83+
end() {}
84+
}
85+
86+
const fi = new FakeInput();
87+
const rli = new readline.Interface({
88+
input: fi,
89+
output: fi,
90+
terminal: true,
91+
completer:
92+
common.mustCallAtLeast((_, cb) => cb(new Error('message'))),
93+
});
94+
95+
rli.on('line', common.mustNotCall());
96+
fi.emit('data', '\t');
97+
queueMicrotask(() => {
98+
assert.match(output, /^Tab completion error: Error: message/);
99+
output = '';
100+
});
101+
rli.close();
102+
}

0 commit comments

Comments
 (0)