From dc733720cc5116d660e383e9f9fc5f299d5d8e34 Mon Sep 17 00:00:00 2001 From: Snehil Shah <130062020+Snehil-Shah@users.noreply.github.com> Date: Tue, 12 Mar 2024 23:01:55 +0000 Subject: [PATCH 01/24] feat: add fuzzy matching algorithm for tab completions Signed-off-by: Snehil Shah <130062020+Snehil-Shah@users.noreply.github.com> --- .../@stdlib/repl/lib/filter_by_prefix.js | 69 ++++++++++++++++++- 1 file changed, 68 insertions(+), 1 deletion(-) diff --git a/lib/node_modules/@stdlib/repl/lib/filter_by_prefix.js b/lib/node_modules/@stdlib/repl/lib/filter_by_prefix.js index 73e014060627..5229967ed718 100644 --- a/lib/node_modules/@stdlib/repl/lib/filter_by_prefix.js +++ b/lib/node_modules/@stdlib/repl/lib/filter_by_prefix.js @@ -21,6 +21,73 @@ // MODULES // var startsWith = require( '@stdlib/string/starts-with' ); +var max = require( '@stdlib/math/base/special/max' ); +var abs = require( '@stdlib/math/base/special/abs' ); + + +// FUNCTIONS // + +/** +* Checks if the completion is a fuzzy match for the input. +* +* A fuzzy match is determined by the number and order of matching characters, penalizing large distances between matches. +* A score above or equal to 0.8 indicates a match. +* +* @private +* @param {string} completion - The completion string. +* @param {string} input - The input string. +* @returns {boolean} - True if the completion is a fuzzy match for the input, false otherwise. +*/ +function fuzzyMatch( completion, input ) { + var charPositions; + var finalScore; + var positions; + var distance; + var score; + var index; + var char; + var i; + var j; + + if ( startsWith( completion, input ) ) { + return true; // Return true for perfect matches + } + + // Preprocess the completion string to get the positions of each character + positions = {}; + for ( i = 0; i < completion.length; i++ ) { + char = completion[ i ]; + if (!positions[ char ]) { + positions[ char] = []; + } + positions[ char ].push( i ); + } + + score = 0; + index = 0; + for ( i = 0; i < input.length; i++ ) { + charPositions = positions[ input[ i ] ]; + if ( !charPositions ) { + continue; + } + + // Find the next position of the character that is greater than or equal to index + j = 0; + while ( j < charPositions.length && charPositions[ j ] < index ) { + j += 1; + } + if ( j === charPositions.length ) { + continue; + } + + distance = abs( charPositions[ j ] - i ); + score += max( 0, 1 - ( distance * 0.25 ) ); // Subtract a penalty based on the distance between matching characters + index = charPositions[ j ] + 1; + } + finalScore = score / input.length; // Calculate relative score + + return finalScore >= 0.65; +} // MAIN // @@ -37,7 +104,7 @@ var startsWith = require( '@stdlib/string/starts-with' ); function filterByPrefix( out, arr, str ) { var i; for ( i = 0; i < arr.length; i++ ) { - if ( startsWith( arr[ i ], str ) ) { + if ( fuzzyMatch( arr[ i ], str ) ) { out.push( arr[ i ] ); } } From 3cae6a7cae28b7b63831a44a317a480528653ca7 Mon Sep 17 00:00:00 2001 From: Snehil Shah Date: Sun, 24 Mar 2024 08:57:40 +0000 Subject: [PATCH 02/24] refactor: fix function jsdoc Signed-off-by: Snehil Shah --- .../@stdlib/repl/lib/filter_by_prefix.js | 75 ++++++++++--------- 1 file changed, 41 insertions(+), 34 deletions(-) diff --git a/lib/node_modules/@stdlib/repl/lib/filter_by_prefix.js b/lib/node_modules/@stdlib/repl/lib/filter_by_prefix.js index 5229967ed718..90ec28fbe27d 100644 --- a/lib/node_modules/@stdlib/repl/lib/filter_by_prefix.js +++ b/lib/node_modules/@stdlib/repl/lib/filter_by_prefix.js @@ -30,13 +30,10 @@ var abs = require( '@stdlib/math/base/special/abs' ); /** * Checks if the completion is a fuzzy match for the input. * -* A fuzzy match is determined by the number and order of matching characters, penalizing large distances between matches. -* A score above or equal to 0.8 indicates a match. -* * @private -* @param {string} completion - The completion string. -* @param {string} input - The input string. -* @returns {boolean} - True if the completion is a fuzzy match for the input, false otherwise. +* @param {string} completion - completion string +* @param {string} input - input string +* @returns {boolean} - boolean indicating if a completion is a fuzzy match for the input string */ function fuzzyMatch( completion, input ) { var charPositions; @@ -49,44 +46,54 @@ function fuzzyMatch( completion, input ) { var i; var j; + // Return true for perfect prefix if ( startsWith( completion, input ) ) { - return true; // Return true for perfect matches + return true; } - // Preprocess the completion string to get the positions of each character - positions = {}; - for ( i = 0; i < completion.length; i++ ) { - char = completion[ i ]; - if (!positions[ char ]) { - positions[ char] = []; + if ( completion.length >= input.length ) { + // Preprocess the completion string to get the positions of each character + positions = {}; + for ( i = 0; i < completion.length; i++ ) { + char = completion[ i ]; + if (!positions[ char ]) { + positions[ char] = []; + } + positions[ char ].push( i ); } - positions[ char ].push( i ); - } - score = 0; - index = 0; - for ( i = 0; i < input.length; i++ ) { - charPositions = positions[ input[ i ] ]; - if ( !charPositions ) { - continue; + score = 0; + index = 0; + for ( i = 0; i < input.length; i++ ) { + charPositions = positions[ input[ i ] ]; + if ( !charPositions ) { + continue; + } + + // Find the next position of the character that is greater than or equal to index + j = 0; + while ( j < charPositions.length && charPositions[ j ] < index ) { + j += 1; + } + if ( j === charPositions.length ) { + continue; + } + + // Subtract a penalty based on the distance between matching characters + distance = abs( charPositions[ j ] - i ); + score += max( 0, 1 - ( distance * 0.25 ) ); + index = charPositions[ j ] + 1; } - // Find the next position of the character that is greater than or equal to index - j = 0; - while ( j < charPositions.length && charPositions[ j ] < index ) { - j += 1; - } - if ( j === charPositions.length ) { - continue; - } + // Calculate relative score + finalScore = score / input.length; - distance = abs( charPositions[ j ] - i ); - score += max( 0, 1 - ( distance * 0.25 ) ); // Subtract a penalty based on the distance between matching characters - index = charPositions[ j ] + 1; + // A score above or equal to 0.7 indicates a match + return finalScore >= 0.7; } - finalScore = score / input.length; // Calculate relative score - return finalScore >= 0.65; + // Return false for completions smaller than the input + return false; } From dcb6014f74478cae80004965e36ae57228133f6f Mon Sep 17 00:00:00 2001 From: Snehil Shah Date: Tue, 2 Apr 2024 12:32:56 +0000 Subject: [PATCH 03/24] feat: rewrite the completer engine Signed-off-by: Snehil Shah --- .../@stdlib/repl/lib/completer.js | 10 +- .../@stdlib/repl/lib/completer_engine.js | 226 ++++++++++++++++++ lib/node_modules/@stdlib/repl/lib/main.js | 12 +- 3 files changed, 240 insertions(+), 8 deletions(-) create mode 100644 lib/node_modules/@stdlib/repl/lib/completer_engine.js diff --git a/lib/node_modules/@stdlib/repl/lib/completer.js b/lib/node_modules/@stdlib/repl/lib/completer.js index a8ca2b851b2c..8fc88fa39b45 100644 --- a/lib/node_modules/@stdlib/repl/lib/completer.js +++ b/lib/node_modules/@stdlib/repl/lib/completer.js @@ -38,7 +38,7 @@ var completeExpression = require( './complete_expression.js' ); // VARIABLES // -var debug = logger( 'repl:completer' ); +var debug = logger( 'repl:completer:callback' ); // FUNCTIONS // @@ -51,6 +51,7 @@ var debug = logger( 'repl:completer' ); * @returns {Array} normalized completion list */ function normalize( list ) { + var uniqueList = []; var hash; var i; @@ -59,16 +60,13 @@ function normalize( list ) { for ( i = 0; i < list.length; i++ ) { if ( !hasOwnProp( hash, list[ i ] ) ) { hash[ list[ i ] ] = true; + uniqueList.push( list[ i ] ); } } - list = objectKeys( hash ); // TODO: sort such that printed columns are in lexicographic order, not rows, similar to bash behavior! - // Sort the values in lexicographic order: - list = list.sort(); - - return list; + return uniqueList; } 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..4b2023d310c1 --- /dev/null +++ b/lib/node_modules/@stdlib/repl/lib/completer_engine.js @@ -0,0 +1,226 @@ +/** +* @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 */ + +'use strict'; + +// MODULES // + +var logger = require( 'debug' ); +var setNonEnumerableReadOnly = require( '@stdlib/utils/define-nonenumerable-read-only-property' ); +var max = require( '@stdlib/stats/base/max' ); +var floor = require( '@stdlib/math/base/special/floor' ); +var repeat = require( '@stdlib/string/repeat' ); +var isEmptyString = require( '@stdlib/assert/is-empty-string' ).isPrimitive; +var displayPrompt = require( './display_prompt.js' ); +var commonPrefix = require( './longest_common_prefix.js' ); + + +// VARIABLES // + +var debug = logger( 'repl:completer:engine' ); + + +// FUNCTIONS // + +/** +* Returns length of a completion string. +* +* @private +* @param {string} completion string +* @returns {number} length of completion string +*/ +function getLength( completion ) { + return completion.length; +} + +/** +* Checks if the completion string is not empty. +* +* @private +* @param {string} completion - completion string +* @returns {boolean} boolean indicating if completion is not an empty string +*/ +function isNotEmptyString( completion ) { + return !isEmptyString( completion ); +} + + +// 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 +* @returns {CompleterEngine} completer engine instance +*/ +function CompleterEngine( repl, completer, ostream ) { + if ( !(this instanceof CompleterEngine) ) { + return new CompleterEngine( repl, completer, ostream ); + } + 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; + + // Create a callback for processing completions: + this._onCompletions = this._completionCallback(); + + // Initialize a buffer containing the current line being processed: + this._line = ''; + + // Initialize a buffer containing the list of generated completions: + this._completionsList = []; + + return this; +} + +/** +* Returns a callback for processing completions. +* +* @private +* @name _completionCallback +* @memberof CompleterEngine.prototype +* @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 autocompletionSubstring; + var prefix; + + // 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' ); + return; + } + self._completionsList = completions[0].filter( isNotEmptyString ); // remove empty completions + if ( self._completionsList.length === 0 ) { + debug( 'No completions to display.' ); + return; + } + // Resolve a common prefix from the completion results: + prefix = commonPrefix( self._completionsList ); // e.g., [ 'back', 'background', 'backward' ] => 'back' + + // Extract auto-completion substring: + autocompletionSubstring = prefix.substring( commonPrefix( prefix, completions[1] ).length ); // eslint-disable-line max-len + + // If the completion candidates have a possible auto-completion (ie. a common prefix longer than the input), auto-complete it... + if ( autocompletionSubstring !== '' ) { + debug( 'Found an auto-completion candidate: %s', prefix ); + self._rli.write( autocompletionSubstring ); + return; + } + debug( 'No auto-completion candidate, displaying all possible completions.' ); + + // Display completions: + self._displayCompletions(); + + // Re-display the prompt: + displayPrompt( self.repl, true ); + } +}); + +setNonEnumerableReadOnly( CompleterEngine.prototype, '_displayCompletions', function displayCompletions() { + var completionsLength; + var widthOfColumn; + var whitespaces; + var completion; + var lineIndex; + var columns; + var output; + var i; + + // Determine number of columns of completions that should be displayed to the output stream + completionsLength = this._completionsList.map( getLength ); + widthOfColumn = max( completionsLength.length, completionsLength, 1 ) + 4; // 4 space padding + columns = floor( this._ostream.columns / widthOfColumn ) || 1; + + output = '\r\n'; + lineIndex = 0; + whitespaces = 0; + for ( i = 0; i < this._completionsList.length; i++ ) { + completion = this._completionsList[i]; + 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 ); + } + // Add completion string to the column in output: + output += completion; + whitespaces = widthOfColumn - completionsLength[i]; + lineIndex += 1; + } + // Ensure a space between completions and the following prompt... + if ( lineIndex !== 0 ) { + output += '\r\n\r\n'; + } + // Write completions to the output stream: + this._ostream.write( output ); +}); + +setNonEnumerableReadOnly( CompleterEngine.prototype, 'beforeKeypress', function beforeKeypress() { + var cursor; + var input; + var line; + + cursor = this._rli.cursor; + line = this._rli.line; + + // Get the line before the cursor: + input = line.slice( 0, cursor ); + + // Pause the input stream before generating completions as the completer may be asynchronous... + this._rli.pause(); + Promise.resolve( this._completer( input, this._onCompletions ) ) + .then( this._rli.resume.bind( this._rli ) ); +}); + + +// 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 e002c41c260f..f30fe067d646 100644 --- a/lib/node_modules/@stdlib/repl/lib/main.js +++ b/lib/node_modules/@stdlib/repl/lib/main.js @@ -55,6 +55,7 @@ var commands = require( './commands.js' ); var displayPrompt = require( './display_prompt.js' ); var inputPrompt = require( './input_prompt.js' ); var processLine = require( './process_line.js' ); +var CompleterEngine = require( './completer_engine.js' ); var completerFactory = require( './completer.js' ); var PreviewCompleter = require( './completer_preview.js' ); var ALIAS_OVERRIDES = require( './alias_overrides.js' ); @@ -108,6 +109,7 @@ var debug = logger( 'repl' ); */ function REPL( options ) { var previewCompleter; + var completerEngine; var completer; var ttyWrite; var opts; @@ -236,10 +238,12 @@ function REPL( options ) { 'input': this._istream, 'output': this._ostream, 'terminal': opts.isTTY, - 'prompt': opts.inputPrompt, - 'completer': completer + 'prompt': opts.inputPrompt })); + // Create a new TAB completer engine: + completerEngine = new CompleterEngine( self, completer, this._ostream ); + this._rli.on( 'close', onClose ); this._rli.on( 'line', onLine ); this._rli.on( 'SIGINT', onSIGINT ); @@ -289,6 +293,10 @@ function REPL( options ) { * @param {Object} key - key object */ function beforeKeypress( data, key ) { + if ( key && key.name === 'tab' ) { + completerEngine.beforeKeypress(); + return; + } previewCompleter.beforeKeypress( data, key ); ttyWrite.call( self._rli, data, key ); } From bef5dd3a972de48dded336b524edb6352694fd97 Mon Sep 17 00:00:00 2001 From: Snehil Shah Date: Tue, 2 Apr 2024 12:35:16 +0000 Subject: [PATCH 04/24] feat: improve fuzzy algorithm and sort by relevancy Signed-off-by: Snehil Shah --- .../@stdlib/repl/lib/complete_expression.js | 8 +- .../@stdlib/repl/lib/filter_by_prefix.js | 175 ++++++++++++------ 2 files changed, 124 insertions(+), 59 deletions(-) diff --git a/lib/node_modules/@stdlib/repl/lib/complete_expression.js b/lib/node_modules/@stdlib/repl/lib/complete_expression.js index 64b664344fad..02628f344e78 100644 --- a/lib/node_modules/@stdlib/repl/lib/complete_expression.js +++ b/lib/node_modules/@stdlib/repl/lib/complete_expression.js @@ -100,9 +100,7 @@ function complete( out, context, expression ) { } filter = node.expression.name; debug( 'Identifier auto-completion. Filter: %s', filter ); - out = filterByPrefix( out, RESERVED_KEYWORDS_COMMON, filter ); - out = filterByPrefix( out, objectKeys( context ), filter ); - out = filterByPrefix( out, ast.locals, filter ); + out = filterByPrefix( out, RESERVED_KEYWORDS_COMMON.concat(objectKeys(context), ast.locals), filter ); // eslint-disable-line max-len return filter; } // Find the identifier or member expression to be completed: @@ -222,9 +220,7 @@ function complete( out, context, expression ) { // Case: `foo<|>` (completing an identifier) filter = ( node.name === '✖' ) ? '' : node.name; debug( 'Identifier auto-completion. Filter: %s', filter ); - out = filterByPrefix( out, res.keywords, filter ); - out = filterByPrefix( out, objectKeys( context ), filter ); - out = filterByPrefix( out, resolveLocalScope( ast, node ), filter ); + out = filterByPrefix( out, res.keywords.concat( objectKeys( context ), resolveLocalScope( ast, node ) ), filter ); // eslint-disable-line max-len return filter; } diff --git a/lib/node_modules/@stdlib/repl/lib/filter_by_prefix.js b/lib/node_modules/@stdlib/repl/lib/filter_by_prefix.js index 90ec28fbe27d..cfa1e3395584 100644 --- a/lib/node_modules/@stdlib/repl/lib/filter_by_prefix.js +++ b/lib/node_modules/@stdlib/repl/lib/filter_by_prefix.js @@ -21,79 +21,129 @@ // MODULES // var startsWith = require( '@stdlib/string/starts-with' ); -var max = require( '@stdlib/math/base/special/max' ); -var abs = require( '@stdlib/math/base/special/abs' ); +var lowercase = require( '@stdlib/string/lowercase' ); + + +// VARIABLES // + +var PENALTIES = { + 'wrong_case': -1, // Penalty for a matching character that is in the wrong case + 'not_start': -4, // Penalty if the first letter of the completion and input are different + 'gap_between': -8, // Penalty for a gap between matching characters of input in the completion + 'gap_beginning': -1, // Penalty for a gap, before a character from input is encountered + 'missing_char': -19 // Penalty for a character in input that doesn't exist in the completion (to tackle possible spelling mistakes) +}; +var LENIENCY_FACTOR = 0.27; // Controls how lenient the algorithm is // FUNCTIONS // /** -* Checks if the completion is a fuzzy match for the input. +* Callback to sort the results array in descending order of score, and in ascending order of item length when scores are equal. +* +* @private +* @param {Object} a - first object representing a completion's fuzzy score +* @param {Object} b - second object representing a completion's fuzzy score +* @returns {number} - comparison result for array sorting +*/ +function sortClbk(a, b) { + if (b.score === a.score) { + return a.completion.length - b.completion.length; + } + return b.score - a.score; +} + +/** +* Checks if the completion is a fuzzy match for the input and returns a score representing the extent of the match. +* Based on a penalty based scoring system that charges a penalty for unfavourable characteristics in the completion string that make it less ideal of a match to the input string. * * @private * @param {string} completion - completion string * @param {string} input - input string -* @returns {boolean} - boolean indicating if a completion is a fuzzy match for the input string +* @returns {number} - a score (<=0) representing the extent of the match. Lower scores (negative) indicate a worse match. A score of 0 indicates a perfect match. +* +* @example +* // The following example demonstrates the penalty system. +* // The input `zz` is a substring of `pizza` in this case. +* // Penalties levied: not_start + ( 2 * gap_beginning ) +* // worstScore: input.length * missing_char = -38 , LENIENCY_FACTOR = 0.27 +* // Threshold: worstScore * LENIENCY_FACTOR = -10.26 +* var score = fuzzyMatch( 'pizza', 'zz' ); +* // returns -6.0 +* // score >= Threshold, it's a fuzzy match. */ function fuzzyMatch( completion, input ) { - var charPositions; - var finalScore; - var positions; - var distance; + var lastMatchedIndex; + var foundFirstMatch; + var completionIndex; + var gapsBetween; + var inputIndex; + var worstScore; var score; - var index; - var char; - var i; - var j; - // Return true for perfect prefix + worstScore = input.length * PENALTIES.missing_char; + + // If the input is an exact prefix of the completion, return a perfect score... if ( startsWith( completion, input ) ) { - return true; + return 0; + } + // If the completion is shorter than the input, don't match... + if ( completion.length < input.length ) { + return worstScore; + } + // If the input is just a single character, don't try to fuzzy match... + if ( input.length === 1 ) { + return worstScore; } - if ( completion.length >= input.length ) { - // Preprocess the completion string to get the positions of each character - positions = {}; - for ( i = 0; i < completion.length; i++ ) { - char = completion[ i ]; - if (!positions[ char ]) { - positions[ char] = []; - } - positions[ char ].push( i ); - } - - score = 0; - index = 0; - for ( i = 0; i < input.length; i++ ) { - charPositions = positions[ input[ i ] ]; - if ( !charPositions ) { - continue; - } + score = 0; + inputIndex = 0; + completionIndex = 0; + lastMatchedIndex = -1; // buffer to store the index in the completion string where the last match was found + gapsBetween = 0; + foundFirstMatch = false; // flag that tracks if we have found a single character from the input in the completion, so that we can check for `gap_between` penalties - // Find the next position of the character that is greater than or equal to index - j = 0; - while ( j < charPositions.length && charPositions[ j ] < index ) { - j += 1; - } - if ( j === charPositions.length ) { - continue; + // If the first character of the input and completion do not match, charge the penalty... + if ( lowercase( input[inputIndex] ) !== lowercase( completion[completionIndex] ) ) { // eslint-disable-line max-len + score += PENALTIES.not_start; + } + // Traverse the completion string looking for characters in the input (in the same order) + while ( inputIndex < input.length && completionIndex < completion.length ) { + if ( lowercase( input[inputIndex] ) === lowercase( completion[completionIndex] ) ) { // eslint-disable-line max-len + // If we find an input character match in the completion, check if the case doesn't match and charge a penalty accordingly... + if ( input[inputIndex] !== completion[completionIndex] ) { + score += PENALTIES.wrong_case; } - - // Subtract a penalty based on the distance between matching characters - distance = abs( charPositions[ j ] - i ); - score += max( 0, 1 - ( distance * 0.25 ) ); - index = charPositions[ j ] + 1; + foundFirstMatch = true; + lastMatchedIndex = completionIndex; + score += gapsBetween * PENALTIES.gap_between; + gapsBetween = 0; + inputIndex += 1; + } else if ( completionIndex + 1 === completion.length ) { + // Failed to find the input character in the completion string after full traversal, charge a penalty and check for the next character in the input string from the buffer index in the completion + score += PENALTIES.missing_char; + gapsBetween = 0; + inputIndex += 1; + completionIndex = lastMatchedIndex + 1; + } else if ( foundFirstMatch ) { + // If input and completion character do not match in this iteration and atleast one input character has been found before, track the gaps till the next match + gapsBetween += 1; + } else { + // If input and completion character do not match in the iteration and no input character is found yet, track the gaps till the first match + score += PENALTIES.gap_beginning; } + completionIndex += 1; - // Calculate relative score - finalScore = score / input.length; - - // A score above or equal to 0.7 indicates a match - return finalScore >= 0.7; + // Check for early rejections... + if ( score < LENIENCY_FACTOR * worstScore ) { + return worstScore; + } } - // Return false for completions smaller than the input - return false; + // Charge penalty for the input characters that are still remaining but the completion string is fully checked for input characters (e.g., input: abcd, completion: xayzb, charge penalty for the remaining c and d ) + score += ( input.length - inputIndex ) * PENALTIES.missing_char; + + return score; } @@ -109,12 +159,31 @@ function fuzzyMatch( completion, input ) { * @returns {Array} output array */ function filterByPrefix( out, arr, str ) { + var worstScore; + var results; + var score; var i; + + results = []; + worstScore = str.length * PENALTIES.missing_char; for ( i = 0; i < arr.length; i++ ) { - if ( fuzzyMatch( arr[ i ], str ) ) { - out.push( arr[ i ] ); + score = fuzzyMatch( arr[ i ], str ); + if ( score >= LENIENCY_FACTOR * worstScore ) { + // If completion is a fuzzy match, push the score and completion string + results.push({ + 'completion': arr[ i ], + 'score': score + }); } } + // Sort the results array in descending order of score, and in ascending order of item length when scores are equal + results.sort( sortClbk ); + + // Push resulting completions into the output array + for ( i = 0; i < results.length; i++ ) { + out.push( results[ i ].completion ); + } + return out; } From 0aee34dd6c73af6a688419218119918e63cd4720 Mon Sep 17 00:00:00 2001 From: Snehil Shah Date: Tue, 2 Apr 2024 12:38:30 +0000 Subject: [PATCH 05/24] feat: add support for highlighted completions in terminal mode Signed-off-by: Snehil Shah --- .../@stdlib/repl/lib/completer_engine.js | 116 +++++++++++++++--- 1 file changed, 100 insertions(+), 16 deletions(-) diff --git a/lib/node_modules/@stdlib/repl/lib/completer_engine.js b/lib/node_modules/@stdlib/repl/lib/completer_engine.js index 4b2023d310c1..e97c3981cd0a 100644 --- a/lib/node_modules/@stdlib/repl/lib/completer_engine.js +++ b/lib/node_modules/@stdlib/repl/lib/completer_engine.js @@ -24,9 +24,11 @@ var logger = require( 'debug' ); var setNonEnumerableReadOnly = require( '@stdlib/utils/define-nonenumerable-read-only-property' ); +var lowercase = require( '@stdlib/string/lowercase' ); var max = require( '@stdlib/stats/base/max' ); var floor = require( '@stdlib/math/base/special/floor' ); var repeat = require( '@stdlib/string/repeat' ); +var startsWith = require( '@stdlib/string/starts-with' ); var isEmptyString = require( '@stdlib/assert/is-empty-string' ).isPrimitive; var displayPrompt = require( './display_prompt.js' ); var commonPrefix = require( './longest_common_prefix.js' ); @@ -125,8 +127,7 @@ setNonEnumerableReadOnly( CompleterEngine.prototype, '_completionCallback', func * @returns {void} */ function clbk( error, completions ) { - var autocompletionSubstring; - var prefix; + var autoCompletion; // Check whether we encountered an error when generating completions... if ( error ) { @@ -134,21 +135,28 @@ setNonEnumerableReadOnly( CompleterEngine.prototype, '_completionCallback', func self._ostream.write( 'Error: couldn\'t generate tab completions' ); return; } - self._completionsList = completions[0].filter( isNotEmptyString ); // remove empty completions + self._completionsList = completions[ 0 ].filter( isNotEmptyString ); // remove empty completions if ( self._completionsList.length === 0 ) { debug( 'No completions to display.' ); return; } // Resolve a common prefix from the completion results: - prefix = commonPrefix( self._completionsList ); // e.g., [ 'back', 'background', 'backward' ] => 'back' - - // Extract auto-completion substring: - autocompletionSubstring = prefix.substring( commonPrefix( prefix, completions[1] ).length ); // eslint-disable-line max-len + 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 ( autocompletionSubstring !== '' ) { - debug( 'Found an auto-completion candidate: %s', prefix ); - self._rli.write( autocompletionSubstring ); + if ( autoCompletion !== '' && autoCompletion.length > completions[ 1 ].length ) { + debug( 'Found an auto-completion candidate: %s', autoCompletion ); + + // Clear the input line + self._ostream.write( repeat( '\x08', completions[1].length ) ); + self._rli.line = self._rli.line.slice( self._rli.cursor ); + + // Move the cursor to the start of the line + self._rli.cursor = 0; + + // Write the auto-completion string + self._rli.write( autoCompletion ); + return; } debug( 'No auto-completion candidate, displaying all possible completions.' ); @@ -161,7 +169,65 @@ setNonEnumerableReadOnly( CompleterEngine.prototype, '_completionCallback', func } }); +/** +* Highlights the matching parts of the completions based on the current line. +* +* @private +* @name _highlightCompletions +* @memberof CompleterEngine.prototype +* @returns {Array} array of highlighted completions +*/ +setNonEnumerableReadOnly( CompleterEngine.prototype, '_highlightCompletions', function highlightCompletions() { + var self = this; + + return this._completionsList.map( function highlightCompletion( completion ) { // eslint-disable-line max-len + var lastMatchedIndex = -1; + var completionIndex = 0; + var highlighted = ''; + var boldIndexes = []; // Buffer to store indexes of characters in completion string that needs to be highlighted + var lineIndex = 0; + var i; + + // If input is an exact prefix of completion, directly highlight the substring... + if ( startsWith( completion, self._line ) ) { + return '\x1b[1m' + completion.slice(0, self._line.length) + '\x1b[0m' + completion.slice(self._line.length); + } + + // Store indexes of each matching character in the completion string in the buffer... + while ( lineIndex < self._line.length && completionIndex < completion.length ) { // eslint-disable-line max-len + if ( lowercase( completion[ completionIndex ] ) === lowercase( self._line[ 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 ( i = 0; i < completion.length; i++ ) { + if ( boldIndexes.includes( i ) ) { + highlighted += '\x1b[1m' + completion[i] + '\x1b[0m'; + } else { + highlighted += completion[ i ]; + } + } + return highlighted; + }); +}); + +/** +* Displays the completions to the output stream. +* +* @private +* @name _displayCompletions +* @memberof CompleterEngine.prototype +* @returns {void} +*/ setNonEnumerableReadOnly( CompleterEngine.prototype, '_displayCompletions', function displayCompletions() { + var highlightedCompletions; var completionsLength; var widthOfColumn; var whitespaces; @@ -176,11 +242,18 @@ setNonEnumerableReadOnly( CompleterEngine.prototype, '_displayCompletions', func widthOfColumn = max( completionsLength.length, completionsLength, 1 ) + 4; // 4 space padding columns = floor( this._ostream.columns / widthOfColumn ) || 1; + // Highlight completions if operating in "terminal" mode... + if ( this.repl._isTTY ) { + highlightedCompletions = this._highlightCompletions(); + } else { + highlightedCompletions = this._completionsList; + } + output = '\r\n'; lineIndex = 0; whitespaces = 0; - for ( i = 0; i < this._completionsList.length; i++ ) { - completion = this._completionsList[i]; + for ( i = 0; i < highlightedCompletions.length; i++ ) { + completion = highlightedCompletions[ i ]; if ( lineIndex === columns ) { // Reached end of column, enter next line: output += '\r\n'; @@ -203,21 +276,32 @@ setNonEnumerableReadOnly( CompleterEngine.prototype, '_displayCompletions', func this._ostream.write( output ); }); +/** +* Callback which should be invoked **before** a "keypress" event is processed by a readline interface. +* +* @name beforeKeypress +* @memberof CompleterEngine.prototype +* @param {string} data - input data +* @param {Object} key - key object +* @returns {void} +*/ setNonEnumerableReadOnly( CompleterEngine.prototype, 'beforeKeypress', function beforeKeypress() { var cursor; - var input; var line; + var self = this; cursor = this._rli.cursor; line = this._rli.line; // Get the line before the cursor: - input = line.slice( 0, cursor ); + this._line = line.slice( 0, cursor ); // Pause the input stream before generating completions as the completer may be asynchronous... this._rli.pause(); - Promise.resolve( this._completer( input, this._onCompletions ) ) - .then( this._rli.resume.bind( this._rli ) ); + this._completer( this._line, function onCompletions( error, completions ) { + self._onCompletions( error, completions ); + self._rli.resume(); + }); }); From 7efe1a7e9bbfd84ca51694c4fd6220cd0f0d0ab4 Mon Sep 17 00:00:00 2001 From: Snehil Shah Date: Tue, 2 Apr 2024 12:57:52 +0000 Subject: [PATCH 06/24] fix: wrong completion previews displayed for fuzzy completion Signed-off-by: Snehil Shah --- lib/node_modules/@stdlib/repl/lib/completer_preview.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lib/node_modules/@stdlib/repl/lib/completer_preview.js b/lib/node_modules/@stdlib/repl/lib/completer_preview.js index 59d4ae1efdcb..e926329eb82a 100644 --- a/lib/node_modules/@stdlib/repl/lib/completer_preview.js +++ b/lib/node_modules/@stdlib/repl/lib/completer_preview.js @@ -26,6 +26,7 @@ var readline = require( 'readline' ); var logger = require( 'debug' ); var setNonEnumerableReadOnly = require( '@stdlib/utils/define-nonenumerable-read-only-property' ); var repeat = require( '@stdlib/string/repeat' ); +var startsWith = require( '@stdlib/string/starts-with' ); var commonPrefix = require( './longest_common_prefix.js' ); @@ -114,6 +115,11 @@ setNonEnumerableReadOnly( PreviewCompleter.prototype, '_completionCallback', fun debug( 'Unable to display a completion preview. Completion candidates have no common prefix.' ); return; } + // If the input is not an exact prefix of the completion candidate's common prefix, no completion preview to display, as the common prefix is a fuzzy match (not exact) to the input string hence cannot contruct by simple concatenation... + if ( !startsWith( prefix, completions[1] ) ) { + debug( 'Unable to display a completion preview. Common prefix is a fuzzy match to the input' ); + return; + } // Extract the completion preview substring (e.g., if the current line is 'ba', preview should be 'ck'): self._preview = prefix.substring( commonPrefix( prefix, completions[ 1 ] ).length ); // eslint-disable-line max-len From b0ca5c120d2701954cf4550aac339aa5a167d53a Mon Sep 17 00:00:00 2001 From: Snehil Shah Date: Wed, 3 Apr 2024 17:59:16 +0000 Subject: [PATCH 07/24] fix: suggestions Signed-off-by: Snehil Shah --- .../@stdlib/repl/lib/completer.js | 5 +- .../@stdlib/repl/lib/completer_engine.js | 129 +++++++++--------- 2 files changed, 69 insertions(+), 65 deletions(-) diff --git a/lib/node_modules/@stdlib/repl/lib/completer.js b/lib/node_modules/@stdlib/repl/lib/completer.js index 8fc88fa39b45..000e3b88407a 100644 --- a/lib/node_modules/@stdlib/repl/lib/completer.js +++ b/lib/node_modules/@stdlib/repl/lib/completer.js @@ -51,7 +51,6 @@ var debug = logger( 'repl:completer:callback' ); * @returns {Array} normalized completion list */ function normalize( list ) { - var uniqueList = []; var hash; var i; @@ -60,13 +59,13 @@ function normalize( list ) { for ( i = 0; i < list.length; i++ ) { if ( !hasOwnProp( hash, list[ i ] ) ) { hash[ list[ i ] ] = true; - uniqueList.push( list[ i ] ); } } + list = objectKeys( hash ); // TODO: sort such that printed columns are in lexicographic order, not rows, similar to bash behavior! - return uniqueList; + return list; } diff --git a/lib/node_modules/@stdlib/repl/lib/completer_engine.js b/lib/node_modules/@stdlib/repl/lib/completer_engine.js index e97c3981cd0a..95bb82448899 100644 --- a/lib/node_modules/@stdlib/repl/lib/completer_engine.js +++ b/lib/node_modules/@stdlib/repl/lib/completer_engine.js @@ -28,6 +28,7 @@ var lowercase = require( '@stdlib/string/lowercase' ); var max = require( '@stdlib/stats/base/max' ); var floor = require( '@stdlib/math/base/special/floor' ); var repeat = require( '@stdlib/string/repeat' ); +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 displayPrompt = require( './display_prompt.js' ); @@ -39,31 +40,6 @@ var commonPrefix = require( './longest_common_prefix.js' ); var debug = logger( 'repl:completer:engine' ); -// FUNCTIONS // - -/** -* Returns length of a completion string. -* -* @private -* @param {string} completion string -* @returns {number} length of completion string -*/ -function getLength( completion ) { - return completion.length; -} - -/** -* Checks if the completion string is not empty. -* -* @private -* @param {string} completion - completion string -* @returns {boolean} boolean indicating if completion is not an empty string -*/ -function isNotEmptyString( completion ) { - return !isEmptyString( completion ); -} - - // MAIN // /** @@ -128,6 +104,7 @@ setNonEnumerableReadOnly( CompleterEngine.prototype, '_completionCallback', func */ function clbk( error, completions ) { var autoCompletion; + var i; // Check whether we encountered an error when generating completions... if ( error ) { @@ -135,7 +112,13 @@ setNonEnumerableReadOnly( CompleterEngine.prototype, '_completionCallback', func self._ostream.write( 'Error: couldn\'t generate tab completions' ); return; } - self._completionsList = completions[ 0 ].filter( isNotEmptyString ); // remove empty completions + // 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.' ); return; @@ -178,44 +161,53 @@ setNonEnumerableReadOnly( CompleterEngine.prototype, '_completionCallback', func * @returns {Array} array of highlighted completions */ setNonEnumerableReadOnly( CompleterEngine.prototype, '_highlightCompletions', function highlightCompletions() { - var self = this; + var highlightedCompletions; + var lastMatchedIndex; + var completionIndex; + var highlighted; + var boldIndexes; + var completion; + var lineIndex; + var i; + var j; - return this._completionsList.map( function highlightCompletion( completion ) { // eslint-disable-line max-len - var lastMatchedIndex = -1; - var completionIndex = 0; - var highlighted = ''; - var boldIndexes = []; // Buffer to store indexes of characters in completion string that needs to be highlighted - var lineIndex = 0; - var i; + 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, self._line ) ) { - return '\x1b[1m' + completion.slice(0, self._line.length) + '\x1b[0m' + completion.slice(self._line.length); - } - - // Store indexes of each matching character in the completion string in the buffer... - while ( lineIndex < self._line.length && completionIndex < completion.length ) { // eslint-disable-line max-len - if ( lowercase( completion[ completionIndex ] ) === lowercase( self._line[ lineIndex ] ) ) { // eslint-disable-line max-len - boldIndexes.push(completionIndex); - lastMatchedIndex = completionIndex; - lineIndex += 1; - } else if ( completionIndex + 1 === completion.length ) { - lineIndex += 1; - completionIndex = lastMatchedIndex + 1; + if ( startsWith( completion, this._line ) ) { + highlighted = '\x1b[1m' + completion.slice( 0, this._line.length ) + '\x1b[0m' + completion.slice( this._line.length ); + } else { + // Store indexes of each matching character in the completion string in the buffer... + while ( lineIndex < this._line.length && completionIndex < completion.length ) { // eslint-disable-line max-len + if ( lowercase( completion[ completionIndex ] ) === lowercase( this._line[ 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; } - completionIndex += 1; - } - - // Highlight stored indexes in the completion string: - for ( i = 0; i < completion.length; i++ ) { - if ( boldIndexes.includes( i ) ) { - highlighted += '\x1b[1m' + completion[i] + '\x1b[0m'; - } else { - highlighted += completion[ i ]; + // Highlight stored indexes in the completion string: + for ( j = 0; j < completion.length; j++ ) { + if ( contains( boldIndexes, j ) ) { + highlighted += '\x1b[1m' + completion[ j ] + '\x1b[0m'; + } else { + highlighted += completion[ j ]; + } } } - return highlighted; - }); + highlightedCompletions.push( highlighted ); + } + return highlightedCompletions; }); /** @@ -238,7 +230,10 @@ setNonEnumerableReadOnly( CompleterEngine.prototype, '_displayCompletions', func var i; // Determine number of columns of completions that should be displayed to the output stream - completionsLength = this._completionsList.map( getLength ); + completionsLength = []; + for ( i = 0; i < this._completionsList.length; i++ ) { + completionsLength.push( this._completionsList[ i ].length ); + } widthOfColumn = max( completionsLength.length, completionsLength, 1 ) + 4; // 4 space padding columns = floor( this._ostream.columns / widthOfColumn ) || 1; @@ -298,10 +293,20 @@ setNonEnumerableReadOnly( CompleterEngine.prototype, 'beforeKeypress', function // Pause the input stream before generating completions as the completer may be asynchronous... this._rli.pause(); - this._completer( this._line, function onCompletions( error, completions ) { - self._onCompletions( error, completions ); + + /** + * Callback invoked upon resolving potential completions. + * + * @private + * @param {(Error|null)} error - error object + * @param {Array} completions - completion results + * @returns {void} + */ + function done(error, completions) { + self._onCompletions(error, completions); self._rli.resume(); - }); + } + this._completer( this._line, done ); }); From 7cc9270f2be7856b533f46c38f13e974e0962140 Mon Sep 17 00:00:00 2001 From: Snehil Shah Date: Fri, 5 Apr 2024 03:12:54 +0000 Subject: [PATCH 08/24] feat: new tab completions UI with navigation Signed-off-by: Snehil Shah --- .../@stdlib/repl/lib/completer_engine.js | 231 ++++++++++++++---- .../@stdlib/repl/lib/completer_preview.js | 15 +- lib/node_modules/@stdlib/repl/lib/main.js | 17 +- 3 files changed, 206 insertions(+), 57 deletions(-) diff --git a/lib/node_modules/@stdlib/repl/lib/completer_engine.js b/lib/node_modules/@stdlib/repl/lib/completer_engine.js index 95bb82448899..c69f77881df6 100644 --- a/lib/node_modules/@stdlib/repl/lib/completer_engine.js +++ b/lib/node_modules/@stdlib/repl/lib/completer_engine.js @@ -22,22 +22,39 @@ // 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 max = require( '@stdlib/stats/base/max' ); 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 contains = require( '@stdlib/array/base/assert/contains' ); var startsWith = require( '@stdlib/string/starts-with' ); var isEmptyString = require( '@stdlib/assert/is-empty-string' ).isPrimitive; -var displayPrompt = require( './display_prompt.js' ); 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 + + +// 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 // @@ -50,9 +67,10 @@ var debug = logger( 'repl:completer:engine' ); * @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 behaviour of the keypress * @returns {CompleterEngine} completer engine instance */ -function CompleterEngine( repl, completer, ostream ) { +function CompleterEngine( repl, completer, ostream, ttyWrite ) { if ( !(this instanceof CompleterEngine) ) { return new CompleterEngine( repl, completer, ostream ); } @@ -70,15 +88,32 @@ function CompleterEngine( repl, completer, 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; + // Create a callback for processing completions: this._onCompletions = this._completionCallback(); - // Initialize a buffer containing the current line being processed: - this._line = ''; + // 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 list of generated completions: this._completionsList = []; + // Initialize a buffer containing the list of highlighted completions: + this._highlightedCompletions = []; + + // Initialize a buffer storing the completion output's dimensions and indexes: + this._output = {}; + this._output.columns = -1; + this._output.rows = -1; + this._output.widthOfColumn = -1; + this._output.completionsLength = []; + this._output.index = -1; // track index of current completion + return this; } @@ -146,9 +181,10 @@ setNonEnumerableReadOnly( CompleterEngine.prototype, '_completionCallback', func // Display completions: self._displayCompletions(); + self.repl._isNavigatingCompletions = true; - // Re-display the prompt: - displayPrompt( self.repl, true ); + // Resume the input stream: + self._rli.resume(); } }); @@ -181,12 +217,12 @@ setNonEnumerableReadOnly( CompleterEngine.prototype, '_highlightCompletions', fu lineIndex = 0; // If input is an exact prefix of completion, directly highlight the substring... - if ( startsWith( completion, this._line ) ) { - highlighted = '\x1b[1m' + completion.slice( 0, this._line.length ) + '\x1b[0m' + completion.slice( this._line.length ); + if ( startsWith( completion, this._inputLine ) ) { + highlighted = '\u001b[1m' + completion.slice(0, this._inputLine.length) + '\u001b[0m' + completion.slice(this._inputLine.length); } else { // Store indexes of each matching character in the completion string in the buffer... - while ( lineIndex < this._line.length && completionIndex < completion.length ) { // eslint-disable-line max-len - if ( lowercase( completion[ completionIndex ] ) === lowercase( this._line[ lineIndex ] ) ) { // eslint-disable-line max-len + while ( lineIndex < this._inputLine.length && completionIndex < completion.length ) { // eslint-disable-line max-len + if ( lowercase( completion[ completionIndex ] ) === lowercase( this._inputLine[ lineIndex ] ) ) { // eslint-disable-line max-len boldIndexes.push( completionIndex ); lastMatchedIndex = completionIndex; lineIndex += 1; @@ -199,7 +235,7 @@ setNonEnumerableReadOnly( CompleterEngine.prototype, '_highlightCompletions', fu // Highlight stored indexes in the completion string: for ( j = 0; j < completion.length; j++ ) { if ( contains( boldIndexes, j ) ) { - highlighted += '\x1b[1m' + completion[ j ] + '\x1b[0m'; + highlighted += '\u001b[1m' + completion[ j ] + '\u001b[0m'; } else { highlighted += completion[ j ]; } @@ -219,37 +255,84 @@ setNonEnumerableReadOnly( CompleterEngine.prototype, '_highlightCompletions', fu * @returns {void} */ setNonEnumerableReadOnly( CompleterEngine.prototype, '_displayCompletions', function displayCompletions() { - var highlightedCompletions; - var completionsLength; - var widthOfColumn; var whitespaces; var completion; var lineIndex; - var columns; var output; var i; // Determine number of columns of completions that should be displayed to the output stream - completionsLength = []; + this._output.completionsLength = []; for ( i = 0; i < this._completionsList.length; i++ ) { - completionsLength.push( this._completionsList[ i ].length ); + this._output.completionsLength.push( this._completionsList[ i ].length ); // eslint-disable-line max-len } - widthOfColumn = max( completionsLength.length, completionsLength, 1 ) + 4; // 4 space padding - columns = floor( this._ostream.columns / widthOfColumn ) || 1; + this._output.widthOfColumn = max( this._output.completionsLength.length, this._output.completionsLength, 1 ) + 4; // eslint-disable-line max-len + this._output.columns = floor( this._ostream.columns / this._output.widthOfColumn ) || 1; // eslint-disable-line max-len + this._output.rows = ceil( this._completionsList.length / this._output.columns ); // eslint-disable-line max-len // Highlight completions if operating in "terminal" mode... if ( this.repl._isTTY ) { - highlightedCompletions = this._highlightCompletions(); + this._highlightedCompletions = this._highlightCompletions(); } else { - highlightedCompletions = this._completionsList; + this._highlightedCompletions = this._completionsList; + } + + output = '\r\n'; + lineIndex = 0; + whitespaces = 0; + for ( i = 0; i < this._highlightedCompletions.length; i++ ) { + completion = this._highlightedCompletions[ i ]; + if ( lineIndex === this._output.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 ); + } + // Add completion string to the column in output: + output += completion; + whitespaces = this._output.widthOfColumn - this._output.completionsLength[ i ]; // eslint-disable-line max-len + lineIndex += 1; } + // Ensure a space between completions and the following prompt... + if ( lineIndex !== 0 ) { + output += '\r\n\r\n'; + } + // Write completions to the output stream: + this._ostream.write( output ); + + // Bring the cursor back to the current line: + readline.moveCursor( this._ostream, 0, -1 * ( 2 + this._output.rows ) ); + readline.cursorTo( this._ostream, this._rli.cursor + this.repl._inputPrompt.length - 1 ); // eslint-disable-line max-len +}); + +/** +* Re-displays the navigated completions to the output stream. +* +* @private +* @name _navigateCompletions +* @memberof CompleterEngine.prototype +* @returns {void} +*/ +setNonEnumerableReadOnly( CompleterEngine.prototype, '_navigateCompletions', function navigateCompletions() { + var whitespaces; + var completion; + var lineIndex; + var output; + var i; + // Clear current completions output: + readline.clearScreenDown( this._ostream ); + + // Create the completions output after navigation: output = '\r\n'; lineIndex = 0; whitespaces = 0; - for ( i = 0; i < highlightedCompletions.length; i++ ) { - completion = highlightedCompletions[ i ]; - if ( lineIndex === columns ) { + for ( i = 0; i < this._highlightedCompletions.length; i++ ) { + completion = this._highlightedCompletions[ i ]; + if ( lineIndex === this._output.columns ) { // Reached end of column, enter next line: output += '\r\n'; lineIndex = 0; @@ -258,9 +341,13 @@ setNonEnumerableReadOnly( CompleterEngine.prototype, '_displayCompletions', func // Fill the space to move to the next column: output += repeat( ' ', whitespaces ); } + if ( i === this._output.index && this.repl._isTTY ) { + completion = stripANSI( completion ); + completion = '\u001b[7m' + completion + '\u001b[27m'; + } // Add completion string to the column in output: output += completion; - whitespaces = widthOfColumn - completionsLength[i]; + whitespaces = this._output.widthOfColumn - this._output.completionsLength[ i ]; // eslint-disable-line max-len lineIndex += 1; } // Ensure a space between completions and the following prompt... @@ -269,6 +356,16 @@ setNonEnumerableReadOnly( CompleterEngine.prototype, '_displayCompletions', func } // Write completions to the output stream: this._ostream.write( output ); + + // Bring the cursor back to the current line: + readline.moveCursor( this._ostream, 0, -1 * ( 2 + this._output.rows ) ); + readline.cursorTo( this._ostream, this.repl._inputPrompt.length - 1 ); + this._rli.cursor = 0; + + // Insert the current suggestion in the line, that the user navigates to: + readline.clearLine( this._ostream, 1 ); + this._rli.line = ''; + this._rli.write( ( this._completionsList[ this._output.index ] || this._inputLine ) + this._remainingLine ); // eslint-disable-line max-len }); /** @@ -280,33 +377,83 @@ setNonEnumerableReadOnly( CompleterEngine.prototype, '_displayCompletions', func * @param {Object} key - key object * @returns {void} */ -setNonEnumerableReadOnly( CompleterEngine.prototype, 'beforeKeypress', function beforeKeypress() { +setNonEnumerableReadOnly( CompleterEngine.prototype, 'beforeKeypress', function beforeKeypress( data, key ) { var cursor; var line; - var self = this; + + if ( !key ) { + this._ttyWrite.call( this._rli, data, key ); + return; + } + // If the completions output is visible, allow navigating it... + if ( this.repl._isNavigatingCompletions ) { + if ( key.name === 'tab' ) { + // Stop navigating and hide the completions output: + readline.clearScreenDown( this._ostream ); + this.repl._isNavigatingCompletions = false; + return; + } + if ( key.name === 'down' ) { + // Move to the next row... + if ( this._output.index === -1 ) { + this._output.index = 0; + this._navigateCompletions(); + } else if ( this._output.index + this._output.columns <= this._completionsList.length ) { // eslint-disable-line max-len + this._output.index += this._output.columns; + this._navigateCompletions(); + } + } else if ( key.name === 'up' ) { + // Move to the previous row or if already on the line, stop navigating and trigger default behaviour... + if ( this._output.index === -1 ) { + readline.clearScreenDown( this._ostream ); + this.repl._isNavigatingCompletions = false; + this._ttyWrite.call( this._rli, data, key ); + return; + } + if ( this._output.index - this._output.columns >= 0 ) { + this._output.index -= this._output.columns; + } else { + this._output.index = -1; + } + this._navigateCompletions(); + } else if ( key.name === 'left' ) { + // Move back an index... + if ( this._output.index > 0 ) { + this._output.index -= 1; + } + this._navigateCompletions(); + } else if ( key.name === 'right' ) { + // Move ahead an index... + if ( this._output.index < this._completionsList.length - 1 ) { + this._output.index += 1; + } + this._navigateCompletions(); + } else { + // For any other keypress, stop navigating and continue default behaviour... + readline.clearScreenDown( this._ostream ); + this.repl._isNavigatingCompletions = false; + this._ttyWrite.call( this._rli, data, key ); + } + return; + } + // For other keypresses, don't trigger TAB completions: + if ( key.name !== 'tab' ) { + this._ttyWrite.call( this._rli, data, key ); + return; + } cursor = this._rli.cursor; line = this._rli.line; // Get the line before the cursor: - this._line = 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(); - - /** - * Callback invoked upon resolving potential completions. - * - * @private - * @param {(Error|null)} error - error object - * @param {Array} completions - completion results - * @returns {void} - */ - function done(error, completions) { - self._onCompletions(error, completions); - self._rli.resume(); - } - this._completer( this._line, done ); + this._completer( this._inputLine, this._onCompletions ); }); diff --git a/lib/node_modules/@stdlib/repl/lib/completer_preview.js b/lib/node_modules/@stdlib/repl/lib/completer_preview.js index e926329eb82a..6c9e16f788a7 100644 --- a/lib/node_modules/@stdlib/repl/lib/completer_preview.js +++ b/lib/node_modules/@stdlib/repl/lib/completer_preview.js @@ -42,19 +42,22 @@ var debug = logger( 'repl:completer:preview' ); * * @private * @constructor -* @param {Object} rli - readline instance +* @param {REPL} repl - REPL instance * @param {Function} completer - function for generating possible completions * @param {WritableStream} ostream - writable stream * @returns {PreviewCompleter} completer instance */ -function PreviewCompleter( rli, completer, ostream ) { +function PreviewCompleter( repl, completer, ostream ) { if ( !(this instanceof PreviewCompleter) ) { - return new PreviewCompleter( rli, completer, ostream ); + return new PreviewCompleter( repl, completer, ostream ); } debug( 'Creating a preview completer...' ); + // Cache a reference to the provided REPL instance: + this.repl = repl; + // Cache a reference to the provided readline interface: - this._rli = rli; + this._rli = repl._rli; // Cache a reference to the output writable stream: this._ostream = ostream; @@ -205,6 +208,10 @@ setNonEnumerableReadOnly( PreviewCompleter.prototype, 'beforeKeypress', function if ( !key || this._preview === '' ) { return; } + // When the user is naviating completions, don't preview completions... + if ( this.repl._isNavigatingCompletions ) { + return; + } // Handle the case where the user is not at the end of the line... if ( this._rli.cursor !== this._rli.line.length ) { // If a user is in the middle of a line and presses ENTER, clear the preview string, as the preview was not accepted prior to executing the expression... diff --git a/lib/node_modules/@stdlib/repl/lib/main.js b/lib/node_modules/@stdlib/repl/lib/main.js index f30fe067d646..85e48b1631cc 100644 --- a/lib/node_modules/@stdlib/repl/lib/main.js +++ b/lib/node_modules/@stdlib/repl/lib/main.js @@ -111,7 +111,6 @@ function REPL( options ) { var previewCompleter; var completerEngine; var completer; - var ttyWrite; var opts; var self; var err; @@ -211,6 +210,9 @@ function REPL( options ) { // Initialize an internal flag indicating whether a user is entering a multi-line command: setNonEnumerable( this, '_multiline_mode', false ); + // Initialize an internal flag indicating whether a user is navigating TAB completions: + setNonEnumerable( this, '_isNavigatingCompletions', false ); + // Initialize an internal flag indicating whether the REPL has been closed: setNonEnumerable( this, '_closed', false ); @@ -242,7 +244,7 @@ function REPL( options ) { })); // Create a new TAB completer engine: - completerEngine = new CompleterEngine( self, completer, this._ostream ); + completerEngine = new CompleterEngine( self, completer, this._ostream, this._rli._ttyWrite ); // eslint-disable-line max-len this._rli.on( 'close', onClose ); this._rli.on( 'line', onLine ); @@ -254,7 +256,7 @@ function REPL( options ) { // If operating in "terminal" mode, initialize a preview completer... if ( this._isTTY ) { // Create a new preview completer: - previewCompleter = new PreviewCompleter( this._rli, completer, this._ostream ); + previewCompleter = new PreviewCompleter( self, completer, this._ostream ); // eslint-disable-line max-len // Instruct the input stream to begin emitting "keypress" events: readline.emitKeypressEvents( this._istream, this._rli ); @@ -262,9 +264,6 @@ function REPL( options ) { // Add a listener for "keypress" events: this._istream.on( 'keypress', onKeypress ); - // Cache a reference to the private readline interface `ttyWrite` to allow calling the method when wanting default behavior: - ttyWrite = this._rli._ttyWrite; - // Overwrite the private `ttyWrite` method to allow processing input before a "keypress" event is triggered: this._rli._ttyWrite = beforeKeypress; // WARNING: overwriting a private property } @@ -293,12 +292,8 @@ function REPL( options ) { * @param {Object} key - key object */ function beforeKeypress( data, key ) { - if ( key && key.name === 'tab' ) { - completerEngine.beforeKeypress(); - return; - } previewCompleter.beforeKeypress( data, key ); - ttyWrite.call( self._rli, data, key ); + completerEngine.beforeKeypress( data, key ); } /** From 24da634b8e53a1cb132f9d6c5b6f882a2d151c32 Mon Sep 17 00:00:00 2001 From: Snehil Shah Date: Fri, 5 Apr 2024 06:36:14 +0000 Subject: [PATCH 09/24] refactor: move `fuzzyMatch` to a module & don't fuzzy match for previews Signed-off-by: Snehil Shah --- .../@stdlib/repl/lib/complete_expression.js | 62 ++++++- .../@stdlib/repl/lib/complete_fs.js | 49 +++++- .../@stdlib/repl/lib/complete_require.js | 92 +++++++++- .../@stdlib/repl/lib/complete_tutorial.js | 26 ++- .../@stdlib/repl/lib/complete_workspace.js | 26 ++- .../@stdlib/repl/lib/completer.js | 13 +- .../@stdlib/repl/lib/completer_engine.js | 10 +- .../@stdlib/repl/lib/completer_preview.js | 8 +- .../@stdlib/repl/lib/filter_by_prefix.js | 166 +++--------------- .../@stdlib/repl/lib/fuzzy_match.js | 143 +++++++++++++++ .../repl/lib/sort_fuzzy_completions.js | 66 +++++++ 11 files changed, 490 insertions(+), 171 deletions(-) create mode 100644 lib/node_modules/@stdlib/repl/lib/fuzzy_match.js create mode 100644 lib/node_modules/@stdlib/repl/lib/sort_fuzzy_completions.js diff --git a/lib/node_modules/@stdlib/repl/lib/complete_expression.js b/lib/node_modules/@stdlib/repl/lib/complete_expression.js index 02628f344e78..e22f7df30c8d 100644 --- a/lib/node_modules/@stdlib/repl/lib/complete_expression.js +++ b/lib/node_modules/@stdlib/repl/lib/complete_expression.js @@ -16,6 +16,8 @@ * limitations under the License. */ +/* eslint-disable max-statements */ + 'use strict'; // MODULES // @@ -32,6 +34,7 @@ var filterByPrefix = require( './filter_by_prefix.js' ); var findLast = require( './complete_walk_find_last.js' ); var resolveLocalScopes = require( './resolve_local_scopes.js' ); var resolveLocalScope = require( './resolve_local_scope.js' ); +var sortFuzzyCompletions = require( './sort_fuzzy_completions.js' ); var RESERVED_KEYWORDS_COMMON = require( './reserved_keywords_common.js' ); @@ -52,9 +55,11 @@ var AOPTS = { * @param {Array} out - output array for storing completions * @param {Object} context - REPL context * @param {string} expression - expression to complete +* @param {boolean} isFuzzy - boolean indicating if the completions should be fuzzy * @returns {string} filter */ -function complete( out, context, expression ) { +function complete( out, context, expression, isFuzzy ) { + var fuzzyResults = []; var filter; var script; var node; @@ -62,6 +67,7 @@ function complete( out, context, expression ) { var ast; var obj; var res; + var i; // Case: `<|>` (a command devoid of expressions/statements) if ( trim( expression ) === '' ) { @@ -100,7 +106,19 @@ function complete( out, context, expression ) { } filter = node.expression.name; debug( 'Identifier auto-completion. Filter: %s', filter ); - out = filterByPrefix( out, RESERVED_KEYWORDS_COMMON.concat(objectKeys(context), ast.locals), filter ); // eslint-disable-line max-len + out = filterByPrefix( out, RESERVED_KEYWORDS_COMMON, filter, false ); + out = filterByPrefix( out, objectKeys( context ), filter, false ); + out = filterByPrefix( out, ast.locals, filter, false ); + out.sort(); + if ( isFuzzy ) { + fuzzyResults = filterByPrefix( fuzzyResults, RESERVED_KEYWORDS_COMMON, filter, true ); // eslint-disable-line max-len + fuzzyResults = filterByPrefix( fuzzyResults, objectKeys( context ), filter, true ); // eslint-disable-line max-len + fuzzyResults = filterByPrefix( fuzzyResults, ast.locals, filter, true ); // eslint-disable-line max-len + fuzzyResults = sortFuzzyCompletions( fuzzyResults ); + for ( i = 0; i < fuzzyResults.length; i++ ) { + out.push( fuzzyResults[ i ] ); + } + } return filter; } // Find the identifier or member expression to be completed: @@ -175,7 +193,14 @@ function complete( out, context, expression ) { // Case: `foo['<|>` || `foo['bar<|>` if ( node.property.type === 'Literal' ) { filter = node.property.value.toString(); // handles numeric literals - out = filterByPrefix( out, propertyNamesIn( obj ), filter ); + out = filterByPrefix( out, propertyNamesIn( obj ), filter, false ); // eslint-disable-line max-len + if ( isFuzzy ) { + fuzzyResults = filterByPrefix( fuzzyResults, propertyNamesIn( obj ), filter, true ); // eslint-disable-line max-len + fuzzyResults = sortFuzzyCompletions( fuzzyResults ); + for ( i = 0; i < fuzzyResults.length; i++ ) { + out.push( fuzzyResults[ i ] ); + } + } return filter; } // Case: `foo[<|>` || `foo[bar<|>` @@ -188,7 +213,14 @@ function complete( out, context, expression ) { else { filter = node.property.name; } - out = filterByPrefix( out, objectKeys( context ), filter ); + out = filterByPrefix( out, objectKeys( context ), filter, false ); // eslint-disable-line max-len + if ( isFuzzy ) { + fuzzyResults = filterByPrefix( fuzzyResults, objectKeys( context ), filter, true ); // eslint-disable-line max-len + fuzzyResults = sortFuzzyCompletions( fuzzyResults ); + for ( i = 0; i < fuzzyResults.length; i++ ) { + out.push( fuzzyResults[ i ] ); + } + } return filter; } // Case: `foo[bar.<|>` || `foo[bar.beep<|>` || `foo[bar.beep.<|>` || `foo[bar[beep<|>` || etc @@ -214,13 +246,31 @@ function complete( out, context, expression ) { filter = node.property.name; } debug( 'Property auto-completion. Filter: %s', filter ); - out = filterByPrefix( out, propertyNamesIn( obj ), filter ); + out = filterByPrefix( out, propertyNamesIn( obj ), filter, false ); + if ( isFuzzy ) { + fuzzyResults = filterByPrefix( fuzzyResults, propertyNamesIn( obj ), filter, true ); // eslint-disable-line max-len + fuzzyResults = sortFuzzyCompletions( fuzzyResults ); + for ( i = 0; i < fuzzyResults.length; i++ ) { + out.push( fuzzyResults[ i ] ); + } + } return filter; } // Case: `foo<|>` (completing an identifier) filter = ( node.name === '✖' ) ? '' : node.name; debug( 'Identifier auto-completion. Filter: %s', filter ); - out = filterByPrefix( out, res.keywords.concat( objectKeys( context ), resolveLocalScope( ast, node ) ), filter ); // eslint-disable-line max-len + out = filterByPrefix( out, res.keywords, filter, false ); + out = filterByPrefix( out, objectKeys( context ), filter, false ); + out = filterByPrefix( out, resolveLocalScope( ast, node ), filter, false ); + if ( isFuzzy ) { + fuzzyResults = filterByPrefix( fuzzyResults, res.keywords, filter, true ); // eslint-disable-line max-len + fuzzyResults = filterByPrefix( fuzzyResults, objectKeys( context ), filter, true ); // eslint-disable-line max-len + fuzzyResults = filterByPrefix( fuzzyResults, resolveLocalScope( ast, node ), filter, true ); // eslint-disable-line max-len + fuzzyResults = sortFuzzyCompletions( fuzzyResults ); + for ( i = 0; i < fuzzyResults.length; i++ ) { + out.push( fuzzyResults[ i ] ); + } + } return filter; } diff --git a/lib/node_modules/@stdlib/repl/lib/complete_fs.js b/lib/node_modules/@stdlib/repl/lib/complete_fs.js index d4d70bef5f48..390077b492e1 100644 --- a/lib/node_modules/@stdlib/repl/lib/complete_fs.js +++ b/lib/node_modules/@stdlib/repl/lib/complete_fs.js @@ -30,6 +30,8 @@ var readDir = require( '@stdlib/fs/read-dir' ).sync; var startsWith = require( '@stdlib/string/starts-with' ); var pathRegExp = require( './regexp_path.js' ); var fsAliasArgs = require( './fs_alias_args.js' ); +var fuzzyMatch = require( './fuzzy_match.js' ); +var sortFuzzyCompletions = require( './sort_fuzzy_completions.js' ); // VARIABLES // @@ -50,13 +52,16 @@ var AOPTS = { * @param {string} expression - expression to complete * @param {string} alias - file system API alias * @param {string} path - path to complete +* @param {boolean} isFuzzy - boolean indicating if the completions should be fuzzy * @returns {string} path filter */ -function complete( out, expression, alias, path ) { +function complete( out, expression, alias, path, isFuzzy ) { + var fuzzyResults = []; var filter; var subdir; var files; var stats; + var match; var args; var ast; var arg; @@ -128,6 +133,48 @@ function complete( out, expression, alias, path ) { continue; } } + out.sort(); + if ( !isFuzzy ) { + return filter; + } + // Start searching for fuzzy completions... + debug( 'Searching path for fuzzy completions...' ); + for ( i = 0; i < files.length; i++ ) { + f = files[ i ]; + match = fuzzyMatch( f, filter ); + if ( !match ) { + debug( '%s does not match fuzzy filter %s. Skipping...', f, filter ); + continue; + } + f = resolve( dir, f ); + debug( 'Examining path: %s', f ); + try { + stats = statSync( f ); + if ( stats.isDirectory() ) { + debug( 'Path resolves to a subdirectory.' ); + fuzzyResults.push({ + 'score': match.score, + 'completion': match.completion + '/' + }); + debug( 'Found a fuzzy completion: %s', fuzzyResults[ fuzzyResults.length-1 ].completion ); + } else if ( stats.isFile() ) { + debug( 'Path resolves to a file.' ); + fuzzyResults.push( match ); + debug( 'Found a fuzzy completion: %s', fuzzyResults[ fuzzyResults.length-1 ].completion ); + } else { + debug( 'Path resolves to neither a directory nor a file. Skipping path...' ); + continue; + } + } catch ( err ) { + debug( 'Error: %s', err.message ); + debug( 'Skipping path...' ); + continue; + } + } + fuzzyResults = sortFuzzyCompletions( fuzzyResults ); + for ( i = 0; i < fuzzyResults.length; i++ ) { + out.push( fuzzyResults[ i ] ); + } return filter; } diff --git a/lib/node_modules/@stdlib/repl/lib/complete_require.js b/lib/node_modules/@stdlib/repl/lib/complete_require.js index 03888ac53a42..bd9a4d1f1de4 100644 --- a/lib/node_modules/@stdlib/repl/lib/complete_require.js +++ b/lib/node_modules/@stdlib/repl/lib/complete_require.js @@ -16,6 +16,8 @@ * limitations under the License. */ +/* eslint-disable max-statements */ + 'use strict'; // MODULES // @@ -27,10 +29,12 @@ var readDir = require( '@stdlib/fs/read-dir' ).sync; var startsWith = require( '@stdlib/string/starts-with' ); var extname = require( '@stdlib/utils/extname' ); var cwd = require( '@stdlib/process/cwd' ); -var indexRegExp = require( './regexp_index.js' ); // eslint-disable-line stdlib/no-require-index +var indexRegExp = require( './regexp_index.js' ); var relativePathRegExp = require( './regexp_relative_require_path.js' ); var pathRegExp = require( './regexp_path.js' ); var contains = require( './contains.js' ); +var fuzzyMatch = require( './fuzzy_match.js' ); +var sortFuzzyCompletions = require( './sort_fuzzy_completions.js' ); // VARIABLES // @@ -49,14 +53,17 @@ var RE_RELATIVE = relativePathRegExp(); * @param {string} path - path to complete * @param {Array} paths - module search paths * @param {Array} exts - supported `require` extensions +* @param {boolean} isFuzzy - boolean indicating if the completions should be fuzzy * @returns {string} path filter */ -function complete( out, path, paths, exts ) { +function complete( out, path, paths, exts, isFuzzy ) { + var fuzzyResults = []; var filter; var sfiles; var subdir; var files; var stats; + var match; var dir; var ext; var re; @@ -157,6 +164,87 @@ function complete( out, path, paths, exts ) { } } } + out.sort(); + if ( !isFuzzy ) { + return filter; + } + // Start searching for fuzzy completions... + debug( 'Searching paths for fuzzy completions: %s', paths.join( ', ' ) ); + for ( i = 0; i < paths.length; i++ ) { + // Resolve the subdirectory path to a file system path: + dir = resolve( paths[ i ], subdir ); + debug( 'Resolved directory: %s', dir ); + + debug( 'Reading directory contents...' ); + files = readDir( dir ); + if ( files instanceof Error ) { + debug( 'Unable to read directory: %s. Error: %s', dir, files.message ); + continue; + } + for ( j = 0; j < files.length; j++ ) { + f = files[ j ]; + match = fuzzyMatch( f, filter ); + if ( !match ) { + debug( '%s does not fuzzy match filter %s. Skipping...', f, filter ); + continue; + } + f = resolve( dir, f ); + debug( 'Examining path: %s', f ); + try { + stats = statSync( f ); + if ( stats.isDirectory() ) { + debug( 'Path resolves to a subdirectory.' ); + fuzzyResults.push({ + 'score': match.score, + 'completion': match.completion + '/' + }); + debug( 'Found a fuzzy completion: %s', fuzzyResults[ fuzzyResults.length-1 ].completion ); + + debug( 'Reading subdirectory contents...' ); + sfiles = readDir( f ); + if ( sfiles instanceof Error ) { + debug( 'Unable to read subdirectory: %s. Error: %s', f, sfiles.message ); + continue; + } + for ( k = 0; k < sfiles.length; k++ ) { + if ( re.test( sfiles[ k ] ) ) { + // Since the subdirectory contains an `index` file, one can simply "require" the subdirectory, thus eliding the full file path: + debug( 'Subdirectory contains an `index` file.' ); + + fuzzyResults.push( match ); + debug( 'Found a fuzzy completion: %s', fuzzyResults[ fuzzyResults.length-1 ].completion ); + } else if ( sfiles[ k ] === 'package.json' ) { + // Since the subdirectory contains a `package.json` file, we **ASSUME** one can simply "require" the subdirectory, thus eliding the full file path (WARNING: we do NOT explicitly check that the main entry point actually exists!): + debug( 'Subdirectory contains a `package.json` file.' ); + + fuzzyResults.push( match ); + debug( 'Found a fuzzy completion: %s', fuzzyResults[ fuzzyResults.length-1 ].completion ); + } + } + } else if ( stats.isFile() ) { + debug( 'Path resolves to a file.' ); + ext = extname( files[ j ] ); + if ( contains( exts.length, exts, 1, 0, ext ) ) { + debug( 'File has supported extension: %s', ext ); + + fuzzyResults.push( match ); + debug( 'Found a fuzzy completion: %s', fuzzyResults[ fuzzyResults.length-1 ].completion ); + } + } else { + debug( 'Path resolves to neither a directory nor a file. Skipping path...' ); + continue; + } + } catch ( err ) { + debug( 'Error: %s', err.message ); + debug( 'Skipping path...' ); + continue; + } + } + } + fuzzyResults = sortFuzzyCompletions( fuzzyResults ); + for ( i = 0; i < fuzzyResults.length; i++ ) { + out.push( fuzzyResults[ i ] ); + } return filter; } diff --git a/lib/node_modules/@stdlib/repl/lib/complete_tutorial.js b/lib/node_modules/@stdlib/repl/lib/complete_tutorial.js index d66e1be6ca39..3db5f7273a4b 100644 --- a/lib/node_modules/@stdlib/repl/lib/complete_tutorial.js +++ b/lib/node_modules/@stdlib/repl/lib/complete_tutorial.js @@ -26,6 +26,8 @@ var startsWith = require( '@stdlib/string/starts-with' ); var objectKeys = require( '@stdlib/utils/keys' ); var tutorialAliasArgs = require( './tutorial_alias_args.js' ); var TUTORIALS = require( './repl_docs.js' ).tutorial; +var fuzzyMatch = require('./fuzzy_match.js'); +var sortFuzzyCompletions = require( './sort_fuzzy_completions.js' ); // VARIABLES // @@ -48,9 +50,12 @@ var AOPTS = { * @param {string} expression - expression to complete * @param {string} alias - tutorial API alias * @param {string} value - value to complete +* @param {boolean} isFuzzy - boolean indicating if the completions should be fuzzy * @returns {string} value filter */ -function complete( out, repl, expression, alias, value ) { +function complete( out, repl, expression, alias, value, isFuzzy ) { + var fuzzyResults; + var match; var args; var ast; var arg; @@ -84,6 +89,25 @@ function complete( out, repl, expression, alias, value ) { debug( '%s does not match filter %s. Skipping...', t, value ); } } + out.sort(); + if ( !isFuzzy ) { + return value; + } + debug( 'Fuzzy searching for completion candidates...' ); + for ( i = 0; i < TUTS.length; i++ ) { + t = TUTS[ i ]; + match = fuzzyMatch( t, value ); + if ( match ) { + debug( 'Found a fuzzy completion: %s', t ); + fuzzyResults.push( t ); + } else { + debug( '%s does not match fuzzy filter %s. Skipping...', t, value ); + } + } + fuzzyResults = sortFuzzyCompletions( fuzzyResults ); + for ( i = 0; i < fuzzyResults.length; i++ ) { + out.push( fuzzyResults[ i ] ); + } return value; } diff --git a/lib/node_modules/@stdlib/repl/lib/complete_workspace.js b/lib/node_modules/@stdlib/repl/lib/complete_workspace.js index d818c4884057..4d136cff3b97 100644 --- a/lib/node_modules/@stdlib/repl/lib/complete_workspace.js +++ b/lib/node_modules/@stdlib/repl/lib/complete_workspace.js @@ -24,6 +24,8 @@ var logger = require( 'debug' ); var parse = require( 'acorn-loose' ).parse; var startsWith = require( '@stdlib/string/starts-with' ); var workspaceAliasArgs = require( './workspace_alias_args.js' ); +var fuzzyMatch = require( './fuzzy_match.js' ); +var sortFuzzyCompletions = require( './sort_fuzzy_completions.js' ); // VARIABLES // @@ -45,9 +47,12 @@ var AOPTS = { * @param {string} expression - expression to complete * @param {string} alias - workspace API alias * @param {string} value - value to complete +* @param {boolean} isFuzzy - boolean indicating if the completions should be fuzzy * @returns {string} value filter */ -function complete( out, repl, expression, alias, value ) { +function complete( out, repl, expression, alias, value, isFuzzy ) { + var fuzzyResults = []; + var match; var args; var ast; var arg; @@ -85,6 +90,25 @@ function complete( out, repl, expression, alias, value ) { debug( '%s does not match filter %s. Skipping...', w, value ); } } + out.sort(); + if ( !isFuzzy ) { + return value; + } + debug( 'Fuzzy searching for completion candidates...' ); + for ( i = 0; i < ws.length; i++ ) { + w = ws[ i ]; + match = fuzzyMatch( w, value ); + if ( match ) { + debug( 'Found a fuzzy completion: %s', w ); + fuzzyResults.push( w ); + } else { + debug( '%s does not match fuzzy filter %s. Skipping...', w, value ); + } + } + fuzzyResults = sortFuzzyCompletions( fuzzyResults ); + for ( i = 0; i < fuzzyResults.length; i++ ) { + out.push( fuzzyResults[ i ] ); + } return value; } diff --git a/lib/node_modules/@stdlib/repl/lib/completer.js b/lib/node_modules/@stdlib/repl/lib/completer.js index 000e3b88407a..3ca40082b98f 100644 --- a/lib/node_modules/@stdlib/repl/lib/completer.js +++ b/lib/node_modules/@stdlib/repl/lib/completer.js @@ -87,9 +87,10 @@ function completer( repl ) { * @private * @param {string} line - current line * @param {Function} clbk - completion callback + * @param {boolean} isFuzzy - boolean indicating if the completions should be fuzzy * @returns {void} */ - function complete( line, clbk ) { + function complete( line, clbk, isFuzzy ) { var match; var exts; var res; @@ -107,7 +108,7 @@ function completer( repl ) { exts = objectKeys( repl._context.require.extensions ); debug( 'Supported `require` filename extensions: %s', exts.join( ', ' ) ); - line = completeRequire( res, match[ 1 ], repl._context.module.paths, exts ); // eslint-disable-line max-len + line = completeRequire( res, match[ 1 ], repl._context.module.paths, exts, isFuzzy ); // eslint-disable-line max-len res = normalize( res ); debug( 'Completion filter: %s', line ); @@ -122,7 +123,7 @@ function completer( repl ) { debug( 'Expression: %s', match[ 0 ] ); debug( 'File system API: %s', match[ 1 ] ); debug( 'Path to complete: %s', match[ 3 ] ); - line = completeFS( res, match[ 0 ], match[ 1 ], match[ 3 ] ); + line = completeFS( res, match[ 0 ], match[ 1 ], match[ 3 ], isFuzzy ); // eslint-disable-line max-len res = normalize( res ); debug( 'Completion filter: %s', line ); @@ -137,7 +138,7 @@ function completer( repl ) { debug( 'Expression: %s', match[ 0 ] ); debug( 'Workspace API: %s', match[ 1 ] ); debug( 'Value to complete: %s', match[ 3 ] ); - line = completeWorkspace( res, repl, match[ 0 ], match[ 1 ], match[ 3 ] ); // eslint-disable-line max-len + line = completeWorkspace( res, repl, match[ 0 ], match[ 1 ], match[ 3 ], isFuzzy ); // eslint-disable-line max-len res = normalize( res ); debug( 'Completion filter: %s', line ); @@ -152,7 +153,7 @@ function completer( repl ) { debug( 'Expression: %s', match[ 0 ] ); debug( 'Tutorial API: %s', match[ 1 ] ); debug( 'Value to complete: %s', match[ 3 ] ); - line = completeTutorial( res, repl, match[ 0 ], match[ 1 ], match[ 3 ] ); // eslint-disable-line max-len + line = completeTutorial( res, repl, match[ 0 ], match[ 1 ], match[ 3 ], isFuzzy ); // eslint-disable-line max-len res = normalize( res ); debug( 'Completion filter: %s', line ); @@ -160,7 +161,7 @@ function completer( repl ) { return clbk( null, [ res, line ] ); } debug( 'Attempting to complete an incomplete expression.' ); - line = completeExpression( res, repl._context, line ); + line = completeExpression( res, repl._context, line, isFuzzy ); res = normalize( res ); debug( 'Results: %s', res.join( ', ' ) ); return clbk( null, [ res, line ] ); diff --git a/lib/node_modules/@stdlib/repl/lib/completer_engine.js b/lib/node_modules/@stdlib/repl/lib/completer_engine.js index c69f77881df6..7b58f943d381 100644 --- a/lib/node_modules/@stdlib/repl/lib/completer_engine.js +++ b/lib/node_modules/@stdlib/repl/lib/completer_engine.js @@ -145,6 +145,9 @@ setNonEnumerableReadOnly( CompleterEngine.prototype, '_completionCallback', func 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: @@ -156,6 +159,9 @@ setNonEnumerableReadOnly( CompleterEngine.prototype, '_completionCallback', func } if ( self._completionsList.length === 0 ) { debug( 'No completions to display.' ); + + // Resume the input stream: + self._rli.resume(); return; } // Resolve a common prefix from the completion results: @@ -175,6 +181,8 @@ setNonEnumerableReadOnly( CompleterEngine.prototype, '_completionCallback', func // 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.' ); @@ -453,7 +461,7 @@ setNonEnumerableReadOnly( CompleterEngine.prototype, 'beforeKeypress', function // Pause the input stream before generating completions as the completer may be asynchronous... this._rli.pause(); - this._completer( this._inputLine, this._onCompletions ); + this._completer( this._inputLine, this._onCompletions, true ); }); diff --git a/lib/node_modules/@stdlib/repl/lib/completer_preview.js b/lib/node_modules/@stdlib/repl/lib/completer_preview.js index 6c9e16f788a7..2b1ace0930bb 100644 --- a/lib/node_modules/@stdlib/repl/lib/completer_preview.js +++ b/lib/node_modules/@stdlib/repl/lib/completer_preview.js @@ -26,7 +26,6 @@ var readline = require( 'readline' ); var logger = require( 'debug' ); var setNonEnumerableReadOnly = require( '@stdlib/utils/define-nonenumerable-read-only-property' ); var repeat = require( '@stdlib/string/repeat' ); -var startsWith = require( '@stdlib/string/starts-with' ); var commonPrefix = require( './longest_common_prefix.js' ); @@ -118,11 +117,6 @@ setNonEnumerableReadOnly( PreviewCompleter.prototype, '_completionCallback', fun debug( 'Unable to display a completion preview. Completion candidates have no common prefix.' ); return; } - // If the input is not an exact prefix of the completion candidate's common prefix, no completion preview to display, as the common prefix is a fuzzy match (not exact) to the input string hence cannot contruct by simple concatenation... - if ( !startsWith( prefix, completions[1] ) ) { - debug( 'Unable to display a completion preview. Common prefix is a fuzzy match to the input' ); - return; - } // Extract the completion preview substring (e.g., if the current line is 'ba', preview should be 'ck'): self._preview = prefix.substring( commonPrefix( prefix, completions[ 1 ] ).length ); // eslint-disable-line max-len @@ -192,7 +186,7 @@ setNonEnumerableReadOnly( PreviewCompleter.prototype, 'clear', function clear() * @returns {void} */ setNonEnumerableReadOnly( PreviewCompleter.prototype, 'onKeypress', function onKeypress() { - this._completer( this._rli.line, this._onCompletions ); + this._completer( this._rli.line, this._onCompletions, false ); }); /** diff --git a/lib/node_modules/@stdlib/repl/lib/filter_by_prefix.js b/lib/node_modules/@stdlib/repl/lib/filter_by_prefix.js index cfa1e3395584..e60ddc3b5d57 100644 --- a/lib/node_modules/@stdlib/repl/lib/filter_by_prefix.js +++ b/lib/node_modules/@stdlib/repl/lib/filter_by_prefix.js @@ -21,130 +21,7 @@ // MODULES // var startsWith = require( '@stdlib/string/starts-with' ); -var lowercase = require( '@stdlib/string/lowercase' ); - - -// VARIABLES // - -var PENALTIES = { - 'wrong_case': -1, // Penalty for a matching character that is in the wrong case - 'not_start': -4, // Penalty if the first letter of the completion and input are different - 'gap_between': -8, // Penalty for a gap between matching characters of input in the completion - 'gap_beginning': -1, // Penalty for a gap, before a character from input is encountered - 'missing_char': -19 // Penalty for a character in input that doesn't exist in the completion (to tackle possible spelling mistakes) -}; -var LENIENCY_FACTOR = 0.27; // Controls how lenient the algorithm is - - -// FUNCTIONS // - -/** -* Callback to sort the results array in descending order of score, and in ascending order of item length when scores are equal. -* -* @private -* @param {Object} a - first object representing a completion's fuzzy score -* @param {Object} b - second object representing a completion's fuzzy score -* @returns {number} - comparison result for array sorting -*/ -function sortClbk(a, b) { - if (b.score === a.score) { - return a.completion.length - b.completion.length; - } - return b.score - a.score; -} - -/** -* Checks if the completion is a fuzzy match for the input and returns a score representing the extent of the match. -* Based on a penalty based scoring system that charges a penalty for unfavourable characteristics in the completion string that make it less ideal of a match to the input string. -* -* @private -* @param {string} completion - completion string -* @param {string} input - input string -* @returns {number} - a score (<=0) representing the extent of the match. Lower scores (negative) indicate a worse match. A score of 0 indicates a perfect match. -* -* @example -* // The following example demonstrates the penalty system. -* // The input `zz` is a substring of `pizza` in this case. -* // Penalties levied: not_start + ( 2 * gap_beginning ) -* // worstScore: input.length * missing_char = -38 , LENIENCY_FACTOR = 0.27 -* // Threshold: worstScore * LENIENCY_FACTOR = -10.26 -* var score = fuzzyMatch( 'pizza', 'zz' ); -* // returns -6.0 -* // score >= Threshold, it's a fuzzy match. -*/ -function fuzzyMatch( completion, input ) { - var lastMatchedIndex; - var foundFirstMatch; - var completionIndex; - var gapsBetween; - var inputIndex; - var worstScore; - var score; - - worstScore = input.length * PENALTIES.missing_char; - - // If the input is an exact prefix of the completion, return a perfect score... - if ( startsWith( completion, input ) ) { - return 0; - } - // If the completion is shorter than the input, don't match... - if ( completion.length < input.length ) { - return worstScore; - } - // If the input is just a single character, don't try to fuzzy match... - if ( input.length === 1 ) { - return worstScore; - } - - score = 0; - inputIndex = 0; - completionIndex = 0; - lastMatchedIndex = -1; // buffer to store the index in the completion string where the last match was found - gapsBetween = 0; - foundFirstMatch = false; // flag that tracks if we have found a single character from the input in the completion, so that we can check for `gap_between` penalties - - // If the first character of the input and completion do not match, charge the penalty... - if ( lowercase( input[inputIndex] ) !== lowercase( completion[completionIndex] ) ) { // eslint-disable-line max-len - score += PENALTIES.not_start; - } - // Traverse the completion string looking for characters in the input (in the same order) - while ( inputIndex < input.length && completionIndex < completion.length ) { - if ( lowercase( input[inputIndex] ) === lowercase( completion[completionIndex] ) ) { // eslint-disable-line max-len - // If we find an input character match in the completion, check if the case doesn't match and charge a penalty accordingly... - if ( input[inputIndex] !== completion[completionIndex] ) { - score += PENALTIES.wrong_case; - } - foundFirstMatch = true; - lastMatchedIndex = completionIndex; - score += gapsBetween * PENALTIES.gap_between; - gapsBetween = 0; - inputIndex += 1; - } else if ( completionIndex + 1 === completion.length ) { - // Failed to find the input character in the completion string after full traversal, charge a penalty and check for the next character in the input string from the buffer index in the completion - score += PENALTIES.missing_char; - gapsBetween = 0; - inputIndex += 1; - completionIndex = lastMatchedIndex + 1; - } else if ( foundFirstMatch ) { - // If input and completion character do not match in this iteration and atleast one input character has been found before, track the gaps till the next match - gapsBetween += 1; - } else { - // If input and completion character do not match in the iteration and no input character is found yet, track the gaps till the first match - score += PENALTIES.gap_beginning; - } - completionIndex += 1; - - // Check for early rejections... - if ( score < LENIENCY_FACTOR * worstScore ) { - return worstScore; - } - } - - // Charge penalty for the input characters that are still remaining but the completion string is fully checked for input characters (e.g., input: abcd, completion: xayzb, charge penalty for the remaining c and d ) - score += ( input.length - inputIndex ) * PENALTIES.missing_char; - - return score; -} +var fuzzyMatch = require( './fuzzy_match.js' ); // MAIN // @@ -156,34 +33,31 @@ function fuzzyMatch( completion, input ) { * @param {Array} out - output array * @param {Array} arr - source array * @param {string} str - string filter +* @param {boolean} isFuzzy - boolean indicating if the completions should be fuzzy * @returns {Array} output array */ -function filterByPrefix( out, arr, str ) { - var worstScore; - var results; - var score; +function filterByPrefix( out, arr, str, isFuzzy ) { + var fuzzyResults = []; + var match; var i; - results = []; - worstScore = str.length * PENALTIES.missing_char; - for ( i = 0; i < arr.length; i++ ) { - score = fuzzyMatch( arr[ i ], str ); - if ( score >= LENIENCY_FACTOR * worstScore ) { - // If completion is a fuzzy match, push the score and completion string - results.push({ - 'completion': arr[ i ], - 'score': score - }); + if ( isFuzzy ) { + for ( i = 0; i < arr.length; i++ ) { + match = fuzzyMatch( arr[ i ], str ); + if ( match ) { + fuzzyResults.push( match ); + } + } + for ( i = 0; i < fuzzyResults.length; i++ ) { + out.push( fuzzyResults[ i ] ); + } + } else { + for ( i = 0; i < arr.length; i++ ) { + if ( startsWith( arr[ i ], str ) ) { + out.push( arr[ i ] ); + } } } - // Sort the results array in descending order of score, and in ascending order of item length when scores are equal - results.sort( sortClbk ); - - // Push resulting completions into the output array - for ( i = 0; i < results.length; i++ ) { - out.push( results[ i ].completion ); - } - return out; } diff --git a/lib/node_modules/@stdlib/repl/lib/fuzzy_match.js b/lib/node_modules/@stdlib/repl/lib/fuzzy_match.js new file mode 100644 index 000000000000..ebe23ae19c71 --- /dev/null +++ b/lib/node_modules/@stdlib/repl/lib/fuzzy_match.js @@ -0,0 +1,143 @@ +/** +* @license Apache-2.0 +* +* Copyright (c) 2024 The Stdlib Authors. +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +'use strict'; + +// MODULES // + +var startsWith = require( '@stdlib/string/starts-with' ); +var lowercase = require( '@stdlib/string/lowercase' ); + + +// VARIABLES // + +var PENALTIES = { + 'wrong_case': -1, // Penalty for a matching character that is in the wrong case + 'not_start': -4, // Penalty if the first letter of the completion and input are different + 'gap_between': -8, // Penalty for a gap between matching characters of input in the completion + 'gap_beginning': -1, // Penalty for a gap, before a character from input is encountered + 'missing_char': -19 // Penalty for a character in input that doesn't exist in the completion (to tackle possible spelling mistakes) +}; +var LENIENCY_FACTOR = 0.27; // Controls how lenient the algorithm is + + +// MAIN // + +/** +* Checks if the completion is strictly a fuzzy match for the input and returns a score representing the extent of the match. +* Based on a penalty based scoring system that charges a penalty for unfavourable characteristics in the completion string that make it less ideal of a match to the input string. +* +* @private +* @param {string} completion - completion string +* @param {string} input - input string +* @returns {boolean|Object} - false if not a fuzzy match. if a fuzzy match, an object with the completion and score (<=0) representing the extent of the match. Lower scores (negative) indicate a worse match. A score of 0 indicates a perfect match. +* +* @example +* // The following example demonstrates the penalty system. +* // The input `zz` is a substring of `pizza` in this case. +* // Penalties levied: not_start + ( 2 * gap_beginning ) +* // worstScore: input.length * missing_char = -38 , LENIENCY_FACTOR = 0.27 +* // Threshold: worstScore * LENIENCY_FACTOR = -10.26 +* var score = fuzzyMatch( 'pizza', 'zz' ); +* // returns { 'score': -6.0, 'completion': 'pizza' } +* // score >= Threshold, it's a fuzzy match. +*/ +function fuzzyMatch( completion, input ) { + var lastMatchedIndex; + var foundFirstMatch; + var completionIndex; + var gapsBetween; + var inputIndex; + var worstScore; + var score; + + worstScore = input.length * PENALTIES.missing_char; + + // If the input is an exact prefix of the completion, reject it as it will be handled by the non-fuzzy logic... + if ( startsWith( completion, input ) ) { + return false; + } + // If the completion is shorter than the input, don't match... + if ( completion.length < input.length ) { + return false; + } + // If the input is just a single character, don't try to fuzzy match... + if ( input.length === 1 ) { + return false; + } + + score = 0; + inputIndex = 0; + completionIndex = 0; + lastMatchedIndex = -1; // buffer to store the index in the completion string where the last match was found + gapsBetween = 0; + foundFirstMatch = false; // flag that tracks if we have found a single character from the input in the completion, so that we can check for `gap_between` penalties + + // If the first character of the input and completion do not match, charge the penalty... + if ( lowercase( input[inputIndex] ) !== lowercase( completion[completionIndex] ) ) { // eslint-disable-line max-len + score += PENALTIES.not_start; + } + // Traverse the completion string looking for characters in the input (in the same order) + while ( inputIndex < input.length && completionIndex < completion.length ) { + if ( lowercase( input[inputIndex] ) === lowercase( completion[completionIndex] ) ) { // eslint-disable-line max-len + // If we find an input character match in the completion, check if the case doesn't match and charge a penalty accordingly... + if ( input[inputIndex] !== completion[completionIndex] ) { + score += PENALTIES.wrong_case; + } + foundFirstMatch = true; + lastMatchedIndex = completionIndex; + score += gapsBetween * PENALTIES.gap_between; + gapsBetween = 0; + inputIndex += 1; + } else if ( completionIndex + 1 === completion.length ) { + // Failed to find the input character in the completion string after full traversal, charge a penalty and check for the next character in the input string from the buffer index in the completion + score += PENALTIES.missing_char; + gapsBetween = 0; + inputIndex += 1; + completionIndex = lastMatchedIndex + 1; + } else if ( foundFirstMatch ) { + // If input and completion character do not match in this iteration and atleast one input character has been found before, track the gaps till the next match + gapsBetween += 1; + } else { + // If input and completion character do not match in the iteration and no input character is found yet, track the gaps till the first match + score += PENALTIES.gap_beginning; + } + completionIndex += 1; + + // Check for early rejections... + if ( score < LENIENCY_FACTOR * worstScore ) { + return false; + } + } + + // Charge penalty for the input characters that are still remaining but the completion string is fully checked for input characters (e.g., input: abcd, completion: xayzb, charge penalty for the remaining c and d ) + score += ( input.length - inputIndex ) * PENALTIES.missing_char; + + if ( score < LENIENCY_FACTOR * worstScore ) { + return false; + } + return { + 'score': score, + 'completion': completion + }; +} + + +// EXPORTS // + +module.exports = fuzzyMatch; diff --git a/lib/node_modules/@stdlib/repl/lib/sort_fuzzy_completions.js b/lib/node_modules/@stdlib/repl/lib/sort_fuzzy_completions.js new file mode 100644 index 000000000000..101ebc447344 --- /dev/null +++ b/lib/node_modules/@stdlib/repl/lib/sort_fuzzy_completions.js @@ -0,0 +1,66 @@ +/** +* @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'; + +// FUNCTIONS // + +/** +* Callback to sort the results array in descending order of score, and lexicographically when scores are equal. +* +* @private +* @param {Object} a - first object representing a completion's fuzzy score +* @param {Object} b - second object representing a completion's fuzzy score +* @returns {number} - comparison result for array sorting +*/ +function sortClbk( a, b ) { + if ( b.score === a.score ) { + // If scores are equal, sort lexicographically... + return a.completion.localeCompare( b.completion ); + } + // Sort by score: + return b.score - a.score; +} + + +// MAIN // + +/** +* Sorts an array of completion objects and returns an array of sorted completion strings. +* +* @private +* @param {Array} completions - array of objects, each containing a 'score' and a 'completion' property +* @returns {Array} - array of sorted completion strings. +*/ +function sortFuzzyCompletions( completions ) { + var out = []; + var i; + + // Sort and return the completions: + completions.sort( sortClbk ); + for ( i = 0; i < completions.length; i++ ) { + out.push( completions[ i ].completion ); + } + + return out; +} + + +// EXPORTS // + +module.exports = sortFuzzyCompletions; From 68180558f7f7796d4f609fbb8d0b5cd7e6ec4a82 Mon Sep 17 00:00:00 2001 From: Snehil Shah Date: Fri, 5 Apr 2024 08:17:08 +0000 Subject: [PATCH 10/24] fix: changes requested Signed-off-by: Snehil Shah --- .../@stdlib/repl/lib/completer_preview.js | 15 ++++----------- lib/node_modules/@stdlib/repl/lib/main.js | 6 ++++-- .../@stdlib/repl/lib/sort_fuzzy_completions.js | 2 +- 3 files changed, 9 insertions(+), 14 deletions(-) diff --git a/lib/node_modules/@stdlib/repl/lib/completer_preview.js b/lib/node_modules/@stdlib/repl/lib/completer_preview.js index 2b1ace0930bb..a08150fbdedc 100644 --- a/lib/node_modules/@stdlib/repl/lib/completer_preview.js +++ b/lib/node_modules/@stdlib/repl/lib/completer_preview.js @@ -41,22 +41,19 @@ var debug = logger( 'repl:completer:preview' ); * * @private * @constructor -* @param {REPL} repl - REPL instance +* @param {Object} rli - readline interface * @param {Function} completer - function for generating possible completions * @param {WritableStream} ostream - writable stream * @returns {PreviewCompleter} completer instance */ -function PreviewCompleter( repl, completer, ostream ) { +function PreviewCompleter( rli, completer, ostream ) { if ( !(this instanceof PreviewCompleter) ) { - return new PreviewCompleter( repl, completer, ostream ); + return new PreviewCompleter( rli, completer, ostream ); } debug( 'Creating a preview completer...' ); - // Cache a reference to the provided REPL instance: - this.repl = repl; - // Cache a reference to the provided readline interface: - this._rli = repl._rli; + this._rli = rli; // Cache a reference to the output writable stream: this._ostream = ostream; @@ -202,10 +199,6 @@ setNonEnumerableReadOnly( PreviewCompleter.prototype, 'beforeKeypress', function if ( !key || this._preview === '' ) { return; } - // When the user is naviating completions, don't preview completions... - if ( this.repl._isNavigatingCompletions ) { - return; - } // Handle the case where the user is not at the end of the line... if ( this._rli.cursor !== this._rli.line.length ) { // If a user is in the middle of a line and presses ENTER, clear the preview string, as the preview was not accepted prior to executing the expression... diff --git a/lib/node_modules/@stdlib/repl/lib/main.js b/lib/node_modules/@stdlib/repl/lib/main.js index 85e48b1631cc..7cc5d0e364f2 100644 --- a/lib/node_modules/@stdlib/repl/lib/main.js +++ b/lib/node_modules/@stdlib/repl/lib/main.js @@ -256,7 +256,7 @@ function REPL( options ) { // If operating in "terminal" mode, initialize a preview completer... if ( this._isTTY ) { // Create a new preview completer: - previewCompleter = new PreviewCompleter( self, completer, this._ostream ); // eslint-disable-line max-len + previewCompleter = new PreviewCompleter( self._rli, completer, this._ostream ); // eslint-disable-line max-len // Instruct the input stream to begin emitting "keypress" events: readline.emitKeypressEvents( this._istream, this._rli ); @@ -292,7 +292,9 @@ function REPL( options ) { * @param {Object} key - key object */ function beforeKeypress( data, key ) { - previewCompleter.beforeKeypress( data, key ); + if ( !this._isNavigatingCompletions ) { + previewCompleter.beforeKeypress( data, key ); + } completerEngine.beforeKeypress( data, key ); } diff --git a/lib/node_modules/@stdlib/repl/lib/sort_fuzzy_completions.js b/lib/node_modules/@stdlib/repl/lib/sort_fuzzy_completions.js index 101ebc447344..a16f1263d1e6 100644 --- a/lib/node_modules/@stdlib/repl/lib/sort_fuzzy_completions.js +++ b/lib/node_modules/@stdlib/repl/lib/sort_fuzzy_completions.js @@ -45,7 +45,7 @@ function sortClbk( a, b ) { * * @private * @param {Array} completions - array of objects, each containing a 'score' and a 'completion' property -* @returns {Array} - array of sorted completion strings. +* @returns {Array} - array of sorted completion strings */ function sortFuzzyCompletions( completions ) { var out = []; From 0c8b11a746f1eb3f5bd402995b4f14023f54e3d7 Mon Sep 17 00:00:00 2001 From: Snehil Shah Date: Fri, 5 Apr 2024 11:17:04 +0000 Subject: [PATCH 11/24] style: lint merged changes 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 f4e7fbfaaa5d..a31369e0d960 100644 --- a/lib/node_modules/@stdlib/repl/lib/main.js +++ b/lib/node_modules/@stdlib/repl/lib/main.js @@ -254,8 +254,8 @@ function REPL( options ) { 'prompt': opts.inputPrompt })); - // Create a new TAB completer engine: - setNonEnumerableReadOnly( this, '_completerEngine', new CompleterEngine( this, this._completer, this._ostream, this._rli._ttyWrite ); + // Create a new TAB completer engine: + setNonEnumerableReadOnly( this, '_completerEngine', new CompleterEngine( this, this._completer, this._ostream, this._rli._ttyWrite ) ); // Create a new auto-closer: setNonEnumerableReadOnly( this, '_autoCloser', new AutoCloser( this._rli ) ); From 6b7dc99dfe9c25bca4a59febc5a0a943fd944b43 Mon Sep 17 00:00:00 2001 From: Snehil Shah Date: Fri, 5 Apr 2024 11:37:33 +0000 Subject: [PATCH 12/24] feat: add a setting to control fuzzy completions Signed-off-by: Snehil Shah --- lib/node_modules/@stdlib/repl/lib/completer_engine.js | 5 ++++- lib/node_modules/@stdlib/repl/lib/defaults.js | 5 ++++- lib/node_modules/@stdlib/repl/lib/settings.js | 4 ++++ 3 files changed, 12 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 7b58f943d381..77a23d4de3b6 100644 --- a/lib/node_modules/@stdlib/repl/lib/completer_engine.js +++ b/lib/node_modules/@stdlib/repl/lib/completer_engine.js @@ -91,6 +91,9 @@ function CompleterEngine( repl, completer, ostream, ttyWrite ) { // 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 settings to determine the type of completions: + this._settings = repl._settings; + // Create a callback for processing completions: this._onCompletions = this._completionCallback(); @@ -461,7 +464,7 @@ setNonEnumerableReadOnly( CompleterEngine.prototype, 'beforeKeypress', function // Pause the input stream before generating completions as the completer may be asynchronous... this._rli.pause(); - this._completer( this._inputLine, this._onCompletions, true ); + this._completer( this._inputLine, this._onCompletions, this._settings.fuzzyCompletions ); // eslint-disable-line max-len }); diff --git a/lib/node_modules/@stdlib/repl/lib/defaults.js b/lib/node_modules/@stdlib/repl/lib/defaults.js index a9ddd5c657a6..34237f25e8d5 100644 --- a/lib/node_modules/@stdlib/repl/lib/defaults.js +++ b/lib/node_modules/@stdlib/repl/lib/defaults.js @@ -87,7 +87,10 @@ function defaults() { 'autoDeletePairs': true, // Flag indicating whether to enable the display of completion previews for auto-completion (note: default depends on whether TTY): - 'completionPreviews': void 0 + 'completionPreviews': void 0, + + // Flag indicating whether to enable fuzzy matching in TAB completions: + 'fuzzyCompletions': true } }; } diff --git a/lib/node_modules/@stdlib/repl/lib/settings.js b/lib/node_modules/@stdlib/repl/lib/settings.js index caa2e40de201..ce55504c071e 100644 --- a/lib/node_modules/@stdlib/repl/lib/settings.js +++ b/lib/node_modules/@stdlib/repl/lib/settings.js @@ -39,6 +39,10 @@ var SETTINGS = { 'completionPreviews': { 'desc': 'Enable the display of completion previews for auto-completion.', 'type': 'boolean' + }, + 'fuzzyCompletions': { + 'desc': 'Include fuzzy results in TAB completions.', + 'type': 'boolean' } }; From 237fb7fe04093ac9f959c85377a591ded9db2325 Mon Sep 17 00:00:00 2001 From: Snehil Shah Date: Fri, 5 Apr 2024 16:01:13 +0000 Subject: [PATCH 13/24] fix: limit height of completions & fix completions with prefixes Signed-off-by: Snehil Shah --- .../@stdlib/repl/lib/completer_engine.js | 51 ++++++++++++++----- 1 file changed, 38 insertions(+), 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 77a23d4de3b6..3d0fa7bb9f0e 100644 --- a/lib/node_modules/@stdlib/repl/lib/completer_engine.js +++ b/lib/node_modules/@stdlib/repl/lib/completer_engine.js @@ -27,10 +27,12 @@ var logger = require( 'debug' ); var setNonEnumerableReadOnly = require( '@stdlib/utils/define-nonenumerable-read-only-property' ); var lowercase = require( '@stdlib/string/lowercase' ); var max = require( '@stdlib/stats/base/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; @@ -103,6 +105,9 @@ function CompleterEngine( repl, completer, ostream, ttyWrite ) { // 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 = []; @@ -167,19 +172,21 @@ setNonEnumerableReadOnly( CompleterEngine.prototype, '_completionCallback', func 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 > completions[ 1 ].length ) { + if ( autoCompletion !== '' && autoCompletion.length > self._completionPrefix.length ) { debug( 'Found an auto-completion candidate: %s', autoCompletion ); - // Clear the input line - self._ostream.write( repeat( '\x08', completions[1].length ) ); - self._rli.line = self._rli.line.slice( self._rli.cursor ); + // 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 the line - self._rli.cursor = 0; + // Move the cursor to the start of completion prefix: + self._rli.cursor -= self._completionPrefix.length; // Write the auto-completion string self._rli.write( autoCompletion ); @@ -228,12 +235,12 @@ setNonEnumerableReadOnly( CompleterEngine.prototype, '_highlightCompletions', fu lineIndex = 0; // If input is an exact prefix of completion, directly highlight the substring... - if ( startsWith( completion, this._inputLine ) ) { - highlighted = '\u001b[1m' + completion.slice(0, this._inputLine.length) + '\u001b[0m' + completion.slice(this._inputLine.length); + 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._inputLine.length && completionIndex < completion.length ) { // eslint-disable-line max-len - if ( lowercase( completion[ completionIndex ] ) === lowercase( this._inputLine[ lineIndex ] ) ) { // eslint-disable-line max-len + 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; @@ -279,7 +286,7 @@ setNonEnumerableReadOnly( CompleterEngine.prototype, '_displayCompletions', func } this._output.widthOfColumn = max( this._output.completionsLength.length, this._output.completionsLength, 1 ) + 4; // eslint-disable-line max-len this._output.columns = floor( this._ostream.columns / this._output.widthOfColumn ) || 1; // eslint-disable-line max-len - this._output.rows = ceil( this._completionsList.length / this._output.columns ); // eslint-disable-line max-len + this._output.rows = min( ceil( this._completionsList.length / this._output.columns ), this._ostream.rows - 5 ); // eslint-disable-line max-len // Highlight completions if operating in "terminal" mode... if ( this.repl._isTTY ) { @@ -294,6 +301,10 @@ setNonEnumerableReadOnly( CompleterEngine.prototype, '_displayCompletions', func for ( i = 0; i < this._highlightedCompletions.length; i++ ) { completion = this._highlightedCompletions[ i ]; if ( lineIndex === this._output.columns ) { + // If completions start overflowing terminal height, stop writing to output... + if ( ceil( i / this._output.columns ) >= this._output.rows ) { + break; + } // Reached end of column, enter next line: output += '\r\n'; lineIndex = 0; @@ -344,6 +355,10 @@ setNonEnumerableReadOnly( CompleterEngine.prototype, '_navigateCompletions', fun for ( i = 0; i < this._highlightedCompletions.length; i++ ) { completion = this._highlightedCompletions[ i ]; if ( lineIndex === this._output.columns ) { + // If completions start overflowing the terminal, stop writing to output... + if ( ceil( i / this._output.columns ) >= this._output.rows ) { + break; + } // Reached end of column, enter next line: output += '\r\n'; lineIndex = 0; @@ -373,10 +388,12 @@ setNonEnumerableReadOnly( CompleterEngine.prototype, '_navigateCompletions', fun readline.cursorTo( this._ostream, this.repl._inputPrompt.length - 1 ); this._rli.cursor = 0; - // Insert the current suggestion in the line, that the user navigates to: + // Insert the navigated suggestion to the current line: readline.clearLine( this._ostream, 1 ); this._rli.line = ''; - this._rli.write( ( this._completionsList[ this._output.index ] || this._inputLine ) + this._remainingLine ); // eslint-disable-line max-len + this._rli.write( removeLast( this._inputLine, this._completionPrefix.length ) + ( this._completionsList[ this._output.index ] || this._completionPrefix ) + this._remainingLine ); // eslint-disable-line max-len + readline.moveCursor( this._ostream, -1 * this._remainingLine.length ); + this._rli.cursor -= this._remainingLine.length; }); /** @@ -401,6 +418,12 @@ setNonEnumerableReadOnly( CompleterEngine.prototype, 'beforeKeypress', function if ( key.name === 'tab' ) { // Stop navigating and hide the completions output: readline.clearScreenDown( this._ostream ); + + // 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 + + this._output.index = -1; this.repl._isNavigatingCompletions = false; return; } @@ -417,6 +440,7 @@ setNonEnumerableReadOnly( CompleterEngine.prototype, 'beforeKeypress', function // Move to the previous row or if already on the line, stop navigating and trigger default behaviour... if ( this._output.index === -1 ) { readline.clearScreenDown( this._ostream ); + this._output.index = -1; this.repl._isNavigatingCompletions = false; this._ttyWrite.call( this._rli, data, key ); return; @@ -442,6 +466,7 @@ setNonEnumerableReadOnly( CompleterEngine.prototype, 'beforeKeypress', function } else { // For any other keypress, stop navigating and continue default behaviour... readline.clearScreenDown( this._ostream ); + this._output.index = -1; this.repl._isNavigatingCompletions = false; this._ttyWrite.call( this._rli, data, key ); } From 576f40288950f3283d3490e5054e42774a9b9d59 Mon Sep 17 00:00:00 2001 From: Snehil Shah Date: Sat, 6 Apr 2024 10:22:49 +0000 Subject: [PATCH 14/24] feat: fuzzy completions only when no exact completions Signed-off-by: Snehil Shah --- .../@stdlib/repl/lib/complete_expression.js | 85 +++++++++++-------- .../@stdlib/repl/lib/complete_fs.js | 6 +- .../@stdlib/repl/lib/complete_require.js | 6 +- .../@stdlib/repl/lib/complete_settings.js | 28 +++++- .../@stdlib/repl/lib/complete_tutorial.js | 6 +- .../@stdlib/repl/lib/complete_workspace.js | 6 +- .../@stdlib/repl/lib/completer.js | 26 ++++-- 7 files changed, 110 insertions(+), 53 deletions(-) diff --git a/lib/node_modules/@stdlib/repl/lib/complete_expression.js b/lib/node_modules/@stdlib/repl/lib/complete_expression.js index e22f7df30c8d..fde15a5c57eb 100644 --- a/lib/node_modules/@stdlib/repl/lib/complete_expression.js +++ b/lib/node_modules/@stdlib/repl/lib/complete_expression.js @@ -55,7 +55,7 @@ var AOPTS = { * @param {Array} out - output array for storing completions * @param {Object} context - REPL context * @param {string} expression - expression to complete -* @param {boolean} isFuzzy - boolean indicating if the completions should be fuzzy +* @param {boolean} isFuzzy - boolean indicating if the completions should be strictly fuzzy * @returns {string} filter */ function complete( out, context, expression, isFuzzy ) { @@ -110,14 +110,17 @@ function complete( out, context, expression, isFuzzy ) { out = filterByPrefix( out, objectKeys( context ), filter, false ); out = filterByPrefix( out, ast.locals, filter, false ); out.sort(); - if ( isFuzzy ) { - fuzzyResults = filterByPrefix( fuzzyResults, RESERVED_KEYWORDS_COMMON, filter, true ); // eslint-disable-line max-len - fuzzyResults = filterByPrefix( fuzzyResults, objectKeys( context ), filter, true ); // eslint-disable-line max-len - fuzzyResults = filterByPrefix( fuzzyResults, ast.locals, filter, true ); // eslint-disable-line max-len - fuzzyResults = sortFuzzyCompletions( fuzzyResults ); - for ( i = 0; i < fuzzyResults.length; i++ ) { - out.push( fuzzyResults[ i ] ); - } + + // Only fuzzy search, when no exact candidates found... + if ( !isFuzzy || out.length !== 0 ) { + return filter; + } + fuzzyResults = filterByPrefix( fuzzyResults, RESERVED_KEYWORDS_COMMON, filter, true ); // eslint-disable-line max-len + fuzzyResults = filterByPrefix( fuzzyResults, objectKeys( context ), filter, true ); // eslint-disable-line max-len + fuzzyResults = filterByPrefix( fuzzyResults, ast.locals, filter, true ); + fuzzyResults = sortFuzzyCompletions( fuzzyResults ); + for ( i = 0; i < fuzzyResults.length; i++ ) { + out.push( fuzzyResults[ i ] ); } return filter; } @@ -194,12 +197,15 @@ function complete( out, context, expression, isFuzzy ) { if ( node.property.type === 'Literal' ) { filter = node.property.value.toString(); // handles numeric literals out = filterByPrefix( out, propertyNamesIn( obj ), filter, false ); // eslint-disable-line max-len - if ( isFuzzy ) { - fuzzyResults = filterByPrefix( fuzzyResults, propertyNamesIn( obj ), filter, true ); // eslint-disable-line max-len - fuzzyResults = sortFuzzyCompletions( fuzzyResults ); - for ( i = 0; i < fuzzyResults.length; i++ ) { - out.push( fuzzyResults[ i ] ); - } + + // Only fuzzy search, when no exact candidates found... + if ( !isFuzzy || out.length !== 0 ) { + return filter; + } + fuzzyResults = filterByPrefix( fuzzyResults, propertyNamesIn( obj ), filter, true ); // eslint-disable-line max-len + fuzzyResults = sortFuzzyCompletions( fuzzyResults ); + for ( i = 0; i < fuzzyResults.length; i++ ) { + out.push( fuzzyResults[ i ] ); } return filter; } @@ -214,12 +220,15 @@ function complete( out, context, expression, isFuzzy ) { filter = node.property.name; } out = filterByPrefix( out, objectKeys( context ), filter, false ); // eslint-disable-line max-len - if ( isFuzzy ) { - fuzzyResults = filterByPrefix( fuzzyResults, objectKeys( context ), filter, true ); // eslint-disable-line max-len - fuzzyResults = sortFuzzyCompletions( fuzzyResults ); - for ( i = 0; i < fuzzyResults.length; i++ ) { - out.push( fuzzyResults[ i ] ); - } + + // Only fuzzy search, when no exact candidates found... + if ( !isFuzzy || out.length !== 0 ) { + return filter; + } + fuzzyResults = filterByPrefix( fuzzyResults, objectKeys( context ), filter, true ); // eslint-disable-line max-len + fuzzyResults = sortFuzzyCompletions( fuzzyResults ); + for ( i = 0; i < fuzzyResults.length; i++ ) { + out.push( fuzzyResults[ i ] ); } return filter; } @@ -247,12 +256,15 @@ function complete( out, context, expression, isFuzzy ) { } debug( 'Property auto-completion. Filter: %s', filter ); out = filterByPrefix( out, propertyNamesIn( obj ), filter, false ); - if ( isFuzzy ) { - fuzzyResults = filterByPrefix( fuzzyResults, propertyNamesIn( obj ), filter, true ); // eslint-disable-line max-len - fuzzyResults = sortFuzzyCompletions( fuzzyResults ); - for ( i = 0; i < fuzzyResults.length; i++ ) { - out.push( fuzzyResults[ i ] ); - } + + // Only fuzzy search, when no exact candidates found... + if ( !isFuzzy || out.length !== 0 ) { + return filter; + } + fuzzyResults = filterByPrefix( fuzzyResults, propertyNamesIn( obj ), filter, true ); // eslint-disable-line max-len + fuzzyResults = sortFuzzyCompletions( fuzzyResults ); + for ( i = 0; i < fuzzyResults.length; i++ ) { + out.push( fuzzyResults[ i ] ); } return filter; } @@ -262,14 +274,17 @@ function complete( out, context, expression, isFuzzy ) { out = filterByPrefix( out, res.keywords, filter, false ); out = filterByPrefix( out, objectKeys( context ), filter, false ); out = filterByPrefix( out, resolveLocalScope( ast, node ), filter, false ); - if ( isFuzzy ) { - fuzzyResults = filterByPrefix( fuzzyResults, res.keywords, filter, true ); // eslint-disable-line max-len - fuzzyResults = filterByPrefix( fuzzyResults, objectKeys( context ), filter, true ); // eslint-disable-line max-len - fuzzyResults = filterByPrefix( fuzzyResults, resolveLocalScope( ast, node ), filter, true ); // eslint-disable-line max-len - fuzzyResults = sortFuzzyCompletions( fuzzyResults ); - for ( i = 0; i < fuzzyResults.length; i++ ) { - out.push( fuzzyResults[ i ] ); - } + + // Only fuzzy search, when no exact candidates found... + if ( !isFuzzy || out.length !== 0 ) { + return filter; + } + fuzzyResults = filterByPrefix( fuzzyResults, res.keywords, filter, true ); + fuzzyResults = filterByPrefix( fuzzyResults, objectKeys( context ), filter, true ); // eslint-disable-line max-len + fuzzyResults = filterByPrefix( fuzzyResults, resolveLocalScope( ast, node ), filter, true ); // eslint-disable-line max-len + fuzzyResults = sortFuzzyCompletions( fuzzyResults ); + for ( i = 0; i < fuzzyResults.length; i++ ) { + out.push( fuzzyResults[ i ] ); } return filter; } diff --git a/lib/node_modules/@stdlib/repl/lib/complete_fs.js b/lib/node_modules/@stdlib/repl/lib/complete_fs.js index 390077b492e1..2cf36f2fde4d 100644 --- a/lib/node_modules/@stdlib/repl/lib/complete_fs.js +++ b/lib/node_modules/@stdlib/repl/lib/complete_fs.js @@ -52,7 +52,7 @@ var AOPTS = { * @param {string} expression - expression to complete * @param {string} alias - file system API alias * @param {string} path - path to complete -* @param {boolean} isFuzzy - boolean indicating if the completions should be fuzzy +* @param {boolean} isFuzzy - boolean indicating if the completions should be strictly fuzzy * @returns {string} path filter */ function complete( out, expression, alias, path, isFuzzy ) { @@ -134,7 +134,9 @@ function complete( out, expression, alias, path, isFuzzy ) { } } out.sort(); - if ( !isFuzzy ) { + + // Only fuzzy search, when no exact candidates found... + if ( !isFuzzy || out.length !== 0 ) { return filter; } // Start searching for fuzzy completions... diff --git a/lib/node_modules/@stdlib/repl/lib/complete_require.js b/lib/node_modules/@stdlib/repl/lib/complete_require.js index bd9a4d1f1de4..e6a7f36401f5 100644 --- a/lib/node_modules/@stdlib/repl/lib/complete_require.js +++ b/lib/node_modules/@stdlib/repl/lib/complete_require.js @@ -53,7 +53,7 @@ var RE_RELATIVE = relativePathRegExp(); * @param {string} path - path to complete * @param {Array} paths - module search paths * @param {Array} exts - supported `require` extensions -* @param {boolean} isFuzzy - boolean indicating if the completions should be fuzzy +* @param {boolean} isFuzzy - boolean indicating if the completions should be strictly fuzzy * @returns {string} path filter */ function complete( out, path, paths, exts, isFuzzy ) { @@ -165,7 +165,9 @@ function complete( out, path, paths, exts, isFuzzy ) { } } out.sort(); - if ( !isFuzzy ) { + + // Only fuzzy search, when no exact candidates found... + if ( !isFuzzy || out.length !== 0 ) { return filter; } // Start searching for fuzzy completions... diff --git a/lib/node_modules/@stdlib/repl/lib/complete_settings.js b/lib/node_modules/@stdlib/repl/lib/complete_settings.js index 93672a1f4da7..1156136a6e16 100644 --- a/lib/node_modules/@stdlib/repl/lib/complete_settings.js +++ b/lib/node_modules/@stdlib/repl/lib/complete_settings.js @@ -23,6 +23,8 @@ var logger = require( 'debug' ); var parse = require( 'acorn-loose' ).parse; var startsWith = require( '@stdlib/string/starts-with' ); +var fuzzyMatch = require( './fuzzy_match.js' ); +var sortFuzzyCompletions = require( './sort_fuzzy_completions.js' ); var settingsAliasArgs = require( './settings_alias_args.js' ); var SETTINGS_NAMES = require( './settings_names.js' ); @@ -46,9 +48,12 @@ var AOPTS = { * @param {string} expression - expression to complete * @param {string} alias - settings API alias * @param {string} value - value to complete +* @param {boolean} isFuzzy - boolean indicating if the completions should be strictly fuzzy * @returns {string} value filter */ -function complete( out, repl, expression, alias, value ) { +function complete( out, repl, expression, alias, value, isFuzzy ) { + var fuzzyResults = []; + var match; var args; var ast; var arg; @@ -82,6 +87,27 @@ function complete( out, repl, expression, alias, value ) { debug( '%s does not match filter %s. Skipping...', v, value ); } } + out.sort(); + + // Only fuzzy search, when no exact candidates found... + if ( !isFuzzy || out.length !== 0 ) { + return value; + } + debug( 'Fuzzy searching for completion candidates...' ); + for ( i = 0; i < SETTINGS_NAMES.length; i++ ) { + v = SETTINGS_NAMES[ i ]; + match = fuzzyMatch( v, value ); + if ( match ) { + debug( 'Found a fuzzy completion: %s', v ); + fuzzyResults.push( v ); + } else { + debug( '%s does not match fuzzy filter %s. Skipping...', v, value ); + } + } + fuzzyResults = sortFuzzyCompletions( fuzzyResults ); + for ( i = 0; i < fuzzyResults.length; i++ ) { + out.push( fuzzyResults[ i ] ); + } return value; } diff --git a/lib/node_modules/@stdlib/repl/lib/complete_tutorial.js b/lib/node_modules/@stdlib/repl/lib/complete_tutorial.js index 3db5f7273a4b..19c1b27da865 100644 --- a/lib/node_modules/@stdlib/repl/lib/complete_tutorial.js +++ b/lib/node_modules/@stdlib/repl/lib/complete_tutorial.js @@ -50,7 +50,7 @@ var AOPTS = { * @param {string} expression - expression to complete * @param {string} alias - tutorial API alias * @param {string} value - value to complete -* @param {boolean} isFuzzy - boolean indicating if the completions should be fuzzy +* @param {boolean} isFuzzy - boolean indicating if the completions should be strictly fuzzy * @returns {string} value filter */ function complete( out, repl, expression, alias, value, isFuzzy ) { @@ -90,7 +90,9 @@ function complete( out, repl, expression, alias, value, isFuzzy ) { } } out.sort(); - if ( !isFuzzy ) { + + // Only fuzzy search, when no exact candidates found... + if ( !isFuzzy || out.length !== 0 ) { return value; } debug( 'Fuzzy searching for completion candidates...' ); diff --git a/lib/node_modules/@stdlib/repl/lib/complete_workspace.js b/lib/node_modules/@stdlib/repl/lib/complete_workspace.js index 4d136cff3b97..22ec9d056fe9 100644 --- a/lib/node_modules/@stdlib/repl/lib/complete_workspace.js +++ b/lib/node_modules/@stdlib/repl/lib/complete_workspace.js @@ -47,7 +47,7 @@ var AOPTS = { * @param {string} expression - expression to complete * @param {string} alias - workspace API alias * @param {string} value - value to complete -* @param {boolean} isFuzzy - boolean indicating if the completions should be fuzzy +* @param {boolean} isFuzzy - boolean indicating if the completions should be strictly fuzzy * @returns {string} value filter */ function complete( out, repl, expression, alias, value, isFuzzy ) { @@ -91,7 +91,9 @@ function complete( out, repl, expression, alias, value, isFuzzy ) { } } out.sort(); - if ( !isFuzzy ) { + + // Only fuzzy search, when no exact candidates found... + if ( !isFuzzy || out.length !== 0 ) { return value; } debug( 'Fuzzy searching for completion candidates...' ); diff --git a/lib/node_modules/@stdlib/repl/lib/completer.js b/lib/node_modules/@stdlib/repl/lib/completer.js index 88ddd2288aaf..111f327bd4b5 100644 --- a/lib/node_modules/@stdlib/repl/lib/completer.js +++ b/lib/node_modules/@stdlib/repl/lib/completer.js @@ -25,6 +25,8 @@ var logger = require( 'debug' ); var objectKeys = require( '@stdlib/utils/keys' ); var hasOwnProp = require( '@stdlib/assert/has-own-property' ); +var slice = require( '@stdlib/array/slice' ); +var startsWith = require( '@stdlib/string/starts-with' ); var fsRegExp = require( './regexp_fs_aliases.js' ); var requireRegExp = require( './regexp_require.js' ); var workspaceRegExp = require( './regexp_workspace.js' ); @@ -51,9 +53,10 @@ var debug = logger( 'repl:completer:callback' ); * * @private * @param {Array} list - completion list +* @param {boolean} isFuzzy - boolean indicating if completions are strictly fuzzy * @returns {Array} normalized completion list */ -function normalize( list ) { +function normalize( list, isFuzzy ) { var hash; var i; @@ -66,6 +69,11 @@ function normalize( list ) { } list = objectKeys( hash ); + // Limit fuzzy completions to 10... + if ( isFuzzy ) { + list = slice( list, 0, 10 ); + } + // TODO: sort such that printed columns are in lexicographic order, not rows, similar to bash behavior! return list; @@ -90,7 +98,7 @@ function completer( repl ) { * @private * @param {string} line - current line * @param {Function} clbk - completion callback - * @param {boolean} isFuzzy - boolean indicating if the completions should be fuzzy + * @param {boolean} isFuzzy - boolean indicating if the completions should be strictly fuzzy * @returns {void} */ function complete( line, clbk, isFuzzy ) { @@ -112,7 +120,7 @@ function completer( repl ) { debug( 'Supported `require` filename extensions: %s', exts.join( ', ' ) ); line = completeRequire( res, match[ 1 ], repl._context.module.paths, exts, isFuzzy ); // eslint-disable-line max-len - res = normalize( res ); + res = normalize( res, !startsWith( res[ 0 ], line ) ); debug( 'Completion filter: %s', line ); debug( 'Results: %s', res.join( ', ' ) ); @@ -127,7 +135,7 @@ function completer( repl ) { debug( 'File system API: %s', match[ 1 ] ); debug( 'Path to complete: %s', match[ 3 ] ); line = completeFS( res, match[ 0 ], match[ 1 ], match[ 3 ], isFuzzy ); // eslint-disable-line max-len - res = normalize( res ); + res = normalize( res, !startsWith( res[ 0 ], line ) ); debug( 'Completion filter: %s', line ); debug( 'Results: %s', res.join( ', ' ) ); @@ -142,7 +150,7 @@ function completer( repl ) { debug( 'Workspace API: %s', match[ 1 ] ); debug( 'Value to complete: %s', match[ 3 ] ); line = completeWorkspace( res, repl, match[ 0 ], match[ 1 ], match[ 3 ], isFuzzy ); // eslint-disable-line max-len - res = normalize( res ); + res = normalize( res, !startsWith( res[ 0 ], line ) ); debug( 'Completion filter: %s', line ); debug( 'Results: %s', res.join( ', ' ) ); @@ -157,7 +165,7 @@ function completer( repl ) { debug( 'Tutorial API: %s', match[ 1 ] ); debug( 'Value to complete: %s', match[ 3 ] ); line = completeTutorial( res, repl, match[ 0 ], match[ 1 ], match[ 3 ], isFuzzy ); // eslint-disable-line max-len - res = normalize( res ); + res = normalize( res, !startsWith( res[ 0 ], line ) ); debug( 'Completion filter: %s', line ); debug( 'Results: %s', res.join( ', ' ) ); @@ -171,8 +179,8 @@ function completer( repl ) { debug( 'Expression: %s', match[ 0 ] ); debug( 'Settings API: %s', match[ 1 ] ); debug( 'Value to complete: %s', match[ 3 ] ); - line = completeSettings( res, repl, match[ 0 ], match[ 1 ], match[ 3 ] ); // eslint-disable-line max-len - res = normalize( res ); + line = completeSettings( res, repl, match[ 0 ], match[ 1 ], match[ 3 ], isFuzzy ); // eslint-disable-line max-len + res = normalize( res, !startsWith( res[ 0 ], line ) ); debug( 'Completion filter: %s', line ); debug( 'Results: %s', res.join( ', ' ) ); @@ -186,7 +194,7 @@ function completer( repl ) { } debug( 'Attempting to complete an incomplete expression.' ); line = completeExpression( res, repl._context, line, isFuzzy ); - res = normalize( res ); + res = normalize( res, !startsWith( res[ 0 ], line ) ); debug( 'Results: %s', res.join( ', ' ) ); return clbk( null, [ res, line ] ); } From 89b5001d125f024d76be32894211b178233592e2 Mon Sep 17 00:00:00 2001 From: Snehil Shah Date: Sat, 6 Apr 2024 10:30:05 +0000 Subject: [PATCH 15/24] fix: bug in completer Signed-off-by: Snehil Shah --- .../@stdlib/repl/lib/complete_settings.js | 2 +- .../@stdlib/repl/lib/complete_tutorial.js | 2 +- .../@stdlib/repl/lib/complete_workspace.js | 2 +- lib/node_modules/@stdlib/repl/lib/completer.js | 12 ++++++------ 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/lib/node_modules/@stdlib/repl/lib/complete_settings.js b/lib/node_modules/@stdlib/repl/lib/complete_settings.js index 1156136a6e16..8419c620cc9b 100644 --- a/lib/node_modules/@stdlib/repl/lib/complete_settings.js +++ b/lib/node_modules/@stdlib/repl/lib/complete_settings.js @@ -99,7 +99,7 @@ function complete( out, repl, expression, alias, value, isFuzzy ) { match = fuzzyMatch( v, value ); if ( match ) { debug( 'Found a fuzzy completion: %s', v ); - fuzzyResults.push( v ); + fuzzyResults.push( match ); } else { debug( '%s does not match fuzzy filter %s. Skipping...', v, value ); } diff --git a/lib/node_modules/@stdlib/repl/lib/complete_tutorial.js b/lib/node_modules/@stdlib/repl/lib/complete_tutorial.js index 19c1b27da865..c23e79b1aa82 100644 --- a/lib/node_modules/@stdlib/repl/lib/complete_tutorial.js +++ b/lib/node_modules/@stdlib/repl/lib/complete_tutorial.js @@ -101,7 +101,7 @@ function complete( out, repl, expression, alias, value, isFuzzy ) { match = fuzzyMatch( t, value ); if ( match ) { debug( 'Found a fuzzy completion: %s', t ); - fuzzyResults.push( t ); + fuzzyResults.push( match ); } else { debug( '%s does not match fuzzy filter %s. Skipping...', t, value ); } diff --git a/lib/node_modules/@stdlib/repl/lib/complete_workspace.js b/lib/node_modules/@stdlib/repl/lib/complete_workspace.js index 22ec9d056fe9..9378edbffeb6 100644 --- a/lib/node_modules/@stdlib/repl/lib/complete_workspace.js +++ b/lib/node_modules/@stdlib/repl/lib/complete_workspace.js @@ -102,7 +102,7 @@ function complete( out, repl, expression, alias, value, isFuzzy ) { match = fuzzyMatch( w, value ); if ( match ) { debug( 'Found a fuzzy completion: %s', w ); - fuzzyResults.push( w ); + fuzzyResults.push( match ); } else { debug( '%s does not match fuzzy filter %s. Skipping...', w, value ); } diff --git a/lib/node_modules/@stdlib/repl/lib/completer.js b/lib/node_modules/@stdlib/repl/lib/completer.js index 111f327bd4b5..488679b5cce8 100644 --- a/lib/node_modules/@stdlib/repl/lib/completer.js +++ b/lib/node_modules/@stdlib/repl/lib/completer.js @@ -120,7 +120,7 @@ function completer( repl ) { debug( 'Supported `require` filename extensions: %s', exts.join( ', ' ) ); line = completeRequire( res, match[ 1 ], repl._context.module.paths, exts, isFuzzy ); // eslint-disable-line max-len - res = normalize( res, !startsWith( res[ 0 ], line ) ); + res = normalize( res, res[ 0 ] && !startsWith( res[ 0 ], line ) ); debug( 'Completion filter: %s', line ); debug( 'Results: %s', res.join( ', ' ) ); @@ -135,7 +135,7 @@ function completer( repl ) { debug( 'File system API: %s', match[ 1 ] ); debug( 'Path to complete: %s', match[ 3 ] ); line = completeFS( res, match[ 0 ], match[ 1 ], match[ 3 ], isFuzzy ); // eslint-disable-line max-len - res = normalize( res, !startsWith( res[ 0 ], line ) ); + res = normalize( res, res[ 0 ] && !startsWith( res[ 0 ], line ) ); debug( 'Completion filter: %s', line ); debug( 'Results: %s', res.join( ', ' ) ); @@ -150,7 +150,7 @@ function completer( repl ) { debug( 'Workspace API: %s', match[ 1 ] ); debug( 'Value to complete: %s', match[ 3 ] ); line = completeWorkspace( res, repl, match[ 0 ], match[ 1 ], match[ 3 ], isFuzzy ); // eslint-disable-line max-len - res = normalize( res, !startsWith( res[ 0 ], line ) ); + res = normalize( res, res[ 0 ] && !startsWith( res[ 0 ], line ) ); debug( 'Completion filter: %s', line ); debug( 'Results: %s', res.join( ', ' ) ); @@ -165,7 +165,7 @@ function completer( repl ) { debug( 'Tutorial API: %s', match[ 1 ] ); debug( 'Value to complete: %s', match[ 3 ] ); line = completeTutorial( res, repl, match[ 0 ], match[ 1 ], match[ 3 ], isFuzzy ); // eslint-disable-line max-len - res = normalize( res, !startsWith( res[ 0 ], line ) ); + res = normalize( res, res[ 0 ] && !startsWith( res[ 0 ], line ) ); debug( 'Completion filter: %s', line ); debug( 'Results: %s', res.join( ', ' ) ); @@ -180,7 +180,7 @@ function completer( repl ) { debug( 'Settings API: %s', match[ 1 ] ); debug( 'Value to complete: %s', match[ 3 ] ); line = completeSettings( res, repl, match[ 0 ], match[ 1 ], match[ 3 ], isFuzzy ); // eslint-disable-line max-len - res = normalize( res, !startsWith( res[ 0 ], line ) ); + res = normalize( res, res[ 0 ] && !startsWith( res[ 0 ], line ) ); debug( 'Completion filter: %s', line ); debug( 'Results: %s', res.join( ', ' ) ); @@ -194,7 +194,7 @@ function completer( repl ) { } debug( 'Attempting to complete an incomplete expression.' ); line = completeExpression( res, repl._context, line, isFuzzy ); - res = normalize( res, !startsWith( res[ 0 ], line ) ); + res = normalize( res, res[ 0 ] && !startsWith( res[ 0 ], line ) ); debug( 'Results: %s', res.join( ', ' ) ); return clbk( null, [ res, line ] ); } From c4649445ef6767805f6a8c3eedbd30d879f3769f Mon Sep 17 00:00:00 2001 From: Snehil Shah Date: Sun, 7 Apr 2024 21:21:32 +0000 Subject: [PATCH 16/24] test: add tests for tab completions Signed-off-by: Snehil Shah --- .../@stdlib/repl/lib/completer_engine.js | 26 +- .../integration/test.fuzzy_completions.js | 692 ++++++++++++++++++ 2 files changed, 712 insertions(+), 6 deletions(-) create mode 100644 lib/node_modules/@stdlib/repl/test/integration/test.fuzzy_completions.js diff --git a/lib/node_modules/@stdlib/repl/lib/completer_engine.js b/lib/node_modules/@stdlib/repl/lib/completer_engine.js index 3d0fa7bb9f0e..8aee374b2417 100644 --- a/lib/node_modules/@stdlib/repl/lib/completer_engine.js +++ b/lib/node_modules/@stdlib/repl/lib/completer_engine.js @@ -188,7 +188,7 @@ setNonEnumerableReadOnly( CompleterEngine.prototype, '_completionCallback', func // Move the cursor to the start of completion prefix: self._rli.cursor -= self._completionPrefix.length; - // Write the auto-completion string + // Write the auto-completion string: self._rli.write( autoCompletion ); // Resume the input stream: @@ -368,6 +368,7 @@ setNonEnumerableReadOnly( CompleterEngine.prototype, '_navigateCompletions', fun output += repeat( ' ', whitespaces ); } if ( i === this._output.index && this.repl._isTTY ) { + // Highlight the current navigated index: completion = stripANSI( completion ); completion = '\u001b[7m' + completion + '\u001b[27m'; } @@ -440,7 +441,6 @@ setNonEnumerableReadOnly( CompleterEngine.prototype, 'beforeKeypress', function // Move to the previous row or if already on the line, stop navigating and trigger default behaviour... if ( this._output.index === -1 ) { readline.clearScreenDown( this._ostream ); - this._output.index = -1; this.repl._isNavigatingCompletions = false; this._ttyWrite.call( this._rli, data, key ); return; @@ -452,17 +452,31 @@ setNonEnumerableReadOnly( CompleterEngine.prototype, 'beforeKeypress', function } this._navigateCompletions(); } else if ( key.name === 'left' ) { - // Move back an index... + // If on current line, trigger default behaviour and stop navigating... + if ( this._output.index === -1 ) { + readline.clearScreenDown( this._ostream ); + this.repl._isNavigatingCompletions = false; + this._ttyWrite.call( this._rli, data, key ); + return; + } + // If navigating, move back an index... if ( this._output.index > 0 ) { this._output.index -= 1; + this._navigateCompletions(); } - this._navigateCompletions(); } else if ( key.name === 'right' ) { - // Move ahead an index... + // If on current line, trigger default behaviour and stop navigating... + if ( this._output.index === -1 ) { + readline.clearScreenDown( this._ostream ); + this.repl._isNavigatingCompletions = false; + this._ttyWrite.call( this._rli, data, key ); + return; + } + // If navigating, move ahead an index... if ( this._output.index < this._completionsList.length - 1 ) { this._output.index += 1; + this._navigateCompletions(); } - this._navigateCompletions(); } else { // For any other keypress, stop navigating and continue default behaviour... readline.clearScreenDown( this._ostream ); diff --git a/lib/node_modules/@stdlib/repl/test/integration/test.fuzzy_completions.js b/lib/node_modules/@stdlib/repl/test/integration/test.fuzzy_completions.js new file mode 100644 index 000000000000..bdaf82edfb63 --- /dev/null +++ b/lib/node_modules/@stdlib/repl/test/integration/test.fuzzy_completions.js @@ -0,0 +1,692 @@ +/** +* @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 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, + 'fuzzyCompletions': 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 = str.split('\r\n'); + 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() + }; + 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-2 ] ) ); + + // 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' ); + + // `data[ data.length-1 ]` brings the cursor to the current line... + + t.end(); + } +}); + +tape( 'a REPL instance supports displaying `fuzzy` 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() + }; + r = repl( opts, onClose ); + + // Declare variables with unique names in order to prevent namespace collisions: + istream.write( 'var pizza = 1;' ); + istream.write( 'var jazz = 2;' ); + + // Write the common substring of the variable names in order to generate fuzzy completions: + istream.write( 'zz' ); + + // Enable Fuzzy completions: + r.settings( 'fuzzyCompletions', true ); + + // 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-2 ] ) ); + + // Check for two fuzzy completions in the output: + t.strictEqual( actual.length, 2, 'returns expected value' ); + + // Check for the declared variables (sorted lexicographically) in the completions: + t.strictEqual( actual[ 0 ], 'jazz', 'returns expected value' ); + t.strictEqual( actual[ 1 ], 'pizza', 'returns expected value' ); + + // `data[ data.length-1 ]` brings the cursor to the current line... + + t.end(); + } +}); + +tape( 'a REPL instance doesn\'t display `fuzzy` completions if `exact` completions exist', function test( t ) { + var istream; + var opts; + var r; + + istream = new DebugStream({ + 'name': 'repl-input-stream' + }); + opts = { + 'input': istream, + 'settings': defaultSettings() + }; + r = repl( opts, onClose ); + + // Declare variables with `zz` as an exact prefix: + istream.write( 'var zzxyz = 1;' ); + istream.write( 'var zzabc = 2;' ); + istream.write( 'var zzpqr = 3;' ); + + // Declare variables with `zz` not as an exact prefix: + istream.write( 'var pizza = 1;' ); + istream.write( 'var jazz = 2;' ); + + // Write the common substring of the variable names in order to generate TAB completions: + istream.write( 'zz' ); + + // Enable Fuzzy completions: + r.settings( 'fuzzyCompletions', true ); + + // 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-2 ] ) ); + + // Check for three exact completions in the output: + t.strictEqual( actual.length, 3, 'returns expected value' ); + + // Check for the declared variables with exact prefixes 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' ); + + // `data[ data.length-1 ]` brings the cursor to the current line... + + t.end(); + } +}); + +tape( 'a REPL instance supports displaying TAB completions with input characters highlighted', function test( t ) { + var istream; + var opts; + var r; + + istream = new DebugStream({ + 'name': 'repl-input-stream' + }); + opts = { + 'input': istream, + 'settings': defaultSettings() + }; + r = repl( opts, onClose ); + + // Declare variables with unique names in order to prevent namespace collisions: + istream.write( 'var pizza = 1;' ); + istream.write( 'var jazz = 2;' ); + + // Write the common substring of the variable names in order to generate fuzzy completions: + istream.write( 'zz' ); + + // Enable Fuzzy completions: + r.settings( 'fuzzyCompletions', true ); + + // 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( data[ data.length-2 ] ); + + // Check for two fuzzy completions in the output: + t.strictEqual( actual.length, 2, 'returns expected value' ); + + // Check for the declared variables in the completions with input characters highlighted: + t.strictEqual( actual[ 0 ], 'ja\x1B[1mz\x1B[0m\x1B[1mz\x1B[0m', 'returns expected value' ); + t.strictEqual( actual[ 1 ], 'pi\x1B[1mz\x1B[0m\x1B[1mz\x1B[0ma', 'returns expected value' ); + + // `data[ data.length-1 ]` brings the cursor to the current line... + + 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() + }; + r = repl( opts, onClose ); + + // Declare variables with unique names in order to prevent namespace collisions: + istream.write( 'var pizza = 1;' ); + istream.write( 'var jazz = 2;' ); + + // Write the common substring of the variable names in order to generate fuzzy completions: + istream.write( 'zz' ); + + // Enable Fuzzy completions: + r.settings( 'fuzzyCompletions', true ); + + // 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' ); + + // `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() + }; + r = repl( opts, onClose ); + + // Declare variables with unique names in order to prevent namespace collisions: + istream.write( 'var pizza = 1;' ); + istream.write( 'var jazz = 2;' ); + + // Write the common substring of the variable names in order to generate fuzzy completions: + istream.write( 'zz' ); + + // Enable Fuzzy completions: + r.settings( 'fuzzyCompletions', true ); + + // 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-12 ] ) ); + t.strictEqual( actual.length, 2, 'returns expected value' ); + t.strictEqual( actual[ 0 ], 'jazz', 'returns expected value' ); + t.strictEqual( actual[ 1 ], 'pizza', 'returns expected value' ); + + // `data[ data.length-11 ]` brings cursor to the current line... + + // Screen is cleared after `down` arrow key to re-display the navigated output: + t.strictEqual( data[ data.length-10 ], '\x1B[0J', 'returns expected value' ); + + // Check for completions after `down` arrow: + actual = extractCompletions( data[ data.length-9 ] ); + + // First completion should be highlighted: + t.strictEqual( actual[ 0 ], '\x1B[7mjazz\x1B[27m', 'returns expected value' ); + + // `data[ data.length-8 ]` brings cursor to the current line... + + // 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 pizza = 1;var jazz = 2;jazz', 'returns expected value' ); + + // Screen is cleared after `right` arrow key to re-display the navigated output: + t.strictEqual( data[ data.length-5 ], '\x1B[0J', 'returns expected value' ); + + // Check for completions after `right` arrow key: + actual = extractCompletions( data[ data.length-4 ] ); + + // Second completion should be highlighted: + t.strictEqual( actual[ 1 ], '\x1B[7mpizza\x1B[27m', 'returns expected value' ); + + // `data[ data.length-3 ]` brings cursor to the current line... + + // 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 pizza = 1;var jazz = 2;pizza', 'returns expected value' ); + + 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() + }; + 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 arguement TAB completions', function test( t ) { + var istream; + var opts; + var r; + + istream = new DebugStream({ + 'name': 'repl-input-stream' + }); + opts = { + 'input': istream, + 'settings': defaultSettings() + }; + 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-2 ] ); + t.ok( contains( actual, '\x1B[1mauto\x1B[0mClosePairs' ), 'returns expected value' ); + t.ok( contains( actual, '\x1B[1mauto\x1B[0mDeletePairs' ), 'returns expected value' ); + + // `data[ data.length-1 ]` brings the cursor to the current line... + + 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() + }; + 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 a single fuzzy completion for the input when hitting TAB', function test( t ) { + var istream; + var opts; + var r; + + istream = new DebugStream({ + 'name': 'repl-input-stream' + }); + opts = { + 'input': istream, + 'settings': defaultSettings() + }; + r = repl( opts, onClose ); + + // Declare a variables with `zz` as a substring: + istream.write( 'var pizza = 1;' ); + + // Write the common substring of the variable names in order to generate a fuzzy completion: + istream.write( 'zz' ); + + // Enable Fuzzy completions: + r.settings( 'fuzzyCompletions', true ); + + // 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 ], 'pizza', 'returns expected value' ); + + t.end(); + } +}); + +tape( 'a REPL instance supports auto-completing common arguement 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() + }; + r = repl( opts, onClose ); + + // Write the arguement with just one possible completion: + istream.write( 'settings(\'fuzzyCompleti' ); + + // 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 (`fuzzyCompleti`) was cleared: + t.strictEqual( data[ data.length-2 ], '\b\b\b\b\b\b\b\b\b\b\b\b\b', 'returns expected value' ); + + // Check if the final completion was auto-inserted: + t.strictEqual( data[ data.length-1 ], 'fuzzyCompletions', 'returns expected value' ); + + t.end(); + } +}); From 6d6d7c97917fc98834ae7db98c3f26309c2dc7f8 Mon Sep 17 00:00:00 2001 From: Snehil Shah Date: Mon, 8 Apr 2024 22:17:04 +0000 Subject: [PATCH 17/24] docs: fix test name and comments Signed-off-by: Snehil Shah --- lib/node_modules/@stdlib/repl/lib/completer_engine.js | 4 ++-- .../@stdlib/repl/test/integration/test.fuzzy_completions.js | 6 +++--- 2 files 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 8aee374b2417..e81f1a0be140 100644 --- a/lib/node_modules/@stdlib/repl/lib/completer_engine.js +++ b/lib/node_modules/@stdlib/repl/lib/completer_engine.js @@ -318,7 +318,7 @@ setNonEnumerableReadOnly( CompleterEngine.prototype, '_displayCompletions', func whitespaces = this._output.widthOfColumn - this._output.completionsLength[ i ]; // eslint-disable-line max-len lineIndex += 1; } - // Ensure a space between completions and the following prompt... + // Ensure a space to even out the number of rows... if ( lineIndex !== 0 ) { output += '\r\n\r\n'; } @@ -377,7 +377,7 @@ setNonEnumerableReadOnly( CompleterEngine.prototype, '_navigateCompletions', fun whitespaces = this._output.widthOfColumn - this._output.completionsLength[ i ]; // eslint-disable-line max-len lineIndex += 1; } - // Ensure a space between completions and the following prompt... + // Ensure a space to even out the number of rows... if ( lineIndex !== 0 ) { output += '\r\n\r\n'; } diff --git a/lib/node_modules/@stdlib/repl/test/integration/test.fuzzy_completions.js b/lib/node_modules/@stdlib/repl/test/integration/test.fuzzy_completions.js index bdaf82edfb63..3e810fb1e245 100644 --- a/lib/node_modules/@stdlib/repl/test/integration/test.fuzzy_completions.js +++ b/lib/node_modules/@stdlib/repl/test/integration/test.fuzzy_completions.js @@ -398,10 +398,10 @@ tape( 'a REPL instance supports navigating the TAB completions using arrow keys' istream.write( '\t' ); // Navigate down using down arrow: - istream.write('\u001B[B'); + istream.write( '\u001B[B' ); // Navigate right to the next completion using right arrow: - istream.write('\u001B[C'); + istream.write( '\u001B[C' ); // Close the input stream: istream.end(); @@ -459,7 +459,7 @@ tape( 'a REPL instance supports navigating the TAB completions using arrow keys' } }); -tape( 'a REPL instance supports navigating the TAB completions using arrow keys', function test( t ) { +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; From 2c15d1513e4383560d4a32653261fc775b7943be Mon Sep 17 00:00:00 2001 From: Snehil Shah Date: Mon, 8 Apr 2024 22:23:00 +0000 Subject: [PATCH 18/24] fix: tune fuzzy algorithm Signed-off-by: Snehil Shah --- lib/node_modules/@stdlib/repl/lib/fuzzy_match.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/node_modules/@stdlib/repl/lib/fuzzy_match.js b/lib/node_modules/@stdlib/repl/lib/fuzzy_match.js index ebe23ae19c71..ac0af027641b 100644 --- a/lib/node_modules/@stdlib/repl/lib/fuzzy_match.js +++ b/lib/node_modules/@stdlib/repl/lib/fuzzy_match.js @@ -29,7 +29,7 @@ var lowercase = require( '@stdlib/string/lowercase' ); var PENALTIES = { 'wrong_case': -1, // Penalty for a matching character that is in the wrong case 'not_start': -4, // Penalty if the first letter of the completion and input are different - 'gap_between': -8, // Penalty for a gap between matching characters of input in the completion + 'gap_between': -4, // Penalty for a gap between matching characters of input in the completion 'gap_beginning': -1, // Penalty for a gap, before a character from input is encountered 'missing_char': -19 // Penalty for a character in input that doesn't exist in the completion (to tackle possible spelling mistakes) }; From 0e54cf5c595c5bdd73399bb74cab3b63fed5b8dc Mon Sep 17 00:00:00 2001 From: Snehil Shah Date: Sun, 21 Apr 2024 11:44:37 +0000 Subject: [PATCH 19/24] docs: document setting Signed-off-by: Snehil Shah --- lib/node_modules/@stdlib/repl/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/node_modules/@stdlib/repl/README.md b/lib/node_modules/@stdlib/repl/README.md index 0d52ba9a4b01..927140518b3e 100644 --- a/lib/node_modules/@stdlib/repl/README.md +++ b/lib/node_modules/@stdlib/repl/README.md @@ -82,6 +82,7 @@ The function supports specifying the following settings: - **autoClosePairs**: boolean indicating whether to automatically insert matching brackets, parentheses, and quotes. Default: `true`. - **autoDeletePairs**: boolean indicating whether to automatically delete adjacent matching brackets, parentheses, and quotes. Default: `true`. - **completionPreviews**: boolean indicating whether to display completion previews for auto-completion. When streams are TTY, the default is `true`; otherwise, the default is `false`. +- **fuzzyCompletions**: boolean indicating whether to include fuzzy results in TAB completions. Default: `true`. #### REPL.prototype.createContext() From fa22afb62562074d18fe815aae55480f5b7387e9 Mon Sep 17 00:00:00 2001 From: Snehil Shah Date: Thu, 25 Apr 2024 19:52:15 +0000 Subject: [PATCH 20/24] fix: abnormal completer behavior 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 a79461685c5e..92b07878ed93 100644 --- a/lib/node_modules/@stdlib/repl/lib/main.js +++ b/lib/node_modules/@stdlib/repl/lib/main.js @@ -268,7 +268,7 @@ function REPL( options ) { })); // Create a new TAB completer engine: - setNonEnumerableReadOnly( this, '_completerEngine', new CompleterEngine( this, this._completer, this._ostream, this._rli._ttyWrite ) ); + 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 ) ); From 7b8074abff6be79f2a6c22db81381dc899ef5479 Mon Sep 17 00:00:00 2001 From: Snehil Shah Date: Thu, 25 Apr 2024 20:10:09 +0000 Subject: [PATCH 21/24] refactor: move flag to the completer engine namespace Signed-off-by: Snehil Shah --- .../@stdlib/repl/lib/completer_engine.js | 17 ++++++++++------- lib/node_modules/@stdlib/repl/lib/main.js | 3 --- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/lib/node_modules/@stdlib/repl/lib/completer_engine.js b/lib/node_modules/@stdlib/repl/lib/completer_engine.js index e81f1a0be140..c1f211d62d27 100644 --- a/lib/node_modules/@stdlib/repl/lib/completer_engine.js +++ b/lib/node_modules/@stdlib/repl/lib/completer_engine.js @@ -96,6 +96,9 @@ function CompleterEngine( repl, completer, ostream, ttyWrite ) { // Cache a reference to the REPL settings to determine the type of completions: this._settings = repl._settings; + // Initialize a flag indicating whether a user is navigating TAB completions: + this._isNavigating = false; + // Create a callback for processing completions: this._onCompletions = this._completionCallback(); @@ -199,7 +202,7 @@ setNonEnumerableReadOnly( CompleterEngine.prototype, '_completionCallback', func // Display completions: self._displayCompletions(); - self.repl._isNavigatingCompletions = true; + self._isNavigating = true; // Resume the input stream: self._rli.resume(); @@ -415,7 +418,7 @@ setNonEnumerableReadOnly( CompleterEngine.prototype, 'beforeKeypress', function return; } // If the completions output is visible, allow navigating it... - if ( this.repl._isNavigatingCompletions ) { + if ( this._isNavigating ) { if ( key.name === 'tab' ) { // Stop navigating and hide the completions output: readline.clearScreenDown( this._ostream ); @@ -425,7 +428,7 @@ setNonEnumerableReadOnly( CompleterEngine.prototype, 'beforeKeypress', function readline.moveCursor( this._ostream, -1 * this._remainingLine.length ); // eslint-disable-line max-len this._output.index = -1; - this.repl._isNavigatingCompletions = false; + this._isNavigating = false; return; } if ( key.name === 'down' ) { @@ -441,7 +444,7 @@ setNonEnumerableReadOnly( CompleterEngine.prototype, 'beforeKeypress', function // Move to the previous row or if already on the line, stop navigating and trigger default behaviour... if ( this._output.index === -1 ) { readline.clearScreenDown( this._ostream ); - this.repl._isNavigatingCompletions = false; + this._isNavigating = false; this._ttyWrite.call( this._rli, data, key ); return; } @@ -455,7 +458,7 @@ setNonEnumerableReadOnly( CompleterEngine.prototype, 'beforeKeypress', function // If on current line, trigger default behaviour and stop navigating... if ( this._output.index === -1 ) { readline.clearScreenDown( this._ostream ); - this.repl._isNavigatingCompletions = false; + this._isNavigating = false; this._ttyWrite.call( this._rli, data, key ); return; } @@ -468,7 +471,7 @@ setNonEnumerableReadOnly( CompleterEngine.prototype, 'beforeKeypress', function // If on current line, trigger default behaviour and stop navigating... if ( this._output.index === -1 ) { readline.clearScreenDown( this._ostream ); - this.repl._isNavigatingCompletions = false; + this._isNavigating = false; this._ttyWrite.call( this._rli, data, key ); return; } @@ -481,7 +484,7 @@ setNonEnumerableReadOnly( CompleterEngine.prototype, 'beforeKeypress', function // For any other keypress, stop navigating and continue default behaviour... readline.clearScreenDown( this._ostream ); this._output.index = -1; - this.repl._isNavigatingCompletions = false; + this._isNavigating = false; this._ttyWrite.call( this._rli, data, key ); } return; diff --git a/lib/node_modules/@stdlib/repl/lib/main.js b/lib/node_modules/@stdlib/repl/lib/main.js index 92b07878ed93..6b7f7bc5a6d3 100644 --- a/lib/node_modules/@stdlib/repl/lib/main.js +++ b/lib/node_modules/@stdlib/repl/lib/main.js @@ -234,9 +234,6 @@ function REPL( options ) { setNonEnumerable( this._multiline, 'active', false ); setNonEnumerable( this._multiline, 'mode', 'incomplete_expression' ); - // Initialize an internal flag indicating whether a user is navigating TAB completions: - setNonEnumerable( this, '_isNavigatingCompletions', false ); - // Initialize an internal flag indicating whether the REPL has been closed: setNonEnumerable( this, '_closed', false ); From cc2f3c58aa85155f23c67c7cfffaf0596ae9da4e Mon Sep 17 00:00:00 2001 From: Snehil Shah Date: Fri, 26 Apr 2024 00:01:36 +0000 Subject: [PATCH 22/24] refactor: abstract logics into private methods Signed-off-by: Snehil Shah --- .../@stdlib/repl/lib/completer_engine.js | 446 +++++++++++------- 1 file changed, 275 insertions(+), 171 deletions(-) diff --git a/lib/node_modules/@stdlib/repl/lib/completer_engine.js b/lib/node_modules/@stdlib/repl/lib/completer_engine.js index c1f211d62d27..afb3ae88172d 100644 --- a/lib/node_modules/@stdlib/repl/lib/completer_engine.js +++ b/lib/node_modules/@stdlib/repl/lib/completer_engine.js @@ -16,7 +16,7 @@ * limitations under the License. */ -/* eslint-disable no-restricted-syntax, no-underscore-dangle, no-invalid-this */ +/* eslint-disable no-restricted-syntax, no-underscore-dangle, no-invalid-this, max-lines */ 'use strict'; @@ -117,13 +117,14 @@ function CompleterEngine( repl, completer, ostream, ttyWrite ) { // Initialize a buffer containing the list of highlighted completions: this._highlightedCompletions = []; - // Initialize a buffer storing the completion output's dimensions and indexes: - this._output = {}; - this._output.columns = -1; - this._output.rows = -1; - this._output.widthOfColumn = -1; - this._output.completionsLength = []; - this._output.index = -1; // track index of current completion + // 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; } @@ -209,6 +210,130 @@ setNonEnumerableReadOnly( CompleterEngine.prototype, '_completionCallback', func } }); +/** +* Displays the completions to the output stream. +* +* @private +* @name _displayCompletions +* @memberof CompleterEngine.prototype +* @returns {void} +*/ +setNonEnumerableReadOnly( CompleterEngine.prototype, '_displayCompletions', function displayCompletions() { + var columns; + var rows; + 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 = max( 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 ); + + // 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 + 1 ) ); + readline.cursorTo( this._ostream, this._rli.cursor + this.repl._inputPrompt.length - 1 ); // eslint-disable-line max-len +}); + +/** +* Re-displays the navigated completions to the output stream. +* +* @private +* @name _updateCompletions +* @memberof CompleterEngine.prototype +* @returns {void} +*/ +setNonEnumerableReadOnly( CompleterEngine.prototype, '_updateCompletions', function updateCompletions() { + var columns; + var rows; + + // Determine dimensions of the output grid: + columns = this._completionColumns(); + rows = this._completionRows( columns ); + + // Clear previous completions output: + readline.clearScreenDown( this._ostream ); + + // 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 + 1 ) ); + readline.cursorTo( this._ostream, this.repl._inputPrompt.length - 1 ); + 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; +}); + +/** +* Draws the completions output grid. +* +* @private +* @name _drawOutput +* @memberof CompleterEngine.prototype +* @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 ( lineIndex >= columns ) { + // If completions start overflowing the maximum allowed rows, stop writing to output... + if ( i >= rows * columns ) { + break; + } + // 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. * @@ -268,136 +393,154 @@ setNonEnumerableReadOnly( CompleterEngine.prototype, '_highlightCompletions', fu }); /** -* Displays the completions to the output stream. +* Returns the number of columns in the completions output grid. +* +* @name _completionColumns +* @memberof CompleterEngine.prototype +* @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 +* @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 = this.repl.viewportHeight() - 2; // two rows reserved for the input prompt and an empty line at the end of viewport + var rows = ceil( this._completionsList.length / columns ); + + // Truncate number of completion rows to fit the viewport: + return min( rows, maxRows ); +}); + +/** +* Closes completer engine. * * @private -* @name _displayCompletions +* @name _closeCompleter * @memberof CompleterEngine.prototype * @returns {void} */ -setNonEnumerableReadOnly( CompleterEngine.prototype, '_displayCompletions', function displayCompletions() { - var whitespaces; - var completion; - var lineIndex; - var output; - var i; +setNonEnumerableReadOnly( CompleterEngine.prototype, '_closeCompleter', function closeCompleter() { + // Reset completer parameters: + this._isNavigating = false; + this._idx = -1; - // Determine number of columns of completions that should be displayed to the output stream - this._output.completionsLength = []; - for ( i = 0; i < this._completionsList.length; i++ ) { - this._output.completionsLength.push( this._completionsList[ i ].length ); // eslint-disable-line max-len - } - this._output.widthOfColumn = max( this._output.completionsLength.length, this._output.completionsLength, 1 ) + 4; // eslint-disable-line max-len - this._output.columns = floor( this._ostream.columns / this._output.widthOfColumn ) || 1; // eslint-disable-line max-len - this._output.rows = min( ceil( this._completionsList.length / this._output.columns ), this._ostream.rows - 5 ); // eslint-disable-line max-len + // Clear completions output: + readline.clearScreenDown( this._ostream ); - // Highlight completions if operating in "terminal" mode... - if ( this.repl._isTTY ) { - this._highlightedCompletions = this._highlightCompletions(); - } else { - this._highlightedCompletions = this._completionsList; - } + // Reset the internal completer buffers: + this._inputLine = ''; + this._remainingLine = ''; + this._completionPrefix = ''; + this._completionsList = []; + this._highlightedCompletions = []; + this._completionsLength = []; + this._widthOfColumn = -1; +}); - output = '\r\n'; - lineIndex = 0; - whitespaces = 0; - for ( i = 0; i < this._highlightedCompletions.length; i++ ) { - completion = this._highlightedCompletions[ i ]; - if ( lineIndex === this._output.columns ) { - // If completions start overflowing terminal height, stop writing to output... - if ( ceil( i / this._output.columns ) >= this._output.rows ) { - break; - } - // 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 ); - } - // Add completion string to the column in output: - output += completion; - whitespaces = this._output.widthOfColumn - this._output.completionsLength[ i ]; // eslint-disable-line max-len - lineIndex += 1; +/** +* Navigate up the completions grid. +* +* @private +* @name _navigateUp +* @memberof CompleterEngine.prototype +* @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; } - // Ensure a space to even out the number of rows... - if ( lineIndex !== 0 ) { - output += '\r\n\r\n'; + // Move to the previous row: + if ( this._idx - columns >= 0 ) { + this._idx -= columns; + } else { + this._idx = -1; } - // Write completions to the output stream: - this._ostream.write( output ); - - // Bring the cursor back to the current line: - readline.moveCursor( this._ostream, 0, -1 * ( 2 + this._output.rows ) ); - readline.cursorTo( this._ostream, this._rli.cursor + this.repl._inputPrompt.length - 1 ); // eslint-disable-line max-len + this._updateCompletions(); }); /** -* Re-displays the navigated completions to the output stream. +* Navigate down the completions grid. * * @private -* @name _navigateCompletions +* @name _navigateDown * @memberof CompleterEngine.prototype * @returns {void} */ -setNonEnumerableReadOnly( CompleterEngine.prototype, '_navigateCompletions', function navigateCompletions() { - var whitespaces; - var completion; - var lineIndex; - var output; - var i; - - // Clear current completions output: - readline.clearScreenDown( this._ostream ); +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(); + } +}); - // Create the completions output after navigation: - output = '\r\n'; - lineIndex = 0; - whitespaces = 0; - for ( i = 0; i < this._highlightedCompletions.length; i++ ) { - completion = this._highlightedCompletions[ i ]; - if ( lineIndex === this._output.columns ) { - // If completions start overflowing the terminal, stop writing to output... - if ( ceil( i / this._output.columns ) >= this._output.rows ) { - break; - } - // 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._output.index && 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._output.widthOfColumn - this._output.completionsLength[ i ]; // eslint-disable-line max-len - lineIndex += 1; +/** +* Navigate to the left in the completions grid. +* +* @private +* @name _navigateLeft +* @memberof CompleterEngine.prototype +* @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 behaviour and stop navigating... + if ( this._idx === -1 ) { + this._closeCompleter(); + this._ttyWrite.call( this._rli, data, key ); + return; } - // Ensure a space to even out the number of rows... - if ( lineIndex !== 0 ) { - output += '\r\n\r\n'; + // If navigating, move back an index... + if ( this._idx > 0 ) { + this._idx -= 1; + this._updateCompletions(); } - // Write completions to the output stream: - this._ostream.write( output ); - - // Bring the cursor back to the current line: - readline.moveCursor( this._ostream, 0, -1 * ( 2 + this._output.rows ) ); - readline.cursorTo( this._ostream, this.repl._inputPrompt.length - 1 ); - 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._output.index ] || this._completionPrefix ) + this._remainingLine ); // eslint-disable-line max-len - readline.moveCursor( this._ostream, -1 * this._remainingLine.length ); - this._rli.cursor -= this._remainingLine.length; +/** +* Navigate to the right in the completions grid. +* +* @private +* @name _navigateRight +* @memberof CompleterEngine.prototype +* @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 behaviour 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(); + } }); /** @@ -417,78 +560,39 @@ setNonEnumerableReadOnly( CompleterEngine.prototype, 'beforeKeypress', function this._ttyWrite.call( this._rli, data, key ); return; } - // If the completions output is visible, allow navigating it... + + // If user is already viewing completions, allow navigating it... if ( this._isNavigating ) { + // If user presses TAB while navigating, toggle close the completer: if ( key.name === 'tab' ) { - // Stop navigating and hide the completions output: - readline.clearScreenDown( this._ostream ); + 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 - - this._output.index = -1; - this._isNavigating = false; return; } + // If arrow keys detected, allow navigating the completions, else stop navigating and continue default behaviour... if ( key.name === 'down' ) { - // Move to the next row... - if ( this._output.index === -1 ) { - this._output.index = 0; - this._navigateCompletions(); - } else if ( this._output.index + this._output.columns <= this._completionsList.length ) { // eslint-disable-line max-len - this._output.index += this._output.columns; - this._navigateCompletions(); - } + debug( 'Received a DOWN keypress event...' ); + this._navigateDown(); } else if ( key.name === 'up' ) { - // Move to the previous row or if already on the line, stop navigating and trigger default behaviour... - if ( this._output.index === -1 ) { - readline.clearScreenDown( this._ostream ); - this._isNavigating = false; - this._ttyWrite.call( this._rli, data, key ); - return; - } - if ( this._output.index - this._output.columns >= 0 ) { - this._output.index -= this._output.columns; - } else { - this._output.index = -1; - } - this._navigateCompletions(); + debug( 'Received an UP keypress event...' ); + this._navigateUp( data, key ); } else if ( key.name === 'left' ) { - // If on current line, trigger default behaviour and stop navigating... - if ( this._output.index === -1 ) { - readline.clearScreenDown( this._ostream ); - this._isNavigating = false; - this._ttyWrite.call( this._rli, data, key ); - return; - } - // If navigating, move back an index... - if ( this._output.index > 0 ) { - this._output.index -= 1; - this._navigateCompletions(); - } + debug( 'Received a LEFT keypress event...' ); + this._navigateLeft( data, key ); } else if ( key.name === 'right' ) { - // If on current line, trigger default behaviour and stop navigating... - if ( this._output.index === -1 ) { - readline.clearScreenDown( this._ostream ); - this._isNavigating = false; - this._ttyWrite.call( this._rli, data, key ); - return; - } - // If navigating, move ahead an index... - if ( this._output.index < this._completionsList.length - 1 ) { - this._output.index += 1; - this._navigateCompletions(); - } + debug( 'Received a RIGHT keypress event...' ); + this._navigateRight( data, key ); } else { - // For any other keypress, stop navigating and continue default behaviour... - readline.clearScreenDown( this._ostream ); - this._output.index = -1; - this._isNavigating = false; + this._closeCompleter(); this._ttyWrite.call( this._rli, data, key ); } return; } + // For other keypresses, don't trigger TAB completions: if ( key.name !== 'tab' ) { this._ttyWrite.call( this._rli, data, key ); From 85efce4f9fe59b8b3fc49e6ed427ec8f74b5d301 Mon Sep 17 00:00:00 2001 From: Snehil Shah Date: Fri, 26 Apr 2024 00:40:47 +0000 Subject: [PATCH 23/24] fix: make completer `SIGWINCH` aware Signed-off-by: Snehil Shah --- .../@stdlib/repl/lib/completer_engine.js | 69 +++++++++++++++++-- lib/node_modules/@stdlib/repl/lib/main.js | 1 + 2 files changed, 63 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 afb3ae88172d..84908adb1ad9 100644 --- a/lib/node_modules/@stdlib/repl/lib/completer_engine.js +++ b/lib/node_modules/@stdlib/repl/lib/completer_engine.js @@ -26,7 +26,8 @@ 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 max = require( '@stdlib/stats/base/max' ); +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' ); @@ -43,6 +44,7 @@ var commonPrefix = require( './longest_common_prefix.js' ); 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 = 2; // input prompt + empty line // FUNCTIONS // @@ -135,6 +137,7 @@ function CompleterEngine( repl, completer, ostream, ttyWrite ) { * @private * @name _completionCallback * @memberof CompleterEngine.prototype +* @type {Function} * @returns {Function} completion callback */ setNonEnumerableReadOnly( CompleterEngine.prototype, '_completionCallback', function completionCallback() { @@ -201,6 +204,13 @@ setNonEnumerableReadOnly( CompleterEngine.prototype, '_completionCallback', func } 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; @@ -216,6 +226,7 @@ setNonEnumerableReadOnly( CompleterEngine.prototype, '_completionCallback', func * @private * @name _displayCompletions * @memberof CompleterEngine.prototype +* @type {Function} * @returns {void} */ setNonEnumerableReadOnly( CompleterEngine.prototype, '_displayCompletions', function displayCompletions() { @@ -228,7 +239,7 @@ setNonEnumerableReadOnly( CompleterEngine.prototype, '_displayCompletions', func for ( i = 0; i < this._completionsList.length; i++ ) { this._completionsLength.push( this._completionsList[ i ].length ); } - this._widthOfColumn = max( this._completionsLength.length, this._completionsLength, 1 ) + 4; // eslint-disable-line max-len + 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 ) { @@ -255,6 +266,7 @@ setNonEnumerableReadOnly( CompleterEngine.prototype, '_displayCompletions', func * @private * @name _updateCompletions * @memberof CompleterEngine.prototype +* @type {Function} * @returns {void} */ setNonEnumerableReadOnly( CompleterEngine.prototype, '_updateCompletions', function updateCompletions() { @@ -290,6 +302,7 @@ setNonEnumerableReadOnly( CompleterEngine.prototype, '_updateCompletions', funct * @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 @@ -307,11 +320,12 @@ setNonEnumerableReadOnly( CompleterEngine.prototype, '_drawOutput', function dra 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 ) { - // If completions start overflowing the maximum allowed rows, stop writing to output... - if ( i >= rows * columns ) { - break; - } // Reached end of column, enter next line: output += '\r\n'; lineIndex = 0; @@ -340,6 +354,7 @@ setNonEnumerableReadOnly( CompleterEngine.prototype, '_drawOutput', function dra * @private * @name _highlightCompletions * @memberof CompleterEngine.prototype +* @type {Function} * @returns {Array} array of highlighted completions */ setNonEnumerableReadOnly( CompleterEngine.prototype, '_highlightCompletions', function highlightCompletions() { @@ -397,6 +412,7 @@ setNonEnumerableReadOnly( CompleterEngine.prototype, '_highlightCompletions', fu * * @name _completionColumns * @memberof CompleterEngine.prototype +* @type {Function} * @returns {number} number of columns */ setNonEnumerableReadOnly( CompleterEngine.prototype, '_completionColumns', function completionColumns() { @@ -408,23 +424,38 @@ setNonEnumerableReadOnly( CompleterEngine.prototype, '_completionColumns', funct * * @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 = this.repl.viewportHeight() - 2; // two rows reserved for the input prompt and an empty line at the end of viewport + var maxRows = max( this.repl.viewportHeight() - 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() { + return this.repl.viewportHeight() > RESERVED_COMPLETER_ROWS; +}); + /** * Closes completer engine. * * @private * @name _closeCompleter * @memberof CompleterEngine.prototype +* @type {Function} * @returns {void} */ setNonEnumerableReadOnly( CompleterEngine.prototype, '_closeCompleter', function closeCompleter() { @@ -451,6 +482,7 @@ setNonEnumerableReadOnly( CompleterEngine.prototype, '_closeCompleter', function * @private * @name _navigateUp * @memberof CompleterEngine.prototype +* @type {Function} * @param {string} data - input data * @param {Object} key - key object * @returns {void} @@ -479,6 +511,7 @@ setNonEnumerableReadOnly( CompleterEngine.prototype, '_navigateUp', function nav * @private * @name _navigateDown * @memberof CompleterEngine.prototype +* @type {Function} * @returns {void} */ setNonEnumerableReadOnly( CompleterEngine.prototype, '_navigateDown', function navigateDown() { @@ -501,6 +534,7 @@ setNonEnumerableReadOnly( CompleterEngine.prototype, '_navigateDown', function n * @private * @name _navigateLeft * @memberof CompleterEngine.prototype +* @type {Function} * @param {string} data - input data * @param {Object} key - key object * @returns {void} @@ -525,6 +559,7 @@ setNonEnumerableReadOnly( CompleterEngine.prototype, '_navigateLeft', function n * @private * @name _navigateRight * @memberof CompleterEngine.prototype +* @type {Function} * @param {string} data - input data * @param {Object} key - key object * @returns {void} @@ -548,6 +583,7 @@ setNonEnumerableReadOnly( CompleterEngine.prototype, '_navigateRight', function * * @name beforeKeypress * @memberof CompleterEngine.prototype +* @type {Function} * @param {string} data - input data * @param {Object} key - key object * @returns {void} @@ -613,6 +649,25 @@ setNonEnumerableReadOnly( CompleterEngine.prototype, 'beforeKeypress', function this._completer( this._inputLine, this._onCompletions, this._settings.fuzzyCompletions ); // eslint-disable-line max-len }); +/** +* 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 // diff --git a/lib/node_modules/@stdlib/repl/lib/main.js b/lib/node_modules/@stdlib/repl/lib/main.js index 6b7f7bc5a6d3..0d99fc1ca19e 100644 --- a/lib/node_modules/@stdlib/repl/lib/main.js +++ b/lib/node_modules/@stdlib/repl/lib/main.js @@ -387,6 +387,7 @@ function REPL( options ) { function onSIGWINCH() { debug( 'Received a SIGWINCH event. Terminal was resized.' ); self._ostream.onResize(); + self._completerEngine.onResize(); } /** From 023c31a0739ad38257a70dde39c3b94536b8154a Mon Sep 17 00:00:00 2001 From: Snehil Shah Date: Fri, 26 Apr 2024 01:15:14 +0000 Subject: [PATCH 24/24] test: update tests for TTY Signed-off-by: Snehil Shah --- .../integration/test.fuzzy_completions.js | 121 ++++++++++++------ 1 file changed, 84 insertions(+), 37 deletions(-) diff --git a/lib/node_modules/@stdlib/repl/test/integration/test.fuzzy_completions.js b/lib/node_modules/@stdlib/repl/test/integration/test.fuzzy_completions.js index 3e810fb1e245..d4e85bd1bdf4 100644 --- a/lib/node_modules/@stdlib/repl/test/integration/test.fuzzy_completions.js +++ b/lib/node_modules/@stdlib/repl/test/integration/test.fuzzy_completions.js @@ -24,6 +24,7 @@ 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' ); @@ -72,7 +73,7 @@ function extractCompletions( str ) { var out = []; var i; - cleanOutput = str.split('\r\n'); + cleanOutput = replace( str, RE_EOL, '' ).split( /\s+/ ); for ( i = 0; i < cleanOutput.length; i++ ) { if ( cleanOutput[i] !== '' ) { out.push( cleanOutput[ i ] ); @@ -100,7 +101,11 @@ tape( 'a REPL instance supports displaying TAB completions of user-defined varia }); opts = { 'input': istream, - 'settings': defaultSettings() + 'settings': defaultSettings(), + 'tty': { + 'rows': 100, + 'columns': 80 + } }; r = repl( opts, onClose ); @@ -129,7 +134,7 @@ tape( 'a REPL instance supports displaying TAB completions of user-defined varia return; } - actual = extractCompletions( stripANSI( data[ data.length-2 ] ) ); + actual = extractCompletions( stripANSI( data[ data.length - 3 ] ) ); // Check for three completions in the output: t.strictEqual( actual.length, 3, 'returns expected value' ); @@ -139,7 +144,8 @@ tape( 'a REPL instance supports displaying TAB completions of user-defined varia t.strictEqual( actual[ 1 ], 'zzpqr', 'returns expected value' ); t.strictEqual( actual[ 2 ], 'zzxyz', 'returns expected value' ); - // `data[ data.length-1 ]` brings the cursor to the current line... + // Check if the cursor is returned back to the prompt: + t.strictEqual( data[ data.length - 2 ], '\x1B[2A', 'returns expected value' ); t.end(); } @@ -155,7 +161,11 @@ tape( 'a REPL instance supports displaying `fuzzy` TAB completions of user-defin }); opts = { 'input': istream, - 'settings': defaultSettings() + 'settings': defaultSettings(), + 'tty': { + 'rows': 100, + 'columns': 80 + } }; r = repl( opts, onClose ); @@ -186,7 +196,7 @@ tape( 'a REPL instance supports displaying `fuzzy` TAB completions of user-defin return; } - actual = extractCompletions( stripANSI( data[ data.length-2 ] ) ); + actual = extractCompletions( stripANSI( data[ data.length - 3 ] ) ); // Check for two fuzzy completions in the output: t.strictEqual( actual.length, 2, 'returns expected value' ); @@ -195,7 +205,8 @@ tape( 'a REPL instance supports displaying `fuzzy` TAB completions of user-defin t.strictEqual( actual[ 0 ], 'jazz', 'returns expected value' ); t.strictEqual( actual[ 1 ], 'pizza', 'returns expected value' ); - // `data[ data.length-1 ]` brings the cursor to the current line... + // Check if the cursor is returned back to the prompt: + t.strictEqual( data[ data.length - 2 ], '\x1B[2A', 'returns expected value' ); t.end(); } @@ -211,7 +222,11 @@ tape( 'a REPL instance doesn\'t display `fuzzy` completions if `exact` completio }); opts = { 'input': istream, - 'settings': defaultSettings() + 'settings': defaultSettings(), + 'tty': { + 'rows': 100, + 'columns': 80 + } }; r = repl( opts, onClose ); @@ -247,7 +262,7 @@ tape( 'a REPL instance doesn\'t display `fuzzy` completions if `exact` completio return; } - actual = extractCompletions( stripANSI( data[ data.length-2 ] ) ); + actual = extractCompletions( stripANSI( data[ data.length - 3 ] ) ); // Check for three exact completions in the output: t.strictEqual( actual.length, 3, 'returns expected value' ); @@ -257,7 +272,8 @@ tape( 'a REPL instance doesn\'t display `fuzzy` completions if `exact` completio t.strictEqual( actual[ 1 ], 'zzpqr', 'returns expected value' ); t.strictEqual( actual[ 2 ], 'zzxyz', 'returns expected value' ); - // `data[ data.length-1 ]` brings the cursor to the current line... + // Check if the cursor is returned back to the prompt: + t.strictEqual( data[ data.length - 2 ], '\x1B[2A', 'returns expected value' ); t.end(); } @@ -273,7 +289,11 @@ tape( 'a REPL instance supports displaying TAB completions with input characters }); opts = { 'input': istream, - 'settings': defaultSettings() + 'settings': defaultSettings(), + 'tty': { + 'rows': 100, + 'columns': 80 + } }; r = repl( opts, onClose ); @@ -304,7 +324,7 @@ tape( 'a REPL instance supports displaying TAB completions with input characters return; } - actual = extractCompletions( data[ data.length-2 ] ); + actual = extractCompletions( data[ data.length - 3 ] ); // Check for two fuzzy completions in the output: t.strictEqual( actual.length, 2, 'returns expected value' ); @@ -313,7 +333,8 @@ tape( 'a REPL instance supports displaying TAB completions with input characters t.strictEqual( actual[ 0 ], 'ja\x1B[1mz\x1B[0m\x1B[1mz\x1B[0m', 'returns expected value' ); t.strictEqual( actual[ 1 ], 'pi\x1B[1mz\x1B[0m\x1B[1mz\x1B[0ma', 'returns expected value' ); - // `data[ data.length-1 ]` brings the cursor to the current line... + // Check if the cursor is returned back to the prompt: + t.strictEqual( data[ data.length - 2 ], '\x1B[2A', 'returns expected value' ); t.end(); } @@ -329,7 +350,11 @@ tape( 'a REPL instance supports hiding the completions panel upon pressing TAB a }); opts = { 'input': istream, - 'settings': defaultSettings() + 'settings': defaultSettings(), + 'tty': { + 'rows': 100, + 'columns': 80 + } }; r = repl( opts, onClose ); @@ -362,9 +387,9 @@ tape( 'a REPL instance supports hiding the completions panel upon pressing TAB a } // Check if the completions were cleared: - t.strictEqual( data[ data.length-2 ], '\x1B[0J', 'returns expected value' ); + t.strictEqual( data[ data.length - 2 ], '\x1B[0J', 'returns expected value' ); - // `data[ data.length-1 ]` adds the remaining string to the right of the cursor because of the abnormal behaviour of `clearScreenDown`... + // NOTE: `data[ data.length-1 ]` adds the remaining string to the right of the cursor because of the abnormal behaviour of `clearScreenDown`... t.end(); } @@ -380,7 +405,11 @@ tape( 'a REPL instance supports navigating the TAB completions using arrow keys' }); opts = { 'input': istream, - 'settings': defaultSettings() + 'settings': defaultSettings(), + 'tty': { + 'rows': 100, + 'columns': 80 + } }; r = repl( opts, onClose ); @@ -418,38 +447,35 @@ tape( 'a REPL instance supports navigating the TAB completions using arrow keys' } // Check for completions before navigation: - actual = extractCompletions( stripANSI( data[ data.length-12 ] ) ); + actual = extractCompletions( stripANSI( data[ data.length - 15 ] ) ); t.strictEqual( actual.length, 2, 'returns expected value' ); t.strictEqual( actual[ 0 ], 'jazz', 'returns expected value' ); t.strictEqual( actual[ 1 ], 'pizza', 'returns expected value' ); - - // `data[ data.length-11 ]` brings cursor to the current line... + t.strictEqual( data[ data.length - 14 ], '\x1B[2A', 'returns expected value' ); // bring cursor back // Screen is cleared after `down` arrow key to re-display the navigated output: - t.strictEqual( data[ data.length-10 ], '\x1B[0J', 'returns expected value' ); + t.strictEqual( data[ data.length-12 ], '\x1B[0J', 'returns expected value' ); // Check for completions after `down` arrow: - actual = extractCompletions( data[ data.length-9 ] ); + actual = extractCompletions( data[ data.length - 11 ] ); // First completion should be highlighted: t.strictEqual( actual[ 0 ], '\x1B[7mjazz\x1B[27m', 'returns expected value' ); - - // `data[ data.length-8 ]` brings cursor to the current line... + t.strictEqual( data[ data.length - 10 ], '\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 pizza = 1;var jazz = 2;jazz', 'returns expected value' ); + t.strictEqual( data[ data.length-8 ], '\x1B[0K', 'returns expected value' ); + t.strictEqual( data[ data.length-7 ], 'var pizza = 1;var jazz = 2;jazz', 'returns expected value' ); // Screen is cleared after `right` arrow key to re-display the navigated output: - t.strictEqual( data[ data.length-5 ], '\x1B[0J', 'returns expected value' ); + t.strictEqual( data[ data.length - 6 ], '\x1B[0J', 'returns expected value' ); // Check for completions after `right` arrow key: - actual = extractCompletions( data[ data.length-4 ] ); + actual = extractCompletions( data[ data.length - 5 ] ); // Second completion should be highlighted: t.strictEqual( actual[ 1 ], '\x1B[7mpizza\x1B[27m', 'returns expected value' ); - - // `data[ data.length-3 ]` brings cursor to the current line... + 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' ); @@ -469,7 +495,11 @@ tape( 'a REPL instance supports bringing back the original line upon navigating }); opts = { 'input': istream, - 'settings': defaultSettings() + 'settings': defaultSettings(), + 'tty': { + 'rows': 100, + 'columns': 80 + } }; r = repl( opts, onClose ); @@ -519,7 +549,11 @@ tape( 'a REPL instance supports displaying highlighted arguement TAB completions }); opts = { 'input': istream, - 'settings': defaultSettings() + 'settings': defaultSettings(), + 'tty': { + 'rows': 100, + 'columns': 80 + } }; r = repl( opts, onClose ); @@ -544,11 +578,12 @@ tape( 'a REPL instance supports displaying highlighted arguement TAB completions } // Check for settings name completions in the output: - actual = extractCompletions( data[ data.length-2 ] ); + 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' ); - // `data[ data.length-1 ]` brings the cursor to the current line... + // Check if the cursor is returned back to the prompt: + t.strictEqual( data[ data.length - 2 ], '\x1B[2A', 'returns expected value' ); t.end(); } @@ -564,7 +599,11 @@ tape( 'a REPL instance supports auto-completing common prefixes when hitting TAB }); opts = { 'input': istream, - 'settings': defaultSettings() + 'settings': defaultSettings(), + 'tty': { + 'rows': 100, + 'columns': 80 + } }; r = repl( opts, onClose ); @@ -611,7 +650,11 @@ tape( 'a REPL instance supports auto-completing a single fuzzy completion for th }); opts = { 'input': istream, - 'settings': defaultSettings() + 'settings': defaultSettings(), + 'tty': { + 'rows': 100, + 'columns': 80 + } }; r = repl( opts, onClose ); @@ -659,7 +702,11 @@ tape( 'a REPL instance supports auto-completing common arguement prefixes when h }); opts = { 'input': istream, - 'settings': defaultSettings() + 'settings': defaultSettings(), + 'tty': { + 'rows': 100, + 'columns': 80 + } }; r = repl( opts, onClose );