Skip to content

Commit 07e6c5a

Browse files
Snehil-Shahkgryte
andauthored
feat: allow cycling through multiline commands using up/down in the REPL
PR-URL: #2531 Closes: #2070 Ref: #2069 --------- Signed-off-by: Snehil Shah <[email protected]> Signed-off-by: Athan Reines <[email protected]> Co-authored-by: Athan Reines <[email protected]> Reviewed-by: Philipp Burckhardt <[email protected]> Reviewed-by: Athan Reines <[email protected]>
1 parent 91256d0 commit 07e6c5a

File tree

1 file changed

+192
-22
lines changed

1 file changed

+192
-22
lines changed

lib/node_modules/@stdlib/repl/lib/multiline_handler.js

+192-22
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ var logger = require( 'debug' );
2727
var Parser = require( 'acorn' ).Parser;
2828
var parseLoose = require( 'acorn-loose' ).parse;
2929
var setNonEnumerableReadOnly = require( '@stdlib/utils/define-nonenumerable-read-only-property' );
30+
var startsWith = require( '@stdlib/string/starts-with' );
3031
var copy = require( '@stdlib/array/base/copy' );
3132
var min = require( '@stdlib/math/base/special/min' );
3233
var max = require( '@stdlib/math/base/special/max' );
@@ -83,6 +84,12 @@ function MultilineHandler( repl, ttyWrite ) {
8384
// Cache a reference to the command queue:
8485
this._queue = repl._queue;
8586

87+
// Initialize an internal object for command history:
88+
this._history = {};
89+
this._history.list = repl._history;
90+
this._history.index = 0; // index points to the next "previous" command in history
91+
this._history.prefix = '';
92+
8693
// Initialize an internal status object for multi-line mode:
8794
this._multiline = {};
8895
this._multiline.active = false;
@@ -155,6 +162,95 @@ setNonEnumerableReadOnly( MultilineHandler.prototype, '_moveCursor', function mo
155162
this._rli.cursor = x;
156163
});
157164

165+
/**
166+
* Inserts a command in the input prompt.
167+
*
168+
* @private
169+
* @name _insertCommand
170+
* @memberof MultilineHandler.prototype
171+
* @type {Function}
172+
* @param {string} cmd - command
173+
* @returns {void}
174+
*/
175+
setNonEnumerableReadOnly( MultilineHandler.prototype, '_insertCommand', function insertCommand( cmd ) {
176+
var i;
177+
178+
this.clearInput();
179+
180+
// For each newline, trigger a `return` keypress in paste-mode...
181+
cmd = cmd.split( '\n' );
182+
this._multiline.pasteMode = true;
183+
for ( i = 0; i < cmd.length - 1; i++ ) {
184+
this._rli.write( cmd[ i ] );
185+
this._rli.write( null, {
186+
'name': 'return'
187+
});
188+
}
189+
this._rli.write( cmd[ cmd.length - 1 ] );
190+
this._multiline.pasteMode = false;
191+
});
192+
193+
/**
194+
* Inserts previous command matching the prefix from history.
195+
*
196+
* @private
197+
* @name _prevCommand
198+
* @memberof MultilineHandler.prototype
199+
* @type {Function}
200+
* @returns {void}
201+
*/
202+
setNonEnumerableReadOnly( MultilineHandler.prototype, '_prevCommand', function prevCommand() {
203+
var cmd;
204+
205+
// If we are starting from zero, save the prefix for this cycle...
206+
if ( this._history.index === 0 ) {
207+
this._history.prefix = this._rli.line.slice( 0, this._rli.cursor );
208+
}
209+
// Traverse the history until we find the command with a common prefix...
210+
while ( this._history.index < this._history.list.length / 3 ) {
211+
cmd = this._history.list[ this._history.list.length - ( 3 * this._history.index ) - 2 ]; // eslint-disable-line max-len
212+
if ( startsWith( cmd, this._history.prefix ) ) {
213+
this._insertCommand( cmd );
214+
this._history.index += 1; // update index to point to the next "previous" command
215+
break;
216+
}
217+
this._history.index += 1;
218+
}
219+
});
220+
221+
/**
222+
* Inserts next command matching the prefix from history.
223+
*
224+
* @private
225+
* @name _nextCommand
226+
* @memberof MultilineHandler.prototype
227+
* @type {Function}
228+
* @returns {void}
229+
*/
230+
setNonEnumerableReadOnly( MultilineHandler.prototype, '_nextCommand', function nextCommand() {
231+
var cmd;
232+
233+
if ( this._history.index === 0 ) {
234+
return; // no more history to traverse
235+
}
236+
// Traverse the history until we find the command with a common prefix...
237+
this._history.index -= 1; // updating index to point to the next "previous" command
238+
while ( this._history.index > 0 ) {
239+
cmd = this._history.list[ this._history.list.length - ( 3 * ( this._history.index - 1 ) ) - 2 ]; // eslint-disable-line max-len
240+
if ( startsWith( cmd, this._history.prefix ) ) {
241+
this._insertCommand( cmd );
242+
break;
243+
}
244+
this._history.index -= 1;
245+
}
246+
// If we didn't find a match in history, bring up the original prefix and reset cycle...
247+
if ( this._history.index === 0 ) {
248+
this.clearInput();
249+
this._rli.write( this._history.prefix );
250+
this._resetHistoryBuffers();
251+
}
252+
});
253+
158254
/**
159255
* Moves cursor up to the previous line.
160256
*
@@ -167,8 +263,9 @@ setNonEnumerableReadOnly( MultilineHandler.prototype, '_moveCursor', function mo
167263
setNonEnumerableReadOnly( MultilineHandler.prototype, '_moveUp', function moveUp() {
168264
var cursor;
169265

170-
// If already at the first line, ignore...
266+
// If already at the first line, try to insert previous command from history...
171267
if ( this._lineIndex <= 0 ) {
268+
this._prevCommand();
172269
return;
173270
}
174271
this._cmd[ this._lineIndex ] = this._rli.line; // update current line in command
@@ -190,8 +287,9 @@ setNonEnumerableReadOnly( MultilineHandler.prototype, '_moveUp', function moveUp
190287
setNonEnumerableReadOnly( MultilineHandler.prototype, '_moveDown', function moveDown() {
191288
var cursor;
192289

193-
// If already at the last line, ignore...
290+
// If already at the last line, try to insert next command from history...
194291
if ( this._lineIndex >= this._lines.length - 1 ) {
292+
this._nextCommand();
195293
return;
196294
}
197295
this._cmd[ this._lineIndex ] = this._rli.line; // update current line in command
@@ -347,9 +445,37 @@ setNonEnumerableReadOnly( MultilineHandler.prototype, '_isMultilineInput', funct
347445
});
348446

349447
/**
350-
* Returns current line number in input.
448+
* Resets input buffers.
449+
*
450+
* @private
451+
* @name _resetInputBuffers
452+
* @memberof MultilineHandler.prototype
453+
* @type {Function}
454+
* @returns {void}
455+
*/
456+
setNonEnumerableReadOnly( MultilineHandler.prototype, '_resetInputBuffers', function resetInputBuffers() {
457+
this._cmd.length = 0;
458+
this._lineIndex = 0;
459+
this._lines.length = 0;
460+
});
461+
462+
/**
463+
* Resets history buffers.
351464
*
352465
* @private
466+
* @name _resetHistoryBuffers
467+
* @memberof MultilineHandler.prototype
468+
* @type {Function}
469+
* @returns {void}
470+
*/
471+
setNonEnumerableReadOnly( MultilineHandler.prototype, '_resetHistoryBuffers', function resetHistoryBuffers() {
472+
this._history.index = 0;
473+
this._history.prefix = '';
474+
});
475+
476+
/**
477+
* Returns current line number in input.
478+
*
353479
* @name lineIndex
354480
* @memberof MultilineHandler.prototype
355481
* @type {Function}
@@ -362,7 +488,6 @@ setNonEnumerableReadOnly( MultilineHandler.prototype, 'lineIndex', function line
362488
/**
363489
* Returns the number of rows occupied by current input.
364490
*
365-
* @private
366491
* @name inputHeight
367492
* @memberof MultilineHandler.prototype
368493
* @type {Function}
@@ -385,19 +510,39 @@ setNonEnumerableReadOnly( MultilineHandler.prototype, 'updateLine', function upd
385510
this._lines[ this._lineIndex ] = line;
386511
});
387512

513+
/**
514+
* Clears current input.
515+
*
516+
* @name clearInput
517+
* @memberof MultilineHandler.prototype
518+
* @type {Function}
519+
* @returns {void}
520+
*/
521+
setNonEnumerableReadOnly( MultilineHandler.prototype, 'clearInput', function clearInput() {
522+
if ( this._lineIndex !== 0 ) {
523+
// Bring the cursor to the first line:
524+
readline.moveCursor( this._ostream, 0, -1 * this._lineIndex );
525+
}
526+
// Clear lines and buffers:
527+
this._resetInputBuffers();
528+
readline.cursorTo( this._ostream, this._repl.promptLength() );
529+
readline.clearLine( this._ostream, 1 );
530+
readline.clearScreenDown( this._ostream );
531+
this._rli.line = '';
532+
this._rli.cursor = 0;
533+
});
534+
388535
/**
389536
* Resets input and command buffers.
390537
*
391-
* @private
392538
* @name resetInput
393539
* @memberof MultilineHandler.prototype
394540
* @type {Function}
395541
* @returns {void}
396542
*/
397543
setNonEnumerableReadOnly( MultilineHandler.prototype, 'resetInput', function resetInput() {
398-
this._cmd.length = 0;
399-
this._lineIndex = 0;
400-
this._lines.length = 0;
544+
this._resetHistoryBuffers();
545+
this._resetInputBuffers();
401546
});
402547

403548
/**
@@ -587,8 +732,9 @@ setNonEnumerableReadOnly( MultilineHandler.prototype, 'beforeKeypress', function
587732
this._ttyWrite.call( this._rli, data, key );
588733
return;
589734
}
735+
switch ( key.name ) {
590736
// Check whether to trigger multi-line mode or execute the command when `return` key is encountered...
591-
if ( key.name === 'return' ) {
737+
case 'return':
592738
cmd = copy( this._cmd );
593739
cmd[ this._lineIndex ] = this._rli.line;
594740

@@ -598,53 +744,77 @@ setNonEnumerableReadOnly( MultilineHandler.prototype, 'beforeKeypress', function
598744
return;
599745
}
600746
this._triggerMultiline();
601-
747+
if ( this._history.index !== 0 && !this._multiline.pasteMode ) {
748+
// Reset current history cycle:
749+
this._resetHistoryBuffers();
750+
}
602751
// Trigger `line` event:
603752
this._ttyWrite.call( this._rli, data, key );
604-
return;
605-
}
606-
if ( !this._multiline.active ) {
607-
this._ttyWrite.call( this._rli, data, key );
608-
return;
609-
}
753+
break;
754+
610755
// If multi-line mode is active, enable navigation...
611-
switch ( key.name ) {
612756
case 'up':
613757
this._moveUp();
614-
this._renderLines();
758+
if ( this._multiline.active ) {
759+
this._renderLines();
760+
}
615761
break;
616762
case 'down':
617763
this._moveDown();
618-
this._renderLines();
764+
if ( this._multiline.active ) {
765+
this._renderLines();
766+
}
619767
break;
620768
case 'left':
769+
if ( this._history.index !== 0 ) {
770+
// Reset current history cycle:
771+
this._resetHistoryBuffers();
772+
}
621773
// If at the beginning of the line, move up to the previous line; otherwise, trigger default behavior...
622774
if ( this._rli.cursor === 0 ) {
623775
this._moveLeft();
624-
this._renderLines();
776+
if ( this._multiline.active ) {
777+
this._renderLines();
778+
}
625779
return;
626780
}
627781
this._ttyWrite.call( this._rli, data, key );
628782
break;
629783
case 'right':
784+
if ( this._history.index !== 0 ) {
785+
// Reset current history cycle:
786+
this._resetHistoryBuffers();
787+
}
630788
// If at the end of the line, move up to the next line; otherwise, trigger default behavior...
631789
if ( this._rli.cursor === this._rli.line.length ) {
632790
this._moveRight();
633-
this._renderLines();
791+
if ( this._multiline.active ) {
792+
this._renderLines();
793+
}
634794
return;
635795
}
636796
this._ttyWrite.call( this._rli, data, key );
637797
break;
638798
case 'backspace':
799+
if ( this._history.index !== 0 ) {
800+
// Reset current history cycle:
801+
this._resetHistoryBuffers();
802+
}
639803
// If at the beginning of the line, remove and move up to the previous line; otherwise, trigger default behavior...
640804
if ( this._rli.cursor === 0 ) {
641805
this._backspace();
642-
this._renderLines();
806+
if ( this._multiline.active ) {
807+
this._renderLines();
808+
}
643809
return;
644810
}
645811
this._ttyWrite.call( this._rli, data, key );
646812
break;
647813
default:
814+
if ( this._history.index !== 0 ) {
815+
// Reset current history cycle:
816+
this._resetHistoryBuffers();
817+
}
648818
this._ttyWrite.call( this._rli, data, key );
649819
break;
650820
}

0 commit comments

Comments
 (0)