Skip to content

Commit 0d660d9

Browse files
Avi-D-coderaddaleax
authored andcommitted
readline: improve Unicode handling
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 <[email protected]> Reviewed-By: James M Snell <[email protected]> Reviewed-By: Ruben Bridgewater <[email protected]>
1 parent 5b8ac58 commit 0d660d9

File tree

2 files changed

+198
-10
lines changed

2 files changed

+198
-10
lines changed

lib/readline.js

+32-10
Original file line numberDiff line numberDiff line change
@@ -579,27 +579,48 @@ Interface.prototype._wordLeft = function() {
579579
Interface.prototype._wordRight = function() {
580580
if (this.cursor < this.line.length) {
581581
var trailing = this.line.slice(this.cursor);
582-
var match = trailing.match(/^(?:\s+|\W+|\w+)\s*/);
582+
var match = trailing.match(/^(?:\s+|[^\w\s]+|\w+)\s*/);
583583
this._moveCursor(match[0].length);
584584
}
585585
};
586586

587+
function charLengthLeft(str, i) {
588+
if (i <= 0)
589+
return 0;
590+
if (i > 1 && str.codePointAt(i - 2) >= 2 ** 16 ||
591+
str.codePointAt(i - 1) >= 2 ** 16) {
592+
return 2;
593+
}
594+
return 1;
595+
}
596+
597+
function charLengthAt(str, i) {
598+
if (str.length <= i)
599+
return 0;
600+
return str.codePointAt(i) >= 2 ** 16 ? 2 : 1;
601+
}
587602

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

593-
this.cursor--;
610+
this.cursor -= charSize;
594611
this._refreshLine();
595612
}
596613
};
597614

598615

599616
Interface.prototype._deleteRight = function() {
600-
this.line = this.line.slice(0, this.cursor) +
601-
this.line.slice(this.cursor + 1, this.line.length);
602-
this._refreshLine();
617+
if (this.cursor < this.line.length) {
618+
// The number of UTF-16 units comprising the character to the left
619+
const charSize = charLengthAt(this.line, this.cursor);
620+
this.line = this.line.slice(0, this.cursor) +
621+
this.line.slice(this.cursor + charSize, this.line.length);
622+
this._refreshLine();
623+
}
603624
};
604625

605626

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

835856
case 'b': // back one character
836-
this._moveCursor(-1);
857+
this._moveCursor(-charLengthLeft(this.line, this.cursor));
837858
break;
838859

839860
case 'f': // forward one character
840-
this._moveCursor(+1);
861+
this._moveCursor(+charLengthAt(this.line, this.cursor));
841862
break;
842863

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

953974
case 'left':
954-
this._moveCursor(-1);
975+
// obtain the code point to the left
976+
this._moveCursor(-charLengthLeft(this.line, this.cursor));
955977
break;
956978

957979
case 'right':
958-
this._moveCursor(+1);
980+
this._moveCursor(+charLengthAt(this.line, this.cursor));
959981
break;
960982

961983
case 'home':

test/parallel/test-readline-interface.js

+166
Original file line numberDiff line numberDiff line change
@@ -650,6 +650,115 @@ function isWarned(emitter) {
650650
rli.close();
651651
}
652652

653+
// Back and Forward one astral character
654+
{
655+
const fi = new FakeInput();
656+
const rli = new readline.Interface({
657+
input: fi,
658+
output: fi,
659+
prompt: '',
660+
terminal: terminal
661+
});
662+
fi.emit('data', '💻');
663+
664+
// move left one character/code point
665+
fi.emit('keypress', '.', { name: 'left' });
666+
let cursorPos = rli._getCursorPos();
667+
assert.strictEqual(cursorPos.rows, 0);
668+
assert.strictEqual(cursorPos.cols, 0);
669+
670+
// move right one character/code point
671+
fi.emit('keypress', '.', { name: 'right' });
672+
cursorPos = rli._getCursorPos();
673+
assert.strictEqual(cursorPos.rows, 0);
674+
if (common.hasIntl) {
675+
assert.strictEqual(cursorPos.cols, 2);
676+
} else {
677+
assert.strictEqual(cursorPos.cols, 1);
678+
}
679+
680+
rli.on('line', common.mustCall((line) => {
681+
assert.strictEqual(line, '💻');
682+
}));
683+
fi.emit('data', '\n');
684+
rli.close();
685+
}
686+
687+
// Two astral characters left
688+
{
689+
const fi = new FakeInput();
690+
const rli = new readline.Interface({
691+
input: fi,
692+
output: fi,
693+
prompt: '',
694+
terminal: terminal
695+
});
696+
fi.emit('data', '💻');
697+
698+
// move left one character/code point
699+
fi.emit('keypress', '.', { name: 'left' });
700+
let cursorPos = rli._getCursorPos();
701+
assert.strictEqual(cursorPos.rows, 0);
702+
assert.strictEqual(cursorPos.cols, 0);
703+
704+
fi.emit('data', '🐕');
705+
cursorPos = rli._getCursorPos();
706+
assert.strictEqual(cursorPos.rows, 0);
707+
708+
if (common.hasIntl) {
709+
assert.strictEqual(cursorPos.cols, 2);
710+
} else {
711+
assert.strictEqual(cursorPos.cols, 1);
712+
// Fix cursor position without internationalization
713+
fi.emit('keypress', '.', { name: 'left' });
714+
}
715+
716+
rli.on('line', common.mustCall((line) => {
717+
assert.strictEqual(line, '🐕💻');
718+
}));
719+
fi.emit('data', '\n');
720+
rli.close();
721+
}
722+
723+
// Two astral characters right
724+
{
725+
const fi = new FakeInput();
726+
const rli = new readline.Interface({
727+
input: fi,
728+
output: fi,
729+
prompt: '',
730+
terminal: terminal
731+
});
732+
fi.emit('data', '💻');
733+
734+
// move left one character/code point
735+
fi.emit('keypress', '.', { name: 'right' });
736+
let cursorPos = rli._getCursorPos();
737+
assert.strictEqual(cursorPos.rows, 0);
738+
if (common.hasIntl) {
739+
assert.strictEqual(cursorPos.cols, 2);
740+
} else {
741+
assert.strictEqual(cursorPos.cols, 1);
742+
// Fix cursor position without internationalization
743+
fi.emit('keypress', '.', { name: 'right' });
744+
}
745+
746+
fi.emit('data', '🐕');
747+
cursorPos = rli._getCursorPos();
748+
assert.strictEqual(cursorPos.rows, 0);
749+
if (common.hasIntl) {
750+
assert.strictEqual(cursorPos.cols, 4);
751+
} else {
752+
assert.strictEqual(cursorPos.cols, 2);
753+
}
754+
755+
rli.on('line', common.mustCall((line) => {
756+
assert.strictEqual(line, '💻🐕');
757+
}));
758+
fi.emit('data', '\n');
759+
rli.close();
760+
}
761+
653762
{
654763
// `wordLeft` and `wordRight`
655764
const fi = new FakeInput();
@@ -791,6 +900,35 @@ function isWarned(emitter) {
791900
rli.close();
792901
}
793902

903+
// deleteLeft astral character
904+
{
905+
const fi = new FakeInput();
906+
const rli = new readline.Interface({
907+
input: fi,
908+
output: fi,
909+
prompt: '',
910+
terminal: terminal
911+
});
912+
fi.emit('data', '💻');
913+
let cursorPos = rli._getCursorPos();
914+
assert.strictEqual(cursorPos.rows, 0);
915+
if (common.hasIntl) {
916+
assert.strictEqual(cursorPos.cols, 2);
917+
} else {
918+
assert.strictEqual(cursorPos.cols, 1);
919+
}
920+
// Delete left character
921+
fi.emit('keypress', '.', { ctrl: true, name: 'h' });
922+
cursorPos = rli._getCursorPos();
923+
assert.strictEqual(cursorPos.rows, 0);
924+
assert.strictEqual(cursorPos.cols, 0);
925+
rli.on('line', common.mustCall((line) => {
926+
assert.strictEqual(line, '');
927+
}));
928+
fi.emit('data', '\n');
929+
rli.close();
930+
}
931+
794932
// deleteRight
795933
{
796934
const fi = new FakeInput();
@@ -820,6 +958,34 @@ function isWarned(emitter) {
820958
rli.close();
821959
}
822960

961+
// deleteRight astral character
962+
{
963+
const fi = new FakeInput();
964+
const rli = new readline.Interface({
965+
input: fi,
966+
output: fi,
967+
prompt: '',
968+
terminal: terminal
969+
});
970+
fi.emit('data', '💻');
971+
972+
// Go to the start of the line
973+
fi.emit('keypress', '.', { ctrl: true, name: 'a' });
974+
let cursorPos = rli._getCursorPos();
975+
assert.strictEqual(cursorPos.rows, 0);
976+
assert.strictEqual(cursorPos.cols, 0);
977+
978+
// Delete right character
979+
fi.emit('keypress', '.', { ctrl: true, name: 'd' });
980+
cursorPos = rli._getCursorPos();
981+
assert.strictEqual(cursorPos.rows, 0);
982+
assert.strictEqual(cursorPos.cols, 0);
983+
rli.on('line', common.mustCall((line) => {
984+
assert.strictEqual(line, '');
985+
}));
986+
fi.emit('data', '\n');
987+
rli.close();
988+
}
823989

824990
// deleteLineLeft
825991
{

0 commit comments

Comments
 (0)