diff --git a/doc/api/repl.md b/doc/api/repl.md index a030da7b4df460..48650e40d247c1 100644 --- a/doc/api/repl.md +++ b/doc/api/repl.md @@ -22,10 +22,11 @@ be connected to any Node.js [stream][]. Instances of [`repl.REPLServer`][] support automatic completion of inputs, completion preview, simplistic Emacs-style line editing, multi-line inputs, -[ZSH][] like reverse-i-search, ANSI-styled output, saving and restoring current -REPL session state, error recovery, and customizable evaluation functions. -Terminals that do not support ANSI-styles and Emacs-style line editing -automatically fall back to a limited feature set. +[ZSH][]-like reverse-i-search, [ZSH][]-like substring-based history search, +ANSI-styled output, saving and restoring current REPL session state, error +recovery, and customizable evaluation functions. Terminals that do not support +ANSI styles and Emacs-style line editing automatically fall back to a limited +feature set. ### Commands and Special Keys diff --git a/lib/internal/readline/utils.js b/lib/internal/readline/utils.js index 3ff50124e74e70..3257ee3a1e4a1f 100644 --- a/lib/internal/readline/utils.js +++ b/lib/internal/readline/utils.js @@ -1,9 +1,8 @@ 'use strict'; const { - Boolean, - NumberIsInteger, RegExp, + Symbol, } = primordials; // Regex used for ansi escape code splitting @@ -17,9 +16,9 @@ const ansi = new RegExp(ansiPattern, 'g'); const kUTF16SurrogateThreshold = 0x10000; // 2 ** 16 const kEscape = '\x1b'; +const kSubstringSearch = Symbol('kSubstringSearch'); let getStringWidth; -let isFullWidthCodePoint; function CSI(strings, ...args) { let ret = `${kEscape}[`; @@ -37,65 +36,65 @@ CSI.kClearToLineEnd = CSI`0K`; CSI.kClearLine = CSI`2K`; CSI.kClearScreenDown = CSI`0J`; +// TODO(BridgeAR): Treat combined characters as single character, i.e, +// 'a\u0301' and '\u0301a' (both have the same visual output). +// Check Canonical_Combining_Class in +// http://userguide.icu-project.org/strings/properties +function charLengthLeft(str, i) { + if (i <= 0) + return 0; + if ((i > 1 && str.codePointAt(i - 2) >= kUTF16SurrogateThreshold) || + str.codePointAt(i - 1) >= kUTF16SurrogateThreshold) { + return 2; + } + return 1; +} + +function charLengthAt(str, i) { + if (str.length <= i) { + // Pretend to move to the right. This is necessary to autocomplete while + // moving to the right. + return 1; + } + return str.codePointAt(i) >= kUTF16SurrogateThreshold ? 2 : 1; +} + if (internalBinding('config').hasIntl) { const icu = internalBinding('icu'); - getStringWidth = function getStringWidth(str, options) { - options = options || {}; - if (NumberIsInteger(str)) { - // Provide information about the character with code point 'str'. - return icu.getStringWidth( - str, - Boolean(options.ambiguousAsFullWidth), - false - ); - } - str = stripVTControlCharacters(String(str)); + // icu.getStringWidth(string, ambiguousAsFullWidth, expandEmojiSequence) + // Defaults: ambiguousAsFullWidth = false; expandEmojiSequence = true; + // TODO(BridgeAR): Expose the options to the user. That is probably the + // best thing possible at the moment, since it's difficult to know what + // the receiving end supports. + getStringWidth = function getStringWidth(str) { let width = 0; + str = stripVTControlCharacters(str); for (let i = 0; i < str.length; i++) { // Try to avoid calling into C++ by first handling the ASCII portion of // the string. If it is fully ASCII, we skip the C++ part. const code = str.charCodeAt(i); - if (code < 127) { - width += code >= 32; - continue; + if (code >= 127) { + width += icu.getStringWidth(str.slice(i)); + break; } - width += icu.getStringWidth( - str.slice(i), - Boolean(options.ambiguousAsFullWidth), - Boolean(options.expandEmojiSequence) - ); - break; + width += code >= 32 ? 1 : 0; } return width; }; - isFullWidthCodePoint = - function isFullWidthCodePoint(code, options) { - if (typeof code !== 'number') - return false; - return icu.getStringWidth(code, options) === 2; - }; } else { /** * Returns the number of columns required to display the given string. */ getStringWidth = function getStringWidth(str) { - if (NumberIsInteger(str)) - return isFullWidthCodePoint(str) ? 2 : 1; - let width = 0; - str = stripVTControlCharacters(String(str)); - - for (let i = 0; i < str.length; i++) { - const code = str.codePointAt(i); - - if (code >= kUTF16SurrogateThreshold) { // Surrogates. - i++; - } + str = stripVTControlCharacters(str); + for (const char of str) { + const code = char.codePointAt(0); if (isFullWidthCodePoint(code)) { width += 2; - } else { + } else if (!isZeroWidthCodePoint(code)) { width++; } } @@ -107,10 +106,10 @@ if (internalBinding('config').hasIntl) { * Returns true if the character represented by a given * Unicode code point is full-width. Otherwise returns false. */ - isFullWidthCodePoint = function isFullWidthCodePoint(code) { - // Code points are derived from: + const isFullWidthCodePoint = (code) => { + // Code points are partially derived from: // http://www.unicode.org/Public/UNIDATA/EastAsianWidth.txt - return NumberIsInteger(code) && code >= 0x1100 && ( + return code >= 0x1100 && ( code <= 0x115f || // Hangul Jamo code === 0x2329 || // LEFT-POINTING ANGLE BRACKET code === 0x232a || // RIGHT-POINTING ANGLE BRACKET @@ -137,10 +136,23 @@ if (internalBinding('config').hasIntl) { (code >= 0x1b000 && code <= 0x1b001) || // Enclosed Ideographic Supplement (code >= 0x1f200 && code <= 0x1f251) || + // Miscellaneous Symbols and Pictographs 0x1f300 - 0x1f5ff + // Emoticons 0x1f600 - 0x1f64f + (code >= 0x1f300 && code <= 0x1f64f) || // CJK Unified Ideographs Extension B .. Tertiary Ideographic Plane (code >= 0x20000 && code <= 0x3fffd) ); }; + + const isZeroWidthCodePoint = (code) => { + return code <= 0x1F || // C0 control codes + (code > 0x7F && code <= 0x9F) || // C1 control codes + (code >= 0x0300 && code <= 0x036F) || // Combining Diacritical Marks + (code >= 0x200B && code <= 0x200F) || // Modifying Invisible Characters + (code >= 0xFE00 && code <= 0xFE0F) || // Variation Selectors + (code >= 0xFE20 && code <= 0xFE2F) || // Combining Half Marks + (code >= 0xE0100 && code <= 0xE01EF); // Variation Selectors + }; } /** @@ -466,11 +478,12 @@ function commonPrefix(strings) { } module.exports = { + charLengthAt, + charLengthLeft, commonPrefix, emitKeys, getStringWidth, - isFullWidthCodePoint, - kUTF16SurrogateThreshold, + kSubstringSearch, stripVTControlCharacters, CSI }; diff --git a/lib/internal/repl/utils.js b/lib/internal/repl/utils.js index 4da23e096c95ed..20904d9d6a2445 100644 --- a/lib/internal/repl/utils.js +++ b/lib/internal/repl/utils.js @@ -31,26 +31,20 @@ const { } = require('readline'); const { - commonPrefix + commonPrefix, + getStringWidth, + kSubstringSearch, } = require('internal/readline/utils'); const { inspect } = require('util'); const debug = require('internal/util/debuglog').debuglog('repl'); -const inspectOptions = { - depth: 1, +const previewOptions = { colors: false, - compact: true, - breakLength: Infinity -}; -// Specify options that might change the output in a way that it's not a valid -// stringified object anymore. -const inspectedOptions = inspect(inspectOptions, { depth: 1, - colors: false, showHidden: false -}); +}; // If the error is that we've unexpectedly ended the input, // then let the user try to recover by adding more input. @@ -142,14 +136,16 @@ function setupPreview(repl, contextSymbol, bufferSymbol, active) { function getPreviewPos() { const displayPos = repl._getDisplayPos(`${repl._prompt}${repl.line}`); - const cursorPos = repl.getCursorPos(); - const rows = 1 + displayPos.rows - cursorPos.rows; - return { rows, cols: cursorPos.cols }; + const cursorPos = repl.line.length !== repl.cursor ? + repl.getCursorPos() : + displayPos; + return { displayPos, cursorPos }; } const clearPreview = () => { if (inputPreview !== null) { - const { rows } = getPreviewPos(); + const { displayPos, cursorPos } = getPreviewPos(); + const rows = displayPos.rows - cursorPos.rows + 1; moveCursor(repl.output, 0, rows); clearLine(repl.output); moveCursor(repl.output, 0, -rows); @@ -159,12 +155,25 @@ function setupPreview(repl, contextSymbol, bufferSymbol, active) { if (completionPreview !== null) { // Prevent cursor moves if not necessary! const move = repl.line.length !== repl.cursor; + let pos, rows; if (move) { - cursorTo(repl.output, repl._prompt.length + repl.line.length); + pos = getPreviewPos(); + cursorTo(repl.output, pos.displayPos.cols); + rows = pos.displayPos.rows - pos.cursorPos.rows; + moveCursor(repl.output, 0, rows); + } + const totalLine = `${repl._prompt}${repl.line}${completionPreview}`; + const newPos = repl._getDisplayPos(totalLine); + // Minimize work for the terminal. It is enough to clear the right part of + // the current line in case the preview is visible on a single line. + if (newPos.rows === 0 || (pos && pos.displayPos.rows === newPos.rows)) { + clearLine(repl.output, 1); + } else { + clearScreenDown(repl.output); } - clearLine(repl.output, 1); if (move) { - cursorTo(repl.output, repl._prompt.length + repl.cursor); + cursorTo(repl.output, pos.cursorPos.cols); + moveCursor(repl.output, 0, -rows); } completionPreview = null; } @@ -204,17 +213,6 @@ function setupPreview(repl, contextSymbol, bufferSymbol, active) { const suffix = prefix.slice(completeOn.length); - const totalLength = repl.line.length + - repl._prompt.length + - suffix.length + - (repl.useColors ? 0 : 4); - - // TODO(BridgeAR): Fix me. This should not be necessary. See similar - // comment in `showPreview()`. - if (totalLength > repl.columns) { - return; - } - if (insertPreview) { repl._insertString(suffix); return; @@ -226,11 +224,17 @@ function setupPreview(repl, contextSymbol, bufferSymbol, active) { `\u001b[90m${suffix}\u001b[39m` : ` // ${suffix}`; + const { cursorPos, displayPos } = getPreviewPos(); if (repl.line.length !== repl.cursor) { - cursorTo(repl.output, repl._prompt.length + repl.line.length); + cursorTo(repl.output, displayPos.cols); + moveCursor(repl.output, 0, displayPos.rows - cursorPos.rows); } repl.output.write(result); - cursorTo(repl.output, repl._prompt.length + repl.cursor); + cursorTo(repl.output, cursorPos.cols); + const totalLine = `${repl._prompt}${repl.line}${suffix}`; + const newPos = repl._getDisplayPos(totalLine); + const rows = newPos.rows - cursorPos.rows - (newPos.cols === 0 ? 1 : 0); + moveCursor(repl.output, 0, -rows); }); } @@ -254,7 +258,7 @@ function setupPreview(repl, contextSymbol, bufferSymbol, active) { } const { result } = preview; if (result.value !== undefined) { - callback(null, inspect(result.value, inspectOptions)); + callback(null, inspect(result.value, previewOptions)); // Ignore EvalErrors, SyntaxErrors and ReferenceErrors. It is not clear // where they came from and if they are recoverable or not. Other errors // may be inspected. @@ -264,8 +268,19 @@ function setupPreview(repl, contextSymbol, bufferSymbol, active) { result.className === 'ReferenceError')) { callback(null, null); } else if (result.objectId) { + // The writer options might change and have influence on the inspect + // output. The user might change e.g., `showProxy`, `getters` or + // `showHidden`. Use `inspect` instead of `JSON.stringify` to keep + // `Infinity` and similar intact. + const inspectOptions = inspect({ + ...repl.writer.options, + colors: false, + depth: 1, + compact: true, + breakLength: Infinity + }, previewOptions); session.post('Runtime.callFunctionOn', { - functionDeclaration: `(v) => util.inspect(v, ${inspectedOptions})`, + functionDeclaration: `(v) => util.inspect(v, ${inspectOptions})`, objectId: result.objectId, arguments: [result] }, (error, preview) => { @@ -283,6 +298,7 @@ function setupPreview(repl, contextSymbol, bufferSymbol, active) { }, () => callback(new ERR_INSPECTOR_NOT_AVAILABLE())); } + // TODO(BridgeAR): Prevent previews while pasting code. const showPreview = () => { // Prevent duplicated previews after a refresh. if (inputPreview !== null) { @@ -337,25 +353,40 @@ function setupPreview(repl, contextSymbol, bufferSymbol, active) { // Limit the output to maximum 250 characters. Otherwise it becomes a) // difficult to read and b) non terminal REPLs would visualize the whole // output. - const maxColumns = MathMin(repl.columns, 250); - - if (inspected.length > maxColumns) { - inspected = `${inspected.slice(0, maxColumns - 6)}...`; + let maxColumns = MathMin(repl.columns, 250); + + // Support unicode characters of width other than one by checking the + // actual width. + if (inspected.length * 2 >= maxColumns && + getStringWidth(inspected) > maxColumns) { + maxColumns -= 4 + (repl.useColors ? 0 : 3); + let res = ''; + for (const char of inspected) { + maxColumns -= getStringWidth(char); + if (maxColumns < 0) + break; + res += char; + } + inspected = `${res}...`; } + + // Line breaks are very rare and probably only occur in case of error + // messages with line breaks. const lineBreakPos = inspected.indexOf('\n'); if (lineBreakPos !== -1) { inspected = `${inspected.slice(0, lineBreakPos)}`; } + const result = repl.useColors ? `\u001b[90m${inspected}\u001b[39m` : `// ${inspected}`; - const { rows: previewRows, cols: cursorCols } = getPreviewPos(); - if (previewRows !== 1) - moveCursor(repl.output, 0, previewRows - 1); + const { cursorPos, displayPos } = getPreviewPos(); + const rows = displayPos.rows - cursorPos.rows; + moveCursor(repl.output, 0, rows); const { cols: resultCols } = repl._getDisplayPos(result); repl.output.write(`\n${result}`); - moveCursor(repl.output, cursorCols - resultCols, -previewRows); + moveCursor(repl.output, cursorPos.cols - resultCols, -rows - 1); }); }; @@ -429,8 +460,8 @@ function setupReverseSearch(repl) { // Reset the already matched set in case the direction is changed. That // way it's possible to find those entries again. alreadyMatched.clear(); + dir = keyName; } - dir = keyName; return true; } @@ -575,16 +606,14 @@ function setupReverseSearch(repl) { // Clear screen and write the current repl.line before exiting. cursorTo(repl.output, promptPos.cols); - if (promptPos.rows !== 0) - moveCursor(repl.output, 0, promptPos.rows); + moveCursor(repl.output, 0, promptPos.rows); clearScreenDown(repl.output); if (repl.line !== '') { repl.output.write(repl.line); if (repl.line.length !== repl.cursor) { const { cols, rows } = repl.getCursorPos(); cursorTo(repl.output, cols); - if (rows !== 0) - moveCursor(repl.output, 0, rows); + moveCursor(repl.output, 0, rows); } } } @@ -624,6 +653,7 @@ function setupReverseSearch(repl) { typeof string !== 'string' || string === '') { reset(); + repl[kSubstringSearch] = ''; } else { reset(`${input}${string}`); search(); diff --git a/lib/internal/util/inspect.js b/lib/internal/util/inspect.js index 3f2f75b909663b..21d088c5a9f98b 100644 --- a/lib/internal/util/inspect.js +++ b/lib/internal/util/inspect.js @@ -1195,6 +1195,8 @@ function groupArrayElements(ctx, output, value) { if (columns <= 1) { return output; } + // TODO(BridgeAR): Add unicode support. Use the readline getStringWidth + // function. const tmp = []; const maxLineLength = []; for (let i = 0; i < columns; i++) { @@ -1272,6 +1274,8 @@ function formatBigInt(fn, value) { function formatPrimitive(fn, value, ctx) { if (typeof value === 'string') { if (ctx.compact !== true && + // TODO(BridgeAR): Add unicode support. Use the readline getStringWidth + // function. value.length > kMinLineLength && value.length > ctx.breakLength - ctx.indentationLvl - 4) { return value @@ -1617,6 +1621,9 @@ function isBelowBreakLength(ctx, output, start, base) { // Each entry is separated by at least a comma. Thus, we start with a total // length of at least `output.length`. In addition, some cases have a // whitespace in-between each other that is added to the total as well. + // TODO(BridgeAR): Add unicode support. Use the readline getStringWidth + // function. Check the performance overhead and make it an opt-in in case it's + // significant. let totalLength = output.length + start; if (totalLength + output.length > ctx.breakLength) return false; diff --git a/lib/readline.js b/lib/readline.js index 255fbe8754ae62..9cbb3c55c8d951 100644 --- a/lib/readline.js +++ b/lib/readline.js @@ -49,12 +49,13 @@ const { validateString } = require('internal/validators'); const { inspect } = require('internal/util/inspect'); const EventEmitter = require('events'); const { + charLengthAt, + charLengthLeft, commonPrefix, CSI, emitKeys, getStringWidth, - isFullWidthCodePoint, - kUTF16SurrogateThreshold, + kSubstringSearch, stripVTControlCharacters } = require('internal/readline/utils'); @@ -153,6 +154,7 @@ function Interface(input, output, completer, terminal) { const self = this; + this[kSubstringSearch] = null; this.output = output; this.input = input; this.historySize = historySize; @@ -488,9 +490,6 @@ Interface.prototype._insertString = function(c) { } else { this._writeToOutput(c); } - - // A hack to get the line refreshed if it's needed - this._moveCursor(0); } }; @@ -591,25 +590,6 @@ Interface.prototype._wordRight = function() { } }; -function charLengthLeft(str, i) { - if (i <= 0) - return 0; - if ((i > 1 && str.codePointAt(i - 2) >= kUTF16SurrogateThreshold) || - str.codePointAt(i - 1) >= kUTF16SurrogateThreshold) { - return 2; - } - return 1; -} - -function charLengthAt(str, i) { - if (str.length <= i) { - // Pretend to move to the right. This is necessary to autocomplete while - // moving to the right. - return 1; - } - return str.codePointAt(i) >= kUTF16SurrogateThreshold ? 2 : 1; -} - Interface.prototype._deleteLeft = function() { if (this.cursor > 0 && this.line.length > 0) { // The number of UTF-16 units comprising the character to the left @@ -688,52 +668,73 @@ Interface.prototype._line = function() { this._onLine(line); }; - +// TODO(BridgeAR): Add underscores to the search part and a red background in +// case no match is found. This should only be the visual part and not the +// actual line content! +// TODO(BridgeAR): In case the substring based search is active and the end is +// reached, show a comment how to search the history as before. E.g., using +// + N. Only show this after two/three UPs or DOWNs, not on the first +// one. Interface.prototype._historyNext = function() { - if (this.historyIndex > 0) { - this.historyIndex--; - this.line = this.history[this.historyIndex]; + if (this.historyIndex >= 0) { + const search = this[kSubstringSearch] || ''; + let index = this.historyIndex - 1; + while (index >= 0 && + (!this.history[index].startsWith(search) || + this.line === this.history[index])) { + index--; + } + if (index === -1) { + this.line = search; + } else { + this.line = this.history[index]; + } + this.historyIndex = index; this.cursor = this.line.length; // Set cursor to end of line. this._refreshLine(); - - } else if (this.historyIndex === 0) { - this.historyIndex = -1; - this.cursor = 0; - this.line = ''; - this._refreshLine(); } }; - Interface.prototype._historyPrev = function() { - if (this.historyIndex + 1 < this.history.length) { - this.historyIndex++; - this.line = this.history[this.historyIndex]; + if (this.historyIndex < this.history.length && this.history.length) { + const search = this[kSubstringSearch] || ''; + let index = this.historyIndex + 1; + while (index < this.history.length && + (!this.history[index].startsWith(search) || + this.line === this.history[index])) { + index++; + } + if (index === this.history.length) { + this.line = search; + } else { + this.line = this.history[index]; + } + this.historyIndex = index; this.cursor = this.line.length; // Set cursor to end of line. - this._refreshLine(); } }; - // Returns the last character's display position of the given string Interface.prototype._getDisplayPos = function(str) { let offset = 0; const col = this.columns; let rows = 0; str = stripVTControlCharacters(str); - for (let i = 0, len = str.length; i < len; i++) { - const code = str.codePointAt(i); - if (code >= kUTF16SurrogateThreshold) { // Surrogates. - i++; - } - if (code === 0x0a) { // new line \n - // rows must be incremented by 1 even if offset = 0 or col = +Infinity + for (const char of str) { + if (char === '\n') { + // Rows must be incremented by 1 even if offset = 0 or col = +Infinity. rows += MathCeil(offset / col) || 1; offset = 0; continue; } - const width = getStringWidth(code); + // Tabs must be aligned by an offset of 8. + // TODO(BridgeAR): Make the tab size configurable. + if (char === '\t') { + offset += 8 - (offset % 8); + continue; + } + const width = getStringWidth(char); if (width === 0 || width === 1) { offset += width; } else { // width === 2 @@ -760,7 +761,7 @@ Interface.prototype.getCursorPos = function() { // move the cursor to the beginning of the next line. if (cols + 1 === columns && this.cursor < this.line.length && - isFullWidthCodePoint(this.line.codePointAt(this.cursor))) { + getStringWidth(this.line[this.cursor]) > 1) { rows++; cols = 0; } @@ -770,33 +771,27 @@ Interface.prototype._getCursorPos = Interface.prototype.getCursorPos; // This function moves cursor dx places to the right -// (-dx for left) and refreshes the line if it is needed +// (-dx for left) and refreshes the line if it is needed. Interface.prototype._moveCursor = function(dx) { - const oldcursor = this.cursor; + if (dx === 0) { + return; + } const oldPos = this.getCursorPos(); this.cursor += dx; - // bounds check - if (this.cursor < 0) this.cursor = 0; - else if (this.cursor > this.line.length) this.cursor = this.line.length; + // Bounds check + if (this.cursor < 0) { + this.cursor = 0; + } else if (this.cursor > this.line.length) { + this.cursor = this.line.length; + } const newPos = this.getCursorPos(); - // Check if cursors are in the same line + // Check if cursor stayed on the line. if (oldPos.rows === newPos.rows) { - const diffCursor = this.cursor - oldcursor; - let diffWidth; - if (diffCursor < 0) { - diffWidth = -getStringWidth( - this.line.substring(this.cursor, oldcursor) - ); - } else if (diffCursor > 0) { - diffWidth = getStringWidth( - this.line.substring(this.cursor, oldcursor) - ); - } + const diffWidth = newPos.cols - oldPos.cols; moveCursor(this.output, diffWidth, 0); - this.prevRows = newPos.rows; } else { this._refreshLine(); } @@ -856,6 +851,20 @@ Interface.prototype._ttyWrite = function(s, key) { key = key || {}; this._previousKey = key; + // Activate or deactivate substring search. + if ((key.name === 'up' || key.name === 'down') && + !key.ctrl && !key.meta && !key.shift) { + if (this[kSubstringSearch] === null) { + this[kSubstringSearch] = this.line.slice(0, this.cursor); + } + } else if (this[kSubstringSearch] !== null) { + this[kSubstringSearch] = null; + // Reset the index in case there's no match. + if (this.history.length === this.historyIndex) { + this.historyIndex = -1; + } + } + // Ignore escape key, fixes // https://github.com/nodejs/node-v0.x-archive/issues/2876. if (key.name === 'escape') return; diff --git a/src/node_i18n.cc b/src/node_i18n.cc index c68e01e1074a4a..46c6ef39f861ec 100644 --- a/src/node_i18n.cc +++ b/src/node_i18n.cc @@ -728,16 +728,6 @@ static void ToASCII(const FunctionCallbackInfo& args) { // Refs: https://github.com/KDE/konsole/blob/8c6a5d13c0/src/konsole_wcwidth.cpp#L101-L223 static int GetColumnWidth(UChar32 codepoint, bool ambiguous_as_full_width = false) { - const auto zero_width_mask = U_GC_CC_MASK | // C0/C1 control code - U_GC_CF_MASK | // Format control character - U_GC_ME_MASK | // Enclosing mark - U_GC_MN_MASK; // Nonspacing mark - if (codepoint != 0x00AD && // SOFT HYPHEN is Cf but not zero-width - ((U_MASK(u_charType(codepoint)) & zero_width_mask) || - u_hasBinaryProperty(codepoint, UCHAR_EMOJI_MODIFIER))) { - return 0; - } - // UCHAR_EAST_ASIAN_WIDTH is the Unicode property that identifies a // codepoint as being full width, wide, ambiguous, neutral, narrow, // or halfwidth. @@ -761,6 +751,15 @@ static int GetColumnWidth(UChar32 codepoint, case U_EA_HALFWIDTH: case U_EA_NARROW: default: + const auto zero_width_mask = U_GC_CC_MASK | // C0/C1 control code + U_GC_CF_MASK | // Format control character + U_GC_ME_MASK | // Enclosing mark + U_GC_MN_MASK; // Nonspacing mark + if (codepoint != 0x00AD && // SOFT HYPHEN is Cf but not zero-width + ((U_MASK(u_charType(codepoint)) & zero_width_mask) || + u_hasBinaryProperty(codepoint, UCHAR_EMOJI_MODIFIER))) { + return 0; + } return 1; } } @@ -768,18 +767,10 @@ static int GetColumnWidth(UChar32 codepoint, // Returns the column width for the given String. static void GetStringWidth(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); - if (args.Length() < 1) - return; + CHECK(args[0]->IsString()); bool ambiguous_as_full_width = args[1]->IsTrue(); - bool expand_emoji_sequence = args[2]->IsTrue(); - - if (args[0]->IsNumber()) { - uint32_t val; - if (!args[0]->Uint32Value(env->context()).To(&val)) return; - args.GetReturnValue().Set(GetColumnWidth(val, ambiguous_as_full_width)); - return; - } + bool expand_emoji_sequence = !args[2]->IsBoolean() || args[2]->IsTrue(); TwoByteValue value(env->isolate(), args[0]); // reinterpret_cast is required by windows to compile @@ -804,6 +795,7 @@ static void GetStringWidth(const FunctionCallbackInfo& args) { // in advance if a particular sequence is going to be supported. // The expand_emoji_sequence option allows the caller to skip this // check and count each code within an emoji sequence separately. + // https://www.unicode.org/reports/tr51/tr51-16.html#Emoji_ZWJ_Sequences if (!expand_emoji_sequence && n > 0 && p == 0x200d && // 0x200d == ZWJ (zero width joiner) (u_hasBinaryProperty(c, UCHAR_EMOJI_PRESENTATION) || diff --git a/test/parallel/test-icu-stringwidth.js b/test/parallel/test-icu-stringwidth.js index 48384f916d9126..a427601803afd3 100644 --- a/test/parallel/test-icu-stringwidth.js +++ b/test/parallel/test-icu-stringwidth.js @@ -12,82 +12,78 @@ const readline = require('internal/readline/utils'); // Ll (Lowercase Letter): LATIN SMALL LETTER A assert.strictEqual(readline.getStringWidth('a'), 1); -assert.strictEqual(readline.getStringWidth(0x0061), 1); +assert.strictEqual(readline.getStringWidth(String.fromCharCode(0x0061)), 1); // Lo (Other Letter) assert.strictEqual(readline.getStringWidth('丁'), 2); -assert.strictEqual(readline.getStringWidth(0x4E01), 2); +assert.strictEqual(readline.getStringWidth(String.fromCharCode(0x4E01)), 2); // Surrogate pairs -assert.strictEqual(readline.getStringWidth('\ud83d\udc78\ud83c\udfff'), 2); +assert.strictEqual(readline.getStringWidth('\ud83d\udc78\ud83c\udfff'), 4); assert.strictEqual(readline.getStringWidth('πŸ‘…'), 2); // Cs (Surrogate): High Surrogate assert.strictEqual(readline.getStringWidth('\ud83d'), 1); // Cs (Surrogate): Low Surrogate assert.strictEqual(readline.getStringWidth('\udc78'), 1); // Cc (Control): NULL -assert.strictEqual(readline.getStringWidth(0), 0); +assert.strictEqual(readline.getStringWidth('\u0000'), 0); // Cc (Control): BELL -assert.strictEqual(readline.getStringWidth(0x0007), 0); +assert.strictEqual(readline.getStringWidth(String.fromCharCode(0x0007)), 0); // Cc (Control): LINE FEED assert.strictEqual(readline.getStringWidth('\n'), 0); // Cf (Format): SOFT HYPHEN -assert.strictEqual(readline.getStringWidth(0x00AD), 1); +assert.strictEqual(readline.getStringWidth(String.fromCharCode(0x00AD)), 1); // Cf (Format): LEFT-TO-RIGHT MARK // Cf (Format): RIGHT-TO-LEFT MARK assert.strictEqual(readline.getStringWidth('\u200Ef\u200F'), 1); // Cn (Unassigned): Not a character -assert.strictEqual(readline.getStringWidth(0x10FFEF), 1); +assert.strictEqual(readline.getStringWidth(String.fromCharCode(0x10FFEF)), 1); // Cn (Unassigned): Not a character (but in a CJK range) -assert.strictEqual(readline.getStringWidth(0x3FFEF), 2); +assert.strictEqual(readline.getStringWidth(String.fromCharCode(0x3FFEF)), 1); // Mn (Nonspacing Mark): COMBINING ACUTE ACCENT -assert.strictEqual(readline.getStringWidth(0x0301), 0); +assert.strictEqual(readline.getStringWidth(String.fromCharCode(0x0301)), 0); // Mc (Spacing Mark): BALINESE ADEG ADEG // Chosen as its Canonical_Combining_Class is not 0, but is not a 0-width // character. -assert.strictEqual(readline.getStringWidth(0x1B44), 1); +assert.strictEqual(readline.getStringWidth(String.fromCharCode(0x1B44)), 1); // Me (Enclosing Mark): COMBINING ENCLOSING CIRCLE -assert.strictEqual(readline.getStringWidth(0x20DD), 0); +assert.strictEqual(readline.getStringWidth(String.fromCharCode(0x20DD)), 0); -// The following is an emoji sequence. In some implementations, it is -// represented as a single glyph, in other implementations as a sequence -// of individual glyphs. By default, the algorithm will assume the single -// glyph interpretation and return a value of 2. By passing the -// expandEmojiSequence: true option, each component will be counted -// individually. -assert.strictEqual(readline.getStringWidth('πŸ‘©β€πŸ‘©β€πŸ‘§β€πŸ‘§'), 2); -assert.strictEqual( - readline.getStringWidth('πŸ‘©β€πŸ‘©β€πŸ‘§β€πŸ‘§', { expandEmojiSequence: true }), 8); +// The following is an emoji sequence with ZWJ (zero-width-joiner). In some +// implementations, it is represented as a single glyph, in other +// implementations as a sequence of individual glyphs. By default, each +// component will be counted individually, since not a lot of systems support +// these fully. +// See https://www.unicode.org/reports/tr51/tr51-16.html#Emoji_ZWJ_Sequences +assert.strictEqual(readline.getStringWidth('πŸ‘©β€πŸ‘©β€πŸ‘§β€πŸ‘§'), 8); +// TODO(BridgeAR): This should have a width of two and six. The heart contains +// the \uFE0F variation selector that indicates that it should be displayed as +// emoji instead of as text. Emojis are all full width characters when not being +// rendered as text. +// https://en.wikipedia.org/wiki/Variation_Selectors_(Unicode_block) +assert.strictEqual(readline.getStringWidth('❀️'), 1); +assert.strictEqual(readline.getStringWidth('πŸ‘©β€β€οΈβ€πŸ‘©'), 5); +// The length of one is correct. It is an emoji treated as text. +assert.strictEqual(readline.getStringWidth('❀'), 1); // By default, unicode characters whose width is considered ambiguous will // be considered half-width. For these characters, getStringWidth will return // 1. In some contexts, however, it is more appropriate to consider them full -// width. By default, the algorithm will assume half width. By passing -// the ambiguousAsFullWidth: true option, ambiguous characters will be counted -// as 2 columns. +// width. By default, the algorithm will assume half width. assert.strictEqual(readline.getStringWidth('\u01d4'), 1); -assert.strictEqual( - readline.getStringWidth('\u01d4', { ambiguousAsFullWidth: true }), 2); // Control chars and combining chars are zero assert.strictEqual(readline.getStringWidth('\u200E\n\u220A\u20D2'), 1); // Test that the fast path for ASCII characters yields results consistent // with the 'slow' path. -for (const ambiguousAsFullWidth of [ false, true ]) { - for (let i = 0; i < 256; i++) { - const char = String.fromCharCode(i); - assert.strictEqual( - readline.getStringWidth(i, { ambiguousAsFullWidth }), - readline.getStringWidth(char, { ambiguousAsFullWidth })); - assert.strictEqual( - readline.getStringWidth(char + 'πŸŽ‰', { ambiguousAsFullWidth }), - readline.getStringWidth(char, { ambiguousAsFullWidth }) + 2); +for (let i = 0; i < 256; i++) { + const char = String.fromCharCode(i); + assert.strictEqual( + readline.getStringWidth(char + 'πŸŽ‰'), + readline.getStringWidth(char) + 2); - if (i < 32 || (i >= 127 && i < 160)) { // Control character - assert.strictEqual( - readline.getStringWidth(i, { ambiguousAsFullWidth }), 0); - } else if (i < 127) { // Regular ASCII character - assert.strictEqual( - readline.getStringWidth(i, { ambiguousAsFullWidth }), 1); - } + if (i < 32 || (i >= 127 && i < 160)) { // Control character + assert.strictEqual(readline.getStringWidth(char), 0); + } else { // Regular ASCII character + assert.strictEqual(readline.getStringWidth(char), 1); } } diff --git a/test/parallel/test-readline-interface.js b/test/parallel/test-readline-interface.js index 3504a68ab9754a..cbc7ed31f488ea 100644 --- a/test/parallel/test-readline-interface.js +++ b/test/parallel/test-readline-interface.js @@ -430,6 +430,7 @@ function isWarned(emitter) { removeHistoryDuplicates: true }); const expectedLines = ['foo', 'bar', 'baz', 'bar', 'bat', 'bat']; + // ['foo', 'baz', 'bar', bat']; let callCount = 0; rli.on('line', function(line) { assert.strictEqual(line, expectedLines[callCount]); @@ -450,12 +451,51 @@ function isWarned(emitter) { assert.strictEqual(callCount, 0); fi.emit('keypress', '.', { name: 'down' }); // 'baz' assert.strictEqual(rli.line, 'baz'); + assert.strictEqual(rli.historyIndex, 2); fi.emit('keypress', '.', { name: 'n', ctrl: true }); // 'bar' assert.strictEqual(rli.line, 'bar'); + assert.strictEqual(rli.historyIndex, 1); + fi.emit('keypress', '.', { name: 'n', ctrl: true }); + assert.strictEqual(rli.line, 'bat'); + assert.strictEqual(rli.historyIndex, 0); + // Activate the substring history search. fi.emit('keypress', '.', { name: 'down' }); // 'bat' assert.strictEqual(rli.line, 'bat'); - fi.emit('keypress', '.', { name: 'down' }); // '' - assert.strictEqual(rli.line, ''); + assert.strictEqual(rli.historyIndex, -1); + // Deactivate substring history search. + fi.emit('keypress', '.', { name: 'backspace' }); // 'ba' + assert.strictEqual(rli.historyIndex, -1); + assert.strictEqual(rli.line, 'ba'); + // Activate the substring history search. + fi.emit('keypress', '.', { name: 'down' }); // 'ba' + assert.strictEqual(rli.historyIndex, -1); + assert.strictEqual(rli.line, 'ba'); + fi.emit('keypress', '.', { name: 'down' }); // 'ba' + assert.strictEqual(rli.historyIndex, -1); + assert.strictEqual(rli.line, 'ba'); + fi.emit('keypress', '.', { name: 'up' }); // 'bat' + assert.strictEqual(rli.historyIndex, 0); + assert.strictEqual(rli.line, 'bat'); + fi.emit('keypress', '.', { name: 'up' }); // 'bar' + assert.strictEqual(rli.historyIndex, 1); + assert.strictEqual(rli.line, 'bar'); + fi.emit('keypress', '.', { name: 'up' }); // 'baz' + assert.strictEqual(rli.historyIndex, 2); + assert.strictEqual(rli.line, 'baz'); + fi.emit('keypress', '.', { name: 'up' }); // 'ba' + assert.strictEqual(rli.historyIndex, 4); + assert.strictEqual(rli.line, 'ba'); + fi.emit('keypress', '.', { name: 'up' }); // 'ba' + assert.strictEqual(rli.historyIndex, 4); + assert.strictEqual(rli.line, 'ba'); + // Deactivate substring history search and reset history index. + fi.emit('keypress', '.', { name: 'right' }); // 'ba' + assert.strictEqual(rli.historyIndex, -1); + assert.strictEqual(rli.line, 'ba'); + // Substring history search activated. + fi.emit('keypress', '.', { name: 'up' }); // 'ba' + assert.strictEqual(rli.historyIndex, 0); + assert.strictEqual(rli.line, 'bat'); rli.close(); } @@ -691,11 +731,7 @@ function isWarned(emitter) { 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); - } + assert.strictEqual(cursorPos.cols, 2); rli.on('line', common.mustCall((line) => { assert.strictEqual(line, 'πŸ’»'); @@ -724,14 +760,7 @@ function isWarned(emitter) { 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' }); - } + assert.strictEqual(cursorPos.cols, 2); rli.on('line', common.mustCall((line) => { assert.strictEqual(line, 'πŸ•πŸ’»'); @@ -755,22 +784,12 @@ function isWarned(emitter) { 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' }); - } + assert.strictEqual(cursorPos.cols, 2); 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); - } + assert.strictEqual(cursorPos.cols, 4); rli.on('line', common.mustCall((line) => { assert.strictEqual(line, 'πŸ’»πŸ•'); @@ -932,11 +951,7 @@ function isWarned(emitter) { 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); - } + assert.strictEqual(cursorPos.cols, 2); // Delete left character fi.emit('keypress', '.', { ctrl: true, name: 'h' }); cursorPos = rli.getCursorPos(); @@ -1119,27 +1134,24 @@ function isWarned(emitter) { } } - // isFullWidthCodePoint() should return false for non-numeric values - [true, false, null, undefined, {}, [], 'あ'].forEach((v) => { - assert.strictEqual(internalReadline.isFullWidthCodePoint('あ'), false); - }); - // Wide characters should be treated as two columns. - assert.strictEqual(internalReadline.isFullWidthCodePoint('a'.charCodeAt(0)), - false); - assert.strictEqual(internalReadline.isFullWidthCodePoint('あ'.charCodeAt(0)), - true); - assert.strictEqual(internalReadline.isFullWidthCodePoint('θ°’'.charCodeAt(0)), - true); - assert.strictEqual(internalReadline.isFullWidthCodePoint('κ³ '.charCodeAt(0)), - true); - assert.strictEqual(internalReadline.isFullWidthCodePoint(0x1f251), true); + assert.strictEqual(internalReadline.getStringWidth('a'), 1); + assert.strictEqual(internalReadline.getStringWidth('あ'), 2); + assert.strictEqual(internalReadline.getStringWidth('θ°’'), 2); + assert.strictEqual(internalReadline.getStringWidth('κ³ '), 2); + assert.strictEqual( + internalReadline.getStringWidth(String.fromCodePoint(0x1f251)), 2); assert.strictEqual(internalReadline.getStringWidth('abcde'), 5); assert.strictEqual(internalReadline.getStringWidth('叀池や'), 6); assert.strictEqual(internalReadline.getStringWidth('γƒŽγƒΌγƒ‰.js'), 9); assert.strictEqual(internalReadline.getStringWidth('δ½ ε₯½'), 4); assert.strictEqual(internalReadline.getStringWidth('μ•ˆλ…•ν•˜μ„Έμš”'), 10); assert.strictEqual(internalReadline.getStringWidth('A\ud83c\ude00BC'), 5); + assert.strictEqual(internalReadline.getStringWidth('πŸ‘¨β€πŸ‘©β€πŸ‘¦β€πŸ‘¦'), 8); + assert.strictEqual(internalReadline.getStringWidth('πŸ•π·γ‚πŸ’»πŸ˜€'), 9); + // TODO(BridgeAR): This should have a width of 4. + assert.strictEqual(internalReadline.getStringWidth('⓬β“ͺ'), 2); + assert.strictEqual(internalReadline.getStringWidth('\u0301\u200D\u200E'), 0); // Check if vt control chars are stripped assert.strictEqual( diff --git a/test/parallel/test-repl-history-navigation.js b/test/parallel/test-repl-history-navigation.js index 8de2b49b0ea5a6..3591f0f336b6e6 100644 --- a/test/parallel/test-repl-history-navigation.js +++ b/test/parallel/test-repl-history-navigation.js @@ -59,6 +59,8 @@ const BACKSPACE = { name: 'backspace' }; const WORD_LEFT = { name: 'left', ctrl: true }; const WORD_RIGHT = { name: 'right', ctrl: true }; const GO_TO_END = { name: 'end' }; +const DELETE_WORD_LEFT = { name: 'backspace', ctrl: true }; +const SIGINT = { name: 'c', ctrl: true }; const prompt = '> '; const WAIT = '€'; @@ -78,19 +80,21 @@ const tests = [ }, { env: { NODE_REPL_HISTORY: defaultHistoryPath }, - test: [UP, UP, UP, UP, UP, DOWN, DOWN, DOWN, DOWN], + test: [UP, UP, UP, UP, UP, DOWN, DOWN, DOWN, DOWN, DOWN], expected: [prompt, `${prompt}Array(100).fill(1).map((e, i) => i ** 2)`, prev && '\n// [ 0, 1, 4, 9, 16, 25, 36, 49, 64, 81, 100, 121, ' + '144, 169, 196, 225, 256, 289, 324, 361, 400, 441, 484, 529,' + ' 576, 625, 676, 729, 784, 841, 900, 961, 1024, 1089, 1156, ' + '1225, 1296, 1369, 1444, 1521, 1600, 1681, 1764, 1849, 1936,' + - ' 2025, 2116, 2209, ...', + ' 2025, 2116, 2209,...', `${prompt}{key : {key2 :[] }}`, prev && '\n// { key: { key2: [] } }', `${prompt}555 + 909`, prev && '\n// 1464', `${prompt}let ab = 45`, + prompt, + `${prompt}let ab = 45`, `${prompt}555 + 909`, prev && '\n// 1464', `${prompt}{key : {key2 :[] }}`, @@ -100,14 +104,77 @@ const tests = [ '144, 169, 196, 225, 256, 289, 324, 361, 400, 441, 484, 529,' + ' 576, 625, 676, 729, 784, 841, 900, 961, 1024, 1089, 1156, ' + '1225, 1296, 1369, 1444, 1521, 1600, 1681, 1764, 1849, 1936,' + - ' 2025, 2116, 2209, ...', + ' 2025, 2116, 2209,...', prompt].filter((e) => typeof e === 'string'), + clean: false + }, + { // Creates more history entries to navigate through. + env: { NODE_REPL_HISTORY: defaultHistoryPath }, + test: [ + '555 + 909', ENTER, // Add a duplicate to the history set. + 'const foo = true', ENTER, + '555n + 111n', ENTER, + '5 + 5', ENTER, + '55 - 13 === 42', ENTER + ], + expected: [], + clean: false + }, + { + env: { NODE_REPL_HISTORY: defaultHistoryPath }, + checkTotal: true, + preview: false, + showEscapeCodes: true, + test: [ + '55', UP, UP, UP, UP, UP, UP, ENTER + ], + expected: [ + '\x1B[1G', '\x1B[0J', prompt, '\x1B[3G', + // '55' + '5', '5', + // UP + '\x1B[1G', '\x1B[0J', + '> 55 - 13 === 42', '\x1B[17G', + // UP - skipping 5 + 5 + '\x1B[1G', '\x1B[0J', + '> 555n + 111n', '\x1B[14G', + // UP - skipping const foo = true + '\x1B[1G', '\x1B[0J', + '> 555 + 909', '\x1B[12G', + // UP, UP + // UPs at the end of the history reset the line to the original input. + '\x1B[1G', '\x1B[0J', + '> 55', '\x1B[5G', + // ENTER + '\r\n', '55\n', + '\x1B[1G', '\x1B[0J', + '> ', '\x1B[3G', + '\r\n' + ], clean: true }, { env: { NODE_REPL_HISTORY: defaultHistoryPath }, skip: !process.features.inspector, test: [ + // あ is a fill width character with a length of one. + // πŸ• is a full width character with a length of two. + // 𐐷 is a half width character with the length of two. + // '\u0301', '0x200D', '\u200E' are zero width characters. + `const x1 = '${'あ'.repeat(124)}'`, ENTER, // Fully visible + ENTER, + `const y1 = '${'あ'.repeat(125)}'`, ENTER, // Cut off + ENTER, + `const x2 = '${'πŸ•'.repeat(124)}'`, ENTER, // Fully visible + ENTER, + `const y2 = '${'πŸ•'.repeat(125)}'`, ENTER, // Cut off + ENTER, + `const x3 = '${'𐐷'.repeat(248)}'`, ENTER, // Fully visible + ENTER, + `const y3 = '${'𐐷'.repeat(249)}'`, ENTER, // Cut off + ENTER, + `const x4 = 'a${'\u0301'.repeat(1000)}'`, ENTER, // á + ENTER, `const ${'veryLongName'.repeat(30)} = 'I should be previewed'`, ENTER, 'const e = new RangeError("visible\\ninvisible")', @@ -127,6 +194,7 @@ const tests = [ { env: { NODE_REPL_HISTORY: defaultHistoryPath }, columns: 250, + checkTotal: true, showEscapeCodes: true, skip: !process.features.inspector, test: [ @@ -135,7 +203,21 @@ const tests = [ UP, WORD_LEFT, UP, - BACKSPACE + BACKSPACE, + 'x1', + BACKSPACE, + '2', + BACKSPACE, + '3', + BACKSPACE, + '4', + DELETE_WORD_LEFT, + 'y1', + BACKSPACE, + '2', + BACKSPACE, + '3', + SIGINT ], // A = Cursor n up // B = Cursor n down @@ -154,6 +236,9 @@ const tests = [ // 236 + 2 + 4 + 8 '\x1B[1G', '\x1B[0J', `${prompt}${' '.repeat(236)} fun`, '\x1B[243G', + ' // ction', '\x1B[243G', + ' // ction', '\x1B[243G', + '\x1B[0K', // 2. UP '\x1B[1G', '\x1B[0J', `${prompt}${' '.repeat(235)} fun`, '\x1B[242G', @@ -190,7 +275,44 @@ const tests = [ '\x1B[1B', '\x1B[2K', '\x1B[1A', // 6. Backspace '\x1B[1G', '\x1B[0J', - prompt, '\x1B[3G' + '> ', '\x1B[3G', 'x', '1', + `\n// '${'あ'.repeat(124)}'`, + '\x1B[1C\x1B[1A', + '\x1B[1B', '\x1B[2K', '\x1B[1A', + '\x1B[1G', '\x1B[0J', + '> x', '\x1B[4G', '2', + `\n// '${'πŸ•'.repeat(124)}'`, + '\x1B[1C\x1B[1A', + '\x1B[1B', '\x1B[2K', '\x1B[1A', + '\x1B[1G', '\x1B[0J', + '> x', '\x1B[4G', '3', + `\n// '${'𐐷'.repeat(248)}'`, + '\x1B[1C\x1B[1A', + '\x1B[1B', '\x1B[2K', '\x1B[1A', + '\x1B[1G', '\x1B[0J', + '> x', '\x1B[4G', '4', + `\n// 'a${'\u0301'.repeat(1000)}'`, + '\x1B[2D\x1B[1A', + '\x1B[1B', '\x1B[2K', '\x1B[1A', + '\x1B[1G', '\x1B[0J', + '> ', '\x1B[3G', 'y', '1', + `\n// '${'あ'.repeat(121)}...`, + '\x1B[245D\x1B[1A', + '\x1B[1B', '\x1B[2K', '\x1B[1A', + '\x1B[1G', '\x1B[0J', + '> y', '\x1B[4G', '2', + `\n// '${'πŸ•'.repeat(121)}...`, + '\x1B[245D\x1B[1A', + '\x1B[1B', '\x1B[2K', '\x1B[1A', + '\x1B[1G', '\x1B[0J', + '> y', '\x1B[4G', '3', + `\n// '${'𐐷'.repeat(242)}...`, + '\x1B[245D\x1B[1A', + '\x1B[1B', '\x1B[2K', '\x1B[1A', + '\r\n', + '\x1B[1G', '\x1B[0J', + '> ', '\x1B[3G', + '\r\n' ], clean: true }, @@ -259,6 +381,51 @@ const tests = [ // 10. Word right. Cleanup '\x1B[0K', '\x1B[3G', '\x1B[7C', ' // n', '\x1B[10G', '\x1B[0K', + // 11. ENTER + '\r\n', + 'Uncaught ReferenceError: functio is not defined\n', + '\x1B[1G', '\x1B[0J', + prompt, '\x1B[3G', '\r\n' + ], + clean: true + }, + { + // Check changed inspection defaults. + env: { NODE_REPL_HISTORY: defaultHistoryPath }, + skip: !process.features.inspector, + test: [ + 'util.inspect.replDefaults.showHidden', + ENTER + ], + expected: [], + clean: false + }, + { + env: { NODE_REPL_HISTORY: defaultHistoryPath }, + skip: !process.features.inspector, + checkTotal: true, + test: [ + '[ ]', + WORD_LEFT, + WORD_LEFT, + UP, + ' = true', + ENTER, + '[ ]', + ENTER + ], + expected: [ + prompt, + '[', ' ', ']', + '\n// []', '\n// []', '\n// []', + '> util.inspect.replDefaults.showHidden', + '\n// false', ' ', '\n// false', + '=', ' ', 't', 'r', 'u', ' // e', 'e', + 'true\n', + '> ', '[', ' ', ']', + '\n// [ [length]: 0 ]', + '[ [length]: 0 ]\n', + '> ', ], clean: true }, @@ -300,6 +467,7 @@ const tests = [ prompt, 's', ' // Always visible', + prompt, ], clean: true } @@ -330,8 +498,8 @@ function runTest() { setImmediate(runTestWrap, true); return; } - const lastChunks = []; + let i = 0; REPL.createInternalRepl(opts.env, { input: new ActionStream(), @@ -344,11 +512,11 @@ function runTest() { return next(); } - lastChunks.push(inspect(output)); + lastChunks.push(output); - if (expected.length) { + if (expected.length && !opts.checkTotal) { try { - assert.strictEqual(output, expected[0]); + assert.strictEqual(output, expected[i]); } catch (e) { console.error(`Failed test # ${numtests - tests.length}`); console.error('Last outputs: ' + inspect(lastChunks, { @@ -356,7 +524,8 @@ function runTest() { })); throw e; } - expected.shift(); + // TODO(BridgeAR): Auto close on last chunk! + i++; } next(); @@ -365,6 +534,7 @@ function runTest() { completer: opts.completer, prompt, useColors: false, + preview: opts.preview, terminal: true }, function(err, repl) { if (err) { @@ -376,9 +546,13 @@ function runTest() { if (opts.clean) cleanupTmpFile(); - if (expected.length !== 0) { + if (opts.checkTotal) { + assert.deepStrictEqual(lastChunks, expected); + } else if (expected.length !== i) { + console.error(tests[numtests - tests.length - 1]); throw new Error(`Failed test # ${numtests - tests.length}`); } + setImmediate(runTestWrap, true); }); diff --git a/test/parallel/test-repl-persistent-history.js b/test/parallel/test-repl-persistent-history.js index bb10085eccfcf6..1d1261a3752365 100644 --- a/test/parallel/test-repl-persistent-history.js +++ b/test/parallel/test-repl-persistent-history.js @@ -10,6 +10,7 @@ const assert = require('assert'); const fs = require('fs'); const path = require('path'); const os = require('os'); +const util = require('util'); const tmpdir = require('../common/tmpdir'); tmpdir.refresh(); @@ -51,6 +52,7 @@ ActionStream.prototype.readable = true; // Mock keys const UP = { name: 'up' }; +const DOWN = { name: 'down' }; const ENTER = { name: 'enter' }; const CLEAR = { ctrl: true, name: 'u' }; @@ -90,20 +92,42 @@ const tests = [ }, { env: {}, - test: [UP, '\'42\'', ENTER], - expected: [prompt, '\'', '4', '2', '\'', '\'42\'\n', prompt, prompt], + test: [UP, '21', ENTER, "'42'", ENTER], + expected: [ + prompt, + '2', '1', '21\n', prompt, prompt, + "'", '4', '2', "'", "'42'\n", prompt, prompt + ], clean: false }, { // Requires the above test case env: {}, - test: [UP, UP, ENTER], - expected: [prompt, `${prompt}'42'`, '\'42\'\n', prompt] + test: [UP, UP, CLEAR, ENTER, DOWN, CLEAR, ENTER, UP, ENTER], + expected: [ + prompt, + `${prompt}'42'`, + `${prompt}21`, + prompt, + prompt, + `${prompt}'42'`, + prompt, + prompt, + `${prompt}21`, + '21\n', + prompt, + ] }, { env: { NODE_REPL_HISTORY: historyPath, NODE_REPL_HISTORY_SIZE: 1 }, - test: [UP, UP, CLEAR], - expected: [prompt, `${prompt}'you look fabulous today'`, prompt] + test: [UP, UP, DOWN, CLEAR], + expected: [ + prompt, + `${prompt}'you look fabulous today'`, + prompt, + `${prompt}'you look fabulous today'`, + prompt + ] }, { env: { NODE_REPL_HISTORY: historyPathFail, @@ -169,6 +193,8 @@ function runTest(assertCleaned) { const opts = tests.shift(); if (!opts) return; // All done + console.log('NEW'); + if (assertCleaned) { try { assert.strictEqual(fs.readFileSync(defaultHistoryPath, 'utf8'), ''); @@ -193,6 +219,7 @@ function runTest(assertCleaned) { output: new stream.Writable({ write(chunk, _, next) { const output = chunk.toString(); + console.log('INPUT', util.inspect(output)); // Ignore escapes and blank lines if (output.charCodeAt(0) === 27 || /^[\r\n]+$/.test(output)) @@ -207,7 +234,7 @@ function runTest(assertCleaned) { next(); } }), - prompt: prompt, + prompt, useColors: false, terminal: true }, function(err, repl) { diff --git a/test/parallel/test-repl-preview.js b/test/parallel/test-repl-preview.js index 4846248bdba294..1c6dfd76b89bc1 100644 --- a/test/parallel/test-repl-preview.js +++ b/test/parallel/test-repl-preview.js @@ -93,9 +93,9 @@ async function tests(options) { '\x1B[33mtrue\x1B[39m', '\x1B[1G\x1B[0Jrepl > \x1B[8G'], [' \t { a: true};', [2, 5], '\x1B[33mtrue\x1B[39m', - ' \t { a: tru\x1B[90me\x1B[39m\x1B[19G\x1B[0Ke}', - '\x1B[90m{ a: true }\x1B[39m\x1B[8C\x1B[1A\x1B[1B\x1B[2K\x1B[1A;', - '\x1B[90mtrue\x1B[39m\x1B[16C\x1B[1A\x1B[1B\x1B[2K\x1B[1A\r', + ' \t { a: tru\x1B[90me\x1B[39m\x1B[26G\x1B[0Ke}', + '\x1B[90m{ a: true }\x1B[39m\x1B[16C\x1B[1A\x1B[1B\x1B[2K\x1B[1A;', + '\x1B[90mtrue\x1B[39m\x1B[24C\x1B[1A\x1B[1B\x1B[2K\x1B[1A\r', '\x1B[33mtrue\x1B[39m', '\x1B[1G\x1B[0Jrepl > \x1B[8G'] ]; diff --git a/test/parallel/test-repl-programmatic-history.js b/test/parallel/test-repl-programmatic-history.js index 7eda401a7c386b..5307ae0556ae74 100644 --- a/test/parallel/test-repl-programmatic-history.js +++ b/test/parallel/test-repl-programmatic-history.js @@ -8,6 +8,7 @@ const assert = require('assert'); const fs = require('fs'); const path = require('path'); const os = require('os'); +const util = require('util'); const tmpdir = require('../common/tmpdir'); tmpdir.refresh(); @@ -49,6 +50,7 @@ ActionStream.prototype.readable = true; // Mock keys const UP = { name: 'up' }; +const DOWN = { name: 'down' }; const ENTER = { name: 'enter' }; const CLEAR = { ctrl: true, name: 'u' }; @@ -88,20 +90,40 @@ const tests = [ }, { env: {}, - test: [UP, '\'42\'', ENTER], - expected: [prompt, '\'', '4', '2', '\'', '\'42\'\n', prompt, prompt], + test: [UP, '21', ENTER, "'42'", ENTER], + expected: [ + prompt, + // TODO(BridgeAR): The line is refreshed too many times. The double prompt + // is redundant and can be optimized away. + '2', '1', '21\n', prompt, prompt, + "'", '4', '2', "'", "'42'\n", prompt, prompt + ], clean: false }, { // Requires the above test case env: {}, - test: [UP, UP, ENTER], - expected: [prompt, `${prompt}'42'`, '\'42\'\n', prompt] + test: [UP, UP, UP, DOWN, ENTER], + expected: [ + prompt, + `${prompt}'42'`, + `${prompt}21`, + prompt, + `${prompt}21`, + '21\n', + prompt + ] }, { env: { NODE_REPL_HISTORY: historyPath, NODE_REPL_HISTORY_SIZE: 1 }, - test: [UP, UP, CLEAR], - expected: [prompt, `${prompt}'you look fabulous today'`, prompt] + test: [UP, UP, DOWN, CLEAR], + expected: [ + prompt, + `${prompt}'you look fabulous today'`, + prompt, + `${prompt}'you look fabulous today'`, + prompt + ] }, { env: { NODE_REPL_HISTORY: historyPathFail, @@ -167,6 +189,8 @@ function runTest(assertCleaned) { const opts = tests.shift(); if (!opts) return; // All done + console.log('NEW'); + if (assertCleaned) { try { assert.strictEqual(fs.readFileSync(defaultHistoryPath, 'utf8'), ''); @@ -192,6 +216,7 @@ function runTest(assertCleaned) { output: new stream.Writable({ write(chunk, _, next) { const output = chunk.toString(); + console.log('INPUT', util.inspect(output)); // Ignore escapes and blank lines if (output.charCodeAt(0) === 27 || /^[\r\n]+$/.test(output)) diff --git a/test/parallel/test-repl-reverse-search.js b/test/parallel/test-repl-reverse-search.js index baafdc9d8fbd33..63cdcc82729597 100644 --- a/test/parallel/test-repl-reverse-search.js +++ b/test/parallel/test-repl-reverse-search.js @@ -309,10 +309,9 @@ function runTest() { lastChunks.push(output); - if (expected.length) { + if (expected.length && !opts.checkTotal) { try { - if (!opts.checkTotal) - assert.strictEqual(output, expected[i]); + assert.strictEqual(output, expected[i]); } catch (e) { console.error(`Failed test # ${numtests - tests.length}`); console.error('Last outputs: ' + inspect(lastChunks, { @@ -342,7 +341,8 @@ function runTest() { if (opts.checkTotal) { assert.deepStrictEqual(lastChunks, expected); - } else if (expected.length !== 0) { + } else if (expected.length !== i) { + console.error(tests[numtests - tests.length - 1]); throw new Error(`Failed test # ${numtests - tests.length}`); }