diff --git a/lib/node_modules/@stdlib/repl/lib/multiline_handler.js b/lib/node_modules/@stdlib/repl/lib/multiline_handler.js index d3fa1a59c7c3..239496548394 100644 --- a/lib/node_modules/@stdlib/repl/lib/multiline_handler.js +++ b/lib/node_modules/@stdlib/repl/lib/multiline_handler.js @@ -27,6 +27,7 @@ var logger = require( 'debug' ); var Parser = require( 'acorn' ).Parser; var parseLoose = require( 'acorn-loose' ).parse; var setNonEnumerableReadOnly = require( '@stdlib/utils/define-nonenumerable-read-only-property' ); +var startsWith = require( '@stdlib/string/starts-with' ); var copy = require( '@stdlib/array/base/copy' ); var min = require( '@stdlib/math/base/special/min' ); var max = require( '@stdlib/math/base/special/max' ); @@ -83,6 +84,12 @@ function MultilineHandler( repl, ttyWrite ) { // Cache a reference to the command queue: this._queue = repl._queue; + // Initialize an internal object for command history: + this._history = {}; + this._history.list = repl._history; + this._history.index = 0; // index points to the next "previous" command in history + this._history.prefix = ''; + // Initialize an internal status object for multi-line mode: this._multiline = {}; this._multiline.active = false; @@ -155,6 +162,95 @@ setNonEnumerableReadOnly( MultilineHandler.prototype, '_moveCursor', function mo this._rli.cursor = x; }); +/** +* Inserts a command in the input prompt. +* +* @private +* @name _insertCommand +* @memberof MultilineHandler.prototype +* @type {Function} +* @param {string} cmd - command +* @returns {void} +*/ +setNonEnumerableReadOnly( MultilineHandler.prototype, '_insertCommand', function insertCommand( cmd ) { + var i; + + this.clearInput(); + + // For each newline, trigger a `return` keypress in paste-mode... + cmd = cmd.split( '\n' ); + this._multiline.pasteMode = true; + for ( i = 0; i < cmd.length - 1; i++ ) { + this._rli.write( cmd[ i ] ); + this._rli.write( null, { + 'name': 'return' + }); + } + this._rli.write( cmd[ cmd.length - 1 ] ); + this._multiline.pasteMode = false; +}); + +/** +* Inserts previous command matching the prefix from history. +* +* @private +* @name _prevCommand +* @memberof MultilineHandler.prototype +* @type {Function} +* @returns {void} +*/ +setNonEnumerableReadOnly( MultilineHandler.prototype, '_prevCommand', function prevCommand() { + var cmd; + + // If we are starting from zero, save the prefix for this cycle... + if ( this._history.index === 0 ) { + this._history.prefix = this._rli.line.slice( 0, this._rli.cursor ); + } + // Traverse the history until we find the command with a common prefix... + while ( this._history.index < this._history.list.length / 3 ) { + cmd = this._history.list[ this._history.list.length - ( 3 * this._history.index ) - 2 ]; // eslint-disable-line max-len + if ( startsWith( cmd, this._history.prefix ) ) { + this._insertCommand( cmd ); + this._history.index += 1; // update index to point to the next "previous" command + break; + } + this._history.index += 1; + } +}); + +/** +* Inserts next command matching the prefix from history. +* +* @private +* @name _nextCommand +* @memberof MultilineHandler.prototype +* @type {Function} +* @returns {void} +*/ +setNonEnumerableReadOnly( MultilineHandler.prototype, '_nextCommand', function nextCommand() { + var cmd; + + if ( this._history.index === 0 ) { + return; // no more history to traverse + } + // Traverse the history until we find the command with a common prefix... + this._history.index -= 1; // updating index to point to the next "previous" command + while ( this._history.index > 0 ) { + cmd = this._history.list[ this._history.list.length - ( 3 * ( this._history.index - 1 ) ) - 2 ]; // eslint-disable-line max-len + if ( startsWith( cmd, this._history.prefix ) ) { + this._insertCommand( cmd ); + break; + } + this._history.index -= 1; + } + // If we didn't find a match in history, bring up the original prefix and reset cycle... + if ( this._history.index === 0 ) { + this.clearInput(); + this._rli.write( this._history.prefix ); + this._resetHistoryBuffers(); + } +}); + /** * Moves cursor up to the previous line. * @@ -167,8 +263,9 @@ setNonEnumerableReadOnly( MultilineHandler.prototype, '_moveCursor', function mo setNonEnumerableReadOnly( MultilineHandler.prototype, '_moveUp', function moveUp() { var cursor; - // If already at the first line, ignore... + // If already at the first line, try to insert previous command from history... if ( this._lineIndex <= 0 ) { + this._prevCommand(); return; } this._cmd[ this._lineIndex ] = this._rli.line; // update current line in command @@ -190,8 +287,9 @@ setNonEnumerableReadOnly( MultilineHandler.prototype, '_moveUp', function moveUp setNonEnumerableReadOnly( MultilineHandler.prototype, '_moveDown', function moveDown() { var cursor; - // If already at the last line, ignore... + // If already at the last line, try to insert next command from history... if ( this._lineIndex >= this._lines.length - 1 ) { + this._nextCommand(); return; } this._cmd[ this._lineIndex ] = this._rli.line; // update current line in command @@ -347,9 +445,37 @@ setNonEnumerableReadOnly( MultilineHandler.prototype, '_isMultilineInput', funct }); /** -* Returns current line number in input. +* Resets input buffers. +* +* @private +* @name _resetInputBuffers +* @memberof MultilineHandler.prototype +* @type {Function} +* @returns {void} +*/ +setNonEnumerableReadOnly( MultilineHandler.prototype, '_resetInputBuffers', function resetInputBuffers() { + this._cmd.length = 0; + this._lineIndex = 0; + this._lines.length = 0; +}); + +/** +* Resets history buffers. * * @private +* @name _resetHistoryBuffers +* @memberof MultilineHandler.prototype +* @type {Function} +* @returns {void} +*/ +setNonEnumerableReadOnly( MultilineHandler.prototype, '_resetHistoryBuffers', function resetHistoryBuffers() { + this._history.index = 0; + this._history.prefix = ''; +}); + +/** +* Returns current line number in input. +* * @name lineIndex * @memberof MultilineHandler.prototype * @type {Function} @@ -362,7 +488,6 @@ setNonEnumerableReadOnly( MultilineHandler.prototype, 'lineIndex', function line /** * Returns the number of rows occupied by current input. * -* @private * @name inputHeight * @memberof MultilineHandler.prototype * @type {Function} @@ -385,19 +510,39 @@ setNonEnumerableReadOnly( MultilineHandler.prototype, 'updateLine', function upd this._lines[ this._lineIndex ] = line; }); +/** +* Clears current input. +* +* @name clearInput +* @memberof MultilineHandler.prototype +* @type {Function} +* @returns {void} +*/ +setNonEnumerableReadOnly( MultilineHandler.prototype, 'clearInput', function clearInput() { + if ( this._lineIndex !== 0 ) { + // Bring the cursor to the first line: + readline.moveCursor( this._ostream, 0, -1 * this._lineIndex ); + } + // Clear lines and buffers: + this._resetInputBuffers(); + readline.cursorTo( this._ostream, this._repl.promptLength() ); + readline.clearLine( this._ostream, 1 ); + readline.clearScreenDown( this._ostream ); + this._rli.line = ''; + this._rli.cursor = 0; +}); + /** * Resets input and command buffers. * -* @private * @name resetInput * @memberof MultilineHandler.prototype * @type {Function} * @returns {void} */ setNonEnumerableReadOnly( MultilineHandler.prototype, 'resetInput', function resetInput() { - this._cmd.length = 0; - this._lineIndex = 0; - this._lines.length = 0; + this._resetHistoryBuffers(); + this._resetInputBuffers(); }); /** @@ -587,8 +732,9 @@ setNonEnumerableReadOnly( MultilineHandler.prototype, 'beforeKeypress', function this._ttyWrite.call( this._rli, data, key ); return; } + switch ( key.name ) { // Check whether to trigger multi-line mode or execute the command when `return` key is encountered... - if ( key.name === 'return' ) { + case 'return': cmd = copy( this._cmd ); cmd[ this._lineIndex ] = this._rli.line; @@ -598,53 +744,77 @@ setNonEnumerableReadOnly( MultilineHandler.prototype, 'beforeKeypress', function return; } this._triggerMultiline(); - + if ( this._history.index !== 0 && !this._multiline.pasteMode ) { + // Reset current history cycle: + this._resetHistoryBuffers(); + } // Trigger `line` event: this._ttyWrite.call( this._rli, data, key ); - return; - } - if ( !this._multiline.active ) { - this._ttyWrite.call( this._rli, data, key ); - return; - } + break; + // If multi-line mode is active, enable navigation... - switch ( key.name ) { case 'up': this._moveUp(); - this._renderLines(); + if ( this._multiline.active ) { + this._renderLines(); + } break; case 'down': this._moveDown(); - this._renderLines(); + if ( this._multiline.active ) { + this._renderLines(); + } break; case 'left': + if ( this._history.index !== 0 ) { + // Reset current history cycle: + this._resetHistoryBuffers(); + } // If at the beginning of the line, move up to the previous line; otherwise, trigger default behavior... if ( this._rli.cursor === 0 ) { this._moveLeft(); - this._renderLines(); + if ( this._multiline.active ) { + this._renderLines(); + } return; } this._ttyWrite.call( this._rli, data, key ); break; case 'right': + if ( this._history.index !== 0 ) { + // Reset current history cycle: + this._resetHistoryBuffers(); + } // If at the end of the line, move up to the next line; otherwise, trigger default behavior... if ( this._rli.cursor === this._rli.line.length ) { this._moveRight(); - this._renderLines(); + if ( this._multiline.active ) { + this._renderLines(); + } return; } this._ttyWrite.call( this._rli, data, key ); break; case 'backspace': + if ( this._history.index !== 0 ) { + // Reset current history cycle: + this._resetHistoryBuffers(); + } // If at the beginning of the line, remove and move up to the previous line; otherwise, trigger default behavior... if ( this._rli.cursor === 0 ) { this._backspace(); - this._renderLines(); + if ( this._multiline.active ) { + this._renderLines(); + } return; } this._ttyWrite.call( this._rli, data, key ); break; default: + if ( this._history.index !== 0 ) { + // Reset current history cycle: + this._resetHistoryBuffers(); + } this._ttyWrite.call( this._rli, data, key ); break; }