Skip to content
Open
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
3 changes: 3 additions & 0 deletions lib/internal/readline/interface.js
Original file line number Diff line number Diff line change
Expand Up @@ -780,6 +780,9 @@ class Interface extends InterfaceConstructor {
output += '\r\n\r\n';
}
this[kWriteToOutput](output);
if (StringPrototypeIncludes(this[kPrompt], '\n') || this[kIsMultiline]) {
this.prevRows = 0;
}
this[kRefreshLine]();
}

Expand Down
177 changes: 177 additions & 0 deletions test/parallel/test-readline-tab-complete.js
Original file line number Diff line number Diff line change
Expand Up @@ -138,3 +138,180 @@ if (process.env.TERM === 'dumb') {
}));
}));
}

{
class VirtualScreen {
constructor() {
this.rows = [[]];
this.row = 0;
this.col = 0;
}

ensureRow(row) {
while (this.rows.length <= row) this.rows.push([]);
}

setChar(row, col, ch) {
this.ensureRow(row);
const target = this.rows[row];
while (target.length <= col) target.push(' ');
target[col] = ch;
}

clearLineRight() {
this.ensureRow(this.row);
const target = this.rows[this.row];
if (this.col < target.length) {
target.length = this.col;
}
}

clearFromCursor() {
this.clearLineRight();
if (this.row + 1 < this.rows.length) {
this.rows.length = this.row + 1;
}
}

moveCursor(dx, dy) {
this.row = Math.max(0, this.row + dy);
this.ensureRow(this.row);
this.col = Math.max(0, this.col + dx);
}

handleEscape(params, code) {
switch (code) {
case 'A': // Cursor Up
this.moveCursor(0, -(Number(params) || 1));
break;
case 'B': // Cursor Down
this.moveCursor(0, Number(params) || 1);
break;
case 'C': // Cursor Forward
this.moveCursor(Number(params) || 1, 0);
break;
case 'D': // Cursor Backward
this.moveCursor(-(Number(params) || 1), 0);
break;
case 'G': // Cursor Horizontal Absolute
this.col = Math.max(0, (Number(params) || 1) - 1);
break;
case 'H':
case 'f': { // Cursor Position
const [row, col] = params.split(';').map((n) => Number(n) || 1);
this.row = Math.max(0, row - 1);
this.col = Math.max(0, (col ?? 1) - 1);
this.ensureRow(this.row);
break;
}
case 'J':
this.clearFromCursor();
break;
case 'K':
this.clearLineRight();
break;
default:
break;
}
}

write(chunk) {
for (let i = 0; i < chunk.length; i++) {
const ch = chunk[i];
if (ch === '\r') {
this.col = 0;
continue;
}
if (ch === '\n') {
this.row++;
this.col = 0;
this.ensureRow(this.row);
continue;
}
if (ch === '\u001b' && chunk[i + 1] === '[') {
let j = i + 2;
let params = '';
while (j < chunk.length) {
const code = chunk[j];
if ((code >= '0' && code <= '9') || code === ';') {
params += code;
j++;
continue;
}
this.handleEscape(params, code);
i = j;
break;
}
continue;
}
this.setChar(this.row, this.col, ch);
this.col++;
}
}

getLines() {
return this.rows.map((row) => row.join('').trimEnd());
}
}

class FakeTTY extends EventEmitter {
columns = 80;
rows = 24;
isTTY = true;

constructor(screen) {
super();
this.screen = screen;
}

write(data) {
this.screen.write(data);
return true;
}

resume() {}

pause() {}

end() {}

setRawMode(mode) {
this.isRaw = mode;
}
}

const screen = new VirtualScreen();
const fi = new FakeTTY(screen);

const rli = new readline.Interface({
input: fi,
output: fi,
terminal: true,
completer: (line) => [['foobar', 'foobaz'], line],
});

const promptLines = ['multiline', 'prompt', 'eats', 'output', '> '];
rli.setPrompt(promptLines.join('\n'));
rli.prompt();

['f', 'o', 'o', '\t', '\t'].forEach((ch) => fi.emit('data', ch));

const display = screen.getLines();

assert.strictEqual(display[0], 'multiline');
assert.strictEqual(display[1], 'prompt');
assert.strictEqual(display[2], 'eats');
assert.strictEqual(display[3], 'output');

const inputLineIndex = 4;
assert.ok(
display[inputLineIndex].includes('> fooba'),
'prompt line should keep completed input',
);

const completionLineExists =
display.some((l) => l.includes('foobar') && l.includes('foobaz'));
assert.ok(completionLineExists, 'completion list should be visible');

rli.close();
}
Loading