diff --git a/lib/node_modules/@stdlib/repl/lib/complete_expression.js b/lib/node_modules/@stdlib/repl/lib/complete_expression.js index 9d718c546004..64b664344fad 100644 --- a/lib/node_modules/@stdlib/repl/lib/complete_expression.js +++ b/lib/node_modules/@stdlib/repl/lib/complete_expression.js @@ -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' ); @@ -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 ); @@ -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 ); diff --git a/lib/node_modules/@stdlib/repl/lib/complete_walk_find_last.js b/lib/node_modules/@stdlib/repl/lib/complete_walk_find_last.js index 5eb4c1ead097..8553ac5b020b 100644 --- a/lib/node_modules/@stdlib/repl/lib/complete_walk_find_last.js +++ b/lib/node_modules/@stdlib/repl/lib/complete_walk_find_last.js @@ -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': @@ -374,6 +378,11 @@ 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': @@ -381,6 +390,11 @@ function walk( node ) { // eslint-disable-line max-lines-per-function 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; diff --git a/lib/node_modules/@stdlib/repl/lib/completer_preview.js b/lib/node_modules/@stdlib/repl/lib/completer_preview.js new file mode 100644 index 000000000000..59d4ae1efdcb --- /dev/null +++ b/lib/node_modules/@stdlib/repl/lib/completer_preview.js @@ -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; diff --git a/lib/node_modules/@stdlib/repl/lib/main.js b/lib/node_modules/@stdlib/repl/lib/main.js index 53b133d586ea..e002c41c260f 100644 --- a/lib/node_modules/@stdlib/repl/lib/main.js +++ b/lib/node_modules/@stdlib/repl/lib/main.js @@ -23,7 +23,7 @@ // MODULES // var EventEmitter = require( 'events' ).EventEmitter; -var readline = require( 'readline' ).createInterface; +var readline = require( 'readline' ); var resolve = require( 'path' ).resolve; var logger = require( 'debug' ); var inherit = require( '@stdlib/utils/inherit' ); @@ -55,7 +55,8 @@ var commands = require( './commands.js' ); var displayPrompt = require( './display_prompt.js' ); var inputPrompt = require( './input_prompt.js' ); var processLine = require( './process_line.js' ); -var completer = require( './completer.js' ); +var completerFactory = require( './completer.js' ); +var PreviewCompleter = require( './completer_preview.js' ); var ALIAS_OVERRIDES = require( './alias_overrides.js' ); @@ -106,6 +107,9 @@ var debug = logger( 'repl' ); * repl.close(); */ function REPL( options ) { + var previewCompleter; + var completer; + var ttyWrite; var opts; var self; var err; @@ -223,15 +227,19 @@ function REPL( options ) { // Create a REPL execution context: setNonEnumerable( this, '_context', this.createContext() ); + // Create a new TAB completer: + completer = completerFactory( this ); + // Create an internal readline interface: debug( 'Creating readline interface...' ); - setNonEnumerableReadOnly( this, '_rli', readline({ + setNonEnumerableReadOnly( this, '_rli', readline.createInterface({ 'input': this._istream, 'output': this._ostream, 'terminal': opts.isTTY, 'prompt': opts.inputPrompt, - 'completer': completer( this ) + 'completer': completer })); + this._rli.on( 'close', onClose ); this._rli.on( 'line', onLine ); this._rli.on( 'SIGINT', onSIGINT ); @@ -239,6 +247,24 @@ function REPL( options ) { // Add listener for "command" events: this.on( 'command', onCommand ); + // If operating in "terminal" mode, initialize a preview completer... + if ( this._isTTY ) { + // Create a new preview completer: + previewCompleter = new PreviewCompleter( this._rli, completer, this._ostream ); + + // Instruct the input stream to begin emitting "keypress" events: + readline.emitKeypressEvents( this._istream, this._rli ); + + // Add a listener for "keypress" events: + this._istream.on( 'keypress', onKeypress ); + + // Cache a reference to the private readline interface `ttyWrite` to allow calling the method when wanting default behavior: + ttyWrite = this._rli._ttyWrite; + + // Overwrite the private `ttyWrite` method to allow processing input before a "keypress" event is triggered: + this._rli._ttyWrite = beforeKeypress; // WARNING: overwriting a private property + } + // Write a welcome message: this._ostream.write( opts.welcome ); @@ -255,6 +281,29 @@ function REPL( options ) { } return this; + /** + * Callback invoked prior to emitting a "keypress" event. + * + * @private + * @param {string} data - input data + * @param {Object} key - key object + */ + function beforeKeypress( data, key ) { + previewCompleter.beforeKeypress( data, key ); + ttyWrite.call( self._rli, data, key ); + } + + /** + * Callback invoked upon an input stream "keypress" event. + * + * @private + * @param {string} data - input data + * @param {Object} key - key object + */ + function onKeypress( data, key ) { + previewCompleter.onKeypress( data, key ); + } + /** * Callback invoked upon a readline interface "line" event. * @@ -275,6 +324,8 @@ function REPL( options ) { */ function onClose() { debug( 'Readline interface closed.' ); + self._istream.removeListener( 'keypress', onKeypress ); + debug( 'Exiting REPL...' ); self.emit( 'exit' ); } diff --git a/lib/node_modules/@stdlib/repl/test/fixtures/repl.js b/lib/node_modules/@stdlib/repl/test/fixtures/repl.js new file mode 100644 index 000000000000..7e702c4f0378 --- /dev/null +++ b/lib/node_modules/@stdlib/repl/test/fixtures/repl.js @@ -0,0 +1,149 @@ +/** +* @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 isFunction = require( '@stdlib/assert/is-function' ); +var inspectSinkStream = require( '@stdlib/streams/node/inspect-sink' ); +var assign = require( '@stdlib/object/assign' ); +var format = require( '@stdlib/string/format' ); +var REPL = require( './../../lib' ); + + +// FUNCTIONS // + +/** +* Returns default options. +* +* @private +* @returns {Object} default options +* +* @example +* var o = defaults(); +* // returns {...} +*/ +function defaults() { + return { + 'isTTY': true, + 'sandbox': true, + 'timeout': 10000, + 'welcome': '', + 'quiet': true + }; +} + + +// MAIN // + +/** +* Returns a REPL instance connected to debugging IO streams. +* +* @private +* @param {Options} options - REPL options +* @param {WritableStream} options.input - input stream +* @param {Callback} clbk - callback to invoke upon closing a REPL +* @throws {Error} must provide an input stream +* @throws {TypeError} second argument must be a function +* @returns {REPL} REPL instance +* +* @example +* var DebugStream = require( '@stdlib/streams/node/debug' ); +* +* // Define a callback for receiving REPL input on close: +* function onClose( error, data ) { +* if ( error ) { +* return console.error( error.message ); +* } +* console.log( data.join( '\n' ) ); +* } +* +* // Create a stream for writing REPL input: +* var istream = new DebugStream({ +* 'name': 'repl-input-stream' +* }); +* +* // Create a test REPL instance: +* var opts = { +* 'input': istream +* }; +* var repl = mock( opts, onClose ); +* +* // Simulate a user entering a command in the REPL: +* istream.write( '2+2\n' ); +* +* // Close the REPL: +* repl.close(); +*/ +function mock( options, clbk ) { + var opts; + var data; + var repl; + + if ( !isFunction( clbk ) ) { + throw new TypeError( format( 'invalid argument. Second argument must be a function. Value: `%s`.', clbk ) ); + } + opts = assign( defaults(), options ); + if ( !opts.input ) { + throw new Error( 'invalid argument. Options argument must specify an input stream.' ); + } + // If we were not provided an output stream, create a default output stream... + if ( !opts.output ) { + opts.output = inspectSinkStream( onWrite ); + } + // Initialize an array for storing streamed data: + data = []; + + // Create a new REPL instance: + repl = new REPL( opts ); + + // Add a listener for when the REPL exits: + repl.on( 'exit', onExit ); + + // Return the REPL instance: + return repl; + + /** + * Callback invoked upon receiving streamed data. + * + * @private + * @param {(Buffer|string)} chunk - data + * @returns {void} + */ + function onWrite( chunk ) { + data.push( chunk.toString() ); + } + + /** + * Callback invoked when a REPL exits. + * + * @private + */ + function onExit() { + if ( isFunction( opts.output.end ) ) { + opts.output.end(); + } + clbk( null, data ); + } +} + + +// EXPORTS // + +module.exports = mock; diff --git a/lib/node_modules/@stdlib/repl/test/integration/test.completion_previews.js b/lib/node_modules/@stdlib/repl/test/integration/test.completion_previews.js new file mode 100644 index 000000000000..6cda82392747 --- /dev/null +++ b/lib/node_modules/@stdlib/repl/test/integration/test.completion_previews.js @@ -0,0 +1,408 @@ +/** +* @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 trim = require( '@stdlib/string/trim' ); +var repl = require( './../fixtures/repl.js' ); + + +// 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 a completion preview of user-defined variables', function test( t ) { + var istream; + var opts; + var r; + + istream = new DebugStream({ + 'name': 'repl-input-stream' + }); + opts = { + 'input': istream + }; + r = repl( opts, onClose ); + + // Declare a variable with an unique name in order to prevent namespace collisions: + istream.write( 'var abcdefgh = 1;\n' ); + + // Write the beginning of the variable's name in order to trigger a completion preview: + istream.write( 'abcdef' ); + + // Close the input stream: + istream.end(); + + // Close the REPL: + r.close(); + + function onClose( error, data ) { + if ( error ) { + t.fail( error.message ); + return; + } + // Check for an ANSI-escaped color-coded 'gh' completion: + t.strictEqual( data[ data.length-2 ], '\u001b[90mgh\u001b[0m', 'returns expected value' ); + + // Check that the cursor is positioned BEFORE the completion preview: + t.strictEqual( data[ data.length-1 ], '\u001b[2D', 'returns expected value' ); + + t.end(); + } +}); + +tape( 'a REPL instance supports displaying a completion preview for common prefixes', function test( t ) { + var istream; + var opts; + var r; + + istream = new DebugStream({ + 'name': 'repl-input-stream' + }); + opts = { + 'input': istream + }; + r = repl( opts, onClose ); + + // Declare variables with unique names having a common prefix: + istream.write( 'var test_var_name_beep_1 = 1;' ); + istream.write( 'var test_var_name_boop_1 = 1;\n' ); + + // Write the beginning of the common prefix in order to trigger a completion preview: + istream.write( 'test_var_' ); + + // Close the input stream: + istream.end(); + + // Close the REPL: + r.close(); + + function onClose( error, data ) { + if ( error ) { + t.fail( error.message ); + return; + } + // Check for an ANSI-escaped color-coded 'name_b' completion: + t.strictEqual( data[ data.length-2 ], '\u001b[90mname_b\u001b[0m', 'returns expected value' ); + + // Check that the cursor is positioned BEFORE the completion preview: + t.strictEqual( data[ data.length-1 ], '\u001b[6D', 'returns expected value' ); + + t.end(); + } +}); + +tape( 'a REPL instance supports displaying a completion preview for recognized identifiers at the end of a line', function test( t ) { + var istream; + var opts; + var r; + + istream = new DebugStream({ + 'name': 'repl-input-stream' + }); + opts = { + 'input': istream + }; + r = repl( opts, onClose ); + + // Declare a variable with an unique name in order to prevent namespace collisions: + istream.write( 'var abcdefgh = 1;\n' ); + + // Write a compound expression: + istream.write( 'abcdefgh; var x = 2 + 2; abcde' ); + + // Close the input stream: + istream.end(); + + // Close the REPL: + r.close(); + + function onClose( error, data ) { + if ( error ) { + t.fail( error.message ); + return; + } + // Check for an ANSI-escaped color-coded 'fgh' completion: + t.strictEqual( data[ data.length-2 ], '\u001b[90mfgh\u001b[0m', 'returns expected value' ); + + // Check that the cursor is positioned BEFORE the completion preview: + t.strictEqual( data[ data.length-1 ], '\u001b[3D', 'returns expected value' ); + + t.end(); + } +}); + +tape( 'a REPL instance supports displaying a completion preview when a cursor is not at the end of the line', function test( t ) { + var istream; + var opts; + var r; + + istream = new DebugStream({ + 'name': 'repl-input-stream' + }); + opts = { + 'input': istream + }; + r = repl( opts, onClose ); + + // Declare a variable with an unique name in order to prevent namespace collisions: + istream.write( 'var abcdefgh = 1;\n' ); + + // Write the beginning of the variable's name in order to trigger a completion preview: + istream.write( 'abcdef' ); + + // Move cursor to backward by four column positions: + istream.write( '\u001b[4D' ); + + // Close the input stream: + istream.end(); + + // Close the REPL: + r.close(); + + function onClose( error, data ) { + if ( error ) { + t.fail( error.message ); + return; + } + // Check for an ANSI-escaped color-coded 'gh' completion before moving the cursor: + t.strictEqual( data[ data.length-4 ], '\u001b[90mgh\u001b[0m', 'returns expected value' ); + + // Check that the cursor is positioned BEFORE the completion preview: + t.strictEqual( data[ data.length-3 ], '\u001b[2D', 'returns expected value' ); + + // Check for an ANSI-escaped color-coded 'gh' completion after moving the cursor: + t.strictEqual( data[ data.length-2 ], '\u001b[90mgh\u001b[0m', 'returns expected value' ); + + // Check that the cursor is positioned BEFORE the completion preview: + t.strictEqual( data[ data.length-1 ], '\u001b[2D', 'returns expected value' ); + + t.end(); + } +}); + +tape( 'a REPL instance supports auto-completing a completion candidate by moving the cursor forward', function test( t ) { + var istream; + var opts; + var r; + + istream = new DebugStream({ + 'name': 'repl-input-stream' + }); + opts = { + 'input': istream + }; + r = repl( opts, onClose ); + + // Declare a variable with an unique name in order to prevent namespace collisions: + istream.write( 'var abcdefgh = 1;\n' ); + + // Write the beginning of the variable's name in order to trigger a completion preview: + istream.write( 'abcdef' ); + + // Move the cursor forward by one column position: + istream.write( '\u001b[1C' ); + + // Close the input stream: + istream.end(); + + // Close the REPL: + r.close(); + + function onClose( error, data ) { + if ( error ) { + t.fail( error.message ); + return; + } + // Check for an ANSI-escaped color-coded 'gh' completion: + t.strictEqual( data[ data.length-3 ], '\u001b[90mgh\u001b[0m', 'returns expected value' ); + + // Check that the cursor is positioned BEFORE the completion preview: + t.strictEqual( data[ data.length-2 ], '\u001b[2D', 'returns expected value' ); + + // Check that the completion preview was auto-completed: + t.strictEqual( data[ data.length-1 ], 'gh', 'returns expected value' ); + + t.end(); + } +}); + +tape( 'a REPL instance supports auto-completing a completion preview and execution by pressing ENTER', function test( t ) { + var istream; + var opts; + var r; + + istream = new DebugStream({ + 'name': 'repl-input-stream' + }); + opts = { + 'input': istream, + 'inputPrompt': '> ', + 'outputPrompt': '' + }; + r = repl( opts, onClose ); + + // Declare a variable with an unique name in order to prevent namespace collisions: + istream.write( 'var abcdefgh = 1;' ); + + // Write the beginning of the variable's name in order to trigger a completion preview: + istream.write( 'abcdef' ); + + // Simulate pressing ENTER: + istream.write( '\n' ); + + // Close the input stream: + istream.end(); + + // Close the REPL: + r.close(); + + function onClose( error, data ) { + if ( error ) { + t.fail( error.message ); + return; + } + // Check for an ANSI-escaped color-coded 'gh' completion: + t.strictEqual( data[ data.length-5 ], '\u001b[90mgh\u001b[0m', 'returns expected value' ); + + // Check that the cursor is positioned BEFORE the completion preview: + t.strictEqual( data[ data.length-4 ], '\u001b[2D', 'returns expected value' ); + + // Check that the completion preview was auto-completed prior to execution: + t.strictEqual( data[ data.length-3 ], 'gh', 'returns expected value' ); + + // Check that the expression was executed: + t.strictEqual( trim( data[ data.length-1 ] ), '1', 'returns expected value' ); + + t.end(); + } +}); + +tape( 'a REPL instance does not display a completion preview when no completion candidate exists', function test( t ) { + var istream; + var opts; + var r; + + istream = new DebugStream({ + 'name': 'repl-input-stream' + }); + opts = { + 'input': istream + }; + r = repl( opts, onClose ); + + // Declare a variable with an unique name in order to prevent namespace collisions: + istream.write( 'var abcdefgh = 1;\n' ); + + // Write the beginning of the variable's name in order to trigger a completion preview: + istream.write( 'abcdef' ); + + // Write a character which invalidates the completion candidate: + istream.write( 'o' ); + + // Close the input stream: + istream.end(); + + // Close the REPL: + r.close(); + + function onClose( error, data ) { + if ( error ) { + t.fail( error.message ); + return; + } + // Check for an ANSI-escaped color-coded 'gh' completion: + t.strictEqual( data[ data.length-5 ], '\u001b[90mgh\u001b[0m', 'returns expected value' ); + + // Check that the cursor is positioned BEFORE the completion preview: + t.strictEqual( data[ data.length-4 ], '\u001b[2D', 'returns expected value' ); + + // Check for the character which invalidated a completion candidate: + t.strictEqual( data[ data.length-3 ], 'o', 'returns expected value' ); + + // Check that the completion preview is replaced with whitepsace: + t.strictEqual( data[ data.length-2 ], ' ', 'returns expected value' ); + + // Check that the cursor is returned to the position preceding the whitespace: + t.strictEqual( data[ data.length-1 ], '\u001b[2D', 'returns expected value' ); + + t.end(); + } +}); + +tape( 'a REPL instance does not display a completion preview once a user enters whitespace', function test( t ) { + var istream; + var opts; + var r; + + istream = new DebugStream({ + 'name': 'repl-input-stream' + }); + opts = { + 'input': istream, + 'inputPrompt': '> ', + 'outputPrompt': '' + }; + r = repl( opts, onClose ); + + // Declare a variable with an unique name in order to prevent namespace collisions: + istream.write( 'var abcdefgh = 1;\n' ); + + // Write the beginning of the variable's name in order to trigger a completion preview: + istream.write( 'abcdef' ); + + // Write whitespace to invalidate the completion candidate: + istream.write( ' ' ); + + // Close the input stream: + istream.end(); + + // Close the REPL: + r.close(); + + function onClose( error, data ) { + if ( error ) { + t.fail( error.message ); + return; + } + // Check for an ANSI-escaped color-coded 'gh' completion: + t.strictEqual( data[ data.length-5 ], '\u001b[90mgh\u001b[0m', 'returns expected value' ); + + // Check that the cursor is positioned BEFORE the completion preview: + t.strictEqual( data[ data.length-4 ], '\u001b[2D', 'returns expected value' ); + + // Check for the whitespace which invalidated a completion candidate: + t.strictEqual( data[ data.length-3 ], ' ', 'returns expected value' ); + + // Check that the completion preview is replaced with whitepsace: + t.strictEqual( data[ data.length-2 ], ' ', 'returns expected value' ); + + // Check that the cursor is returned to the position preceding the whitespace: + t.strictEqual( data[ data.length-1 ], '\u001b[2D', 'returns expected value' ); + + t.end(); + } +});