diff --git a/lib/node_modules/@stdlib/repl/lib/editor_actions.js b/lib/node_modules/@stdlib/repl/lib/editor_actions.js index e8803da827d7..b26587818228 100644 --- a/lib/node_modules/@stdlib/repl/lib/editor_actions.js +++ b/lib/node_modules/@stdlib/repl/lib/editor_actions.js @@ -92,6 +92,9 @@ function EditorActions( repl, ttyWrite ) { // Cache a reference to the REPL keybindings: this._keybindings = repl._keybindings; + // Cache a reference to the multiline handler: + this._multilineHandler = repl._multilineHandler; + return this; } @@ -170,6 +173,9 @@ setNonEnumerableReadOnly( EditorActions.prototype, '_modifyNextWord', function m * @returns {void} */ setNonEnumerableReadOnly( EditorActions.prototype, 'moveRight', function moveRight() { + if ( this._multilineHandler && this._multilineHandler.moveRight() ) { + return; + } this._ttyWrite.call( this._rli, null, { 'name': 'right' }); @@ -184,6 +190,9 @@ setNonEnumerableReadOnly( EditorActions.prototype, 'moveRight', function moveRig * @returns {void} */ setNonEnumerableReadOnly( EditorActions.prototype, 'moveLeft', function moveLeft() { + if ( this._multilineHandler && this._multilineHandler.moveLeft() ) { + return; + } this._ttyWrite.call( this._rli, null, { 'name': 'left' }); @@ -308,6 +317,9 @@ setNonEnumerableReadOnly( EditorActions.prototype, 'indentLineLeft', function in * @returns {void} */ setNonEnumerableReadOnly( EditorActions.prototype, 'deleteLeft', function deleteLeft() { + if ( this._multilineHandler && this._multilineHandler.deleteLeft() ) { + return; + } this._ttyWrite.call( this._rli, null, { 'name': 'backspace' }); diff --git a/lib/node_modules/@stdlib/repl/lib/main.js b/lib/node_modules/@stdlib/repl/lib/main.js index 9739d1b941ff..d19dd2c6049c 100644 --- a/lib/node_modules/@stdlib/repl/lib/main.js +++ b/lib/node_modules/@stdlib/repl/lib/main.js @@ -380,8 +380,6 @@ function REPL( options ) { } self._autoCloser.beforeKeypress( data, key ); completed = self._previewCompleter.beforeKeypress( data, key ); - FLG = self._editorActions.beforeKeypress( data, key ); - self._eagerEvaluator.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' ) ) { @@ -390,6 +388,8 @@ function REPL( options ) { self._completerEngine.beforeKeypress( data, key ); return; } + FLG = self._editorActions.beforeKeypress( data, key ); + self._eagerEvaluator.beforeKeypress( data, key ); // 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 ); diff --git a/lib/node_modules/@stdlib/repl/lib/multiline_handler.js b/lib/node_modules/@stdlib/repl/lib/multiline_handler.js index d78009de4959..e2e9efd6b9df 100644 --- a/lib/node_modules/@stdlib/repl/lib/multiline_handler.js +++ b/lib/node_modules/@stdlib/repl/lib/multiline_handler.js @@ -714,6 +714,78 @@ setNonEnumerableReadOnly( MultilineHandler.prototype, 'onKeypress', function onK } }); +/** +* Moves the cursor to the previous line if at the beginning of the current line in multi-line mode. +* +* @name moveLeft +* @memberof MultilineHandler.prototype +* @type {Function} +* @returns {boolean} boolean indicating whether the cursor moved to the previous line +*/ +setNonEnumerableReadOnly( MultilineHandler.prototype, 'moveLeft', function moveLeft() { + if ( this._history.index !== 0 ) { + // Reset current history cycle: + this._resetHistoryBuffers(); + } + // If at the beginning of the line, move up to the previous line: + if ( this._rli.cursor === 0 && this._lineIndex > 0 ) { + this._moveLeft(); + if ( this._multiline.active ) { + this._renderLines(); + } + return true; + } + return false; +}); + +/** +* Moves the cursor to the next line if at the end of the current line in multi-line mode. +* +* @name moveRight +* @memberof MultilineHandler.prototype +* @type {Function} +* @returns {boolean} boolean indicating whether the cursor moved to the next line +*/ +setNonEnumerableReadOnly( MultilineHandler.prototype, 'moveRight', function moveRight() { + if ( this._history.index !== 0 ) { + // Reset current history cycle: + this._resetHistoryBuffers(); + } + // If at the end of the line, move to the next line: + if ( this._rli.cursor === this._rli.line.length && this._lineIndex < this._lines.length - 1 ) { + this._moveRight(); + if ( this._multiline.active ) { + this._renderLines(); + } + return true; + } + return false; +}); + +/** +* Simulates a backspace across line boundaries if at the beginning of the current line in multi-line mode. +* +* @name deleteLeft +* @memberof MultilineHandler.prototype +* @type {Function} +* @returns {boolean} boolean indicating whether the current line was removed and merged into the previous line +*/ +setNonEnumerableReadOnly( MultilineHandler.prototype, 'deleteLeft', function deleteLeft() { + 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: + if ( this._rli.cursor === 0 && this._lineIndex > 0 ) { + this._backspace(); + if ( this._multiline.active ) { + this._renderLines(); + } + return true; + } + return false; +}); + /** * Callback which should be invoked **before** a "keypress" event is processed by a readline interface. * @@ -766,46 +838,19 @@ setNonEnumerableReadOnly( MultilineHandler.prototype, 'beforeKeypress', function } 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(); - if ( this._multiline.active ) { - this._renderLines(); - } + if ( this.moveLeft() ) { 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(); - if ( this._multiline.active ) { - this._renderLines(); - } + if ( this.moveRight() ) { 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(); - if ( this._multiline.active ) { - this._renderLines(); - } + if ( this.deleteLeft() ) { return; } this._ttyWrite.call( this._rli, data, key ); diff --git a/lib/node_modules/@stdlib/repl/test/integration/test.multiline_editing.js b/lib/node_modules/@stdlib/repl/test/integration/test.multiline_editing.js new file mode 100644 index 000000000000..9f59bffe08cb --- /dev/null +++ b/lib/node_modules/@stdlib/repl/test/integration/test.multiline_editing.js @@ -0,0 +1,231 @@ +/** +* @license Apache-2.0 +* +* Copyright (c) 2025 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 tape = require( 'tape' ); +var DebugStream = require( '@stdlib/streams/node/debug' ); +var repl = require( './fixtures/repl.js' ); + + +// FUNCTIONS // + +/** +* Returns default settings. +* +* @private +* @returns {Object} default settings +*/ +function defaultSettings() { + return { + 'autoDeletePairs': false, + 'autoClosePairs': false, + 'completionPreviews': false, + 'syntaxHighlighting': false, + 'eagerEvaluation': false, + 'autoPage': false + }; +} + + +// TESTS // + +tape( 'main export is a function', function test( t ) { + t.ok( true, __filename ); + t.strictEqual( typeof repl, 'function', 'main export is a function' ); + t.end(); +}); + +tape( 'a REPL instance supports multiline continuous backspaces into the previous line', function test( t ) { + var istream; + var opts; + var r; + + istream = new DebugStream({ + 'name': 'repl-input-stream' + }); + opts = { + 'input': istream, + 'settings': defaultSettings(), + 'tty': { + 'rows': 100, + 'columns': 80 + } + }; + r = repl( opts, onClose ); + + // Enter an incomplete expression that triggers multi-line input: + istream.write( 'function foo() {' ); + istream.write( '\r' ); + + t.strictEqual( r._multilineHandler.lineIndex(), 1, 'cursor is on line 1' ); + t.strictEqual( r._multilineHandler._lines.length, 2, 'two lines present' ); + + // Backspace at the beginning of line 1: + istream.write( '\u0008' ); + + t.strictEqual( r._multilineHandler.lineIndex(), 0, 'cursor moves back to line 0' ); + t.strictEqual( r._multilineHandler._lines.length, 1, 'lines merged into line 0' ); + t.strictEqual( r._rli.line, 'function foo() {', 'line content matches' ); + + // Close the input stream: + istream.end(); + r.close(); + + function onClose( error ) { + if ( error ) { + t.fail( error.message ); + return; + } + t.end(); + } +}); + +tape( 'a REPL instance supports moving to the previous line using left arrow in multiline mode', function test( t ) { + var istream; + var opts; + var r; + + istream = new DebugStream({ + 'name': 'repl-input-stream' + }); + opts = { + 'input': istream, + 'settings': defaultSettings(), + 'tty': { + 'rows': 100, + 'columns': 80 + } + }; + r = repl( opts, onClose ); + + // Enter multi-line input: + istream.write( 'function foo() {' ); + istream.write( '\r' ); + + t.strictEqual( r._multilineHandler.lineIndex(), 1, 'cursor is on line 1' ); + + // Press left arrow at the beginning of line 1: + istream.write( '\u001b[1D' ); + + t.strictEqual( r._multilineHandler.lineIndex(), 0, 'cursor moves back to line 0' ); + t.strictEqual( r._rli.cursor, 16, 'cursor is at end of line 0' ); + + istream.end(); + r.close(); + + function onClose( error ) { + if ( error ) { + t.fail( error.message ); + return; + } + t.end(); + } +}); + +tape( 'a REPL instance supports moving to the next line using right arrow in multiline mode', function test( t ) { + var istream; + var opts; + var r; + + istream = new DebugStream({ + 'name': 'repl-input-stream' + }); + opts = { + 'input': istream, + 'settings': defaultSettings(), + 'tty': { + 'rows': 100, + 'columns': 80 + } + }; + r = repl( opts, onClose ); + + // Enter multi-line input: + istream.write( 'function foo() {' ); + istream.write( '\r' ); + istream.write( 'var x = 1;' ); + + // Move cursor up to line 0: + istream.write( '\u001b[1A' ); + t.strictEqual( r._multilineHandler.lineIndex(), 0, 'cursor is on line 0' ); + + // Move cursor to the end of line 0: + r._rli.cursor = r._rli.line.length; + + // Press right arrow at the end of line 0: + istream.write( '\u001b[1C' ); + + t.strictEqual( r._multilineHandler.lineIndex(), 1, 'cursor moves to line 1' ); + t.strictEqual( r._rli.cursor, 0, 'cursor is at start of line 1' ); + + istream.end(); + r.close(); + + function onClose( error ) { + if ( error ) { + t.fail( error.message ); + return; + } + t.end(); + } +}); + +tape( 'a REPL instance supports custom keybinding aliases for multiline backspace (ctrl+h)', function test( t ) { + var istream; + var opts; + var r; + + istream = new DebugStream({ + 'name': 'repl-input-stream' + }); + opts = { + 'input': istream, + 'settings': defaultSettings(), + 'tty': { + 'rows': 100, + 'columns': 80 + } + }; + r = repl( opts, onClose ); + + // Enter multi-line input: + istream.write( 'function foo() {' ); + istream.write( '\r' ); + + t.strictEqual( r._multilineHandler.lineIndex(), 1, 'cursor is on line 1' ); + + // Press ctrl+h (ASCII 8) at the start of line 1: + istream.write( '\x08' ); + + t.strictEqual( r._multilineHandler.lineIndex(), 0, 'cursor moves back to line 0' ); + t.strictEqual( r._multilineHandler._lines.length, 1, 'lines merged into line 0' ); + + istream.end(); + r.close(); + + function onClose( error ) { + if ( error ) { + t.fail( error.message ); + return; + } + t.end(); + } +});