1
0
mirror of https://github.com/roytam1/UXP.git synced 2026-05-27 13:28:54 +00:00
Files
UXP/devtools/shared/gcli/source/lib/gcli/cli.js
T

2210 lines
70 KiB
JavaScript

/*
* 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;