summaryrefslogtreecommitdiffstats
path: root/devtools/shared/gcli/source/lib/gcli/cli.js
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/shared/gcli/source/lib/gcli/cli.js')
-rw-r--r--devtools/shared/gcli/source/lib/gcli/cli.js2209
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 &nbsp;
+ }
+ 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;