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' ) ;
@@ -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' ) ;
7974
8075const { StringDecoder } = require ( 'string_decoder' ) ;
76+ const { ReplHistory } = require ( 'internal/repl/history' ) ;
8177
82- 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) {
175169
176170 FunctionPrototypeCall ( EventEmitter , this ) ;
177171
178- 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) {
215209
216210 crlfDelay = input . crlfDelay ;
217211 input = input . input ;
218- }
219212
220- 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 }
223217
224- if ( history === undefined ) {
225- history = [ ] ;
226- } else {
227- validateArray ( history , 'history' ) ;
228- }
218+ this . setupHistoryManager ( input ) ;
229219
230- if ( historySize === undefined ) {
231- historySize = kHistorySize ;
220+ if ( completer !== undefined && typeof completer !== 'function' ) {
221+ throw new ERR_INVALID_ARG_VALUE ( 'completer' , completer ) ;
232222 }
233223
234- 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 ;
254240
255241 // 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 ;
262248
263- this . removeHistoryDuplicates = ! ! removeHistoryDuplicates ;
264249 this . crlfDelay = crlfDelay ?
265250 MathMax ( kMincrlfDelay , crlfDelay ) :
266251 kMincrlfDelay ;
@@ -270,7 +255,6 @@ function InterfaceConstructor(input, output, completer, terminal) {
270255
271256 this . terminal = ! ! terminal ;
272257
273-
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 ;
351335
352- 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 }
405387
388+ 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 }
480492
481- // 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 }
546496
547497 [ 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 }
12081144
12091145 [ kMoveUpOrHistoryPrev ] ( ) {
@@ -1218,26 +1154,12 @@ class Interface extends InterfaceConstructor {
12181154 }
12191155
12201156 [ 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 }
12421164
12431165 // Returns the last character's display position of the given string
0 commit comments