Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

readline,repl: add substring history search #31112

Closed
Closed
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
9 changes: 5 additions & 4 deletions doc/api/repl.md
Expand Up @@ -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

Expand Down
89 changes: 44 additions & 45 deletions lib/internal/readline/utils.js
@@ -1,9 +1,8 @@
'use strict';

const {
Boolean,
NumberIsInteger,
RegExp,
Symbol,
} = primordials;

// Regex used for ansi escape code splitting
Expand All @@ -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}[`;
Expand All @@ -37,63 +36,62 @@ 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);

if (isFullWidthCodePoint(code)) {
for (const char of str) {
if (isFullWidthCodePoint(char.codePointAt(0))) {
width += 2;
} else {
width++;
Expand All @@ -107,10 +105,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) {
const isFullWidthCodePoint = (code) => {
// Code points are 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
Expand Down Expand Up @@ -466,11 +464,12 @@ function commonPrefix(strings) {
}

module.exports = {
charLengthAt,
charLengthLeft,
commonPrefix,
emitKeys,
getStringWidth,
isFullWidthCodePoint,
kUTF16SurrogateThreshold,
kSubstringSearch,
stripVTControlCharacters,
CSI
};