diff --git a/CHANGELOG.md b/CHANGELOG.md index f45f768c..251646b1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ ##### Features +- [`32b9ebf`](https://github.com/stdlib-js/stdlib/commit/32b9ebf43277ff53c079178ba563fb3597661a2c) - add multiline editing in the REPL [(#2347)](https://github.com/stdlib-js/stdlib/pull/2347) - [`0856277`](https://github.com/stdlib-js/stdlib/commit/0856277523259bf111501ff87d54104b361b0fa3) - add combined styles and inbuilt syntax highlighting themes in the REPL - [`b4c12b7`](https://github.com/stdlib-js/stdlib/commit/b4c12b7c4a76cfa71164d1b01fcbfca0426abbb3) - add APIs, commands, and tests for REPL syntax-highlighting [(#2291)](https://github.com/stdlib-js/stdlib/pull/2291) - [`24f4a8f`](https://github.com/stdlib-js/stdlib/commit/24f4a8f24c08dd25686afc4cfb78be2e0045e844) - add syntax highlighting in the REPL @@ -53,9 +54,9 @@ ##### Closed Issues -A total of 4 issues were closed in this release: +A total of 5 issues were closed in this release: -[#1672](https://github.com/stdlib-js/stdlib/issues/1672), [#1775](https://github.com/stdlib-js/stdlib/issues/1775), [#2149](https://github.com/stdlib-js/stdlib/issues/2149), [#2175](https://github.com/stdlib-js/stdlib/issues/2175) +[#1672](https://github.com/stdlib-js/stdlib/issues/1672), [#1775](https://github.com/stdlib-js/stdlib/issues/1775), [#2060](https://github.com/stdlib-js/stdlib/issues/2060), [#2149](https://github.com/stdlib-js/stdlib/issues/2149), [#2175](https://github.com/stdlib-js/stdlib/issues/2175) @@ -233,9 +234,9 @@ A total of 4 issues were closed in this release: ### Closed Issues -A total of 4 issues were closed in this release: +A total of 5 issues were closed in this release: -[#1672](https://github.com/stdlib-js/stdlib/issues/1672), [#1775](https://github.com/stdlib-js/stdlib/issues/1775), [#2149](https://github.com/stdlib-js/stdlib/issues/2149), [#2175](https://github.com/stdlib-js/stdlib/issues/2175) +[#1672](https://github.com/stdlib-js/stdlib/issues/1672), [#1775](https://github.com/stdlib-js/stdlib/issues/1775), [#2060](https://github.com/stdlib-js/stdlib/issues/2060), [#2149](https://github.com/stdlib-js/stdlib/issues/2149), [#2175](https://github.com/stdlib-js/stdlib/issues/2175) @@ -267,6 +268,7 @@ A total of 9 people contributed to this release. Thank you to the following cont
+- [`32b9ebf`](https://github.com/stdlib-js/stdlib/commit/32b9ebf43277ff53c079178ba563fb3597661a2c) - **feat:** add multiline editing in the REPL [(#2347)](https://github.com/stdlib-js/stdlib/pull/2347) _(by Snehil Shah, Athan Reines)_ - [`ba4ce18`](https://github.com/stdlib-js/stdlib/commit/ba4ce188564d0207be7b780202dd4966b8dd9459) - **feat:** add `amskput` to namespace _(by Athan Reines)_ - [`3eb5c20`](https://github.com/stdlib-js/stdlib/commit/3eb5c20b3f683af347e2c502e670fb6c88527f6c) - **feat:** add `aplace` to namespace _(by Athan Reines)_ - [`642d473`](https://github.com/stdlib-js/stdlib/commit/642d4736d832f9dd83df75dfb63e56689e1fbb89) - **docs:** update REPL namespace documentation [(#2416)](https://github.com/stdlib-js/stdlib/pull/2416) _(by stdlib-bot, Athan Reines)_ diff --git a/lib/completer_preview.js b/lib/completer_preview.js index 8551e283..2eae0a89 100644 --- a/lib/completer_preview.js +++ b/lib/completer_preview.js @@ -246,34 +246,37 @@ setNonEnumerableReadOnly( PreviewCompleter.prototype, 'onKeypress', function onK * @type {Function} * @param {string} data - input data * @param {(Object|void)} key - key object -* @returns {void} +* @returns {boolean} boolean indicating whether the preview was auto-completed */ setNonEnumerableReadOnly( PreviewCompleter.prototype, 'beforeKeypress', function beforeKeypress( data, key ) { if ( !this._enabled ) { - return; + return false; } if ( !key || this._preview === '' ) { - return; + return false; } // Avoid clashing with existing TAB completion behavior... if ( key.name === 'tab' ) { - return this.clear(); + this.clear(); + return false; } // Handle the case where the user is not at the end of the line... if ( this._rli.cursor !== this._rli.line.length ) { // If a user is in the middle of a line and presses ENTER, clear the preview string, as the preview was not accepted prior to executing the expression... if ( key.name === 'return' || key.name === 'enter' ) { debug( 'Received an ENTER keypress event while in the middle of the line.' ); - return this.clear(); + this.clear(); } - return; + return false; } // When the user is at the end of the line, auto-complete the line with the completion preview when a user presses RETURN or the RIGHT arrow key (note: pressing ENTER will result in both completion AND execution)... if ( key.name === 'return' || key.name === 'enter' || key.name === 'right' ) { debug( 'Completion preview accepted. Performing auto-completion...' ); this._rli.write( this._preview ); this._preview = ''; + return true; } + return false; }); diff --git a/lib/main.js b/lib/main.js index d5fc46ca..52c7ee03 100644 --- a/lib/main.js +++ b/lib/main.js @@ -61,8 +61,8 @@ var commands = require( './commands.js' ); var displayPrompt = require( './display_prompt.js' ); var inputPrompt = require( './input_prompt.js' ); var OutputStream = require( './output_stream.js' ); -var processLine = require( './process_line.js' ); var completerFactory = require( './completer.js' ); +var MultilineHandler = require( './multiline_handler.js' ); var PreviewCompleter = require( './completer_preview.js' ); var AutoCloser = require( './auto_close_pairs.js' ); var SyntaxHighlighter = require( './syntax_highlighter.js' ); @@ -237,11 +237,6 @@ function REPL( options ) { // Define the current workspace: setNonEnumerable( this, '_currentWorkspace', 'base' ); - // Initialize an internal status object for multi-line mode: - setNonEnumerable( this, '_multiline', {} ); - setNonEnumerable( this._multiline, 'active', false ); - setNonEnumerable( this._multiline, 'mode', 'incomplete_expression' ); - // Initialize an internal flag indicating whether the REPL has been closed: setNonEnumerable( this, '_closed', false ); @@ -273,6 +268,9 @@ function REPL( options ) { 'completer': this._completer })); + // Initialize a multi-line handler: + setNonEnumerableReadOnly( this, '_multilineHandler', new MultilineHandler( this, this._rli._ttyWrite ) ); + // Create a new auto-closer: setNonEnumerableReadOnly( this, '_autoCloser', new AutoCloser( this._rli, this._settings.autoClosePairs, this._settings.autoDeletePairs ) ); @@ -337,12 +335,20 @@ function REPL( options ) { * @param {(Object|void)} key - key object */ function beforeKeypress( data, key ) { + var completed; + if ( self._ostream.isPaging ) { self._ostream.beforeKeypress( data, key ); return; } self._autoCloser.beforeKeypress( data, key ); - self._previewCompleter.beforeKeypress( data, key ); + completed = self._previewCompleter.beforeKeypress( data, key ); + + // If completion was auto-completed, don't trigger multi-line keybindings to avoid double operations... + if ( !completed ) { + self._multilineHandler.beforeKeypress( data, key ); + return; + } self._ttyWrite.call( self._rli, data, key ); } @@ -366,6 +372,7 @@ function REPL( options ) { if ( autoClosed ) { self._previewCompleter.clear(); } + self._multilineHandler.onKeypress( data, key ); self._syntaxHighlighter.onKeypress(); self._previewCompleter.onKeypress( data, key ); } @@ -379,7 +386,7 @@ function REPL( options ) { function onLine( line ) { self._SIGINT = false; // reset flag if ( self._closed === false ) { - processLine( self, line ); + self._multilineHandler.processLine( line ); } } @@ -1261,7 +1268,7 @@ setNonEnumerableReadOnly( REPL.prototype, 'clearCommand', function onClearComman throw new Error( 'invalid operation. Cannot clear the command buffer of a REPL which has already closed.' ); } // Clear any command which has been buffered but not yet executed: - this._cmd.length = 0; + this._multilineHandler.resetInput(); return this; }); diff --git a/lib/multiline_handler.js b/lib/multiline_handler.js new file mode 100644 index 00000000..ba989938 --- /dev/null +++ b/lib/multiline_handler.js @@ -0,0 +1,606 @@ +/** +* @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 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 copy = require( '@stdlib/array/base/copy' ); +var min = require( '@stdlib/math/base/special/min' ); +var displayPrompt = require( './display_prompt.js' ); +var drain = require( './drain.js' ); +var multilinePlugin = require( './acorn_detect_multiline_input.js' ); +var processCommand = require( './process_command.js' ); +var compileCommand = require( './compile_command.js' ); + + +// VARIABLES // + +var debug = logger( 'repl:line' ); +var hasMultilineError = Parser.extend( multilinePlugin ).hasMultilineError; +var RE_WHITESPACE = /^\s*$/; +var RE_SINGLE_LINE_COMMENT = /^\s*\/\//; +var RE_MULTI_LINE_COMMENT = /^\s*\/\*.*\*\/$/; +var AOPTS = { + 'ecmaVersion': 'latest' +}; + + +// MAIN // + +/** +* Constructor for creating a multi-line handler. +* +* @private +* @constructor +* @param {REPL} repl - REPL instance +* @param {Function} ttyWrite - function to trigger default behavior of a keypress +* @returns {MultilineHandler} multi-line handler instance +*/ +function MultilineHandler( repl, ttyWrite ) { + if ( !( this instanceof MultilineHandler ) ) { + return new MultilineHandler( repl, ttyWrite ); + } + + // Cache a reference to the provided REPL instance: + this._repl = repl; + + // 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 command array: + this._cmd = repl._cmd; + + // Cache a reference to the command queue: + this._queue = repl._queue; + + // Cache the length of the input prompt: + this._promptLength = repl._inputPrompt.length; + + // Initialize an internal status object for multi-line mode: + this._multiline = {}; + this._multiline.active = false; + this._multiline.trigger = false; + + // Initialize a buffer for caching input lines: + this._lines = []; + + // Initialize a variable storing current line index: + this._lineIndex = 0; + + // Initialize a buffer for storing code after cursor when manually entering multi-line mode: + this._remainingLine = ''; + + return this; +} + +/** +* Returns cursor offset for the current line index based on the prompt. +* +* @private +* @name _xOffset +* @memberof MultilineHandler.prototype +* @type {Function} +* @returns {number} `x` offset +*/ +setNonEnumerableReadOnly( MultilineHandler.prototype, '_xOffset', function xOffset() { + // If on first line, include length of input prompt as offset... + if ( this._lineIndex === 0 ) { + return this._promptLength - 1; + } + return 0; +}); + +/** +* Renders remaining lines. +* +* @name _renderLines +* @memberof MultilineHandler.prototype +* @type {Function} +* @returns {void} +*/ +setNonEnumerableReadOnly( MultilineHandler.prototype, '_renderLines', function renderLines() { + var lines; + + // Clear existing renders: + readline.clearScreenDown( this._ostream ); + + // Write remaining lines below the current line: + lines = this._lines.slice( this._lineIndex + 1 ); + this._ostream.write( '\n' + lines.join( '\n' ) ); + + // Reset cursor position: + readline.moveCursor( this._ostream, 0, min( -1 * lines.length, -1 ) ); + readline.cursorTo( this._ostream, this._xOffset() + this._rli.cursor ); +}); + +/** +* Moves cursor to a specified position in the multi-line input. +* +* @private +* @name _moveCursor +* @memberof MultilineHandler.prototype +* @type {Function} +* @param {number} x - cursor position on the line +* @param {number} dy - number of lines to move down +* @returns {void} +*/ +setNonEnumerableReadOnly( MultilineHandler.prototype, '_moveCursor', function moveCursor( x, dy ) { + var prompt = ''; + + // Clear any existing completion previews before moving lines: + this._repl._previewCompleter.clear(); + + // Change line: + this._lineIndex += dy; + readline.moveCursor( this._ostream, 0, dy ); + this._rli.line = this._cmd[ this._lineIndex ]; + + // Reset prompt: + if ( this._lineIndex === 0 ) { + prompt = this._repl._prompt(); // restore input prompt if on the first prompt + } + this._rli.setPrompt( prompt ); + this._rli.prompt(); + + // Set x cursor position: + readline.cursorTo( this._ostream, this._xOffset() + x ); + this._rli.cursor = x; +}); + +/** +* Moves cursor up to the previous line. +* +* @private +* @name _moveUp +* @memberof MultilineHandler.prototype +* @type {Function} +* @returns {void} +*/ +setNonEnumerableReadOnly( MultilineHandler.prototype, '_moveUp', function moveUp() { + var cursor; + + // If already at the first line, ignore... + if ( this._lineIndex <= 0 ) { + return; + } + this._cmd[ this._lineIndex ] = this._rli.line; // update current line in command + + // Make sure the cursor never exceeds the length of the line: + cursor = min( this._rli.cursor, this._cmd[ this._lineIndex - 1 ].length ); + this._moveCursor( cursor, -1 ); +}); + +/** +* Moves cursor down to the next line. +* +* @private +* @name _moveDown +* @memberof MultilineHandler.prototype +* @type {Function} +* @returns {void} +*/ +setNonEnumerableReadOnly( MultilineHandler.prototype, '_moveDown', function moveDown() { + var cursor; + + // If already at the last line, ignore... + if ( this._lineIndex >= this._lines.length - 1 ) { + return; + } + this._cmd[ this._lineIndex ] = this._rli.line; // update current line in command + + // Make sure the cursor never exceeds the length of the line: + cursor = min( this._rli.cursor, this._cmd[ this._lineIndex + 1 ].length ); + this._moveCursor( cursor, 1 ); +}); + +/** +* Moves cursor left to the end of the previous line. +* +* @private +* @name _moveLeft +* @memberof MultilineHandler.prototype +* @type {Function} +* @returns {void} +*/ +setNonEnumerableReadOnly( MultilineHandler.prototype, '_moveLeft', function moveLeft() { + // If already at the first line, ignore... + if ( this._lineIndex <= 0 ) { + return; + } + this._cmd[ this._lineIndex ] = this._rli.line; // update current line in command + + // Move cursor to the end of the previous line: + this._moveCursor( this._cmd[ this._lineIndex - 1 ].length, -1 ); +}); + +/** +* Moves cursor right to the start of the next line. +* +* @private +* @name _moveRight +* @memberof MultilineHandler.prototype +* @type {Function} +* @returns {void} +*/ +setNonEnumerableReadOnly( MultilineHandler.prototype, '_moveRight', function moveRight() { + // If already at the last line, ignore... + if ( this._lineIndex >= this._lines.length - 1 ) { + return; + } + this._cmd[ this._lineIndex ] = this._rli.line; // update current line in command + + // Move cursor to the start of the next line: + this._moveCursor( 0, 1 ); +}); + +/** +* Simulates a backspace by removing the current line and appending it to the previous line. +* +* @private +* @name _backspace +* @memberof MultilineHandler.prototype +* @type {Function} +* @returns {void} +*/ +setNonEnumerableReadOnly( MultilineHandler.prototype, '_backspace', function backspace() { + var cursor; + + // If already at the first line, ignore... + if ( this._lineIndex <= 0 ) { + return; + } + // Save cursor position: + cursor = this._cmd[ this._lineIndex - 1 ].length; + + // Append current line to the previous line: + this._cmd[ this._lineIndex - 1 ] += this._rli.line; + this._lines[ this._lineIndex - 1 ] += this._lines[ this._lineIndex ]; + + // Remove current line: + this._cmd.splice( this._lineIndex, 1 ); + this._lines.splice( this._lineIndex, 1 ); + + // Move cursor to the saved cursor position in the previous line: + this._moveCursor( cursor, -1 ); + + // If we deleted all additional lines, update flag... + if ( this._lines.length <= 1 ) { + this._multiline.active = false; + } +}); + +/** +* Updates flags and buffers before triggering multi-line mode. +* +* @private +* @name _triggerMultiline +* @memberof MultilineHandler.prototype +* @type {Function} +* @returns {void} +*/ +setNonEnumerableReadOnly( MultilineHandler.prototype, '_triggerMultiline', function triggerMultiline() { + // Update flag: + this._multiline.trigger = true; + + // Save expression after cursor in buffer: + readline.clearLine( this._ostream, 1 ); // clear line after cursor + this._remainingLine = this._rli.line.substring( this._rli.cursor ); + this._rli.line = this._rli.line.substring( 0, this._rli.cursor ); +}); + +/** +* Checks if the command is incomplete and a multi-line input. +* +* @private +* @name _isMultilineInput +* @memberof MultilineHandler.prototype +* @type {Function} +* @param {string} cmd - command +* @returns {boolean} boolean indicating whether the command is a multi-line input +*/ +setNonEnumerableReadOnly( MultilineHandler.prototype, '_isMultilineInput', function isMultilineInput( cmd ) { + var node; + var tmp; + var ast; + + debug( 'Attempting to detect multi-line input...' ); + if ( RE_WHITESPACE.test( cmd ) ) { + debug( 'Multi-line input not detected.' ); + return false; + } + if ( RE_SINGLE_LINE_COMMENT.test( cmd ) || RE_MULTI_LINE_COMMENT.test( cmd ) ) { // eslint-disable-line max-len + debug( 'Multi-line input not detected.' ); + return false; + } + // Check if the command has valid syntax... + tmp = processCommand( cmd ); + if ( !( tmp instanceof Error ) ) { + debug( 'Multi-line input not detected.' ); + return false; + } + if ( hasMultilineError( cmd, AOPTS ) ) { + debug( 'Detected multi-line input. Triggering multi-line mode...' ); + return true; + } + // Still possible that a user is attempting to enter an object literal across multiple lines... + ast = parseLoose( cmd, AOPTS ); + + // Check for a trailing node which is being interpreted as a block statement, as this could be an object literal... + node = ast.body[ ast.body.length-1 ]; + if ( node.type === 'BlockStatement' && node.end === ast.end ) { + tmp = cmd.slice( node.start, node.end ); + if ( hasMultilineError( tmp, AOPTS ) ) { + debug( 'Detected multi-line input. Triggering multi-line mode...' ); + return true; + } + } + debug( 'Multi-line input not detected.' ); + return false; +}); + +/** +* Updates current input line in buffer. +* +* @name updateLine +* @memberof MultilineHandler.prototype +* @type {Function} +* @param {string} line - updated line +* @returns {void} +*/ +setNonEnumerableReadOnly( MultilineHandler.prototype, 'updateLine', function updateLine( line ) { + this._lines[ this._lineIndex ] = line; +}); + +/** +* 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; +}); + +/** +* Processes input line data. +* +* @name processLine +* @memberof MultilineHandler.prototype +* @type {Function} +* @param {string} line - line data +* @returns {void} +*/ +setNonEnumerableReadOnly( MultilineHandler.prototype, 'processLine', function processLine( line ) { + var code; + var cmd; + var tmp; + var dy; + + // Save line: + debug( 'Line: %s', line ); + this._cmd[ this._lineIndex ] = line; + + // Check for multi-line triggers... + if ( this._multiline.trigger ) { + debug( 'Detected multi-line trigger. Waiting for additional lines...' ); + + // Insert a newline: + this._lineIndex += 1; + this._cmd.splice( this._lineIndex, 0, this._remainingLine ); + this._lines.splice( this._lineIndex, 0, this._remainingLine ); + + // Insert buffer input after cursor from previous line: + displayPrompt( this._repl, false ); + this._ostream.write( this._remainingLine ); + readline.cursorTo( this._ostream, 0 ); + this._rli.line = this._remainingLine; + + // Update flags and buffers: + this._remainingLine = ''; + this._multiline.trigger = false; + this._multiline.active = true; + return; + } + this._multiline.active = false; // false until proven otherwise + cmd = this._cmd.join( '\n' ); + if ( RE_WHITESPACE.test( cmd ) ) { + this.resetInput(); + displayPrompt( this._repl, false ); + return; + } + if ( RE_SINGLE_LINE_COMMENT.test( cmd ) || RE_MULTI_LINE_COMMENT.test( cmd ) ) { // eslint-disable-line max-len + debug( 'Detected single-line comment.' ); + tmp = cmd; + } else { + // Check if the command has valid syntax... + debug( 'Processing command...' ); + tmp = processCommand( cmd ); + if ( tmp instanceof Error ) { + // Move cursor to the output row: + dy = this._lines.length - this._lineIndex - 1; + readline.moveCursor( this._ostream, 0, dy ); + + // Reset the input buffers: + this.resetInput(); + + // Write error message and display next prompt: + this._ostream.write( 'Error: '+tmp.message+'\n' ); + this._repl.emit( 'command', cmd, false ); // command failed + displayPrompt( this._repl, false ); + return; + } + } + debug( 'Successfully processed command.' ); + + // Move cursor to the output row: + dy = this._lines.length - this._lineIndex - 1; + readline.moveCursor( this._ostream, 0, dy ); + + // Reset the input buffers: + this.resetInput(); + + // Attempt to compile the command: + debug( 'Attempting to compile command...' ); + code = compileCommand( tmp ); + if ( code instanceof Error ) { + debug( 'Error: %s', code.message ); + this._ostream.write( 'Error: '+code.message+'\n' ); + this._repl.emit( 'command', cmd, false ); // command failed + displayPrompt( this._repl, false ); + return; + } + debug( 'Successfully compiled command.' ); + code.raw = cmd; + + // Add the command to the command queue: + this._queue.push( code ); + + // Request to run the command: + drain( this._repl ); +}); + +/** +* Callback for handling a "keypress" event. +* +* @name onKeypress +* @memberof MultilineHandler.prototype +* @type {Function} +* @param {string} data - input data +* @param {(Object|void)} key - key object +* @returns {void} +*/ +setNonEnumerableReadOnly( MultilineHandler.prototype, 'onKeypress', function onKeypress( data, key ) { + if ( !key ) { + return; + } + // Trigger multi-line input when encountering `CTRL+O` keybinding... + if ( key.name === 'o' && key.ctrl ) { + this._triggerMultiline(); + + // Simulate `line` event: + this._rli.write( '\n' ); + } + if ( this._multiline.active ) { + // Render remaining lines with each keypress when in multi-line mode: + this._renderLines(); + } +}); + +/** +* Callback which should be invoked **before** a "keypress" event is processed by a readline interface. +* +* @name beforeKeypress +* @memberof MultilineHandler.prototype +* @type {Function} +* @param {string} data - input data +* @param {(Object|void)} key - key object +* @returns {void} +*/ +setNonEnumerableReadOnly( MultilineHandler.prototype, 'beforeKeypress', function beforeKeypress( data, key ) { + var cmd; + + if ( !key ) { + this._ttyWrite.call( this._rli, data, key ); + return; + } + // Check whether to trigger multi-line mode or execute the command when `return` key is encountered... + if ( key.name === 'return' ) { + cmd = copy( this._cmd ); + cmd[ this._lineIndex ] = this._rli.line; + + // If command is incomplete, trigger multi-line mode... + if ( !this._isMultilineInput( cmd.join( '\n' ) ) ) { + this._ttyWrite.call( this._rli, data, key ); + return; + } + this._triggerMultiline(); + + // Trigger `line` event: + this._ttyWrite.call( this._rli, data, key ); + return; + } + if ( !this._multiline.active ) { + this._ttyWrite.call( this._rli, data, key ); + return; + } + // If multi-line mode is active, enable navigation... + switch ( key.name ) { + case 'up': + this._moveUp(); + this._renderLines(); + break; + case 'down': + this._moveDown(); + this._renderLines(); + break; + case 'left': + // 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(); + return; + } + this._ttyWrite.call( this._rli, data, key ); + break; + case 'right': + // 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(); + return; + } + this._ttyWrite.call( this._rli, data, key ); + break; + case 'backspace': + // 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(); + return; + } + this._ttyWrite.call( this._rli, data, key ); + break; + default: + this._ttyWrite.call( this._rli, data, key ); + break; + } +}); + + +// EXPORTS // + +module.exports = MultilineHandler; diff --git a/lib/process_line.js b/lib/process_line.js deleted file mode 100644 index 8dcce735..00000000 --- a/lib/process_line.js +++ /dev/null @@ -1,141 +0,0 @@ -/** -* @license Apache-2.0 -* -* Copyright (c) 2019 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 */ - -'use strict'; - -// MODULES // - -var logger = require( 'debug' ); -var Parser = require( 'acorn' ).Parser; -var parseLoose = require( 'acorn-loose' ).parse; -var displayPrompt = require( './display_prompt.js' ); -var drain = require( './drain.js' ); -var multilinePlugin = require( './acorn_detect_multiline_input.js' ); -var processCommand = require( './process_command.js' ); -var compileCommand = require( './compile_command.js' ); - - -// VARIABLES // - -var debug = logger( 'repl:line' ); -var hasMultilineError = Parser.extend( multilinePlugin ).hasMultilineError; -var RE_WHITESPACE = /^\s*$/; -var RE_SINGLE_LINE_COMMENT = /^\s*\/\//; -var RE_MULTI_LINE_COMMENT = /^\s*\/\*.*\*\/$/; -var AOPTS = { - 'ecmaVersion': 'latest' -}; - - -// MAIN // - -/** -* Processes input line data. -* -* @private -* @param {REPL} repl - REPL instance -* @param {string} line - line data -* @returns {void} -*/ -function processLine( repl, line ) { - var code; - var node; - var ast; - var cmd; - var tmp; - - debug( 'Line: %s', line ); - repl._multiline.active = false; // false until proven otherwise - - cmd = repl._cmd.join( '\n' ) + line; - if ( RE_WHITESPACE.test( cmd ) ) { - displayPrompt( repl, false ); - return; - } - if ( RE_SINGLE_LINE_COMMENT.test( cmd ) || RE_MULTI_LINE_COMMENT.test( cmd ) ) { // eslint-disable-line max-len - debug( 'Detected single-line comment.' ); - tmp = cmd; - } else { - // Check if the command has valid syntax... - debug( 'Processing command...' ); - tmp = processCommand( cmd ); - if ( tmp instanceof Error ) { - debug( 'Unable to process command.' ); - debug( 'Error: %s', tmp.message ); - debug( 'Attempting to detect multi-line input...' ); - if ( hasMultilineError( cmd, AOPTS ) ) { - debug( 'Detected multi-line input. Waiting for additional lines...' ); - repl._cmd.push( line ); - repl._multiline.active = true; - displayPrompt( repl, false ); - return; - } - // Still possible that a user is attempting to enter an object literal across multiple lines... - ast = parseLoose( cmd, AOPTS ); - - // Check for a trailing node which is being interpreted as a block statement, as this could be an object literal... - node = ast.body[ ast.body.length-1 ]; - if ( node.type === 'BlockStatement' && node.end === ast.end ) { - tmp = cmd.slice( node.start, node.end ); - if ( hasMultilineError( tmp, AOPTS ) ) { - debug( 'Detected multi-line input. Waiting for additional lines...' ); - repl._cmd.push( line ); - repl._multiline.active = true; - displayPrompt( repl, false ); - return; - } - } - debug( 'Multi-line input not detected.' ); - repl._ostream.write( 'Error: '+tmp.message+'\n' ); - repl._cmd.length = 0; - repl.emit( 'command', cmd, false ); // command failed - displayPrompt( repl, false ); - return; - } - } - debug( 'Successfully processed command.' ); - - // Reset the command buffer: - repl._cmd.length = 0; - - // Attempt to compile the command: - debug( 'Attempting to compile command...' ); - code = compileCommand( tmp ); - if ( code instanceof Error ) { - debug( 'Error: %s', code.message ); - repl._ostream.write( 'Error: '+code.message+'\n' ); - repl.emit( 'command', cmd, false ); // command failed - displayPrompt( repl, false ); - return; - } - debug( 'Successfully compiled command.' ); - code.raw = cmd; - - // Add the command to the command queue: - repl._queue.push( code ); - - // Request to run the command: - drain( repl ); -} - - -// EXPORTS // - -module.exports = processLine; diff --git a/lib/syntax_highlighter.js b/lib/syntax_highlighter.js index f314d492..d0b14613 100644 --- a/lib/syntax_highlighter.js +++ b/lib/syntax_highlighter.js @@ -85,6 +85,9 @@ function SyntaxHighlighter( repl, ostream, enabled ) { // Cache a reference to the output writable stream: this._ostream = ostream; + // Cache a reference to the REPL's multi-line handler: + this._multilineHandler = repl._multilineHandler; + // Initialize a buffer containing the current line to validate line changes: this._line = ''; @@ -293,25 +296,27 @@ setNonEnumerableReadOnly( SyntaxHighlighter.prototype, 'onKeypress', function on var tokens; if ( !this._enabled ) { + // Save raw output if syntax highlighting is disabled: + this._multilineHandler.updateLine( this._rli.line ); // save displayed line return; } if ( !this._rli.line ) { debug( 'Empty line detected. Skipping highlighting...' ); + this._multilineHandler.updateLine( this._rli.line ); // save displayed line return; } if ( this._line !== this._rli.line ) { - // Update line buffer: - this._line = this._rli.line; - // Tokenize: - debug( 'Line change detected. Tokenizing line: %s', this._line ); - tokens = tokenizer( this._line, this._repl._context ); + debug( 'Line change detected. Tokenizing line: %s', this._rli.line ); + tokens = tokenizer( this._rli.line, this._repl._context ); if ( !tokens ) { debug( 'No tokens found. Skipping highlighting...' ); + this._multilineHandler.updateLine( this._rli.line ); // save displayed line return; } // Highlight: debug( '%d tokens found. Highlighting...', tokens.length ); + this._line = this._rli.line; // updated line buffer this._highlightedLine = this._highlightLine( this._line, tokens ); } // Replace: @@ -320,6 +325,7 @@ setNonEnumerableReadOnly( SyntaxHighlighter.prototype, 'onKeypress', function on readline.clearLine( this._ostream, 1 ); this._ostream.write( this._highlightedLine ); readline.moveCursor( this._ostream, this._rli.cursor - this._line.length, 0 ); // eslint-disable-line max-len + this._multilineHandler.updateLine( this._highlightedLine ); // save displayed line });