Skip to content

Commit 01dcfe4

Browse files
committed
repl: extract and standardize history from both repl and interface
1 parent c46b2b9 commit 01dcfe4

File tree

6 files changed

+773
-254
lines changed

6 files changed

+773
-254
lines changed

doc/api/repl.md

+22-1
Original file line numberDiff line numberDiff line change
@@ -649,10 +649,31 @@ called from within the action function for commands registered using the
649649

650650
<!-- YAML
651651
added: v11.10.0
652+
changes:
653+
- version: REPLACEME
654+
pr-url: https://github.com/nodejs/node/pull/58225
655+
description: Updated the `historyPath` parameter to accept an object
656+
with `historyFile`, `historySize`, `removeHistoryDuplicates` and
657+
`onHistoryFileLoaded` properties.
652658
-->
653659

654-
* `historyPath` {string} the path to the history file
660+
* `historyPath` {Object|string} the path to the history file
661+
If it is a string, it is the path to the history file.
662+
If it is an object, it can have the following properties:
663+
* `historyFile` {string} the path to the history file
664+
* `historySize` {number} Maximum number of history lines retained. To disable
665+
the history set this value to `0`. This option makes sense only if
666+
`terminal` is set to `true` by the user or by an internal `output` check,
667+
otherwise the history caching mechanism is not initialized at all.
668+
**Default:** `30`.
669+
* `removeHistoryDuplicates` {boolean} If `true`, when a new input line added
670+
to the history list duplicates an older one, this removes the older line
671+
from the list. **Default:** `false`.
672+
* `onHistoryFileLoaded` {Function} called when history writes are ready or upon error
673+
* `err` {Error}
674+
* `repl` {repl.REPLServer}
655675
* `callback` {Function} called when history writes are ready or upon error
676+
(Optional if provided as `onHistoryFileLoaded` in `historyPath`)
656677
* `err` {Error}
657678
* `repl` {repl.REPLServer}
658679

lib/internal/readline/interface.js

+57-135
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,12 @@
33
const {
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 {
4644

4745
const {
4846
validateAbortSignal,
49-
validateArray,
50-
validateNumber,
5147
validateString,
5248
validateUint32,
5349
} = require('internal/validators');
@@ -64,7 +60,6 @@ const {
6460
charLengthLeft,
6561
commonPrefix,
6662
kSubstringSearch,
67-
reverseString,
6863
} = require('internal/readline/utils');
6964
let emitKeypressEvents;
7065
let kFirstEventParam;
@@ -75,8 +70,8 @@ const {
7570
} = require('internal/readline/callbacks');
7671

7772
const { StringDecoder } = require('string_decoder');
73+
const { ReplHistory } = require('internal/repl/history');
7874

79-
const kHistorySize = 30;
8075
const kMaxUndoRedoStackSize = 2048;
8176
const kMincrlfDelay = 100;
8277
/**
@@ -150,7 +145,6 @@ const kWriteToOutput = Symbol('_writeToOutput');
150145
const kYank = Symbol('_yank');
151146
const kYanking = Symbol('_yanking');
152147
const kYankPop = Symbol('_yankPop');
153-
const kNormalizeHistoryLineEndings = Symbol('_normalizeHistoryLineEndings');
154148
const kSavePreviousState = Symbol('_savePreviousState');
155149
const kRestorePreviousState = Symbol('_restorePreviousState');
156150
const kPreviousLine = Symbol('_previousLine');
@@ -172,9 +166,6 @@ function InterfaceConstructor(input, output, completer, terminal) {
172166

173167
FunctionPrototypeCall(EventEmitter, this);
174168

175-
let history;
176-
let historySize;
177-
let removeHistoryDuplicates = false;
178169
let crlfDelay;
179170
let prompt = '> ';
180171
let signal;
@@ -184,14 +175,17 @@ function InterfaceConstructor(input, output, completer, terminal) {
184175
output = input.output;
185176
completer = input.completer;
186177
terminal = input.terminal;
187-
history = input.history;
188-
historySize = input.historySize;
189178
signal = input.signal;
179+
180+
// It is possible to configure the history through the input object
181+
const historySize = input.historySize;
182+
const history = input.history;
183+
const removeHistoryDuplicates = input.removeHistoryDuplicates;
184+
190185
if (input.tabSize !== undefined) {
191186
validateUint32(input.tabSize, 'tabSize', true);
192187
this.tabSize = input.tabSize;
193188
}
194-
removeHistoryDuplicates = input.removeHistoryDuplicates;
195189
if (input.prompt !== undefined) {
196190
prompt = input.prompt;
197191
}
@@ -212,24 +206,18 @@ function InterfaceConstructor(input, output, completer, terminal) {
212206

213207
crlfDelay = input.crlfDelay;
214208
input = input.input;
215-
}
216209

217-
if (completer !== undefined && typeof completer !== 'function') {
218-
throw new ERR_INVALID_ARG_VALUE('completer', completer);
210+
input.historySize = historySize;
211+
input.history = history;
212+
input.removeHistoryDuplicates = removeHistoryDuplicates;
219213
}
220214

221-
if (history === undefined) {
222-
history = [];
223-
} else {
224-
validateArray(history, 'history');
225-
}
215+
this.setupHistoryManager(input);
226216

227-
if (historySize === undefined) {
228-
historySize = kHistorySize;
217+
if (completer !== undefined && typeof completer !== 'function') {
218+
throw new ERR_INVALID_ARG_VALUE('completer', completer);
229219
}
230220

231-
validateNumber(historySize, 'historySize', 0);
232-
233221
// Backwards compat; check the isTTY prop of the output stream
234222
// when `terminal` was not specified
235223
if (terminal === undefined && !(output === null || output === undefined)) {
@@ -245,8 +233,6 @@ function InterfaceConstructor(input, output, completer, terminal) {
245233
this.input = input;
246234
this[kUndoStack] = [];
247235
this[kRedoStack] = [];
248-
this.history = history;
249-
this.historySize = historySize;
250236
this[kPreviousCursorCols] = -1;
251237

252238
// The kill ring is a global list of blocks of text that were previously
@@ -257,7 +243,6 @@ function InterfaceConstructor(input, output, completer, terminal) {
257243
this[kKillRing] = [];
258244
this[kKillRingCursor] = 0;
259245

260-
this.removeHistoryDuplicates = !!removeHistoryDuplicates;
261246
this.crlfDelay = crlfDelay ?
262247
MathMax(kMincrlfDelay, crlfDelay) :
263248
kMincrlfDelay;
@@ -267,7 +252,6 @@ function InterfaceConstructor(input, output, completer, terminal) {
267252

268253
this.terminal = !!terminal;
269254

270-
271255
function onerror(err) {
272256
self.emit('error', err);
273257
}
@@ -346,8 +330,6 @@ function InterfaceConstructor(input, output, completer, terminal) {
346330
// Cursor position on the line.
347331
this.cursor = 0;
348332

349-
this.historyIndex = -1;
350-
351333
if (output !== null && output !== undefined)
352334
output.on('resize', onresize);
353335

@@ -400,6 +382,36 @@ class Interface extends InterfaceConstructor {
400382
return this[kPrompt];
401383
}
402384

385+
setupHistoryManager(options) {
386+
this.historyManager = new ReplHistory(this, options);
387+
388+
if (options.onHistoryFileLoaded) {
389+
this.historyManager.initialize(options.onHistoryFileLoaded);
390+
}
391+
392+
ObjectDefineProperty(this, 'history', {
393+
__proto__: null, configurable: true, enumerable: true,
394+
get() { return this.historyManager.getHistory(); },
395+
set(newHistory) { return this.historyManager.setHistory(newHistory); },
396+
});
397+
398+
ObjectDefineProperty(this, 'historyIndex', {
399+
__proto__: null, configurable: true, enumerable: true,
400+
get() { return this.historyManager.getHistoryIndex(); },
401+
set(historyIndex) { return this.historyManager.setHistoryIndex(historyIndex); },
402+
});
403+
404+
ObjectDefineProperty(this, 'historySize', {
405+
__proto__: null, configurable: true, enumerable: true,
406+
get() { return this.historyManager.getSize(); },
407+
});
408+
409+
ObjectDefineProperty(this, '_flushing', {
410+
__proto__: null, configurable: true, enumerable: true,
411+
get() { return this.historyManager.isFlushing(); },
412+
});
413+
}
414+
403415
[kSetRawMode](mode) {
404416
const wasInRawMode = this.input.isRaw;
405417

@@ -475,70 +487,8 @@ class Interface extends InterfaceConstructor {
475487
}
476488
}
477489

478-
// Convert newlines to a consistent format for history storage
479-
[kNormalizeHistoryLineEndings](line, from, to, reverse = true) {
480-
// Multiline history entries are saved reversed
481-
// History is structured with the newest entries at the top
482-
// and the oldest at the bottom. Multiline histories, however, only occupy
483-
// one line in the history file. When loading multiline history with
484-
// an old node binary, the history will be saved in the old format.
485-
// This is why we need to reverse the multilines.
486-
// Reversing the multilines is necessary when adding / editing and displaying them
487-
if (reverse) {
488-
// First reverse the lines for proper order, then convert separators
489-
return reverseString(line, from, to);
490-
}
491-
// For normal cases (saving to history or non-multiline entries)
492-
return StringPrototypeReplaceAll(line, from, to);
493-
}
494-
495490
[kAddHistory]() {
496-
if (this.line.length === 0) return '';
497-
498-
// If the history is disabled then return the line
499-
if (this.historySize === 0) return this.line;
500-
501-
// If the trimmed line is empty then return the line
502-
if (StringPrototypeTrim(this.line).length === 0) return this.line;
503-
504-
// This is necessary because each line would be saved in the history while creating
505-
// A new multiline, and we don't want that.
506-
if (this[kIsMultiline] && this.historyIndex === -1) {
507-
ArrayPrototypeShift(this.history);
508-
} else if (this[kLastCommandErrored]) {
509-
// If the last command errored and we are trying to edit the history to fix it
510-
// Remove the broken one from the history
511-
ArrayPrototypeShift(this.history);
512-
}
513-
514-
const normalizedLine = this[kNormalizeHistoryLineEndings](this.line, '\n', '\r', true);
515-
516-
if (this.history.length === 0 || this.history[0] !== normalizedLine) {
517-
if (this.removeHistoryDuplicates) {
518-
// Remove older history line if identical to new one
519-
const dupIndex = ArrayPrototypeIndexOf(this.history, this.line);
520-
if (dupIndex !== -1) ArrayPrototypeSplice(this.history, dupIndex, 1);
521-
}
522-
523-
// Add the new line to the history
524-
ArrayPrototypeUnshift(this.history, normalizedLine);
525-
526-
// Only store so many
527-
if (this.history.length > this.historySize)
528-
ArrayPrototypePop(this.history);
529-
}
530-
531-
this.historyIndex = -1;
532-
533-
// The listener could change the history object, possibly
534-
// to remove the last added entry if it is sensitive and should
535-
// not be persisted in the history, like a password
536-
const line = this[kIsMultiline] ? reverseString(this.history[0]) : this.history[0];
537-
538-
// Emit history event to notify listeners of update
539-
this.emit('history', this.history);
540-
541-
return line;
491+
return this.historyManager.addHistory(this[kIsMultiline], this[kLastCommandErrored]);
542492
}
543493

544494
[kRefreshLine]() {
@@ -1172,26 +1122,12 @@ class Interface extends InterfaceConstructor {
11721122
// <ctrl> + N. Only show this after two/three UPs or DOWNs, not on the first
11731123
// one.
11741124
[kHistoryNext]() {
1175-
if (this.historyIndex >= 0) {
1176-
this[kBeforeEdit](this.line, this.cursor);
1177-
const search = this[kSubstringSearch] || '';
1178-
let index = this.historyIndex - 1;
1179-
while (
1180-
index >= 0 &&
1181-
(!StringPrototypeStartsWith(this.history[index], search) ||
1182-
this.line === this.history[index])
1183-
) {
1184-
index--;
1185-
}
1186-
if (index === -1) {
1187-
this[kSetLine](search);
1188-
} else {
1189-
this[kSetLine](this[kNormalizeHistoryLineEndings](this.history[index], '\r', '\n'));
1190-
}
1191-
this.historyIndex = index;
1192-
this.cursor = this.line.length; // Set cursor to end of line.
1193-
this[kRefreshLine]();
1194-
}
1125+
if (!this.historyManager.canNavigateToNext()) { return; }
1126+
1127+
this[kBeforeEdit](this.line, this.cursor);
1128+
this[kSetLine](this.historyManager.navigateToNext(this[kSubstringSearch]));
1129+
this.cursor = this.line.length; // Set cursor to end of line.
1130+
this[kRefreshLine]();
11951131
}
11961132

11971133
[kMoveUpOrHistoryPrev]() {
@@ -1206,26 +1142,12 @@ class Interface extends InterfaceConstructor {
12061142
}
12071143

12081144
[kHistoryPrev]() {
1209-
if (this.historyIndex < this.history.length && this.history.length) {
1210-
this[kBeforeEdit](this.line, this.cursor);
1211-
const search = this[kSubstringSearch] || '';
1212-
let index = this.historyIndex + 1;
1213-
while (
1214-
index < this.history.length &&
1215-
(!StringPrototypeStartsWith(this.history[index], search) ||
1216-
this.line === this.history[index])
1217-
) {
1218-
index++;
1219-
}
1220-
if (index === this.history.length) {
1221-
this[kSetLine](search);
1222-
} else {
1223-
this[kSetLine](this[kNormalizeHistoryLineEndings](this.history[index], '\r', '\n'));
1224-
}
1225-
this.historyIndex = index;
1226-
this.cursor = this.line.length; // Set cursor to end of line.
1227-
this[kRefreshLine]();
1228-
}
1145+
if (!this.historyManager.canNavigateToPrevious()) { return; }
1146+
1147+
this[kBeforeEdit](this.line, this.cursor);
1148+
this[kSetLine](this.historyManager.navigateToPrevious(this[kSubstringSearch]));
1149+
this.cursor = this.line.length; // Set cursor to end of line.
1150+
this[kRefreshLine]();
12291151
}
12301152

12311153
// Returns the last character's display position of the given string

lib/internal/repl.js

+9-2
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,14 @@ function createRepl(env, opts, cb) {
4747
opts.historySize = 1000;
4848
}
4949

50-
const repl = REPL.start(opts);
5150
const term = 'terminal' in opts ? opts.terminal : process.stdout.isTTY;
52-
repl.setupHistory(term ? env.NODE_REPL_HISTORY : '', cb);
51+
opts.historyFile = term ? env.NODE_REPL_HISTORY : '';
52+
53+
const repl = REPL.start(opts);
54+
55+
repl.setupHistory({
56+
historyFile: opts.historyFile,
57+
historySize: opts.historySize,
58+
onHistoryFileLoaded: cb,
59+
});
5360
}

0 commit comments

Comments
 (0)