mirror of
https://github.com/roytam1/UXP.git
synced 2026-05-27 13:28:54 +00:00
2210 lines
70 KiB
JavaScript
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
|
|
}
|
|
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;
|