Skip to content

Commit

Permalink
readline: improve Unicode handling
Browse files Browse the repository at this point in the history
Prevents moving left or right from placing the cursor in between code
units comprising a code point.

PR-URL: #25723
Fixes: #25693
Reviewed-By: Anna Henningsen <anna@addaleax.net>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Ruben Bridgewater <ruben@bridgewater.de>
  • Loading branch information
Avi-D-coder authored and addaleax committed Mar 1, 2019
1 parent 5b8ac58 commit 0d660d9
Show file tree
Hide file tree
Showing 2 changed files with 198 additions and 10 deletions.
42 changes: 32 additions & 10 deletions lib/readline.js
Original file line number Diff line number Diff line change
Expand Up @@ -579,27 +579,48 @@ Interface.prototype._wordLeft = function() {
Interface.prototype._wordRight = function() {
if (this.cursor < this.line.length) {
var trailing = this.line.slice(this.cursor);
var match = trailing.match(/^(?:\s+|\W+|\w+)\s*/);
var match = trailing.match(/^(?:\s+|[^\w\s]+|\w+)\s*/);
this._moveCursor(match[0].length);
}
};

function charLengthLeft(str, i) {
if (i <= 0)
return 0;
if (i > 1 && str.codePointAt(i - 2) >= 2 ** 16 ||
str.codePointAt(i - 1) >= 2 ** 16) {
return 2;
}
return 1;
}

function charLengthAt(str, i) {
if (str.length <= i)
return 0;
return str.codePointAt(i) >= 2 ** 16 ? 2 : 1;
}

Interface.prototype._deleteLeft = function() {
if (this.cursor > 0 && this.line.length > 0) {
this.line = this.line.slice(0, this.cursor - 1) +
// The number of UTF-16 units comprising the character to the left
const charSize = charLengthLeft(this.line, this.cursor);
this.line = this.line.slice(0, this.cursor - charSize) +
this.line.slice(this.cursor, this.line.length);

this.cursor--;
this.cursor -= charSize;
this._refreshLine();
}
};


Interface.prototype._deleteRight = function() {
this.line = this.line.slice(0, this.cursor) +
this.line.slice(this.cursor + 1, this.line.length);
this._refreshLine();
if (this.cursor < this.line.length) {
// The number of UTF-16 units comprising the character to the left
const charSize = charLengthAt(this.line, this.cursor);
this.line = this.line.slice(0, this.cursor) +
this.line.slice(this.cursor + charSize, this.line.length);
this._refreshLine();
}
};


Expand Down Expand Up @@ -833,11 +854,11 @@ Interface.prototype._ttyWrite = function(s, key) {
break;

case 'b': // back one character
this._moveCursor(-1);
this._moveCursor(-charLengthLeft(this.line, this.cursor));
break;

case 'f': // forward one character
this._moveCursor(+1);
this._moveCursor(+charLengthAt(this.line, this.cursor));
break;

case 'l': // clear the whole screen
Expand Down Expand Up @@ -951,11 +972,12 @@ Interface.prototype._ttyWrite = function(s, key) {
break;

case 'left':
this._moveCursor(-1);
// obtain the code point to the left
this._moveCursor(-charLengthLeft(this.line, this.cursor));
break;

case 'right':
this._moveCursor(+1);
this._moveCursor(+charLengthAt(this.line, this.cursor));
break;

case 'home':
Expand Down
166 changes: 166 additions & 0 deletions test/parallel/test-readline-interface.js
Original file line number Diff line number Diff line change
Expand Up @@ -650,6 +650,115 @@ function isWarned(emitter) {
rli.close();
}

// Back and Forward one astral character
{
const fi = new FakeInput();
const rli = new readline.Interface({
input: fi,
output: fi,
prompt: '',
terminal: terminal
});
fi.emit('data', '💻');

// move left one character/code point
fi.emit('keypress', '.', { name: 'left' });
let cursorPos = rli._getCursorPos();
assert.strictEqual(cursorPos.rows, 0);
assert.strictEqual(cursorPos.cols, 0);

// move right one character/code point
fi.emit('keypress', '.', { name: 'right' });
cursorPos = rli._getCursorPos();
assert.strictEqual(cursorPos.rows, 0);
if (common.hasIntl) {
assert.strictEqual(cursorPos.cols, 2);
} else {
assert.strictEqual(cursorPos.cols, 1);
}

rli.on('line', common.mustCall((line) => {
assert.strictEqual(line, '💻');
}));
fi.emit('data', '\n');
rli.close();
}

// Two astral characters left
{
const fi = new FakeInput();
const rli = new readline.Interface({
input: fi,
output: fi,
prompt: '',
terminal: terminal
});
fi.emit('data', '💻');

// move left one character/code point
fi.emit('keypress', '.', { name: 'left' });
let cursorPos = rli._getCursorPos();
assert.strictEqual(cursorPos.rows, 0);
assert.strictEqual(cursorPos.cols, 0);

fi.emit('data', '🐕');
cursorPos = rli._getCursorPos();
assert.strictEqual(cursorPos.rows, 0);

if (common.hasIntl) {
assert.strictEqual(cursorPos.cols, 2);
} else {
assert.strictEqual(cursorPos.cols, 1);
// Fix cursor position without internationalization
fi.emit('keypress', '.', { name: 'left' });
}

rli.on('line', common.mustCall((line) => {
assert.strictEqual(line, '🐕💻');
}));
fi.emit('data', '\n');
rli.close();
}

// Two astral characters right
{
const fi = new FakeInput();
const rli = new readline.Interface({
input: fi,
output: fi,
prompt: '',
terminal: terminal
});
fi.emit('data', '💻');

// move left one character/code point
fi.emit('keypress', '.', { name: 'right' });
let cursorPos = rli._getCursorPos();
assert.strictEqual(cursorPos.rows, 0);
if (common.hasIntl) {
assert.strictEqual(cursorPos.cols, 2);
} else {
assert.strictEqual(cursorPos.cols, 1);
// Fix cursor position without internationalization
fi.emit('keypress', '.', { name: 'right' });
}

fi.emit('data', '🐕');
cursorPos = rli._getCursorPos();
assert.strictEqual(cursorPos.rows, 0);
if (common.hasIntl) {
assert.strictEqual(cursorPos.cols, 4);
} else {
assert.strictEqual(cursorPos.cols, 2);
}

rli.on('line', common.mustCall((line) => {
assert.strictEqual(line, '💻🐕');
}));
fi.emit('data', '\n');
rli.close();
}

{
// `wordLeft` and `wordRight`
const fi = new FakeInput();
Expand Down Expand Up @@ -791,6 +900,35 @@ function isWarned(emitter) {
rli.close();
}

// deleteLeft astral character
{
const fi = new FakeInput();
const rli = new readline.Interface({
input: fi,
output: fi,
prompt: '',
terminal: terminal
});
fi.emit('data', '💻');
let cursorPos = rli._getCursorPos();
assert.strictEqual(cursorPos.rows, 0);
if (common.hasIntl) {
assert.strictEqual(cursorPos.cols, 2);
} else {
assert.strictEqual(cursorPos.cols, 1);
}
// Delete left character
fi.emit('keypress', '.', { ctrl: true, name: 'h' });
cursorPos = rli._getCursorPos();
assert.strictEqual(cursorPos.rows, 0);
assert.strictEqual(cursorPos.cols, 0);
rli.on('line', common.mustCall((line) => {
assert.strictEqual(line, '');
}));
fi.emit('data', '\n');
rli.close();
}

// deleteRight
{
const fi = new FakeInput();
Expand Down Expand Up @@ -820,6 +958,34 @@ function isWarned(emitter) {
rli.close();
}

// deleteRight astral character
{
const fi = new FakeInput();
const rli = new readline.Interface({
input: fi,
output: fi,
prompt: '',
terminal: terminal
});
fi.emit('data', '💻');

// Go to the start of the line
fi.emit('keypress', '.', { ctrl: true, name: 'a' });
let cursorPos = rli._getCursorPos();
assert.strictEqual(cursorPos.rows, 0);
assert.strictEqual(cursorPos.cols, 0);

// Delete right character
fi.emit('keypress', '.', { ctrl: true, name: 'd' });
cursorPos = rli._getCursorPos();
assert.strictEqual(cursorPos.rows, 0);
assert.strictEqual(cursorPos.cols, 0);
rli.on('line', common.mustCall((line) => {
assert.strictEqual(line, '');
}));
fi.emit('data', '\n');
rli.close();
}

// deleteLineLeft
{
Expand Down

0 comments on commit 0d660d9

Please sign in to comment.