repl: extract and standardize history from both repl and interface · nodejs/node@e3e36f9
@@ -3,14 +3,12 @@
33const {
44 ArrayFrom,
55 ArrayPrototypeFilter,
6- ArrayPrototypeIndexOf,
76 ArrayPrototypeJoin,
87 ArrayPrototypeMap,
98 ArrayPrototypePop,
109 ArrayPrototypePush,
1110 ArrayPrototypeReverse,
1211 ArrayPrototypeShift,
13- ArrayPrototypeSplice,
1412 ArrayPrototypeUnshift,
1513 DateNow,
1614 FunctionPrototypeCall,
@@ -19,6 +17,7 @@ const {
1917 MathMax,
2018 MathMaxApply,
2119 NumberIsFinite,
20+ ObjectDefineProperty,
2221 ObjectSetPrototypeOf,
2322 RegExpPrototypeExec,
2423 SafeStringIterator,
@@ -30,7 +29,6 @@ const {
3029 StringPrototypeSlice,
3130 StringPrototypeSplit,
3231 StringPrototypeStartsWith,
33- StringPrototypeTrim,
3432 Symbol,
3533 SymbolAsyncIterator,
3634 SymbolDispose,
@@ -46,8 +44,6 @@ const {
46444745const {
4846 validateAbortSignal,
49- validateArray,
50- validateNumber,
5147 validateString,
5248 validateUint32,
5349} = require('internal/validators');
@@ -67,7 +63,6 @@ const {
6763 charLengthLeft,
6864 commonPrefix,
6965 kSubstringSearch,
70- reverseString,
7166} = require('internal/readline/utils');
7267let emitKeypressEvents;
7368let kFirstEventParam;
@@ -78,8 +73,8 @@ const {
7873} = require('internal/readline/callbacks');
79748075const { StringDecoder } = require('string_decoder');
76+const { ReplHistory } = require('internal/repl/history');
817782-const kHistorySize = 30;
8378const kMaxUndoRedoStackSize = 2048;
8479const kMincrlfDelay = 100;
8580/**
@@ -153,7 +148,6 @@ const kWriteToOutput = Symbol('_writeToOutput');
153148const kYank = Symbol('_yank');
154149const kYanking = Symbol('_yanking');
155150const kYankPop = Symbol('_yankPop');
156-const kNormalizeHistoryLineEndings = Symbol('_normalizeHistoryLineEndings');
157151const kSavePreviousState = Symbol('_savePreviousState');
158152const kRestorePreviousState = Symbol('_restorePreviousState');
159153const kPreviousLine = Symbol('_previousLine');
@@ -175,9 +169,6 @@ function InterfaceConstructor(input, output, completer, terminal) {
175169176170FunctionPrototypeCall(EventEmitter, this);
177171178-let history;
179-let historySize;
180-let removeHistoryDuplicates = false;
181172let crlfDelay;
182173let prompt = '> ';
183174let signal;
@@ -187,14 +178,17 @@ function InterfaceConstructor(input, output, completer, terminal) {
187178output = input.output;
188179completer = input.completer;
189180terminal = input.terminal;
190-history = input.history;
191-historySize = input.historySize;
192181signal = input.signal;
182+183+// It is possible to configure the history through the input object
184+const historySize = input.historySize;
185+const history = input.history;
186+const removeHistoryDuplicates = input.removeHistoryDuplicates;
187+193188if (input.tabSize !== undefined) {
194189validateUint32(input.tabSize, 'tabSize', true);
195190this.tabSize = input.tabSize;
196191}
197-removeHistoryDuplicates = input.removeHistoryDuplicates;
198192if (input.prompt !== undefined) {
199193prompt = input.prompt;
200194}
@@ -215,24 +209,18 @@ function InterfaceConstructor(input, output, completer, terminal) {
215209216210crlfDelay = input.crlfDelay;
217211input = input.input;
218-}
219212220-if (completer !== undefined && typeof completer !== 'function') {
221-throw new ERR_INVALID_ARG_VALUE('completer', completer);
213+input.size = historySize;
214+input.history = history;
215+input.removeHistoryDuplicates = removeHistoryDuplicates;
222216}
223217224-if (history === undefined) {
225-history = [];
226-} else {
227-validateArray(history, 'history');
228-}
218+this.setupHistoryManager(input);
229219230-if (historySize === undefined) {
231-historySize = kHistorySize;
220+if (completer !== undefined && typeof completer !== 'function') {
221+throw new ERR_INVALID_ARG_VALUE('completer', completer);
232222}
233223234-validateNumber(historySize, 'historySize', 0);
235-236224// Backwards compat; check the isTTY prop of the output stream
237225// when `terminal` was not specified
238226if (terminal === undefined && !(output === null || output === undefined)) {
@@ -248,8 +236,6 @@ function InterfaceConstructor(input, output, completer, terminal) {
248236this.input = input;
249237this[kUndoStack] = [];
250238this[kRedoStack] = [];
251-this.history = history;
252-this.historySize = historySize;
253239this[kPreviousCursorCols] = -1;
254240255241// The kill ring is a global list of blocks of text that were previously
@@ -260,7 +246,6 @@ function InterfaceConstructor(input, output, completer, terminal) {
260246this[kKillRing] = [];
261247this[kKillRingCursor] = 0;
262248263-this.removeHistoryDuplicates = !!removeHistoryDuplicates;
264249this.crlfDelay = crlfDelay ?
265250MathMax(kMincrlfDelay, crlfDelay) :
266251kMincrlfDelay;
@@ -270,7 +255,6 @@ function InterfaceConstructor(input, output, completer, terminal) {
270255271256this.terminal = !!terminal;
272257273-274258function onerror(err) {
275259self.emit('error', err);
276260}
@@ -349,8 +333,6 @@ function InterfaceConstructor(input, output, completer, terminal) {
349333// Cursor position on the line.
350334this.cursor = 0;
351335352-this.historyIndex = -1;
353-354336if (output !== null && output !== undefined)
355337output.on('resize', onresize);
356338@@ -403,6 +385,36 @@ class Interface extends InterfaceConstructor {
403385return this[kPrompt];
404386}
405387388+setupHistoryManager(options) {
389+this.historyManager = new ReplHistory(this, options);
390+391+if (options.onHistoryFileLoaded) {
392+this.historyManager.initialize(options.onHistoryFileLoaded);
393+}
394+395+ObjectDefineProperty(this, 'history', {
396+__proto__: null, configurable: true, enumerable: true,
397+get() { return this.historyManager.history; },
398+set(newHistory) { return this.historyManager.history = newHistory; },
399+});
400+401+ObjectDefineProperty(this, 'historyIndex', {
402+__proto__: null, configurable: true, enumerable: true,
403+get() { return this.historyManager.index; },
404+set(historyIndex) { return this.historyManager.index = historyIndex; },
405+});
406+407+ObjectDefineProperty(this, 'historySize', {
408+__proto__: null, configurable: true, enumerable: true,
409+get() { return this.historyManager.size; },
410+});
411+412+ObjectDefineProperty(this, 'isFlushing', {
413+__proto__: null, configurable: true, enumerable: true,
414+get() { return this.historyManager.isFlushing; },
415+});
416+}
417+406418[kSetRawMode](mode) {
407419const wasInRawMode = this.input.isRaw;
408420@@ -478,70 +490,8 @@ class Interface extends InterfaceConstructor {
478490}
479491}
480492481-// Convert newlines to a consistent format for history storage
482-[kNormalizeHistoryLineEndings](line, from, to, reverse = true) {
483-// Multiline history entries are saved reversed
484-// History is structured with the newest entries at the top
485-// and the oldest at the bottom. Multiline histories, however, only occupy
486-// one line in the history file. When loading multiline history with
487-// an old node binary, the history will be saved in the old format.
488-// This is why we need to reverse the multilines.
489-// Reversing the multilines is necessary when adding / editing and displaying them
490-if (reverse) {
491-// First reverse the lines for proper order, then convert separators
492-return reverseString(line, from, to);
493-}
494-// For normal cases (saving to history or non-multiline entries)
495-return StringPrototypeReplaceAll(line, from, to);
496-}
497-498493[kAddHistory]() {
499-if (this.line.length === 0) return '';
500-501-// If the history is disabled then return the line
502-if (this.historySize === 0) return this.line;
503-504-// If the trimmed line is empty then return the line
505-if (StringPrototypeTrim(this.line).length === 0) return this.line;
506-507-// This is necessary because each line would be saved in the history while creating
508-// A new multiline, and we don't want that.
509-if (this[kIsMultiline] && this.historyIndex === -1) {
510-ArrayPrototypeShift(this.history);
511-} else if (this[kLastCommandErrored]) {
512-// If the last command errored and we are trying to edit the history to fix it
513-// Remove the broken one from the history
514-ArrayPrototypeShift(this.history);
515-}
516-517-const normalizedLine = this[kNormalizeHistoryLineEndings](this.line, '\n', '\r', true);
518-519-if (this.history.length === 0 || this.history[0] !== normalizedLine) {
520-if (this.removeHistoryDuplicates) {
521-// Remove older history line if identical to new one
522-const dupIndex = ArrayPrototypeIndexOf(this.history, this.line);
523-if (dupIndex !== -1) ArrayPrototypeSplice(this.history, dupIndex, 1);
524-}
525-526-// Add the new line to the history
527-ArrayPrototypeUnshift(this.history, normalizedLine);
528-529-// Only store so many
530-if (this.history.length > this.historySize)
531-ArrayPrototypePop(this.history);
532-}
533-534-this.historyIndex = -1;
535-536-// The listener could change the history object, possibly
537-// to remove the last added entry if it is sensitive and should
538-// not be persisted in the history, like a password
539-const line = this[kIsMultiline] ? reverseString(this.history[0]) : this.history[0];
540-541-// Emit history event to notify listeners of update
542-this.emit('history', this.history);
543-544-return line;
494+return this.historyManager.addHistory(this[kIsMultiline], this[kLastCommandErrored]);
545495}
546496547497[kRefreshLine]() {
@@ -1184,26 +1134,12 @@ class Interface extends InterfaceConstructor {
11841134// <ctrl> + N. Only show this after two/three UPs or DOWNs, not on the first
11851135// one.
11861136[kHistoryNext]() {
1187-if (this.historyIndex >= 0) {
1188-this[kBeforeEdit](this.line, this.cursor);
1189-const search = this[kSubstringSearch] || '';
1190-let index = this.historyIndex - 1;
1191-while (
1192-index >= 0 &&
1193-(!StringPrototypeStartsWith(this.history[index], search) ||
1194-this.line === this.history[index])
1195-) {
1196-index--;
1197-}
1198-if (index === -1) {
1199-this[kSetLine](search);
1200-} else {
1201-this[kSetLine](this[kNormalizeHistoryLineEndings](this.history[index], '\r', '\n'));
1202-}
1203-this.historyIndex = index;
1204-this.cursor = this.line.length; // Set cursor to end of line.
1205-this[kRefreshLine]();
1206-}
1137+if (!this.historyManager.canNavigateToNext()) { return; }
1138+1139+this[kBeforeEdit](this.line, this.cursor);
1140+this[kSetLine](this.historyManager.navigateToNext(this[kSubstringSearch]));
1141+this.cursor = this.line.length; // Set cursor to end of line.
1142+this[kRefreshLine]();
12071143}
1208114412091145[kMoveUpOrHistoryPrev]() {
@@ -1218,26 +1154,12 @@ class Interface extends InterfaceConstructor {
12181154}
1219115512201156[kHistoryPrev]() {
1221-if (this.historyIndex < this.history.length && this.history.length) {
1222-this[kBeforeEdit](this.line, this.cursor);
1223-const search = this[kSubstringSearch] || '';
1224-let index = this.historyIndex + 1;
1225-while (
1226-index < this.history.length &&
1227-(!StringPrototypeStartsWith(this.history[index], search) ||
1228-this.line === this.history[index])
1229-) {
1230-index++;
1231-}
1232-if (index === this.history.length) {
1233-this[kSetLine](search);
1234-} else {
1235-this[kSetLine](this[kNormalizeHistoryLineEndings](this.history[index], '\r', '\n'));
1236-}
1237-this.historyIndex = index;
1238-this.cursor = this.line.length; // Set cursor to end of line.
1239-this[kRefreshLine]();
1240-}
1157+if (!this.historyManager.canNavigateToPrevious()) { return; }
1158+1159+this[kBeforeEdit](this.line, this.cursor);
1160+this[kSetLine](this.historyManager.navigateToPrevious(this[kSubstringSearch]));
1161+this.cursor = this.line.length; // Set cursor to end of line.
1162+this[kRefreshLine]();
12411163}
1242116412431165// Returns the last character's display position of the given string