Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 0d660d9

Browse filesBrowse 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 <anna@addaleax.net> Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Ruben Bridgewater <ruben@bridgewater.de>
1 parent 5b8ac58 commit 0d660d9
Copy full SHA for 0d660d9

File tree

Expand file treeCollapse file tree

2 files changed

+198
-10
lines changed
Open diff view settings
Filter options
Expand file treeCollapse file tree

2 files changed

+198
-10
lines changed
Open diff view settings
Collapse file

‎lib/readline.js‎

Copy file name to clipboardExpand all lines: lib/readline.js
+32-10Lines changed: 32 additions & 10 deletions
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':
Collapse file

‎test/parallel/test-readline-interface.js‎

Copy file name to clipboardExpand all lines: test/parallel/test-readline-interface.js
+166Lines changed: 166 additions & 0 deletions
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 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.