Skip to content

feat: add bracketed-paste mode in the REPL #2502

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

Merged
merged 13 commits into from
Jul 5, 2024
Merged
Show file tree
Hide file tree
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
2 changes: 2 additions & 0 deletions lib/node_modules/@stdlib/repl/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,8 @@ The function supports specifying the following settings:
- **autoClosePairs**: boolean indicating whether to automatically insert matching brackets, parentheses, and quotes. Default: `true`.
- **autoDeletePairs**: boolean indicating whether to automatically delete adjacent matching brackets, parentheses, and quotes. Default: `true`.
- **autoPage**: boolean indicating whether to automatically page return values having a display size exceeding the visible screen. When streams are TTY, the default is `true`; otherwise, the default is `false`.
- **bracketedPaste**: boolean indicating whether to enable bracketed-paste mode. When streams are TTY, the default is `true`; otherwise, the default is `false`.
- **autoDisableBracketedPasteOnExit**: boolean indicating whether to automatically disable bracketed-paste upon exiting the REPL. When streams are TTY and bracketed paste is enabled, the default is `true`; otherwise, the default is `false`.
- **completionPreviews**: boolean indicating whether to display completion previews for auto-completion. When streams are TTY, the default is `true`; otherwise, the default is `false`.
- **syntaxHighlighting**: boolean indicating whether to enable syntax highlighting of entered input expressions. When streams are TTY, the default is `true`; otherwise, the default is `false`.
- **theme**: initial color theme for syntax highlighting. Default: `stdlib-ansi-basic`.
Expand Down
12 changes: 10 additions & 2 deletions lib/node_modules/@stdlib/repl/lib/auto_close_pairs.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,17 +65,19 @@ function isQuote( ch ) {
* @param {Object} rli - readline instance
* @param {boolean} autoClose - boolean indicating whether auto-closing should be initially enabled
* @param {boolean} autoDelete - boolean indicating whether auto-deleting should be initially enabled
* @param {MultilineHandler} multiline - multiline handler instance
* @returns {AutoCloser} auto-closer instance
*/
function AutoCloser( rli, autoClose, autoDelete ) {
function AutoCloser( rli, autoClose, autoDelete, multiline ) {
if ( !(this instanceof AutoCloser) ) {
return new AutoCloser( rli, autoClose, autoDelete );
return new AutoCloser( rli, autoClose, autoDelete, multiline );
}
debug( 'Creating an auto-closer...' );
this._rli = rli;
this._ignoreBackspace = false;
this._autoClose = autoClose;
this._autoDelete = autoDelete;
this._multiline = multiline;
return this;
}

Expand Down Expand Up @@ -316,6 +318,9 @@ setNonEnumerableReadOnly( AutoCloser.prototype, 'beforeKeypress', function befor
if ( !this._autoDelete ) {
return false;
}
if ( this._multiline.isPasting() ) {
return false;
}
if ( !key || key.name !== 'backspace' ) {
return false;
}
Expand Down Expand Up @@ -360,6 +365,9 @@ setNonEnumerableReadOnly( AutoCloser.prototype, 'onKeypress', function onKeypres
if ( !this._autoClose ) {
return false;
}
if ( this._multiline.isPasting() ) {
return false;
}
cursor = this._rli.cursor;
line = this._rli.line;

Expand Down
12 changes: 11 additions & 1 deletion lib/node_modules/@stdlib/repl/lib/completer_engine.js
Original file line number Diff line number Diff line change
Expand Up @@ -628,7 +628,6 @@ setNonEnumerableReadOnly( CompleterEngine.prototype, 'beforeKeypress', function
this._ttyWrite.call( this._rli, data, key );
return;
}

// If user is already viewing completions, allow navigating it...
if ( this._isNavigating ) {
switch ( key.name ) {
Expand All @@ -638,6 +637,12 @@ setNonEnumerableReadOnly( CompleterEngine.prototype, 'beforeKeypress', function
this.closeCompleter();
break;

// If paste sequences detected, close the completer:
case 'paste-start':
this.closeCompleter();
this._ttyWrite.call( this._rli, data, key );
break;

// If arrow keys detected, allow navigating the completions...
case 'down':
debug( 'Received a DOWN keypress event...' );
Expand All @@ -660,6 +665,11 @@ setNonEnumerableReadOnly( CompleterEngine.prototype, 'beforeKeypress', function
}
return;
}
// If we are in the middle of receiving pasted input, use TAB for indentation and don't trigger completions...
if ( this._multiline.isPasting() ) {
this._ttyWrite.call( this._rli, data, key );
return;
}
// Trigger TAB completions:
cursor = this._rli.cursor;
line = this._rli.line;
Expand Down
6 changes: 6 additions & 0 deletions lib/node_modules/@stdlib/repl/lib/defaults.js
Original file line number Diff line number Diff line change
Expand Up @@ -89,9 +89,15 @@ function defaults() {
// Flag indicating whether to enable automatically page return values requiring a display size exceeding the visible screen (note: default depends on whether TTY):
'autoPage': void 0,

// Flag indicating whether to enable bracketed-paste mode (note: default depends on whether TTY):
'bracketedPaste': void 0,

// Flag indicating whether to enable the display of completion previews for auto-completion (note: default depends on whether TTY):
'completionPreviews': void 0,

// Flag indicating whether to automatically disable bracketed-paste upon exiting the REPL (note: default depends on whether TTY):
'autoDisableBracketedPasteOnExit': void 0,

// Flag indicating whether to enable syntax highlighting (note: default depends on whether TTY):
'syntaxHighlighting': void 0,

Expand Down
21 changes: 19 additions & 2 deletions lib/node_modules/@stdlib/repl/lib/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
* limitations under the License.
*/

/* eslint-disable no-restricted-syntax, no-invalid-this, no-underscore-dangle, max-lines */
/* eslint-disable no-restricted-syntax, no-invalid-this, no-underscore-dangle, max-lines, max-lines-per-function */

'use strict';

Expand Down Expand Up @@ -102,7 +102,9 @@
* @param {boolean} [options.settings.autoClosePairs=true] - boolean indicating whether to automatically insert matching brackets, parentheses, and quotes
* @param {boolean} [options.settings.autoDeletePairs=true] - boolean indicating whether to automatically delete adjacent matching brackets, parentheses, and quotes
* @param {boolean} [options.settings.autoPage] - boolean indicating whether to automatically page return values requiring a display size exceeding the visible screen
* @param {boolean} [options.settings.bracketedPaste] - boolean indicating whether to enable bracketed-paste mode
* @param {boolean} [options.settings.completionPreviews] - boolean indicating whether to enable completion previews for auto-completion
* @param {boolean} [options.settings.autoDisableBracketedPasteOnExit] - boolean indicating whether to automatically disable bracketed-paste upon exiting the REPL
* @param {boolean} [options.settings.syntaxHighlighting] - boolean indicating whether to enable syntax highlighting
* @param {string} [options.settings.theme] - initial color theme for syntax highlighting
* @throws {Error} must provide valid options
Expand Down Expand Up @@ -151,7 +153,9 @@
}
opts.isTTY = ( opts.isTTY === void 0 ) ? opts.output.isTTY : opts.isTTY;
opts.settings.autoPage = ( opts.settings.autoPage === void 0 ) ? opts.isTTY : opts.settings.autoPage; // eslint-disable-line max-len
opts.settings.bracketedPaste = ( opts.settings.bracketedPaste === void 0 && opts.isTTY ) ? true : opts.settings.bracketedPaste; // eslint-disable-line max-len
opts.settings.completionPreviews = ( opts.settings.completionPreviews === void 0 ) ? opts.isTTY : opts.settings.completionPreviews; // eslint-disable-line max-len
opts.settings.autoDisableBracketedPasteOnExit = ( opts.settings.autoDisableBracketedPasteOnExit === void 0 ) ? opts.isTTY : opts.settings.autoDisableBracketedPasteOnExit; // eslint-disable-line max-len
opts.settings.syntaxHighlighting = ( opts.settings.syntaxHighlighting === void 0 ) ? opts.isTTY : opts.settings.syntaxHighlighting; // eslint-disable-line max-len

debug( 'Options: %s', JSON.stringify({
Expand Down Expand Up @@ -275,7 +279,7 @@
setNonEnumerableReadOnly( this, '_completerEngine', new CompleterEngine( this, this._completer, this._wstream, this._rli._ttyWrite ) );

// Create a new auto-closer:
setNonEnumerableReadOnly( this, '_autoCloser', new AutoCloser( this._rli, this._settings.autoClosePairs, this._settings.autoDeletePairs ) );
setNonEnumerableReadOnly( this, '_autoCloser', new AutoCloser( this._rli, this._settings.autoClosePairs, this._settings.autoDeletePairs, this._multilineHandler ) );

// Initialize a preview completer:
setNonEnumerableReadOnly( this, '_previewCompleter', new PreviewCompleter( this._rli, this._completer, this._ostream, this._settings.completionPreviews ) );
Expand All @@ -287,7 +291,7 @@
setNonEnumerableReadOnly( this, '_ttyWrite', this._rli._ttyWrite );

// Overwrite the private `ttyWrite` method to allow processing input before a "keypress" event is triggered:
this._rli._ttyWrite = beforeKeypress; // WARNING: overwriting a private property

Check warning on line 294 in lib/node_modules/@stdlib/repl/lib/main.js

View workflow job for this annotation

GitHub Actions / Lint Changed Files

Unexpected 'warning' comment: 'WARNING: overwriting a private property'

// Add event listeners:
this._rli.on( 'close', onClose );
Expand All @@ -307,9 +311,9 @@
// Write a welcome message:
this._wstream.write( opts.welcome );

// TODO: check whether to synchronously initialize a REPL history file

Check warning on line 314 in lib/node_modules/@stdlib/repl/lib/main.js

View workflow job for this annotation

GitHub Actions / Lint Changed Files

Unexpected 'todo' comment: 'TODO: check whether to synchronously...'

// TODO: check whether to synchronously initialize a REPL log file

Check warning on line 316 in lib/node_modules/@stdlib/repl/lib/main.js

View workflow job for this annotation

GitHub Actions / Lint Changed Files

Unexpected 'todo' comment: 'TODO: check whether to synchronously...'

// Add any provided user-defined themes...
if ( opts.themes ) {
Expand All @@ -321,6 +325,10 @@
// Set the syntax highlighting theme...
this.settings( 'theme', opts.settings.theme );

// Initialize bracketed-paste:
if ( opts.settings.bracketedPaste !== void 0 ) {
this.settings( 'bracketedPaste', opts.settings.bracketedPaste );
}
// Check whether to load and execute a JavaScript file (e.g., prior REPL history) upon startup...
if ( opts.load ) {
this.load( opts.load );
Expand Down Expand Up @@ -404,6 +412,9 @@
* @private
*/
function onClose() {
if ( self._settings.bracketedPaste && self._settings.autoDisableBracketedPasteOnExit ) { // eslint-disable-line max-len
self._multilineHandler.disableBracketedPaste();
}
ostream.end();
ostream.unpipe();

Expand Down Expand Up @@ -488,9 +499,9 @@
// Update the internal command history buffer: [..., <id>, <cmd>, <success>, ...]
self._history.push( self._count, cmd, success );

// TODO: if successful and if necessary, (asynchronously?) write the command to a history file (question: do we only want to write successful commands to the history file? maybe we need to option for limiting to successful commands?)

Check warning on line 502 in lib/node_modules/@stdlib/repl/lib/main.js

View workflow job for this annotation

GitHub Actions / Lint Changed Files

Unexpected 'todo' comment: 'TODO: if successful and if necessary,...'

// TODO: if necessary, (asynchronously?) write the command and result to a log file (JSON serialization?)

Check warning on line 504 in lib/node_modules/@stdlib/repl/lib/main.js

View workflow job for this annotation

GitHub Actions / Lint Changed Files

Unexpected 'todo' comment: 'TODO: if necessary, (asynchronously?)...'
}
}

Expand Down Expand Up @@ -729,7 +740,7 @@

// Before creating a new execution context in a non-sandboxed environment, remove current workspace variables in order to allow garbage collection and avoid memory leaks (e.g., variables/functions declared during a REPL session which might remain bound to the environment `global` after clearing a REPL):
if ( this._sandbox === false ) {
// WARNING: in a non-sandboxed environment, if a global variable is externally introduced during a REPL session (i.e., introduced via a mechanism outside of the REPL environment), we will delete that global variable, which means the following logic may introduce unintended side-effects for this particular edge case (e.g., application code may expect the presence of the subsequently deleted global variable). While not ideal, (a) user applications should not be introducing globals to begin with and (b) the probability of a user running a REPL session, a user clearing that REPL session, AND a global variable being introduced between starting a REPL and clearing the REPL should be negligible.

Check warning on line 743 in lib/node_modules/@stdlib/repl/lib/main.js

View workflow job for this annotation

GitHub Actions / Lint Changed Files

Unexpected 'warning' comment: 'WARNING: in a non-sandboxed environment,...'
tmp = this._context.vars();
for ( i = 0; i < tmp.length; i++ ) {
if ( isConfigurableProperty( this._context, tmp[ i ] ) ) {
Expand Down Expand Up @@ -1333,9 +1344,9 @@
// Clear the command queue:
this._queue.clear();

// TODO: ensure REPL history is saved (flushed) to file before closing the REPL (see https://github.com/nodejs/node/blob/b21e7c7bcf23a2715951e4cd96180e4dbf1dcd4d/lib/repl.js#L805)

Check warning on line 1347 in lib/node_modules/@stdlib/repl/lib/main.js

View workflow job for this annotation

GitHub Actions / Lint Changed Files

Unexpected 'todo' comment: 'TODO: ensure REPL history is saved...'

// TODO: ensure REPL log is saved (flushed) to file before closing the REPL

Check warning on line 1349 in lib/node_modules/@stdlib/repl/lib/main.js

View workflow job for this annotation

GitHub Actions / Lint Changed Files

Unexpected 'todo' comment: 'TODO: ensure REPL log is saved (flushed)...'

nextTick( onTick );

Expand All @@ -1358,7 +1369,7 @@

// If this is a non-sandboxed REPL, remove global variables/properties which were introduced during context creation and by a user during a REPL session...
if ( self._sandbox === false ) {
// WARNING: in a non-sandboxed environment, if a global variable is externally introduced during a REPL session (i.e., introduced via a mechanism outside of the REPL environment), we will delete that global variable, which means the following logic may introduce unintended side-effects for this particular edge case (e.g., application code may expect the presence of the subsequently deleted global variable). While not ideal, (a) user applications should not be introducing globals to begin with and (b) the probability of a user running a REPL session, a user closing that REPL session, AND a global variable being introduced between starting a REPL and closing the REPL should be negligible.

Check warning on line 1372 in lib/node_modules/@stdlib/repl/lib/main.js

View workflow job for this annotation

GitHub Actions / Lint Changed Files

Unexpected 'warning' comment: 'WARNING: in a non-sandboxed environment,...'
tmp = self._context.vars(); // current workspace variables
for ( i = 0; i < tmp.length; i++ ) {
if ( isConfigurableProperty( self._context, tmp[ i ] ) ) {
Expand Down Expand Up @@ -1438,7 +1449,7 @@
throw new Error( format( 'invalid argument. First argument must be a recognized setting. Value: `%s`.', name ) );
}
if ( nargs === 1 ) {
return this._settings[ name ]; // TODO: we should consider returning a deep copy if settings are allowed to be objects, not just primitives, in order to avoid unintentional mutation

Check warning on line 1452 in lib/node_modules/@stdlib/repl/lib/main.js

View workflow job for this annotation

GitHub Actions / Lint Changed Files

Unexpected 'todo' comment: 'TODO: we should consider returning a...'
}
value = arguments[ 1 ];
f = SETTINGS_VALIDATORS[ SETTINGS[ name ].type ];
Expand Down Expand Up @@ -1481,6 +1492,12 @@
}
} else if ( name === 'theme' ) {
this._syntaxHighlighter.setTheme( value );
} else if ( name === 'bracketedPaste' ) {
if ( value ) {
this._multilineHandler.enableBracketedPaste();
} else {
this._multilineHandler.disableBracketedPaste();
}
}

return this;
Expand Down
52 changes: 49 additions & 3 deletions lib/node_modules/@stdlib/repl/lib/multiline_handler.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
* limitations under the License.
*/

/* eslint-disable no-underscore-dangle, no-restricted-syntax, no-invalid-this */
/* eslint-disable no-underscore-dangle, no-restricted-syntax, no-invalid-this, max-lines */

'use strict';

Expand Down Expand Up @@ -87,6 +87,7 @@ function MultilineHandler( repl, ttyWrite ) {
this._multiline = {};
this._multiline.active = false;
this._multiline.trigger = false;
this._multiline.pasteMode = false;

// Initialize a buffer for caching input lines:
this._lines = [];
Expand Down Expand Up @@ -399,6 +400,42 @@ setNonEnumerableReadOnly( MultilineHandler.prototype, 'resetInput', function res
this._lines.length = 0;
});

/**
* Enables bracketed-paste mode.
*
* @name enableBracketedPaste
* @memberof MultilineHandler.prototype
* @type {Function}
* @returns {void}
*/
setNonEnumerableReadOnly( MultilineHandler.prototype, 'enableBracketedPaste', function enableBracketedPaste() {
this._ostream.write( '\u001b[?2004h' );
});

/**
* Disables bracketed-paste mode.
*
* @name disableBracketedPaste
* @memberof MultilineHandler.prototype
* @type {Function}
* @returns {void}
*/
setNonEnumerableReadOnly( MultilineHandler.prototype, 'disableBracketedPaste', function disableBracketedPaste() {
this._ostream.write( '\u001b[?2004l' );
});

/**
* Checks whether the REPL is currently receiving pasted input.
*
* @name isPasting
* @memberof MultilineHandler.prototype
* @type {Function}
* @returns {boolean} boolean indicating whether the REPL is currently receiving pasted input
*/
setNonEnumerableReadOnly( MultilineHandler.prototype, 'isPasting', function isPasting() {
return this._multiline.pasteMode;
});

/**
* Processes input line data.
*
Expand Down Expand Up @@ -511,6 +548,15 @@ setNonEnumerableReadOnly( MultilineHandler.prototype, 'onKeypress', function onK
if ( !key ) {
return;
}
// Check for paste sequences...
if ( key.name === 'paste-start' ) {
this._multiline.pasteMode = true;
return;
}
if ( key.name === 'paste-end' ) {
this._multiline.pasteMode = false;
return;
}
// Trigger multi-line input when encountering `CTRL+O` keybinding...
if ( key.name === 'o' && key.ctrl ) {
this._triggerMultiline();
Expand Down Expand Up @@ -546,8 +592,8 @@ setNonEnumerableReadOnly( MultilineHandler.prototype, 'beforeKeypress', function
cmd = copy( this._cmd );
cmd[ this._lineIndex ] = this._rli.line;

// If command is incomplete, trigger multi-line mode...
if ( !this._isMultilineInput( cmd.join( '\n' ) ) ) {
// If we are in paste mode or the command is incomplete, trigger multi-line mode...
if ( !this._multiline.pasteMode && !this._isMultilineInput( cmd.join( '\n' ) ) ) {
this._ttyWrite.call( this._rli, data, key );
return;
}
Expand Down
8 changes: 8 additions & 0 deletions lib/node_modules/@stdlib/repl/lib/settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,18 @@ var SETTINGS = {
'desc': 'Automatically page return values whose display size exceeds the visible screen.',
'type': 'boolean'
},
'bracketedPaste': {
'desc': 'Enable bracketed-paste mode.',
'type': 'boolean'
},
'completionPreviews': {
'desc': 'Enable the display of completion previews for auto-completion.',
'type': 'boolean'
},
'autoDisableBracketedPasteOnExit': {
'desc': 'Automatically disable bracketed-paste upon exiting the REPL.',
'type': 'boolean'
},
'syntaxHighlighting': {
'desc': 'Enable syntax highlighting.',
'type': 'boolean'
Expand Down
Loading