readline: improve Unicode handling
Prevents moving left or right from placing the cursor in between code units comprising a code point. PR-URL: https://github.com/nodejs/node/pull/25723 Fixes: https://github.com/nodejs/node/issues/25693 Reviewed-By: Anna Henningsen <anna@addaleax.net> Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Ruben Bridgewater <ruben@bridgewater.de>
This commit is contained in:
parent
0fd5b458be
commit
fedc31bb3c
@ -580,27 +580,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() {
|
||||
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 + 1, this.line.length);
|
||||
this.line.slice(this.cursor + charSize, this.line.length);
|
||||
this._refreshLine();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@ -834,11 +855,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
|
||||
@ -952,11 +973,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':
|
||||
|
@ -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();
|
||||
@ -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();
|
||||
@ -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
|
||||
{
|
||||
|
Loading…
x
Reference in New Issue
Block a user