diff --git a/lib/readline.js b/lib/readline.js index 52dff98e05c07a..9865b78adc9e99 100644 --- a/lib/readline.js +++ b/lib/readline.js @@ -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(); + } }; @@ -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 @@ -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': diff --git a/test/parallel/test-readline-interface.js b/test/parallel/test-readline-interface.js index 87547a9b6ac197..c08c6d8ce82e6a 100644 --- a/test/parallel/test-readline-interface.js +++ b/test/parallel/test-readline-interface.js @@ -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 {