From 8b2be75505ac00749bb3f9f64be366a6578ac67f Mon Sep 17 00:00:00 2001 From: Snehil Shah Date: Sun, 7 Jul 2024 09:59:18 +0000 Subject: [PATCH 1/3] feat: allow cycling through multiline commands Signed-off-by: Snehil Shah --- .../@stdlib/repl/lib/multiline_handler.js | 214 ++++++++++++++++-- 1 file changed, 192 insertions(+), 22 deletions(-) diff --git a/lib/node_modules/@stdlib/repl/lib/multiline_handler.js b/lib/node_modules/@stdlib/repl/lib/multiline_handler.js index d3fa1a59c7c3..d3aaa4a59d88 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 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; + var i; + + // 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 till 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.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; + + // Update index to point to the next "previous" command: + this._history.index += 1; + 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; + var i; + + if ( this._history.index === 0 ) { + return; // no more history to traverse + } + // Traverse the history till 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.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; + 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; } From 0d2d95f46812f281d1f25e609d33dbce73c157f8 Mon Sep 17 00:00:00 2001 From: Snehil Shah Date: Sun, 7 Jul 2024 18:41:58 +0000 Subject: [PATCH 2/3] refactor: abstract duplicate logic into a method Signed-off-by: Snehil Shah --- .../@stdlib/repl/lib/multiline_handler.js | 62 +++++++++---------- 1 file changed, 31 insertions(+), 31 deletions(-) diff --git a/lib/node_modules/@stdlib/repl/lib/multiline_handler.js b/lib/node_modules/@stdlib/repl/lib/multiline_handler.js index d3aaa4a59d88..d778d561859c 100644 --- a/lib/node_modules/@stdlib/repl/lib/multiline_handler.js +++ b/lib/node_modules/@stdlib/repl/lib/multiline_handler.js @@ -162,6 +162,34 @@ setNonEnumerableReadOnly( MultilineHandler.prototype, '_moveCursor', function mo this._rli.cursor = x; }); +/** +* Inserts given command to input. +* +* @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. * @@ -173,7 +201,6 @@ setNonEnumerableReadOnly( MultilineHandler.prototype, '_moveCursor', function mo */ setNonEnumerableReadOnly( MultilineHandler.prototype, '_prevCommand', function prevCommand() { var cmd; - var i; // If we are starting from zero, save the prefix for this cycle... if ( this._history.index === 0 ) { @@ -183,22 +210,8 @@ setNonEnumerableReadOnly( MultilineHandler.prototype, '_prevCommand', function p 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.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; - - // Update index to point to the next "previous" command: - this._history.index += 1; + this._insertCommand( cmd ); + this._history.index += 1; // update index to point to the next "previous" command break; } this._history.index += 1; @@ -216,7 +229,6 @@ setNonEnumerableReadOnly( MultilineHandler.prototype, '_prevCommand', function p */ setNonEnumerableReadOnly( MultilineHandler.prototype, '_nextCommand', function nextCommand() { var cmd; - var i; if ( this._history.index === 0 ) { return; // no more history to traverse @@ -226,19 +238,7 @@ setNonEnumerableReadOnly( MultilineHandler.prototype, '_nextCommand', function n 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.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; + this._insertCommand( cmd ); break; } this._history.index -= 1; From d2f79081b01c38a5a0b82fd3dff60c615fa9dec2 Mon Sep 17 00:00:00 2001 From: Athan Date: Sat, 13 Jul 2024 23:53:43 -0700 Subject: [PATCH 3/3] Apply suggestions from code review Signed-off-by: Athan --- lib/node_modules/@stdlib/repl/lib/multiline_handler.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/node_modules/@stdlib/repl/lib/multiline_handler.js b/lib/node_modules/@stdlib/repl/lib/multiline_handler.js index d778d561859c..239496548394 100644 --- a/lib/node_modules/@stdlib/repl/lib/multiline_handler.js +++ b/lib/node_modules/@stdlib/repl/lib/multiline_handler.js @@ -163,7 +163,7 @@ setNonEnumerableReadOnly( MultilineHandler.prototype, '_moveCursor', function mo }); /** -* Inserts given command to input. +* Inserts a command in the input prompt. * * @private * @name _insertCommand @@ -206,7 +206,7 @@ setNonEnumerableReadOnly( MultilineHandler.prototype, '_prevCommand', function p if ( this._history.index === 0 ) { this._history.prefix = this._rli.line.slice( 0, this._rli.cursor ); } - // Traverse the history till we find the command with a common prefix... + // 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 ) ) { @@ -233,7 +233,7 @@ setNonEnumerableReadOnly( MultilineHandler.prototype, '_nextCommand', function n if ( this._history.index === 0 ) { return; // no more history to traverse } - // Traverse the history till we find the command with a common prefix... + // 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