Skip to content

Commit ec283ec

Browse files
tudor-pagukgryteTudor Stefan Pagustdlib-bot
authored
feat: add auto-completion preview for REPL
PR-URL: #1832 Closes: #1775 Co-authored-by: Athan Reines <[email protected]> Reviewed-by: Athan Reines <[email protected]> Co-authored-by: Tudor Stefan Pagu <[email protected]> Co-authored-by: stdlib-bot <[email protected]>
1 parent f48de9d commit ec283ec

File tree

6 files changed

+857
-4
lines changed

6 files changed

+857
-4
lines changed

lib/node_modules/@stdlib/repl/lib/complete_expression.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ var logger = require( 'debug' );
2525
var parse = require( 'acorn-loose' ).parse;
2626
var objectKeys = require( '@stdlib/utils/keys' );
2727
var trim = require( '@stdlib/string/trim' );
28+
var trimRight = require( '@stdlib/string/right-trim' );
2829
var hasOwnProp = require( '@stdlib/assert/has-own-property' );
2930
var propertyNamesIn = require( '@stdlib/utils/property-names-in' );
3031
var filterByPrefix = require( './filter_by_prefix.js' );
@@ -93,6 +94,10 @@ function complete( out, context, expression ) {
9394
}
9495
// Case: `foo<|>` (completing an identifier at the top-level)
9596
if ( node.type === 'ExpressionStatement' && node.expression.type === 'Identifier' ) {
97+
// Case: `conso <|>`
98+
if ( trimRight( expression ) !== expression ) {
99+
return '';
100+
}
96101
filter = node.expression.name;
97102
debug( 'Identifier auto-completion. Filter: %s', filter );
98103
out = filterByPrefix( out, RESERVED_KEYWORDS_COMMON, filter );
@@ -204,6 +209,10 @@ function complete( out, context, expression ) {
204209
}
205210
// Case: `foo.bar<|>`
206211
else {
212+
// Case: `foo.bar <|>`
213+
if ( trimRight( expression ) !== expression ) {
214+
return '';
215+
}
207216
filter = node.property.name;
208217
}
209218
debug( 'Property auto-completion. Filter: %s', filter );

lib/node_modules/@stdlib/repl/lib/complete_walk_find_last.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,10 @@ function walk( node ) { // eslint-disable-line max-lines-per-function
124124
break;
125125
case 'ArrayExpression':
126126
// `[ <|>` || `[ foo<|>` || `[ 1, 2, <|>` || `[ 1, 2, foo<|>` || etc
127+
if ( node.elements.length === 0 ) {
128+
FLG = false;
129+
break;
130+
}
127131
node = node.elements[ node.elements.length-1 ];
128132
break;
129133
case 'ForStatement':
@@ -374,13 +378,23 @@ function walk( node ) { // eslint-disable-line max-lines-per-function
374378
node = node.handler.body;
375379
break;
376380
case 'TemplateLiteral':
381+
// ``<|>
382+
if ( node.expressions.length === 0 ) {
383+
FLG = false;
384+
break;
385+
}
377386
node = node.expressions[ node.expressions.length-1 ];
378387
break;
379388
case 'SpreadElement':
380389
// `[...<|>` || `[...foo<|>`
381390
node = node.argument;
382391
break;
383392
case 'ObjectExpression':
393+
// `{<|>`
394+
if ( node.properties.length === 0 ) {
395+
FLG = false;
396+
break;
397+
}
384398
// `{ 'a': 1, ...<|>` || `{ 'a': 1, ...foo<|>`
385399
node = node.properties[ node.properties.length-1 ];
386400
break;
Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
/**
2+
* @license Apache-2.0
3+
*
4+
* Copyright (c) 2024 The Stdlib Authors.
5+
*
6+
* Licensed under the Apache License, Version 2.0 (the "License");
7+
* you may not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
19+
/* eslint-disable no-restricted-syntax, no-underscore-dangle, no-invalid-this */
20+
21+
'use strict';
22+
23+
// MODULES //
24+
25+
var readline = require( 'readline' );
26+
var logger = require( 'debug' );
27+
var setNonEnumerableReadOnly = require( '@stdlib/utils/define-nonenumerable-read-only-property' );
28+
var repeat = require( '@stdlib/string/repeat' );
29+
var commonPrefix = require( './longest_common_prefix.js' );
30+
31+
32+
// VARIABLES //
33+
34+
var debug = logger( 'repl:completer:preview' );
35+
36+
37+
// MAIN //
38+
39+
/**
40+
* Constructor for creating a preview completer.
41+
*
42+
* @private
43+
* @constructor
44+
* @param {Object} rli - readline instance
45+
* @param {Function} completer - function for generating possible completions
46+
* @param {WritableStream} ostream - writable stream
47+
* @returns {PreviewCompleter} completer instance
48+
*/
49+
function PreviewCompleter( rli, completer, ostream ) {
50+
if ( !(this instanceof PreviewCompleter) ) {
51+
return new PreviewCompleter( rli, completer, ostream );
52+
}
53+
debug( 'Creating a preview completer...' );
54+
55+
// Cache a reference to the provided readline interface:
56+
this._rli = rli;
57+
58+
// Cache a reference to the output writable stream:
59+
this._ostream = ostream;
60+
61+
// Cache a reference to the provided completer:
62+
this._completer = completer;
63+
64+
// Create a callback for processing potential completion previews:
65+
this._onCompletions = this._completionCallback();
66+
67+
// Initialize a buffer containing the currently displayed completion preview:
68+
this._preview = '';
69+
70+
return this;
71+
}
72+
73+
/**
74+
* Returns a callback for processing potential completion previews.
75+
*
76+
* @private
77+
* @name _completionCallback
78+
* @memberof PreviewCompleter.prototype
79+
* @returns {Function} completion callback
80+
*/
81+
setNonEnumerableReadOnly( PreviewCompleter.prototype, '_completionCallback', function completionCallback() {
82+
var self = this;
83+
return clbk;
84+
85+
/**
86+
* Callback invoked upon resolving potential completion previews.
87+
*
88+
* @private
89+
* @param {(Error|null)} error - error object
90+
* @param {Array} completions - completion results
91+
* @returns {void}
92+
*/
93+
function clbk( error, completions ) {
94+
var prefix;
95+
var list;
96+
var N;
97+
98+
// Check whether we encountered an error when generating completions...
99+
if ( error ) {
100+
debug( 'Encountered an error when generating completions. Unable to display a completion preview.' );
101+
return;
102+
}
103+
list = completions[ 0 ];
104+
if ( list.length === 0 ) {
105+
debug( 'Unable to display a completion preview. No completion preview candidates.' );
106+
self.clear();
107+
return;
108+
}
109+
// Resolve a common prefix from the completion results:
110+
prefix = commonPrefix( list ); // e.g., [ 'back', 'background', 'backward' ] => 'back'
111+
112+
// 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...
113+
if ( prefix === '' ) {
114+
debug( 'Unable to display a completion preview. Completion candidates have no common prefix.' );
115+
return;
116+
}
117+
// Extract the completion preview substring (e.g., if the current line is 'ba', preview should be 'ck'):
118+
self._preview = prefix.substring( commonPrefix( prefix, completions[ 1 ] ).length ); // eslint-disable-line max-len
119+
120+
// If the substring is empty, nothing to display...
121+
if ( self._preview === '' ) {
122+
debug( 'Unable to display a completion preview. Exact match.' );
123+
return;
124+
}
125+
debug( 'Completion preview: %s', self._preview );
126+
127+
// Compute the number of characters until the end of the line from the current cursor position:
128+
N = self._rli.line.length - self._rli.cursor;
129+
130+
// Move the cursor to the end of the line:
131+
readline.moveCursor( self._ostream, N );
132+
133+
// Append the completion preview to the current line (using ASCII color escape codes for displaying grey text):
134+
self._ostream.write( '\u001b[90m' + self._preview + '\u001b[0m' );
135+
136+
// Move the cursor back to previous position:
137+
readline.moveCursor( self._ostream, -self._preview.length-N );
138+
}
139+
});
140+
141+
/**
142+
* Clears a completion preview.
143+
*
144+
* @name clear
145+
* @memberof PreviewCompleter.prototype
146+
* @returns {void}
147+
*/
148+
setNonEnumerableReadOnly( PreviewCompleter.prototype, 'clear', function clear() {
149+
var preview;
150+
var N;
151+
152+
preview = this._preview;
153+
154+
// If no preview currently displayed, nothing to clear...
155+
if ( preview === '' ) {
156+
return;
157+
}
158+
debug( 'Clearing completion preview...' );
159+
160+
// Compute the number of character until the end of the line from the current cursor position:
161+
N = this._rli.line.length - this._rli.cursor;
162+
163+
// Move the cursor to the end of the line:
164+
readline.moveCursor( this._ostream, N );
165+
166+
// Replace the current display text with whitespace:
167+
this._ostream.write( repeat( ' ', preview.length ) );
168+
169+
// Reset the cursor:
170+
readline.moveCursor( this._ostream, -preview.length-N );
171+
172+
// Reset the completion preview buffer:
173+
this._preview = '';
174+
});
175+
176+
/**
177+
* Callback for handling a "keypress" event.
178+
*
179+
* @name onKeypress
180+
* @memberof PreviewCompleter.prototype
181+
* @param {string} data - input data
182+
* @param {Object} key - key object
183+
* @returns {void}
184+
*/
185+
setNonEnumerableReadOnly( PreviewCompleter.prototype, 'onKeypress', function onKeypress() {
186+
this._completer( this._rli.line, this._onCompletions );
187+
});
188+
189+
/**
190+
* Callback which should be invoked **before** a "keypress" event is processed by a readline interface.
191+
*
192+
* @name beforeKeypress
193+
* @memberof PreviewCompleter.prototype
194+
* @param {string} data - input data
195+
* @param {Object} key - key object
196+
* @returns {void}
197+
*/
198+
setNonEnumerableReadOnly( PreviewCompleter.prototype, 'beforeKeypress', function beforeKeypress( data, key ) {
199+
if ( !key || this._preview === '' ) {
200+
return;
201+
}
202+
// Handle the case where the user is not at the end of the line...
203+
if ( this._rli.cursor !== this._rli.line.length ) {
204+
// 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...
205+
if ( key.name === 'return' || key.name === 'enter' ) {
206+
debug( 'Received an ENTER keypress event while in the middle of the line.' );
207+
return this.clear();
208+
}
209+
return;
210+
}
211+
// 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)...
212+
if ( key.name === 'return' || key.name === 'enter' || key.name === 'right' ) {
213+
debug( 'Completion preview accepted. Performing auto-completion...' );
214+
this._rli.write( this._preview );
215+
this._preview = '';
216+
}
217+
});
218+
219+
220+
// EXPORTS //
221+
222+
module.exports = PreviewCompleter;

0 commit comments

Comments
 (0)