Skip to content

feat: add fuzzy completions extension in the REPL #2493

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 8 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions lib/node_modules/@stdlib/repl/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ The function supports specifying the following settings:
- **bracketedPaste**: boolean indicating whether to enable bracketed-paste mode. When streams are TTY, the default is `true`; otherwise, the default is `false`.
- **autoDisableBracketedPasteOnExit**: boolean indicating whether to automatically disable bracketed-paste upon exiting the REPL. When streams are TTY and bracketed paste is enabled, 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`.
- **syntaxHighlighting**: boolean indicating whether to enable syntax highlighting of entered input expressions. When streams are TTY, the default is `true`; otherwise, the default is `false`.
- **theme**: initial color theme for syntax highlighting. Default: `stdlib-ansi-basic`.

Expand Down
85 changes: 74 additions & 11 deletions lib/node_modules/@stdlib/repl/lib/complete_expression.js
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Planeshifter Do you mean a file like this w.r.t code duplication?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Snehil-Shah Yes, was wondering whether we can consolidate some of it. Your call!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, but I'm kinda confused with how we can abstract those😅. Do you have something in mind?

Copy link
Member

@Planeshifter Planeshifter Sep 12, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Snehil-Shah

Couldn't we define a helper function like

function collectCompletions( items, filter, isFuzzy ) {
	var fuzzyResults = [];
	var out = [];
	var match;
	var item;
	var i;

	for ( i = 0; i < items.length; i++ ) {
		item = items[ i ];
		if ( startsWith( item, filter ) ) {
			out.push( item );
		}
	}
	out.sort();

	// Only perform fuzzy search if no exact matches are found...
	if ( !isFuzzy || out.length !== 0 ) {
		return out;
	}
	for ( i = 0; i < items.length; i++ ) {
		item = items[ i ];
		match = fuzzyMatch( item, filter );
		if ( match ) {
			fuzzyResults.push( match );
		}
	}
	fuzzyResults = sortFuzzyCompletions( fuzzyResults );
	for ( i = 0; i < fuzzyResults.length; i++ ) {
		out.push( fuzzyResults[ i ] );
	}
	return out;
}

and then use them in the various completers? E.g., for complete_settings.js, could directly invoke

var completions = collectCompletions( SETTINGS_NAMES, value, isFuzzy );

and then push the completions to the out array. I realize that some of the completers have additional logic like the file system one, but may be worth exploring...

Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
* limitations under the License.
*/

/* eslint-disable max-statements */

'use strict';

// MODULES //
Expand All @@ -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' );


Expand All @@ -52,16 +55,19 @@ 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;
var opts;
var ast;
var obj;
var res;
var i;

// Case: `<|>` (a command devoid of expressions/statements)
if ( trim( expression ) === '' ) {
Expand All @@ -79,7 +85,9 @@ function complete( out, context, expression ) {
// Get the last program top-level AST "node":
debug( 'Number of statements: %d', ast.body.length );
node = ast.body[ ast.body.length-1 ];

if ( !node ) {
return '';
}
// Check for an empty trailing "expression"...
if (
node.end !== ast.end &&
Expand All @@ -100,9 +108,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:
Expand Down Expand Up @@ -177,7 +198,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<|>`
Expand All @@ -190,7 +221,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
Expand All @@ -216,15 +257,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;
}

Expand Down
51 changes: 50 additions & 1 deletion lib/node_modules/@stdlib/repl/lib/complete_fs.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
// MODULES //

var resolve = require( 'path' ).resolve;
var statSync = require( 'fs' ).statSync; // TODO: replace with stdlib equivalent

Check warning on line 24 in lib/node_modules/@stdlib/repl/lib/complete_fs.js

View workflow job for this annotation

GitHub Actions / Lint Changed Files

Unexpected sync method: 'statSync'

Check warning on line 24 in lib/node_modules/@stdlib/repl/lib/complete_fs.js

View workflow job for this annotation

GitHub Actions / Lint Changed Files

Unexpected 'todo' comment: 'TODO: replace with stdlib equivalent'
var logger = require( 'debug' );
var parse = require( 'acorn-loose' ).parse;
var isRelativePath = require( '@stdlib/assert/is-relative-path' );
Expand All @@ -30,6 +30,8 @@
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 //
Expand All @@ -50,13 +52,16 @@
* @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;
Expand Down Expand Up @@ -128,6 +133,50 @@
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;
}

Expand Down
94 changes: 92 additions & 2 deletions lib/node_modules/@stdlib/repl/lib/complete_require.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,21 +16,25 @@
* limitations under the License.
*/

/* eslint-disable max-statements */

'use strict';

// MODULES //

var resolve = require( 'path' ).resolve;
var statSync = require( 'fs' ).statSync; // TODO: replace with stdlib equivalent

Check warning on line 26 in lib/node_modules/@stdlib/repl/lib/complete_require.js

View workflow job for this annotation

GitHub Actions / Lint Changed Files

Unexpected sync method: 'statSync'

Check warning on line 26 in lib/node_modules/@stdlib/repl/lib/complete_require.js

View workflow job for this annotation

GitHub Actions / Lint Changed Files

Unexpected 'todo' comment: 'TODO: replace with stdlib equivalent'
var logger = require( 'debug' );
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 //
Expand All @@ -49,14 +53,17 @@
* @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;
Expand Down Expand Up @@ -157,6 +164,89 @@
}
}
}
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;
}

Expand Down
Loading
Loading