Skip to content

Commit 0ec2f65

Browse files
committed
test: add multiline prompt completion regression for readline
1 parent e28656a commit 0ec2f65

File tree

1 file changed

+169
-0
lines changed

1 file changed

+169
-0
lines changed

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

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,3 +138,172 @@ if (process.env.TERM === 'dumb') {
138138
}));
139139
}));
140140
}
141+
142+
{
143+
class VirtualScreen {
144+
constructor() {
145+
this.rows = [[]];
146+
this.row = 0;
147+
this.col = 0;
148+
}
149+
150+
ensureRow(row) {
151+
while (this.rows.length <= row) this.rows.push([]);
152+
}
153+
154+
setChar(row, col, ch) {
155+
this.ensureRow(row);
156+
const target = this.rows[row];
157+
while (target.length <= col) target.push(' ');
158+
target[col] = ch;
159+
}
160+
161+
clearLineRight() {
162+
this.ensureRow(this.row);
163+
const target = this.rows[this.row];
164+
if (this.col < target.length) {
165+
target.length = this.col;
166+
}
167+
}
168+
169+
clearFromCursor() {
170+
this.clearLineRight();
171+
if (this.row + 1 < this.rows.length) {
172+
this.rows.length = this.row + 1;
173+
}
174+
}
175+
176+
moveCursor(dx, dy) {
177+
this.row = Math.max(0, this.row + dy);
178+
this.ensureRow(this.row);
179+
this.col = Math.max(0, this.col + dx);
180+
}
181+
182+
handleEscape(params, code) {
183+
switch (code) {
184+
case 'A': // Cursor Up
185+
this.moveCursor(0, -(Number(params) || 1));
186+
break;
187+
case 'B': // Cursor Down
188+
this.moveCursor(0, Number(params) || 1);
189+
break;
190+
case 'C': // Cursor Forward
191+
this.moveCursor(Number(params) || 1, 0);
192+
break;
193+
case 'D': // Cursor Backward
194+
this.moveCursor(-(Number(params) || 1), 0);
195+
break;
196+
case 'G': // Cursor Horizontal Absolute
197+
this.col = Math.max(0, (Number(params) || 1) - 1);
198+
break;
199+
case 'H':
200+
case 'f': { // Cursor Position
201+
const [row, col] = params.split(';').map((n) => Number(n) || 1);
202+
this.row = Math.max(0, row - 1);
203+
this.col = Math.max(0, (col ?? 1) - 1);
204+
this.ensureRow(this.row);
205+
break;
206+
}
207+
case 'J':
208+
this.clearFromCursor();
209+
break;
210+
case 'K':
211+
this.clearLineRight();
212+
break;
213+
default:
214+
break;
215+
}
216+
}
217+
218+
write(chunk) {
219+
for (let i = 0; i < chunk.length; i++) {
220+
const ch = chunk[i];
221+
if (ch === '\r') {
222+
this.col = 0;
223+
continue;
224+
}
225+
if (ch === '\n') {
226+
this.row++;
227+
this.col = 0;
228+
this.ensureRow(this.row);
229+
continue;
230+
}
231+
if (ch === '\u001b' && chunk[i + 1] === '[') {
232+
const match = /^\u001b\[([0-9;]*)([A-Za-z])/.exec(chunk.slice(i));
233+
if (match) {
234+
this.handleEscape(match[1], match[2]);
235+
i += match[0].length - 1;
236+
continue;
237+
}
238+
}
239+
this.setChar(this.row, this.col, ch);
240+
this.col++;
241+
}
242+
}
243+
244+
getLines() {
245+
return this.rows.map((row) => row.join('').trimEnd());
246+
}
247+
}
248+
249+
class FakeTTY extends EventEmitter {
250+
columns = 80;
251+
rows = 24;
252+
isTTY = true;
253+
254+
constructor(screen) {
255+
super();
256+
this.screen = screen;
257+
}
258+
259+
write(data) {
260+
this.screen.write(data);
261+
return true;
262+
}
263+
264+
resume() {}
265+
266+
pause() {}
267+
268+
end() {}
269+
270+
setRawMode(mode) {
271+
this.isRaw = mode;
272+
}
273+
}
274+
275+
const screen = new VirtualScreen();
276+
const fi = new FakeTTY(screen);
277+
278+
const rli = new readline.Interface({
279+
input: fi,
280+
output: fi,
281+
terminal: true,
282+
completer: (line) => [['foobar', 'foobaz'], line],
283+
});
284+
285+
const promptLines = ['multiline', 'prompt', 'eats', 'output', '> '];
286+
rli.setPrompt(promptLines.join('\n'));
287+
rli.prompt();
288+
289+
['f', 'o', 'o', '\t', '\t'].forEach((ch) => fi.emit('data', ch));
290+
291+
const display = screen.getLines();
292+
293+
assert.strictEqual(display[0], 'multiline');
294+
assert.strictEqual(display[1], 'prompt');
295+
assert.strictEqual(display[2], 'eats');
296+
assert.strictEqual(display[3], 'output');
297+
298+
const inputLineIndex = 4;
299+
assert.ok(
300+
display[inputLineIndex].includes('> fooba'),
301+
'prompt line should keep completed input',
302+
);
303+
304+
const completionLineExists =
305+
display.some((l) => l.includes('foobar') && l.includes('foobaz'));
306+
assert.ok(completionLineExists, 'completion list should be visible');
307+
308+
rli.close();
309+
}

0 commit comments

Comments
 (0)