From 989e2b499d94a117a4df07a48b698709df1349d7 Mon Sep 17 00:00:00 2001 From: Snehil Shah Date: Wed, 26 Jun 2024 12:38:59 +0000 Subject: [PATCH 01/27] feat: add UX to cycle through completions Signed-off-by: Snehil Shah --- .../@stdlib/repl/lib/completer.js | 2 +- .../@stdlib/repl/lib/completer_engine.js | 693 ++++++++++++++++++ lib/node_modules/@stdlib/repl/lib/main.js | 30 +- .../@stdlib/repl/lib/multiline_handler.js | 53 +- .../test/integration/test.completer_engine.js | 488 ++++++++++++ 5 files changed, 1235 insertions(+), 31 deletions(-) create mode 100644 lib/node_modules/@stdlib/repl/lib/completer_engine.js create mode 100644 lib/node_modules/@stdlib/repl/test/integration/test.completer_engine.js diff --git a/lib/node_modules/@stdlib/repl/lib/completer.js b/lib/node_modules/@stdlib/repl/lib/completer.js index 5871b7887ae2..ce393c6e5b43 100644 --- a/lib/node_modules/@stdlib/repl/lib/completer.js +++ b/lib/node_modules/@stdlib/repl/lib/completer.js @@ -41,7 +41,7 @@ var completeExpression = require( './complete_expression.js' ); // VARIABLES // -var debug = logger( 'repl:completer' ); +var debug = logger( 'repl:completer:callback' ); // FUNCTIONS // diff --git a/lib/node_modules/@stdlib/repl/lib/completer_engine.js b/lib/node_modules/@stdlib/repl/lib/completer_engine.js new file mode 100644 index 000000000000..e0896c589475 --- /dev/null +++ b/lib/node_modules/@stdlib/repl/lib/completer_engine.js @@ -0,0 +1,693 @@ +/** +* @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-restricted-syntax, no-underscore-dangle, no-invalid-this, max-lines */ + +'use strict'; + +// MODULES // + +var readline = require( 'readline' ); +var logger = require( 'debug' ); +var setNonEnumerableReadOnly = require( '@stdlib/utils/define-nonenumerable-read-only-property' ); +var lowercase = require( '@stdlib/string/lowercase' ); +var maxInArray = require( '@stdlib/stats/base/max' ); +var max = require( '@stdlib/math/base/special/max' ); +var min = require( '@stdlib/math/base/special/min' ); +var floor = require( '@stdlib/math/base/special/floor' ); +var ceil = require( '@stdlib/math/base/special/ceil' ); +var repeat = require( '@stdlib/string/repeat' ); +var replace = require( '@stdlib/string/replace' ); +var removeLast = require( '@stdlib/string/remove-last' ); +var contains = require( '@stdlib/array/base/assert/contains' ); +var startsWith = require( '@stdlib/string/starts-with' ); +var isEmptyString = require( '@stdlib/assert/is-empty-string' ).isPrimitive; +var commonPrefix = require( './longest_common_prefix.js' ); + + +// VARIABLES // + +var debug = logger( 'repl:completer:engine' ); +var RE_ANSI = /[\u001B\u009B][[\]()#;?]*(?:(?:(?:(?:;[-a-zA-Z\d/#&.:=?%@~_]+)*|[a-zA-Z\d]+(?:;[-a-zA-Z\d/#&.:=?%@~_]*)*)?\u0007)|(?:(?:\d{1,4}(?:;\d{0,4})*)?[\dA-PR-TZcf-nq-uy=><~]))/g; // eslint-disable-line no-control-regex +var RESERVED_COMPLETER_ROWS = 1; // bottom margin + + +// FUNCTIONS // + +/** +* Removes ANSI escape codes from a string. +* +* @private +* @param {string} str - input string +* @returns {string} string with ANSI escape codes removed +*/ +function stripANSI( str ) { + return replace( str, RE_ANSI, '' ); +} + + +// MAIN // + +/** +* Constructor for creating a completer engine. +* +* @private +* @constructor +* @param {REPL} repl - REPL instance +* @param {Function} completer - function for generating possible completions +* @param {WritableStream} ostream - writable stream +* @param {Function} ttyWrite - function to trigger the default behavior of the keypress +* @returns {CompleterEngine} completer engine instance +*/ +function CompleterEngine( repl, completer, ostream, ttyWrite ) { + if ( !( this instanceof CompleterEngine ) ) { + return new CompleterEngine( repl, completer, ostream, ttyWrite ); + } + debug( 'Creating a completer engine...' ); + + // 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 = ostream; + + // Cache a reference to the provided completer; + this._completer = completer; + + // 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's multi-line handler: + this._multiline = repl._multilineHandler; + + // Initialize a flag indicating whether a user is navigating TAB completions: + this._isNavigating = false; + + // Create a callback for processing completions: + this._onCompletions = this._completionCallback(); + + // Initialize a buffer containing the input line being processed: + this._inputLine = ''; + + // Initialize a buffer containing the remaining line after cursor: + this._remainingLine = ''; + + // Initialize a buffer containing the completion prefix: + this._completionPrefix = ''; + + // Initialize a buffer containing the list of generated completions: + this._completionsList = []; + + // Initialize a buffer containing the list of highlighted completions: + this._highlightedCompletions = []; + + // Initialize a buffer array storing the lengths of all completions: + this._completionsLength = []; + + // Initialize a buffer storing the width of a column: + this._widthOfColumn = -1; + + // Initialize a buffer to store the index of the current completion: + this._idx = -1; + + return this; +} + +/** +* Returns a callback for processing completions. +* +* @private +* @name _completionCallback +* @memberof CompleterEngine.prototype +* @type {Function} +* @returns {Function} completion callback +*/ +setNonEnumerableReadOnly( CompleterEngine.prototype, '_completionCallback', function completionCallback() { + var self = this; + return clbk; + + /** + * Callback invoked upon resolving potential completions. + * + * @private + * @param {(Error|null)} error - error object + * @param {Array} completions - completion results + * @returns {void} + */ + function clbk( error, completions ) { + var autoCompletion; + var i; + + // Check whether we encountered an error when generating completions... + if ( error ) { + debug( 'Encountered an error when generating completions.' ); + self._ostream.write( 'Error: couldn\'t generate tab completions' ); + + // Resume the input stream: + self._rli.resume(); + return; + } + // Remove empty completions: + self._completionsList = []; + for ( i = 0; i < completions[ 0 ].length; i++ ) { + if ( !isEmptyString( completions[ 0 ][ i ] ) ) { + self._completionsList.push( completions[ 0 ][ i ] ); + } + } + if ( self._completionsList.length === 0 ) { + debug( 'No completions to display.' ); + + // Resume the input stream: + self._rli.resume(); + return; + } + self._completionPrefix = completions[ 1 ]; + + // Resolve a common prefix from the completion results: + autoCompletion = commonPrefix( self._completionsList ); // e.g., [ 'back', 'background', 'backward' ] => 'back' + + // If the completion candidates have a possible auto-completion (ie. a common prefix longer than the input), auto-complete it... + if ( autoCompletion !== '' && autoCompletion.length > self._completionPrefix.length ) { + debug( 'Found an auto-completion candidate: %s', autoCompletion ); + + // Clear the completion prefix: + self._ostream.write( repeat( '\x08', self._completionPrefix.length ) ); + self._rli.line = self._rli.line.slice( 0, self._rli.cursor - self._completionPrefix.length ) + self._rli.line.slice( self._rli.cursor ); // eslint-disable-line max-len + + // Move the cursor to the start of completion prefix: + self._rli.cursor -= self._completionPrefix.length; + + // Write the auto-completion string: + self._rli.write( autoCompletion ); + + // Resume the input stream: + self._rli.resume(); + return; + } + debug( 'No auto-completion candidate, displaying all possible completions.' ); + + // Check if completions can be displayed... + if ( !self._isDisplayable() ) { + debug( 'TTY height too short. exiting completer...' ); + self._rli.resume(); + return; + } + + // Display completions: + self._displayCompletions(); + self._isNavigating = true; + + // Resume the input stream: + self._rli.resume(); + } +}); + +/** +* Displays the completions to the output stream. +* +* @private +* @name _displayCompletions +* @memberof CompleterEngine.prototype +* @type {Function} +* @returns {void} +*/ +setNonEnumerableReadOnly( CompleterEngine.prototype, '_displayCompletions', function displayCompletions() { + var columns; + var rows; + var dy; + var i; + + // Determine number of columns of completions that should be displayed to the output stream + this._completionsLength = []; + for ( i = 0; i < this._completionsList.length; i++ ) { + this._completionsLength.push( this._completionsList[ i ].length ); + } + this._widthOfColumn = maxInArray( this._completionsLength.length, this._completionsLength, 1 ) + 4; // eslint-disable-line max-len + + // Highlight completions if operating in "terminal" mode... + if ( this._repl._isTTY ) { + this._highlightedCompletions = this._highlightCompletions(); + } else { + this._highlightedCompletions = this._completionsList; + } + + // Determine dimensions of the output grid: + columns = this._completionColumns(); + rows = this._completionRows( columns ); + + // Move cursor to the output row: + dy = this._multiline.inputHeight() - this._multiline.lineIndex() - 1; + readline.moveCursor( this._ostream, 0, dy ); + + // Write completions to the output stream: + this._ostream.write( this._drawOutput( rows, columns ) ); + + // Bring the cursor back to the current line: + readline.moveCursor( this._ostream, 0, -1 * ( rows + this._multiline.inputHeight() - this._multiline.lineIndex() ) ); // eslint-disable-line max-len + readline.cursorTo( this._ostream, this._rli.cursor + this._repl.promptLength() ); // eslint-disable-line max-len +}); + +/** +* Draws the completions output grid. +* +* @private +* @name _drawOutput +* @memberof CompleterEngine.prototype +* @type {Function} +* @param {number} rows - number of rows in output grid +* @param {number} columns - number of columns in output grid +* @returns {string} output string +*/ +setNonEnumerableReadOnly( CompleterEngine.prototype, '_drawOutput', function drawOutput( rows, columns ) { + var whitespaces; + var completion; + var lineIndex; + var output; + var i; + + // Draw the output grid: + output = '\r\n'; + lineIndex = 0; + whitespaces = 0; + for ( i = 0; i < this._highlightedCompletions.length; i++ ) { + completion = this._highlightedCompletions[ i ]; + + // If completions start overflowing the maximum allowed rows, stop writing to output... + if ( i >= rows * columns ) { + break; + } + if ( lineIndex >= columns ) { + // Reached end of column, enter next line: + output += '\r\n'; + lineIndex = 0; + whitespaces = 0; + } else { + // Fill the space to move to the next column: + output += repeat( ' ', whitespaces ); + } + if ( i === this._idx && this._repl._isTTY ) { + // Highlight the current navigated index: + completion = stripANSI( completion ); + completion = '\u001b[7m' + completion + '\u001b[27m'; + } + // Add completion string to the column in output: + output += completion; + whitespaces = this._widthOfColumn - this._completionsLength[ i ]; + lineIndex += 1; + } + output += '\r\n'; + return output; +}); + +/** +* Highlights the matching parts of the completions based on the current line. +* +* @private +* @name _highlightCompletions +* @memberof CompleterEngine.prototype +* @type {Function} +* @returns {Array} array of highlighted completions +*/ +setNonEnumerableReadOnly( CompleterEngine.prototype, '_highlightCompletions', function highlightCompletions() { + var highlightedCompletions; + var lastMatchedIndex; + var completionIndex; + var highlighted; + var boldIndexes; + var completion; + var lineIndex; + var i; + var j; + + highlightedCompletions = []; + for ( i = 0; i < this._completionsList.length; i++) { + completion = this._completionsList[ i ]; + lastMatchedIndex = -1; + completionIndex = 0; + highlighted = ''; + boldIndexes = []; // Buffer to store indexes of characters in completion string that needs to be highlighted + lineIndex = 0; + + // If input is an exact prefix of completion, directly highlight the substring... + if ( startsWith( completion, this._completionPrefix ) ) { + highlighted = '\u001b[1m' + completion.slice( 0, this._completionPrefix.length ) + '\u001b[0m' + completion.slice( this._completionPrefix.length ); + } else { + // Store indexes of each matching character in the completion string in the buffer... + while ( lineIndex < this._completionPrefix.length && completionIndex < completion.length ) { // eslint-disable-line max-len + if ( lowercase( completion[ completionIndex ] ) === lowercase( this._completionPrefix[ lineIndex ] ) ) { // eslint-disable-line max-len + boldIndexes.push( completionIndex ); + lastMatchedIndex = completionIndex; + lineIndex += 1; + } else if ( completionIndex + 1 === completion.length ) { + lineIndex += 1; + completionIndex = lastMatchedIndex + 1; + } + completionIndex += 1; + } + // Highlight stored indexes in the completion string: + for ( j = 0; j < completion.length; j++ ) { + if ( contains( boldIndexes, j ) ) { + highlighted += '\u001b[1m' + completion[ j ] + '\u001b[0m'; + } else { + highlighted += completion[ j ]; + } + } + } + highlightedCompletions.push( highlighted ); + } + return highlightedCompletions; +}); + +/** +* Returns the number of columns in the completions output grid. +* +* @name _completionColumns +* @memberof CompleterEngine.prototype +* @type {Function} +* @returns {number} number of columns +*/ +setNonEnumerableReadOnly( CompleterEngine.prototype, '_completionColumns', function completionColumns() { + return floor( this._repl.viewportWidth() / this._widthOfColumn ) || 1; +}); + +/** +* Returns the number of rows in the completions output grid. +* +* @name _completionRows +* @memberof CompleterEngine.prototype +* @type {Function} +* @param {number} columns - number of columns in the completions output grid +* @returns {number} number of rows +*/ +setNonEnumerableReadOnly( CompleterEngine.prototype, '_completionRows', function completionRows( columns ) { + var maxRows = max( this._repl.viewportHeight() - ( this._multiline.inputHeight() + RESERVED_COMPLETER_ROWS ), 0 ); // eslint-disable-line max-len + var rows = ceil( this._completionsList.length / columns ); + + // Truncate number of completion rows to fit the viewport: + return min( rows, maxRows ); +}); + +/** +* Checks whether content having a specified number of lines is unable to fit within the current viewport. +* +* @private +* @name _isDisplayable +* @memberof CompleterEngine.prototype +* @type {Function} +* @returns {boolean} boolean indicating whether content is "displayable" +*/ +setNonEnumerableReadOnly( CompleterEngine.prototype, '_isDisplayable', function isDisplayable() { + var vh = this._repl.viewportHeight(); + var ih = this._multiline.inputHeight(); + return vh > ih + RESERVED_COMPLETER_ROWS; +}); + +/** +* Closes completer engine. +* +* @private +* @name _closeCompleter +* @memberof CompleterEngine.prototype +* @type {Function} +* @returns {void} +*/ +setNonEnumerableReadOnly( CompleterEngine.prototype, '_closeCompleter', function closeCompleter() { + // Reset completer parameters: + this._isNavigating = false; + this._idx = -1; + + // Clear completions output: + readline.clearScreenDown( this._ostream ); + + // Reset the internal completer buffers: + this._inputLine = ''; + this._remainingLine = ''; + this._completionPrefix = ''; + this._completionsList = []; + this._highlightedCompletions = []; + this._completionsLength = []; + this._widthOfColumn = -1; +}); + +/** +* Navigate up the completions grid. +* +* @private +* @name _navigateUp +* @memberof CompleterEngine.prototype +* @type {Function} +* @param {string} data - input data +* @param {Object} key - key object +* @returns {void} +*/ +setNonEnumerableReadOnly( CompleterEngine.prototype, '_navigateUp', function navigateUp( data, key ) { + var columns = this._completionColumns(); + + // If already on the line, close the completer... + if ( this._idx === -1 ) { + this._closeCompleter(); + this._ttyWrite.call( this._rli, data, key ); + return; + } + // Move to the previous row: + if ( this._idx - columns >= 0 ) { + this._idx -= columns; + } else { + this._idx = -1; + } + this.updateCompletions(); +}); + +/** +* Navigate down the completions grid. +* +* @private +* @name _navigateDown +* @memberof CompleterEngine.prototype +* @type {Function} +* @returns {void} +*/ +setNonEnumerableReadOnly( CompleterEngine.prototype, '_navigateDown', function navigateDown() { + var columns = this._completionColumns(); + var rows = this._completionRows( columns ); + + // Move to the next row... + if ( this._idx === -1 ) { + this._idx = 0; + this.updateCompletions(); + } else if ( this._idx + columns < rows * columns ) { + this._idx += columns; + this.updateCompletions(); + } +}); + +/** +* Navigate to the left in the completions grid. +* +* @private +* @name _navigateLeft +* @memberof CompleterEngine.prototype +* @type {Function} +* @param {string} data - input data +* @param {Object} key - key object +* @returns {void} +*/ +setNonEnumerableReadOnly( CompleterEngine.prototype, '_navigateLeft', function navigateLeft( data, key ) { + // If on current line, trigger default behavior and stop navigating... + if ( this._idx === -1 ) { + this._closeCompleter(); + this._ttyWrite.call( this._rli, data, key ); + return; + } + // If navigating, move back an index... + if ( this._idx > 0 ) { + this._idx -= 1; + this.updateCompletions(); + } +}); + +/** +* Navigate to the right in the completions grid. +* +* @private +* @name _navigateRight +* @memberof CompleterEngine.prototype +* @type {Function} +* @param {string} data - input data +* @param {Object} key - key object +* @returns {void} +*/ +setNonEnumerableReadOnly( CompleterEngine.prototype, '_navigateRight', function navigateRight( data, key ) { + // If on current line, trigger default behavior and stop navigating... + if ( this._idx === -1 ) { + this._closeCompleter(); + this._ttyWrite.call( this._rli, data, key ); + return; + } + // If navigating, move ahead an index... + if ( this._idx < this._completionsList.length - 1 ) { + this._idx += 1; + this.updateCompletions(); + } +}); + +/** +* Re-displays the navigated completions to the output stream. +* +* @name updateCompletions +* @memberof CompleterEngine.prototype +* @type {Function} +* @returns {void} +*/ +setNonEnumerableReadOnly( CompleterEngine.prototype, 'updateCompletions', function updateCompletions() { + var columns; + var rows; + var dy; + + // Determine dimensions of the output grid: + columns = this._completionColumns(); + rows = this._completionRows( columns ); + + // Move cursor to the output row: + dy = this._multiline.inputHeight() - this._multiline.lineIndex() - 1; + readline.moveCursor( this._ostream, 0, dy ); + + // Write completions to the output stream: + this._ostream.write( this._drawOutput( rows, columns ) ); + + // Bring the cursor back to the current line: + readline.moveCursor( this._ostream, 0, -1 * ( rows + this._multiline.inputHeight() - this._multiline.lineIndex() ) ); // eslint-disable-line max-len + readline.cursorTo( this._ostream, this._repl.promptLength() ); + this._rli.cursor = 0; + + // Insert the navigated suggestion to the current line: + readline.clearLine( this._ostream, 1 ); + this._rli.line = ''; + this._rli.write( removeLast( this._inputLine, this._completionPrefix.length ) + ( this._completionsList[ this._idx ] || this._completionPrefix ) + this._remainingLine ); // eslint-disable-line max-len + readline.moveCursor( this._ostream, -1 * this._remainingLine.length ); + this._rli.cursor -= this._remainingLine.length; +}); + +/** +* Checks whether the user is currently navigating completions. +* +* @name isNavigating +* @memberof CompleterEngine.prototype +* @type {Function} +* @returns {boolean} boolean indicating whether user is currently navigating completions +*/ +setNonEnumerableReadOnly( CompleterEngine.prototype, 'isNavigating', function isNavigating() { + return this._isNavigating; +}); + +/** +* Callback which should be invoked **before** a "keypress" event is processed by a readline interface. +* +* @name beforeKeypress +* @memberof CompleterEngine.prototype +* @type {Function} +* @param {string} data - input data +* @param {Object} key - key object +* @returns {void} +*/ +setNonEnumerableReadOnly( CompleterEngine.prototype, 'beforeKeypress', function beforeKeypress( data, key ) { + var cursor; + var line; + + if ( !key ) { + this._ttyWrite.call( this._rli, data, key ); + return; + } + + // If user is already viewing completions, allow navigating it... + if ( this._isNavigating ) { + switch ( key.name ) { + // If user presses TAB while navigating, toggle close the completer: + case 'tab': + debug( 'Received a TAB keypress event. Closing the completer engine...' ); + this._closeCompleter(); + + // NOTE: `clearScreenDown` seems to behave abnormally in this case by also clearing the `ostream` on this right of the cursor. Hence writing it again below: + this._ostream.write( this._remainingLine ); + readline.moveCursor( this._ostream, -1 * this._remainingLine.length ); // eslint-disable-line max-len + break; + + // If arrow keys detected, allow navigating the completions, else stop navigating and continue default behavior... + case 'down': + debug( 'Received a DOWN keypress event...' ); + this._navigateDown(); + break; + case 'up': + debug( 'Received an UP keypress event...' ); + this._navigateUp( data, key ); + break; + case 'left': + debug( 'Received a LEFT keypress event...' ); + this._navigateLeft( data, key ); + break; + case 'right': + debug( 'Received a RIGHT keypress event...' ); + this._navigateRight( data, key ); + break; + default: + this._closeCompleter(); + this._ttyWrite.call( this._rli, data, key ); + } + return; + } + // Trigger TAB completions: + cursor = this._rli.cursor; + line = this._rli.line; + + // Get the line before the cursor: + this._inputLine = line.slice(0, cursor); + + // Get the line after the cursor: + this._remainingLine = line.slice(cursor); + + // Pause the input stream before generating completions as the completer may be asynchronous... + this._rli.pause(); + this._completer( this._inputLine, this._onCompletions ); +}); + +/** +* Callback which should be invoked upon a "resize" event. +* +* @name onResize +* @memberof CompleterEngine.prototype +* @type {Function} +* @returns {void} +*/ +setNonEnumerableReadOnly( CompleterEngine.prototype, 'onResize', function onResize() { + if ( !this._isNavigating ) { + return; + } + if ( !this._isDisplayable() ) { + this._closeCompleter(); + return; + } + this.updateCompletions(); +}); + + +// EXPORTS // + +module.exports = CompleterEngine; diff --git a/lib/node_modules/@stdlib/repl/lib/main.js b/lib/node_modules/@stdlib/repl/lib/main.js index 52c7ee038fb8..a850007e6eb2 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 CompleterEngine = require( './completer_engine.js' ); var PreviewCompleter = require( './completer_preview.js' ); var AutoCloser = require( './auto_close_pairs.js' ); var SyntaxHighlighter = require( './syntax_highlighter.js' ); @@ -264,13 +265,15 @@ function REPL( options ) { 'input': this._istream, 'output': this._ostream, 'terminal': opts.isTTY, - 'prompt': opts.inputPrompt, - 'completer': this._completer + 'prompt': opts.inputPrompt })); // Initialize a multi-line handler: setNonEnumerableReadOnly( this, '_multilineHandler', new MultilineHandler( this, this._rli._ttyWrite ) ); + // Create a new TAB completer engine: + 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 ) ); @@ -342,6 +345,10 @@ function REPL( options ) { return; } self._autoCloser.beforeKeypress( data, key ); + if ( ( key && key.name === 'tab' ) || self._completerEngine.isNavigating() ) { + self._completerEngine.beforeKeypress( data, key ); + return; + } completed = self._previewCompleter.beforeKeypress( data, key ); // If completion was auto-completed, don't trigger multi-line keybindings to avoid double operations... @@ -363,9 +370,6 @@ function REPL( options ) { function onKeypress( data, key ) { var autoClosed; - if ( key && key.name === 'tab' ) { - return; - } autoClosed = self._autoCloser.onKeypress( data, key ); // If auto-closing was performed, explicitly remove any currently displayed completion preview... @@ -416,6 +420,7 @@ function REPL( options ) { function onSIGWINCH() { debug( 'Received a SIGWINCH event. Terminal was resized.' ); self._ostream.onResize(); + self._completerEngine.onResize(); } /** @@ -515,6 +520,21 @@ setNonEnumerableReadOnly( REPL.prototype, '_prompt', function prompt() { return inputPrompt( this._inputPrompt, this._count ); }); +/** +* Returns the current line's prompt length. +* +* @name promptLength +* @memberof REPL.prototype +* @type {Function} +* @returns {number} prompt length +*/ +setNonEnumerableReadOnly( REPL.prototype, 'promptLength', function promptLength() { + if ( this._multilineHandler.lineIndex() === 0 ) { + return this._prompt().length; + } + return 0; +}); + /** * Returns the REPL viewport. * diff --git a/lib/node_modules/@stdlib/repl/lib/multiline_handler.js b/lib/node_modules/@stdlib/repl/lib/multiline_handler.js index ba9899380cd9..3b5ac57407a0 100644 --- a/lib/node_modules/@stdlib/repl/lib/multiline_handler.js +++ b/lib/node_modules/@stdlib/repl/lib/multiline_handler.js @@ -82,9 +82,6 @@ function MultilineHandler( repl, ttyWrite ) { // 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; @@ -102,23 +99,6 @@ function MultilineHandler( repl, ttyWrite ) { 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. * @@ -130,16 +110,13 @@ setNonEnumerableReadOnly( MultilineHandler.prototype, '_xOffset', function xOffs 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 ); + readline.cursorTo( this._ostream, this._repl.promptLength() + this._rli.cursor ); // eslint-disable-line max-len }); /** @@ -172,7 +149,7 @@ setNonEnumerableReadOnly( MultilineHandler.prototype, '_moveCursor', function mo this._rli.prompt(); // Set x cursor position: - readline.cursorTo( this._ostream, this._xOffset() + x ); + readline.cursorTo( this._ostream, this._repl.promptLength() + x ); this._rli.cursor = x; }); @@ -367,6 +344,32 @@ setNonEnumerableReadOnly( MultilineHandler.prototype, '_isMultilineInput', funct return false; }); +/** +* Returns current line number in input. +* +* @private +* @name lineIndex +* @memberof MultilineHandler.prototype +* @type {Function} +* @returns {number} line index +*/ +setNonEnumerableReadOnly( MultilineHandler.prototype, 'lineIndex', function lineIndex() { + return this._lineIndex; +}); + +/** +* Returns the number of rows occupied by current input. +* +* @private +* @name inputHeight +* @memberof MultilineHandler.prototype +* @type {Function} +* @returns {number} input height +*/ +setNonEnumerableReadOnly( MultilineHandler.prototype, 'inputHeight', function inputHeight() { + return this._lines.length; +}); + /** * Updates current input line in buffer. * diff --git a/lib/node_modules/@stdlib/repl/test/integration/test.completer_engine.js b/lib/node_modules/@stdlib/repl/test/integration/test.completer_engine.js new file mode 100644 index 000000000000..4d594d3b5add --- /dev/null +++ b/lib/node_modules/@stdlib/repl/test/integration/test.completer_engine.js @@ -0,0 +1,488 @@ +/** +* @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 tape = require( 'tape' ); +var DebugStream = require( '@stdlib/streams/node/debug' ); +var contains = require( '@stdlib/assert/contains' ); +var replace = require( '@stdlib/string/replace' ); +var RE_EOL = require( '@stdlib/regexp/eol' ).REGEXP; +var repl = require( './fixtures/repl.js' ); + + +// VARIABLES // + +var RE_ANSI = /[\u001B\u009B][[\]()#;?]*(?:(?:(?:(?:;[-a-zA-Z\d/#&.:=?%@~_]+)*|[a-zA-Z\d]+(?:;[-a-zA-Z\d/#&.:=?%@~_]*)*)?\u0007)|(?:(?:\d{1,4}(?:;\d{0,4})*)?[\dA-PR-TZcf-nq-uy=><~]))/g; // eslint-disable-line no-control-regex + + +// FUNCTIONS // + +/** +* Returns default settings. +* +* @private +* @returns {Object} default settings +*/ +function defaultSettings() { + return { + 'autoDeletePairs': false, + 'autoClosePairs': false, + 'completionPreviews': false, + 'syntaxHighlighting': false, + 'autoPage': false + }; +} + +/** +* Removes ANSI escape codes from a string. +* +* @private +* @param {string} str - input string +* @returns {string} string with ANSI escape codes removed +*/ +function stripANSI( str ) { + return replace( str, RE_ANSI, '' ); +} + +/** +* Extract completions from a TAB completions output string. +* +* @private +* @param {string} str - completions output +* @returns {Array} array of completions +*/ +function extractCompletions( str ) { + var cleanOutput; + var out = []; + var i; + + cleanOutput = replace( str, RE_EOL, '' ).split( /\s+/ ); + for ( i = 0; i < cleanOutput.length; i++ ) { + if ( cleanOutput[i] !== '' ) { + out.push( cleanOutput[ i ] ); + } + } + return out; +} + + +// 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 displaying TAB completions of user-defined variables', 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 ); + + // Declare variables with unique names in order to prevent namespace collisions: + istream.write( 'var zzxyz = 1;' ); + istream.write( 'var zzabc = 2;' ); + istream.write( 'var zzpqr = 3;' ); + + // Write the common beginning of the variable names in order to generate TAB completions: + istream.write( 'zz' ); + + // Write TAB to display completions: + istream.write( '\t' ); + + // Close the input stream: + istream.end(); + + // Close the REPL: + r.close(); + + function onClose( error, data ) { + var actual; + + if ( error ) { + t.fail( error.message ); + return; + } + + actual = extractCompletions( stripANSI( data[ data.length - 3 ] ) ); + + // Check for three completions in the output: + t.strictEqual( actual.length, 3, 'returns expected value' ); + + // Check for the declared variables (sorted lexicographically) in the completions: + t.strictEqual( actual[ 0 ], 'zzabc', 'returns expected value' ); + t.strictEqual( actual[ 1 ], 'zzpqr', 'returns expected value' ); + t.strictEqual( actual[ 2 ], 'zzxyz', 'returns expected value' ); + + // Check if the cursor is returned back to the prompt: + t.strictEqual( data[ data.length - 2 ], '\x1B[2A', 'returns expected value' ); + + t.end(); + } +}); + +tape( 'a REPL instance supports hiding the completions panel upon pressing TAB again', 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 ); + + // Declare variables with unique names in order to prevent namespace collisions: + istream.write( 'var zzzxy = 1;' ); + istream.write( 'var zzzab = 2;' ); + + // Write the common substring of the variable names in order to generate completions: + istream.write( 'zzz' ); + + // Write TAB to display completions: + istream.write( '\t' ); + + // Write TAB again to hide completions: + istream.write( '\t' ); + + // Close the input stream: + istream.end(); + + // Close the REPL: + r.close(); + + function onClose( error, data ) { + if ( error ) { + t.fail( error.message ); + return; + } + + // Check if the completions were cleared: + t.strictEqual( data[ data.length - 2 ], '\x1B[0J', 'returns expected value' ); + + // NOTE: `data[ data.length-1 ]` adds the remaining string to the right of the cursor because of the abnormal behaviour of `clearScreenDown`... + + t.end(); + } +}); + +tape( 'a REPL instance supports navigating the TAB completions using arrow keys', 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 ); + + // Declare variables with unique names in order to prevent namespace collisions: + istream.write( 'var zzzab = 1;' ); + istream.write( 'var zzzxy = 2;' ); + + // Write the common substring of the variable names in order to generate completions: + istream.write( 'zzz' ); + + // Write TAB to display completions: + istream.write( '\t' ); + + // Navigate down using down arrow: + istream.write( '\u001B[B' ); + + // Navigate right to the next completion using right arrow: + istream.write( '\u001B[C' ); + + // Close the input stream: + istream.end(); + + // Close the REPL: + r.close(); + + function onClose( error, data ) { + var actual; + + if ( error ) { + t.fail( error.message ); + return; + } + + // Check for completions before navigation: + actual = extractCompletions( stripANSI( data[ data.length - 13 ] ) ); + t.strictEqual( actual.length, 2, 'returns expected value' ); + t.strictEqual( actual[ 0 ], 'zzzab', 'returns expected value' ); + t.strictEqual( actual[ 1 ], 'zzzxy', 'returns expected value' ); + t.strictEqual( data[ data.length - 12 ], '\x1B[2A', 'returns expected value' ); // bring cursor back + + // Check for completions after `down` arrow: + actual = extractCompletions( data[ data.length - 10 ] ); + + // First completion should be highlighted: + t.strictEqual( actual[ 0 ], '\x1B[7mzzzab\x1B[27m', 'returns expected value' ); + t.strictEqual( data[ data.length - 9 ], '\x1B[2A', 'returns expected value' ); // bring cursor back + + // Current line is cleared and the first completion is inserted: + t.strictEqual( data[ data.length - 7 ], '\x1B[0K', 'returns expected value' ); + t.strictEqual( data[ data.length - 6 ], 'var zzzab = 1;var zzzxy = 2;zzzab', 'returns expected value' ); + + // Check for completions after `right` arrow key: + actual = extractCompletions( data[ data.length - 5 ] ); + + // Second completion should be highlighted: + t.strictEqual( actual[ 1 ], '\x1B[7mzzzxy\x1B[27m', 'returns expected value' ); + t.strictEqual( data[ data.length - 4 ], '\x1B[2A', 'returns expected value' ); // bring cursor back + + // Current line is cleared and the second completion is inserted: + t.strictEqual( data[ data.length - 2 ], '\x1B[0K', 'returns expected value' ); + t.strictEqual( data[ data.length - 1 ], 'var zzzab = 1;var zzzxy = 2;zzzxy', 'returns expected value' ); + + t.end(); + } +}); + +tape( 'a REPL instance supports bringing back the original line upon navigating back up from the TAB completions', 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 ); + + // Declare variables with unique names in order to prevent namespace collisions: + istream.write( 'var zzabc = 1;' ); + istream.write( 'var zzpqr = 2;' ); + + // Write the common beginning of the variable names in order to generate TAB completions: + istream.write( 'zz' ); + + // Write TAB to display completions: + istream.write( '\t' ); + + // Navigate down using down arrow: + istream.write('\u001B[B'); + + // Navigate up towards the line to bring back the original line: + istream.write('\u001B[A'); + + // Close the input stream: + istream.end(); + + // Close the REPL: + r.close(); + + function onClose( error, data ) { + if ( error ) { + t.fail( error.message ); + return; + } + + // Current line is cleared and the original line is inserted: + t.strictEqual( data[ data.length-2 ], '\x1B[0K', 'returns expected value' ); + t.strictEqual( data[ data.length-1 ], 'var zzabc = 1;var zzpqr = 2;zz', 'returns expected value' ); + + t.end(); + } +}); + +tape( 'a REPL instance supports displaying highlighted argument TAB completions', 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 ); + + // Write the common beginning of the settings in order to generate TAB completions: + istream.write( 'settings(\'auto' ); + + // Write TAB to display completions: + istream.write( '\t' ); + + // Close the input stream: + istream.end(); + + // Close the REPL: + r.close(); + + function onClose( error, data ) { + var actual; + + if ( error ) { + t.fail( error.message ); + return; + } + + // Check for settings name completions in the output: + actual = extractCompletions( data[ data.length - 3 ] ); + t.ok( contains( actual, '\x1B[1mauto\x1B[0mClosePairs' ), 'returns expected value' ); + t.ok( contains( actual, '\x1B[1mauto\x1B[0mDeletePairs' ), 'returns expected value' ); + t.ok( contains( actual, '\x1B[1mauto\x1B[0mPage' ), 'returns expected value' ); + + // Check if the cursor is returned back to the prompt: + t.strictEqual( data[ data.length - 2 ], '\x1B[2A', 'returns expected value' ); + + t.end(); + } +}); + +tape( 'a REPL instance supports auto-completing common prefixes when hitting TAB', 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 ); + + // Declare variables with `zzzz` as an exact prefix: + istream.write( 'var zzzz = 1;' ); + istream.write( 'var zzzzabc = 2;' ); + istream.write( 'var zzzzpqr = 3;' ); + + // Partially write the common beginning of the variable names in order to generate a TAB auto-completion: + istream.write( 'zz' ); + + // Write TAB to trigger auto-completion: + istream.write( '\t' ); + + // Close the input stream: + istream.end(); + + // Close the REPL: + r.close(); + + function onClose( error, data ) { + if ( error ) { + t.fail( error.message ); + return; + } + + // Check if the completion prefix was cleared: + t.strictEqual( data[ data.length-2 ], '\b\b', 'returns expected value' ); + + // Check if the final completion was auto-inserted: + t.strictEqual( data[ data.length-1 ], 'zzzz', 'returns expected value' ); + + t.end(); + } +}); + +tape( 'a REPL instance supports auto-completing common argument prefixes when hitting TAB', 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 ); + + // Write the arguement with just one possible completion: + istream.write( 'settings(\'autoPa' ); + + // Write TAB to trigger auto completion: + istream.write( '\t' ); + + // Close the input stream: + istream.end(); + + // Close the REPL: + r.close(); + + function onClose( error, data ) { + if ( error ) { + t.fail( error.message ); + return; + } + + // Check if the completion prefix (`autoPa`) was cleared: + t.strictEqual( data[ data.length-2 ], '\b\b\b\b\b\b', 'returns expected value' ); + + // Check if the final completion was auto-inserted: + t.strictEqual( data[ data.length-1 ], 'autoPage', 'returns expected value' ); + + t.end(); + } +}); From 1d6ad407fc3569e82721c2b91c1437e0b69e2b22 Mon Sep 17 00:00:00 2001 From: Athan Date: Wed, 26 Jun 2024 16:41:54 -0700 Subject: [PATCH 02/27] Apply suggestions from code review Signed-off-by: Athan --- .../@stdlib/repl/lib/completer_engine.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/node_modules/@stdlib/repl/lib/completer_engine.js b/lib/node_modules/@stdlib/repl/lib/completer_engine.js index e0896c589475..ac75c5756b46 100644 --- a/lib/node_modules/@stdlib/repl/lib/completer_engine.js +++ b/lib/node_modules/@stdlib/repl/lib/completer_engine.js @@ -184,7 +184,7 @@ setNonEnumerableReadOnly( CompleterEngine.prototype, '_completionCallback', func // Resolve a common prefix from the completion results: autoCompletion = commonPrefix( self._completionsList ); // e.g., [ 'back', 'background', 'backward' ] => 'back' - // If the completion candidates have a possible auto-completion (ie. a common prefix longer than the input), auto-complete it... + // If the completion candidates have a possible auto-completion (i.e., a common prefix longer than the input), auto-complete it... if ( autoCompletion !== '' && autoCompletion.length > self._completionPrefix.length ) { debug( 'Found an auto-completion candidate: %s', autoCompletion ); @@ -206,7 +206,7 @@ setNonEnumerableReadOnly( CompleterEngine.prototype, '_completionCallback', func // Check if completions can be displayed... if ( !self._isDisplayable() ) { - debug( 'TTY height too short. exiting completer...' ); + debug( 'TTY height too short. Unable to display completions. Exiting completion mode...' ); self._rli.resume(); return; } @@ -406,7 +406,7 @@ setNonEnumerableReadOnly( CompleterEngine.prototype, '_completionRows', function }); /** -* Checks whether content having a specified number of lines is unable to fit within the current viewport. +* Checks whether content having a specified number of lines is able to fit within the current viewport. * * @private * @name _isDisplayable @@ -620,17 +620,17 @@ setNonEnumerableReadOnly( CompleterEngine.prototype, 'beforeKeypress', function // If user is already viewing completions, allow navigating it... if ( this._isNavigating ) { switch ( key.name ) { - // If user presses TAB while navigating, toggle close the completer: + // If user presses TAB while navigating, close the completer: case 'tab': debug( 'Received a TAB keypress event. Closing the completer engine...' ); this._closeCompleter(); - // NOTE: `clearScreenDown` seems to behave abnormally in this case by also clearing the `ostream` on this right of the cursor. Hence writing it again below: + // NOTE: `clearScreenDown` seems to behave abnormally in this case by also clearing the `ostream` to the right of the cursor. Hence writing it again below: this._ostream.write( this._remainingLine ); readline.moveCursor( this._ostream, -1 * this._remainingLine.length ); // eslint-disable-line max-len break; - // If arrow keys detected, allow navigating the completions, else stop navigating and continue default behavior... + // If arrow keys detected, allow navigating the completions... case 'down': debug( 'Received a DOWN keypress event...' ); this._navigateDown(); From e8e0e9fa3daa537ec080fbfdcc1e1df4c68c0420 Mon Sep 17 00:00:00 2001 From: Snehil Shah Date: Thu, 27 Jun 2024 13:03:25 +0530 Subject: [PATCH 03/27] Apply suggestions from code review Co-authored-by: Athan Signed-off-by: Snehil Shah --- lib/node_modules/@stdlib/repl/lib/completer_engine.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/node_modules/@stdlib/repl/lib/completer_engine.js b/lib/node_modules/@stdlib/repl/lib/completer_engine.js index ac75c5756b46..4a65321c1d34 100644 --- a/lib/node_modules/@stdlib/repl/lib/completer_engine.js +++ b/lib/node_modules/@stdlib/repl/lib/completer_engine.js @@ -421,7 +421,7 @@ setNonEnumerableReadOnly( CompleterEngine.prototype, '_isDisplayable', function }); /** -* Closes completer engine. +* Closes the completer engine. * * @private * @name _closeCompleter From 535986f39f085f8ed41da5e90e42f378000d2fa6 Mon Sep 17 00:00:00 2001 From: Snehil Shah Date: Fri, 28 Jun 2024 20:47:24 +0000 Subject: [PATCH 04/27] fix: avoid navigating hidden completions Signed-off-by: Snehil Shah --- lib/node_modules/@stdlib/repl/lib/completer_engine.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/node_modules/@stdlib/repl/lib/completer_engine.js b/lib/node_modules/@stdlib/repl/lib/completer_engine.js index 4a65321c1d34..b589166061d3 100644 --- a/lib/node_modules/@stdlib/repl/lib/completer_engine.js +++ b/lib/node_modules/@stdlib/repl/lib/completer_engine.js @@ -536,6 +536,9 @@ setNonEnumerableReadOnly( CompleterEngine.prototype, '_navigateLeft', function n * @returns {void} */ setNonEnumerableReadOnly( CompleterEngine.prototype, '_navigateRight', function navigateRight( data, key ) { + var columns = this._completionColumns(); + var rows = this._completionRows( columns ); + // If on current line, trigger default behavior and stop navigating... if ( this._idx === -1 ) { this._closeCompleter(); @@ -543,7 +546,7 @@ setNonEnumerableReadOnly( CompleterEngine.prototype, '_navigateRight', function return; } // If navigating, move ahead an index... - if ( this._idx < this._completionsList.length - 1 ) { + if ( this._idx + 1 < rows * columns ) { this._idx += 1; this.updateCompletions(); } From 4babb9848452545f4df58eda0f682a495fcdae18 Mon Sep 17 00:00:00 2001 From: Snehil Shah Date: Fri, 28 Jun 2024 20:58:54 +0000 Subject: [PATCH 05/27] refactor: use `\n` instead of `\r\n` Signed-off-by: Snehil Shah --- lib/node_modules/@stdlib/repl/lib/completer_engine.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/node_modules/@stdlib/repl/lib/completer_engine.js b/lib/node_modules/@stdlib/repl/lib/completer_engine.js index b589166061d3..00bacd6d9e52 100644 --- a/lib/node_modules/@stdlib/repl/lib/completer_engine.js +++ b/lib/node_modules/@stdlib/repl/lib/completer_engine.js @@ -284,7 +284,7 @@ setNonEnumerableReadOnly( CompleterEngine.prototype, '_drawOutput', function dra var i; // Draw the output grid: - output = '\r\n'; + output = '\n'; lineIndex = 0; whitespaces = 0; for ( i = 0; i < this._highlightedCompletions.length; i++ ) { @@ -296,7 +296,7 @@ setNonEnumerableReadOnly( CompleterEngine.prototype, '_drawOutput', function dra } if ( lineIndex >= columns ) { // Reached end of column, enter next line: - output += '\r\n'; + output += '\n'; lineIndex = 0; whitespaces = 0; } else { @@ -313,7 +313,7 @@ setNonEnumerableReadOnly( CompleterEngine.prototype, '_drawOutput', function dra whitespaces = this._widthOfColumn - this._completionsLength[ i ]; lineIndex += 1; } - output += '\r\n'; + output += '\n'; return output; }); From 755ddee35ce9aa3289e25120ccf807feeb34950c Mon Sep 17 00:00:00 2001 From: Snehil Shah Date: Fri, 28 Jun 2024 21:55:35 +0000 Subject: [PATCH 06/27] fix: abnormal behavior with ENTER Signed-off-by: Snehil Shah --- .../@stdlib/repl/lib/completer_engine.js | 102 ++++++++++-------- 1 file changed, 58 insertions(+), 44 deletions(-) diff --git a/lib/node_modules/@stdlib/repl/lib/completer_engine.js b/lib/node_modules/@stdlib/repl/lib/completer_engine.js index 00bacd6d9e52..167a5d8ae670 100644 --- a/lib/node_modules/@stdlib/repl/lib/completer_engine.js +++ b/lib/node_modules/@stdlib/repl/lib/completer_engine.js @@ -265,6 +265,44 @@ setNonEnumerableReadOnly( CompleterEngine.prototype, '_displayCompletions', func readline.cursorTo( this._ostream, this._rli.cursor + this._repl.promptLength() ); // eslint-disable-line max-len }); +/** +* Re-displays the navigated completions to the output stream. +* +* @name _updateCompletions +* @memberof CompleterEngine.prototype +* @type {Function} +* @returns {void} +*/ +setNonEnumerableReadOnly( CompleterEngine.prototype, '_updateCompletions', function updateCompletions() { + var columns; + var rows; + var dy; + var cu; + + // Determine dimensions of the output grid: + columns = this._completionColumns(); + rows = this._completionRows( columns ); + + // Move cursor to the output row: + dy = this._multiline.inputHeight() - this._multiline.lineIndex() - 1; + readline.moveCursor( this._ostream, 0, dy ); + + // Write completions to the output stream: + this._ostream.write( this._drawOutput( rows, columns ) ); + + // Bring the cursor back to the current line: + readline.moveCursor( this._ostream, 0, -1 * ( rows + this._multiline.inputHeight() - this._multiline.lineIndex() ) ); // eslint-disable-line max-len + readline.cursorTo( this._ostream, this._repl.promptLength() ); + this._rli.cursor = 0; + + // Insert the navigated suggestion to the current line: + readline.clearLine( this._ostream, 1 ); + this._rli.line = removeLast( this._inputLine, this._completionPrefix.length ) + ( this._completionsList[ this._idx ] || this._completionPrefix ) + this._remainingLine; // eslint-disable-line max-len + cu = this._rli.line.length - this._remainingLine.length; + this._rli.cursor = cu; + readline.moveCursor( this._ostream, cu ); +}); + /** * Draws the completions output grid. * @@ -473,7 +511,7 @@ setNonEnumerableReadOnly( CompleterEngine.prototype, '_navigateUp', function nav } else { this._idx = -1; } - this.updateCompletions(); + this._updateCompletions(); }); /** @@ -492,10 +530,10 @@ setNonEnumerableReadOnly( CompleterEngine.prototype, '_navigateDown', function n // Move to the next row... if ( this._idx === -1 ) { this._idx = 0; - this.updateCompletions(); + this._updateCompletions(); } else if ( this._idx + columns < rows * columns ) { this._idx += columns; - this.updateCompletions(); + this._updateCompletions(); } }); @@ -520,7 +558,7 @@ setNonEnumerableReadOnly( CompleterEngine.prototype, '_navigateLeft', function n // If navigating, move back an index... if ( this._idx > 0 ) { this._idx -= 1; - this.updateCompletions(); + this._updateCompletions(); } }); @@ -546,49 +584,12 @@ setNonEnumerableReadOnly( CompleterEngine.prototype, '_navigateRight', function return; } // If navigating, move ahead an index... - if ( this._idx + 1 < rows * columns ) { + if ( this._idx + 1 < min( rows * columns, this._completionsList.length ) ) { this._idx += 1; - this.updateCompletions(); + this._updateCompletions(); } }); -/** -* Re-displays the navigated completions to the output stream. -* -* @name updateCompletions -* @memberof CompleterEngine.prototype -* @type {Function} -* @returns {void} -*/ -setNonEnumerableReadOnly( CompleterEngine.prototype, 'updateCompletions', function updateCompletions() { - var columns; - var rows; - var dy; - - // Determine dimensions of the output grid: - columns = this._completionColumns(); - rows = this._completionRows( columns ); - - // Move cursor to the output row: - dy = this._multiline.inputHeight() - this._multiline.lineIndex() - 1; - readline.moveCursor( this._ostream, 0, dy ); - - // Write completions to the output stream: - this._ostream.write( this._drawOutput( rows, columns ) ); - - // Bring the cursor back to the current line: - readline.moveCursor( this._ostream, 0, -1 * ( rows + this._multiline.inputHeight() - this._multiline.lineIndex() ) ); // eslint-disable-line max-len - readline.cursorTo( this._ostream, this._repl.promptLength() ); - this._rli.cursor = 0; - - // Insert the navigated suggestion to the current line: - readline.clearLine( this._ostream, 1 ); - this._rli.line = ''; - this._rli.write( removeLast( this._inputLine, this._completionPrefix.length ) + ( this._completionsList[ this._idx ] || this._completionPrefix ) + this._remainingLine ); // eslint-disable-line max-len - readline.moveCursor( this._ostream, -1 * this._remainingLine.length ); - this._rli.cursor -= this._remainingLine.length; -}); - /** * Checks whether the user is currently navigating completions. * @@ -633,6 +634,19 @@ setNonEnumerableReadOnly( CompleterEngine.prototype, 'beforeKeypress', function readline.moveCursor( this._ostream, -1 * this._remainingLine.length ); // eslint-disable-line max-len break; + // If user presses ENTER while navigating, close the completer and simulate `line` event: + case 'return': + debug( 'Received an ENTER keypress event. Closing the completer engine...' ); + this._closeCompleter(); + + // NOTE: `clearScreenDown` seems to behave abnormally in this case by also clearing the `ostream` to the right of the cursor. Hence writing it again below: + this._ostream.write( this._remainingLine ); + readline.moveCursor( this._ostream, -1 * this._remainingLine.length ); // eslint-disable-line max-len + + // Simulate `line` event + this._rli.write( '\n' ); + break; + // If arrow keys detected, allow navigating the completions... case 'down': debug( 'Received a DOWN keypress event...' ); @@ -687,7 +701,7 @@ setNonEnumerableReadOnly( CompleterEngine.prototype, 'onResize', function onResi this._closeCompleter(); return; } - this.updateCompletions(); + this._updateCompletions(); }); From 2fcb35623ae1ee24a0b68ce34516f7d63393a236 Mon Sep 17 00:00:00 2001 From: Snehil Shah Date: Fri, 28 Jun 2024 22:21:09 +0000 Subject: [PATCH 07/27] fix: duplicate line events Signed-off-by: Snehil Shah --- .../@stdlib/repl/lib/completer_engine.js | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/lib/node_modules/@stdlib/repl/lib/completer_engine.js b/lib/node_modules/@stdlib/repl/lib/completer_engine.js index 167a5d8ae670..9eb4c5c915e0 100644 --- a/lib/node_modules/@stdlib/repl/lib/completer_engine.js +++ b/lib/node_modules/@stdlib/repl/lib/completer_engine.js @@ -634,19 +634,6 @@ setNonEnumerableReadOnly( CompleterEngine.prototype, 'beforeKeypress', function readline.moveCursor( this._ostream, -1 * this._remainingLine.length ); // eslint-disable-line max-len break; - // If user presses ENTER while navigating, close the completer and simulate `line` event: - case 'return': - debug( 'Received an ENTER keypress event. Closing the completer engine...' ); - this._closeCompleter(); - - // NOTE: `clearScreenDown` seems to behave abnormally in this case by also clearing the `ostream` to the right of the cursor. Hence writing it again below: - this._ostream.write( this._remainingLine ); - readline.moveCursor( this._ostream, -1 * this._remainingLine.length ); // eslint-disable-line max-len - - // Simulate `line` event - this._rli.write( '\n' ); - break; - // If arrow keys detected, allow navigating the completions... case 'down': debug( 'Received a DOWN keypress event...' ); From 9de549d9e12996d6c6297cdf6511af61707ef876 Mon Sep 17 00:00:00 2001 From: Snehil Shah Date: Fri, 28 Jun 2024 23:01:43 +0000 Subject: [PATCH 08/27] fix: incorrect height Signed-off-by: Snehil Shah --- lib/node_modules/@stdlib/repl/lib/multiline_handler.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/node_modules/@stdlib/repl/lib/multiline_handler.js b/lib/node_modules/@stdlib/repl/lib/multiline_handler.js index 3b5ac57407a0..817704db71fd 100644 --- a/lib/node_modules/@stdlib/repl/lib/multiline_handler.js +++ b/lib/node_modules/@stdlib/repl/lib/multiline_handler.js @@ -29,6 +29,7 @@ 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 max = require( '@stdlib/math/base/special/max' ); var displayPrompt = require( './display_prompt.js' ); var drain = require( './drain.js' ); var multilinePlugin = require( './acorn_detect_multiline_input.js' ); @@ -367,7 +368,7 @@ setNonEnumerableReadOnly( MultilineHandler.prototype, 'lineIndex', function line * @returns {number} input height */ setNonEnumerableReadOnly( MultilineHandler.prototype, 'inputHeight', function inputHeight() { - return this._lines.length; + return max( this._lines.length, 1 ); }); /** From 2b1f666a39b3db6b4f68f20ddec037f338b72b37 Mon Sep 17 00:00:00 2001 From: Snehil Shah Date: Fri, 28 Jun 2024 23:20:57 +0000 Subject: [PATCH 09/27] fix: bug with ENTER Signed-off-by: Snehil Shah --- .../@stdlib/repl/lib/completer_engine.js | 83 +++++++++---------- lib/node_modules/@stdlib/repl/lib/main.js | 6 +- 2 files changed, 46 insertions(+), 43 deletions(-) diff --git a/lib/node_modules/@stdlib/repl/lib/completer_engine.js b/lib/node_modules/@stdlib/repl/lib/completer_engine.js index 9eb4c5c915e0..5ad2179e0891 100644 --- a/lib/node_modules/@stdlib/repl/lib/completer_engine.js +++ b/lib/node_modules/@stdlib/repl/lib/completer_engine.js @@ -277,7 +277,6 @@ setNonEnumerableReadOnly( CompleterEngine.prototype, '_updateCompletions', funct var columns; var rows; var dy; - var cu; // Determine dimensions of the output grid: columns = this._completionColumns(); @@ -297,10 +296,10 @@ setNonEnumerableReadOnly( CompleterEngine.prototype, '_updateCompletions', funct // Insert the navigated suggestion to the current line: readline.clearLine( this._ostream, 1 ); - this._rli.line = removeLast( this._inputLine, this._completionPrefix.length ) + ( this._completionsList[ this._idx ] || this._completionPrefix ) + this._remainingLine; // eslint-disable-line max-len - cu = this._rli.line.length - this._remainingLine.length; - this._rli.cursor = cu; - readline.moveCursor( this._ostream, cu ); + this._rli.line = ''; + this._rli.write( removeLast( this._inputLine, this._completionPrefix.length ) + ( this._completionsList[ this._idx ] || this._completionPrefix ) + this._remainingLine ); // eslint-disable-line max-len + readline.moveCursor( this._ostream, -1 * this._remainingLine.length ); + this._rli.cursor -= this._remainingLine.length; }); /** @@ -458,33 +457,6 @@ setNonEnumerableReadOnly( CompleterEngine.prototype, '_isDisplayable', function return vh > ih + RESERVED_COMPLETER_ROWS; }); -/** -* Closes the completer engine. -* -* @private -* @name _closeCompleter -* @memberof CompleterEngine.prototype -* @type {Function} -* @returns {void} -*/ -setNonEnumerableReadOnly( CompleterEngine.prototype, '_closeCompleter', function closeCompleter() { - // Reset completer parameters: - this._isNavigating = false; - this._idx = -1; - - // Clear completions output: - readline.clearScreenDown( this._ostream ); - - // Reset the internal completer buffers: - this._inputLine = ''; - this._remainingLine = ''; - this._completionPrefix = ''; - this._completionsList = []; - this._highlightedCompletions = []; - this._completionsLength = []; - this._widthOfColumn = -1; -}); - /** * Navigate up the completions grid. * @@ -501,7 +473,7 @@ setNonEnumerableReadOnly( CompleterEngine.prototype, '_navigateUp', function nav // If already on the line, close the completer... if ( this._idx === -1 ) { - this._closeCompleter(); + this.closeCompleter(); this._ttyWrite.call( this._rli, data, key ); return; } @@ -551,7 +523,7 @@ setNonEnumerableReadOnly( CompleterEngine.prototype, '_navigateDown', function n setNonEnumerableReadOnly( CompleterEngine.prototype, '_navigateLeft', function navigateLeft( data, key ) { // If on current line, trigger default behavior and stop navigating... if ( this._idx === -1 ) { - this._closeCompleter(); + this.closeCompleter(); this._ttyWrite.call( this._rli, data, key ); return; } @@ -579,7 +551,7 @@ setNonEnumerableReadOnly( CompleterEngine.prototype, '_navigateRight', function // If on current line, trigger default behavior and stop navigating... if ( this._idx === -1 ) { - this._closeCompleter(); + this.closeCompleter(); this._ttyWrite.call( this._rli, data, key ); return; } @@ -602,6 +574,37 @@ setNonEnumerableReadOnly( CompleterEngine.prototype, 'isNavigating', function is return this._isNavigating; }); +/** +* Closes the completer engine. +* +* @private +* @name closeCompleter +* @memberof CompleterEngine.prototype +* @type {Function} +* @returns {void} +*/ +setNonEnumerableReadOnly( CompleterEngine.prototype, 'closeCompleter', function closeCompleter() { + // Reset completer parameters: + this._isNavigating = false; + this._idx = -1; + + // Clear completions output: + readline.clearScreenDown( this._ostream ); + + // NOTE: `clearScreenDown` seems to behave abnormally in this case by also clearing the `ostream` to the right of the cursor. Hence writing it again below: + this._ostream.write( this._remainingLine ); + readline.moveCursor( this._ostream, -1 * this._remainingLine.length ); + + // Reset the internal completer buffers: + this._inputLine = ''; + this._remainingLine = ''; + this._completionPrefix = ''; + this._completionsList = []; + this._highlightedCompletions = []; + this._completionsLength = []; + this._widthOfColumn = -1; +}); + /** * Callback which should be invoked **before** a "keypress" event is processed by a readline interface. * @@ -627,11 +630,7 @@ setNonEnumerableReadOnly( CompleterEngine.prototype, 'beforeKeypress', function // If user presses TAB while navigating, close the completer: case 'tab': debug( 'Received a TAB keypress event. Closing the completer engine...' ); - this._closeCompleter(); - - // NOTE: `clearScreenDown` seems to behave abnormally in this case by also clearing the `ostream` to the right of the cursor. Hence writing it again below: - this._ostream.write( this._remainingLine ); - readline.moveCursor( this._ostream, -1 * this._remainingLine.length ); // eslint-disable-line max-len + this.closeCompleter(); break; // If arrow keys detected, allow navigating the completions... @@ -652,7 +651,7 @@ setNonEnumerableReadOnly( CompleterEngine.prototype, 'beforeKeypress', function this._navigateRight( data, key ); break; default: - this._closeCompleter(); + this.closeCompleter(); this._ttyWrite.call( this._rli, data, key ); } return; @@ -685,7 +684,7 @@ setNonEnumerableReadOnly( CompleterEngine.prototype, 'onResize', function onResi return; } if ( !this._isDisplayable() ) { - this._closeCompleter(); + this.closeCompleter(); return; } this._updateCompletions(); diff --git a/lib/node_modules/@stdlib/repl/lib/main.js b/lib/node_modules/@stdlib/repl/lib/main.js index a850007e6eb2..8a5bd3af7a2f 100644 --- a/lib/node_modules/@stdlib/repl/lib/main.js +++ b/lib/node_modules/@stdlib/repl/lib/main.js @@ -345,7 +345,11 @@ function REPL( options ) { return; } self._autoCloser.beforeKeypress( data, key ); - if ( ( key && key.name === 'tab' ) || self._completerEngine.isNavigating() ) { + + // If ENTER keypress is encountered while navigating completions, gracefully close the completer... + if ( self._completerEngine.isNavigating() && ( key && key.name === 'return' ) ) { + self._completerEngine.closeCompleter(); + } else if ( ( key && key.name === 'tab' ) || self._completerEngine.isNavigating() ) { self._completerEngine.beforeKeypress( data, key ); return; } From 7510ba9feca397edbe7b3993b68bdcb41ab4f2e4 Mon Sep 17 00:00:00 2001 From: Snehil Shah Date: Fri, 28 Jun 2024 23:22:46 +0000 Subject: [PATCH 10/27] test: add test case Signed-off-by: Snehil Shah --- .../test/integration/test.completer_engine.js | 53 ++++++++++++++++++- 1 file changed, 52 insertions(+), 1 deletion(-) diff --git a/lib/node_modules/@stdlib/repl/test/integration/test.completer_engine.js b/lib/node_modules/@stdlib/repl/test/integration/test.completer_engine.js index 4d594d3b5add..2ddf64c9ca04 100644 --- a/lib/node_modules/@stdlib/repl/test/integration/test.completer_engine.js +++ b/lib/node_modules/@stdlib/repl/test/integration/test.completer_engine.js @@ -194,7 +194,6 @@ tape( 'a REPL instance supports hiding the completions panel upon pressing TAB a t.fail( error.message ); return; } - // Check if the completions were cleared: t.strictEqual( data[ data.length - 2 ], '\x1B[0J', 'returns expected value' ); @@ -204,6 +203,58 @@ tape( 'a REPL instance supports hiding the completions panel upon pressing TAB a } }); +tape( 'a REPL instance supports hiding the completions panel upon typing again', 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 ); + + // Declare variables with unique names in order to prevent namespace collisions: + istream.write( 'var zzzxy = 1;' ); + istream.write( 'var zzzab = 2;' ); + + // Write the common substring of the variable names in order to generate completions: + istream.write( 'zzz' ); + + // Write TAB to display completions: + istream.write( '\t' ); + + // Write TAB again to hide completions: + istream.write( 'a' ); + + // Close the input stream: + istream.end(); + + // Close the REPL: + r.close(); + + function onClose( error, data ) { + if ( error ) { + t.fail( error.message ); + return; + } + // Check if the completions were cleared: + t.strictEqual( data[ data.length - 3 ], '\x1B[0J', 'returns expected value' ); + t.strictEqual( data[ data.length - 1 ], 'a', 'returns expected value' ); + + // NOTE: `data[ data.length-2 ]` adds the remaining string to the right of the cursor because of the abnormal behaviour of `clearScreenDown`... + + t.end(); + } +}); + tape( 'a REPL instance supports navigating the TAB completions using arrow keys', function test( t ) { var istream; var opts; From da8f7ee3ac35cf0a9737c7ef0a0fc9da0051762c Mon Sep 17 00:00:00 2001 From: Snehil Shah Date: Fri, 28 Jun 2024 23:36:47 +0000 Subject: [PATCH 11/27] test: fix brittle test cases Signed-off-by: Snehil Shah --- .../test/integration/test.completer_engine.js | 26 ++++++++++++------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/lib/node_modules/@stdlib/repl/test/integration/test.completer_engine.js b/lib/node_modules/@stdlib/repl/test/integration/test.completer_engine.js index 2ddf64c9ca04..c6a59928c9a2 100644 --- a/lib/node_modules/@stdlib/repl/test/integration/test.completer_engine.js +++ b/lib/node_modules/@stdlib/repl/test/integration/test.completer_engine.js @@ -408,8 +408,11 @@ tape( 'a REPL instance supports displaying highlighted argument TAB completions' }; r = repl( opts, onClose ); - // Write the common beginning of the settings in order to generate TAB completions: - istream.write( 'settings(\'auto' ); + // Declare an object with properties having a common prefix: + istream.write( 'var a = { "zzzxy": 4, "zzzab": 6 };\n' ); + + // Write the common beginning of the property names in order to generate TAB completions: + istream.write( 'a.zzz' ); // Write TAB to display completions: istream.write( '\t' ); @@ -430,9 +433,9 @@ tape( 'a REPL instance supports displaying highlighted argument TAB completions' // Check for settings name completions in the output: actual = extractCompletions( data[ data.length - 3 ] ); - t.ok( contains( actual, '\x1B[1mauto\x1B[0mClosePairs' ), 'returns expected value' ); - t.ok( contains( actual, '\x1B[1mauto\x1B[0mDeletePairs' ), 'returns expected value' ); - t.ok( contains( actual, '\x1B[1mauto\x1B[0mPage' ), 'returns expected value' ); + t.strictEqual( actual.length, 2, 'returns expected value' ); + t.ok( contains( actual, '\x1B[1mzzz\x1B[0mab' ), 'returns expected value' ); + t.ok( contains( actual, '\x1B[1mzzz\x1B[0mxy' ), 'returns expected value' ); // Check if the cursor is returned back to the prompt: t.strictEqual( data[ data.length - 2 ], '\x1B[2A', 'returns expected value' ); @@ -510,8 +513,11 @@ tape( 'a REPL instance supports auto-completing common argument prefixes when hi }; r = repl( opts, onClose ); - // Write the arguement with just one possible completion: - istream.write( 'settings(\'autoPa' ); + // Declare an object with a unique property: + istream.write( 'var a = { "zzzabc": 6 };\n' ); + + // Write the common beginning of the property names in order to generate TAB completions: + istream.write( 'a.zzz' ); // Write TAB to trigger auto completion: istream.write( '\t' ); @@ -528,11 +534,11 @@ tape( 'a REPL instance supports auto-completing common argument prefixes when hi return; } - // Check if the completion prefix (`autoPa`) was cleared: - t.strictEqual( data[ data.length-2 ], '\b\b\b\b\b\b', 'returns expected value' ); + // Check if the completion prefix (`zzz`) was cleared: + t.strictEqual( data[ data.length-2 ], '\b\b\b', 'returns expected value' ); // Check if the final completion was auto-inserted: - t.strictEqual( data[ data.length-1 ], 'autoPage', 'returns expected value' ); + t.strictEqual( data[ data.length-1 ], 'zzzabc', 'returns expected value' ); t.end(); } From 63ce461ddbc2a5a987af13873928e63063377568 Mon Sep 17 00:00:00 2001 From: Snehil Shah Date: Sat, 29 Jun 2024 10:43:58 +0000 Subject: [PATCH 12/27] feat: allow sustaining the completions panel Signed-off-by: Snehil Shah --- .../@stdlib/repl/lib/completer_engine.js | 53 +++++++++++++++++-- lib/node_modules/@stdlib/repl/lib/main.js | 1 + 2 files changed, 51 insertions(+), 3 deletions(-) diff --git a/lib/node_modules/@stdlib/repl/lib/completer_engine.js b/lib/node_modules/@stdlib/repl/lib/completer_engine.js index 5ad2179e0891..879ddc9ad0c0 100644 --- a/lib/node_modules/@stdlib/repl/lib/completer_engine.js +++ b/lib/node_modules/@stdlib/repl/lib/completer_engine.js @@ -188,6 +188,11 @@ setNonEnumerableReadOnly( CompleterEngine.prototype, '_completionCallback', func if ( autoCompletion !== '' && autoCompletion.length > self._completionPrefix.length ) { debug( 'Found an auto-completion candidate: %s', autoCompletion ); + // If we were already navigating, don't auto-complete it as it's not a TAB trigger... + if ( self._isNavigating ) { + self._rli.resume(); + return; + } // Clear the completion prefix: self._ostream.write( repeat( '\x08', self._completionPrefix.length ) ); self._rli.line = self._rli.line.slice( 0, self._rli.cursor - self._completionPrefix.length ) + self._rli.line.slice( self._rli.cursor ); // eslint-disable-line max-len @@ -651,7 +656,6 @@ setNonEnumerableReadOnly( CompleterEngine.prototype, 'beforeKeypress', function this._navigateRight( data, key ); break; default: - this.closeCompleter(); this._ttyWrite.call( this._rli, data, key ); } return; @@ -661,10 +665,53 @@ setNonEnumerableReadOnly( CompleterEngine.prototype, 'beforeKeypress', function line = this._rli.line; // Get the line before the cursor: - this._inputLine = line.slice(0, cursor); + this._inputLine = line.slice( 0, cursor ); + + // Get the line after the cursor: + this._remainingLine = line.slice( cursor ); + + // Pause the input stream before generating completions as the completer may be asynchronous... + this._rli.pause(); + this._completer( this._inputLine, this._onCompletions ); +}); + +/** +* Callback for handling a "keypress" event. +* +* @name onKeypress +* @memberof CompleterEngine.prototype +* @type {Function} +* @param {string} data - input data +* @param {(Object|void)} key - key object +* @returns {void} +*/ +setNonEnumerableReadOnly( CompleterEngine.prototype, 'onKeypress', function onKeypress( data, key ) { + var cursor; + var line; + + // If user is not navigating completions, don't sustain completions... + if ( !this._isNavigating ) { + return; + } + // If user is trying to navigate, don't update completions based on the updated line... + if ( key && contains( [ 'left', 'right', 'up', 'down' ], key.name ) ) { + return; + } + // Clear invalid cache: + this.closeCompleter(); + + // Sustain the completer: + this._isNavigating = true; + + // Re-trigger completions based on updated line: + cursor = this._rli.cursor; + line = this._rli.line; + + // Get the line before the cursor: + this._inputLine = line.slice( 0, cursor ); // Get the line after the cursor: - this._remainingLine = line.slice(cursor); + this._remainingLine = line.slice( cursor ); // Pause the input stream before generating completions as the completer may be asynchronous... this._rli.pause(); diff --git a/lib/node_modules/@stdlib/repl/lib/main.js b/lib/node_modules/@stdlib/repl/lib/main.js index 8a5bd3af7a2f..1e3c0a834e4b 100644 --- a/lib/node_modules/@stdlib/repl/lib/main.js +++ b/lib/node_modules/@stdlib/repl/lib/main.js @@ -382,6 +382,7 @@ function REPL( options ) { } self._multilineHandler.onKeypress( data, key ); self._syntaxHighlighter.onKeypress(); + self._completerEngine.onKeypress( data, key ); self._previewCompleter.onKeypress( data, key ); } From d4afb813a074c9f139645b4ac613bec71e0ce3cd Mon Sep 17 00:00:00 2001 From: Snehil Shah Date: Sat, 29 Jun 2024 10:48:42 +0000 Subject: [PATCH 13/27] fix: reorder keypress handlers according to precedence Signed-off-by: Snehil Shah --- lib/node_modules/@stdlib/repl/lib/main.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/node_modules/@stdlib/repl/lib/main.js b/lib/node_modules/@stdlib/repl/lib/main.js index 1e3c0a834e4b..86a40161cf0c 100644 --- a/lib/node_modules/@stdlib/repl/lib/main.js +++ b/lib/node_modules/@stdlib/repl/lib/main.js @@ -380,9 +380,9 @@ function REPL( options ) { if ( autoClosed ) { self._previewCompleter.clear(); } + self._completerEngine.onKeypress( data, key ); self._multilineHandler.onKeypress( data, key ); self._syntaxHighlighter.onKeypress(); - self._completerEngine.onKeypress( data, key ); self._previewCompleter.onKeypress( data, key ); } From 858eddba35d44ea0bc8d528f9a788c5a5900a876 Mon Sep 17 00:00:00 2001 From: Snehil Shah Date: Sat, 29 Jun 2024 10:56:13 +0000 Subject: [PATCH 14/27] fix: make it receptive to SIGINT events Signed-off-by: Snehil Shah --- lib/node_modules/@stdlib/repl/lib/main.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/node_modules/@stdlib/repl/lib/main.js b/lib/node_modules/@stdlib/repl/lib/main.js index 86a40161cf0c..84dddf3cfb0f 100644 --- a/lib/node_modules/@stdlib/repl/lib/main.js +++ b/lib/node_modules/@stdlib/repl/lib/main.js @@ -447,6 +447,9 @@ function REPL( options ) { // Check whether the user has entered any characters: isEmpty = ( self._rli.line.length === 0 ); + // Close the completer engine: + self._completerEngine.closeCompleter(); + // Clear the current line: self.clearLine(); From 6fd48a8ea6b358af87997150171bd806727ee06f Mon Sep 17 00:00:00 2001 From: Snehil Shah Date: Sat, 29 Jun 2024 11:00:08 +0000 Subject: [PATCH 15/27] fix: reorder keypress handlers according to precedence Signed-off-by: Snehil Shah --- lib/node_modules/@stdlib/repl/lib/main.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/node_modules/@stdlib/repl/lib/main.js b/lib/node_modules/@stdlib/repl/lib/main.js index 84dddf3cfb0f..64c425b789ef 100644 --- a/lib/node_modules/@stdlib/repl/lib/main.js +++ b/lib/node_modules/@stdlib/repl/lib/main.js @@ -345,6 +345,7 @@ function REPL( options ) { return; } self._autoCloser.beforeKeypress( data, key ); + completed = self._previewCompleter.beforeKeypress( data, key ); // If ENTER keypress is encountered while navigating completions, gracefully close the completer... if ( self._completerEngine.isNavigating() && ( key && key.name === 'return' ) ) { @@ -353,7 +354,6 @@ function REPL( options ) { self._completerEngine.beforeKeypress( data, key ); return; } - completed = self._previewCompleter.beforeKeypress( data, key ); // If completion was auto-completed, don't trigger multi-line keybindings to avoid double operations... if ( !completed ) { From 5bb319c18b17ab277426256da8f53e5a9c59590c Mon Sep 17 00:00:00 2001 From: Snehil Shah Date: Sat, 29 Jun 2024 11:28:19 +0000 Subject: [PATCH 16/27] feat: allow navigating the line when navigating Signed-off-by: Snehil Shah --- .../@stdlib/repl/lib/completer_engine.js | 40 +++++++++---------- lib/node_modules/@stdlib/repl/lib/main.js | 6 +-- 2 files changed, 21 insertions(+), 25 deletions(-) diff --git a/lib/node_modules/@stdlib/repl/lib/completer_engine.js b/lib/node_modules/@stdlib/repl/lib/completer_engine.js index 879ddc9ad0c0..fc2257525808 100644 --- a/lib/node_modules/@stdlib/repl/lib/completer_engine.js +++ b/lib/node_modules/@stdlib/repl/lib/completer_engine.js @@ -184,28 +184,26 @@ setNonEnumerableReadOnly( CompleterEngine.prototype, '_completionCallback', func // Resolve a common prefix from the completion results: autoCompletion = commonPrefix( self._completionsList ); // e.g., [ 'back', 'background', 'backward' ] => 'back' - // If the completion candidates have a possible auto-completion (i.e., a common prefix longer than the input), auto-complete it... - if ( autoCompletion !== '' && autoCompletion.length > self._completionPrefix.length ) { - debug( 'Found an auto-completion candidate: %s', autoCompletion ); + // If we were already navigating, don't try to auto-complete as it's not a TAB trigger. + if ( !self._isNavigating ) { + // If the completion candidates have a possible auto-completion (i.e., a common prefix longer than the input), auto-complete it... + if ( autoCompletion !== '' && autoCompletion.length > self._completionPrefix.length ) { + debug( 'Found an auto-completion candidate: %s', autoCompletion ); - // If we were already navigating, don't auto-complete it as it's not a TAB trigger... - if ( self._isNavigating ) { - self._rli.resume(); - return; - } - // Clear the completion prefix: - self._ostream.write( repeat( '\x08', self._completionPrefix.length ) ); - self._rli.line = self._rli.line.slice( 0, self._rli.cursor - self._completionPrefix.length ) + self._rli.line.slice( self._rli.cursor ); // eslint-disable-line max-len + // Clear the completion prefix: + self._ostream.write( repeat( '\x08', self._completionPrefix.length ) ); + self._rli.line = self._rli.line.slice( 0, self._rli.cursor - self._completionPrefix.length ) + self._rli.line.slice( self._rli.cursor ); // eslint-disable-line max-len - // Move the cursor to the start of completion prefix: - self._rli.cursor -= self._completionPrefix.length; + // Move the cursor to the start of completion prefix: + self._rli.cursor -= self._completionPrefix.length; - // Write the auto-completion string: - self._rli.write( autoCompletion ); + // Write the auto-completion string: + self._rli.write( autoCompletion ); - // Resume the input stream: - self._rli.resume(); - return; + // Resume the input stream: + self._rli.resume(); + return; + } } debug( 'No auto-completion candidate, displaying all possible completions.' ); @@ -528,7 +526,6 @@ setNonEnumerableReadOnly( CompleterEngine.prototype, '_navigateDown', function n setNonEnumerableReadOnly( CompleterEngine.prototype, '_navigateLeft', function navigateLeft( data, key ) { // If on current line, trigger default behavior and stop navigating... if ( this._idx === -1 ) { - this.closeCompleter(); this._ttyWrite.call( this._rli, data, key ); return; } @@ -556,7 +553,6 @@ setNonEnumerableReadOnly( CompleterEngine.prototype, '_navigateRight', function // If on current line, trigger default behavior and stop navigating... if ( this._idx === -1 ) { - this.closeCompleter(); this._ttyWrite.call( this._rli, data, key ); return; } @@ -685,7 +681,7 @@ setNonEnumerableReadOnly( CompleterEngine.prototype, 'beforeKeypress', function * @param {(Object|void)} key - key object * @returns {void} */ -setNonEnumerableReadOnly( CompleterEngine.prototype, 'onKeypress', function onKeypress( data, key ) { +setNonEnumerableReadOnly( CompleterEngine.prototype, 'onKeypress', function onKeypress() { var cursor; var line; @@ -694,7 +690,7 @@ setNonEnumerableReadOnly( CompleterEngine.prototype, 'onKeypress', function onKe return; } // If user is trying to navigate, don't update completions based on the updated line... - if ( key && contains( [ 'left', 'right', 'up', 'down' ], key.name ) ) { + if ( this._idx !== -1 ) { return; } // Clear invalid cache: diff --git a/lib/node_modules/@stdlib/repl/lib/main.js b/lib/node_modules/@stdlib/repl/lib/main.js index 64c425b789ef..c09dd65111d7 100644 --- a/lib/node_modules/@stdlib/repl/lib/main.js +++ b/lib/node_modules/@stdlib/repl/lib/main.js @@ -347,8 +347,8 @@ function REPL( options ) { self._autoCloser.beforeKeypress( data, key ); completed = self._previewCompleter.beforeKeypress( data, key ); - // If ENTER keypress is encountered while navigating completions, gracefully close the completer... - if ( self._completerEngine.isNavigating() && ( key && key.name === 'return' ) ) { + // If ENTER keypress is encountered or if a preview was completed, gracefully close the completer... + if ( completed || ( key && key.name === 'return' ) ) { self._completerEngine.closeCompleter(); } else if ( ( key && key.name === 'tab' ) || self._completerEngine.isNavigating() ) { self._completerEngine.beforeKeypress( data, key ); @@ -381,7 +381,7 @@ function REPL( options ) { self._previewCompleter.clear(); } self._completerEngine.onKeypress( data, key ); - self._multilineHandler.onKeypress( data, key ); + self._multilineHandler.onKeypress(); self._syntaxHighlighter.onKeypress(); self._previewCompleter.onKeypress( data, key ); } From 7f4eac71ab01a3be0540b46f27a40a51a1364188 Mon Sep 17 00:00:00 2001 From: Snehil Shah Date: Sat, 29 Jun 2024 13:53:22 +0000 Subject: [PATCH 17/27] refactor: clean logic and avoid unnecessary re-rendering Signed-off-by: Snehil Shah --- .../@stdlib/repl/lib/completer_engine.js | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/lib/node_modules/@stdlib/repl/lib/completer_engine.js b/lib/node_modules/@stdlib/repl/lib/completer_engine.js index fc2257525808..a3f3936d5558 100644 --- a/lib/node_modules/@stdlib/repl/lib/completer_engine.js +++ b/lib/node_modules/@stdlib/repl/lib/completer_engine.js @@ -181,11 +181,11 @@ setNonEnumerableReadOnly( CompleterEngine.prototype, '_completionCallback', func } self._completionPrefix = completions[ 1 ]; - // Resolve a common prefix from the completion results: - autoCompletion = commonPrefix( self._completionsList ); // e.g., [ 'back', 'background', 'backward' ] => 'back' - // If we were already navigating, don't try to auto-complete as it's not a TAB trigger. if ( !self._isNavigating ) { + // Resolve a common prefix from the completion results: + autoCompletion = commonPrefix( self._completionsList ); // e.g., [ 'back', 'background', 'backward' ] => 'back' + // If the completion candidates have a possible auto-completion (i.e., a common prefix longer than the input), auto-complete it... if ( autoCompletion !== '' && autoCompletion.length > self._completionPrefix.length ) { debug( 'Found an auto-completion candidate: %s', autoCompletion ); @@ -693,16 +693,20 @@ setNonEnumerableReadOnly( CompleterEngine.prototype, 'onKeypress', function onKe if ( this._idx !== -1 ) { return; } + // Re-trigger completions based on updated line: + cursor = this._rli.cursor; + line = this._rli.line; + + // If line is unchanged, no need to re-render completions panel.. + if ( line === this._inputLine + this._remainingLine ) { + return; + } // Clear invalid cache: this.closeCompleter(); // Sustain the completer: this._isNavigating = true; - // Re-trigger completions based on updated line: - cursor = this._rli.cursor; - line = this._rli.line; - // Get the line before the cursor: this._inputLine = line.slice( 0, cursor ); From ddcd3d6b42aef84242b9f1929aab6e81f0ecc557 Mon Sep 17 00:00:00 2001 From: Snehil Shah Date: Sat, 29 Jun 2024 13:54:00 +0000 Subject: [PATCH 18/27] test: remove outdated test Signed-off-by: Snehil Shah --- .../test/integration/test.completer_engine.js | 52 ------------------- 1 file changed, 52 deletions(-) diff --git a/lib/node_modules/@stdlib/repl/test/integration/test.completer_engine.js b/lib/node_modules/@stdlib/repl/test/integration/test.completer_engine.js index c6a59928c9a2..760094714e1d 100644 --- a/lib/node_modules/@stdlib/repl/test/integration/test.completer_engine.js +++ b/lib/node_modules/@stdlib/repl/test/integration/test.completer_engine.js @@ -203,58 +203,6 @@ tape( 'a REPL instance supports hiding the completions panel upon pressing TAB a } }); -tape( 'a REPL instance supports hiding the completions panel upon typing again', 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 ); - - // Declare variables with unique names in order to prevent namespace collisions: - istream.write( 'var zzzxy = 1;' ); - istream.write( 'var zzzab = 2;' ); - - // Write the common substring of the variable names in order to generate completions: - istream.write( 'zzz' ); - - // Write TAB to display completions: - istream.write( '\t' ); - - // Write TAB again to hide completions: - istream.write( 'a' ); - - // Close the input stream: - istream.end(); - - // Close the REPL: - r.close(); - - function onClose( error, data ) { - if ( error ) { - t.fail( error.message ); - return; - } - // Check if the completions were cleared: - t.strictEqual( data[ data.length - 3 ], '\x1B[0J', 'returns expected value' ); - t.strictEqual( data[ data.length - 1 ], 'a', 'returns expected value' ); - - // NOTE: `data[ data.length-2 ]` adds the remaining string to the right of the cursor because of the abnormal behaviour of `clearScreenDown`... - - t.end(); - } -}); - tape( 'a REPL instance supports navigating the TAB completions using arrow keys', function test( t ) { var istream; var opts; From 494f8451a07564bc9af5d90478fa36c62725506b Mon Sep 17 00:00:00 2001 From: Snehil Shah Date: Sat, 29 Jun 2024 14:06:18 +0000 Subject: [PATCH 19/27] fix: don't close when completer is already closed Signed-off-by: Snehil Shah --- lib/node_modules/@stdlib/repl/lib/completer_engine.js | 4 ++++ lib/node_modules/@stdlib/repl/lib/main.js | 3 +-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/node_modules/@stdlib/repl/lib/completer_engine.js b/lib/node_modules/@stdlib/repl/lib/completer_engine.js index a3f3936d5558..7adb2f9ca9bf 100644 --- a/lib/node_modules/@stdlib/repl/lib/completer_engine.js +++ b/lib/node_modules/@stdlib/repl/lib/completer_engine.js @@ -585,6 +585,10 @@ setNonEnumerableReadOnly( CompleterEngine.prototype, 'isNavigating', function is * @returns {void} */ setNonEnumerableReadOnly( CompleterEngine.prototype, 'closeCompleter', function closeCompleter() { + // Check if completer is already closed... + if ( !this._isNavigating ) { + return; + } // Reset completer parameters: this._isNavigating = false; this._idx = -1; diff --git a/lib/node_modules/@stdlib/repl/lib/main.js b/lib/node_modules/@stdlib/repl/lib/main.js index c09dd65111d7..e88c110d2e63 100644 --- a/lib/node_modules/@stdlib/repl/lib/main.js +++ b/lib/node_modules/@stdlib/repl/lib/main.js @@ -347,14 +347,13 @@ function REPL( options ) { self._autoCloser.beforeKeypress( data, key ); completed = self._previewCompleter.beforeKeypress( data, key ); - // If ENTER keypress is encountered or if a preview was completed, gracefully close the completer... + // If ENTER keypress is encountered or if a preview was completed while navigating, gracefully close the completer... if ( completed || ( key && key.name === 'return' ) ) { self._completerEngine.closeCompleter(); } else if ( ( key && key.name === 'tab' ) || self._completerEngine.isNavigating() ) { self._completerEngine.beforeKeypress( data, key ); return; } - // If completion was auto-completed, don't trigger multi-line keybindings to avoid double operations... if ( !completed ) { self._multilineHandler.beforeKeypress( data, key ); From 141bfe45a8ed48637820db0bb019cb9ed2662d20 Mon Sep 17 00:00:00 2001 From: Snehil Shah Date: Sun, 30 Jun 2024 22:56:29 +0000 Subject: [PATCH 20/27] test: add test case for live completions Signed-off-by: Snehil Shah --- .../test/integration/test.completer_engine.js | 76 +++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/lib/node_modules/@stdlib/repl/test/integration/test.completer_engine.js b/lib/node_modules/@stdlib/repl/test/integration/test.completer_engine.js index 760094714e1d..b568519ce52c 100644 --- a/lib/node_modules/@stdlib/repl/test/integration/test.completer_engine.js +++ b/lib/node_modules/@stdlib/repl/test/integration/test.completer_engine.js @@ -203,6 +203,82 @@ tape( 'a REPL instance supports hiding the completions panel upon pressing TAB a } }); +tape( 'a REPL instance supports updating the completions upon typing', 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 ); + + // Declare variables with unique names in order to prevent namespace collisions: + istream.write( 'var zzzxy = 1;' ); + istream.write( 'var zzzab = 2;' ); + istream.write( 'var zzzxyz = 4;' ); + + // Write the common substring of the variable names in order to generate completions: + istream.write( 'zzz' ); + + // Write TAB to display completions: + istream.write( '\t' ); + + // Update completions to narrow down names: + istream.write( 'x' ); + istream.write( 'y' ); + istream.write( 'z' ); + + // Close the input stream: + istream.end(); + + // Close the REPL: + r.close(); + + function onClose( error, data ) { + var actual; + + if ( error ) { + t.fail( error.message ); + return; + } + + // Check for the completions after hitting TAB: + actual = extractCompletions( stripANSI( data[ data.length - 21 ] ) ); + t.strictEqual( actual.length, 3, 'returns expected value' ); + t.strictEqual( actual[ 0 ], 'zzzab', 'returns expected value' ); + t.strictEqual( actual[ 1 ], 'zzzxy', 'returns expected value' ); + t.strictEqual( actual[ 2 ], 'zzzxyz', 'returns expected value' ); + + // Check for updated completions after hitting `x`: + actual = extractCompletions( stripANSI( data[ data.length - 15 ] ) ); + t.strictEqual( actual.length, 2, 'returns expected value' ); + t.strictEqual( actual[ 0 ], 'zzzxy', 'returns expected value' ); + t.strictEqual( actual[ 1 ], 'zzzxyz', 'returns expected value' ); + + // Check for updated completions after hitting `y`: + actual = extractCompletions( stripANSI( data[ data.length - 9 ] ) ); + t.strictEqual( actual.length, 2, 'returns expected value' ); + t.strictEqual( actual[ 0 ], 'zzzxy', 'returns expected value' ); + t.strictEqual( actual[ 1 ], 'zzzxyz', 'returns expected value' ); + + // Check for updated completions after hitting `z`: + actual = extractCompletions( stripANSI( data[ data.length - 3 ] ) ); + t.strictEqual( actual.length, 1, 'returns expected value' ); + t.strictEqual( actual[ 0 ], 'zzzxyz', 'returns expected value' ); + + t.end(); + } +}); + tape( 'a REPL instance supports navigating the TAB completions using arrow keys', function test( t ) { var istream; var opts; From 5410b2b44bf817461296efb730d7a48be09a5244 Mon Sep 17 00:00:00 2001 From: Snehil Shah Date: Mon, 1 Jul 2024 11:47:46 +0000 Subject: [PATCH 21/27] fix: incorrect logic Signed-off-by: Snehil Shah --- .../@stdlib/repl/lib/completer_engine.js | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/lib/node_modules/@stdlib/repl/lib/completer_engine.js b/lib/node_modules/@stdlib/repl/lib/completer_engine.js index 7adb2f9ca9bf..343029418932 100644 --- a/lib/node_modules/@stdlib/repl/lib/completer_engine.js +++ b/lib/node_modules/@stdlib/repl/lib/completer_engine.js @@ -688,6 +688,8 @@ setNonEnumerableReadOnly( CompleterEngine.prototype, 'beforeKeypress', function setNonEnumerableReadOnly( CompleterEngine.prototype, 'onKeypress', function onKeypress() { var cursor; var line; + var il; + var rl; // If user is not navigating completions, don't sustain completions... if ( !this._isNavigating ) { @@ -701,22 +703,24 @@ setNonEnumerableReadOnly( CompleterEngine.prototype, 'onKeypress', function onKe cursor = this._rli.cursor; line = this._rli.line; + // Get line before and after the cursor: + il = line.slice( 0, cursor ); + rl = line.slice( cursor ); + // If line is unchanged, no need to re-render completions panel.. - if ( line === this._inputLine + this._remainingLine ) { + if ( il === this._inputLine && rl === this._remainingLine ) { return; } // Clear invalid cache: this.closeCompleter(); + // Update line buffers: + this._inputLine = il; + this._remainingLine = rl; + // Sustain the completer: this._isNavigating = true; - // Get the line before the cursor: - this._inputLine = line.slice( 0, cursor ); - - // Get the line after the cursor: - this._remainingLine = line.slice( cursor ); - // Pause the input stream before generating completions as the completer may be asynchronous... this._rli.pause(); this._completer( this._inputLine, this._onCompletions ); From 3677b2a304b400c1d7d5f398418e744441ebb818 Mon Sep 17 00:00:00 2001 From: Snehil Shah Date: Mon, 1 Jul 2024 11:48:38 +0000 Subject: [PATCH 22/27] fix: incorrect keypress handlers Signed-off-by: Snehil Shah --- lib/node_modules/@stdlib/repl/lib/main.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/node_modules/@stdlib/repl/lib/main.js b/lib/node_modules/@stdlib/repl/lib/main.js index e88c110d2e63..d757ebf64666 100644 --- a/lib/node_modules/@stdlib/repl/lib/main.js +++ b/lib/node_modules/@stdlib/repl/lib/main.js @@ -379,8 +379,8 @@ function REPL( options ) { if ( autoClosed ) { self._previewCompleter.clear(); } - self._completerEngine.onKeypress( data, key ); - self._multilineHandler.onKeypress(); + self._completerEngine.onKeypress(); + self._multilineHandler.onKeypress( data, key ); self._syntaxHighlighter.onKeypress(); self._previewCompleter.onKeypress( data, key ); } From e53f691b2cbcd939f11cbf04fadf2daa0bdc2f58 Mon Sep 17 00:00:00 2001 From: Snehil Shah Date: Mon, 1 Jul 2024 11:51:01 +0000 Subject: [PATCH 23/27] test: update test names Signed-off-by: Snehil Shah --- .../@stdlib/repl/test/integration/test.completer_engine.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/node_modules/@stdlib/repl/test/integration/test.completer_engine.js b/lib/node_modules/@stdlib/repl/test/integration/test.completer_engine.js index b568519ce52c..2429c5f2c477 100644 --- a/lib/node_modules/@stdlib/repl/test/integration/test.completer_engine.js +++ b/lib/node_modules/@stdlib/repl/test/integration/test.completer_engine.js @@ -414,7 +414,7 @@ tape( 'a REPL instance supports bringing back the original line upon navigating } }); -tape( 'a REPL instance supports displaying highlighted argument TAB completions', function test( t ) { +tape( 'a REPL instance supports displaying highlighted object property TAB completions', function test( t ) { var istream; var opts; var r; @@ -519,7 +519,7 @@ tape( 'a REPL instance supports auto-completing common prefixes when hitting TAB } }); -tape( 'a REPL instance supports auto-completing common argument prefixes when hitting TAB', function test( t ) { +tape( 'a REPL instance supports auto-completing common object property prefixes when hitting TAB', function test( t ) { var istream; var opts; var r; From 4b8a7523943f3a3944af5701e2742c7065d8b76b Mon Sep 17 00:00:00 2001 From: Snehil Shah Date: Mon, 1 Jul 2024 11:54:36 +0000 Subject: [PATCH 24/27] docs: fix jsdoc Signed-off-by: Snehil Shah --- lib/node_modules/@stdlib/repl/lib/completer_engine.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/node_modules/@stdlib/repl/lib/completer_engine.js b/lib/node_modules/@stdlib/repl/lib/completer_engine.js index 343029418932..dddacc913b01 100644 --- a/lib/node_modules/@stdlib/repl/lib/completer_engine.js +++ b/lib/node_modules/@stdlib/repl/lib/completer_engine.js @@ -446,13 +446,13 @@ setNonEnumerableReadOnly( CompleterEngine.prototype, '_completionRows', function }); /** -* Checks whether content having a specified number of lines is able to fit within the current viewport. +* Checks whether completions are able to fit within the current viewport. * * @private * @name _isDisplayable * @memberof CompleterEngine.prototype * @type {Function} -* @returns {boolean} boolean indicating whether content is "displayable" +* @returns {boolean} boolean indicating whether the completions are "displayable" */ setNonEnumerableReadOnly( CompleterEngine.prototype, '_isDisplayable', function isDisplayable() { var vh = this._repl.viewportHeight(); From 71b5c4c36c6adb6713234b0873b2e342f0e117b4 Mon Sep 17 00:00:00 2001 From: Snehil Shah Date: Mon, 1 Jul 2024 16:00:23 +0000 Subject: [PATCH 25/27] fix: update after inserting a completion Signed-off-by: Snehil Shah --- lib/node_modules/@stdlib/repl/lib/completer_engine.js | 4 ++-- lib/node_modules/@stdlib/repl/lib/main.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/node_modules/@stdlib/repl/lib/completer_engine.js b/lib/node_modules/@stdlib/repl/lib/completer_engine.js index dddacc913b01..553caa9546eb 100644 --- a/lib/node_modules/@stdlib/repl/lib/completer_engine.js +++ b/lib/node_modules/@stdlib/repl/lib/completer_engine.js @@ -685,7 +685,7 @@ setNonEnumerableReadOnly( CompleterEngine.prototype, 'beforeKeypress', function * @param {(Object|void)} key - key object * @returns {void} */ -setNonEnumerableReadOnly( CompleterEngine.prototype, 'onKeypress', function onKeypress() { +setNonEnumerableReadOnly( CompleterEngine.prototype, 'onKeypress', function onKeypress( data, key ) { var cursor; var line; var il; @@ -696,7 +696,7 @@ setNonEnumerableReadOnly( CompleterEngine.prototype, 'onKeypress', function onKe return; } // If user is trying to navigate, don't update completions based on the updated line... - if ( this._idx !== -1 ) { + if ( this._idx !== -1 && contains( [ 'left', 'right', 'up', 'down' ], key.name ) ) { return; } // Re-trigger completions based on updated line: diff --git a/lib/node_modules/@stdlib/repl/lib/main.js b/lib/node_modules/@stdlib/repl/lib/main.js index d757ebf64666..b63a95e23f01 100644 --- a/lib/node_modules/@stdlib/repl/lib/main.js +++ b/lib/node_modules/@stdlib/repl/lib/main.js @@ -379,7 +379,7 @@ function REPL( options ) { if ( autoClosed ) { self._previewCompleter.clear(); } - self._completerEngine.onKeypress(); + self._completerEngine.onKeypress( data, key ); self._multilineHandler.onKeypress( data, key ); self._syntaxHighlighter.onKeypress(); self._previewCompleter.onKeypress( data, key ); From e862980037e02199e38de0822d5cb9322f89d395 Mon Sep 17 00:00:00 2001 From: Athan Date: Mon, 1 Jul 2024 11:28:22 -0700 Subject: [PATCH 26/27] Apply suggestions from code review Signed-off-by: Athan --- lib/node_modules/@stdlib/repl/lib/completer_engine.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/node_modules/@stdlib/repl/lib/completer_engine.js b/lib/node_modules/@stdlib/repl/lib/completer_engine.js index 553caa9546eb..c7aa39ff3b03 100644 --- a/lib/node_modules/@stdlib/repl/lib/completer_engine.js +++ b/lib/node_modules/@stdlib/repl/lib/completer_engine.js @@ -181,7 +181,7 @@ setNonEnumerableReadOnly( CompleterEngine.prototype, '_completionCallback', func } self._completionPrefix = completions[ 1 ]; - // If we were already navigating, don't try to auto-complete as it's not a TAB trigger. + // If we were already navigating, don't try to auto-complete as it's not a TAB trigger... if ( !self._isNavigating ) { // Resolve a common prefix from the completion results: autoCompletion = commonPrefix( self._completionsList ); // e.g., [ 'back', 'background', 'backward' ] => 'back' @@ -238,7 +238,7 @@ setNonEnumerableReadOnly( CompleterEngine.prototype, '_displayCompletions', func var dy; var i; - // Determine number of columns of completions that should be displayed to the output stream + // Determine number of columns of completions that should be displayed to the output stream... this._completionsLength = []; for ( i = 0; i < this._completionsList.length; i++ ) { this._completionsLength.push( this._completionsList[ i ].length ); @@ -670,7 +670,7 @@ setNonEnumerableReadOnly( CompleterEngine.prototype, 'beforeKeypress', function // Get the line after the cursor: this._remainingLine = line.slice( cursor ); - // Pause the input stream before generating completions as the completer may be asynchronous... + // Pause the input stream before generating completions as the completer may be asynchronous: this._rli.pause(); this._completer( this._inputLine, this._onCompletions ); }); @@ -707,7 +707,7 @@ setNonEnumerableReadOnly( CompleterEngine.prototype, 'onKeypress', function onKe il = line.slice( 0, cursor ); rl = line.slice( cursor ); - // If line is unchanged, no need to re-render completions panel.. + // If line is unchanged, no need to re-render completions panel... if ( il === this._inputLine && rl === this._remainingLine ) { return; } @@ -718,7 +718,7 @@ setNonEnumerableReadOnly( CompleterEngine.prototype, 'onKeypress', function onKe this._inputLine = il; this._remainingLine = rl; - // Sustain the completer: + // Ensure that the displayed completions remain visible: this._isNavigating = true; // Pause the input stream before generating completions as the completer may be asynchronous... From 97329bf8d77eed7a71051f7d2de224fab9d557e0 Mon Sep 17 00:00:00 2001 From: Athan Date: Mon, 1 Jul 2024 11:29:37 -0700 Subject: [PATCH 27/27] Apply suggestions from code review Signed-off-by: Athan --- lib/node_modules/@stdlib/repl/lib/completer_engine.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/node_modules/@stdlib/repl/lib/completer_engine.js b/lib/node_modules/@stdlib/repl/lib/completer_engine.js index c7aa39ff3b03..5de916695d2e 100644 --- a/lib/node_modules/@stdlib/repl/lib/completer_engine.js +++ b/lib/node_modules/@stdlib/repl/lib/completer_engine.js @@ -461,7 +461,7 @@ setNonEnumerableReadOnly( CompleterEngine.prototype, '_isDisplayable', function }); /** -* Navigate up the completions grid. +* Navigates up the completions grid. * * @private * @name _navigateUp @@ -490,7 +490,7 @@ setNonEnumerableReadOnly( CompleterEngine.prototype, '_navigateUp', function nav }); /** -* Navigate down the completions grid. +* Navigates down the completions grid. * * @private * @name _navigateDown @@ -513,7 +513,7 @@ setNonEnumerableReadOnly( CompleterEngine.prototype, '_navigateDown', function n }); /** -* Navigate to the left in the completions grid. +* Navigates to the left in the completions grid. * * @private * @name _navigateLeft @@ -537,7 +537,7 @@ setNonEnumerableReadOnly( CompleterEngine.prototype, '_navigateLeft', function n }); /** -* Navigate to the right in the completions grid. +* Navigates to the right in the completions grid. * * @private * @name _navigateRight