diff --git a/lib/node_modules/@stdlib/repl/lib/defaults.js b/lib/node_modules/@stdlib/repl/lib/defaults.js index 6bc31efa71e0..0c0789ec801c 100644 --- a/lib/node_modules/@stdlib/repl/lib/defaults.js +++ b/lib/node_modules/@stdlib/repl/lib/defaults.js @@ -22,6 +22,7 @@ var stdin = require( '@stdlib/streams/node/stdin' ); var stdout = require( '@stdlib/streams/node/stdout' ); +var KEYBINDINGS = require( './keybindings.js' ); var WELCOME = require( './welcome_text.js' ); @@ -78,6 +79,9 @@ function defaults() { // Flag indicating whether log information, confirmation messages, and other possible REPL diagnostics should be silenced: 'quiet': false, + // REPL keybindings: + 'keybindings': KEYBINDINGS, + // User settings: 'settings': { // Flag indicating whether to automatically insert matching brackets, parentheses, and quotes: diff --git a/lib/node_modules/@stdlib/repl/lib/editor_actions.js b/lib/node_modules/@stdlib/repl/lib/editor_actions.js new file mode 100644 index 000000000000..3a06e7ae34b1 --- /dev/null +++ b/lib/node_modules/@stdlib/repl/lib/editor_actions.js @@ -0,0 +1,550 @@ +/** +* @license Apache-2.0 +* +* Copyright (c) 2024 The Stdlib Authors. +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +/* eslint-disable no-underscore-dangle, no-restricted-syntax, no-invalid-this */ + +'use strict'; + +// MODULES // + +var readline = require( 'readline' ); +var setNonEnumerableReadOnly = require( '@stdlib/utils/define-nonenumerable-read-only-property' ); +var ltrimN = require( '@stdlib/string/left-trim-n' ); +var uppercase = require( '@stdlib/string/base/uppercase' ); +var lowercase = require( '@stdlib/string/base/lowercase' ); +var capitalize = require( '@stdlib/string/base/capitalize' ); +var matchKeybindings = require( './match_keybinding.js' ); +var parseKey = require( './parse_key.js' ); + + +// VARIABLES // + +var RE_NON_WHITESPACE = /\S+/; +var EDITOR_ACTIONS = [ + 'moveRight', + 'moveLeft', + 'moveWordRight', + 'moveWordLeft', + 'moveBeginning', + 'moveEnd', + 'tab', + 'indentLineRight', + 'indentLineLeft', + 'deleteLeft', + 'deleteRight', + 'deleteWordLeft', + 'deleteWordRight', + 'deleteLineLeft', + 'deleteLineRight', + 'yankKilled', + 'yankPop', + 'undo', + 'redo', + 'transposeAboutCursor', + 'uppercaseNextWord', + 'capitalizeNextWord', + 'lowercaseNextWord', + 'clearScreen' +]; + + +// MAIN // + +/** +* Constructor for creating an editor actions prototype. +* +* @private +* @constructor +* @param {REPL} repl - REPL instance +* @param {Function} ttyWrite - function to trigger default behavior of a keypress +* @returns {EditorActions} editor actions instance +*/ +function EditorActions( repl, ttyWrite ) { + if ( !( this instanceof EditorActions ) ) { + return new EditorActions( repl, ttyWrite ); + } + + // Cache a reference to the readline interface: + this._rli = repl._rli; + + // Cache a reference to the output writable stream: + this._ostream = repl._ostream; + + // Cache a reference to the private readline interface `ttyWrite` to allow calling the method when wanting default behavior: + this._ttyWrite = ttyWrite; + + // Cache a reference to the REPL keybindings: + this._keybindings = repl._keybindings; + + return this; +} + +/** +* Moves cursor to the right by specified offset. +* +* @private +* @name _moveCursorX +* @memberof EditorActions.prototype +* @type {Function} +* @param {number} offset - x offset +* @returns {void} +*/ +setNonEnumerableReadOnly( EditorActions.prototype, '_moveCursorX', function moveCursorX( offset ) { + readline.moveCursor( this._ostream, offset ); + this._rli.cursor += offset; +}); + +/** +* Replaces current line with the given line. +* +* @private +* @name _replaceLine +* @memberof EditorActions.prototype +* @type {Function} +* @param {string} line - line +* @returns {void} +*/ +setNonEnumerableReadOnly( EditorActions.prototype, '_replaceLine', function replaceLine( line ) { + readline.moveCursor( this._ostream, -1 * this._rli.cursor ); + readline.clearLine( this._ostream, 1 ); + this._ostream.write( line ); + this._rli.line = line; + this._rli.cursor = line.length; +}); + +/** +* Modifies the next word in line. +* +* @private +* @name _modifyNextWord +* @memberof EditorActions.prototype +* @type {Function} +* @param {Function} modifier - modifier function +* @returns {void} +*/ +setNonEnumerableReadOnly( EditorActions.prototype, '_modifyNextWord', function modifyNextWord( modifier ) { + var updatedLine; + var substring; + var match; + var start; + var end; + + // Use regex to find the first non-whitespace character and the end of the word after the cursor: + match = this._rli.line.slice( this._rli.cursor ).match( RE_NON_WHITESPACE ); + if ( match ) { + start = this._rli.cursor + match.index; + end = start + match[ 0 ].length; + } else { + start = this._rli.cursor; + end = this._rli.line.length; + } + // Extract the word, apply the modifier, and reconstruct the line: + substring = this._rli.line.slice( start, end ); + updatedLine = this._rli.line.slice( 0, start ) + modifier( substring ) + this._rli.line.slice( end ); // eslint-disable-line max-len + this._replaceLine( updatedLine ); + this._moveCursorX( end - this._rli.line.length ); +}); + +/** +* Moves cursor a character right. +* +* @name moveRight +* @memberof EditorActions.prototype +* @type {Function} +* @returns {void} +*/ +setNonEnumerableReadOnly( EditorActions.prototype, 'moveRight', function moveRight() { + this._ttyWrite.call( this._rli, null, { + 'name': 'right' + }); +}); + +/** +* Moves cursor a character left. +* +* @name moveLeft +* @memberof EditorActions.prototype +* @type {Function} +* @returns {void} +*/ +setNonEnumerableReadOnly( EditorActions.prototype, 'moveLeft', function moveLeft() { + this._ttyWrite.call( this._rli, null, { + 'name': 'left' + }); +}); + +/** +* Moves cursor a word to the right. +* +* @name moveWordRight +* @memberof EditorActions.prototype +* @type {Function} +* @returns {void} +*/ +setNonEnumerableReadOnly( EditorActions.prototype, 'moveWordRight', function moveWordRight() { + this._ttyWrite.call( this._rli, null, { + 'name': 'f', + 'meta': true + }); +}); + +/** +* Moves cursor a word to the left. +* +* @name moveWordLeft +* @memberof EditorActions.prototype +* @type {Function} +* @returns {void} +*/ +setNonEnumerableReadOnly( EditorActions.prototype, 'moveWordLeft', function moveWordLeft() { + this._ttyWrite.call( this._rli, null, { + 'name': 'b', + 'meta': true + }); +}); + +/** +* Moves cursor to the beginning of line. +* +* @name moveBeginning +* @memberof EditorActions.prototype +* @type {Function} +* @returns {void} +*/ +setNonEnumerableReadOnly( EditorActions.prototype, 'moveBeginning', function moveBeginning() { + this._ttyWrite.call( this._rli, null, { + 'name': 'home' + }); +}); + +/** +* Moves cursor to the end of line. +* +* @name moveEnd +* @memberof EditorActions.prototype +* @type {Function} +* @returns {void} +*/ +setNonEnumerableReadOnly( EditorActions.prototype, 'moveEnd', function moveEnd() { + this._ttyWrite.call( this._rli, null, { + 'name': 'end' + }); +}); + +/** +* Inserts TAB indentation at cursor. +* +* @name tab +* @memberof EditorActions.prototype +* @type {Function} +* @returns {void} +*/ +setNonEnumerableReadOnly( EditorActions.prototype, 'tab', function tab() { + this._rli.write( '\t' ); +}); + +/** +* Indents line to the right. +* +* @name indentLineRight +* @memberof EditorActions.prototype +* @type {Function} +* @returns {void} +*/ +setNonEnumerableReadOnly( EditorActions.prototype, 'indentLineRight', function indentLineRight() { + var indentedLine = '\t' + this._rli.line; + var cursorPos = this._rli.cursor + 1; + this._replaceLine( indentedLine ); + this._moveCursorX( cursorPos - this._rli.line.length ); // maintain cursor position +}); + +/** +* Indents line to the left. +* +* @name indentLineLeft +* @memberof EditorActions.prototype +* @type {Function} +* @returns {void} +*/ +setNonEnumerableReadOnly( EditorActions.prototype, 'indentLineLeft', function indentLineLeft() { + var indentedLine; + var cursorPos; + var offset; + + indentedLine = ltrimN( this._rli.line, 1, [ '\t' ] ); + offset = this._rli.line.length - indentedLine.length; + cursorPos = this._rli.cursor - offset; + + // NOTE: `readline.moveCursor()` doesn't know tabs, it treats `\t` as 8 whitespace characters instead of a single character. Hence manually move the cursor when indenting to the left... + if ( offset ) { + readline.moveCursor( this._ostream, ( -1 * this._rli.tabSize ) + 1 ); + } + this._replaceLine( indentedLine ); + this._moveCursorX( cursorPos - this._rli.line.length ); // maintain cursor position +}); + +/** +* Deletes character left to the cursor. +* +* @name deleteLeft +* @memberof EditorActions.prototype +* @type {Function} +* @returns {void} +*/ +setNonEnumerableReadOnly( EditorActions.prototype, 'deleteLeft', function deleteLeft() { + this._ttyWrite.call( this._rli, null, { + 'name': 'backspace' + }); +}); + +/** +* Deletes character right to the cursor. +* +* @name deleteRight +* @memberof EditorActions.prototype +* @type {Function} +* @returns {void} +*/ +setNonEnumerableReadOnly( EditorActions.prototype, 'deleteRight', function deleteRight() { + this._ttyWrite.call( this._rli, null, { + 'name': 'delete' + }); +}); + +/** +* Deletes a word left to the cursor. +* +* @name deleteWordLeft +* @memberof EditorActions.prototype +* @type {Function} +* @returns {void} +*/ +setNonEnumerableReadOnly( EditorActions.prototype, 'deleteWordLeft', function deleteWordLeft() { + this._ttyWrite.call( this._rli, null, { + 'name': 'backspace', + 'ctrl': true + }); +}); + +/** +* Deletes a word right to the cursor. +* +* @name deleteWordRight +* @memberof EditorActions.prototype +* @type {Function} +* @returns {void} +*/ +setNonEnumerableReadOnly( EditorActions.prototype, 'deleteWordRight', function deleteWordRight() { + this._ttyWrite.call( this._rli, null, { + 'name': 'delete', + 'ctrl': true + }); +}); + +/** +* Deletes line to the left of the cursor. +* +* @name deleteLineLeft +* @memberof EditorActions.prototype +* @type {Function} +* @returns {void} +*/ +setNonEnumerableReadOnly( EditorActions.prototype, 'deleteLineLeft', function deleteLineLeft() { + this._ttyWrite.call( this._rli, null, { + 'name': 'backspace', + 'ctrl': true, + 'shift': true + }); +}); + +/** +* Deletes line to the right of the cursor. +* +* @name deleteLineRight +* @memberof EditorActions.prototype +* @type {Function} +* @returns {void} +*/ +setNonEnumerableReadOnly( EditorActions.prototype, 'deleteLineRight', function deleteLineRight() { + this._ttyWrite.call( this._rli, null, { + 'name': 'delete', + 'ctrl': true, + 'shift': true + }); +}); + +/** +* Yank string from the "killed" buffer. +* +* @name yankKilled +* @memberof EditorActions.prototype +* @type {Function} +* @returns {void} +*/ +setNonEnumerableReadOnly( EditorActions.prototype, 'yankKilled', function yankKilled() { + this._ttyWrite.call( this._rli, null, { + 'name': 'y', + 'ctrl': true + }); +}); + +/** +* Yank-pop the next string from the "killed" buffer. +* +* @name yankPop +* @memberof EditorActions.prototype +* @type {Function} +* @returns {void} +*/ +setNonEnumerableReadOnly( EditorActions.prototype, 'yankPop', function yankPop() { + this._ttyWrite.call( this._rli, null, { + 'name': 'y', + 'meta': true + }); +}); + +/** +* Yank-pop the next string from the undo stack. +* +* @name undo +* @memberof EditorActions.prototype +* @type {Function} +* @returns {void} +*/ +setNonEnumerableReadOnly( EditorActions.prototype, 'undo', function undo() { + this._ttyWrite.call( this._rli, null, { + 'sequence': '\u001F' + }); +}); + +/** +* Yank-pop the next string from the redo stack. +* +* @name redo +* @memberof EditorActions.prototype +* @type {Function} +* @returns {void} +*/ +setNonEnumerableReadOnly( EditorActions.prototype, 'redo', function redo() { + this._ttyWrite.call( this._rli, null, { + 'sequence': '\u001E' + }); +}); + +/** +* Transposes the characters about cursor. +* +* @name transposeAboutCursor +* @memberof EditorActions.prototype +* @type {Function} +* @returns {void} +*/ +setNonEnumerableReadOnly( EditorActions.prototype, 'transposeAboutCursor', function transposeAboutCursor() { + var c = this._rli.line[ this._rli.cursor - 1 ]; + this.deleteLeft(); + this.moveRight(); + this._rli.write( c ); +}); + +/** +* Changes the next word to uppercase. +* +* @name uppercaseNextWord +* @memberof EditorActions.prototype +* @type {Function} +* @returns {void} +*/ +setNonEnumerableReadOnly( EditorActions.prototype, 'uppercaseNextWord', function uppercaseNextWord() { + this._modifyNextWord( uppercase ); +}); + +/** +* Changes the next word to titlecase. +* +* @name capitalizeNextWord +* @memberof EditorActions.prototype +* @type {Function} +* @returns {void} +*/ +setNonEnumerableReadOnly( EditorActions.prototype, 'capitalizeNextWord', function capitalizeNextWord() { + this._modifyNextWord( capitalize ); +}); + +/** +* Changes the next word to lowercase. +* +* @name lowercaseNextWord +* @memberof EditorActions.prototype +* @type {Function} +* @returns {void} +*/ +setNonEnumerableReadOnly( EditorActions.prototype, 'lowercaseNextWord', function lowercaseNextWord() { + this._modifyNextWord( lowercase ); +}); + +/** +* Clears the entire REPL screen and scrollback history. +* +* @name clearScreen +* @memberof EditorActions.prototype +* @type {Function} +* @returns {void} +*/ +setNonEnumerableReadOnly( EditorActions.prototype, 'clearScreen', function clearScreen() { + this._ttyWrite.call( this._rli, null, { + 'name': 'l', + 'ctrl': true + }); +}); + +/** +* Callback which should be invoked **before** a "keypress" event is processed by a readline interface. +* +* @name beforeKeypress +* @memberof EditorActions.prototype +* @type {Function} +* @param {string} data - input data +* @param {(Object|void)} key - key object +* @returns {boolean} boolean indicating whether an editor action was triggered +*/ +setNonEnumerableReadOnly( EditorActions.prototype, 'beforeKeypress', function beforeKeypress( data, key ) { + var actionKeys; + var inputKeys; + var i; + + if ( !key ) { + return false; + } + inputKeys = parseKey( key ); + for ( i = 0; i < EDITOR_ACTIONS.length; i++ ) { + actionKeys = this._keybindings[ EDITOR_ACTIONS[ i ] ]; + if ( !actionKeys ) { + continue; // no keybindings configured for the action + } + if ( matchKeybindings( inputKeys, actionKeys ) ) { + this[ EDITOR_ACTIONS[ i ] ](); + return true; + } + } + return false; +}); + + +// EXPORTS // + +module.exports = EditorActions; diff --git a/lib/node_modules/@stdlib/repl/lib/keybindings.js b/lib/node_modules/@stdlib/repl/lib/keybindings.js new file mode 100644 index 000000000000..e45834a4d360 --- /dev/null +++ b/lib/node_modules/@stdlib/repl/lib/keybindings.js @@ -0,0 +1,349 @@ +/** +* @license Apache-2.0 +* +* Copyright (c) 2024 The Stdlib Authors. +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +/* eslint-disable max-lines */ + +'use strict'; + +// MAIN // + +/** +* Table mapping of REPL actions and their corresponding keybinding triggers. +* +* @name KEYBINDINGS +* @type {Object} +*/ +var KEYBINDINGS = { + 'moveRight': [ + { + 'name': 'right', + 'ctrl': false, + 'shift': false, + 'meta': false + }, + { + 'name': 'f', + 'ctrl': true, + 'shift': false, + 'meta': false + } + ], + 'moveLeft': [ + { + 'name': 'left', + 'ctrl': false, + 'shift': false, + 'meta': false + }, + { + 'name': 'b', + 'ctrl': true, + 'shift': false, + 'meta': false + } + ], + 'moveWordRight': [ + { + 'name': 'right', + 'ctrl': true, + 'shift': false, + 'meta': false + }, + { + 'name': 'f', + 'ctrl': false, + 'shift': false, + 'meta': true + } + ], + 'moveWordLeft': [ + { + 'name': 'left', + 'ctrl': true, + 'shift': false, + 'meta': false + }, + { + 'name': 'b', + 'ctrl': false, + 'shift': false, + 'meta': true + } + ], + 'moveBeginning': [ + { + 'name': 'home', + 'ctrl': false, + 'shift': false, + 'meta': false + }, + { + 'name': 'a', + 'ctrl': true, + 'shift': false, + 'meta': false + } + ], + 'moveEnd': [ + { + 'name': 'end', + 'ctrl': false, + 'shift': false, + 'meta': false + }, + { + 'name': 'e', + 'ctrl': true, + 'shift': false, + 'meta': false + } + ], + 'tab': [ + { + 'name': 't', + 'ctrl': false, + 'shift': false, + 'meta': true + } + ], + 'indentLineRight': [ + { + 'name': 'right', + 'ctrl': false, + 'shift': false, + 'meta': true + }, + { + 'name': 'i', + 'ctrl': true, + 'shift': false, + 'meta': false + } + ], + 'indentLineLeft': [ + { + 'name': 'left', + 'ctrl': false, + 'shift': false, + 'meta': true + }, + { + 'name': 'i', + 'ctrl': false, + 'shift': false, + 'meta': true + } + ], + 'deleteLeft': [ + { + 'name': 'backspace', + 'ctrl': false, + 'shift': false, + 'meta': false + }, + { + 'name': 'h', + 'ctrl': true, + 'shift': false, + 'meta': false + } + ], + 'deleteRight': [ + { + 'name': 'delete', + 'ctrl': false, + 'shift': false, + 'meta': false + }, + { + 'name': 'd', + 'ctrl': true, + 'shift': false, + 'meta': false + } + ], + 'deleteWordLeft': [ + { + 'name': 'backspace', + 'ctrl': true, + 'shift': false, + 'meta': false + }, + { + 'name': 'backspace', + 'ctrl': false, + 'shift': false, + 'meta': true + }, + { + 'name': 'w', + 'ctrl': true, + 'shift': false, + 'meta': false + } + ], + 'deleteWordRight': [ + { + 'name': 'delete', + 'ctrl': true, + 'shift': false, + 'meta': false + }, + { + 'name': 'delete', + 'ctrl': false, + 'shift': false, + 'meta': true + }, + { + 'name': 'd', + 'ctrl': false, + 'shift': false, + 'meta': true + } + ], + 'deleteLineLeft': [ + { + 'name': 'backspace', + 'ctrl': true, + 'shift': true, + 'meta': false + }, + { + 'name': 'u', + 'ctrl': true, + 'shift': false, + 'meta': false + } + ], + 'deleteLineRight': [ + { + 'name': 'delete', + 'ctrl': true, + 'shift': true, + 'meta': false + }, + { + 'name': 'k', + 'ctrl': true, + 'shift': false, + 'meta': false + } + ], + 'yankKilled': [ + { + 'name': 'y', + 'ctrl': true, + 'shift': false, + 'meta': false + } + ], + 'yankPop': [ + { + 'name': 'y', + 'ctrl': false, + 'shift': false, + 'meta': true + } + ], + 'undo': [ + { + 'name': '/', + 'ctrl': true, + 'shift': false, + 'meta': false + }, + { + 'name': '-', + 'ctrl': true, + 'shift': false, + 'meta': false + } + ], + 'redo': [ + { + 'name': '6', + 'ctrl': true, + 'shift': false, + 'meta': false + }, + { + 'name': '6', + 'ctrl': true, + 'shift': true, + 'meta': false + }, + { + 'name': '^', + 'ctrl': true, + 'shift': false, + 'meta': false + }, + { + 'name': 'y', + 'ctrl': false, + 'shift': true, + 'meta': false + } + ], + 'transposeAboutCursor': [ + { + 'name': 't', + 'ctrl': true, + 'shift': false, + 'meta': false + } + ], + 'uppercaseNextWord': [ + { + 'name': 'u', + 'ctrl': false, + 'shift': false, + 'meta': true + } + ], + 'capitalizeNextWord': [ + { + 'name': 'c', + 'ctrl': false, + 'shift': false, + 'meta': true + } + ], + 'lowercaseNextWord': [ + { + 'name': 'l', + 'ctrl': false, + 'shift': false, + 'meta': true + } + ], + 'clearScreen': [ + { + 'name': 'l', + 'ctrl': true, + 'shift': false, + 'meta': false + } + ] +}; + + +// EXPORTS // + +module.exports = KEYBINDINGS; diff --git a/lib/node_modules/@stdlib/repl/lib/main.js b/lib/node_modules/@stdlib/repl/lib/main.js index 4c872c37bf2f..b04bcf12da61 100644 --- a/lib/node_modules/@stdlib/repl/lib/main.js +++ b/lib/node_modules/@stdlib/repl/lib/main.js @@ -63,6 +63,7 @@ var inputPrompt = require( './input_prompt.js' ); var OutputStream = require( './output_stream.js' ); var completerFactory = require( './completer.js' ); var MultilineHandler = require( './multiline_handler.js' ); +var EditorActions = require( './editor_actions.js' ); var CompleterEngine = require( './completer_engine.js' ); var PreviewCompleter = require( './completer_preview.js' ); var AutoCloser = require( './auto_close_pairs.js' ); @@ -98,6 +99,7 @@ var debug = logger( 'repl' ); * @param {string} [options.save] - file path specifying where to save REPL command history * @param {string} [options.log] - file path specifying where to save REPL commands and printed output * @param {string} [options.quiet=false] - boolean indicating whether log information, confirmation messages, and other possible REPL diagnostics should be silenced +* @param {Object} [options.keybindings] - REPL keybindings * @param {Object} [options.settings] - REPL settings * @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 @@ -196,6 +198,7 @@ function REPL( options ) { setNonEnumerableReadOnly( this, '_timeout', opts.timeout ); setNonEnumerableReadOnly( this, '_isTTY', opts.isTTY ); setNonEnumerableReadOnly( this, '_sandbox', opts.sandbox ); + setNonEnumerableReadOnly( this, '_keybindings', opts.keybindings ); setNonEnumerableReadOnly( this, '_settings', opts.settings ); setNonEnumerable( this, '_quiet', opts.quiet ); // allow this to be internally toggled @@ -275,6 +278,9 @@ function REPL( options ) { // Initialize a multi-line handler: setNonEnumerableReadOnly( this, '_multilineHandler', new MultilineHandler( this, this._rli._ttyWrite ) ); + // Initialize an editor actions instance: + setNonEnumerableReadOnly( this, '_editorActions', new EditorActions( this, this._rli._ttyWrite ) ); + // Create a new TAB completer engine: setNonEnumerableReadOnly( this, '_completerEngine', new CompleterEngine( this, this._completer, this._wstream, this._rli._ttyWrite ) ); @@ -347,6 +353,7 @@ function REPL( options ) { */ function beforeKeypress( data, key ) { var completed; + var FLG; // flag denoting whether to bypass the default keypress behavior if ( self._ostream.isPaging ) { self._ostream.beforeKeypress( data, key ); @@ -354,6 +361,7 @@ function REPL( options ) { } self._autoCloser.beforeKeypress( data, key ); completed = self._previewCompleter.beforeKeypress( data, key ); + FLG = self._editorActions.beforeKeypress( data, key ); // If ENTER keypress is encountered or if a preview was completed while navigating, gracefully close the completer... if ( completed || ( key && key.name === 'return' ) ) { @@ -362,12 +370,14 @@ function REPL( options ) { self._completerEngine.beforeKeypress( data, key ); return; } - // If completion was auto-completed, don't trigger multi-line keybindings to avoid double operations... - if ( !completed ) { + // If completion was auto-completed or an action was triggered, don't trigger multi-line keybindings to avoid double operations... + if ( !completed && !FLG ) { self._multilineHandler.beforeKeypress( data, key ); return; } - self._ttyWrite.call( self._rli, data, key ); + if ( !FLG ) { + self._ttyWrite.call( self._rli, data, key ); + } } /** diff --git a/lib/node_modules/@stdlib/repl/lib/match_keybinding.js b/lib/node_modules/@stdlib/repl/lib/match_keybinding.js new file mode 100644 index 000000000000..abe171ad9011 --- /dev/null +++ b/lib/node_modules/@stdlib/repl/lib/match_keybinding.js @@ -0,0 +1,53 @@ +/** +* @license Apache-2.0 +* +* Copyright (c) 2024 The Stdlib Authors. +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +'use strict'; + +// MAIN // + +/** +* Matches a list of possible input keybindings against a list of accepted keybindings. +* +* @private +* @param {Array} inputKeys - list of possible input keys +* @param {Array} acceptedKeys - list of accepted keys +* @returns {boolean} boolean indicating whether the input keys match the accepted keys +*/ +function matchKeybindings( inputKeys, acceptedKeys ) { + var i; + var j; + + for ( i = 0; i < acceptedKeys.length; i++ ) { + for ( j = 0; j < inputKeys.length; j++ ) { + if ( + acceptedKeys[ i ].name === inputKeys[ j ].name && + acceptedKeys[ i ].ctrl === inputKeys[ j ].ctrl && + acceptedKeys[ i ].shift === inputKeys[ j ].shift && + acceptedKeys[ i ].meta === inputKeys[ j ].meta + ) { + return true; + } + } + } + return false; +} + + +// EXPORTS // + +module.exports = matchKeybindings; diff --git a/lib/node_modules/@stdlib/repl/lib/parse_key.js b/lib/node_modules/@stdlib/repl/lib/parse_key.js new file mode 100644 index 000000000000..b1acccd0ff8e --- /dev/null +++ b/lib/node_modules/@stdlib/repl/lib/parse_key.js @@ -0,0 +1,261 @@ +/** +* @license Apache-2.0 +* +* Copyright (c) 2024 The Stdlib Authors. +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +'use strict'; + +// MODULES // + +var hasOwnProp = require( '@stdlib/assert/has-own-property' ); +var pick = require( '@stdlib/utils/pick' ); +var containsFactory = require( '@stdlib/array/base/assert/contains' ).factory; +var isUndefined = require( '@stdlib/assert/is-undefined' ); + + +// VARIABLES // + +// Control sequence prefix in ALT combinations: +var ALT_CSI = '\u001B'; + +// Properties in parsed keybinding: +var KEYBINDING_PROPS = [ 'name', 'ctrl', 'shift', 'meta' ]; + +/** +* Asserts if a sequence is an unrecognized symbol. +* +* ## Notes +* +* - `SHIFT+` will result in a keybinding with the name of the resulting symbol and not the original symbol with shift enabled. +* +* @private +* @name isUnrecognizedSymbol +* @type {Function} +*/ +var isUnrecognizedSymbol = containsFactory([ + '`', + '~', + '!', + '@', + '#', + '$', + '%', + '^', + '&', + '*', + '(', + ')', + '-', + '_', + '=', + '+', + '{', + '}', + '[', + ']', + '|', + '\\', + ':', + ';', + '"', + '\'', + '<', + ',', + '>', + '.', + '?', + '/' +]); + +/** +* Table mapping unrecognized control sequences to their possible key objects. +* +* ## Notes +* +* - The following map is recorded as observed in gnome. +* +* @private +* @name CTRL_SEQUENCES_KEYMAP +* @type {Object} +*/ +var CTRL_SEQUENCES_KEYMAP = { + '\u001F': [ + { + 'name': '/', + 'ctrl': true, + 'shift': false, + 'meta': false + }, + { + 'name': '-', + 'ctrl': true, + 'shift': false, + 'meta': true + }, + { + 'name': '7', + 'ctrl': true, + 'shift': false, + 'meta': true + } + ], + '\u001C': [ + { + 'name': '\\', + 'ctrl': true, + 'shift': false, + 'meta': false + }, + { + 'name': '\\', + 'ctrl': true, + 'shift': true, + 'meta': false + }, + { + 'name': '|', + 'ctrl': true, + 'shift': false, + 'meta': false + }, + { + 'name': '4', + 'ctrl': true, + 'shift': false, + 'meta': false + } + ], + '\u001D': [ + { + 'name': ']', + 'ctrl': true, + 'shift': false, + 'meta': false + }, + { + 'name': ']', + 'ctrl': true, + 'shift': true, + 'meta': false + }, + { + 'name': '}', + 'ctrl': true, + 'shift': false, + 'meta': false + }, + { + 'name': '5', + 'ctrl': true, + 'shift': false, + 'meta': false + } + ], + '\u001E': [ + { + 'name': '6', + 'ctrl': true, + 'shift': false, + 'meta': false + }, + { + 'name': '6', + 'ctrl': true, + 'shift': true, + 'meta': false + }, + { + 'name': '^', + 'ctrl': true, + 'shift': false, + 'meta': false + } + ] +}; + + +// FUNCTIONS // + +/** +* Parses unrecognized control sequences emitted by `ALT+` combinations. +* +* @private +* @param {string} seq - control sequence +* @returns {(Object|null)} parsed key object or null if not a meta symbol sequence +*/ +function parseMetaSymbolSequence( seq ) { + if ( + !seq || + seq.length !== 2 || + seq[ 0 ] !== ALT_CSI || + isUnrecognizedSymbol( seq[ 1 ] ) + ) { + return null; + } + return { + 'name': seq[ 1 ], + 'ctrl': false, + 'shift': false, + 'meta': true + }; +} + + +// MAIN // + +/** +* Parses a readline keypress object. +* +* @param {Object} key - readline keypress object +* @returns {Array} list of possible key objects +*/ +function parseKey( key ) { + var out; + var seq; + + // If key is already defined, no need to parse it... + if ( !isUndefined( key.name ) ) { + return [ pick( key, KEYBINDING_PROPS ) ]; + } + seq = key.sequence; + + // Check if it's an unrecognized symbol: + if ( isUnrecognizedSymbol( seq ) ) { + return [ + { + 'name': seq, + 'ctrl': false, + 'shift': false, + 'meta': false + } + ]; + } + // Check if it's a `CTRL+` combination: + if ( hasOwnProp( CTRL_SEQUENCES_KEYMAP, seq ) ) { + return CTRL_SEQUENCES_KEYMAP[ seq ]; + } + // Check if it's a `META+` combination: + out = parseMetaSymbolSequence( seq ); + if ( out ) { + return [ out ]; + } + return [ pick( key, KEYBINDING_PROPS ) ]; // couldn't parse, return original key +} + + +// EXPORTS // + +module.exports = parseKey; diff --git a/lib/node_modules/@stdlib/repl/lib/validate.js b/lib/node_modules/@stdlib/repl/lib/validate.js index 11ce529317d8..c969ab72ccd2 100644 --- a/lib/node_modules/@stdlib/repl/lib/validate.js +++ b/lib/node_modules/@stdlib/repl/lib/validate.js @@ -29,6 +29,7 @@ var isBoolean = require( '@stdlib/assert/is-boolean' ).isPrimitive; var isPositiveInteger = require( '@stdlib/assert/is-positive-integer' ).isPrimitive; var isNonNegativeInteger = require( '@stdlib/assert/is-nonnegative-integer' ).isPrimitive; var format = require( '@stdlib/string/format' ); +var validateKeybindings = require( './validate_keybindings.js' ); var validateSettings = require( './validate_settings.js' ); @@ -155,6 +156,12 @@ function validate( opts, options ) { return new TypeError( format( 'invalid option. `%s` option must be a boolean. Option: `%s`.', 'quiet', options.quiet ) ); } } + if ( hasOwnProp( options, 'keybindings' ) ) { + err = validateKeybindings( opts.keybindings, options.keybindings ); + if ( err ) { + return err; + } + } if ( hasOwnProp( options, 'settings' ) ) { err = validateSettings( opts.settings, options.settings ); if ( err ) { diff --git a/lib/node_modules/@stdlib/repl/lib/validate_keybindings.js b/lib/node_modules/@stdlib/repl/lib/validate_keybindings.js new file mode 100644 index 000000000000..7398c0bc7ec7 --- /dev/null +++ b/lib/node_modules/@stdlib/repl/lib/validate_keybindings.js @@ -0,0 +1,143 @@ +/** +* @license Apache-2.0 +* +* Copyright (c) 2024 The Stdlib Authors. +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +'use strict'; + +// MODULES // + +var isPlainObject = require( '@stdlib/assert/is-plain-object' ); +var isArray = require( '@stdlib/assert/is-array' ); +var isString = require( '@stdlib/assert/is-string' ); +var isBoolean = require( '@stdlib/assert/is-boolean' ); +var hasOwnProp = require( '@stdlib/assert/has-own-property' ); +var format = require( '@stdlib/string/format' ); + + +// VARIABLES // + +var ACTIONS = [ + 'moveRight', + 'moveLeft', + 'moveWordRight', + 'moveWordLeft', + 'moveBeginning', + 'moveEnd', + 'tab', + 'indentLineRight', + 'indentLineLeft', + 'deleteLeft', + 'deleteRight', + 'deleteWordLeft', + 'deleteWordRight', + 'deleteLineLeft', + 'deleteLineRight', + 'yankKilled', + 'yankPop', + 'undo', + 'redo', + 'transposeAboutCursor', + 'uppercaseNextWord', + 'capitalizeNextWord', + 'lowercaseNextWord', + 'clearScreen' +]; +var KEY_BOOLEAN_PROPS = [ 'ctrl', 'shift', 'meta' ]; + + +// FUNCTIONS // + +/** +* Validates a key object. +* +* @private +* @param {Object} obj - key object +* @returns {(Error|null)} error or null +*/ +function validateKey( obj ) { + var prop; + var i; + if ( !hasOwnProp( obj, 'name' ) ) { + return new TypeError( format( 'invalid option. Each key object must have a `name` property. Value: `%s`.', obj ) ); + } + if ( !isString( obj[ 'name' ] ) ) { + return new TypeError( format( 'invalid option. Each key object\'s `name` property must be a string. Value: `%s`.', obj ) ); + } + for ( i = 0; i < KEY_BOOLEAN_PROPS.length; i++ ) { + prop = KEY_BOOLEAN_PROPS[ i ]; + if ( hasOwnProp( obj, prop ) ) { + if ( !isBoolean( obj[ prop ] ) ) { + return new TypeError( format( 'invalid option. Each key object\'s `%s` property must be a boolean. Value: `%s`.', prop, obj ) ); + } + } + } + return null; +} + + +// MAIN // + +/** +* Validates keybindings. +* +* @param {Object} opts - destination object +* @param {Object} options - settings options +* @returns {(Error|null)} error or null +*/ +function validate( opts, options ) { + var list; + var out; + var err; + var i; + var j; + var o; + if ( !isPlainObject( options ) ) { + return new TypeError( format( 'invalid argument. Options argument must be an object. Value: `%s`.', options ) ); + } + for ( i = 0; i < ACTIONS.length; i++ ) { + if ( hasOwnProp( options, ACTIONS[ i ] ) ) { + list = options[ ACTIONS[ i ] ]; + if ( !isArray( list ) ) { + return new TypeError( format( 'invalid option. Each action must be an array of objects. Value: `%s`.', list ) ); + } + out = []; + for ( j = 0; j < list.length; j++ ) { + o = list[ j ]; + if ( !isPlainObject( o ) ) { + return new TypeError( format( 'invalid option. Each action must be an array of objects. Value: `%s`.', list ) ); + } + err = validateKey( o ); + if ( err ) { + return err; + } + out.push( { + 'name': o.name, + 'ctrl': o.ctrl || false, + 'shift': o.shift || false, + 'meta': o.meta || false + }); + } + opts[ ACTIONS[ i ] ] = out; + } + } + return null; +} + + +// EXPORTS // + +module.exports = validate;