diff options
Diffstat (limited to 'devtools/shared/gcli/source/lib/gcli/cli.js')
-rw-r--r-- | devtools/shared/gcli/source/lib/gcli/cli.js | 2209 |
1 files changed, 2209 insertions, 0 deletions
diff --git a/devtools/shared/gcli/source/lib/gcli/cli.js b/devtools/shared/gcli/source/lib/gcli/cli.js new file mode 100644 index 000000000..4b7c115e2 --- /dev/null +++ b/devtools/shared/gcli/source/lib/gcli/cli.js @@ -0,0 +1,2209 @@ +/* + * Copyright 2012, Mozilla Foundation and contributors + * + * 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'; + +var util = require('./util/util'); +var host = require('./util/host'); +var l10n = require('./util/l10n'); + +var view = require('./ui/view'); +var Parameter = require('./commands/commands').Parameter; +var CommandOutputManager = require('./commands/commands').CommandOutputManager; + +var Status = require('./types/types').Status; +var Conversion = require('./types/types').Conversion; +var commandModule = require('./types/command'); +var selectionModule = require('./types/selection'); + +var Argument = require('./types/types').Argument; +var ArrayArgument = require('./types/types').ArrayArgument; +var NamedArgument = require('./types/types').NamedArgument; +var TrueNamedArgument = require('./types/types').TrueNamedArgument; +var MergedArgument = require('./types/types').MergedArgument; +var ScriptArgument = require('./types/types').ScriptArgument; + +var RESOLVED = Promise.resolve(undefined); + +// Helper to produce a `deferred` object +// using DOM Promise +function defer() { + let resolve, reject; + let p = new Promise((a, b) => { + resolve = a; + reject = b; + }); + return { + promise: p, + resolve: resolve, + reject: reject + }; +} + +/** + * This is a list of the known command line components to enable certain + * privileged commands to alter parts of a running command line. It is an array + * of objects shaped like: + * { conversionContext:..., executionContext:..., mapping:... } + * So lookup is O(n) where 'n' is the number of command lines. + */ +var instances = []; + +/** + * An indexOf that looks-up both types of context + */ +function instanceIndex(context) { + for (var i = 0; i < instances.length; i++) { + var instance = instances[i]; + if (instance.conversionContext === context || + instance.executionContext === context) { + return i; + } + } + return -1; +} + +/** + * findInstance gets access to a Terminal object given a conversionContext or + * an executionContext (it doesn't have to be a terminal object, just whatever + * was passed into addMapping() + */ +exports.getMapping = function(context) { + var index = instanceIndex(context); + if (index === -1) { + console.log('Missing mapping for context: ', context); + console.log('Known contexts: ', instances); + throw new Error('Missing mapping for context'); + } + return instances[index].mapping; +}; + +/** + * Add a requisition context->terminal mapping + */ +var addMapping = function(requisition) { + if (instanceIndex(requisition.conversionContext) !== -1) { + throw new Error('Remote existing mapping before adding a new one'); + } + + instances.push({ + conversionContext: requisition.conversionContext, + executionContext: requisition.executionContext, + mapping: { requisition: requisition } + }); +}; + +/** + * Remove a requisition context->terminal mapping + */ +var removeMapping = function(requisition) { + var index = instanceIndex(requisition.conversionContext); + instances.splice(index, 1); +}; + +/** + * Assignment is a link between a parameter and the data for that parameter. + * The data for the parameter is available as in the preferred type and as + * an Argument for the CLI. + * <p>We also record validity information where applicable. + * <p>For values, null and undefined have distinct definitions. null means + * that a value has been provided, undefined means that it has not. + * Thus, null is a valid default value, and common because it identifies an + * parameter that is optional. undefined means there is no value from + * the command line. + * @constructor + */ +function Assignment(param) { + // The parameter that we are assigning to + this.param = param; + this.conversion = undefined; +} + +/** + * Easy accessor for conversion.arg. + * This is a read-only property because writes to arg should be done through + * the 'conversion' property. + */ +Object.defineProperty(Assignment.prototype, 'arg', { + get: function() { + return this.conversion == null ? undefined : this.conversion.arg; + }, + enumerable: true +}); + +/** + * Easy accessor for conversion.value. + * This is a read-only property because writes to value should be done through + * the 'conversion' property. + */ +Object.defineProperty(Assignment.prototype, 'value', { + get: function() { + return this.conversion == null ? undefined : this.conversion.value; + }, + enumerable: true +}); + +/** + * Easy (and safe) accessor for conversion.message + */ +Object.defineProperty(Assignment.prototype, 'message', { + get: function() { + if (this.conversion != null && this.conversion.message) { + return this.conversion.message; + } + // ERROR conversions have messages, VALID conversions don't need one, so + // we just need to consider INCOMPLETE conversions. + if (this.getStatus() === Status.INCOMPLETE) { + return l10n.lookupFormat('cliIncompleteParam', [ this.param.name ]); + } + return ''; + }, + enumerable: true +}); + +/** + * Easy (and safe) accessor for conversion.getPredictions() + * @return An array of objects with name and value elements. For example: + * [ { name:'bestmatch', value:foo1 }, { name:'next', value:foo2 }, ... ] + */ +Assignment.prototype.getPredictions = function(context) { + return this.conversion == null ? [] : this.conversion.getPredictions(context); +}; + +/** + * Accessor for a prediction by index. + * This is useful above <tt>getPredictions()[index]</tt> because it normalizes + * index to be within the bounds of the predictions, which means that the UI + * can maintain an index of which prediction to choose without caring how many + * predictions there are. + * @param rank The index of the prediction to choose + */ +Assignment.prototype.getPredictionRanked = function(context, rank) { + if (rank == null) { + rank = 0; + } + + if (this.isInName()) { + return Promise.resolve(undefined); + } + + return this.getPredictions(context).then(function(predictions) { + if (predictions.length === 0) { + return undefined; + } + + rank = rank % predictions.length; + if (rank < 0) { + rank = predictions.length + rank; + } + return predictions[rank]; + }.bind(this)); +}; + +/** + * Some places want to take special action if we are in the name part of a + * named argument (i.e. the '--foo' bit). + * Currently this does not take actual cursor position into account, it just + * assumes that the cursor is at the end. In the future we will probably want + * to take this into account. + */ +Assignment.prototype.isInName = function() { + return this.conversion.arg.type === 'NamedArgument' && + this.conversion.arg.prefix.slice(-1) !== ' '; +}; + +/** + * Work out what the status of the current conversion is which involves looking + * not only at the conversion, but also checking if data has been provided + * where it should. + * @param arg For assignments with multiple args (e.g. array assignments) we + * can narrow the search for status to a single argument. + */ +Assignment.prototype.getStatus = function(arg) { + if (this.param.isDataRequired && !this.conversion.isDataProvided()) { + return Status.INCOMPLETE; + } + + // Selection/Boolean types with a defined range of values will say that + // '' is INCOMPLETE, but the parameter may be optional, so we don't ask + // if the user doesn't need to enter something and hasn't done so. + if (!this.param.isDataRequired && this.arg.type === 'BlankArgument') { + return Status.VALID; + } + + return this.conversion.getStatus(arg); +}; + +/** + * Helper when we're rebuilding command lines. + */ +Assignment.prototype.toString = function() { + return this.conversion.toString(); +}; + +/** + * For test/debug use only. The output from this function is subject to wanton + * random change without notice, and should not be relied upon to even exist + * at some later date. + */ +Object.defineProperty(Assignment.prototype, '_summaryJson', { + get: function() { + return { + param: this.param.name + '/' + this.param.type.name, + defaultValue: this.param.defaultValue, + arg: this.conversion.arg._summaryJson, + value: this.value, + message: this.message, + status: this.getStatus().toString() + }; + }, + enumerable: true +}); + +exports.Assignment = Assignment; + + +/** + * How to dynamically execute JavaScript code + */ +var customEval = eval; + +/** + * Setup a function to be called in place of 'eval', generally for security + * reasons + */ +exports.setEvalFunction = function(newCustomEval) { + customEval = newCustomEval; +}; + +/** + * Remove the binding done by setEvalFunction(). + * We purposely set customEval to undefined rather than to 'eval' because there + * is an implication of setEvalFunction that we're in a security sensitive + * situation. What if we can trick GCLI into calling unsetEvalFunction() at the + * wrong time? + * So to properly undo the effects of setEvalFunction(), you need to call + * setEvalFunction(eval) rather than unsetEvalFunction(), however the latter is + * preferred in most cases. + */ +exports.unsetEvalFunction = function() { + customEval = undefined; +}; + +/** + * 'eval' command + */ +var evalCmd = { + item: 'command', + name: '{', + params: [ + { + name: 'javascript', + type: 'javascript', + description: '' + } + ], + hidden: true, + description: { key: 'cliEvalJavascript' }, + exec: function(args, context) { + var reply = customEval(args.javascript); + return context.typedData(typeof reply, reply); + }, + isCommandRegexp: /^\s*\{\s*/ +}; + +exports.items = [ evalCmd ]; + +/** + * This is a special assignment to reflect the command itself. + */ +function CommandAssignment(requisition) { + var commandParamMetadata = { + name: '__command', + type: { name: 'command', allowNonExec: false } + }; + // This is a hack so that rather than reply with a generic description of the + // command assignment, we reply with the description of the assigned command, + // (using a generic term if there is no assigned command) + var self = this; + Object.defineProperty(commandParamMetadata, 'description', { + get: function() { + var value = self.value; + return value && value.description ? + value.description : + 'The command to execute'; + }, + enumerable: true + }); + this.param = new Parameter(requisition.system.types, commandParamMetadata); +} + +CommandAssignment.prototype = Object.create(Assignment.prototype); + +CommandAssignment.prototype.getStatus = function(arg) { + return Status.combine( + Assignment.prototype.getStatus.call(this, arg), + this.conversion.value && this.conversion.value.exec ? + Status.VALID : Status.INCOMPLETE + ); +}; + +exports.CommandAssignment = CommandAssignment; + + +/** + * Special assignment used when ignoring parameters that don't have a home + */ +function UnassignedAssignment(requisition, arg) { + var isIncompleteName = (arg.text.charAt(0) === '-'); + this.param = new Parameter(requisition.system.types, { + name: '__unassigned', + description: l10n.lookup('cliOptions'), + type: { + name: 'param', + requisition: requisition, + isIncompleteName: isIncompleteName + } + }); + + // It would be nice to do 'conversion = parm.type.parse(arg, ...)' except + // that type.parse returns a promise (even though it's synchronous in this + // case) + if (isIncompleteName) { + var lookup = commandModule.getDisplayedParamLookup(requisition); + var predictions = selectionModule.findPredictions(arg, lookup); + this.conversion = selectionModule.convertPredictions(arg, predictions); + } + else { + var message = l10n.lookup('cliUnusedArg'); + this.conversion = new Conversion(undefined, arg, Status.ERROR, message); + } + + this.conversion.assignment = this; +} + +UnassignedAssignment.prototype = Object.create(Assignment.prototype); + +UnassignedAssignment.prototype.getStatus = function(arg) { + return this.conversion.getStatus(); +}; + +var logErrors = true; + +/** + * Allow tests that expect failures to avoid clogging up the console + */ +Object.defineProperty(exports, 'logErrors', { + get: function() { + return logErrors; + }, + set: function(val) { + logErrors = val; + }, + enumerable: true +}); + +/** + * A Requisition collects the information needed to execute a command. + * + * (For a definition of the term, see http://en.wikipedia.org/wiki/Requisition) + * This term is used because carries the notion of a work-flow, or process to + * getting the information to execute a command correct. + * There is little point in a requisition for parameter-less commands because + * there is no information to collect. A Requisition is a collection of + * assignments of values to parameters, each handled by an instance of + * Assignment. + * + * @param system Allows access to the various plug-in points in GCLI. At a + * minimum it must contain commands and types objects. + * @param options A set of options to customize how GCLI is used. Includes: + * - environment An optional opaque object passed to commands in the + * Execution Context. + * - document A DOM Document passed to commands using the Execution Context in + * order to allow creation of DOM nodes. If missing Requisition will use the + * global 'document', or leave undefined. + * - commandOutputManager A custom commandOutputManager to which output should + * be sent + * @constructor + */ +function Requisition(system, options) { + options = options || {}; + + this.environment = options.environment || {}; + this.document = options.document; + if (this.document == null) { + try { + this.document = document; + } + catch (ex) { + // Ignore + } + } + + this.commandOutputManager = options.commandOutputManager || new CommandOutputManager(); + this.system = system; + + this.shell = { + cwd: '/', // Where we store the current working directory + env: {} // Where we store the current environment + }; + + // The command that we are about to execute. + // @see setCommandConversion() + this.commandAssignment = new CommandAssignment(this); + + // The object that stores of Assignment objects that we are filling out. + // The Assignment objects are stored under their param.name for named + // lookup. Note: We make use of the property of Javascript objects that + // they are not just hashmaps, but linked-list hashmaps which iterate in + // insertion order. + // _assignments excludes the commandAssignment. + this._assignments = {}; + + // The count of assignments. Excludes the commandAssignment + this.assignmentCount = 0; + + // Used to store cli arguments in the order entered on the cli + this._args = []; + + // Used to store cli arguments that were not assigned to parameters + this._unassigned = []; + + // Changes can be asynchronous, when one update starts before another + // finishes we abandon the former change + this._nextUpdateId = 0; + + // We can set a prefix to typed commands to make it easier to focus on + // Allowing us to type "add -a; commit" in place of "git add -a; git commit" + this.prefix = ''; + + addMapping(this); + this._setBlankAssignment(this.commandAssignment); + + // If a command calls context.update then the UI needs some way to be + // informed of the change + this.onExternalUpdate = util.createEvent('Requisition.onExternalUpdate'); +} + +/** + * Avoid memory leaks + */ +Requisition.prototype.destroy = function() { + this.document = undefined; + this.environment = undefined; + removeMapping(this); +}; + +/** + * If we're about to make an asynchronous change when other async changes could + * overtake this one, then we want to be able to bail out if overtaken. The + * value passed back from beginChange should be passed to endChangeCheckOrder + * on completion of calculation, before the results are applied in order to + * check that the calculation has not been overtaken + */ +Requisition.prototype._beginChange = function() { + var updateId = this._nextUpdateId; + this._nextUpdateId++; + return updateId; +}; + +/** + * Check to see if another change has started since updateId started. + * This allows us to bail out of an update. + * It's hard to make updates atomic because until you've responded to a parse + * of the command argument, you don't know how to parse the arguments to that + * command. + */ +Requisition.prototype._isChangeCurrent = function(updateId) { + return updateId + 1 === this._nextUpdateId; +}; + +/** + * See notes on beginChange + */ +Requisition.prototype._endChangeCheckOrder = function(updateId) { + if (updateId + 1 !== this._nextUpdateId) { + // An update that started after we did has already finished, so our + // changes are out of date. Abandon further work. + return false; + } + + return true; +}; + +var legacy = false; + +/** + * Functions and data related to the execution of a command + */ +Object.defineProperty(Requisition.prototype, 'executionContext', { + get: function() { + if (this._executionContext == null) { + this._executionContext = { + defer: defer, + typedData: function(type, data) { + return { + isTypedData: true, + data: data, + type: type + }; + }, + getArgsObject: this.getArgsObject.bind(this) + }; + + // Alias requisition so we're clear about what's what + var requisition = this; + Object.defineProperty(this._executionContext, 'prefix', { + get: function() { return requisition.prefix; }, + enumerable: true + }); + Object.defineProperty(this._executionContext, 'typed', { + get: function() { return requisition.toString(); }, + enumerable: true + }); + Object.defineProperty(this._executionContext, 'environment', { + get: function() { return requisition.environment; }, + enumerable: true + }); + Object.defineProperty(this._executionContext, 'shell', { + get: function() { return requisition.shell; }, + enumerable: true + }); + Object.defineProperty(this._executionContext, 'system', { + get: function() { return requisition.system; }, + enumerable: true + }); + + this._executionContext.updateExec = this._contextUpdateExec.bind(this); + + if (legacy) { + this._executionContext.createView = view.createView; + this._executionContext.exec = this.exec.bind(this); + this._executionContext.update = this._contextUpdate.bind(this); + + Object.defineProperty(this._executionContext, 'document', { + get: function() { return requisition.document; }, + enumerable: true + }); + } + } + + return this._executionContext; + }, + enumerable: true +}); + +/** + * Functions and data related to the conversion of the output of a command + */ +Object.defineProperty(Requisition.prototype, 'conversionContext', { + get: function() { + if (this._conversionContext == null) { + this._conversionContext = { + defer: defer, + + createView: view.createView, + exec: this.exec.bind(this), + update: this._contextUpdate.bind(this), + updateExec: this._contextUpdateExec.bind(this) + }; + + // Alias requisition so we're clear about what's what + var requisition = this; + + Object.defineProperty(this._conversionContext, 'document', { + get: function() { return requisition.document; }, + enumerable: true + }); + Object.defineProperty(this._conversionContext, 'environment', { + get: function() { return requisition.environment; }, + enumerable: true + }); + Object.defineProperty(this._conversionContext, 'system', { + get: function() { return requisition.system; }, + enumerable: true + }); + } + + return this._conversionContext; + }, + enumerable: true +}); + +/** + * Assignments have an order, so we need to store them in an array. + * But we also need named access ... + * @return The found assignment, or undefined, if no match was found + */ +Requisition.prototype.getAssignment = function(nameOrNumber) { + var name = (typeof nameOrNumber === 'string') ? + nameOrNumber : + Object.keys(this._assignments)[nameOrNumber]; + return this._assignments[name] || undefined; +}; + +/** + * Where parameter name == assignment names - they are the same + */ +Requisition.prototype.getParameterNames = function() { + return Object.keys(this._assignments); +}; + +/** + * The overall status is the most severe status. + * There is no such thing as an INCOMPLETE overall status because the + * definition of INCOMPLETE takes into account the cursor position to say 'this + * isn't quite ERROR because the user can fix it by typing', however overall, + * this is still an error status. + */ +Object.defineProperty(Requisition.prototype, 'status', { + get: function() { + var status = Status.VALID; + if (this._unassigned.length !== 0) { + var isAllIncomplete = true; + this._unassigned.forEach(function(assignment) { + if (!assignment.param.type.isIncompleteName) { + isAllIncomplete = false; + } + }); + status = isAllIncomplete ? Status.INCOMPLETE : Status.ERROR; + } + + this.getAssignments(true).forEach(function(assignment) { + var assignStatus = assignment.getStatus(); + if (assignStatus > status) { + status = assignStatus; + } + }, this); + if (status === Status.INCOMPLETE) { + status = Status.ERROR; + } + return status; + }, + enumerable : true +}); + +/** + * If ``requisition.status != VALID`` message then return a string which + * best describes what is wrong. Generally error messages are delivered by + * looking at the error associated with the argument at the cursor, but there + * are times when you just want to say 'tell me the worst'. + * If ``requisition.status != VALID`` then return ``null``. + */ +Requisition.prototype.getStatusMessage = function() { + if (this.commandAssignment.getStatus() !== Status.VALID) { + return l10n.lookupFormat('cliUnknownCommand2', + [ this.commandAssignment.arg.text ]); + } + + var assignments = this.getAssignments(); + for (var i = 0; i < assignments.length; i++) { + if (assignments[i].getStatus() !== Status.VALID) { + return assignments[i].message; + } + } + + if (this._unassigned.length !== 0) { + return l10n.lookup('cliUnusedArg'); + } + + return null; +}; + +/** + * Extract the names and values of all the assignments, and return as + * an object. + */ +Requisition.prototype.getArgsObject = function() { + var args = {}; + this.getAssignments().forEach(function(assignment) { + args[assignment.param.name] = assignment.conversion.isDataProvided() ? + assignment.value : + assignment.param.defaultValue; + }, this); + return args; +}; + +/** + * Access the arguments as an array. + * @param includeCommand By default only the parameter arguments are + * returned unless (includeCommand === true), in which case the list is + * prepended with commandAssignment.arg + */ +Requisition.prototype.getAssignments = function(includeCommand) { + var assignments = []; + if (includeCommand === true) { + assignments.push(this.commandAssignment); + } + Object.keys(this._assignments).forEach(function(name) { + assignments.push(this.getAssignment(name)); + }, this); + return assignments; +}; + +/** + * There are a few places where we need to know what the 'next thing' is. What + * is the user going to be filling out next (assuming they don't enter a named + * argument). The next argument is the first in line that is both blank, and + * that can be filled in positionally. + * @return The next assignment to be used, or null if all the positional + * parameters have values. + */ +Requisition.prototype._getFirstBlankPositionalAssignment = function() { + var reply = null; + Object.keys(this._assignments).some(function(name) { + var assignment = this.getAssignment(name); + if (assignment.arg.type === 'BlankArgument' && + assignment.param.isPositionalAllowed) { + reply = assignment; + return true; // i.e. break + } + return false; + }, this); + return reply; +}; + +/** + * The update process is asynchronous, so there is (unavoidably) a window + * where we've worked out the command but don't yet understand all the params. + * If we try to do things to a requisition in this window we may get + * inconsistent results. Asynchronous promises have made the window bigger. + * The only time we've seen this in practice is during focus events due to + * clicking on a shortcut. The focus want to check the cursor position while + * the shortcut is updating the command line. + * This function allows us to detect and back out of this problem. + * We should be able to remove this function when all the state in a + * requisition can be encapsulated and updated atomically. + */ +Requisition.prototype.isUpToDate = function() { + if (!this._args) { + return false; + } + for (var i = 0; i < this._args.length; i++) { + if (this._args[i].assignment == null) { + return false; + } + } + return true; +}; + +/** + * Look through the arguments attached to our assignments for the assignment + * at the given position. + * @param {number} cursor The cursor position to query + */ +Requisition.prototype.getAssignmentAt = function(cursor) { + // We short circuit this one because we may have no args, or no args with + // any size and the alg below only finds arguments with size. + if (cursor === 0) { + return this.commandAssignment; + } + + var assignForPos = []; + var i, j; + for (i = 0; i < this._args.length; i++) { + var arg = this._args[i]; + var assignment = arg.assignment; + + // prefix and text are clearly part of the argument + for (j = 0; j < arg.prefix.length; j++) { + assignForPos.push(assignment); + } + for (j = 0; j < arg.text.length; j++) { + assignForPos.push(assignment); + } + + // suffix is part of the argument only if this is a named parameter, + // otherwise it looks forwards + if (arg.assignment.arg.type === 'NamedArgument') { + // leave the argument as it is + } + else if (this._args.length > i + 1) { + // first to the next argument + assignment = this._args[i + 1].assignment; + } + else { + // then to the first blank positional parameter, leaving 'as is' if none + var nextAssignment = this._getFirstBlankPositionalAssignment(); + if (nextAssignment != null) { + assignment = nextAssignment; + } + } + + for (j = 0; j < arg.suffix.length; j++) { + assignForPos.push(assignment); + } + } + + // Possible shortcut, we don't really need to go through all the args + // to work out the solution to this + + return assignForPos[cursor - 1]; +}; + +/** + * Extract a canonical version of the input + * @return a promise of a string which is the canonical version of what was + * typed + */ +Requisition.prototype.toCanonicalString = function() { + var cmd = this.commandAssignment.value ? + this.commandAssignment.value.name : + this.commandAssignment.arg.text; + + // Canonically, if we've opened with a { then we should have a } to close + var lineSuffix = ''; + if (cmd === '{') { + var scriptSuffix = this.getAssignment(0).arg.suffix; + lineSuffix = (scriptSuffix.indexOf('}') === -1) ? ' }' : ''; + } + + var ctx = this.executionContext; + + // First stringify all the arguments + var argPromise = util.promiseEach(this.getAssignments(), function(assignment) { + // Bug 664377: This will cause problems if there is a non-default value + // after a default value. Also we need to decide when to use + // named parameters in place of positional params. Both can wait. + if (assignment.value === assignment.param.defaultValue) { + return ''; + } + + var val = assignment.param.type.stringify(assignment.value, ctx); + return Promise.resolve(val).then(function(str) { + return ' ' + str; + }.bind(this)); + }.bind(this)); + + return argPromise.then(function(strings) { + return cmd + strings.join('') + lineSuffix; + }.bind(this)); +}; + +/** + * Reconstitute the input from the args + */ +Requisition.prototype.toString = function() { + if (!this._args) { + throw new Error('toString requires a command line. See source.'); + } + + return this._args.map(function(arg) { + return arg.toString(); + }).join(''); +}; + +/** + * For test/debug use only. The output from this function is subject to wanton + * random change without notice, and should not be relied upon to even exist + * at some later date. + */ +Object.defineProperty(Requisition.prototype, '_summaryJson', { + get: function() { + var summary = { + $args: this._args.map(function(arg) { + return arg._summaryJson; + }), + _command: this.commandAssignment._summaryJson, + _unassigned: this._unassigned.forEach(function(assignment) { + return assignment._summaryJson; + }) + }; + + Object.keys(this._assignments).forEach(function(name) { + summary[name] = this.getAssignment(name)._summaryJson; + }.bind(this)); + + return summary; + }, + enumerable: true +}); + +/** + * When any assignment changes, we might need to update the _args array to + * match and inform people of changes to the typed input text. + */ +Requisition.prototype._setAssignmentInternal = function(assignment, conversion) { + var oldConversion = assignment.conversion; + + assignment.conversion = conversion; + assignment.conversion.assignment = assignment; + + // Do nothing if the conversion is unchanged + if (assignment.conversion.equals(oldConversion)) { + if (assignment === this.commandAssignment) { + this._setBlankArguments(); + } + return; + } + + // When the command changes, we need to keep a bunch of stuff in sync + if (assignment === this.commandAssignment) { + this._assignments = {}; + + var command = this.commandAssignment.value; + if (command) { + for (var i = 0; i < command.params.length; i++) { + var param = command.params[i]; + var newAssignment = new Assignment(param); + this._setBlankAssignment(newAssignment); + this._assignments[param.name] = newAssignment; + } + } + this.assignmentCount = Object.keys(this._assignments).length; + } +}; + +/** + * Internal function to alter the given assignment using the given arg. + * @param assignment The assignment to alter + * @param arg The new value for the assignment. An instance of Argument, or an + * instance of Conversion, or null to set the blank value. + * @param options There are a number of ways to customize how the assignment + * is made, including: + * - internal: (default:false) External updates are required to do more work, + * including adjusting the args in this requisition to stay in sync. + * On the other hand non internal changes use beginChange to back out of + * changes when overtaken asynchronously. + * Setting internal:true effectively means this is being called as part of + * the update process. + * - matchPadding: (default:false) Alter the whitespace on the prefix and + * suffix of the new argument to match that of the old argument. This only + * makes sense with internal=false + * @return A promise that resolves to undefined when the assignment is complete + */ +Requisition.prototype.setAssignment = function(assignment, arg, options) { + options = options || {}; + if (!options.internal) { + var originalArgs = assignment.arg.getArgs(); + + // Update the args array + var replacementArgs = arg.getArgs(); + var maxLen = Math.max(originalArgs.length, replacementArgs.length); + for (var i = 0; i < maxLen; i++) { + // If there are no more original args, or if the original arg was blank + // (i.e. not typed by the user), we'll just need to add at the end + if (i >= originalArgs.length || originalArgs[i].type === 'BlankArgument') { + this._args.push(replacementArgs[i]); + continue; + } + + var index = this._args.indexOf(originalArgs[i]); + if (index === -1) { + console.error('Couldn\'t find ', originalArgs[i], ' in ', this._args); + throw new Error('Couldn\'t find ' + originalArgs[i]); + } + + // If there are no more replacement args, we just remove the original args + // Otherwise swap original args and replacements + if (i >= replacementArgs.length) { + this._args.splice(index, 1); + } + else { + if (options.matchPadding) { + if (replacementArgs[i].prefix.length === 0 && + this._args[index].prefix.length !== 0) { + replacementArgs[i].prefix = this._args[index].prefix; + } + if (replacementArgs[i].suffix.length === 0 && + this._args[index].suffix.length !== 0) { + replacementArgs[i].suffix = this._args[index].suffix; + } + } + this._args[index] = replacementArgs[i]; + } + } + } + + var updateId = options.internal ? null : this._beginChange(); + + var setAssignmentInternal = function(conversion) { + if (options.internal || this._isChangeCurrent(updateId)) { + this._setAssignmentInternal(assignment, conversion); + } + + if (!options.internal) { + this._endChangeCheckOrder(updateId); + } + + return Promise.resolve(undefined); + }.bind(this); + + if (arg == null) { + var blank = assignment.param.type.getBlank(this.executionContext); + return setAssignmentInternal(blank); + } + + if (typeof arg.getStatus === 'function') { + // It's not really an arg, it's a conversion already + return setAssignmentInternal(arg); + } + + var parsed = assignment.param.type.parse(arg, this.executionContext); + return parsed.then(setAssignmentInternal); +}; + +/** + * Reset an assignment to its default value. + * For internal use only. + * Happens synchronously. + */ +Requisition.prototype._setBlankAssignment = function(assignment) { + var blank = assignment.param.type.getBlank(this.executionContext); + this._setAssignmentInternal(assignment, blank); +}; + +/** + * Reset all the assignments to their default values. + * For internal use only. + * Happens synchronously. + */ +Requisition.prototype._setBlankArguments = function() { + this.getAssignments().forEach(this._setBlankAssignment.bind(this)); +}; + +/** + * Input trace gives us an array of Argument tracing objects, one for each + * character in the typed input, from which we can derive information about how + * to display this typed input. It's a bit like toString on steroids. + * <p> + * The returned object has the following members:<ul> + * <li>character: The character to which this arg trace refers. + * <li>arg: The Argument to which this character is assigned. + * <li>part: One of ['prefix'|'text'|suffix'] - how was this char understood + * </ul> + * <p> + * The Argument objects are as output from tokenize() rather than as applied + * to Assignments by _assign() (i.e. they are not instances of NamedArgument, + * ArrayArgument, etc). + * <p> + * To get at the arguments applied to the assignments simply call + * <tt>arg.assignment.arg</tt>. If <tt>arg.assignment.arg !== arg</tt> then + * the arg applied to the assignment will contain the original arg. + * See _assign() for details. + */ +Requisition.prototype.createInputArgTrace = function() { + if (!this._args) { + throw new Error('createInputMap requires a command line. See source.'); + } + + var args = []; + var i; + this._args.forEach(function(arg) { + for (i = 0; i < arg.prefix.length; i++) { + args.push({ arg: arg, character: arg.prefix[i], part: 'prefix' }); + } + for (i = 0; i < arg.text.length; i++) { + args.push({ arg: arg, character: arg.text[i], part: 'text' }); + } + for (i = 0; i < arg.suffix.length; i++) { + args.push({ arg: arg, character: arg.suffix[i], part: 'suffix' }); + } + }); + + return args; +}; + +/** + * If the last character is whitespace then things that we suggest to add to + * the end don't need a space prefix. + * While this is quite a niche function, it has 2 benefits: + * - it's more correct because we can distinguish between final whitespace that + * is part of an unclosed string, and parameter separating whitespace. + * - also it's faster than toString() the whole thing and checking the end char + * @return true iff the last character is interpreted as parameter separating + * whitespace + */ +Requisition.prototype.typedEndsWithSeparator = function() { + if (!this._args) { + throw new Error('typedEndsWithSeparator requires a command line. See source.'); + } + + if (this._args.length === 0) { + return false; + } + + // This is not as easy as doing (this.toString().slice(-1) === ' ') + // See the doc comments above; We're checking for separators, not spaces + var lastArg = this._args.slice(-1)[0]; + if (lastArg.suffix.slice(-1) === ' ') { + return true; + } + + return lastArg.text === '' && lastArg.suffix === '' + && lastArg.prefix.slice(-1) === ' '; +}; + +/** + * Return an array of Status scores so we can create a marked up + * version of the command line input. + * @param cursor We only take a status of INCOMPLETE to be INCOMPLETE when the + * cursor is actually in the argument. Otherwise it's an error. + * @return Array of objects each containing <tt>status</tt> property and a + * <tt>string</tt> property containing the characters to which the status + * applies. Concatenating the strings in order gives the original input. + */ +Requisition.prototype.getInputStatusMarkup = function(cursor) { + var argTraces = this.createInputArgTrace(); + // Generally the 'argument at the cursor' is the argument before the cursor + // unless it is before the first char, in which case we take the first. + cursor = cursor === 0 ? 0 : cursor - 1; + var cTrace = argTraces[cursor]; + + var markup = []; + for (var i = 0; i < argTraces.length; i++) { + var argTrace = argTraces[i]; + var arg = argTrace.arg; + var status = Status.VALID; + // When things get very async we can get here while something else is + // doing an update, in which case arg.assignment == null, so we check first + if (argTrace.part === 'text' && arg.assignment != null) { + status = arg.assignment.getStatus(arg); + // Promote INCOMPLETE to ERROR ... + if (status === Status.INCOMPLETE) { + // If the cursor is in the prefix or suffix of an argument then we + // don't consider it in the argument for the purposes of preventing + // the escalation to ERROR. However if this is a NamedArgument, then we + // allow the suffix (as space between 2 parts of the argument) to be in. + // We use arg.assignment.arg not arg because we're looking at the arg + // that got put into the assignment not as returned by tokenize() + var isNamed = (cTrace.arg.assignment.arg.type === 'NamedArgument'); + var isInside = cTrace.part === 'text' || + (isNamed && cTrace.part === 'suffix'); + if (arg.assignment !== cTrace.arg.assignment || !isInside) { + // And if we're not in the command + if (!(arg.assignment instanceof CommandAssignment)) { + status = Status.ERROR; + } + } + } + } + + markup.push({ status: status, string: argTrace.character }); + } + + // De-dupe: merge entries where 2 adjacent have same status + i = 0; + while (i < markup.length - 1) { + if (markup[i].status === markup[i + 1].status) { + markup[i].string += markup[i + 1].string; + markup.splice(i + 1, 1); + } + else { + i++; + } + } + + return markup; +}; + +/** + * Describe the state of the current input in a way that allows display of + * predictions and completion hints + * @param start The location of the cursor + * @param rank The index of the chosen prediction + * @return A promise of an object containing the following properties: + * - statusMarkup: An array of Status scores so we can create a marked up + * version of the command line input. See getInputStatusMarkup() for details + * - unclosedJs: Is the entered command a JS command with no closing '}'? + * - directTabText: A promise of the text that we *add* to the command line + * when TAB is pressed, to be displayed directly after the cursor. See also + * arrowTabText. + * - emptyParameters: A promise of the text that describes the arguments that + * the user is yet to type. + * - arrowTabText: A promise of the text that *replaces* the current argument + * when TAB is pressed, generally displayed after a "|->" symbol. See also + * directTabText. + */ +Requisition.prototype.getStateData = function(start, rank) { + var typed = this.toString(); + var current = this.getAssignmentAt(start); + var context = this.executionContext; + var predictionPromise = (typed.trim().length !== 0) ? + current.getPredictionRanked(context, rank) : + Promise.resolve(null); + + return predictionPromise.then(function(prediction) { + // directTabText is for when the current input is a prefix of the completion + // arrowTabText is for when we need to use an -> to show what will be used + var directTabText = ''; + var arrowTabText = ''; + var emptyParameters = []; + + if (typed.trim().length !== 0) { + var cArg = current.arg; + + if (prediction) { + var tabText = prediction.name; + var existing = cArg.text; + + // Normally the cursor being just before whitespace means that you are + // 'in' the previous argument, which means that the prediction is based + // on that argument, however NamedArguments break this by having 2 parts + // so we need to prepend the tabText with a space for NamedArguments, + // but only when there isn't already a space at the end of the prefix + // (i.e. ' --name' not ' --name ') + if (current.isInName()) { + tabText = ' ' + tabText; + } + + if (existing !== tabText) { + // Decide to use directTabText or arrowTabText + // Strip any leading whitespace from the user inputted value because + // the tabText will never have leading whitespace. + var inputValue = existing.replace(/^\s*/, ''); + var isStrictCompletion = tabText.indexOf(inputValue) === 0; + if (isStrictCompletion && start === typed.length) { + // Display the suffix of the prediction as the completion + var numLeadingSpaces = existing.match(/^(\s*)/)[0].length; + + directTabText = tabText.slice(existing.length - numLeadingSpaces); + } + else { + // Display the '-> prediction' at the end of the completer element + // \u21E5 is the JS escape right arrow + arrowTabText = '\u21E5 ' + tabText; + } + } + } + else { + // There's no prediction, but if this is a named argument that needs a + // value (that is without any) then we need to show that one is needed + // For example 'git commit --message ', clearly needs some more text + if (cArg.type === 'NamedArgument' && cArg.valueArg == null) { + emptyParameters.push('<' + current.param.type.name + '>\u00a0'); + } + } + } + + // Add a space between the typed text (+ directTabText) and the hints, + // making sure we don't add 2 sets of padding + if (directTabText !== '') { + directTabText += '\u00a0'; // a.k.a + } + else if (!this.typedEndsWithSeparator()) { + emptyParameters.unshift('\u00a0'); + } + + // Calculate the list of parameters to be filled in + // We generate an array of emptyParameter markers for each positional + // parameter to the current command. + // Generally each emptyParameter marker begins with a space to separate it + // from whatever came before, unless what comes before ends in a space. + + this.getAssignments().forEach(function(assignment) { + // Named arguments are handled with a group [options] marker + if (!assignment.param.isPositionalAllowed) { + return; + } + + // No hints if we've got content for this parameter + if (assignment.arg.toString().trim() !== '') { + return; + } + + // No hints if we have a prediction + if (directTabText !== '' && current === assignment) { + return; + } + + var text = (assignment.param.isDataRequired) ? + '<' + assignment.param.name + '>\u00a0' : + '[' + assignment.param.name + ']\u00a0'; + + emptyParameters.push(text); + }.bind(this)); + + var command = this.commandAssignment.value; + var addOptionsMarker = false; + + // We add an '[options]' marker when there are named parameters that are + // not filled in and not hidden, and we don't have any directTabText + if (command && command.hasNamedParameters) { + command.params.forEach(function(param) { + var arg = this.getAssignment(param.name).arg; + if (!param.isPositionalAllowed && !param.hidden + && arg.type === 'BlankArgument') { + addOptionsMarker = true; + } + }, this); + } + + if (addOptionsMarker) { + // Add an nbsp if we don't have one at the end of the input or if + // this isn't the first param we've mentioned + emptyParameters.push('[options]\u00a0'); + } + + // Is the entered command a JS command with no closing '}'? + var unclosedJs = command && command.name === '{' && + this.getAssignment(0).arg.suffix.indexOf('}') === -1; + + return { + statusMarkup: this.getInputStatusMarkup(start), + unclosedJs: unclosedJs, + directTabText: directTabText, + arrowTabText: arrowTabText, + emptyParameters: emptyParameters + }; + }.bind(this)); +}; + +/** + * Pressing TAB sometimes requires that we add a space to denote that we're on + * to the 'next thing'. + * @param assignment The assignment to which to append the space + */ +Requisition.prototype._addSpace = function(assignment) { + var arg = assignment.arg.beget({ suffixSpace: true }); + if (arg !== assignment.arg) { + return this.setAssignment(assignment, arg); + } + else { + return Promise.resolve(undefined); + } +}; + +/** + * Complete the argument at <tt>cursor</tt>. + * Basically the same as: + * assignment = getAssignmentAt(cursor); + * assignment.value = assignment.conversion.predictions[0]; + * Except it's done safely, and with particular care to where we place the + * space, which is complex, and annoying if we get it wrong. + * + * WARNING: complete() can happen asynchronously. + * + * @param cursor The cursor configuration. Should have start and end properties + * which should be set to start and end of the selection. + * @param rank The index of the prediction that we should choose. + * This number is not bounded by the size of the prediction array, we take the + * modulus to get it within bounds + * @return A promise which completes (with undefined) when any outstanding + * completion tasks are done. + */ +Requisition.prototype.complete = function(cursor, rank) { + var assignment = this.getAssignmentAt(cursor.start); + + var context = this.executionContext; + var predictionPromise = assignment.getPredictionRanked(context, rank); + return predictionPromise.then(function(prediction) { + var outstanding = []; + + // Note: Since complete is asynchronous we should perhaps have a system to + // bail out of making changes if the command line has changed since TAB + // was pressed. It's not yet clear if this will be a problem. + + if (prediction == null) { + // No predictions generally means we shouldn't change anything on TAB, + // but TAB has the connotation of 'next thing' and when we're at the end + // of a thing that implies that we should add a space. i.e. + // 'help<TAB>' -> 'help ' + // But we should only do this if the thing that we're 'completing' is + // valid and doesn't already end in a space. + if (assignment.arg.suffix.slice(-1) !== ' ' && + assignment.getStatus() === Status.VALID) { + outstanding.push(this._addSpace(assignment)); + } + + // Also add a space if we are in the name part of an assignment, however + // this time we don't want the 'push the space to the next assignment' + // logic, so we don't use addSpace + if (assignment.isInName()) { + var newArg = assignment.arg.beget({ prefixPostSpace: true }); + outstanding.push(this.setAssignment(assignment, newArg)); + } + } + else { + // Mutate this argument to hold the completion + var arg = assignment.arg.beget({ + text: prediction.name, + dontQuote: (assignment === this.commandAssignment) + }); + var assignPromise = this.setAssignment(assignment, arg); + + if (!prediction.incomplete) { + assignPromise = assignPromise.then(function() { + // The prediction is complete, add a space to let the user move-on + return this._addSpace(assignment).then(function() { + // Bug 779443 - Remove or explain the re-parse + if (assignment instanceof UnassignedAssignment) { + return this.update(this.toString()); + } + }.bind(this)); + }.bind(this)); + } + + outstanding.push(assignPromise); + } + + return Promise.all(outstanding).then(function() { + return true; + }.bind(this)); + }.bind(this)); +}; + +/** + * Replace the current value with the lower value if such a concept exists. + */ +Requisition.prototype.nudge = function(assignment, by) { + var ctx = this.executionContext; + var val = assignment.param.type.nudge(assignment.value, by, ctx); + return Promise.resolve(val).then(function(replacement) { + if (replacement != null) { + var val = assignment.param.type.stringify(replacement, ctx); + return Promise.resolve(val).then(function(str) { + var arg = assignment.arg.beget({ text: str }); + return this.setAssignment(assignment, arg); + }.bind(this)); + } + }.bind(this)); +}; + +/** + * Helper to find the 'data-command' attribute, used by |update()| + */ +function getDataCommandAttribute(element) { + var command = element.getAttribute('data-command'); + if (!command) { + command = element.querySelector('*[data-command]') + .getAttribute('data-command'); + } + return command; +} + +/** + * Designed to be called from context.update(). Acts just like update() except + * that it also calls onExternalUpdate() to inform the UI of an unexpected + * change to the current command. + */ +Requisition.prototype._contextUpdate = function(typed) { + return this.update(typed).then(function(reply) { + this.onExternalUpdate({ typed: typed }); + return reply; + }.bind(this)); +}; + +/** + * Called by the UI when ever the user interacts with a command line input + * @param typed The contents of the input field OR an HTML element (or an event + * that targets an HTML element) which has a data-command attribute or a child + * with the same that contains the command to update with + */ +Requisition.prototype.update = function(typed) { + // Should be "if (typed instanceof HTMLElement)" except Gecko + if (typeof typed.querySelector === 'function') { + typed = getDataCommandAttribute(typed); + } + // Should be "if (typed instanceof Event)" except Gecko + if (typeof typed.currentTarget === 'object') { + typed = getDataCommandAttribute(typed.currentTarget); + } + + var updateId = this._beginChange(); + + this._args = exports.tokenize(typed); + var args = this._args.slice(0); // i.e. clone + + this._split(args); + + return this._assign(args).then(function() { + return this._endChangeCheckOrder(updateId); + }.bind(this)); +}; + +/** + * Similar to update('') except that it's guaranteed to execute synchronously + */ +Requisition.prototype.clear = function() { + var arg = new Argument('', '', ''); + this._args = [ arg ]; + + var conversion = commandModule.parse(this.executionContext, arg, false); + this.setAssignment(this.commandAssignment, conversion, { internal: true }); +}; + +/** + * tokenize() is a state machine. These are the states. + */ +var In = { + /** + * The last character was ' '. + * Typing a ' ' character will not change the mode + * Typing one of '"{ will change mode to SINGLE_Q, DOUBLE_Q or SCRIPT. + * Anything else goes into SIMPLE mode. + */ + WHITESPACE: 1, + + /** + * The last character was part of a parameter. + * Typing ' ' returns to WHITESPACE mode. Any other character + * (including '"{} which are otherwise special) does not change the mode. + */ + SIMPLE: 2, + + /** + * We're inside single quotes: ' + * Typing ' returns to WHITESPACE mode. Other characters do not change mode. + */ + SINGLE_Q: 3, + + /** + * We're inside double quotes: " + * Typing " returns to WHITESPACE mode. Other characters do not change mode. + */ + DOUBLE_Q: 4, + + /** + * We're inside { and } + * Typing } returns to WHITESPACE mode. Other characters do not change mode. + * SCRIPT mode is slightly different from other modes in that spaces between + * the {/} delimiters and the actual input are not considered significant. + * e.g: " x " is a 3 character string, delimited by double quotes, however + * { x } is a 1 character JavaScript surrounded by whitespace and {} + * delimiters. + * In the short term we assume that the JS routines can make sense of the + * extra whitespace, however at some stage we may need to move the space into + * the Argument prefix/suffix. + * Also we don't attempt to handle nested {}. See bug 678961 + */ + SCRIPT: 5 +}; + +/** + * Split up the input taking into account ', " and {. + * We don't consider \t or other classical whitespace characters to split + * arguments apart. For one thing these characters are hard to type, but also + * if the user has gone to the trouble of pasting a TAB character into the + * input field (or whatever it takes), they probably mean it. + */ +exports.tokenize = function(typed) { + // For blank input, place a dummy empty argument into the list + if (typed == null || typed.length === 0) { + return [ new Argument('', '', '') ]; + } + + if (isSimple(typed)) { + return [ new Argument(typed, '', '') ]; + } + + var mode = In.WHITESPACE; + + // First we swap out escaped characters that are special to the tokenizer. + // So a backslash followed by any of ['"{} ] is turned into a unicode private + // char so we can swap back later + typed = typed + .replace(/\\\\/g, '\uF000') + .replace(/\\ /g, '\uF001') + .replace(/\\'/g, '\uF002') + .replace(/\\"/g, '\uF003') + .replace(/\\{/g, '\uF004') + .replace(/\\}/g, '\uF005'); + + function unescape2(escaped) { + return escaped + .replace(/\uF000/g, '\\\\') + .replace(/\uF001/g, '\\ ') + .replace(/\uF002/g, '\\\'') + .replace(/\uF003/g, '\\\"') + .replace(/\uF004/g, '\\{') + .replace(/\uF005/g, '\\}'); + } + + var i = 0; // The index of the current character + var start = 0; // Where did this section start? + var prefix = ''; // Stuff that comes before the current argument + var args = []; // The array that we're creating + var blockDepth = 0; // For JS with nested {} + + // This is just a state machine. We're going through the string char by char + // The 'mode' is one of the 'In' states. As we go, we're adding Arguments + // to the 'args' array. + + while (true) { + var c = typed[i]; + var str; + switch (mode) { + case In.WHITESPACE: + if (c === '\'') { + prefix = typed.substring(start, i + 1); + mode = In.SINGLE_Q; + start = i + 1; + } + else if (c === '"') { + prefix = typed.substring(start, i + 1); + mode = In.DOUBLE_Q; + start = i + 1; + } + else if (c === '{') { + prefix = typed.substring(start, i + 1); + mode = In.SCRIPT; + blockDepth++; + start = i + 1; + } + else if (/ /.test(c)) { + // Still whitespace, do nothing + } + else { + prefix = typed.substring(start, i); + mode = In.SIMPLE; + start = i; + } + break; + + case In.SIMPLE: + // There is an edge case of xx'xx which we are assuming to + // be a single parameter (and same with ") + if (c === ' ') { + str = unescape2(typed.substring(start, i)); + args.push(new Argument(str, prefix, '')); + mode = In.WHITESPACE; + start = i; + prefix = ''; + } + break; + + case In.SINGLE_Q: + if (c === '\'') { + str = unescape2(typed.substring(start, i)); + args.push(new Argument(str, prefix, c)); + mode = In.WHITESPACE; + start = i + 1; + prefix = ''; + } + break; + + case In.DOUBLE_Q: + if (c === '"') { + str = unescape2(typed.substring(start, i)); + args.push(new Argument(str, prefix, c)); + mode = In.WHITESPACE; + start = i + 1; + prefix = ''; + } + break; + + case In.SCRIPT: + if (c === '{') { + blockDepth++; + } + else if (c === '}') { + blockDepth--; + if (blockDepth === 0) { + str = unescape2(typed.substring(start, i)); + args.push(new ScriptArgument(str, prefix, c)); + mode = In.WHITESPACE; + start = i + 1; + prefix = ''; + } + } + break; + } + + i++; + + if (i >= typed.length) { + // There is nothing else to read - tidy up + if (mode === In.WHITESPACE) { + if (i !== start) { + // There's whitespace at the end of the typed string. Add it to the + // last argument's suffix, creating an empty argument if needed. + var extra = typed.substring(start, i); + var lastArg = args[args.length - 1]; + if (!lastArg) { + args.push(new Argument('', extra, '')); + } + else { + lastArg.suffix += extra; + } + } + } + else if (mode === In.SCRIPT) { + str = unescape2(typed.substring(start, i + 1)); + args.push(new ScriptArgument(str, prefix, '')); + } + else { + str = unescape2(typed.substring(start, i + 1)); + args.push(new Argument(str, prefix, '')); + } + break; + } + } + + return args; +}; + +/** + * If the input has no spaces, quotes, braces or escapes, + * we can take the fast track. + */ +function isSimple(typed) { + for (var i = 0; i < typed.length; i++) { + var c = typed.charAt(i); + if (c === ' ' || c === '"' || c === '\'' || + c === '{' || c === '}' || c === '\\') { + return false; + } + } + return true; +} + +/** + * Looks in the commands for a command extension that matches what has been + * typed at the command line. + */ +Requisition.prototype._split = function(args) { + // Handle the special case of the user typing { javascript(); } + // We use the hidden 'eval' command directly rather than shift()ing one of + // the parameters, and parse()ing it. + var conversion; + if (args[0].type === 'ScriptArgument') { + // Special case: if the user enters { console.log('foo'); } then we need to + // use the hidden 'eval' command + var command = this.system.commands.get(evalCmd.name); + conversion = new Conversion(command, new ScriptArgument()); + this._setAssignmentInternal(this.commandAssignment, conversion); + return; + } + + var argsUsed = 1; + + while (argsUsed <= args.length) { + var arg = (argsUsed === 1) ? + args[0] : + new MergedArgument(args, 0, argsUsed); + + if (this.prefix != null && this.prefix !== '') { + var prefixArg = new Argument(this.prefix, '', ' '); + var prefixedArg = new MergedArgument([ prefixArg, arg ]); + + conversion = commandModule.parse(this.executionContext, prefixedArg, false); + if (conversion.value == null) { + conversion = commandModule.parse(this.executionContext, arg, false); + } + } + else { + conversion = commandModule.parse(this.executionContext, arg, false); + } + + // We only want to carry on if this command is a parent command, + // which means that there is a commandAssignment, but not one with + // an exec function. + if (!conversion.value || conversion.value.exec) { + break; + } + + // Previously we needed a way to hide commands depending context. + // We have not resurrected that feature yet, but if we do we should + // insert code here to ignore certain commands depending on the + // context/environment + + argsUsed++; + } + + // This could probably be re-written to consume args as we go + for (var i = 0; i < argsUsed; i++) { + args.shift(); + } + + this._setAssignmentInternal(this.commandAssignment, conversion); +}; + +/** + * Add all the passed args to the list of unassigned assignments. + */ +Requisition.prototype._addUnassignedArgs = function(args) { + args.forEach(function(arg) { + this._unassigned.push(new UnassignedAssignment(this, arg)); + }.bind(this)); + + return RESOLVED; +}; + +/** + * Work out which arguments are applicable to which parameters. + */ +Requisition.prototype._assign = function(args) { + // See comment in _split. Avoid multiple updates + var noArgUp = { internal: true }; + + this._unassigned = []; + + if (!this.commandAssignment.value) { + return this._addUnassignedArgs(args); + } + + if (args.length === 0) { + this._setBlankArguments(); + return RESOLVED; + } + + // Create an error if the command does not take parameters, but we have + // been given them ... + if (this.assignmentCount === 0) { + return this._addUnassignedArgs(args); + } + + // Special case: if there is only 1 parameter, and that's of type + // text, then we put all the params into the first param + if (this.assignmentCount === 1) { + var assignment = this.getAssignment(0); + if (assignment.param.type.name === 'string') { + var arg = (args.length === 1) ? args[0] : new MergedArgument(args); + return this.setAssignment(assignment, arg, noArgUp); + } + } + + // Positional arguments can still be specified by name, but if they are + // then we need to ignore them when working them out positionally + var unassignedParams = this.getParameterNames(); + + // We collect the arguments used in arrays here before assigning + var arrayArgs = {}; + + // Extract all the named parameters + var assignments = this.getAssignments(false); + var namedDone = util.promiseEach(assignments, function(assignment) { + // Loop over the arguments + // Using while rather than loop because we remove args as we go + var i = 0; + while (i < args.length) { + if (!assignment.param.isKnownAs(args[i].text)) { + // Skip this parameter and handle as a positional parameter + i++; + continue; + } + + var arg = args.splice(i, 1)[0]; + /* jshint loopfunc:true */ + unassignedParams = unassignedParams.filter(function(test) { + return test !== assignment.param.name; + }); + + // boolean parameters don't have values, default to false + if (assignment.param.type.name === 'boolean') { + arg = new TrueNamedArgument(arg); + } + else { + var valueArg = null; + if (i + 1 <= args.length) { + valueArg = args.splice(i, 1)[0]; + } + arg = new NamedArgument(arg, valueArg); + } + + if (assignment.param.type.name === 'array') { + var arrayArg = arrayArgs[assignment.param.name]; + if (!arrayArg) { + arrayArg = new ArrayArgument(); + arrayArgs[assignment.param.name] = arrayArg; + } + arrayArg.addArgument(arg); + return RESOLVED; + } + else { + if (assignment.arg.type === 'BlankArgument') { + return this.setAssignment(assignment, arg, noArgUp); + } + else { + return this._addUnassignedArgs(arg.getArgs()); + } + } + } + }, this); + + // What's left are positional parameters: assign in order + var positionalDone = namedDone.then(function() { + return util.promiseEach(unassignedParams, function(name) { + var assignment = this.getAssignment(name); + + // If not set positionally, and we can't set it non-positionally, + // we have to default it to prevent previous values surviving + if (!assignment.param.isPositionalAllowed) { + this._setBlankAssignment(assignment); + return RESOLVED; + } + + // If this is a positional array argument, then it swallows the + // rest of the arguments. + if (assignment.param.type.name === 'array') { + var arrayArg = arrayArgs[assignment.param.name]; + if (!arrayArg) { + arrayArg = new ArrayArgument(); + arrayArgs[assignment.param.name] = arrayArg; + } + arrayArg.addArguments(args); + args = []; + // The actual assignment to the array parameter is done below + return RESOLVED; + } + + // Set assignment to defaults if there are no more arguments + if (args.length === 0) { + this._setBlankAssignment(assignment); + return RESOLVED; + } + + var arg = args.splice(0, 1)[0]; + // --foo and -f are named parameters, -4 is a number. So '-' is either + // the start of a named parameter or a number depending on the context + var isIncompleteName = assignment.param.type.name === 'number' ? + /-[-a-zA-Z_]/.test(arg.text) : + arg.text.charAt(0) === '-'; + + if (isIncompleteName) { + this._unassigned.push(new UnassignedAssignment(this, arg)); + return RESOLVED; + } + else { + return this.setAssignment(assignment, arg, noArgUp); + } + }, this); + }.bind(this)); + + // Now we need to assign the array argument (if any) + var arrayDone = positionalDone.then(function() { + return util.promiseEach(Object.keys(arrayArgs), function(name) { + var assignment = this.getAssignment(name); + return this.setAssignment(assignment, arrayArgs[name], noArgUp); + }, this); + }.bind(this)); + + // What's left is can't be assigned, but we need to officially unassign them + return arrayDone.then(function() { + return this._addUnassignedArgs(args); + }.bind(this)); +}; + +/** + * Entry point for keyboard accelerators or anything else that wants to execute + * a command. + * @param options Object describing how the execution should be handled. + * (optional). Contains some of the following properties: + * - hidden (boolean, default=false) Should the output be hidden from the + * commandOutputManager for this requisition + * - command/args A fast shortcut to executing a known command with a known + * set of parsed arguments. + */ +Requisition.prototype.exec = function(options) { + var command = null; + var args = null; + var hidden = false; + + if (options) { + if (options.hidden) { + hidden = true; + } + + if (options.command != null) { + // Fast track by looking up the command directly since passed args + // means there is no command line to parse. + command = this.system.commands.get(options.command); + if (!command) { + console.error('Command not found: ' + options.command); + } + args = options.args; + } + } + + if (!command) { + command = this.commandAssignment.value; + args = this.getArgsObject(); + } + + // Display JavaScript input without the initial { or closing } + var typed = this.toString(); + if (evalCmd.isCommandRegexp.test(typed)) { + typed = typed.replace(evalCmd.isCommandRegexp, ''); + // Bug 717763: What if the JavaScript naturally ends with a }? + typed = typed.replace(/\s*}\s*$/, ''); + } + + var output = new Output({ + command: command, + args: args, + typed: typed, + canonical: this.toCanonicalString(), + hidden: hidden + }); + + this.commandOutputManager.onOutput({ output: output }); + + var onDone = function(data) { + output.complete(data, false); + return output; + }; + + var onError = function(data, ex) { + if (logErrors) { + if (ex != null) { + util.errorHandler(ex); + } + else { + console.error(data); + } + } + + if (data != null && typeof data === 'string') { + data = data.replace(/^Protocol error: /, ''); // Temp fix for bug 1035296 + } + + data = (data != null && data.isTypedData) ? data : { + isTypedData: true, + data: data, + type: 'error' + }; + output.complete(data, true); + return output; + }; + + if (this.status !== Status.VALID) { + var ex = new Error(this.getStatusMessage()); + // We only reject a call to exec if GCLI breaks. Errors with commands are + // exposed in the 'error' status of the Output object + return Promise.resolve(onError(ex)).then(function(output) { + this.clear(); + return output; + }.bind(this)); + } + else { + try { + return host.exec(function() { + return command.exec(args, this.executionContext); + }.bind(this)).then(onDone, onError); + } + catch (ex) { + var data = (typeof ex.message === 'string' && ex.stack != null) ? + ex.message : ex; + return Promise.resolve(onError(data, ex)); + } + finally { + this.clear(); + } + } +}; + +/** + * Designed to be called from context.updateExec(). Acts just like updateExec() + * except that it also calls onExternalUpdate() to inform the UI of an + * unexpected change to the current command. + */ +Requisition.prototype._contextUpdateExec = function(typed, options) { + var reqOpts = { + document: this.document, + environment: this.environment + }; + var child = new Requisition(this.system, reqOpts); + return child.updateExec(typed, options).then(function(reply) { + child.destroy(); + return reply; + }.bind(child)); +}; + +/** + * A shortcut for calling update, resolving the promise and then exec. + * @param input The string to execute + * @param options Passed to exec + * @return A promise of an output object + */ +Requisition.prototype.updateExec = function(input, options) { + return this.update(input).then(function() { + return this.exec(options); + }.bind(this)); +}; + +exports.Requisition = Requisition; + +/** + * A simple object to hold information about the output of a command + */ +function Output(options) { + options = options || {}; + this.command = options.command || ''; + this.args = options.args || {}; + this.typed = options.typed || ''; + this.canonical = options.canonical || ''; + this.hidden = options.hidden === true ? true : false; + + this.type = undefined; + this.data = undefined; + this.completed = false; + this.error = false; + this.start = new Date(); + + this.promise = new Promise(function(resolve, reject) { + this._resolve = resolve; + }.bind(this)); +} + +/** + * Called when there is data to display, and the command has finished executing + * See changed() for details on parameters. + */ +Output.prototype.complete = function(data, error) { + this.end = new Date(); + this.completed = true; + this.error = error; + + if (data != null && data.isTypedData) { + this.data = data.data; + this.type = data.type; + } + else { + this.data = data; + this.type = this.command.returnType; + if (this.type == null) { + this.type = (this.data == null) ? 'undefined' : typeof this.data; + } + } + + if (this.type === 'object') { + throw new Error('No type from output of ' + this.typed); + } + + this._resolve(); +}; + +/** + * Call converters.convert using the data in this Output object + */ +Output.prototype.convert = function(type, conversionContext) { + var converters = conversionContext.system.converters; + return converters.convert(this.data, this.type, type, conversionContext); +}; + +Output.prototype.toJson = function() { + // Exceptions don't stringify, so we try a bit harder + var data = this.data; + if (this.error && JSON.stringify(this.data) === '{}') { + data = { + columnNumber: data.columnNumber, + fileName: data.fileName, + lineNumber: data.lineNumber, + message: data.message, + stack: data.stack + }; + } + + return { + typed: this.typed, + type: this.type, + data: data, + isError: this.error + }; +}; + +exports.Output = Output; |