repl: extract and standardize history from both repl and interface · nodejs/node@e3e36f9

@@ -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 {

46444745

const {

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');

7267

let emitKeypressEvents;

7368

let kFirstEventParam;

@@ -78,8 +73,8 @@ const {

7873

} = require('internal/readline/callbacks');

79748075

const { StringDecoder } = require('string_decoder');

76+

const { ReplHistory } = require('internal/repl/history');

817782-

const kHistorySize = 30;

8378

const kMaxUndoRedoStackSize = 2048;

8479

const kMincrlfDelay = 100;

8580

/**

@@ -153,7 +148,6 @@ const kWriteToOutput = Symbol('_writeToOutput');

153148

const kYank = Symbol('_yank');

154149

const kYanking = Symbol('_yanking');

155150

const kYankPop = Symbol('_yankPop');

156-

const kNormalizeHistoryLineEndings = Symbol('_normalizeHistoryLineEndings');

157151

const kSavePreviousState = Symbol('_savePreviousState');

158152

const kRestorePreviousState = Symbol('_restorePreviousState');

159153

const kPreviousLine = Symbol('_previousLine');

@@ -175,9 +169,6 @@ function InterfaceConstructor(input, output, completer, terminal) {

175169176170

FunctionPrototypeCall(EventEmitter, this);

177171178-

let history;

179-

let historySize;

180-

let removeHistoryDuplicates = false;

181172

let crlfDelay;

182173

let prompt = '> ';

183174

let signal;

@@ -187,14 +178,17 @@ function InterfaceConstructor(input, output, completer, terminal) {

187178

output = input.output;

188179

completer = input.completer;

189180

terminal = input.terminal;

190-

history = input.history;

191-

historySize = input.historySize;

192181

signal = 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+193188

if (input.tabSize !== undefined) {

194189

validateUint32(input.tabSize, 'tabSize', true);

195190

this.tabSize = input.tabSize;

196191

}

197-

removeHistoryDuplicates = input.removeHistoryDuplicates;

198192

if (input.prompt !== undefined) {

199193

prompt = input.prompt;

200194

}

@@ -215,24 +209,18 @@ function InterfaceConstructor(input, output, completer, terminal) {

215209216210

crlfDelay = input.crlfDelay;

217211

input = 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

238226

if (terminal === undefined && !(output === null || output === undefined)) {

@@ -248,8 +236,6 @@ function InterfaceConstructor(input, output, completer, terminal) {

248236

this.input = input;

249237

this[kUndoStack] = [];

250238

this[kRedoStack] = [];

251-

this.history = history;

252-

this.historySize = historySize;

253239

this[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) {

260246

this[kKillRing] = [];

261247

this[kKillRingCursor] = 0;

262248263-

this.removeHistoryDuplicates = !!removeHistoryDuplicates;

264249

this.crlfDelay = crlfDelay ?

265250

MathMax(kMincrlfDelay, crlfDelay) :

266251

kMincrlfDelay;

@@ -270,7 +255,6 @@ function InterfaceConstructor(input, output, completer, terminal) {

270255271256

this.terminal = !!terminal;

272257273-274258

function onerror(err) {

275259

self.emit('error', err);

276260

}

@@ -349,8 +333,6 @@ function InterfaceConstructor(input, output, completer, terminal) {

349333

// Cursor position on the line.

350334

this.cursor = 0;

351335352-

this.historyIndex = -1;

353-354336

if (output !== null && output !== undefined)

355337

output.on('resize', onresize);

356338

@@ -403,6 +385,36 @@ class Interface extends InterfaceConstructor {

403385

return 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) {

407419

const 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