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 {
4644
4745const {
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' ) ;
6964let emitKeypressEvents ;
7065let kFirstEventParam ;
@@ -75,8 +70,8 @@ const {
7570} = require ( 'internal/readline/callbacks' ) ;
7671
7772const { StringDecoder } = require ( 'string_decoder' ) ;
73+ const { ReplHistory } = require ( 'internal/repl/history' ) ;
7874
79- const kHistorySize = 30 ;
8075const kMaxUndoRedoStackSize = 2048 ;
8176const kMincrlfDelay = 100 ;
8277/**
@@ -150,7 +145,6 @@ const kWriteToOutput = Symbol('_writeToOutput');
150145const kYank = Symbol ( '_yank' ) ;
151146const kYanking = Symbol ( '_yanking' ) ;
152147const kYankPop = Symbol ( '_yankPop' ) ;
153- const kNormalizeHistoryLineEndings = Symbol ( '_normalizeHistoryLineEndings' ) ;
154148const kSavePreviousState = Symbol ( '_savePreviousState' ) ;
155149const kRestorePreviousState = Symbol ( '_restorePreviousState' ) ;
156150const 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,31 @@ 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 . history ; } ,
395+ set ( newHistory ) { return this . historyManager . history = newHistory ; } ,
396+ } ) ;
397+
398+ ObjectDefineProperty ( this , 'historyIndex' , {
399+ __proto__ : null , configurable : true , enumerable : true ,
400+ get ( ) { return this . historyManager . index ; } ,
401+ set ( historyIndex ) { return this . historyManager . index = historyIndex ; } ,
402+ } ) ;
403+
404+ ObjectDefineProperty ( this , 'historySize' , {
405+ __proto__ : null , configurable : true , enumerable : true ,
406+ get ( ) { return this . historyManager . size ; } ,
407+ } ) ;
408+ }
409+
403410 [ kSetRawMode ] ( mode ) {
404411 const wasInRawMode = this . input . isRaw ;
405412
@@ -475,70 +482,8 @@ class Interface extends InterfaceConstructor {
475482 }
476483 }
477484
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-
495485 [ 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 ;
486+ return this . historyManager . addHistory ( this [ kIsMultiline ] , this [ kLastCommandErrored ] ) ;
542487 }
543488
544489 [ kRefreshLine ] ( ) {
@@ -1172,26 +1117,12 @@ class Interface extends InterfaceConstructor {
11721117 // <ctrl> + N. Only show this after two/three UPs or DOWNs, not on the first
11731118 // one.
11741119 [ 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- }
1120+ if ( ! this . historyManager . canNavigateToNext ( ) ) { return ; }
1121+
1122+ this [ kBeforeEdit ] ( this . line , this . cursor ) ;
1123+ this [ kSetLine ] ( this . historyManager . navigateToNext ( this [ kSubstringSearch ] ) ) ;
1124+ this . cursor = this . line . length ; // Set cursor to end of line.
1125+ this [ kRefreshLine ] ( ) ;
11951126 }
11961127
11971128 [ kMoveUpOrHistoryPrev ] ( ) {
@@ -1206,26 +1137,12 @@ class Interface extends InterfaceConstructor {
12061137 }
12071138
12081139 [ 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- }
1140+ if ( ! this . historyManager . canNavigateToPrevious ( ) ) { return ; }
1141+
1142+ this [ kBeforeEdit ] ( this . line , this . cursor ) ;
1143+ this [ kSetLine ] ( this . historyManager . navigateToPrevious ( this [ kSubstringSearch ] ) ) ;
1144+ this . cursor = this . line . length ; // Set cursor to end of line.
1145+ this [ kRefreshLine ] ( ) ;
12291146 }
12301147
12311148 // Returns the last character's display position of the given string
0 commit comments