diff --git a/lib/node_modules/@stdlib/repl/lib/defaults.js b/lib/node_modules/@stdlib/repl/lib/defaults.js index 6bc31efa71e0..e440962529e4 100644 --- a/lib/node_modules/@stdlib/repl/lib/defaults.js +++ b/lib/node_modules/@stdlib/repl/lib/defaults.js @@ -23,6 +23,7 @@ var stdin = require( '@stdlib/streams/node/stdin' ); var stdout = require( '@stdlib/streams/node/stdout' ); var WELCOME = require( './welcome_text.js' ); +var THEMES = require( './themes.js' ); // MAIN // @@ -66,6 +67,9 @@ function defaults() { // Welcome message: 'welcome': WELCOME, + // Syntax-highlighting themes: + 'themes': THEMES, + // File path specifying where to save REPL command history: 'save': '', diff --git a/lib/node_modules/@stdlib/repl/lib/main.js b/lib/node_modules/@stdlib/repl/lib/main.js index 4c872c37bf2f..a6d92c4b3271 100644 --- a/lib/node_modules/@stdlib/repl/lib/main.js +++ b/lib/node_modules/@stdlib/repl/lib/main.js @@ -26,6 +26,9 @@ var EventEmitter = require( 'events' ).EventEmitter; var readline = require( 'readline' ); var proc = require( 'process' ); var resolve = require( 'path' ).resolve; +var join = require( 'path' ).join; +var mkdir = require( 'fs' ).mkdirSync; // eslint-disable-line node/no-sync +var dirname = require( 'path' ).dirname; var logger = require( 'debug' ); var inherit = require( '@stdlib/utils/inherit' ); var isString = require( '@stdlib/assert/is-string' ).isPrimitive; @@ -34,7 +37,11 @@ var isPlainObject = require( '@stdlib/assert/is-plain-object' ); var isFunction = require( '@stdlib/assert/is-function' ); var isConfigurableProperty = require( '@stdlib/assert/is-configurable-property' ); var hasOwnProp = require( '@stdlib/assert/has-own-property' ); -var objectKeys = require( '@stdlib/utils/keys' ); +var isEmptyObject = require( '@stdlib/assert/is-empty-object' ); +var deepEqual = require( '@stdlib/assert/deep-equal' ); +var copy = require( '@stdlib/utils/copy' ); +var pick = require( '@stdlib/utils/pick' ); +var mergeFcn = require( '@stdlib/utils/merge' ).factory; var setNonEnumerable = require( '@stdlib/utils/define-nonenumerable-property' ); var setNonEnumerableReadOnly = require( '@stdlib/utils/define-nonenumerable-read-only-property' ); var setReadOnly = require( '@stdlib/utils/define-read-only-property' ); @@ -43,7 +50,11 @@ var append = require( '@stdlib/utils/append' ); var format = require( '@stdlib/string/format' ); var Boolean = require( '@stdlib/boolean/ctor' ); var cwd = require( '@stdlib/process/cwd' ); +var resolveParentPath = require( '@stdlib/fs/resolve-parent-path' ).sync; +var homeDir = require( '@stdlib/os/homedir' ); +var dirExists = require( '@stdlib/fs/exists' ).sync; var readFileSync = require( '@stdlib/fs/read-file' ).sync; +var writeFileSync = require( '@stdlib/fs/write-file' ).sync; var RE_EOL = require( '@stdlib/regexp/eol' ).REGEXP; var fifo = require( '@stdlib/utils/fifo' ); var nextTick = require( '@stdlib/utils/next-tick' ); @@ -75,6 +86,22 @@ var SETTINGS_VALIDATORS = require( './settings_validators.js' ); // VARIABLES // var debug = logger( 'repl' ); +var mergeOptions = mergeFcn({ + 'level': 2 // to merge individual settings and themes +}); + +// List of persistable options (note: keep in alphabetical order): +var PERSISTABLE_OPTIONS = [ + 'inputPrompt', + 'outputPrompt', + 'welcome', + 'padding', + 'themes', + 'save', + 'log', + 'quiet', + 'settings' +]; // MAIN // @@ -129,12 +156,12 @@ var debug = logger( 'repl' ); * repl.close(); */ function REPL( options ) { + var configPath; var ostream; - var themes; + var config; var opts; var self; var err; - var i; if ( !( this instanceof REPL ) ) { if ( arguments.length ) { @@ -145,6 +172,9 @@ function REPL( options ) { self = this; opts = defaults(); + configPath = this._resolveConfigurationPath(); + config = this._getConfiguration( configPath ); + opts = mergeOptions( opts, config ); if ( arguments.length ) { err = validate( opts, options ); if ( err ) { @@ -184,6 +214,12 @@ function REPL( options ) { // Setup the output stream pipeline: ostream.pipe( opts.output ); + // Cache references to initial REPL configuration: + setNonEnumerableReadOnly( this, '_config', config ); + setNonEnumerableReadOnly( this, '_configPath', configPath ); + setNonEnumerableReadOnly( this, '_options', options ); + setNonEnumerableReadOnly( this, '_opts', copy( opts ) ); + // Cache references to the input and output streams: setNonEnumerableReadOnly( this, '_istream', opts.input ); setNonEnumerableReadOnly( this, '_ostream', ostream ); @@ -248,8 +284,8 @@ function REPL( options ) { // Initialize an internal flag indicating whether the REPL is currently busy with asynchronous processing: setNonEnumerable( this, '_busy', false ); - // Initialize an internal flag indicating whether we've received a `SIGINT` signal: - setNonEnumerable( this, '_SIGINT', false ); + // Initialize an internal counter indicating number of processed `SIGINT` signals: + setNonEnumerable( this, '_SIGINT', 0 ); // Initialize an internal variable for caching the result of the last successfully evaluated command: setNonEnumerable( this, '_ans', void 0 ); @@ -285,7 +321,7 @@ function REPL( options ) { setNonEnumerableReadOnly( this, '_previewCompleter', new PreviewCompleter( this._rli, this._completer, this._ostream, this._settings.completionPreviews ) ); // Initialize a syntax-highlighter: - setNonEnumerableReadOnly( this, '_syntaxHighlighter', new SyntaxHighlighter( this, this._ostream, this._settings.syntaxHighlighting ) ); + setNonEnumerableReadOnly( this, '_syntaxHighlighter', new SyntaxHighlighter( this, this._ostream, opts.themes, this._settings.theme, this._settings.syntaxHighlighting ) ); // Cache a reference to the private readline interface `ttyWrite` to allow calling the method when wanting default behavior: setNonEnumerableReadOnly( this, '_ttyWrite', this._rli._ttyWrite ); @@ -315,16 +351,6 @@ function REPL( options ) { // TODO: check whether to synchronously initialize a REPL log file - // Add any provided user-defined themes... - if ( opts.themes ) { - themes = objectKeys( opts.themes ); - for ( i = 0; i < themes.length; i++ ) { - this.addTheme( themes[ i ], opts.themes[ themes[ i ] ] ); - } - } - // Set the syntax highlighting theme... - this.settings( 'theme', opts.settings.theme ); - // Initialize bracketed-paste: if ( opts.settings.bracketedPaste !== void 0 ) { this.settings( 'bracketedPaste', opts.settings.bracketedPaste ); @@ -400,7 +426,7 @@ function REPL( options ) { * @param {string} line - line data */ function onLine( line ) { - self._SIGINT = false; // reset flag + self._SIGINT = 0; // reset flag if ( self._closed === false ) { self._multilineHandler.processLine( line ); } @@ -465,15 +491,25 @@ function REPL( options ) { // In the absence of any entered partial and/or unevaluated commands, determine whether we should close the REPL... if ( self._cmd.length === 0 && isEmpty ) { - if ( self._SIGINT ) { - self._SIGINT = false; + if ( self._SIGINT === 2 ) { + self._SIGINT = 0; + self.close(); + return; + } + if ( self._SIGINT === 1 ) { + if ( self._detectUnsavedChanges() ) { + self._SIGINT += 1; + self._rli.question( 'You have unsaved preferences. Do you want to save? [Y/N]: ', onAnswer ); + return; + } + self._SIGINT = 0; self.close(); return; } self._ostream.write( 'To exit, enter ^D or ^C again or enter quit().\n' ); - self._SIGINT = true; + self._SIGINT += 1; } else { - self._SIGINT = false; + self._SIGINT = 0; } // Reset the command queue: self._queue.clear(); @@ -483,6 +519,28 @@ function REPL( options ) { // Display a new prompt: displayPrompt( self, false ); + + /** + * Callback invoked upon receiving an answer to a question. + * + * @private + * @param {string} answer - user answer + */ + function onAnswer( answer ) { + if ( answer === 'y' || answer === 'Y' ) { + self._saveConfiguration( config, options ); + self._SIGINT = 0; + self._ostream.write( 'Preferences saved.\n' ); + self.close(); + return; + } + if ( answer === 'n' || answer === 'N' ) { + self._SIGINT = 0; + self.close(); + return; + } + self._rli.question( 'You have unsaved preferences. Do you want to save? [Y/N]: ', onAnswer ); + } } /** @@ -538,6 +596,96 @@ setNonEnumerableReadOnly( REPL.prototype, '_prompt', function prompt() { return inputPrompt( this._inputPrompt, this._count ); }); +/** +* Resolves any existing REPL configuration path. +* +* @private +* @name _resolveConfigurationPath +* @memberof REPL.prototype +* @type {Function} +* @returns {string} configuration file path +*/ +setNonEnumerableReadOnly( REPL.prototype, '_resolveConfigurationPath', function resolveConfigurationPath() { + var f; + + f = resolveParentPath( '.stdlib_repl.json' ); + if ( f ) { + return f; + } + return join( homeDir(), '.stdlib', 'repl.json' ); +}); + +/** +* Reads from a configuration file path. +* +* @private +* @name _getConfiguration +* @memberof REPL.prototype +* @type {Function} +* @param {string} fpath - configuration file path +* @returns {Object} configuration object +*/ +setNonEnumerableReadOnly( REPL.prototype, '_getConfiguration', function getConfiguration( fpath ) { + /* eslint-disable stdlib/no-dynamic-require, no-unused-vars */ + var config; + try { + config = require( fpath ); + if ( isPlainObject( config ) ) { + return config; + } + } catch ( e ) { + return {}; + } + return {}; +}); + +/** +* Saves preferences to a REPL configuration file. +* +* @private +* @name _saveConfiguration +* @memberof REPL.prototype +* @type {Function} +* @returns {void} +*/ +setNonEnumerableReadOnly( REPL.prototype, '_saveConfiguration', function saveConfiguration() { + var newConfig; + var options; + var path; + + options = pick( this._options, PERSISTABLE_OPTIONS ); + newConfig = mergeOptions( this._config, options ); + newConfig.settings = this._settings; + newConfig.themes = this._syntaxHighlighter.getAllThemesConfig(); + path = dirname( this._configPath ); + if ( !dirExists( path ) ) { + mkdir( path ); + } + writeFileSync( this._configPath, JSON.stringify( newConfig, null, 2 ) ); +}); + +/** +* Checks if there are any unsaved user preferences in the current REPL session. +* +* @private +* @name _detectUnsavedChanges +* @memberof REPL.prototype +* @type {Function} +* @returns {boolean} boolean indicating whether there are unsaved preferences in the REPL +*/ +setNonEnumerableReadOnly( REPL.prototype, '_detectUnsavedChanges', function detectUnsavedChanges() { + if ( !isEmptyObject( this._options ) ) { + return true; + } + if ( !deepEqual( this._opts.settings, this._settings ) ) { + return true; + } + if ( !deepEqual( this._opts.themes, this._syntaxHighlighter.getAllThemesConfig() ) ) { // eslint-disable-line max-len + return true; + } + return false; +}); + /** * Returns the current line's prompt length. * diff --git a/lib/node_modules/@stdlib/repl/lib/syntax_highlighter.js b/lib/node_modules/@stdlib/repl/lib/syntax_highlighter.js index dbe1c848f7de..697cb1e7eb62 100644 --- a/lib/node_modules/@stdlib/repl/lib/syntax_highlighter.js +++ b/lib/node_modules/@stdlib/repl/lib/syntax_highlighter.js @@ -31,7 +31,6 @@ var objectKeys = require( '@stdlib/utils/keys' ); var omit = require( '@stdlib/utils/omit' ); var hasOwnProp = require( '@stdlib/assert/has-own-property' ); var tokenizer = require( './tokenizer.js' ); -var THEMES = require( './themes.js' ); var ANSI = require( './ansi_colors.js' ); @@ -85,12 +84,14 @@ function removeDuplicateTokens( tokens ) { * @constructor * @param {REPL} repl - REPL instance * @param {WritableStream} ostream - writable stream +* @param {Object} themes - table of initial syntax-highlighting themes +* @param {theme} theme - initial color theme * @param {boolean} enabled - boolean indicating whether the syntax-highlighter should be initially enabled * @returns {SyntaxHighlighter} syntax-highlighter instance */ -function SyntaxHighlighter( repl, ostream, enabled ) { +function SyntaxHighlighter( repl, ostream, themes, theme, enabled ) { if ( !( this instanceof SyntaxHighlighter ) ) { - return new SyntaxHighlighter( repl, ostream, enabled ); + return new SyntaxHighlighter( repl, ostream, themes, theme, enabled ); } debug( 'Creating a new syntax-highlighter' ); @@ -116,10 +117,10 @@ function SyntaxHighlighter( repl, ostream, enabled ) { this._highlightedLine = ''; // Initialize an object storing all available themes: - this._themes = THEMES; + this._themes = themes; // Initialize a variable storing the current theme: - this._theme = DEFAULT_THEME; + this._theme = theme; return this; } @@ -216,6 +217,18 @@ setNonEnumerableReadOnly( SyntaxHighlighter.prototype, 'getThemeConfig', functio return this._themes[ theme ]; }); +/** +* Returns all color themes. +* +* @name getAllThemesConfig +* @memberof SyntaxHighlighter.prototype +* @type {Function} +* @returns {Object} themes object +*/ +setNonEnumerableReadOnly( SyntaxHighlighter.prototype, 'getAllThemesConfig', function getAllThemesConfig() { + return this._themes; +}); + /** * Adds a new color theme. * diff --git a/lib/node_modules/@stdlib/repl/lib/validate.js b/lib/node_modules/@stdlib/repl/lib/validate.js index 11ce529317d8..e6c9da7f015f 100644 --- a/lib/node_modules/@stdlib/repl/lib/validate.js +++ b/lib/node_modules/@stdlib/repl/lib/validate.js @@ -22,6 +22,8 @@ var isPlainObject = require( '@stdlib/assert/is-plain-object' ); var hasOwnProp = require( '@stdlib/assert/has-own-property' ); +var objectKeys = require( '@stdlib/utils/keys' ); +var mergeFcn = require( '@stdlib/utils/merge' ).factory; var isReadableStreamLike = require( '@stdlib/assert/is-node-readable-stream-like' ); var isWritableStreamLike = require( '@stdlib/assert/is-node-writable-stream-like' ); var isString = require( '@stdlib/assert/is-string' ).isPrimitive; @@ -32,6 +34,13 @@ var format = require( '@stdlib/string/format' ); var validateSettings = require( './validate_settings.js' ); +// VARIABLES // + +var extendThemes = mergeFcn({ + 'level': 1 +}); + + // MAIN // /** @@ -67,7 +76,10 @@ var validateSettings = require( './validate_settings.js' ); * } */ function validate( opts, options ) { + var themes; var err; + var i; + if ( !isPlainObject( options ) ) { return new TypeError( format( 'invalid argument. Options argument must be an object. Value: `%s`.', options ) ); } @@ -108,10 +120,16 @@ function validate( opts, options ) { } } if ( hasOwnProp( options, 'themes' ) ) { - opts.themes = options.themes; if ( !isPlainObject( options.themes ) ) { return new TypeError( format( 'invalid option. `%s` option must be an object. Option: `%s`.', 'themes', options.themes ) ); } + themes = objectKeys( options.themes ); + for ( i = 0; i < themes.length; i++ ) { + if ( !isPlainObject( themes[ i ] ) ) { + return new TypeError( format( 'invalid option. Each theme must be an object. Option: `%s`.', options.themes ) ); + } + } + extendThemes( opts.themes, options.themes ); } if ( hasOwnProp( options, 'save' ) ) { opts.save = options.save;