Skip to content

feat: add auto-completion preview for REPL #1832

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

Merged
merged 37 commits into from
Mar 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
6e309e3
feat(repl): add preview for REPL tab completion
Mar 8, 2024
f4e96ca
feat(repl): add special interactions for right and return
Mar 10, 2024
891fbc0
test(repl): test the preview functionality
Mar 11, 2024
1baf23a
chore: update copyright years
stdlib-bot Mar 11, 2024
fd21d12
refactor(repl): make preview_completer class
Mar 12, 2024
77980f9
style(repl): fix parantheses spacing
Mar 12, 2024
4456788
fix(repl): put PreviewCompleter methods in prototype
Mar 14, 2024
f6dd2e7
fix(repl): put back accidentally remove clearPreview
Mar 14, 2024
85fb886
docs(repl): make comment clearer
Mar 14, 2024
1251c88
refactor: make property private
kgryte Mar 16, 2024
57046cd
refactor: only enable a preview completer when TTY
kgryte Mar 16, 2024
e4345e4
refactor: rename file
kgryte Mar 16, 2024
acc1e10
refactor: clean-up logic for clearing a completion preview
kgryte Mar 16, 2024
280d20a
refactor: update callback for processing completion candidates
kgryte Mar 16, 2024
a586946
refactor: inline the prototype method
kgryte Mar 16, 2024
06aac39
refactor: clean-up keypress handlers
kgryte Mar 16, 2024
2792326
test: rename file
kgryte Mar 16, 2024
caf9295
fix: add missing argument
kgryte Mar 16, 2024
8fe54aa
refactor: add debug statements
kgryte Mar 16, 2024
026fd3c
refactor: update debug messages
kgryte Mar 16, 2024
915a596
docs: update comments and debug messages
kgryte Mar 16, 2024
47b53cf
refactor: update debug messages
kgryte Mar 16, 2024
6afbb55
refactor: update debug messages
kgryte Mar 16, 2024
a25e60d
fix: don't show preview when line has trailing space
Mar 16, 2024
e110c2c
refactor: avoid executing completion logic when user enters whitespace
kgryte Mar 16, 2024
b6976b5
refactor: use try/catch to prevent REPL crash
kgryte Mar 16, 2024
7adbdff
fix: handle case when hitting TAB for line starting with `[` bracket
kgryte Mar 16, 2024
dd4bc3c
fix: handle additional completion edge cases
kgryte Mar 16, 2024
9b0f547
docs: add comments
kgryte Mar 16, 2024
3872cd5
refactor: remove obsolete logic
kgryte Mar 16, 2024
206ce02
fix: handle trailing whitespace for top-level identifiers
kgryte Mar 16, 2024
8bbb5d1
refactor: clean up repl tests
Mar 18, 2024
d1c0d6e
feat: show common prefix of completion candidates
Mar 18, 2024
bc416a8
refactor: inline operations and update comments
kgryte Mar 19, 2024
7a65900
refactor: rename debug namespace
kgryte Mar 19, 2024
273c2b9
docs: update comment
kgryte Mar 25, 2024
2721cba
test: refactor tests and move to an 'integration' folder
kgryte Mar 25, 2024
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
9 changes: 9 additions & 0 deletions lib/node_modules/@stdlib/repl/lib/complete_expression.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ var logger = require( 'debug' );
var parse = require( 'acorn-loose' ).parse;
var objectKeys = require( '@stdlib/utils/keys' );
var trim = require( '@stdlib/string/trim' );
var trimRight = require( '@stdlib/string/right-trim' );
var hasOwnProp = require( '@stdlib/assert/has-own-property' );
var propertyNamesIn = require( '@stdlib/utils/property-names-in' );
var filterByPrefix = require( './filter_by_prefix.js' );
Expand Down Expand Up @@ -93,6 +94,10 @@ function complete( out, context, expression ) {
}
// Case: `foo<|>` (completing an identifier at the top-level)
if ( node.type === 'ExpressionStatement' && node.expression.type === 'Identifier' ) {
// Case: `conso <|>`
if ( trimRight( expression ) !== expression ) {
return '';
}
filter = node.expression.name;
debug( 'Identifier auto-completion. Filter: %s', filter );
out = filterByPrefix( out, RESERVED_KEYWORDS_COMMON, filter );
Expand Down Expand Up @@ -204,6 +209,10 @@ function complete( out, context, expression ) {
}
// Case: `foo.bar<|>`
else {
// Case: `foo.bar <|>`
if ( trimRight( expression ) !== expression ) {
return '';
}
filter = node.property.name;
}
debug( 'Property auto-completion. Filter: %s', filter );
Expand Down
14 changes: 14 additions & 0 deletions lib/node_modules/@stdlib/repl/lib/complete_walk_find_last.js
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,10 @@ function walk( node ) { // eslint-disable-line max-lines-per-function
break;
case 'ArrayExpression':
// `[ <|>` || `[ foo<|>` || `[ 1, 2, <|>` || `[ 1, 2, foo<|>` || etc
if ( node.elements.length === 0 ) {
FLG = false;
break;
}
node = node.elements[ node.elements.length-1 ];
break;
case 'ForStatement':
Expand Down Expand Up @@ -374,13 +378,23 @@ function walk( node ) { // eslint-disable-line max-lines-per-function
node = node.handler.body;
break;
case 'TemplateLiteral':
// ``<|>
if ( node.expressions.length === 0 ) {
FLG = false;
break;
}
node = node.expressions[ node.expressions.length-1 ];
break;
case 'SpreadElement':
// `[...<|>` || `[...foo<|>`
node = node.argument;
break;
case 'ObjectExpression':
// `{<|>`
if ( node.properties.length === 0 ) {
FLG = false;
break;
}
// `{ 'a': 1, ...<|>` || `{ 'a': 1, ...foo<|>`
node = node.properties[ node.properties.length-1 ];
break;
Expand Down
222 changes: 222 additions & 0 deletions lib/node_modules/@stdlib/repl/lib/completer_preview.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
/**
* @license Apache-2.0
*
* Copyright (c) 2024 The Stdlib Authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

/* eslint-disable no-restricted-syntax, no-underscore-dangle, no-invalid-this */

'use strict';

// MODULES //

var readline = require( 'readline' );
var logger = require( 'debug' );
var setNonEnumerableReadOnly = require( '@stdlib/utils/define-nonenumerable-read-only-property' );
var repeat = require( '@stdlib/string/repeat' );
var commonPrefix = require( './longest_common_prefix.js' );


// VARIABLES //

var debug = logger( 'repl:completer:preview' );


// MAIN //

/**
* Constructor for creating a preview completer.
*
* @private
* @constructor
* @param {Object} rli - readline instance
* @param {Function} completer - function for generating possible completions
* @param {WritableStream} ostream - writable stream
* @returns {PreviewCompleter} completer instance
*/
function PreviewCompleter( rli, completer, ostream ) {
if ( !(this instanceof PreviewCompleter) ) {
return new PreviewCompleter( rli, completer, ostream );
}
debug( 'Creating a preview completer...' );

// Cache a reference to the provided readline interface:
this._rli = rli;

// Cache a reference to the output writable stream:
this._ostream = ostream;

// Cache a reference to the provided completer:
this._completer = completer;

// Create a callback for processing potential completion previews:
this._onCompletions = this._completionCallback();

// Initialize a buffer containing the currently displayed completion preview:
this._preview = '';

return this;
}

/**
* Returns a callback for processing potential completion previews.
*
* @private
* @name _completionCallback
* @memberof PreviewCompleter.prototype
* @returns {Function} completion callback
*/
setNonEnumerableReadOnly( PreviewCompleter.prototype, '_completionCallback', function completionCallback() {
var self = this;
return clbk;

/**
* Callback invoked upon resolving potential completion previews.
*
* @private
* @param {(Error|null)} error - error object
* @param {Array} completions - completion results
* @returns {void}
*/
function clbk( error, completions ) {
var prefix;
var list;
var N;

// Check whether we encountered an error when generating completions...
if ( error ) {
debug( 'Encountered an error when generating completions. Unable to display a completion preview.' );
return;
}
list = completions[ 0 ];
if ( list.length === 0 ) {
debug( 'Unable to display a completion preview. No completion preview candidates.' );
self.clear();
return;
}
// Resolve a common prefix from the completion results:
prefix = commonPrefix( list ); // e.g., [ 'back', 'background', 'backward' ] => 'back'

// If the completion candidates do not have a common prefix, no completion preview to display, as we do not have a criteria for choosing one candidate over another...
if ( prefix === '' ) {
debug( 'Unable to display a completion preview. Completion candidates have no common prefix.' );
return;
}
// Extract the completion preview substring (e.g., if the current line is 'ba', preview should be 'ck'):
self._preview = prefix.substring( commonPrefix( prefix, completions[ 1 ] ).length ); // eslint-disable-line max-len

// If the substring is empty, nothing to display...
if ( self._preview === '' ) {
debug( 'Unable to display a completion preview. Exact match.' );
return;
}
debug( 'Completion preview: %s', self._preview );

// Compute the number of characters until the end of the line from the current cursor position:
N = self._rli.line.length - self._rli.cursor;

// Move the cursor to the end of the line:
readline.moveCursor( self._ostream, N );

// Append the completion preview to the current line (using ASCII color escape codes for displaying grey text):
self._ostream.write( '\u001b[90m' + self._preview + '\u001b[0m' );

// Move the cursor back to previous position:
readline.moveCursor( self._ostream, -self._preview.length-N );
}
});

/**
* Clears a completion preview.
*
* @name clear
* @memberof PreviewCompleter.prototype
* @returns {void}
*/
setNonEnumerableReadOnly( PreviewCompleter.prototype, 'clear', function clear() {
var preview;
var N;

preview = this._preview;

// If no preview currently displayed, nothing to clear...
if ( preview === '' ) {
return;
}
debug( 'Clearing completion preview...' );

// Compute the number of character until the end of the line from the current cursor position:
N = this._rli.line.length - this._rli.cursor;

// Move the cursor to the end of the line:
readline.moveCursor( this._ostream, N );

// Replace the current display text with whitespace:
this._ostream.write( repeat( ' ', preview.length ) );

// Reset the cursor:
readline.moveCursor( this._ostream, -preview.length-N );

// Reset the completion preview buffer:
this._preview = '';
});

/**
* Callback for handling a "keypress" event.
*
* @name onKeypress
* @memberof PreviewCompleter.prototype
* @param {string} data - input data
* @param {Object} key - key object
* @returns {void}
*/
setNonEnumerableReadOnly( PreviewCompleter.prototype, 'onKeypress', function onKeypress() {
this._completer( this._rli.line, this._onCompletions );
});

/**
* Callback which should be invoked **before** a "keypress" event is processed by a readline interface.
*
* @name beforeKeypress
* @memberof PreviewCompleter.prototype
* @param {string} data - input data
* @param {Object} key - key object
* @returns {void}
*/
setNonEnumerableReadOnly( PreviewCompleter.prototype, 'beforeKeypress', function beforeKeypress( data, key ) {
if ( !key || this._preview === '' ) {
return;
}
// Handle the case where the user is not at the end of the line...
if ( this._rli.cursor !== this._rli.line.length ) {
// If a user is in the middle of a line and presses ENTER, clear the preview string, as the preview was not accepted prior to executing the expression...
if ( key.name === 'return' || key.name === 'enter' ) {
debug( 'Received an ENTER keypress event while in the middle of the line.' );
return this.clear();
}
return;
}
// When the user is at the end of the line, auto-complete the line with the completion preview when a user presses RETURN or the RIGHT arrow key (note: pressing ENTER will result in both completion AND execution)...
if ( key.name === 'return' || key.name === 'enter' || key.name === 'right' ) {
debug( 'Completion preview accepted. Performing auto-completion...' );
this._rli.write( this._preview );
this._preview = '';
}
});


// EXPORTS //

module.exports = PreviewCompleter;
Loading