diff --git a/lib/node_modules/@stdlib/repl/README.md b/lib/node_modules/@stdlib/repl/README.md index 3e7f7bfb3947..560bf19cc4b8 100644 --- a/lib/node_modules/@stdlib/repl/README.md +++ b/lib/node_modules/@stdlib/repl/README.md @@ -83,6 +83,7 @@ The function supports specifying the following settings: - **autoDeletePairs**: boolean indicating whether to automatically delete adjacent matching brackets, parentheses, and quotes. Default: `true`. - **autoPage**: boolean indicating whether to automatically page return values having a display size exceeding the visible screen. When streams are TTY, the default is `true`; otherwise, the default is `false`. - **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() diff --git a/lib/node_modules/@stdlib/repl/lib/complete_expression.js b/lib/node_modules/@stdlib/repl/lib/complete_expression.js index 64b664344fad..fde15a5c57eb 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 strictly 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,9 +106,22 @@ 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, filter, false ); + out = filterByPrefix( out, objectKeys( context ), filter, false ); + out = filterByPrefix( out, ast.locals, filter, false ); + out.sort(); + + // 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; } // Find the identifier or member expression to be completed: @@ -177,7 +196,17 @@ 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 + + // 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; } // Case: `foo[<|>` || `foo[bar<|>` @@ -190,7 +219,17 @@ 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 + + // 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; } // Case: `foo[bar.<|>` || `foo[bar.beep<|>` || `foo[bar.beep.<|>` || `foo[bar[beep<|>` || etc @@ -216,15 +255,37 @@ 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 ); + + // 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; } // 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, filter, false ); + out = filterByPrefix( out, objectKeys( context ), filter, false ); + out = filterByPrefix( out, resolveLocalScope( ast, node ), filter, false ); + + // 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 d4d70bef5f48..2cf36f2fde4d 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 strictly 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,50 @@ function complete( out, expression, alias, path ) { continue; } } + out.sort(); + + // Only fuzzy search, when no exact candidates found... + if ( !isFuzzy || out.length !== 0 ) { + 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..e6a7f36401f5 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 strictly 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,89 @@ function complete( out, path, paths, exts ) { } } } + out.sort(); + + // Only fuzzy search, when no exact candidates found... + if ( !isFuzzy || out.length !== 0 ) { + 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_settings.js b/lib/node_modules/@stdlib/repl/lib/complete_settings.js index 93672a1f4da7..8419c620cc9b 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( match ); + } 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 d66e1be6ca39..c23e79b1aa82 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 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; @@ -84,6 +89,27 @@ function complete( out, repl, expression, alias, value ) { debug( '%s does not match filter %s. Skipping...', t, 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 < TUTS.length; i++ ) { + t = TUTS[ i ]; + match = fuzzyMatch( t, value ); + if ( match ) { + debug( 'Found a fuzzy completion: %s', t ); + fuzzyResults.push( match ); + } 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..9378edbffeb6 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 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; @@ -85,6 +90,27 @@ function complete( out, repl, expression, alias, value ) { debug( '%s does not match filter %s. Skipping...', w, 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 < ws.length; i++ ) { + w = ws[ i ]; + match = fuzzyMatch( w, value ); + if ( match ) { + debug( 'Found a fuzzy completion: %s', w ); + fuzzyResults.push( match ); + } 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 5871b7887ae2..488679b5cce8 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' ); @@ -41,7 +43,7 @@ var completeExpression = require( './complete_expression.js' ); // VARIABLES // -var debug = logger( 'repl:completer' ); +var debug = logger( 'repl:completer:callback' ); // FUNCTIONS // @@ -51,9 +53,10 @@ var debug = logger( 'repl:completer' ); * * @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,10 +69,12 @@ function normalize( list ) { } list = objectKeys( hash ); - // TODO: sort such that printed columns are in lexicographic order, not rows, similar to bash behavior! + // Limit fuzzy completions to 10... + if ( isFuzzy ) { + list = slice( list, 0, 10 ); + } - // Sort the values in lexicographic order: - list = list.sort(); + // TODO: sort such that printed columns are in lexicographic order, not rows, similar to bash behavior! return list; } @@ -93,9 +98,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 strictly fuzzy * @returns {void} */ - function complete( line, clbk ) { + function complete( line, clbk, isFuzzy ) { var match; var exts; var res; @@ -113,8 +119,8 @@ 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 - res = normalize( res ); + line = completeRequire( res, match[ 1 ], repl._context.module.paths, exts, isFuzzy ); // eslint-disable-line max-len + res = normalize( res, res[ 0 ] && !startsWith( res[ 0 ], line ) ); debug( 'Completion filter: %s', line ); debug( 'Results: %s', res.join( ', ' ) ); @@ -128,8 +134,8 @@ 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 ] ); - res = normalize( res ); + line = completeFS( res, match[ 0 ], match[ 1 ], match[ 3 ], isFuzzy ); // eslint-disable-line max-len + res = normalize( res, res[ 0 ] && !startsWith( res[ 0 ], line ) ); debug( 'Completion filter: %s', line ); debug( 'Results: %s', res.join( ', ' ) ); @@ -143,8 +149,8 @@ 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 - res = normalize( res ); + line = completeWorkspace( res, repl, match[ 0 ], match[ 1 ], match[ 3 ], isFuzzy ); // eslint-disable-line max-len + res = normalize( res, res[ 0 ] && !startsWith( res[ 0 ], line ) ); debug( 'Completion filter: %s', line ); debug( 'Results: %s', res.join( ', ' ) ); @@ -158,8 +164,8 @@ 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 - res = normalize( res ); + line = completeTutorial( res, repl, match[ 0 ], match[ 1 ], match[ 3 ], isFuzzy ); // eslint-disable-line max-len + res = normalize( res, res[ 0 ] && !startsWith( res[ 0 ], line ) ); debug( 'Completion filter: %s', line ); debug( 'Results: %s', res.join( ', ' ) ); @@ -173,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, res[ 0 ] && !startsWith( res[ 0 ], line ) ); debug( 'Completion filter: %s', line ); debug( 'Results: %s', res.join( ', ' ) ); @@ -187,8 +193,8 @@ function completer( repl ) { return clbk( null, [ res, line ] ); } debug( 'Attempting to complete an incomplete expression.' ); - line = completeExpression( res, repl._context, line ); - res = normalize( res ); + line = completeExpression( res, repl._context, line, isFuzzy ); + res = normalize( res, res[ 0 ] && !startsWith( res[ 0 ], line ) ); 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 new file mode 100644 index 000000000000..84908adb1ad9 --- /dev/null +++ b/lib/node_modules/@stdlib/repl/lib/completer_engine.js @@ -0,0 +1,674 @@ +/** +* @license Apache-2.0 +* +* Copyright (c) 2024 The Stdlib Authors. +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +/* eslint-disable no-restricted-syntax, no-underscore-dangle, no-invalid-this, max-lines */ + +'use strict'; + +// MODULES // + +var readline = require( 'readline' ); +var logger = require( 'debug' ); +var setNonEnumerableReadOnly = require( '@stdlib/utils/define-nonenumerable-read-only-property' ); +var lowercase = require( '@stdlib/string/lowercase' ); +var maxInArray = require( '@stdlib/stats/base/max' ); +var max = require( '@stdlib/math/base/special/max' ); +var min = require( '@stdlib/math/base/special/min' ); +var floor = require( '@stdlib/math/base/special/floor' ); +var ceil = require( '@stdlib/math/base/special/ceil' ); +var repeat = require( '@stdlib/string/repeat' ); +var replace = require( '@stdlib/string/replace' ); +var removeLast = require( '@stdlib/string/remove-last' ); +var contains = require( '@stdlib/array/base/assert/contains' ); +var startsWith = require( '@stdlib/string/starts-with' ); +var isEmptyString = require( '@stdlib/assert/is-empty-string' ).isPrimitive; +var commonPrefix = require( './longest_common_prefix.js' ); + + +// VARIABLES // + +var debug = logger( 'repl:completer:engine' ); +var RE_ANSI = /[\u001B\u009B][[\]()#;?]*(?:(?:(?:(?:;[-a-zA-Z\d/#&.:=?%@~_]+)*|[a-zA-Z\d]+(?:;[-a-zA-Z\d/#&.:=?%@~_]*)*)?\u0007)|(?:(?:\d{1,4}(?:;\d{0,4})*)?[\dA-PR-TZcf-nq-uy=><~]))/g; // eslint-disable-line no-control-regex +var RESERVED_COMPLETER_ROWS = 2; // input prompt + empty line + + +// FUNCTIONS // + +/** +* Removes ANSI escape codes from a string. +* +* @private +* @param {string} str - input string +* @returns {string} string with ANSI escape codes removed +*/ +function stripANSI( str ) { + return replace( str, RE_ANSI, '' ); +} + + +// MAIN // + +/** +* Constructor for creating a completer engine. +* +* @private +* @constructor +* @param {REPL} repl - REPL instance +* @param {Function} completer - function for generating possible completions +* @param {WritableStream} ostream - writable stream +* @param {Function} ttyWrite - function to trigger the default behaviour of the keypress +* @returns {CompleterEngine} completer engine instance +*/ +function CompleterEngine( repl, completer, ostream, ttyWrite ) { + 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; + + // 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; + + // Initialize a flag indicating whether a user is navigating TAB completions: + this._isNavigating = false; + + // Create a callback for processing completions: + this._onCompletions = this._completionCallback(); + + // Initialize a buffer containing the input line being processed: + this._inputLine = ''; + + // Initialize a buffer containing the remaining line after cursor: + this._remainingLine = ''; + + // Initialize a buffer containing the completion prefix: + this._completionPrefix = ''; + + // Initialize a buffer containing the list of generated completions: + this._completionsList = []; + + // Initialize a buffer containing the list of highlighted completions: + this._highlightedCompletions = []; + + // Initialize a buffer array storing the lengths of all completions: + this._completionsLength = []; + + // Initialize a buffer storing the width of a column: + this._widthOfColumn = -1; + + // Initialize a buffer to store the index of the current completion: + this._idx = -1; + + return this; +} + +/** +* Returns a callback for processing completions. +* +* @private +* @name _completionCallback +* @memberof CompleterEngine.prototype +* @type {Function} +* @returns {Function} completion callback +*/ +setNonEnumerableReadOnly( CompleterEngine.prototype, '_completionCallback', function completionCallback() { + var self = this; + return clbk; + + /** + * Callback invoked upon resolving potential completions. + * + * @private + * @param {(Error|null)} error - error object + * @param {Array} completions - completion results + * @returns {void} + */ + function clbk( error, completions ) { + var autoCompletion; + var i; + + // Check whether we encountered an error when generating completions... + if ( error ) { + debug( 'Encountered an error when generating completions.' ); + self._ostream.write( 'Error: couldn\'t generate tab completions' ); + + // Resume the input stream: + self._rli.resume(); + return; + } + // Remove empty completions: + self._completionsList = []; + for ( i = 0; i < completions[ 0 ].length; i++ ) { + if ( !isEmptyString( completions[ 0 ][ i ] ) ) { + self._completionsList.push( completions[ 0 ][ i ] ); + } + } + if ( self._completionsList.length === 0 ) { + debug( 'No completions to display.' ); + + // Resume the input stream: + self._rli.resume(); + return; + } + self._completionPrefix = completions[ 1 ]; + + // Resolve a common prefix from the completion results: + autoCompletion = commonPrefix( self._completionsList ); // e.g., [ 'back', 'background', 'backward' ] => 'back' + + // If the completion candidates have a possible auto-completion (ie. a common prefix longer than the input), auto-complete it... + if ( autoCompletion !== '' && autoCompletion.length > self._completionPrefix.length ) { + debug( 'Found an auto-completion candidate: %s', autoCompletion ); + + // Clear the completion prefix: + self._ostream.write( repeat( '\x08', self._completionPrefix.length ) ); + self._rli.line = self._rli.line.slice( 0, self._rli.cursor - self._completionPrefix.length ) + self._rli.line.slice( self._rli.cursor ); // eslint-disable-line max-len + + // Move the cursor to the start of completion prefix: + self._rli.cursor -= self._completionPrefix.length; + + // Write the auto-completion string: + self._rli.write( autoCompletion ); + + // Resume the input stream: + self._rli.resume(); + return; + } + debug( 'No auto-completion candidate, displaying all possible completions.' ); + + // Check if completions can be displayed... + if ( !self._isDisplayable() ) { + debug( 'TTY height too short. exiting completer...' ); + self._rli.resume(); + return; + } + + // Display completions: + self._displayCompletions(); + self._isNavigating = true; + + // Resume the input stream: + self._rli.resume(); + } +}); + +/** +* Displays the completions to the output stream. +* +* @private +* @name _displayCompletions +* @memberof CompleterEngine.prototype +* @type {Function} +* @returns {void} +*/ +setNonEnumerableReadOnly( CompleterEngine.prototype, '_displayCompletions', function displayCompletions() { + var columns; + var rows; + var i; + + // Determine number of columns of completions that should be displayed to the output stream + this._completionsLength = []; + for ( i = 0; i < this._completionsList.length; i++ ) { + this._completionsLength.push( this._completionsList[ i ].length ); + } + this._widthOfColumn = maxInArray( this._completionsLength.length, this._completionsLength, 1 ) + 4; // eslint-disable-line max-len + + // Highlight completions if operating in "terminal" mode... + if ( this.repl._isTTY ) { + this._highlightedCompletions = this._highlightCompletions(); + } else { + this._highlightedCompletions = this._completionsList; + } + + // Determine dimensions of the output grid: + columns = this._completionColumns(); + rows = this._completionRows( columns ); + + // 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 +* @type {Function} +* @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 +* @type {Function} +* @param {number} rows - number of rows in output grid +* @param {number} columns - number of columns in output grid +* @returns {string} output string +*/ +setNonEnumerableReadOnly( CompleterEngine.prototype, '_drawOutput', function drawOutput( rows, columns ) { + var whitespaces; + var completion; + var lineIndex; + var output; + var i; + + // Draw the output grid: + output = '\r\n'; + lineIndex = 0; + whitespaces = 0; + for ( i = 0; i < this._highlightedCompletions.length; i++ ) { + completion = this._highlightedCompletions[ i ]; + + // If completions start overflowing the maximum allowed rows, stop writing to output... + if ( i >= rows * columns ) { + break; + } + if ( lineIndex >= columns ) { + // Reached end of column, enter next line: + output += '\r\n'; + lineIndex = 0; + whitespaces = 0; + } else { + // Fill the space to move to the next column: + output += repeat( ' ', whitespaces ); + } + if ( i === this._idx && this.repl._isTTY ) { + // Highlight the current navigated index: + completion = stripANSI( completion ); + completion = '\u001b[7m' + completion + '\u001b[27m'; + } + // Add completion string to the column in output: + output += completion; + whitespaces = this._widthOfColumn - this._completionsLength[ i ]; + lineIndex += 1; + } + output += '\r\n'; + return output; +}); + +/** +* Highlights the matching parts of the completions based on the current line. +* +* @private +* @name _highlightCompletions +* @memberof CompleterEngine.prototype +* @type {Function} +* @returns {Array} array of highlighted completions +*/ +setNonEnumerableReadOnly( CompleterEngine.prototype, '_highlightCompletions', function highlightCompletions() { + var highlightedCompletions; + var lastMatchedIndex; + var completionIndex; + var highlighted; + var boldIndexes; + var completion; + var lineIndex; + var i; + var j; + + highlightedCompletions = []; + for ( i = 0; i < this._completionsList.length; i++) { + completion = this._completionsList[ i ]; + lastMatchedIndex = -1; + completionIndex = 0; + highlighted = ''; + boldIndexes = []; // Buffer to store indexes of characters in completion string that needs to be highlighted + lineIndex = 0; + + // If input is an exact prefix of completion, directly highlight the substring... + if ( startsWith( completion, this._completionPrefix ) ) { + highlighted = '\u001b[1m' + completion.slice( 0, this._completionPrefix.length ) + '\u001b[0m' + completion.slice( this._completionPrefix.length ); + } else { + // Store indexes of each matching character in the completion string in the buffer... + while ( lineIndex < this._completionPrefix.length && completionIndex < completion.length ) { // eslint-disable-line max-len + if ( lowercase( completion[ completionIndex ] ) === lowercase( this._completionPrefix[ lineIndex ] ) ) { // eslint-disable-line max-len + boldIndexes.push( completionIndex ); + lastMatchedIndex = completionIndex; + lineIndex += 1; + } else if ( completionIndex + 1 === completion.length ) { + lineIndex += 1; + completionIndex = lastMatchedIndex + 1; + } + completionIndex += 1; + } + // Highlight stored indexes in the completion string: + for ( j = 0; j < completion.length; j++ ) { + if ( contains( boldIndexes, j ) ) { + highlighted += '\u001b[1m' + completion[ j ] + '\u001b[0m'; + } else { + highlighted += completion[ j ]; + } + } + } + highlightedCompletions.push( highlighted ); + } + return highlightedCompletions; +}); + +/** +* Returns the number of columns in the completions output grid. +* +* @name _completionColumns +* @memberof CompleterEngine.prototype +* @type {Function} +* @returns {number} number of columns +*/ +setNonEnumerableReadOnly( CompleterEngine.prototype, '_completionColumns', function completionColumns() { + return floor( this.repl.viewportWidth() / this._widthOfColumn ) || 1; +}); + +/** +* Returns the number of rows in the completions output grid. +* +* @name _completionRows +* @memberof CompleterEngine.prototype +* @type {Function} +* @param {number} columns - number of columns in the completions output grid +* @returns {number} number of rows +*/ +setNonEnumerableReadOnly( CompleterEngine.prototype, '_completionRows', function completionRows( columns ) { + var maxRows = max( this.repl.viewportHeight() - 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() { + // Reset completer parameters: + this._isNavigating = false; + this._idx = -1; + + // Clear completions output: + readline.clearScreenDown( this._ostream ); + + // Reset the internal completer buffers: + this._inputLine = ''; + this._remainingLine = ''; + this._completionPrefix = ''; + this._completionsList = []; + this._highlightedCompletions = []; + this._completionsLength = []; + this._widthOfColumn = -1; +}); + +/** +* Navigate up the completions grid. +* +* @private +* @name _navigateUp +* @memberof CompleterEngine.prototype +* @type {Function} +* @param {string} data - input data +* @param {Object} key - key object +* @returns {void} +*/ +setNonEnumerableReadOnly( CompleterEngine.prototype, '_navigateUp', function navigateUp( data, key ) { + var columns = this._completionColumns(); + + // If already on the line, close the completer... + if ( this._idx === -1 ) { + this._closeCompleter(); + this._ttyWrite.call( this._rli, data, key ); + return; + } + // Move to the previous row: + if ( this._idx - columns >= 0 ) { + this._idx -= columns; + } else { + this._idx = -1; + } + this._updateCompletions(); +}); + +/** +* Navigate down the completions grid. +* +* @private +* @name _navigateDown +* @memberof CompleterEngine.prototype +* @type {Function} +* @returns {void} +*/ +setNonEnumerableReadOnly( CompleterEngine.prototype, '_navigateDown', function navigateDown() { + var columns = this._completionColumns(); + var rows = this._completionRows( columns ); + + // Move to the next row... + if ( this._idx === -1 ) { + this._idx = 0; + this._updateCompletions(); + } else if ( this._idx + columns < rows * columns ) { + this._idx += columns; + this._updateCompletions(); + } +}); + +/** +* Navigate to the left in the completions grid. +* +* @private +* @name _navigateLeft +* @memberof CompleterEngine.prototype +* @type {Function} +* @param {string} data - input data +* @param {Object} key - key object +* @returns {void} +*/ +setNonEnumerableReadOnly( CompleterEngine.prototype, '_navigateLeft', function navigateLeft( data, key ) { + // If on current line, trigger default behaviour and stop navigating... + if ( this._idx === -1 ) { + this._closeCompleter(); + this._ttyWrite.call( this._rli, data, key ); + return; + } + // If navigating, move back an index... + if ( this._idx > 0 ) { + this._idx -= 1; + this._updateCompletions(); + } +}); + +/** +* Navigate to the right in the completions grid. +* +* @private +* @name _navigateRight +* @memberof CompleterEngine.prototype +* @type {Function} +* @param {string} data - input data +* @param {Object} key - key object +* @returns {void} +*/ +setNonEnumerableReadOnly( CompleterEngine.prototype, '_navigateRight', function navigateRight( data, key ) { + // If on current line, trigger default 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(); + } +}); + +/** +* Callback which should be invoked **before** a "keypress" event is processed by a readline interface. +* +* @name beforeKeypress +* @memberof CompleterEngine.prototype +* @type {Function} +* @param {string} data - input data +* @param {Object} key - key object +* @returns {void} +*/ +setNonEnumerableReadOnly( CompleterEngine.prototype, 'beforeKeypress', function beforeKeypress( data, key ) { + var cursor; + var line; + + if ( !key ) { + this._ttyWrite.call( this._rli, data, key ); + return; + } + + // If user is already viewing completions, allow navigating it... + if ( this._isNavigating ) { + // If user presses TAB while navigating, toggle close the completer: + if ( key.name === 'tab' ) { + debug( 'Received a TAB keypress event. Closing the completer engine...' ); + this._closeCompleter(); + + // NOTE: `clearScreenDown` seems to behave abnormally in this case by also clearing the ostream on this right of the cursor. Hence writing it again below: + this._ostream.write( this._remainingLine ); + readline.moveCursor( this._ostream, -1 * this._remainingLine.length ); // eslint-disable-line max-len + return; + } + // If arrow keys detected, allow navigating the completions, else stop navigating and continue default behaviour... + if ( key.name === 'down' ) { + debug( 'Received a DOWN keypress event...' ); + this._navigateDown(); + } else if ( key.name === 'up' ) { + debug( 'Received an UP keypress event...' ); + this._navigateUp( data, key ); + } else if ( key.name === 'left' ) { + debug( 'Received a LEFT keypress event...' ); + this._navigateLeft( data, key ); + } else if ( key.name === 'right' ) { + debug( 'Received a RIGHT keypress event...' ); + this._navigateRight( data, key ); + } else { + 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 ); + return; + } + + cursor = this._rli.cursor; + line = this._rli.line; + + // Get the line before the cursor: + this._inputLine = line.slice(0, cursor); + + // Get the line after the cursor: + this._remainingLine = line.slice(cursor); + + // Pause the input stream before generating completions as the completer may be asynchronous... + this._rli.pause(); + this._completer( this._inputLine, this._onCompletions, 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 // + +module.exports = CompleterEngine; diff --git a/lib/node_modules/@stdlib/repl/lib/completer_preview.js b/lib/node_modules/@stdlib/repl/lib/completer_preview.js index 8551e2833cee..9cdceefac198 100644 --- a/lib/node_modules/@stdlib/repl/lib/completer_preview.js +++ b/lib/node_modules/@stdlib/repl/lib/completer_preview.js @@ -41,7 +41,7 @@ var debug = logger( 'repl:completer:preview' ); * * @private * @constructor -* @param {Object} rli - readline instance +* @param {Object} rli - readline interface * @param {Function} completer - function for generating possible completions * @param {WritableStream} ostream - writable stream * @param {boolean} enabled - boolean indicating whether the completer should be initially enabled @@ -232,7 +232,7 @@ setNonEnumerableReadOnly( PreviewCompleter.prototype, 'onKeypress', function onK return; } try { - this._completer( this._rli.line, this._onCompletions ); + this._completer( this._rli.line, this._onCompletions, false ); } catch ( err ) { debug( 'Error: %s', err.message ); } diff --git a/lib/node_modules/@stdlib/repl/lib/defaults.js b/lib/node_modules/@stdlib/repl/lib/defaults.js index 52ea52ee72bc..62cf144ec01d 100644 --- a/lib/node_modules/@stdlib/repl/lib/defaults.js +++ b/lib/node_modules/@stdlib/repl/lib/defaults.js @@ -90,7 +90,10 @@ function defaults() { 'autoPage': void 0, // 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/filter_by_prefix.js b/lib/node_modules/@stdlib/repl/lib/filter_by_prefix.js index 73e014060627..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,6 +21,7 @@ // MODULES // var startsWith = require( '@stdlib/string/starts-with' ); +var fuzzyMatch = require( './fuzzy_match.js' ); // MAIN // @@ -32,13 +33,29 @@ var startsWith = require( '@stdlib/string/starts-with' ); * @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 ) { +function filterByPrefix( out, arr, str, isFuzzy ) { + var fuzzyResults = []; + var match; var i; - for ( i = 0; i < arr.length; i++ ) { - if ( startsWith( arr[ i ], str ) ) { - out.push( arr[ i ] ); + + 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 ] ); + } } } 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..ac0af027641b --- /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': -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) +}; +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/main.js b/lib/node_modules/@stdlib/repl/lib/main.js index fbde77472e2f..0d99fc1ca19e 100644 --- a/lib/node_modules/@stdlib/repl/lib/main.js +++ b/lib/node_modules/@stdlib/repl/lib/main.js @@ -60,6 +60,7 @@ var displayPrompt = require( './display_prompt.js' ); var inputPrompt = require( './input_prompt.js' ); var OutputStream = require( './output_stream.js' ); var processLine = require( './process_line.js' ); +var CompleterEngine = require( './completer_engine.js' ); var completerFactory = require( './completer.js' ); var PreviewCompleter = require( './completer_preview.js' ); var AutoCloser = require( './auto_close_pairs.js' ); @@ -260,10 +261,12 @@ function REPL( options ) { 'input': this._istream, 'output': this._ostream, 'terminal': opts.isTTY, - 'prompt': opts.inputPrompt, - 'completer': this._completer + 'prompt': opts.inputPrompt })); + // Create a new TAB completer engine: + setNonEnumerableReadOnly( this, '_completerEngine', new CompleterEngine( this, this._completer, this._wstream, this._rli._ttyWrite ) ); + // Create a new auto-closer: setNonEnumerableReadOnly( this, '_autoCloser', new AutoCloser( this._rli, this._settings.autoClosePairs, this._settings.autoDeletePairs ) ); @@ -321,7 +324,7 @@ function REPL( options ) { } self._autoCloser.beforeKeypress( data, key ); self._previewCompleter.beforeKeypress( data, key ); - self._ttyWrite.call( self._rli, data, key ); + self._completerEngine.beforeKeypress( data, key ); } /** @@ -384,6 +387,7 @@ function REPL( options ) { function onSIGWINCH() { debug( 'Received a SIGWINCH event. Terminal was resized.' ); self._ostream.onResize(); + self._completerEngine.onResize(); } /** diff --git a/lib/node_modules/@stdlib/repl/lib/settings.js b/lib/node_modules/@stdlib/repl/lib/settings.js index b60354e6e6eb..b43b191853a3 100644 --- a/lib/node_modules/@stdlib/repl/lib/settings.js +++ b/lib/node_modules/@stdlib/repl/lib/settings.js @@ -43,6 +43,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' } }; 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..a16f1263d1e6 --- /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; 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..d4e85bd1bdf4 --- /dev/null +++ b/lib/node_modules/@stdlib/repl/test/integration/test.fuzzy_completions.js @@ -0,0 +1,739 @@ +/** +* @license Apache-2.0 +* +* Copyright (c) 2024 The Stdlib Authors. +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +'use strict'; + +// MODULES // + +var tape = require( 'tape' ); +var DebugStream = require( '@stdlib/streams/node/debug' ); +var contains = require( '@stdlib/assert/contains' ); +var replace = require( '@stdlib/string/replace' ); +var RE_EOL = require( '@stdlib/regexp/eol' ).REGEXP; +var repl = require( './fixtures/repl.js' ); + + +// VARIABLES // + +var RE_ANSI = /[\u001B\u009B][[\]()#;?]*(?:(?:(?:(?:;[-a-zA-Z\d/#&.:=?%@~_]+)*|[a-zA-Z\d]+(?:;[-a-zA-Z\d/#&.:=?%@~_]*)*)?\u0007)|(?:(?:\d{1,4}(?:;\d{0,4})*)?[\dA-PR-TZcf-nq-uy=><~]))/g; // eslint-disable-line no-control-regex + + +// FUNCTIONS // + +/** +* Returns default settings. +* +* @private +* @returns {Object} default settings +*/ +function defaultSettings() { + return { + 'autoDeletePairs': false, + 'autoClosePairs': false, + 'completionPreviews': false, + '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 = replace( str, RE_EOL, '' ).split( /\s+/ ); + for ( i = 0; i < cleanOutput.length; i++ ) { + if ( cleanOutput[i] !== '' ) { + out.push( cleanOutput[ i ] ); + } + } + return out; +} + + +// TESTS // + +tape( 'main export is a function', function test( t ) { + t.ok( true, __filename ); + t.strictEqual( typeof repl, 'function', 'main export is a function' ); + t.end(); +}); + +tape( 'a REPL instance supports displaying TAB completions of user-defined variables', function test( t ) { + var istream; + var opts; + var r; + + istream = new DebugStream({ + 'name': 'repl-input-stream' + }); + opts = { + 'input': istream, + 'settings': defaultSettings(), + 'tty': { + 'rows': 100, + 'columns': 80 + } + }; + r = repl( opts, onClose ); + + // Declare variables with unique names in order to prevent namespace collisions: + istream.write( 'var zzxyz = 1;' ); + istream.write( 'var zzabc = 2;' ); + istream.write( 'var zzpqr = 3;' ); + + // Write the common beginning of the variable names in order to generate TAB completions: + istream.write( 'zz' ); + + // Write TAB to display completions: + istream.write( '\t' ); + + // Close the input stream: + istream.end(); + + // Close the REPL: + r.close(); + + function onClose( error, data ) { + var actual; + + if ( error ) { + t.fail( error.message ); + return; + } + + actual = extractCompletions( stripANSI( data[ data.length - 3 ] ) ); + + // Check for three completions in the output: + t.strictEqual( actual.length, 3, 'returns expected value' ); + + // Check for the declared variables (sorted lexicographically) in the completions: + t.strictEqual( actual[ 0 ], 'zzabc', 'returns expected value' ); + t.strictEqual( actual[ 1 ], 'zzpqr', 'returns expected value' ); + t.strictEqual( actual[ 2 ], 'zzxyz', 'returns expected value' ); + + // Check if the cursor is returned back to the prompt: + t.strictEqual( data[ data.length - 2 ], '\x1B[2A', 'returns expected value' ); + + t.end(); + } +}); + +tape( 'a REPL instance supports 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(), + 'tty': { + 'rows': 100, + 'columns': 80 + } + }; + 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 - 3 ] ) ); + + // 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' ); + + // Check if the cursor is returned back to the prompt: + t.strictEqual( data[ data.length - 2 ], '\x1B[2A', 'returns expected value' ); + + t.end(); + } +}); + +tape( 'a REPL instance 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(), + 'tty': { + 'rows': 100, + 'columns': 80 + } + }; + 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 - 3 ] ) ); + + // 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' ); + + // Check if the cursor is returned back to the prompt: + t.strictEqual( data[ data.length - 2 ], '\x1B[2A', 'returns expected value' ); + + t.end(); + } +}); + +tape( 'a REPL instance supports 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(), + 'tty': { + 'rows': 100, + 'columns': 80 + } + }; + 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 - 3 ] ); + + // 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' ); + + // Check if the cursor is returned back to the prompt: + t.strictEqual( data[ data.length - 2 ], '\x1B[2A', 'returns expected value' ); + + t.end(); + } +}); + +tape( 'a REPL instance supports hiding the completions panel upon pressing TAB again', function test( t ) { + var istream; + var opts; + var r; + + istream = new DebugStream({ + 'name': 'repl-input-stream' + }); + opts = { + 'input': istream, + 'settings': defaultSettings(), + 'tty': { + 'rows': 100, + 'columns': 80 + } + }; + r = repl( opts, onClose ); + + // Declare variables with unique names in order to prevent namespace collisions: + istream.write( 'var 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' ); + + // NOTE: `data[ data.length-1 ]` adds the remaining string to the right of the cursor because of the abnormal behaviour of `clearScreenDown`... + + t.end(); + } +}); + +tape( 'a REPL instance supports navigating the TAB completions using arrow keys', function test( t ) { + var istream; + var opts; + var r; + + istream = new DebugStream({ + 'name': 'repl-input-stream' + }); + opts = { + 'input': istream, + 'settings': defaultSettings(), + 'tty': { + 'rows': 100, + 'columns': 80 + } + }; + r = repl( opts, onClose ); + + // Declare variables with unique names in order to prevent namespace collisions: + istream.write( 'var 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 - 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' ); + 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-12 ], '\x1B[0J', 'returns expected value' ); + + // Check for completions after `down` arrow: + actual = extractCompletions( data[ data.length - 11 ] ); + + // First completion should be highlighted: + t.strictEqual( actual[ 0 ], '\x1B[7mjazz\x1B[27m', 'returns expected value' ); + 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-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 - 6 ], '\x1B[0J', 'returns expected value' ); + + // Check for completions after `right` arrow key: + actual = extractCompletions( data[ data.length - 5 ] ); + + // Second completion should be highlighted: + t.strictEqual( actual[ 1 ], '\x1B[7mpizza\x1B[27m', 'returns expected value' ); + t.strictEqual( data[ data.length - 4 ], '\x1B[2A', 'returns expected value' ); // bring cursor back + + // Current line is cleared and the second completion is inserted: + t.strictEqual( data[ data.length-2 ], '\x1B[0K', 'returns expected value' ); + t.strictEqual( data[ data.length-1 ], 'var pizza = 1;var jazz = 2;pizza', 'returns expected value' ); + + t.end(); + } +}); + +tape( 'a REPL instance supports bringing back the original line upon navigating back up from the TAB completions', function test( t ) { + var istream; + var opts; + var r; + + istream = new DebugStream({ + 'name': 'repl-input-stream' + }); + opts = { + 'input': istream, + 'settings': defaultSettings(), + 'tty': { + 'rows': 100, + 'columns': 80 + } + }; + r = repl( opts, onClose ); + + // Declare variables with unique names in order to prevent namespace collisions: + istream.write( 'var zzabc = 1;' ); + istream.write( 'var zzpqr = 2;' ); + + // Write the common beginning of the variable names in order to generate TAB completions: + istream.write( 'zz' ); + + // Write TAB to display completions: + istream.write( '\t' ); + + // Navigate down using down arrow: + istream.write('\u001B[B'); + + // Navigate up towards the line to bring back the original line: + istream.write('\u001B[A'); + + // Close the input stream: + istream.end(); + + // Close the REPL: + r.close(); + + function onClose( error, data ) { + if ( error ) { + t.fail( error.message ); + return; + } + + // Current line is cleared and the original line is inserted: + t.strictEqual( data[ data.length-2 ], '\x1B[0K', 'returns expected value' ); + t.strictEqual( data[ data.length-1 ], 'var zzabc = 1;var zzpqr = 2;zz', 'returns expected value' ); + + t.end(); + } +}); + +tape( 'a REPL instance supports displaying highlighted arguement TAB completions', function test( t ) { + var istream; + var opts; + var r; + + istream = new DebugStream({ + 'name': 'repl-input-stream' + }); + opts = { + 'input': istream, + 'settings': defaultSettings(), + 'tty': { + 'rows': 100, + 'columns': 80 + } + }; + r = repl( opts, onClose ); + + // Write the common beginning of the settings in order to generate TAB completions: + istream.write( 'settings(\'auto' ); + + // Write TAB to display completions: + istream.write( '\t' ); + + // Close the input stream: + istream.end(); + + // Close the REPL: + r.close(); + + function onClose( error, data ) { + var actual; + + if ( error ) { + t.fail( error.message ); + return; + } + + // Check for settings name completions in the output: + actual = extractCompletions( data[ data.length - 3 ] ); + t.ok( contains( actual, '\x1B[1mauto\x1B[0mClosePairs' ), 'returns expected value' ); + t.ok( contains( actual, '\x1B[1mauto\x1B[0mDeletePairs' ), 'returns expected value' ); + + // Check if the cursor is returned back to the prompt: + t.strictEqual( data[ data.length - 2 ], '\x1B[2A', 'returns expected value' ); + + t.end(); + } +}); + +tape( 'a REPL instance supports auto-completing common prefixes when hitting TAB', function test( t ) { + var istream; + var opts; + var r; + + istream = new DebugStream({ + 'name': 'repl-input-stream' + }); + opts = { + 'input': istream, + 'settings': defaultSettings(), + 'tty': { + 'rows': 100, + 'columns': 80 + } + }; + r = repl( opts, onClose ); + + // Declare variables with `zzzz` as an exact prefix: + istream.write( 'var zzzz = 1;' ); + istream.write( 'var zzzzabc = 2;' ); + istream.write( 'var zzzzpqr = 3;' ); + + // Partially write the common beginning of the variable names in order to generate a TAB auto-completion: + istream.write( 'zz' ); + + // Write TAB to trigger auto-completion: + istream.write( '\t' ); + + // Close the input stream: + istream.end(); + + // Close the REPL: + r.close(); + + function onClose( error, data ) { + if ( error ) { + t.fail( error.message ); + return; + } + + // Check if the completion prefix was cleared: + t.strictEqual( data[ data.length-2 ], '\b\b', 'returns expected value' ); + + // Check if the final completion was auto-inserted: + t.strictEqual( data[ data.length-1 ], 'zzzz', 'returns expected value' ); + + t.end(); + } +}); + +tape( 'a REPL instance supports auto-completing 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(), + 'tty': { + 'rows': 100, + 'columns': 80 + } + }; + 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(), + 'tty': { + 'rows': 100, + 'columns': 80 + } + }; + 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(); + } +});