Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: allow cycling through multiline commands using up/down in the REPL #2531

Merged
merged 3 commits into from
Jul 16, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
214 changes: 192 additions & 22 deletions lib/node_modules/@stdlib/repl/lib/multiline_handler.js
Original file line number Diff line number Diff line change
Expand Up @@ -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' );
Expand Down Expand Up @@ -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 = {};
kgryte marked this conversation as resolved.
Show resolved Hide resolved
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;
Expand Down Expand Up @@ -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.
*
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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}
Expand All @@ -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}
Expand All @@ -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();
});

/**
Expand Down Expand Up @@ -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;

Expand All @@ -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;
}
Expand Down
Loading