summaryrefslogtreecommitdiffstats
path: root/devtools/shared/gcli/source/lib/gcli
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/shared/gcli/source/lib/gcli')
-rw-r--r--devtools/shared/gcli/source/lib/gcli/cli.js2209
-rw-r--r--devtools/shared/gcli/source/lib/gcli/commands/clear.js59
-rw-r--r--devtools/shared/gcli/source/lib/gcli/commands/commands.js570
-rw-r--r--devtools/shared/gcli/source/lib/gcli/commands/context.js62
-rw-r--r--devtools/shared/gcli/source/lib/gcli/commands/help.js387
-rw-r--r--devtools/shared/gcli/source/lib/gcli/commands/mocks.js68
-rw-r--r--devtools/shared/gcli/source/lib/gcli/commands/moz.build16
-rw-r--r--devtools/shared/gcli/source/lib/gcli/commands/pref.js93
-rw-r--r--devtools/shared/gcli/source/lib/gcli/commands/preflist.js214
-rw-r--r--devtools/shared/gcli/source/lib/gcli/commands/test.js215
-rw-r--r--devtools/shared/gcli/source/lib/gcli/connectors/connectors.js157
-rw-r--r--devtools/shared/gcli/source/lib/gcli/connectors/moz.build9
-rw-r--r--devtools/shared/gcli/source/lib/gcli/converters/basic.js94
-rw-r--r--devtools/shared/gcli/source/lib/gcli/converters/converters.js280
-rw-r--r--devtools/shared/gcli/source/lib/gcli/converters/html.js47
-rw-r--r--devtools/shared/gcli/source/lib/gcli/converters/moz.build12
-rw-r--r--devtools/shared/gcli/source/lib/gcli/converters/terminal.js56
-rw-r--r--devtools/shared/gcli/source/lib/gcli/fields/delegate.js96
-rw-r--r--devtools/shared/gcli/source/lib/gcli/fields/fields.js245
-rw-r--r--devtools/shared/gcli/source/lib/gcli/fields/moz.build11
-rw-r--r--devtools/shared/gcli/source/lib/gcli/fields/selection.js124
-rw-r--r--devtools/shared/gcli/source/lib/gcli/index.js29
-rw-r--r--devtools/shared/gcli/source/lib/gcli/l10n.js74
-rw-r--r--devtools/shared/gcli/source/lib/gcli/languages/command.html14
-rw-r--r--devtools/shared/gcli/source/lib/gcli/languages/command.js563
-rw-r--r--devtools/shared/gcli/source/lib/gcli/languages/javascript.js86
-rw-r--r--devtools/shared/gcli/source/lib/gcli/languages/languages.js179
-rw-r--r--devtools/shared/gcli/source/lib/gcli/languages/moz.build12
-rw-r--r--devtools/shared/gcli/source/lib/gcli/moz.build13
-rw-r--r--devtools/shared/gcli/source/lib/gcli/mozui/completer.js151
-rw-r--r--devtools/shared/gcli/source/lib/gcli/mozui/inputter.js657
-rw-r--r--devtools/shared/gcli/source/lib/gcli/mozui/moz.build11
-rw-r--r--devtools/shared/gcli/source/lib/gcli/mozui/tooltip.js298
-rw-r--r--devtools/shared/gcli/source/lib/gcli/settings.js284
-rw-r--r--devtools/shared/gcli/source/lib/gcli/system.js370
-rw-r--r--devtools/shared/gcli/source/lib/gcli/types/array.js80
-rw-r--r--devtools/shared/gcli/source/lib/gcli/types/boolean.js62
-rw-r--r--devtools/shared/gcli/source/lib/gcli/types/command.js255
-rw-r--r--devtools/shared/gcli/source/lib/gcli/types/date.js248
-rw-r--r--devtools/shared/gcli/source/lib/gcli/types/delegate.js158
-rw-r--r--devtools/shared/gcli/source/lib/gcli/types/file.js96
-rw-r--r--devtools/shared/gcli/source/lib/gcli/types/fileparser.js19
-rw-r--r--devtools/shared/gcli/source/lib/gcli/types/javascript.js522
-rw-r--r--devtools/shared/gcli/source/lib/gcli/types/moz.build25
-rw-r--r--devtools/shared/gcli/source/lib/gcli/types/node.js201
-rw-r--r--devtools/shared/gcli/source/lib/gcli/types/number.js181
-rw-r--r--devtools/shared/gcli/source/lib/gcli/types/resource.js270
-rw-r--r--devtools/shared/gcli/source/lib/gcli/types/selection.js389
-rw-r--r--devtools/shared/gcli/source/lib/gcli/types/setting.js62
-rw-r--r--devtools/shared/gcli/source/lib/gcli/types/string.js92
-rw-r--r--devtools/shared/gcli/source/lib/gcli/types/types.js1146
-rw-r--r--devtools/shared/gcli/source/lib/gcli/types/union.js117
-rw-r--r--devtools/shared/gcli/source/lib/gcli/types/url.js86
-rw-r--r--devtools/shared/gcli/source/lib/gcli/ui/focus.js403
-rw-r--r--devtools/shared/gcli/source/lib/gcli/ui/history.js71
-rw-r--r--devtools/shared/gcli/source/lib/gcli/ui/intro.js90
-rw-r--r--devtools/shared/gcli/source/lib/gcli/ui/menu.css69
-rw-r--r--devtools/shared/gcli/source/lib/gcli/ui/menu.html20
-rw-r--r--devtools/shared/gcli/source/lib/gcli/ui/menu.js328
-rw-r--r--devtools/shared/gcli/source/lib/gcli/ui/moz.build15
-rw-r--r--devtools/shared/gcli/source/lib/gcli/ui/view.js87
-rw-r--r--devtools/shared/gcli/source/lib/gcli/util/domtemplate.js20
-rw-r--r--devtools/shared/gcli/source/lib/gcli/util/fileparser.js281
-rw-r--r--devtools/shared/gcli/source/lib/gcli/util/filesystem.js130
-rw-r--r--devtools/shared/gcli/source/lib/gcli/util/host.js230
-rw-r--r--devtools/shared/gcli/source/lib/gcli/util/l10n.js80
-rw-r--r--devtools/shared/gcli/source/lib/gcli/util/legacy.js147
-rw-r--r--devtools/shared/gcli/source/lib/gcli/util/moz.build17
-rw-r--r--devtools/shared/gcli/source/lib/gcli/util/prism.js361
-rw-r--r--devtools/shared/gcli/source/lib/gcli/util/spell.js197
-rw-r--r--devtools/shared/gcli/source/lib/gcli/util/util.js685
71 files changed, 15005 insertions, 0 deletions
diff --git a/devtools/shared/gcli/source/lib/gcli/cli.js b/devtools/shared/gcli/source/lib/gcli/cli.js
new file mode 100644
index 000000000..4b7c115e2
--- /dev/null
+++ b/devtools/shared/gcli/source/lib/gcli/cli.js
@@ -0,0 +1,2209 @@
+/*
+ * Copyright 2012, Mozilla Foundation and contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+'use strict';
+
+var util = require('./util/util');
+var host = require('./util/host');
+var l10n = require('./util/l10n');
+
+var view = require('./ui/view');
+var Parameter = require('./commands/commands').Parameter;
+var CommandOutputManager = require('./commands/commands').CommandOutputManager;
+
+var Status = require('./types/types').Status;
+var Conversion = require('./types/types').Conversion;
+var commandModule = require('./types/command');
+var selectionModule = require('./types/selection');
+
+var Argument = require('./types/types').Argument;
+var ArrayArgument = require('./types/types').ArrayArgument;
+var NamedArgument = require('./types/types').NamedArgument;
+var TrueNamedArgument = require('./types/types').TrueNamedArgument;
+var MergedArgument = require('./types/types').MergedArgument;
+var ScriptArgument = require('./types/types').ScriptArgument;
+
+var RESOLVED = Promise.resolve(undefined);
+
+// Helper to produce a `deferred` object
+// using DOM Promise
+function defer() {
+ let resolve, reject;
+ let p = new Promise((a, b) => {
+ resolve = a;
+ reject = b;
+ });
+ return {
+ promise: p,
+ resolve: resolve,
+ reject: reject
+ };
+}
+
+/**
+ * This is a list of the known command line components to enable certain
+ * privileged commands to alter parts of a running command line. It is an array
+ * of objects shaped like:
+ * { conversionContext:..., executionContext:..., mapping:... }
+ * So lookup is O(n) where 'n' is the number of command lines.
+ */
+var instances = [];
+
+/**
+ * An indexOf that looks-up both types of context
+ */
+function instanceIndex(context) {
+ for (var i = 0; i < instances.length; i++) {
+ var instance = instances[i];
+ if (instance.conversionContext === context ||
+ instance.executionContext === context) {
+ return i;
+ }
+ }
+ return -1;
+}
+
+/**
+ * findInstance gets access to a Terminal object given a conversionContext or
+ * an executionContext (it doesn't have to be a terminal object, just whatever
+ * was passed into addMapping()
+ */
+exports.getMapping = function(context) {
+ var index = instanceIndex(context);
+ if (index === -1) {
+ console.log('Missing mapping for context: ', context);
+ console.log('Known contexts: ', instances);
+ throw new Error('Missing mapping for context');
+ }
+ return instances[index].mapping;
+};
+
+/**
+ * Add a requisition context->terminal mapping
+ */
+var addMapping = function(requisition) {
+ if (instanceIndex(requisition.conversionContext) !== -1) {
+ throw new Error('Remote existing mapping before adding a new one');
+ }
+
+ instances.push({
+ conversionContext: requisition.conversionContext,
+ executionContext: requisition.executionContext,
+ mapping: { requisition: requisition }
+ });
+};
+
+/**
+ * Remove a requisition context->terminal mapping
+ */
+var removeMapping = function(requisition) {
+ var index = instanceIndex(requisition.conversionContext);
+ instances.splice(index, 1);
+};
+
+/**
+ * Assignment is a link between a parameter and the data for that parameter.
+ * The data for the parameter is available as in the preferred type and as
+ * an Argument for the CLI.
+ * <p>We also record validity information where applicable.
+ * <p>For values, null and undefined have distinct definitions. null means
+ * that a value has been provided, undefined means that it has not.
+ * Thus, null is a valid default value, and common because it identifies an
+ * parameter that is optional. undefined means there is no value from
+ * the command line.
+ * @constructor
+ */
+function Assignment(param) {
+ // The parameter that we are assigning to
+ this.param = param;
+ this.conversion = undefined;
+}
+
+/**
+ * Easy accessor for conversion.arg.
+ * This is a read-only property because writes to arg should be done through
+ * the 'conversion' property.
+ */
+Object.defineProperty(Assignment.prototype, 'arg', {
+ get: function() {
+ return this.conversion == null ? undefined : this.conversion.arg;
+ },
+ enumerable: true
+});
+
+/**
+ * Easy accessor for conversion.value.
+ * This is a read-only property because writes to value should be done through
+ * the 'conversion' property.
+ */
+Object.defineProperty(Assignment.prototype, 'value', {
+ get: function() {
+ return this.conversion == null ? undefined : this.conversion.value;
+ },
+ enumerable: true
+});
+
+/**
+ * Easy (and safe) accessor for conversion.message
+ */
+Object.defineProperty(Assignment.prototype, 'message', {
+ get: function() {
+ if (this.conversion != null && this.conversion.message) {
+ return this.conversion.message;
+ }
+ // ERROR conversions have messages, VALID conversions don't need one, so
+ // we just need to consider INCOMPLETE conversions.
+ if (this.getStatus() === Status.INCOMPLETE) {
+ return l10n.lookupFormat('cliIncompleteParam', [ this.param.name ]);
+ }
+ return '';
+ },
+ enumerable: true
+});
+
+/**
+ * Easy (and safe) accessor for conversion.getPredictions()
+ * @return An array of objects with name and value elements. For example:
+ * [ { name:'bestmatch', value:foo1 }, { name:'next', value:foo2 }, ... ]
+ */
+Assignment.prototype.getPredictions = function(context) {
+ return this.conversion == null ? [] : this.conversion.getPredictions(context);
+};
+
+/**
+ * Accessor for a prediction by index.
+ * This is useful above <tt>getPredictions()[index]</tt> because it normalizes
+ * index to be within the bounds of the predictions, which means that the UI
+ * can maintain an index of which prediction to choose without caring how many
+ * predictions there are.
+ * @param rank The index of the prediction to choose
+ */
+Assignment.prototype.getPredictionRanked = function(context, rank) {
+ if (rank == null) {
+ rank = 0;
+ }
+
+ if (this.isInName()) {
+ return Promise.resolve(undefined);
+ }
+
+ return this.getPredictions(context).then(function(predictions) {
+ if (predictions.length === 0) {
+ return undefined;
+ }
+
+ rank = rank % predictions.length;
+ if (rank < 0) {
+ rank = predictions.length + rank;
+ }
+ return predictions[rank];
+ }.bind(this));
+};
+
+/**
+ * Some places want to take special action if we are in the name part of a
+ * named argument (i.e. the '--foo' bit).
+ * Currently this does not take actual cursor position into account, it just
+ * assumes that the cursor is at the end. In the future we will probably want
+ * to take this into account.
+ */
+Assignment.prototype.isInName = function() {
+ return this.conversion.arg.type === 'NamedArgument' &&
+ this.conversion.arg.prefix.slice(-1) !== ' ';
+};
+
+/**
+ * Work out what the status of the current conversion is which involves looking
+ * not only at the conversion, but also checking if data has been provided
+ * where it should.
+ * @param arg For assignments with multiple args (e.g. array assignments) we
+ * can narrow the search for status to a single argument.
+ */
+Assignment.prototype.getStatus = function(arg) {
+ if (this.param.isDataRequired && !this.conversion.isDataProvided()) {
+ return Status.INCOMPLETE;
+ }
+
+ // Selection/Boolean types with a defined range of values will say that
+ // '' is INCOMPLETE, but the parameter may be optional, so we don't ask
+ // if the user doesn't need to enter something and hasn't done so.
+ if (!this.param.isDataRequired && this.arg.type === 'BlankArgument') {
+ return Status.VALID;
+ }
+
+ return this.conversion.getStatus(arg);
+};
+
+/**
+ * Helper when we're rebuilding command lines.
+ */
+Assignment.prototype.toString = function() {
+ return this.conversion.toString();
+};
+
+/**
+ * For test/debug use only. The output from this function is subject to wanton
+ * random change without notice, and should not be relied upon to even exist
+ * at some later date.
+ */
+Object.defineProperty(Assignment.prototype, '_summaryJson', {
+ get: function() {
+ return {
+ param: this.param.name + '/' + this.param.type.name,
+ defaultValue: this.param.defaultValue,
+ arg: this.conversion.arg._summaryJson,
+ value: this.value,
+ message: this.message,
+ status: this.getStatus().toString()
+ };
+ },
+ enumerable: true
+});
+
+exports.Assignment = Assignment;
+
+
+/**
+ * How to dynamically execute JavaScript code
+ */
+var customEval = eval;
+
+/**
+ * Setup a function to be called in place of 'eval', generally for security
+ * reasons
+ */
+exports.setEvalFunction = function(newCustomEval) {
+ customEval = newCustomEval;
+};
+
+/**
+ * Remove the binding done by setEvalFunction().
+ * We purposely set customEval to undefined rather than to 'eval' because there
+ * is an implication of setEvalFunction that we're in a security sensitive
+ * situation. What if we can trick GCLI into calling unsetEvalFunction() at the
+ * wrong time?
+ * So to properly undo the effects of setEvalFunction(), you need to call
+ * setEvalFunction(eval) rather than unsetEvalFunction(), however the latter is
+ * preferred in most cases.
+ */
+exports.unsetEvalFunction = function() {
+ customEval = undefined;
+};
+
+/**
+ * 'eval' command
+ */
+var evalCmd = {
+ item: 'command',
+ name: '{',
+ params: [
+ {
+ name: 'javascript',
+ type: 'javascript',
+ description: ''
+ }
+ ],
+ hidden: true,
+ description: { key: 'cliEvalJavascript' },
+ exec: function(args, context) {
+ var reply = customEval(args.javascript);
+ return context.typedData(typeof reply, reply);
+ },
+ isCommandRegexp: /^\s*\{\s*/
+};
+
+exports.items = [ evalCmd ];
+
+/**
+ * This is a special assignment to reflect the command itself.
+ */
+function CommandAssignment(requisition) {
+ var commandParamMetadata = {
+ name: '__command',
+ type: { name: 'command', allowNonExec: false }
+ };
+ // This is a hack so that rather than reply with a generic description of the
+ // command assignment, we reply with the description of the assigned command,
+ // (using a generic term if there is no assigned command)
+ var self = this;
+ Object.defineProperty(commandParamMetadata, 'description', {
+ get: function() {
+ var value = self.value;
+ return value && value.description ?
+ value.description :
+ 'The command to execute';
+ },
+ enumerable: true
+ });
+ this.param = new Parameter(requisition.system.types, commandParamMetadata);
+}
+
+CommandAssignment.prototype = Object.create(Assignment.prototype);
+
+CommandAssignment.prototype.getStatus = function(arg) {
+ return Status.combine(
+ Assignment.prototype.getStatus.call(this, arg),
+ this.conversion.value && this.conversion.value.exec ?
+ Status.VALID : Status.INCOMPLETE
+ );
+};
+
+exports.CommandAssignment = CommandAssignment;
+
+
+/**
+ * Special assignment used when ignoring parameters that don't have a home
+ */
+function UnassignedAssignment(requisition, arg) {
+ var isIncompleteName = (arg.text.charAt(0) === '-');
+ this.param = new Parameter(requisition.system.types, {
+ name: '__unassigned',
+ description: l10n.lookup('cliOptions'),
+ type: {
+ name: 'param',
+ requisition: requisition,
+ isIncompleteName: isIncompleteName
+ }
+ });
+
+ // It would be nice to do 'conversion = parm.type.parse(arg, ...)' except
+ // that type.parse returns a promise (even though it's synchronous in this
+ // case)
+ if (isIncompleteName) {
+ var lookup = commandModule.getDisplayedParamLookup(requisition);
+ var predictions = selectionModule.findPredictions(arg, lookup);
+ this.conversion = selectionModule.convertPredictions(arg, predictions);
+ }
+ else {
+ var message = l10n.lookup('cliUnusedArg');
+ this.conversion = new Conversion(undefined, arg, Status.ERROR, message);
+ }
+
+ this.conversion.assignment = this;
+}
+
+UnassignedAssignment.prototype = Object.create(Assignment.prototype);
+
+UnassignedAssignment.prototype.getStatus = function(arg) {
+ return this.conversion.getStatus();
+};
+
+var logErrors = true;
+
+/**
+ * Allow tests that expect failures to avoid clogging up the console
+ */
+Object.defineProperty(exports, 'logErrors', {
+ get: function() {
+ return logErrors;
+ },
+ set: function(val) {
+ logErrors = val;
+ },
+ enumerable: true
+});
+
+/**
+ * A Requisition collects the information needed to execute a command.
+ *
+ * (For a definition of the term, see http://en.wikipedia.org/wiki/Requisition)
+ * This term is used because carries the notion of a work-flow, or process to
+ * getting the information to execute a command correct.
+ * There is little point in a requisition for parameter-less commands because
+ * there is no information to collect. A Requisition is a collection of
+ * assignments of values to parameters, each handled by an instance of
+ * Assignment.
+ *
+ * @param system Allows access to the various plug-in points in GCLI. At a
+ * minimum it must contain commands and types objects.
+ * @param options A set of options to customize how GCLI is used. Includes:
+ * - environment An optional opaque object passed to commands in the
+ * Execution Context.
+ * - document A DOM Document passed to commands using the Execution Context in
+ * order to allow creation of DOM nodes. If missing Requisition will use the
+ * global 'document', or leave undefined.
+ * - commandOutputManager A custom commandOutputManager to which output should
+ * be sent
+ * @constructor
+ */
+function Requisition(system, options) {
+ options = options || {};
+
+ this.environment = options.environment || {};
+ this.document = options.document;
+ if (this.document == null) {
+ try {
+ this.document = document;
+ }
+ catch (ex) {
+ // Ignore
+ }
+ }
+
+ this.commandOutputManager = options.commandOutputManager || new CommandOutputManager();
+ this.system = system;
+
+ this.shell = {
+ cwd: '/', // Where we store the current working directory
+ env: {} // Where we store the current environment
+ };
+
+ // The command that we are about to execute.
+ // @see setCommandConversion()
+ this.commandAssignment = new CommandAssignment(this);
+
+ // The object that stores of Assignment objects that we are filling out.
+ // The Assignment objects are stored under their param.name for named
+ // lookup. Note: We make use of the property of Javascript objects that
+ // they are not just hashmaps, but linked-list hashmaps which iterate in
+ // insertion order.
+ // _assignments excludes the commandAssignment.
+ this._assignments = {};
+
+ // The count of assignments. Excludes the commandAssignment
+ this.assignmentCount = 0;
+
+ // Used to store cli arguments in the order entered on the cli
+ this._args = [];
+
+ // Used to store cli arguments that were not assigned to parameters
+ this._unassigned = [];
+
+ // Changes can be asynchronous, when one update starts before another
+ // finishes we abandon the former change
+ this._nextUpdateId = 0;
+
+ // We can set a prefix to typed commands to make it easier to focus on
+ // Allowing us to type "add -a; commit" in place of "git add -a; git commit"
+ this.prefix = '';
+
+ addMapping(this);
+ this._setBlankAssignment(this.commandAssignment);
+
+ // If a command calls context.update then the UI needs some way to be
+ // informed of the change
+ this.onExternalUpdate = util.createEvent('Requisition.onExternalUpdate');
+}
+
+/**
+ * Avoid memory leaks
+ */
+Requisition.prototype.destroy = function() {
+ this.document = undefined;
+ this.environment = undefined;
+ removeMapping(this);
+};
+
+/**
+ * If we're about to make an asynchronous change when other async changes could
+ * overtake this one, then we want to be able to bail out if overtaken. The
+ * value passed back from beginChange should be passed to endChangeCheckOrder
+ * on completion of calculation, before the results are applied in order to
+ * check that the calculation has not been overtaken
+ */
+Requisition.prototype._beginChange = function() {
+ var updateId = this._nextUpdateId;
+ this._nextUpdateId++;
+ return updateId;
+};
+
+/**
+ * Check to see if another change has started since updateId started.
+ * This allows us to bail out of an update.
+ * It's hard to make updates atomic because until you've responded to a parse
+ * of the command argument, you don't know how to parse the arguments to that
+ * command.
+ */
+Requisition.prototype._isChangeCurrent = function(updateId) {
+ return updateId + 1 === this._nextUpdateId;
+};
+
+/**
+ * See notes on beginChange
+ */
+Requisition.prototype._endChangeCheckOrder = function(updateId) {
+ if (updateId + 1 !== this._nextUpdateId) {
+ // An update that started after we did has already finished, so our
+ // changes are out of date. Abandon further work.
+ return false;
+ }
+
+ return true;
+};
+
+var legacy = false;
+
+/**
+ * Functions and data related to the execution of a command
+ */
+Object.defineProperty(Requisition.prototype, 'executionContext', {
+ get: function() {
+ if (this._executionContext == null) {
+ this._executionContext = {
+ defer: defer,
+ typedData: function(type, data) {
+ return {
+ isTypedData: true,
+ data: data,
+ type: type
+ };
+ },
+ getArgsObject: this.getArgsObject.bind(this)
+ };
+
+ // Alias requisition so we're clear about what's what
+ var requisition = this;
+ Object.defineProperty(this._executionContext, 'prefix', {
+ get: function() { return requisition.prefix; },
+ enumerable: true
+ });
+ Object.defineProperty(this._executionContext, 'typed', {
+ get: function() { return requisition.toString(); },
+ enumerable: true
+ });
+ Object.defineProperty(this._executionContext, 'environment', {
+ get: function() { return requisition.environment; },
+ enumerable: true
+ });
+ Object.defineProperty(this._executionContext, 'shell', {
+ get: function() { return requisition.shell; },
+ enumerable: true
+ });
+ Object.defineProperty(this._executionContext, 'system', {
+ get: function() { return requisition.system; },
+ enumerable: true
+ });
+
+ this._executionContext.updateExec = this._contextUpdateExec.bind(this);
+
+ if (legacy) {
+ this._executionContext.createView = view.createView;
+ this._executionContext.exec = this.exec.bind(this);
+ this._executionContext.update = this._contextUpdate.bind(this);
+
+ Object.defineProperty(this._executionContext, 'document', {
+ get: function() { return requisition.document; },
+ enumerable: true
+ });
+ }
+ }
+
+ return this._executionContext;
+ },
+ enumerable: true
+});
+
+/**
+ * Functions and data related to the conversion of the output of a command
+ */
+Object.defineProperty(Requisition.prototype, 'conversionContext', {
+ get: function() {
+ if (this._conversionContext == null) {
+ this._conversionContext = {
+ defer: defer,
+
+ createView: view.createView,
+ exec: this.exec.bind(this),
+ update: this._contextUpdate.bind(this),
+ updateExec: this._contextUpdateExec.bind(this)
+ };
+
+ // Alias requisition so we're clear about what's what
+ var requisition = this;
+
+ Object.defineProperty(this._conversionContext, 'document', {
+ get: function() { return requisition.document; },
+ enumerable: true
+ });
+ Object.defineProperty(this._conversionContext, 'environment', {
+ get: function() { return requisition.environment; },
+ enumerable: true
+ });
+ Object.defineProperty(this._conversionContext, 'system', {
+ get: function() { return requisition.system; },
+ enumerable: true
+ });
+ }
+
+ return this._conversionContext;
+ },
+ enumerable: true
+});
+
+/**
+ * Assignments have an order, so we need to store them in an array.
+ * But we also need named access ...
+ * @return The found assignment, or undefined, if no match was found
+ */
+Requisition.prototype.getAssignment = function(nameOrNumber) {
+ var name = (typeof nameOrNumber === 'string') ?
+ nameOrNumber :
+ Object.keys(this._assignments)[nameOrNumber];
+ return this._assignments[name] || undefined;
+};
+
+/**
+ * Where parameter name == assignment names - they are the same
+ */
+Requisition.prototype.getParameterNames = function() {
+ return Object.keys(this._assignments);
+};
+
+/**
+ * The overall status is the most severe status.
+ * There is no such thing as an INCOMPLETE overall status because the
+ * definition of INCOMPLETE takes into account the cursor position to say 'this
+ * isn't quite ERROR because the user can fix it by typing', however overall,
+ * this is still an error status.
+ */
+Object.defineProperty(Requisition.prototype, 'status', {
+ get: function() {
+ var status = Status.VALID;
+ if (this._unassigned.length !== 0) {
+ var isAllIncomplete = true;
+ this._unassigned.forEach(function(assignment) {
+ if (!assignment.param.type.isIncompleteName) {
+ isAllIncomplete = false;
+ }
+ });
+ status = isAllIncomplete ? Status.INCOMPLETE : Status.ERROR;
+ }
+
+ this.getAssignments(true).forEach(function(assignment) {
+ var assignStatus = assignment.getStatus();
+ if (assignStatus > status) {
+ status = assignStatus;
+ }
+ }, this);
+ if (status === Status.INCOMPLETE) {
+ status = Status.ERROR;
+ }
+ return status;
+ },
+ enumerable : true
+});
+
+/**
+ * If ``requisition.status != VALID`` message then return a string which
+ * best describes what is wrong. Generally error messages are delivered by
+ * looking at the error associated with the argument at the cursor, but there
+ * are times when you just want to say 'tell me the worst'.
+ * If ``requisition.status != VALID`` then return ``null``.
+ */
+Requisition.prototype.getStatusMessage = function() {
+ if (this.commandAssignment.getStatus() !== Status.VALID) {
+ return l10n.lookupFormat('cliUnknownCommand2',
+ [ this.commandAssignment.arg.text ]);
+ }
+
+ var assignments = this.getAssignments();
+ for (var i = 0; i < assignments.length; i++) {
+ if (assignments[i].getStatus() !== Status.VALID) {
+ return assignments[i].message;
+ }
+ }
+
+ if (this._unassigned.length !== 0) {
+ return l10n.lookup('cliUnusedArg');
+ }
+
+ return null;
+};
+
+/**
+ * Extract the names and values of all the assignments, and return as
+ * an object.
+ */
+Requisition.prototype.getArgsObject = function() {
+ var args = {};
+ this.getAssignments().forEach(function(assignment) {
+ args[assignment.param.name] = assignment.conversion.isDataProvided() ?
+ assignment.value :
+ assignment.param.defaultValue;
+ }, this);
+ return args;
+};
+
+/**
+ * Access the arguments as an array.
+ * @param includeCommand By default only the parameter arguments are
+ * returned unless (includeCommand === true), in which case the list is
+ * prepended with commandAssignment.arg
+ */
+Requisition.prototype.getAssignments = function(includeCommand) {
+ var assignments = [];
+ if (includeCommand === true) {
+ assignments.push(this.commandAssignment);
+ }
+ Object.keys(this._assignments).forEach(function(name) {
+ assignments.push(this.getAssignment(name));
+ }, this);
+ return assignments;
+};
+
+/**
+ * There are a few places where we need to know what the 'next thing' is. What
+ * is the user going to be filling out next (assuming they don't enter a named
+ * argument). The next argument is the first in line that is both blank, and
+ * that can be filled in positionally.
+ * @return The next assignment to be used, or null if all the positional
+ * parameters have values.
+ */
+Requisition.prototype._getFirstBlankPositionalAssignment = function() {
+ var reply = null;
+ Object.keys(this._assignments).some(function(name) {
+ var assignment = this.getAssignment(name);
+ if (assignment.arg.type === 'BlankArgument' &&
+ assignment.param.isPositionalAllowed) {
+ reply = assignment;
+ return true; // i.e. break
+ }
+ return false;
+ }, this);
+ return reply;
+};
+
+/**
+ * The update process is asynchronous, so there is (unavoidably) a window
+ * where we've worked out the command but don't yet understand all the params.
+ * If we try to do things to a requisition in this window we may get
+ * inconsistent results. Asynchronous promises have made the window bigger.
+ * The only time we've seen this in practice is during focus events due to
+ * clicking on a shortcut. The focus want to check the cursor position while
+ * the shortcut is updating the command line.
+ * This function allows us to detect and back out of this problem.
+ * We should be able to remove this function when all the state in a
+ * requisition can be encapsulated and updated atomically.
+ */
+Requisition.prototype.isUpToDate = function() {
+ if (!this._args) {
+ return false;
+ }
+ for (var i = 0; i < this._args.length; i++) {
+ if (this._args[i].assignment == null) {
+ return false;
+ }
+ }
+ return true;
+};
+
+/**
+ * Look through the arguments attached to our assignments for the assignment
+ * at the given position.
+ * @param {number} cursor The cursor position to query
+ */
+Requisition.prototype.getAssignmentAt = function(cursor) {
+ // We short circuit this one because we may have no args, or no args with
+ // any size and the alg below only finds arguments with size.
+ if (cursor === 0) {
+ return this.commandAssignment;
+ }
+
+ var assignForPos = [];
+ var i, j;
+ for (i = 0; i < this._args.length; i++) {
+ var arg = this._args[i];
+ var assignment = arg.assignment;
+
+ // prefix and text are clearly part of the argument
+ for (j = 0; j < arg.prefix.length; j++) {
+ assignForPos.push(assignment);
+ }
+ for (j = 0; j < arg.text.length; j++) {
+ assignForPos.push(assignment);
+ }
+
+ // suffix is part of the argument only if this is a named parameter,
+ // otherwise it looks forwards
+ if (arg.assignment.arg.type === 'NamedArgument') {
+ // leave the argument as it is
+ }
+ else if (this._args.length > i + 1) {
+ // first to the next argument
+ assignment = this._args[i + 1].assignment;
+ }
+ else {
+ // then to the first blank positional parameter, leaving 'as is' if none
+ var nextAssignment = this._getFirstBlankPositionalAssignment();
+ if (nextAssignment != null) {
+ assignment = nextAssignment;
+ }
+ }
+
+ for (j = 0; j < arg.suffix.length; j++) {
+ assignForPos.push(assignment);
+ }
+ }
+
+ // Possible shortcut, we don't really need to go through all the args
+ // to work out the solution to this
+
+ return assignForPos[cursor - 1];
+};
+
+/**
+ * Extract a canonical version of the input
+ * @return a promise of a string which is the canonical version of what was
+ * typed
+ */
+Requisition.prototype.toCanonicalString = function() {
+ var cmd = this.commandAssignment.value ?
+ this.commandAssignment.value.name :
+ this.commandAssignment.arg.text;
+
+ // Canonically, if we've opened with a { then we should have a } to close
+ var lineSuffix = '';
+ if (cmd === '{') {
+ var scriptSuffix = this.getAssignment(0).arg.suffix;
+ lineSuffix = (scriptSuffix.indexOf('}') === -1) ? ' }' : '';
+ }
+
+ var ctx = this.executionContext;
+
+ // First stringify all the arguments
+ var argPromise = util.promiseEach(this.getAssignments(), function(assignment) {
+ // Bug 664377: This will cause problems if there is a non-default value
+ // after a default value. Also we need to decide when to use
+ // named parameters in place of positional params. Both can wait.
+ if (assignment.value === assignment.param.defaultValue) {
+ return '';
+ }
+
+ var val = assignment.param.type.stringify(assignment.value, ctx);
+ return Promise.resolve(val).then(function(str) {
+ return ' ' + str;
+ }.bind(this));
+ }.bind(this));
+
+ return argPromise.then(function(strings) {
+ return cmd + strings.join('') + lineSuffix;
+ }.bind(this));
+};
+
+/**
+ * Reconstitute the input from the args
+ */
+Requisition.prototype.toString = function() {
+ if (!this._args) {
+ throw new Error('toString requires a command line. See source.');
+ }
+
+ return this._args.map(function(arg) {
+ return arg.toString();
+ }).join('');
+};
+
+/**
+ * For test/debug use only. The output from this function is subject to wanton
+ * random change without notice, and should not be relied upon to even exist
+ * at some later date.
+ */
+Object.defineProperty(Requisition.prototype, '_summaryJson', {
+ get: function() {
+ var summary = {
+ $args: this._args.map(function(arg) {
+ return arg._summaryJson;
+ }),
+ _command: this.commandAssignment._summaryJson,
+ _unassigned: this._unassigned.forEach(function(assignment) {
+ return assignment._summaryJson;
+ })
+ };
+
+ Object.keys(this._assignments).forEach(function(name) {
+ summary[name] = this.getAssignment(name)._summaryJson;
+ }.bind(this));
+
+ return summary;
+ },
+ enumerable: true
+});
+
+/**
+ * When any assignment changes, we might need to update the _args array to
+ * match and inform people of changes to the typed input text.
+ */
+Requisition.prototype._setAssignmentInternal = function(assignment, conversion) {
+ var oldConversion = assignment.conversion;
+
+ assignment.conversion = conversion;
+ assignment.conversion.assignment = assignment;
+
+ // Do nothing if the conversion is unchanged
+ if (assignment.conversion.equals(oldConversion)) {
+ if (assignment === this.commandAssignment) {
+ this._setBlankArguments();
+ }
+ return;
+ }
+
+ // When the command changes, we need to keep a bunch of stuff in sync
+ if (assignment === this.commandAssignment) {
+ this._assignments = {};
+
+ var command = this.commandAssignment.value;
+ if (command) {
+ for (var i = 0; i < command.params.length; i++) {
+ var param = command.params[i];
+ var newAssignment = new Assignment(param);
+ this._setBlankAssignment(newAssignment);
+ this._assignments[param.name] = newAssignment;
+ }
+ }
+ this.assignmentCount = Object.keys(this._assignments).length;
+ }
+};
+
+/**
+ * Internal function to alter the given assignment using the given arg.
+ * @param assignment The assignment to alter
+ * @param arg The new value for the assignment. An instance of Argument, or an
+ * instance of Conversion, or null to set the blank value.
+ * @param options There are a number of ways to customize how the assignment
+ * is made, including:
+ * - internal: (default:false) External updates are required to do more work,
+ * including adjusting the args in this requisition to stay in sync.
+ * On the other hand non internal changes use beginChange to back out of
+ * changes when overtaken asynchronously.
+ * Setting internal:true effectively means this is being called as part of
+ * the update process.
+ * - matchPadding: (default:false) Alter the whitespace on the prefix and
+ * suffix of the new argument to match that of the old argument. This only
+ * makes sense with internal=false
+ * @return A promise that resolves to undefined when the assignment is complete
+ */
+Requisition.prototype.setAssignment = function(assignment, arg, options) {
+ options = options || {};
+ if (!options.internal) {
+ var originalArgs = assignment.arg.getArgs();
+
+ // Update the args array
+ var replacementArgs = arg.getArgs();
+ var maxLen = Math.max(originalArgs.length, replacementArgs.length);
+ for (var i = 0; i < maxLen; i++) {
+ // If there are no more original args, or if the original arg was blank
+ // (i.e. not typed by the user), we'll just need to add at the end
+ if (i >= originalArgs.length || originalArgs[i].type === 'BlankArgument') {
+ this._args.push(replacementArgs[i]);
+ continue;
+ }
+
+ var index = this._args.indexOf(originalArgs[i]);
+ if (index === -1) {
+ console.error('Couldn\'t find ', originalArgs[i], ' in ', this._args);
+ throw new Error('Couldn\'t find ' + originalArgs[i]);
+ }
+
+ // If there are no more replacement args, we just remove the original args
+ // Otherwise swap original args and replacements
+ if (i >= replacementArgs.length) {
+ this._args.splice(index, 1);
+ }
+ else {
+ if (options.matchPadding) {
+ if (replacementArgs[i].prefix.length === 0 &&
+ this._args[index].prefix.length !== 0) {
+ replacementArgs[i].prefix = this._args[index].prefix;
+ }
+ if (replacementArgs[i].suffix.length === 0 &&
+ this._args[index].suffix.length !== 0) {
+ replacementArgs[i].suffix = this._args[index].suffix;
+ }
+ }
+ this._args[index] = replacementArgs[i];
+ }
+ }
+ }
+
+ var updateId = options.internal ? null : this._beginChange();
+
+ var setAssignmentInternal = function(conversion) {
+ if (options.internal || this._isChangeCurrent(updateId)) {
+ this._setAssignmentInternal(assignment, conversion);
+ }
+
+ if (!options.internal) {
+ this._endChangeCheckOrder(updateId);
+ }
+
+ return Promise.resolve(undefined);
+ }.bind(this);
+
+ if (arg == null) {
+ var blank = assignment.param.type.getBlank(this.executionContext);
+ return setAssignmentInternal(blank);
+ }
+
+ if (typeof arg.getStatus === 'function') {
+ // It's not really an arg, it's a conversion already
+ return setAssignmentInternal(arg);
+ }
+
+ var parsed = assignment.param.type.parse(arg, this.executionContext);
+ return parsed.then(setAssignmentInternal);
+};
+
+/**
+ * Reset an assignment to its default value.
+ * For internal use only.
+ * Happens synchronously.
+ */
+Requisition.prototype._setBlankAssignment = function(assignment) {
+ var blank = assignment.param.type.getBlank(this.executionContext);
+ this._setAssignmentInternal(assignment, blank);
+};
+
+/**
+ * Reset all the assignments to their default values.
+ * For internal use only.
+ * Happens synchronously.
+ */
+Requisition.prototype._setBlankArguments = function() {
+ this.getAssignments().forEach(this._setBlankAssignment.bind(this));
+};
+
+/**
+ * Input trace gives us an array of Argument tracing objects, one for each
+ * character in the typed input, from which we can derive information about how
+ * to display this typed input. It's a bit like toString on steroids.
+ * <p>
+ * The returned object has the following members:<ul>
+ * <li>character: The character to which this arg trace refers.
+ * <li>arg: The Argument to which this character is assigned.
+ * <li>part: One of ['prefix'|'text'|suffix'] - how was this char understood
+ * </ul>
+ * <p>
+ * The Argument objects are as output from tokenize() rather than as applied
+ * to Assignments by _assign() (i.e. they are not instances of NamedArgument,
+ * ArrayArgument, etc).
+ * <p>
+ * To get at the arguments applied to the assignments simply call
+ * <tt>arg.assignment.arg</tt>. If <tt>arg.assignment.arg !== arg</tt> then
+ * the arg applied to the assignment will contain the original arg.
+ * See _assign() for details.
+ */
+Requisition.prototype.createInputArgTrace = function() {
+ if (!this._args) {
+ throw new Error('createInputMap requires a command line. See source.');
+ }
+
+ var args = [];
+ var i;
+ this._args.forEach(function(arg) {
+ for (i = 0; i < arg.prefix.length; i++) {
+ args.push({ arg: arg, character: arg.prefix[i], part: 'prefix' });
+ }
+ for (i = 0; i < arg.text.length; i++) {
+ args.push({ arg: arg, character: arg.text[i], part: 'text' });
+ }
+ for (i = 0; i < arg.suffix.length; i++) {
+ args.push({ arg: arg, character: arg.suffix[i], part: 'suffix' });
+ }
+ });
+
+ return args;
+};
+
+/**
+ * If the last character is whitespace then things that we suggest to add to
+ * the end don't need a space prefix.
+ * While this is quite a niche function, it has 2 benefits:
+ * - it's more correct because we can distinguish between final whitespace that
+ * is part of an unclosed string, and parameter separating whitespace.
+ * - also it's faster than toString() the whole thing and checking the end char
+ * @return true iff the last character is interpreted as parameter separating
+ * whitespace
+ */
+Requisition.prototype.typedEndsWithSeparator = function() {
+ if (!this._args) {
+ throw new Error('typedEndsWithSeparator requires a command line. See source.');
+ }
+
+ if (this._args.length === 0) {
+ return false;
+ }
+
+ // This is not as easy as doing (this.toString().slice(-1) === ' ')
+ // See the doc comments above; We're checking for separators, not spaces
+ var lastArg = this._args.slice(-1)[0];
+ if (lastArg.suffix.slice(-1) === ' ') {
+ return true;
+ }
+
+ return lastArg.text === '' && lastArg.suffix === ''
+ && lastArg.prefix.slice(-1) === ' ';
+};
+
+/**
+ * Return an array of Status scores so we can create a marked up
+ * version of the command line input.
+ * @param cursor We only take a status of INCOMPLETE to be INCOMPLETE when the
+ * cursor is actually in the argument. Otherwise it's an error.
+ * @return Array of objects each containing <tt>status</tt> property and a
+ * <tt>string</tt> property containing the characters to which the status
+ * applies. Concatenating the strings in order gives the original input.
+ */
+Requisition.prototype.getInputStatusMarkup = function(cursor) {
+ var argTraces = this.createInputArgTrace();
+ // Generally the 'argument at the cursor' is the argument before the cursor
+ // unless it is before the first char, in which case we take the first.
+ cursor = cursor === 0 ? 0 : cursor - 1;
+ var cTrace = argTraces[cursor];
+
+ var markup = [];
+ for (var i = 0; i < argTraces.length; i++) {
+ var argTrace = argTraces[i];
+ var arg = argTrace.arg;
+ var status = Status.VALID;
+ // When things get very async we can get here while something else is
+ // doing an update, in which case arg.assignment == null, so we check first
+ if (argTrace.part === 'text' && arg.assignment != null) {
+ status = arg.assignment.getStatus(arg);
+ // Promote INCOMPLETE to ERROR ...
+ if (status === Status.INCOMPLETE) {
+ // If the cursor is in the prefix or suffix of an argument then we
+ // don't consider it in the argument for the purposes of preventing
+ // the escalation to ERROR. However if this is a NamedArgument, then we
+ // allow the suffix (as space between 2 parts of the argument) to be in.
+ // We use arg.assignment.arg not arg because we're looking at the arg
+ // that got put into the assignment not as returned by tokenize()
+ var isNamed = (cTrace.arg.assignment.arg.type === 'NamedArgument');
+ var isInside = cTrace.part === 'text' ||
+ (isNamed && cTrace.part === 'suffix');
+ if (arg.assignment !== cTrace.arg.assignment || !isInside) {
+ // And if we're not in the command
+ if (!(arg.assignment instanceof CommandAssignment)) {
+ status = Status.ERROR;
+ }
+ }
+ }
+ }
+
+ markup.push({ status: status, string: argTrace.character });
+ }
+
+ // De-dupe: merge entries where 2 adjacent have same status
+ i = 0;
+ while (i < markup.length - 1) {
+ if (markup[i].status === markup[i + 1].status) {
+ markup[i].string += markup[i + 1].string;
+ markup.splice(i + 1, 1);
+ }
+ else {
+ i++;
+ }
+ }
+
+ return markup;
+};
+
+/**
+ * Describe the state of the current input in a way that allows display of
+ * predictions and completion hints
+ * @param start The location of the cursor
+ * @param rank The index of the chosen prediction
+ * @return A promise of an object containing the following properties:
+ * - statusMarkup: An array of Status scores so we can create a marked up
+ * version of the command line input. See getInputStatusMarkup() for details
+ * - unclosedJs: Is the entered command a JS command with no closing '}'?
+ * - directTabText: A promise of the text that we *add* to the command line
+ * when TAB is pressed, to be displayed directly after the cursor. See also
+ * arrowTabText.
+ * - emptyParameters: A promise of the text that describes the arguments that
+ * the user is yet to type.
+ * - arrowTabText: A promise of the text that *replaces* the current argument
+ * when TAB is pressed, generally displayed after a "|->" symbol. See also
+ * directTabText.
+ */
+Requisition.prototype.getStateData = function(start, rank) {
+ var typed = this.toString();
+ var current = this.getAssignmentAt(start);
+ var context = this.executionContext;
+ var predictionPromise = (typed.trim().length !== 0) ?
+ current.getPredictionRanked(context, rank) :
+ Promise.resolve(null);
+
+ return predictionPromise.then(function(prediction) {
+ // directTabText is for when the current input is a prefix of the completion
+ // arrowTabText is for when we need to use an -> to show what will be used
+ var directTabText = '';
+ var arrowTabText = '';
+ var emptyParameters = [];
+
+ if (typed.trim().length !== 0) {
+ var cArg = current.arg;
+
+ if (prediction) {
+ var tabText = prediction.name;
+ var existing = cArg.text;
+
+ // Normally the cursor being just before whitespace means that you are
+ // 'in' the previous argument, which means that the prediction is based
+ // on that argument, however NamedArguments break this by having 2 parts
+ // so we need to prepend the tabText with a space for NamedArguments,
+ // but only when there isn't already a space at the end of the prefix
+ // (i.e. ' --name' not ' --name ')
+ if (current.isInName()) {
+ tabText = ' ' + tabText;
+ }
+
+ if (existing !== tabText) {
+ // Decide to use directTabText or arrowTabText
+ // Strip any leading whitespace from the user inputted value because
+ // the tabText will never have leading whitespace.
+ var inputValue = existing.replace(/^\s*/, '');
+ var isStrictCompletion = tabText.indexOf(inputValue) === 0;
+ if (isStrictCompletion && start === typed.length) {
+ // Display the suffix of the prediction as the completion
+ var numLeadingSpaces = existing.match(/^(\s*)/)[0].length;
+
+ directTabText = tabText.slice(existing.length - numLeadingSpaces);
+ }
+ else {
+ // Display the '-> prediction' at the end of the completer element
+ // \u21E5 is the JS escape right arrow
+ arrowTabText = '\u21E5 ' + tabText;
+ }
+ }
+ }
+ else {
+ // There's no prediction, but if this is a named argument that needs a
+ // value (that is without any) then we need to show that one is needed
+ // For example 'git commit --message ', clearly needs some more text
+ if (cArg.type === 'NamedArgument' && cArg.valueArg == null) {
+ emptyParameters.push('<' + current.param.type.name + '>\u00a0');
+ }
+ }
+ }
+
+ // Add a space between the typed text (+ directTabText) and the hints,
+ // making sure we don't add 2 sets of padding
+ if (directTabText !== '') {
+ directTabText += '\u00a0'; // a.k.a &nbsp;
+ }
+ else if (!this.typedEndsWithSeparator()) {
+ emptyParameters.unshift('\u00a0');
+ }
+
+ // Calculate the list of parameters to be filled in
+ // We generate an array of emptyParameter markers for each positional
+ // parameter to the current command.
+ // Generally each emptyParameter marker begins with a space to separate it
+ // from whatever came before, unless what comes before ends in a space.
+
+ this.getAssignments().forEach(function(assignment) {
+ // Named arguments are handled with a group [options] marker
+ if (!assignment.param.isPositionalAllowed) {
+ return;
+ }
+
+ // No hints if we've got content for this parameter
+ if (assignment.arg.toString().trim() !== '') {
+ return;
+ }
+
+ // No hints if we have a prediction
+ if (directTabText !== '' && current === assignment) {
+ return;
+ }
+
+ var text = (assignment.param.isDataRequired) ?
+ '<' + assignment.param.name + '>\u00a0' :
+ '[' + assignment.param.name + ']\u00a0';
+
+ emptyParameters.push(text);
+ }.bind(this));
+
+ var command = this.commandAssignment.value;
+ var addOptionsMarker = false;
+
+ // We add an '[options]' marker when there are named parameters that are
+ // not filled in and not hidden, and we don't have any directTabText
+ if (command && command.hasNamedParameters) {
+ command.params.forEach(function(param) {
+ var arg = this.getAssignment(param.name).arg;
+ if (!param.isPositionalAllowed && !param.hidden
+ && arg.type === 'BlankArgument') {
+ addOptionsMarker = true;
+ }
+ }, this);
+ }
+
+ if (addOptionsMarker) {
+ // Add an nbsp if we don't have one at the end of the input or if
+ // this isn't the first param we've mentioned
+ emptyParameters.push('[options]\u00a0');
+ }
+
+ // Is the entered command a JS command with no closing '}'?
+ var unclosedJs = command && command.name === '{' &&
+ this.getAssignment(0).arg.suffix.indexOf('}') === -1;
+
+ return {
+ statusMarkup: this.getInputStatusMarkup(start),
+ unclosedJs: unclosedJs,
+ directTabText: directTabText,
+ arrowTabText: arrowTabText,
+ emptyParameters: emptyParameters
+ };
+ }.bind(this));
+};
+
+/**
+ * Pressing TAB sometimes requires that we add a space to denote that we're on
+ * to the 'next thing'.
+ * @param assignment The assignment to which to append the space
+ */
+Requisition.prototype._addSpace = function(assignment) {
+ var arg = assignment.arg.beget({ suffixSpace: true });
+ if (arg !== assignment.arg) {
+ return this.setAssignment(assignment, arg);
+ }
+ else {
+ return Promise.resolve(undefined);
+ }
+};
+
+/**
+ * Complete the argument at <tt>cursor</tt>.
+ * Basically the same as:
+ * assignment = getAssignmentAt(cursor);
+ * assignment.value = assignment.conversion.predictions[0];
+ * Except it's done safely, and with particular care to where we place the
+ * space, which is complex, and annoying if we get it wrong.
+ *
+ * WARNING: complete() can happen asynchronously.
+ *
+ * @param cursor The cursor configuration. Should have start and end properties
+ * which should be set to start and end of the selection.
+ * @param rank The index of the prediction that we should choose.
+ * This number is not bounded by the size of the prediction array, we take the
+ * modulus to get it within bounds
+ * @return A promise which completes (with undefined) when any outstanding
+ * completion tasks are done.
+ */
+Requisition.prototype.complete = function(cursor, rank) {
+ var assignment = this.getAssignmentAt(cursor.start);
+
+ var context = this.executionContext;
+ var predictionPromise = assignment.getPredictionRanked(context, rank);
+ return predictionPromise.then(function(prediction) {
+ var outstanding = [];
+
+ // Note: Since complete is asynchronous we should perhaps have a system to
+ // bail out of making changes if the command line has changed since TAB
+ // was pressed. It's not yet clear if this will be a problem.
+
+ if (prediction == null) {
+ // No predictions generally means we shouldn't change anything on TAB,
+ // but TAB has the connotation of 'next thing' and when we're at the end
+ // of a thing that implies that we should add a space. i.e.
+ // 'help<TAB>' -> 'help '
+ // But we should only do this if the thing that we're 'completing' is
+ // valid and doesn't already end in a space.
+ if (assignment.arg.suffix.slice(-1) !== ' ' &&
+ assignment.getStatus() === Status.VALID) {
+ outstanding.push(this._addSpace(assignment));
+ }
+
+ // Also add a space if we are in the name part of an assignment, however
+ // this time we don't want the 'push the space to the next assignment'
+ // logic, so we don't use addSpace
+ if (assignment.isInName()) {
+ var newArg = assignment.arg.beget({ prefixPostSpace: true });
+ outstanding.push(this.setAssignment(assignment, newArg));
+ }
+ }
+ else {
+ // Mutate this argument to hold the completion
+ var arg = assignment.arg.beget({
+ text: prediction.name,
+ dontQuote: (assignment === this.commandAssignment)
+ });
+ var assignPromise = this.setAssignment(assignment, arg);
+
+ if (!prediction.incomplete) {
+ assignPromise = assignPromise.then(function() {
+ // The prediction is complete, add a space to let the user move-on
+ return this._addSpace(assignment).then(function() {
+ // Bug 779443 - Remove or explain the re-parse
+ if (assignment instanceof UnassignedAssignment) {
+ return this.update(this.toString());
+ }
+ }.bind(this));
+ }.bind(this));
+ }
+
+ outstanding.push(assignPromise);
+ }
+
+ return Promise.all(outstanding).then(function() {
+ return true;
+ }.bind(this));
+ }.bind(this));
+};
+
+/**
+ * Replace the current value with the lower value if such a concept exists.
+ */
+Requisition.prototype.nudge = function(assignment, by) {
+ var ctx = this.executionContext;
+ var val = assignment.param.type.nudge(assignment.value, by, ctx);
+ return Promise.resolve(val).then(function(replacement) {
+ if (replacement != null) {
+ var val = assignment.param.type.stringify(replacement, ctx);
+ return Promise.resolve(val).then(function(str) {
+ var arg = assignment.arg.beget({ text: str });
+ return this.setAssignment(assignment, arg);
+ }.bind(this));
+ }
+ }.bind(this));
+};
+
+/**
+ * Helper to find the 'data-command' attribute, used by |update()|
+ */
+function getDataCommandAttribute(element) {
+ var command = element.getAttribute('data-command');
+ if (!command) {
+ command = element.querySelector('*[data-command]')
+ .getAttribute('data-command');
+ }
+ return command;
+}
+
+/**
+ * Designed to be called from context.update(). Acts just like update() except
+ * that it also calls onExternalUpdate() to inform the UI of an unexpected
+ * change to the current command.
+ */
+Requisition.prototype._contextUpdate = function(typed) {
+ return this.update(typed).then(function(reply) {
+ this.onExternalUpdate({ typed: typed });
+ return reply;
+ }.bind(this));
+};
+
+/**
+ * Called by the UI when ever the user interacts with a command line input
+ * @param typed The contents of the input field OR an HTML element (or an event
+ * that targets an HTML element) which has a data-command attribute or a child
+ * with the same that contains the command to update with
+ */
+Requisition.prototype.update = function(typed) {
+ // Should be "if (typed instanceof HTMLElement)" except Gecko
+ if (typeof typed.querySelector === 'function') {
+ typed = getDataCommandAttribute(typed);
+ }
+ // Should be "if (typed instanceof Event)" except Gecko
+ if (typeof typed.currentTarget === 'object') {
+ typed = getDataCommandAttribute(typed.currentTarget);
+ }
+
+ var updateId = this._beginChange();
+
+ this._args = exports.tokenize(typed);
+ var args = this._args.slice(0); // i.e. clone
+
+ this._split(args);
+
+ return this._assign(args).then(function() {
+ return this._endChangeCheckOrder(updateId);
+ }.bind(this));
+};
+
+/**
+ * Similar to update('') except that it's guaranteed to execute synchronously
+ */
+Requisition.prototype.clear = function() {
+ var arg = new Argument('', '', '');
+ this._args = [ arg ];
+
+ var conversion = commandModule.parse(this.executionContext, arg, false);
+ this.setAssignment(this.commandAssignment, conversion, { internal: true });
+};
+
+/**
+ * tokenize() is a state machine. These are the states.
+ */
+var In = {
+ /**
+ * The last character was ' '.
+ * Typing a ' ' character will not change the mode
+ * Typing one of '"{ will change mode to SINGLE_Q, DOUBLE_Q or SCRIPT.
+ * Anything else goes into SIMPLE mode.
+ */
+ WHITESPACE: 1,
+
+ /**
+ * The last character was part of a parameter.
+ * Typing ' ' returns to WHITESPACE mode. Any other character
+ * (including '"{} which are otherwise special) does not change the mode.
+ */
+ SIMPLE: 2,
+
+ /**
+ * We're inside single quotes: '
+ * Typing ' returns to WHITESPACE mode. Other characters do not change mode.
+ */
+ SINGLE_Q: 3,
+
+ /**
+ * We're inside double quotes: "
+ * Typing " returns to WHITESPACE mode. Other characters do not change mode.
+ */
+ DOUBLE_Q: 4,
+
+ /**
+ * We're inside { and }
+ * Typing } returns to WHITESPACE mode. Other characters do not change mode.
+ * SCRIPT mode is slightly different from other modes in that spaces between
+ * the {/} delimiters and the actual input are not considered significant.
+ * e.g: " x " is a 3 character string, delimited by double quotes, however
+ * { x } is a 1 character JavaScript surrounded by whitespace and {}
+ * delimiters.
+ * In the short term we assume that the JS routines can make sense of the
+ * extra whitespace, however at some stage we may need to move the space into
+ * the Argument prefix/suffix.
+ * Also we don't attempt to handle nested {}. See bug 678961
+ */
+ SCRIPT: 5
+};
+
+/**
+ * Split up the input taking into account ', " and {.
+ * We don't consider \t or other classical whitespace characters to split
+ * arguments apart. For one thing these characters are hard to type, but also
+ * if the user has gone to the trouble of pasting a TAB character into the
+ * input field (or whatever it takes), they probably mean it.
+ */
+exports.tokenize = function(typed) {
+ // For blank input, place a dummy empty argument into the list
+ if (typed == null || typed.length === 0) {
+ return [ new Argument('', '', '') ];
+ }
+
+ if (isSimple(typed)) {
+ return [ new Argument(typed, '', '') ];
+ }
+
+ var mode = In.WHITESPACE;
+
+ // First we swap out escaped characters that are special to the tokenizer.
+ // So a backslash followed by any of ['"{} ] is turned into a unicode private
+ // char so we can swap back later
+ typed = typed
+ .replace(/\\\\/g, '\uF000')
+ .replace(/\\ /g, '\uF001')
+ .replace(/\\'/g, '\uF002')
+ .replace(/\\"/g, '\uF003')
+ .replace(/\\{/g, '\uF004')
+ .replace(/\\}/g, '\uF005');
+
+ function unescape2(escaped) {
+ return escaped
+ .replace(/\uF000/g, '\\\\')
+ .replace(/\uF001/g, '\\ ')
+ .replace(/\uF002/g, '\\\'')
+ .replace(/\uF003/g, '\\\"')
+ .replace(/\uF004/g, '\\{')
+ .replace(/\uF005/g, '\\}');
+ }
+
+ var i = 0; // The index of the current character
+ var start = 0; // Where did this section start?
+ var prefix = ''; // Stuff that comes before the current argument
+ var args = []; // The array that we're creating
+ var blockDepth = 0; // For JS with nested {}
+
+ // This is just a state machine. We're going through the string char by char
+ // The 'mode' is one of the 'In' states. As we go, we're adding Arguments
+ // to the 'args' array.
+
+ while (true) {
+ var c = typed[i];
+ var str;
+ switch (mode) {
+ case In.WHITESPACE:
+ if (c === '\'') {
+ prefix = typed.substring(start, i + 1);
+ mode = In.SINGLE_Q;
+ start = i + 1;
+ }
+ else if (c === '"') {
+ prefix = typed.substring(start, i + 1);
+ mode = In.DOUBLE_Q;
+ start = i + 1;
+ }
+ else if (c === '{') {
+ prefix = typed.substring(start, i + 1);
+ mode = In.SCRIPT;
+ blockDepth++;
+ start = i + 1;
+ }
+ else if (/ /.test(c)) {
+ // Still whitespace, do nothing
+ }
+ else {
+ prefix = typed.substring(start, i);
+ mode = In.SIMPLE;
+ start = i;
+ }
+ break;
+
+ case In.SIMPLE:
+ // There is an edge case of xx'xx which we are assuming to
+ // be a single parameter (and same with ")
+ if (c === ' ') {
+ str = unescape2(typed.substring(start, i));
+ args.push(new Argument(str, prefix, ''));
+ mode = In.WHITESPACE;
+ start = i;
+ prefix = '';
+ }
+ break;
+
+ case In.SINGLE_Q:
+ if (c === '\'') {
+ str = unescape2(typed.substring(start, i));
+ args.push(new Argument(str, prefix, c));
+ mode = In.WHITESPACE;
+ start = i + 1;
+ prefix = '';
+ }
+ break;
+
+ case In.DOUBLE_Q:
+ if (c === '"') {
+ str = unescape2(typed.substring(start, i));
+ args.push(new Argument(str, prefix, c));
+ mode = In.WHITESPACE;
+ start = i + 1;
+ prefix = '';
+ }
+ break;
+
+ case In.SCRIPT:
+ if (c === '{') {
+ blockDepth++;
+ }
+ else if (c === '}') {
+ blockDepth--;
+ if (blockDepth === 0) {
+ str = unescape2(typed.substring(start, i));
+ args.push(new ScriptArgument(str, prefix, c));
+ mode = In.WHITESPACE;
+ start = i + 1;
+ prefix = '';
+ }
+ }
+ break;
+ }
+
+ i++;
+
+ if (i >= typed.length) {
+ // There is nothing else to read - tidy up
+ if (mode === In.WHITESPACE) {
+ if (i !== start) {
+ // There's whitespace at the end of the typed string. Add it to the
+ // last argument's suffix, creating an empty argument if needed.
+ var extra = typed.substring(start, i);
+ var lastArg = args[args.length - 1];
+ if (!lastArg) {
+ args.push(new Argument('', extra, ''));
+ }
+ else {
+ lastArg.suffix += extra;
+ }
+ }
+ }
+ else if (mode === In.SCRIPT) {
+ str = unescape2(typed.substring(start, i + 1));
+ args.push(new ScriptArgument(str, prefix, ''));
+ }
+ else {
+ str = unescape2(typed.substring(start, i + 1));
+ args.push(new Argument(str, prefix, ''));
+ }
+ break;
+ }
+ }
+
+ return args;
+};
+
+/**
+ * If the input has no spaces, quotes, braces or escapes,
+ * we can take the fast track.
+ */
+function isSimple(typed) {
+ for (var i = 0; i < typed.length; i++) {
+ var c = typed.charAt(i);
+ if (c === ' ' || c === '"' || c === '\'' ||
+ c === '{' || c === '}' || c === '\\') {
+ return false;
+ }
+ }
+ return true;
+}
+
+/**
+ * Looks in the commands for a command extension that matches what has been
+ * typed at the command line.
+ */
+Requisition.prototype._split = function(args) {
+ // Handle the special case of the user typing { javascript(); }
+ // We use the hidden 'eval' command directly rather than shift()ing one of
+ // the parameters, and parse()ing it.
+ var conversion;
+ if (args[0].type === 'ScriptArgument') {
+ // Special case: if the user enters { console.log('foo'); } then we need to
+ // use the hidden 'eval' command
+ var command = this.system.commands.get(evalCmd.name);
+ conversion = new Conversion(command, new ScriptArgument());
+ this._setAssignmentInternal(this.commandAssignment, conversion);
+ return;
+ }
+
+ var argsUsed = 1;
+
+ while (argsUsed <= args.length) {
+ var arg = (argsUsed === 1) ?
+ args[0] :
+ new MergedArgument(args, 0, argsUsed);
+
+ if (this.prefix != null && this.prefix !== '') {
+ var prefixArg = new Argument(this.prefix, '', ' ');
+ var prefixedArg = new MergedArgument([ prefixArg, arg ]);
+
+ conversion = commandModule.parse(this.executionContext, prefixedArg, false);
+ if (conversion.value == null) {
+ conversion = commandModule.parse(this.executionContext, arg, false);
+ }
+ }
+ else {
+ conversion = commandModule.parse(this.executionContext, arg, false);
+ }
+
+ // We only want to carry on if this command is a parent command,
+ // which means that there is a commandAssignment, but not one with
+ // an exec function.
+ if (!conversion.value || conversion.value.exec) {
+ break;
+ }
+
+ // Previously we needed a way to hide commands depending context.
+ // We have not resurrected that feature yet, but if we do we should
+ // insert code here to ignore certain commands depending on the
+ // context/environment
+
+ argsUsed++;
+ }
+
+ // This could probably be re-written to consume args as we go
+ for (var i = 0; i < argsUsed; i++) {
+ args.shift();
+ }
+
+ this._setAssignmentInternal(this.commandAssignment, conversion);
+};
+
+/**
+ * Add all the passed args to the list of unassigned assignments.
+ */
+Requisition.prototype._addUnassignedArgs = function(args) {
+ args.forEach(function(arg) {
+ this._unassigned.push(new UnassignedAssignment(this, arg));
+ }.bind(this));
+
+ return RESOLVED;
+};
+
+/**
+ * Work out which arguments are applicable to which parameters.
+ */
+Requisition.prototype._assign = function(args) {
+ // See comment in _split. Avoid multiple updates
+ var noArgUp = { internal: true };
+
+ this._unassigned = [];
+
+ if (!this.commandAssignment.value) {
+ return this._addUnassignedArgs(args);
+ }
+
+ if (args.length === 0) {
+ this._setBlankArguments();
+ return RESOLVED;
+ }
+
+ // Create an error if the command does not take parameters, but we have
+ // been given them ...
+ if (this.assignmentCount === 0) {
+ return this._addUnassignedArgs(args);
+ }
+
+ // Special case: if there is only 1 parameter, and that's of type
+ // text, then we put all the params into the first param
+ if (this.assignmentCount === 1) {
+ var assignment = this.getAssignment(0);
+ if (assignment.param.type.name === 'string') {
+ var arg = (args.length === 1) ? args[0] : new MergedArgument(args);
+ return this.setAssignment(assignment, arg, noArgUp);
+ }
+ }
+
+ // Positional arguments can still be specified by name, but if they are
+ // then we need to ignore them when working them out positionally
+ var unassignedParams = this.getParameterNames();
+
+ // We collect the arguments used in arrays here before assigning
+ var arrayArgs = {};
+
+ // Extract all the named parameters
+ var assignments = this.getAssignments(false);
+ var namedDone = util.promiseEach(assignments, function(assignment) {
+ // Loop over the arguments
+ // Using while rather than loop because we remove args as we go
+ var i = 0;
+ while (i < args.length) {
+ if (!assignment.param.isKnownAs(args[i].text)) {
+ // Skip this parameter and handle as a positional parameter
+ i++;
+ continue;
+ }
+
+ var arg = args.splice(i, 1)[0];
+ /* jshint loopfunc:true */
+ unassignedParams = unassignedParams.filter(function(test) {
+ return test !== assignment.param.name;
+ });
+
+ // boolean parameters don't have values, default to false
+ if (assignment.param.type.name === 'boolean') {
+ arg = new TrueNamedArgument(arg);
+ }
+ else {
+ var valueArg = null;
+ if (i + 1 <= args.length) {
+ valueArg = args.splice(i, 1)[0];
+ }
+ arg = new NamedArgument(arg, valueArg);
+ }
+
+ if (assignment.param.type.name === 'array') {
+ var arrayArg = arrayArgs[assignment.param.name];
+ if (!arrayArg) {
+ arrayArg = new ArrayArgument();
+ arrayArgs[assignment.param.name] = arrayArg;
+ }
+ arrayArg.addArgument(arg);
+ return RESOLVED;
+ }
+ else {
+ if (assignment.arg.type === 'BlankArgument') {
+ return this.setAssignment(assignment, arg, noArgUp);
+ }
+ else {
+ return this._addUnassignedArgs(arg.getArgs());
+ }
+ }
+ }
+ }, this);
+
+ // What's left are positional parameters: assign in order
+ var positionalDone = namedDone.then(function() {
+ return util.promiseEach(unassignedParams, function(name) {
+ var assignment = this.getAssignment(name);
+
+ // If not set positionally, and we can't set it non-positionally,
+ // we have to default it to prevent previous values surviving
+ if (!assignment.param.isPositionalAllowed) {
+ this._setBlankAssignment(assignment);
+ return RESOLVED;
+ }
+
+ // If this is a positional array argument, then it swallows the
+ // rest of the arguments.
+ if (assignment.param.type.name === 'array') {
+ var arrayArg = arrayArgs[assignment.param.name];
+ if (!arrayArg) {
+ arrayArg = new ArrayArgument();
+ arrayArgs[assignment.param.name] = arrayArg;
+ }
+ arrayArg.addArguments(args);
+ args = [];
+ // The actual assignment to the array parameter is done below
+ return RESOLVED;
+ }
+
+ // Set assignment to defaults if there are no more arguments
+ if (args.length === 0) {
+ this._setBlankAssignment(assignment);
+ return RESOLVED;
+ }
+
+ var arg = args.splice(0, 1)[0];
+ // --foo and -f are named parameters, -4 is a number. So '-' is either
+ // the start of a named parameter or a number depending on the context
+ var isIncompleteName = assignment.param.type.name === 'number' ?
+ /-[-a-zA-Z_]/.test(arg.text) :
+ arg.text.charAt(0) === '-';
+
+ if (isIncompleteName) {
+ this._unassigned.push(new UnassignedAssignment(this, arg));
+ return RESOLVED;
+ }
+ else {
+ return this.setAssignment(assignment, arg, noArgUp);
+ }
+ }, this);
+ }.bind(this));
+
+ // Now we need to assign the array argument (if any)
+ var arrayDone = positionalDone.then(function() {
+ return util.promiseEach(Object.keys(arrayArgs), function(name) {
+ var assignment = this.getAssignment(name);
+ return this.setAssignment(assignment, arrayArgs[name], noArgUp);
+ }, this);
+ }.bind(this));
+
+ // What's left is can't be assigned, but we need to officially unassign them
+ return arrayDone.then(function() {
+ return this._addUnassignedArgs(args);
+ }.bind(this));
+};
+
+/**
+ * Entry point for keyboard accelerators or anything else that wants to execute
+ * a command.
+ * @param options Object describing how the execution should be handled.
+ * (optional). Contains some of the following properties:
+ * - hidden (boolean, default=false) Should the output be hidden from the
+ * commandOutputManager for this requisition
+ * - command/args A fast shortcut to executing a known command with a known
+ * set of parsed arguments.
+ */
+Requisition.prototype.exec = function(options) {
+ var command = null;
+ var args = null;
+ var hidden = false;
+
+ if (options) {
+ if (options.hidden) {
+ hidden = true;
+ }
+
+ if (options.command != null) {
+ // Fast track by looking up the command directly since passed args
+ // means there is no command line to parse.
+ command = this.system.commands.get(options.command);
+ if (!command) {
+ console.error('Command not found: ' + options.command);
+ }
+ args = options.args;
+ }
+ }
+
+ if (!command) {
+ command = this.commandAssignment.value;
+ args = this.getArgsObject();
+ }
+
+ // Display JavaScript input without the initial { or closing }
+ var typed = this.toString();
+ if (evalCmd.isCommandRegexp.test(typed)) {
+ typed = typed.replace(evalCmd.isCommandRegexp, '');
+ // Bug 717763: What if the JavaScript naturally ends with a }?
+ typed = typed.replace(/\s*}\s*$/, '');
+ }
+
+ var output = new Output({
+ command: command,
+ args: args,
+ typed: typed,
+ canonical: this.toCanonicalString(),
+ hidden: hidden
+ });
+
+ this.commandOutputManager.onOutput({ output: output });
+
+ var onDone = function(data) {
+ output.complete(data, false);
+ return output;
+ };
+
+ var onError = function(data, ex) {
+ if (logErrors) {
+ if (ex != null) {
+ util.errorHandler(ex);
+ }
+ else {
+ console.error(data);
+ }
+ }
+
+ if (data != null && typeof data === 'string') {
+ data = data.replace(/^Protocol error: /, ''); // Temp fix for bug 1035296
+ }
+
+ data = (data != null && data.isTypedData) ? data : {
+ isTypedData: true,
+ data: data,
+ type: 'error'
+ };
+ output.complete(data, true);
+ return output;
+ };
+
+ if (this.status !== Status.VALID) {
+ var ex = new Error(this.getStatusMessage());
+ // We only reject a call to exec if GCLI breaks. Errors with commands are
+ // exposed in the 'error' status of the Output object
+ return Promise.resolve(onError(ex)).then(function(output) {
+ this.clear();
+ return output;
+ }.bind(this));
+ }
+ else {
+ try {
+ return host.exec(function() {
+ return command.exec(args, this.executionContext);
+ }.bind(this)).then(onDone, onError);
+ }
+ catch (ex) {
+ var data = (typeof ex.message === 'string' && ex.stack != null) ?
+ ex.message : ex;
+ return Promise.resolve(onError(data, ex));
+ }
+ finally {
+ this.clear();
+ }
+ }
+};
+
+/**
+ * Designed to be called from context.updateExec(). Acts just like updateExec()
+ * except that it also calls onExternalUpdate() to inform the UI of an
+ * unexpected change to the current command.
+ */
+Requisition.prototype._contextUpdateExec = function(typed, options) {
+ var reqOpts = {
+ document: this.document,
+ environment: this.environment
+ };
+ var child = new Requisition(this.system, reqOpts);
+ return child.updateExec(typed, options).then(function(reply) {
+ child.destroy();
+ return reply;
+ }.bind(child));
+};
+
+/**
+ * A shortcut for calling update, resolving the promise and then exec.
+ * @param input The string to execute
+ * @param options Passed to exec
+ * @return A promise of an output object
+ */
+Requisition.prototype.updateExec = function(input, options) {
+ return this.update(input).then(function() {
+ return this.exec(options);
+ }.bind(this));
+};
+
+exports.Requisition = Requisition;
+
+/**
+ * A simple object to hold information about the output of a command
+ */
+function Output(options) {
+ options = options || {};
+ this.command = options.command || '';
+ this.args = options.args || {};
+ this.typed = options.typed || '';
+ this.canonical = options.canonical || '';
+ this.hidden = options.hidden === true ? true : false;
+
+ this.type = undefined;
+ this.data = undefined;
+ this.completed = false;
+ this.error = false;
+ this.start = new Date();
+
+ this.promise = new Promise(function(resolve, reject) {
+ this._resolve = resolve;
+ }.bind(this));
+}
+
+/**
+ * Called when there is data to display, and the command has finished executing
+ * See changed() for details on parameters.
+ */
+Output.prototype.complete = function(data, error) {
+ this.end = new Date();
+ this.completed = true;
+ this.error = error;
+
+ if (data != null && data.isTypedData) {
+ this.data = data.data;
+ this.type = data.type;
+ }
+ else {
+ this.data = data;
+ this.type = this.command.returnType;
+ if (this.type == null) {
+ this.type = (this.data == null) ? 'undefined' : typeof this.data;
+ }
+ }
+
+ if (this.type === 'object') {
+ throw new Error('No type from output of ' + this.typed);
+ }
+
+ this._resolve();
+};
+
+/**
+ * Call converters.convert using the data in this Output object
+ */
+Output.prototype.convert = function(type, conversionContext) {
+ var converters = conversionContext.system.converters;
+ return converters.convert(this.data, this.type, type, conversionContext);
+};
+
+Output.prototype.toJson = function() {
+ // Exceptions don't stringify, so we try a bit harder
+ var data = this.data;
+ if (this.error && JSON.stringify(this.data) === '{}') {
+ data = {
+ columnNumber: data.columnNumber,
+ fileName: data.fileName,
+ lineNumber: data.lineNumber,
+ message: data.message,
+ stack: data.stack
+ };
+ }
+
+ return {
+ typed: this.typed,
+ type: this.type,
+ data: data,
+ isError: this.error
+ };
+};
+
+exports.Output = Output;
diff --git a/devtools/shared/gcli/source/lib/gcli/commands/clear.js b/devtools/shared/gcli/source/lib/gcli/commands/clear.js
new file mode 100644
index 000000000..8f9327021
--- /dev/null
+++ b/devtools/shared/gcli/source/lib/gcli/commands/clear.js
@@ -0,0 +1,59 @@
+/*
+ * 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 l10n = require('../util/l10n');
+
+exports.items = [
+ {
+ // A command to clear the output area
+ item: 'command',
+ runAt: 'client',
+ name: 'clear',
+ description: l10n.lookup('clearDesc'),
+ returnType: 'clearoutput',
+ exec: function(args, context) { }
+ },
+ {
+ item: 'converter',
+ from: 'clearoutput',
+ to: 'view',
+ exec: function(ignore, conversionContext) {
+ return {
+ html: '<span onload="${onload}"></span>',
+ data: {
+ onload: function(ev) {
+ // element starts off being the span above, and we walk up the
+ // tree looking for the terminal
+ var element = ev.target;
+ while (element != null && element.terminal == null) {
+ element = element.parentElement;
+ }
+
+ if (element == null) {
+ // This is only an event handler on a completed command
+ // So we're relying on this showing up in the console
+ throw new Error('Failed to find clear');
+ }
+
+ element.terminal.clear();
+ }
+ }
+ };
+ }
+ }
+];
diff --git a/devtools/shared/gcli/source/lib/gcli/commands/commands.js b/devtools/shared/gcli/source/lib/gcli/commands/commands.js
new file mode 100644
index 000000000..67793b2dc
--- /dev/null
+++ b/devtools/shared/gcli/source/lib/gcli/commands/commands.js
@@ -0,0 +1,570 @@
+/*
+ * 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 l10n = require('../util/l10n');
+
+/**
+ * Implement the localization algorithm for any documentation objects (i.e.
+ * description and manual) in a command.
+ * @param data The data assigned to a description or manual property
+ * @param onUndefined If data == null, should we return the data untouched or
+ * lookup a 'we don't know' key in it's place.
+ */
+function lookup(data, onUndefined) {
+ if (data == null) {
+ if (onUndefined) {
+ return l10n.lookup(onUndefined);
+ }
+
+ return data;
+ }
+
+ if (typeof data === 'string') {
+ return data;
+ }
+
+ if (typeof data === 'object') {
+ if (data.key) {
+ return l10n.lookup(data.key);
+ }
+
+ var locales = l10n.getPreferredLocales();
+ var translated;
+ locales.some(function(locale) {
+ translated = data[locale];
+ return translated != null;
+ });
+ if (translated != null) {
+ return translated;
+ }
+
+ console.error('Can\'t find locale in descriptions: ' +
+ 'locales=' + JSON.stringify(locales) + ', ' +
+ 'description=' + JSON.stringify(data));
+ return '(No description)';
+ }
+
+ return l10n.lookup(onUndefined);
+}
+
+
+/**
+ * The command object is mostly just setup around a commandSpec (as passed to
+ * Commands.add()).
+ */
+function Command(types, commandSpec) {
+ Object.keys(commandSpec).forEach(function(key) {
+ this[key] = commandSpec[key];
+ }, this);
+
+ if (!this.name) {
+ throw new Error('All registered commands must have a name');
+ }
+
+ if (this.params == null) {
+ this.params = [];
+ }
+ if (!Array.isArray(this.params)) {
+ throw new Error('command.params must be an array in ' + this.name);
+ }
+
+ this.hasNamedParameters = false;
+ this.description = 'description' in this ? this.description : undefined;
+ this.description = lookup(this.description, 'canonDescNone');
+ this.manual = 'manual' in this ? this.manual : undefined;
+ this.manual = lookup(this.manual);
+
+ // At this point this.params has nested param groups. We want to flatten it
+ // out and replace the param object literals with Parameter objects
+ var paramSpecs = this.params;
+ this.params = [];
+ this.paramGroups = {};
+ this._shortParams = {};
+
+ var addParam = function(param) {
+ var groupName = param.groupName || l10n.lookup('canonDefaultGroupName');
+ this.params.push(param);
+ if (!this.paramGroups.hasOwnProperty(groupName)) {
+ this.paramGroups[groupName] = [];
+ }
+ this.paramGroups[groupName].push(param);
+ }.bind(this);
+
+ // Track if the user is trying to mix default params and param groups.
+ // All the non-grouped parameters must come before all the param groups
+ // because non-grouped parameters can be assigned positionally, so their
+ // index is important. We don't want 'holes' in the order caused by
+ // parameter groups.
+ var usingGroups = false;
+
+ // In theory this could easily be made recursive, so param groups could
+ // contain nested param groups. Current thinking is that the added
+ // complexity for the UI probably isn't worth it, so this implementation
+ // prevents nesting.
+ paramSpecs.forEach(function(spec) {
+ if (!spec.group) {
+ var param = new Parameter(types, spec, this, null);
+ addParam(param);
+
+ if (!param.isPositionalAllowed) {
+ this.hasNamedParameters = true;
+ }
+
+ if (usingGroups && param.groupName == null) {
+ throw new Error('Parameters can\'t come after param groups.' +
+ ' Ignoring ' + this.name + '/' + spec.name);
+ }
+
+ if (param.groupName != null) {
+ usingGroups = true;
+ }
+ }
+ else {
+ spec.params.forEach(function(ispec) {
+ var param = new Parameter(types, ispec, this, spec.group);
+ addParam(param);
+
+ if (!param.isPositionalAllowed) {
+ this.hasNamedParameters = true;
+ }
+ }, this);
+
+ usingGroups = true;
+ }
+ }, this);
+
+ this.params.forEach(function(param) {
+ if (param.short != null) {
+ if (this._shortParams[param.short] != null) {
+ throw new Error('Multiple params using short name ' + param.short);
+ }
+ this._shortParams[param.short] = param;
+ }
+ }, this);
+}
+
+/**
+ * JSON serializer that avoids non-serializable data
+ * @param customProps Array of strings containing additional properties which,
+ * if specified in the command spec, will be included in the JSON. Normally we
+ * transfer only the properties required for GCLI to function.
+ */
+Command.prototype.toJson = function(customProps) {
+ var json = {
+ item: 'command',
+ name: this.name,
+ params: this.params.map(function(param) { return param.toJson(); }),
+ returnType: this.returnType,
+ isParent: (this.exec == null)
+ };
+
+ if (this.description !== l10n.lookup('canonDescNone')) {
+ json.description = this.description;
+ }
+ if (this.manual != null) {
+ json.manual = this.manual;
+ }
+ if (this.hidden != null) {
+ json.hidden = this.hidden;
+ }
+
+ if (Array.isArray(customProps)) {
+ customProps.forEach(function(prop) {
+ if (this[prop] != null) {
+ json[prop] = this[prop];
+ }
+ }.bind(this));
+ }
+
+ return json;
+};
+
+/**
+ * Easy way to lookup parameters by full name
+ */
+Command.prototype.getParameterByName = function(name) {
+ var reply;
+ this.params.forEach(function(param) {
+ if (param.name === name) {
+ reply = param;
+ }
+ });
+ return reply;
+};
+
+/**
+ * Easy way to lookup parameters by short name
+ */
+Command.prototype.getParameterByShortName = function(short) {
+ return this._shortParams[short];
+};
+
+exports.Command = Command;
+
+
+/**
+ * A wrapper for a paramSpec so we can sort out shortened versions names for
+ * option switches
+ */
+function Parameter(types, paramSpec, command, groupName) {
+ this.command = command || { name: 'unnamed' };
+ this.paramSpec = paramSpec;
+ this.name = this.paramSpec.name;
+ this.type = this.paramSpec.type;
+ this.short = this.paramSpec.short;
+
+ if (this.short != null && !/[0-9A-Za-z]/.test(this.short)) {
+ throw new Error('\'short\' value must be a single alphanumeric digit.');
+ }
+
+ this.groupName = groupName;
+ if (this.groupName != null) {
+ if (this.paramSpec.option != null) {
+ throw new Error('Can\'t have a "option" property in a nested parameter');
+ }
+ }
+ else {
+ if (this.paramSpec.option != null) {
+ this.groupName = (this.paramSpec.option === true) ?
+ l10n.lookup('canonDefaultGroupName') :
+ '' + this.paramSpec.option;
+ }
+ }
+
+ if (!this.name) {
+ throw new Error('In ' + this.command.name +
+ ': all params must have a name');
+ }
+
+ var typeSpec = this.type;
+ this.type = types.createType(typeSpec);
+ if (this.type == null) {
+ console.error('Known types: ' + types.getTypeNames().join(', '));
+ throw new Error('In ' + this.command.name + '/' + this.name +
+ ': can\'t find type for: ' + JSON.stringify(typeSpec));
+ }
+
+ // boolean parameters have an implicit defaultValue:false, which should
+ // not be changed. See the docs.
+ if (this.type.name === 'boolean' &&
+ this.paramSpec.defaultValue !== undefined) {
+ throw new Error('In ' + this.command.name + '/' + this.name +
+ ': boolean parameters can not have a defaultValue.' +
+ ' Ignoring');
+ }
+
+ // All parameters that can only be set via a named parameter must have a
+ // non-undefined default value
+ if (!this.isPositionalAllowed && this.paramSpec.defaultValue === undefined &&
+ this.type.getBlank == null && this.type.name !== 'boolean') {
+ throw new Error('In ' + this.command.name + '/' + this.name +
+ ': Missing defaultValue for optional parameter.');
+ }
+
+ if (this.paramSpec.defaultValue !== undefined) {
+ this.defaultValue = this.paramSpec.defaultValue;
+ }
+ else {
+ Object.defineProperty(this, 'defaultValue', {
+ get: function() {
+ return this.type.getBlank().value;
+ },
+ enumerable: true
+ });
+ }
+
+ // Resolve the documentation
+ this.manual = lookup(this.paramSpec.manual);
+ this.description = lookup(this.paramSpec.description, 'canonDescNone');
+
+ // Is the user required to enter data for this parameter? (i.e. has
+ // defaultValue been set to something other than undefined)
+ // TODO: When the defaultValue comes from type.getBlank().value (see above)
+ // then perhaps we should set using something like
+ // isDataRequired = (type.getBlank().status !== VALID)
+ this.isDataRequired = (this.defaultValue === undefined);
+
+ // Are we allowed to assign data to this parameter using positional
+ // parameters?
+ this.isPositionalAllowed = this.groupName == null;
+}
+
+/**
+ * Does the given name uniquely identify this param (among the other params
+ * in this command)
+ * @param name The name to check
+ */
+Parameter.prototype.isKnownAs = function(name) {
+ return (name === '--' + this.name) || (name === '-' + this.short);
+};
+
+/**
+ * Reflect the paramSpec 'hidden' property (dynamically so it can change)
+ */
+Object.defineProperty(Parameter.prototype, 'hidden', {
+ get: function() {
+ return this.paramSpec.hidden;
+ },
+ enumerable: true
+});
+
+/**
+ * JSON serializer that avoids non-serializable data
+ */
+Parameter.prototype.toJson = function() {
+ var json = {
+ name: this.name,
+ type: this.type.getSpec(this.command.name, this.name),
+ short: this.short
+ };
+
+ // Values do not need to be serializable, so we don't try. For the client
+ // side (which doesn't do any executing) we don't actually care what the
+ // default value is, just that it exists
+ if (this.paramSpec.defaultValue !== undefined) {
+ json.defaultValue = {};
+ }
+ if (this.paramSpec.description != null) {
+ json.description = this.paramSpec.description;
+ }
+ if (this.paramSpec.manual != null) {
+ json.manual = this.paramSpec.manual;
+ }
+ if (this.paramSpec.hidden != null) {
+ json.hidden = this.paramSpec.hidden;
+ }
+
+ // groupName can be set outside a paramSpec, (e.g. in grouped parameters)
+ // but it works like 'option' does so we use 'option' for groupNames
+ if (this.groupName != null || this.paramSpec.option != null) {
+ json.option = this.groupName || this.paramSpec.option;
+ }
+
+ return json;
+};
+
+exports.Parameter = Parameter;
+
+
+/**
+ * A store for a list of commands
+ * @param types Each command uses a set of Types to parse its parameters so the
+ * Commands container needs access to the list of available types.
+ * @param location String that, if set will force all commands to have a
+ * matching runAt property to be accepted
+ */
+function Commands(types, location) {
+ this.types = types;
+ this.location = location;
+
+ // A lookup hash of our registered commands
+ this._commands = {};
+ // A sorted list of command names, we regularly want them in order, so pre-sort
+ this._commandNames = [];
+ // A lookup of the original commandSpecs by command name
+ this._commandSpecs = {};
+
+ // Enable people to be notified of changes to the list of commands
+ this.onCommandsChange = util.createEvent('commands.onCommandsChange');
+}
+
+/**
+ * Add a command to the list of known commands.
+ * @param commandSpec The command and its metadata.
+ * @return The new command, or null if a location property has been set and the
+ * commandSpec doesn't have a matching runAt property.
+ */
+Commands.prototype.add = function(commandSpec) {
+ if (this.location != null && commandSpec.runAt != null &&
+ commandSpec.runAt !== this.location) {
+ return;
+ }
+
+ if (this._commands[commandSpec.name] != null) {
+ // Roughly commands.remove() without the event call, which we do later
+ delete this._commands[commandSpec.name];
+ this._commandNames = this._commandNames.filter(function(test) {
+ return test !== commandSpec.name;
+ });
+ }
+
+ var command = new Command(this.types, commandSpec);
+ this._commands[commandSpec.name] = command;
+ this._commandNames.push(commandSpec.name);
+ this._commandNames.sort();
+
+ this._commandSpecs[commandSpec.name] = commandSpec;
+
+ this.onCommandsChange();
+ return command;
+};
+
+/**
+ * Remove an individual command. The opposite of Commands.add().
+ * Removing a non-existent command is a no-op.
+ * @param commandOrName Either a command name or the command itself.
+ * @return true if a command was removed, false otherwise.
+ */
+Commands.prototype.remove = function(commandOrName) {
+ var name = typeof commandOrName === 'string' ?
+ commandOrName :
+ commandOrName.name;
+
+ if (!this._commands[name]) {
+ return false;
+ }
+
+ // See start of commands.add if changing this code
+ delete this._commands[name];
+ delete this._commandSpecs[name];
+ this._commandNames = this._commandNames.filter(function(test) {
+ return test !== name;
+ });
+
+ this.onCommandsChange();
+ return true;
+};
+
+/**
+ * Retrieve a command by name
+ * @param name The name of the command to retrieve
+ */
+Commands.prototype.get = function(name) {
+ // '|| undefined' is to silence 'reference to undefined property' warnings
+ return this._commands[name] || undefined;
+};
+
+/**
+ * Get an array of all the registered commands.
+ */
+Commands.prototype.getAll = function() {
+ return Object.keys(this._commands).map(function(name) {
+ return this._commands[name];
+ }, this);
+};
+
+/**
+ * Get access to the stored commandMetaDatas (i.e. before they were made into
+ * instances of Command/Parameters) so we can remote them.
+ * @param customProps Array of strings containing additional properties which,
+ * if specified in the command spec, will be included in the JSON. Normally we
+ * transfer only the properties required for GCLI to function.
+ */
+Commands.prototype.getCommandSpecs = function(customProps) {
+ var commandSpecs = [];
+
+ Object.keys(this._commands).forEach(function(name) {
+ var command = this._commands[name];
+ if (!command.noRemote) {
+ commandSpecs.push(command.toJson(customProps));
+ }
+ }.bind(this));
+
+ return commandSpecs;
+};
+
+/**
+ * Add a set of commands that are executed somewhere else, optionally with a
+ * command prefix to distinguish these commands from a local set of commands.
+ * @param commandSpecs Presumably as obtained from getCommandSpecs
+ * @param remoter Function to call on exec of a new remote command. This is
+ * defined just like an exec function (i.e. that takes args/context as params
+ * and returns a promise) with one extra feature, that the context includes a
+ * 'commandName' property that contains the original command name.
+ * @param prefix The name prefix that we assign to all command names
+ * @param to URL-like string that describes where the commands are executed.
+ * This is to complete the parent command description.
+ */
+Commands.prototype.addProxyCommands = function(commandSpecs, remoter, prefix, to) {
+ if (prefix != null) {
+ if (this._commands[prefix] != null) {
+ throw new Error(l10n.lookupFormat('canonProxyExists', [ prefix ]));
+ }
+
+ // We need to add the parent command so all the commands from the other
+ // system have a parent
+ this.add({
+ name: prefix,
+ isProxy: true,
+ description: l10n.lookupFormat('canonProxyDesc', [ to ]),
+ manual: l10n.lookupFormat('canonProxyManual', [ to ])
+ });
+ }
+
+ commandSpecs.forEach(function(commandSpec) {
+ var originalName = commandSpec.name;
+ if (!commandSpec.isParent) {
+ commandSpec.exec = function(args, context) {
+ context.commandName = originalName;
+ return remoter(args, context);
+ }.bind(this);
+ }
+
+ if (prefix != null) {
+ commandSpec.name = prefix + ' ' + commandSpec.name;
+ }
+ commandSpec.isProxy = true;
+ this.add(commandSpec);
+ }.bind(this));
+};
+
+/**
+ * Remove a set of commands added with addProxyCommands.
+ * @param prefix The name prefix that we assign to all command names
+ */
+Commands.prototype.removeProxyCommands = function(prefix) {
+ var toRemove = [];
+ Object.keys(this._commandSpecs).forEach(function(name) {
+ if (name.indexOf(prefix) === 0) {
+ toRemove.push(name);
+ }
+ }.bind(this));
+
+ var removed = [];
+ toRemove.forEach(function(name) {
+ var command = this.get(name);
+ if (command.isProxy) {
+ this.remove(name);
+ removed.push(name);
+ }
+ else {
+ console.error('Skipping removal of \'' + name +
+ '\' because it is not a proxy command.');
+ }
+ }.bind(this));
+
+ return removed;
+};
+
+exports.Commands = Commands;
+
+/**
+ * CommandOutputManager stores the output objects generated by executed
+ * commands.
+ *
+ * CommandOutputManager is exposed to the the outside world and could (but
+ * shouldn't) be used before gcli.startup() has been called.
+ * This could should be defensive to that where possible, and we should
+ * certainly document if the use of it or similar will fail if used too soon.
+ */
+function CommandOutputManager() {
+ this.onOutput = util.createEvent('CommandOutputManager.onOutput');
+}
+
+exports.CommandOutputManager = CommandOutputManager;
diff --git a/devtools/shared/gcli/source/lib/gcli/commands/context.js b/devtools/shared/gcli/source/lib/gcli/commands/context.js
new file mode 100644
index 000000000..ad1f87ee8
--- /dev/null
+++ b/devtools/shared/gcli/source/lib/gcli/commands/context.js
@@ -0,0 +1,62 @@
+/*
+ * 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 l10n = require('../util/l10n');
+var cli = require('../cli');
+
+/**
+ * 'context' command
+ */
+var context = {
+ item: 'command',
+ name: 'context',
+ description: l10n.lookup('contextDesc'),
+ manual: l10n.lookup('contextManual'),
+ params: [
+ {
+ name: 'prefix',
+ type: 'command',
+ description: l10n.lookup('contextPrefixDesc'),
+ defaultValue: null
+ }
+ ],
+ returnType: 'string',
+ // The context command is client only because it's essentially sugar for
+ // typing commands. When there is a command prefix in action, it is the job
+ // of the remoter to add the prefix to the typed strings that are sent for
+ // remote execution
+ noRemote: true,
+ exec: function echo(args, context) {
+ var requisition = cli.getMapping(context).requisition;
+
+ if (args.prefix == null) {
+ requisition.prefix = null;
+ return l10n.lookup('contextEmptyReply');
+ }
+
+ if (args.prefix.exec != null) {
+ throw new Error(l10n.lookupFormat('contextNotParentError',
+ [ args.prefix.name ]));
+ }
+
+ requisition.prefix = args.prefix.name;
+ return l10n.lookupFormat('contextReply', [ args.prefix.name ]);
+ }
+};
+
+exports.items = [ context ];
diff --git a/devtools/shared/gcli/source/lib/gcli/commands/help.js b/devtools/shared/gcli/source/lib/gcli/commands/help.js
new file mode 100644
index 000000000..317f80240
--- /dev/null
+++ b/devtools/shared/gcli/source/lib/gcli/commands/help.js
@@ -0,0 +1,387 @@
+/*
+ * 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 l10n = require('../util/l10n');
+var cli = require('../cli');
+
+/**
+ * Add an 'paramGroups' accessor to a command metadata object to sort the
+ * params into groups according to the option of the param.
+ */
+function addParamGroups(command) {
+ Object.defineProperty(command, 'paramGroups', {
+ get: function() {
+ var paramGroups = {};
+ this.params.forEach(function(param) {
+ var groupName = param.option || l10n.lookup('canonDefaultGroupName');
+ if (paramGroups[groupName] == null) {
+ paramGroups[groupName] = [];
+ }
+ paramGroups[groupName].push(param);
+ });
+ return paramGroups;
+ },
+ enumerable: true
+ });
+}
+
+/**
+ * Get a data block for the help_man.html/help_man.txt templates
+ */
+function getHelpManData(commandData, context) {
+ // Filter out hidden parameters
+ commandData.command.params = commandData.command.params.filter(
+ param => !param.hidden
+ );
+
+ addParamGroups(commandData.command);
+ commandData.subcommands.forEach(addParamGroups);
+
+ return {
+ l10n: l10n.propertyLookup,
+ onclick: context.update,
+ ondblclick: context.updateExec,
+ describe: function(item) {
+ return item.manual || item.description;
+ },
+ getTypeDescription: function(param) {
+ var input = '';
+ if (param.defaultValue === undefined) {
+ input = l10n.lookup('helpManRequired');
+ }
+ else if (param.defaultValue === null) {
+ input = l10n.lookup('helpManOptional');
+ }
+ else {
+ // We need defaultText to work the text version of defaultValue
+ input = l10n.lookupFormat('helpManOptional');
+ /*
+ var val = param.type.stringify(param.defaultValue);
+ input = Promise.resolve(val).then(function(defaultValue) {
+ return l10n.lookupFormat('helpManDefault', [ defaultValue ]);
+ }.bind(this));
+ */
+ }
+
+ return Promise.resolve(input).then(function(defaultDescr) {
+ return '(' + (param.type.name || param.type) + ', ' + defaultDescr + ')';
+ }.bind(this));
+ },
+ getSynopsis: function(param) {
+ var name = param.name + (param.short ? '|-' + param.short : '');
+ if (param.option == null) {
+ return param.defaultValue !== undefined ?
+ '[' + name + ']' :
+ '<' + name + '>';
+ }
+ else {
+ return param.type === 'boolean' || param.type.name === 'boolean' ?
+ '[--' + name + ']' :
+ '[--' + name + ' ...]';
+ }
+ },
+ command: commandData.command,
+ subcommands: commandData.subcommands
+ };
+}
+
+/**
+ * Get a data block for the help_list.html/help_list.txt templates
+ */
+function getHelpListData(commandsData, context) {
+ commandsData.commands.forEach(addParamGroups);
+
+ var heading;
+ if (commandsData.commands.length === 0) {
+ heading = l10n.lookupFormat('helpListNone', [ commandsData.prefix ]);
+ }
+ else if (commandsData.prefix == null) {
+ heading = l10n.lookup('helpListAll');
+ }
+ else {
+ heading = l10n.lookupFormat('helpListPrefix', [ commandsData.prefix ]);
+ }
+
+ return {
+ l10n: l10n.propertyLookup,
+ includeIntro: commandsData.prefix == null,
+ heading: heading,
+ onclick: context.update,
+ ondblclick: context.updateExec,
+ matchingCommands: commandsData.commands
+ };
+}
+
+/**
+ * Create a block of data suitable to be passed to the help_list.html template
+ */
+function getMatchingCommands(context, prefix) {
+ var commands = cli.getMapping(context).requisition.system.commands;
+ var reply = commands.getAll().filter(function(command) {
+ if (command.hidden) {
+ return false;
+ }
+
+ if (prefix && command.name.indexOf(prefix) !== 0) {
+ // Filtered out because they don't match the search
+ return false;
+ }
+ if (!prefix && command.name.indexOf(' ') != -1) {
+ // We don't show sub commands with plain 'help'
+ return false;
+ }
+ return true;
+ });
+
+ reply.sort(function(c1, c2) {
+ return c1.name.localeCompare(c2.name);
+ });
+
+ reply = reply.map(function(command) {
+ return command.toJson();
+ });
+
+ return reply;
+}
+
+/**
+ * Find all the sub commands of the given command
+ */
+function getSubCommands(context, command) {
+ var commands = cli.getMapping(context).requisition.system.commands;
+ var subcommands = commands.getAll().filter(function(subcommand) {
+ return subcommand.name.indexOf(command.name) === 0 &&
+ subcommand.name !== command.name &&
+ !subcommand.hidden;
+ });
+
+ subcommands.sort(function(c1, c2) {
+ return c1.name.localeCompare(c2.name);
+ });
+
+ subcommands = subcommands.map(function(subcommand) {
+ return subcommand.toJson();
+ });
+
+ return subcommands;
+}
+
+var helpCss = '' +
+ '.gcli-help-name {\n' +
+ ' text-align: end;\n' +
+ '}\n' +
+ '\n' +
+ '.gcli-help-arrow {\n' +
+ ' color: #AAA;\n' +
+ '}\n' +
+ '\n' +
+ '.gcli-help-description {\n' +
+ ' margin: 0 20px;\n' +
+ ' padding: 0;\n' +
+ '}\n' +
+ '\n' +
+ '.gcli-help-parameter {\n' +
+ ' margin: 0 30px;\n' +
+ ' padding: 0;\n' +
+ '}\n' +
+ '\n' +
+ '.gcli-help-header {\n' +
+ ' margin: 10px 0 6px;\n' +
+ '}\n';
+
+exports.items = [
+ {
+ // 'help' command
+ item: 'command',
+ name: 'help',
+ runAt: 'client',
+ description: l10n.lookup('helpDesc'),
+ manual: l10n.lookup('helpManual'),
+ params: [
+ {
+ name: 'search',
+ type: 'string',
+ description: l10n.lookup('helpSearchDesc'),
+ manual: l10n.lookup('helpSearchManual3'),
+ defaultValue: null
+ }
+ ],
+
+ exec: function(args, context) {
+ var commands = cli.getMapping(context).requisition.system.commands;
+ var command = commands.get(args.search);
+ if (command) {
+ return context.typedData('commandData', {
+ command: command.toJson(),
+ subcommands: getSubCommands(context, command)
+ });
+ }
+
+ return context.typedData('commandsData', {
+ prefix: args.search,
+ commands: getMatchingCommands(context, args.search)
+ });
+ }
+ },
+ {
+ // Convert a command into an HTML man page
+ item: 'converter',
+ from: 'commandData',
+ to: 'view',
+ exec: function(commandData, context) {
+ return {
+ html:
+ '<div>\n' +
+ ' <p class="gcli-help-header">\n' +
+ ' ${l10n.helpManSynopsis}:\n' +
+ ' <span class="gcli-out-shortcut" data-command="${command.name}"\n' +
+ ' onclick="${onclick}" ondblclick="${ondblclick}">\n' +
+ ' ${command.name}\n' +
+ ' <span foreach="param in ${command.params}">${getSynopsis(param)} </span>\n' +
+ ' </span>\n' +
+ ' </p>\n' +
+ '\n' +
+ ' <p class="gcli-help-description">${describe(command)}</p>\n' +
+ '\n' +
+ ' <div if="${!command.isParent}">\n' +
+ ' <div foreach="groupName in ${command.paramGroups}">\n' +
+ ' <p class="gcli-help-header">${groupName}:</p>\n' +
+ ' <ul class="gcli-help-parameter">\n' +
+ ' <li if="${command.params.length === 0}">${l10n.helpManNone}</li>\n' +
+ ' <li foreach="param in ${command.paramGroups[groupName]}">\n' +
+ ' <code>${getSynopsis(param)}</code> <em>${getTypeDescription(param)}</em>\n' +
+ ' <br/>\n' +
+ ' ${describe(param)}\n' +
+ ' </li>\n' +
+ ' </ul>\n' +
+ ' </div>\n' +
+ ' </div>\n' +
+ '\n' +
+ ' <div if="${command.isParent}">\n' +
+ ' <p class="gcli-help-header">${l10n.subCommands}:</p>\n' +
+ ' <ul class="gcli-help-${subcommands}">\n' +
+ ' <li if="${subcommands.length === 0}">${l10n.subcommandsNone}</li>\n' +
+ ' <li foreach="subcommand in ${subcommands}">\n' +
+ ' ${subcommand.name}: ${subcommand.description}\n' +
+ ' <span class="gcli-out-shortcut" data-command="help ${subcommand.name}"\n' +
+ ' onclick="${onclick}" ondblclick="${ondblclick}">\n' +
+ ' help ${subcommand.name}\n' +
+ ' </span>\n' +
+ ' </li>\n' +
+ ' </ul>\n' +
+ ' </div>\n' +
+ '\n' +
+ '</div>\n',
+ options: { allowEval: true, stack: 'commandData->view' },
+ data: getHelpManData(commandData, context),
+ css: helpCss,
+ cssId: 'gcli-help'
+ };
+ }
+ },
+ {
+ // Convert a command into a string based man page
+ item: 'converter',
+ from: 'commandData',
+ to: 'stringView',
+ exec: function(commandData, context) {
+ return {
+ html:
+ '<div>## ${command.name}\n' +
+ '\n' +
+ '# ${l10n.helpManSynopsis}: ${command.name} <loop foreach="param in ${command.params}">${getSynopsis(param)} </loop>\n' +
+ '\n' +
+ '# ${l10n.helpManDescription}:\n' +
+ '\n' +
+ '${command.manual || command.description}\n' +
+ '\n' +
+ '<loop foreach="groupName in ${command.paramGroups}">\n' +
+ '<span if="${!command.isParent}"># ${groupName}:\n' +
+ '\n' +
+ '<span if="${command.params.length === 0}">${l10n.helpManNone}</span><loop foreach="param in ${command.paramGroups[groupName]}">* ${param.name}: ${getTypeDescription(param)}\n' +
+ ' ${param.manual || param.description}\n' +
+ '</loop>\n' +
+ '</span>\n' +
+ '</loop>\n' +
+ '\n' +
+ '<span if="${command.isParent}"># ${l10n.subCommands}:</span>\n' +
+ '\n' +
+ '<span if="${subcommands.length === 0}">${l10n.subcommandsNone}</span>\n' +
+ '<loop foreach="subcommand in ${subcommands}">* ${subcommand.name}: ${subcommand.description}\n' +
+ '</loop>\n' +
+ '</div>\n',
+ options: { allowEval: true, stack: 'commandData->stringView' },
+ data: getHelpManData(commandData, context)
+ };
+ }
+ },
+ {
+ // Convert a list of commands into a formatted list
+ item: 'converter',
+ from: 'commandsData',
+ to: 'view',
+ exec: function(commandsData, context) {
+ return {
+ html:
+ '<div>\n' +
+ ' <div if="${includeIntro}">\n' +
+ ' <p>${l10n.helpIntro}</p>\n' +
+ ' </div>\n' +
+ '\n' +
+ ' <p>${heading}</p>\n' +
+ '\n' +
+ ' <table>\n' +
+ ' <tr foreach="command in ${matchingCommands}">\n' +
+ ' <td class="gcli-help-name">${command.name}</td>\n' +
+ ' <td class="gcli-help-arrow">-</td>\n' +
+ ' <td>\n' +
+ ' ${command.description}\n' +
+ ' <span class="gcli-out-shortcut"\n' +
+ ' onclick="${onclick}" ondblclick="${ondblclick}"\n' +
+ ' data-command="help ${command.name}">help ${command.name}</span>\n' +
+ ' </td>\n' +
+ ' </tr>\n' +
+ ' </table>\n' +
+ '</div>\n',
+ options: { allowEval: true, stack: 'commandsData->view' },
+ data: getHelpListData(commandsData, context),
+ css: helpCss,
+ cssId: 'gcli-help'
+ };
+ }
+ },
+ {
+ // Convert a list of commands into a formatted list
+ item: 'converter',
+ from: 'commandsData',
+ to: 'stringView',
+ exec: function(commandsData, context) {
+ return {
+ html:
+ '<pre><span if="${includeIntro}">## ${l10n.helpIntro}</span>\n' +
+ '\n' +
+ '# ${heading}\n' +
+ '\n' +
+ '<loop foreach="command in ${matchingCommands}">${command.name} &#x2192; ${command.description}\n' +
+ '</loop></pre>',
+ options: { allowEval: true, stack: 'commandsData->stringView' },
+ data: getHelpListData(commandsData, context)
+ };
+ }
+ }
+];
diff --git a/devtools/shared/gcli/source/lib/gcli/commands/mocks.js b/devtools/shared/gcli/source/lib/gcli/commands/mocks.js
new file mode 100644
index 000000000..12b2ade86
--- /dev/null
+++ b/devtools/shared/gcli/source/lib/gcli/commands/mocks.js
@@ -0,0 +1,68 @@
+/*
+ * 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 cli = require('../cli');
+var mockCommands = require('../test/mockCommands');
+var mockFileCommands = require('../test/mockFileCommands');
+var mockSettings = require('../test/mockSettings');
+
+var isNode = (typeof(process) !== 'undefined' &&
+ process.title.indexOf('node') != -1);
+
+exports.items = [
+ {
+ item: 'command',
+ name: 'mocks',
+ description: 'Add/remove mock commands',
+ params: [
+ {
+ name: 'included',
+ type: {
+ name: 'selection',
+ data: [ 'on', 'off' ]
+ },
+ description: 'Turn mock commands on or off',
+ }
+ ],
+ returnType: 'string',
+
+ exec: function(args, context) {
+ var requisition = cli.getMapping(context).requisition;
+ this[args.included](requisition);
+ return 'Mock commands are now ' + args.included;
+ },
+
+ on: function(requisition) {
+ mockCommands.setup(requisition);
+ mockSettings.setup(requisition.system);
+
+ if (isNode) {
+ mockFileCommands.setup(requisition);
+ }
+ },
+
+ off: function(requisition) {
+ mockCommands.shutdown(requisition);
+ mockSettings.shutdown(requisition.system);
+
+ if (isNode) {
+ mockFileCommands.shutdown(requisition);
+ }
+ }
+ }
+];
diff --git a/devtools/shared/gcli/source/lib/gcli/commands/moz.build b/devtools/shared/gcli/source/lib/gcli/commands/moz.build
new file mode 100644
index 000000000..8cf5f0e96
--- /dev/null
+++ b/devtools/shared/gcli/source/lib/gcli/commands/moz.build
@@ -0,0 +1,16 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+DevToolsModules(
+ 'clear.js',
+ 'commands.js',
+ 'context.js',
+ 'help.js',
+ 'mocks.js',
+ 'pref.js',
+ 'preflist.js',
+ 'test.js',
+)
diff --git a/devtools/shared/gcli/source/lib/gcli/commands/pref.js b/devtools/shared/gcli/source/lib/gcli/commands/pref.js
new file mode 100644
index 000000000..387b1f8e4
--- /dev/null
+++ b/devtools/shared/gcli/source/lib/gcli/commands/pref.js
@@ -0,0 +1,93 @@
+/*
+ * 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 l10n = require('../util/l10n');
+
+exports.items = [
+ {
+ // 'pref' command
+ item: 'command',
+ name: 'pref',
+ description: l10n.lookup('prefDesc'),
+ manual: l10n.lookup('prefManual')
+ },
+ {
+ // 'pref show' command
+ item: 'command',
+ name: 'pref show',
+ runAt: 'client',
+ description: l10n.lookup('prefShowDesc'),
+ manual: l10n.lookup('prefShowManual'),
+ params: [
+ {
+ name: 'setting',
+ type: 'setting',
+ description: l10n.lookup('prefShowSettingDesc'),
+ manual: l10n.lookup('prefShowSettingManual')
+ }
+ ],
+ exec: function(args, context) {
+ return l10n.lookupFormat('prefShowSettingValue',
+ [ args.setting.name, args.setting.value ]);
+ }
+ },
+ {
+ // 'pref set' command
+ item: 'command',
+ name: 'pref set',
+ runAt: 'client',
+ description: l10n.lookup('prefSetDesc'),
+ manual: l10n.lookup('prefSetManual'),
+ params: [
+ {
+ name: 'setting',
+ type: 'setting',
+ description: l10n.lookup('prefSetSettingDesc'),
+ manual: l10n.lookup('prefSetSettingManual')
+ },
+ {
+ name: 'value',
+ type: 'settingValue',
+ description: l10n.lookup('prefSetValueDesc'),
+ manual: l10n.lookup('prefSetValueManual')
+ }
+ ],
+ exec: function(args, context) {
+ args.setting.value = args.value;
+ }
+ },
+ {
+ // 'pref reset' command
+ item: 'command',
+ name: 'pref reset',
+ runAt: 'client',
+ description: l10n.lookup('prefResetDesc'),
+ manual: l10n.lookup('prefResetManual'),
+ params: [
+ {
+ name: 'setting',
+ type: 'setting',
+ description: l10n.lookup('prefResetSettingDesc'),
+ manual: l10n.lookup('prefResetSettingManual')
+ }
+ ],
+ exec: function(args, context) {
+ args.setting.setDefault();
+ }
+ }
+];
diff --git a/devtools/shared/gcli/source/lib/gcli/commands/preflist.js b/devtools/shared/gcli/source/lib/gcli/commands/preflist.js
new file mode 100644
index 000000000..b6ca04a0b
--- /dev/null
+++ b/devtools/shared/gcli/source/lib/gcli/commands/preflist.js
@@ -0,0 +1,214 @@
+/*
+ * 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 l10n = require('../util/l10n');
+
+/**
+ * Format a list of settings for display
+ */
+var prefsViewConverter = {
+ item: 'converter',
+ from: 'prefsData',
+ to: 'view',
+ exec: function(prefsData, conversionContext) {
+ var prefList = new PrefList(prefsData, conversionContext);
+ return {
+ html:
+ '<div ignore="${onLoad(__element)}">\n' +
+ ' <!-- This is broken, and unimportant. Comment out for now\n' +
+ ' <div class="gcli-pref-list-filter">\n' +
+ ' ${l10n.prefOutputFilter}:\n' +
+ ' <input onKeyUp="${onFilterChange}" value="${search}"/>\n' +
+ ' </div>\n' +
+ ' -->\n' +
+ ' <table class="gcli-pref-list-table">\n' +
+ ' <colgroup>\n' +
+ ' <col class="gcli-pref-list-name"/>\n' +
+ ' <col class="gcli-pref-list-value"/>\n' +
+ ' </colgroup>\n' +
+ ' <tr>\n' +
+ ' <th>${l10n.prefOutputName}</th>\n' +
+ ' <th>${l10n.prefOutputValue}</th>\n' +
+ ' </tr>\n' +
+ ' </table>\n' +
+ ' <div class="gcli-pref-list-scroller">\n' +
+ ' <table class="gcli-pref-list-table" save="${table}">\n' +
+ ' </table>\n' +
+ ' </div>\n' +
+ '</div>\n',
+ data: prefList,
+ options: {
+ blankNullUndefined: true,
+ allowEval: true,
+ stack: 'prefsData->view'
+ },
+ css:
+ '.gcli-pref-list-scroller {\n' +
+ ' max-height: 200px;\n' +
+ ' overflow-y: auto;\n' +
+ ' overflow-x: hidden;\n' +
+ ' display: inline-block;\n' +
+ '}\n' +
+ '\n' +
+ '.gcli-pref-list-table {\n' +
+ ' width: 500px;\n' +
+ ' table-layout: fixed;\n' +
+ '}\n' +
+ '\n' +
+ '.gcli-pref-list-table tr > th {\n' +
+ ' text-align: left;\n' +
+ '}\n' +
+ '\n' +
+ '.gcli-pref-list-table tr > td {\n' +
+ ' text-overflow: elipsis;\n' +
+ ' word-wrap: break-word;\n' +
+ '}\n' +
+ '\n' +
+ '.gcli-pref-list-name {\n' +
+ ' width: 70%;\n' +
+ '}\n' +
+ '\n' +
+ '.gcli-pref-list-command {\n' +
+ ' display: none;\n' +
+ '}\n' +
+ '\n' +
+ '.gcli-pref-list-row:hover .gcli-pref-list-command {\n' +
+ ' /* \'pref list\' is a bit broken and unimportant. Band-aid follows */\n' +
+ ' /* display: inline-block; */\n' +
+ '}\n',
+ cssId: 'gcli-pref-list'
+ };
+ }
+};
+
+/**
+ * Format a list of settings for display
+ */
+var prefsStringConverter = {
+ item: 'converter',
+ from: 'prefsData',
+ to: 'string',
+ exec: function(prefsData, conversionContext) {
+ var reply = '';
+ prefsData.settings.forEach(function(setting) {
+ reply += setting.name + ' -> ' + setting.value + '\n';
+ });
+ return reply;
+ }
+};
+
+/**
+ * 'pref list' command
+ */
+var prefList = {
+ item: 'command',
+ name: 'pref list',
+ description: l10n.lookup('prefListDesc'),
+ manual: l10n.lookup('prefListManual'),
+ params: [
+ {
+ name: 'search',
+ type: 'string',
+ defaultValue: null,
+ description: l10n.lookup('prefListSearchDesc'),
+ manual: l10n.lookup('prefListSearchManual')
+ }
+ ],
+ returnType: 'prefsData',
+ exec: function(args, context) {
+ return new Promise(function(resolve, reject) {
+ // This can be slow, get out of the way of the main thread
+ setTimeout(function() {
+ var prefsData = {
+ settings: context.system.settings.getAll(args.search),
+ search: args.search
+ };
+ resolve(prefsData);
+ }.bind(this), 10);
+ });
+ }
+};
+
+/**
+ * A manager for our version of about:config
+ */
+function PrefList(prefsData, conversionContext) {
+ this.search = prefsData.search;
+ this.settings = prefsData.settings;
+ this.conversionContext = conversionContext;
+
+ this.onLoad = this.onLoad.bind(this);
+}
+
+/**
+ * A load event handler registered by the template engine so we can load the
+ * inner document
+ */
+PrefList.prototype.onLoad = function(element) {
+ var table = element.querySelector('.gcli-pref-list-table');
+ this.updateTable(table);
+ return '';
+};
+
+/**
+ * Forward localization lookups
+ */
+PrefList.prototype.l10n = l10n.propertyLookup;
+
+/**
+ * Called from the template onkeyup for the filter element
+ */
+PrefList.prototype.updateTable = function(table) {
+ var view = this.conversionContext.createView({
+ html:
+ '<table>\n' +
+ ' <colgroup>\n' +
+ ' <col class="gcli-pref-list-name"/>\n' +
+ ' <col class="gcli-pref-list-value"/>\n' +
+ ' </colgroup>\n' +
+ ' <tr class="gcli-pref-list-row" foreach="setting in ${settings}">\n' +
+ ' <td>${setting.name}</td>\n' +
+ ' <td onclick="${onSetClick}" data-command="pref set ${setting.name} ">\n' +
+ ' ${setting.value}\n' +
+ ' [Edit]\n' +
+ ' </td>\n' +
+ ' </tr>\n' +
+ '</table>\n',
+ options: { blankNullUndefined: true, stack: 'prefsData#inner' },
+ data: this
+ });
+
+ view.appendTo(table, true);
+};
+
+PrefList.prototype.onFilterChange = function(ev) {
+ if (ev.target.value !== this.search) {
+ this.search = ev.target.value;
+
+ var root = ev.target.parentNode.parentNode;
+ var table = root.querySelector('.gcli-pref-list-table');
+ this.updateTable(table);
+ }
+};
+
+PrefList.prototype.onSetClick = function(ev) {
+ var typed = ev.currentTarget.getAttribute('data-command');
+ this.conversionContext.update(typed);
+};
+
+exports.items = [ prefsViewConverter, prefsStringConverter, prefList ];
diff --git a/devtools/shared/gcli/source/lib/gcli/commands/test.js b/devtools/shared/gcli/source/lib/gcli/commands/test.js
new file mode 100644
index 000000000..90f56c361
--- /dev/null
+++ b/devtools/shared/gcli/source/lib/gcli/commands/test.js
@@ -0,0 +1,215 @@
+/*
+ * 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 examiner = require('../testharness/examiner');
+var stati = require('../testharness/status').stati;
+var helpers = require('../test/helpers');
+var suite = require('../test/suite');
+var cli = require('../cli');
+var Requisition = require('../cli').Requisition;
+var createRequisitionAutomator = require('../test/automators/requisition').createRequisitionAutomator;
+
+var isNode = (typeof(process) !== 'undefined' &&
+ process.title.indexOf('node') != -1);
+
+suite.init(isNode);
+
+exports.optionsContainer = [];
+
+exports.items = [
+ {
+ item: 'type',
+ name: 'suite',
+ parent: 'selection',
+ cacheable: true,
+ lookup: function() {
+ return Object.keys(examiner.suites).map(function(name) {
+ return { name: name, value: examiner.suites[name] };
+ });
+ }
+ },
+ {
+ item: 'command',
+ name: 'test',
+ description: 'Run GCLI unit tests',
+ params: [
+ {
+ name: 'suite',
+ type: 'suite',
+ description: 'Test suite to run.',
+ defaultValue: examiner
+ },
+ {
+ name: 'usehost',
+ type: 'boolean',
+ description: 'Run the unit tests in the host window',
+ option: true
+ }
+ ],
+ returnType: 'examiner-output',
+ noRemote: true,
+ exec: function(args, context) {
+ if (args.usehost && exports.optionsContainer.length === 0) {
+ throw new Error('Can\'t use --usehost without injected options');
+ }
+
+ var options;
+ if (args.usehost) {
+ options = exports.optionsContainer[0];
+ }
+ else {
+ var env = {
+ document: document,
+ window: window
+ };
+ options = {
+ isNode: isNode,
+ isFirefox: false,
+ isPhantomjs: false,
+ requisition: new Requisition(context.system, { environment: env })
+ };
+ options.automator = createRequisitionAutomator(options.requisition);
+ }
+
+ var requisition = options.requisition;
+ requisition.system.commands.get('mocks').on(requisition);
+ helpers.resetResponseTimes();
+ examiner.reset();
+
+ return args.suite.run(options).then(function() {
+ requisition.system.commands.get('mocks').off(requisition);
+ var output = context.typedData('examiner-output', examiner.toRemote());
+
+ if (output.data.summary.status === stati.pass) {
+ return output;
+ }
+ else {
+ cli.logErrors = false;
+ throw output;
+ }
+ });
+ }
+ },
+ {
+ item: 'converter',
+ from: 'examiner-output',
+ to: 'string',
+ exec: function(output, conversionContext) {
+ return '\n' + examiner.detailedResultLog('NodeJS/NoDom') +
+ '\n' + helpers.timingSummary;
+ }
+ },
+ {
+ item: 'converter',
+ from: 'examiner-output',
+ to: 'view',
+ exec: function(output, conversionContext) {
+ return {
+ html:
+ '<div>\n' +
+ ' <table class="gcliTestResults">\n' +
+ ' <thead>\n' +
+ ' <tr>\n' +
+ ' <th class="gcliTestSuite">Suite</th>\n' +
+ ' <th>Test</th>\n' +
+ ' <th>Results</th>\n' +
+ ' <th>Checks</th>\n' +
+ ' <th>Notes</th>\n' +
+ ' </tr>\n' +
+ ' </thead>\n' +
+ ' <tbody foreach="suite in ${suites}">\n' +
+ ' <tr foreach="test in ${suite.tests}" title="${suite.name}.${test.name}()">\n' +
+ ' <td class="gcliTestSuite">${suite.name}</td>\n' +
+ ' <td class="gcliTestTitle">${test.title}</td>\n' +
+ ' <td class="gcliTest${test.status.name}">${test.status.name}</td>\n' +
+ ' <td class="gcliTestChecks">${test.checks}</td>\n' +
+ ' <td class="gcliTestMessages">\n' +
+ ' <div foreach="failure in ${test.failures}">\n' +
+ ' ${failure.message}\n' +
+ ' <ul if="${failure.params}">\n' +
+ ' <li>P1: ${failure.p1}</li>\n' +
+ ' <li>P2: ${failure.p2}</li>\n' +
+ ' </ul>\n' +
+ ' </div>\n' +
+ ' </td>\n' +
+ ' </tr>\n' +
+ ' </tbody>\n' +
+ ' <tfoot>\n' +
+ ' <tr>\n' +
+ ' <th></th>\n' +
+ ' <th>Total</th>\n' +
+ ' <th>${summary.status.name}</th>\n' +
+ ' <th class="gcliTestChecks">${summary.checks}</th>\n' +
+ ' <th></th>\n' +
+ ' </tr>\n' +
+ ' </tfoot>\n' +
+ ' </table>\n' +
+ '</div>',
+ css:
+ '.gcliTestSkipped {\n' +
+ ' background-color: #EEE;\n' +
+ ' color: #000;\n' +
+ '}\n' +
+ '\n' +
+ '.gcliTestExecuting {\n' +
+ ' background-color: #888;\n' +
+ ' color: #FFF;\n' +
+ '}\n' +
+ '\n' +
+ '.gcliTestWaiting {\n' +
+ ' background-color: #FFA;\n' +
+ ' color: #000;\n' +
+ '}\n' +
+ '\n' +
+ '.gcliTestPass {\n' +
+ ' background-color: #8F8;\n' +
+ ' color: #000;\n' +
+ '}\n' +
+ '\n' +
+ '.gcliTestFail {\n' +
+ ' background-color: #F00;\n' +
+ ' color: #FFF;\n' +
+ '}\n' +
+ '\n' +
+ 'td.gcliTestSuite {\n' +
+ ' font-family: monospace;\n' +
+ ' font-size: 90%;\n' +
+ ' text-align: right;\n' +
+ '}\n' +
+ '\n' +
+ '.gcliTestResults th.gcliTestSuite,\n' +
+ '.gcliTestResults .gcliTestChecks {\n' +
+ ' text-align: right;\n' +
+ '}\n' +
+ '\n' +
+ '.gcliTestResults th {\n' +
+ ' text-align: left;\n' +
+ '}\n' +
+ '\n' +
+ '.gcliTestMessages ul {\n' +
+ ' margin: 0 0 10px;\n' +
+ ' padding-left: 20px;\n' +
+ ' list-style-type: square;\n' +
+ '}\n',
+ cssId: 'gcli-test',
+ data: output,
+ options: { allowEval: true, stack: 'test.html' }
+ };
+ }
+ }
+];
diff --git a/devtools/shared/gcli/source/lib/gcli/connectors/connectors.js b/devtools/shared/gcli/source/lib/gcli/connectors/connectors.js
new file mode 100644
index 000000000..f1a6fe339
--- /dev/null
+++ b/devtools/shared/gcli/source/lib/gcli/connectors/connectors.js
@@ -0,0 +1,157 @@
+/*
+ * 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';
+
+/**
+ * This is how to implement a connector
+ * var baseConnector = {
+ * item: 'connector',
+ * name: 'foo',
+ *
+ * connect: function(url) {
+ * return Promise.resolve(new FooConnection(url));
+ * }
+ * };
+ */
+
+/**
+ * A prototype base for Connectors
+ */
+function Connection() {
+}
+
+/**
+ * Add an event listener
+ */
+Connection.prototype.on = function(event, action) {
+ if (!this._listeners) {
+ this._listeners = {};
+ }
+ if (!this._listeners[event]) {
+ this._listeners[event] = [];
+ }
+ this._listeners[event].push(action);
+};
+
+/**
+ * Remove an event listener
+ */
+Connection.prototype.off = function(event, action) {
+ if (!this._listeners) {
+ return;
+ }
+ var actions = this._listeners[event];
+ if (actions) {
+ this._listeners[event] = actions.filter(function(li) {
+ return li !== action;
+ }.bind(this));
+ }
+};
+
+/**
+ * Emit an event. For internal use only
+ */
+Connection.prototype._emit = function(event, data) {
+ if (this._listeners == null || this._listeners[event] == null) {
+ return;
+ }
+
+ var listeners = this._listeners[event];
+ listeners.forEach(function(listener) {
+ // Fail fast if we mutate the list of listeners while emitting
+ if (listeners !== this._listeners[event]) {
+ throw new Error('Listener list changed while emitting');
+ }
+
+ try {
+ listener.call(null, data);
+ }
+ catch (ex) {
+ console.log('Error calling listeners to ' + event);
+ console.error(ex);
+ }
+ }.bind(this));
+};
+
+/**
+ * Send a message to the other side of the connection
+ */
+Connection.prototype.call = function(feature, data) {
+ throw new Error('Not implemented');
+};
+
+/**
+ * Disconnecting a Connection destroys the resources it holds. There is no
+ * common route back to being connected once this has been called
+ */
+Connection.prototype.disconnect = function() {
+ return Promise.resolve();
+};
+
+exports.Connection = Connection;
+
+/**
+ * A manager for the registered Connectors
+ */
+function Connectors() {
+ // This is where we cache the connectors that we know about
+ this._registered = {};
+}
+
+/**
+ * Add a new connector to the cache
+ */
+Connectors.prototype.add = function(connector) {
+ this._registered[connector.name] = connector;
+};
+
+/**
+ * Remove an existing connector from the cache
+ */
+Connectors.prototype.remove = function(connector) {
+ var name = typeof connector === 'string' ? connector : connector.name;
+ delete this._registered[name];
+};
+
+/**
+ * Get access to the list of known connectors
+ */
+Connectors.prototype.getAll = function() {
+ return Object.keys(this._registered).map(function(name) {
+ return this._registered[name];
+ }.bind(this));
+};
+
+var defaultConnectorName;
+
+/**
+ * Get access to a connector by name. If name is undefined then first try to
+ * use the same connector that we used last time, and if there was no last
+ * time, then just use the first registered connector as a default.
+ */
+Connectors.prototype.get = function(name) {
+ if (name == null) {
+ name = (defaultConnectorName == null) ?
+ Object.keys(this._registered)[0] :
+ defaultConnectorName;
+ }
+
+ defaultConnectorName = name;
+ return this._registered[name];
+};
+
+exports.Connectors = Connectors;
diff --git a/devtools/shared/gcli/source/lib/gcli/connectors/moz.build b/devtools/shared/gcli/source/lib/gcli/connectors/moz.build
new file mode 100644
index 000000000..33fda8fbc
--- /dev/null
+++ b/devtools/shared/gcli/source/lib/gcli/connectors/moz.build
@@ -0,0 +1,9 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+DevToolsModules(
+ 'connectors.js',
+)
diff --git a/devtools/shared/gcli/source/lib/gcli/converters/basic.js b/devtools/shared/gcli/source/lib/gcli/converters/basic.js
new file mode 100644
index 000000000..3cb448e91
--- /dev/null
+++ b/devtools/shared/gcli/source/lib/gcli/converters/basic.js
@@ -0,0 +1,94 @@
+/*
+ * 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');
+
+/**
+ * Several converters are just data.toString inside a 'p' element
+ */
+function nodeFromDataToString(data, conversionContext) {
+ var node = util.createElement(conversionContext.document, 'p');
+ node.textContent = data.toString();
+ return node;
+}
+
+exports.items = [
+ {
+ item: 'converter',
+ from: 'string',
+ to: 'dom',
+ exec: nodeFromDataToString
+ },
+ {
+ item: 'converter',
+ from: 'number',
+ to: 'dom',
+ exec: nodeFromDataToString
+ },
+ {
+ item: 'converter',
+ from: 'boolean',
+ to: 'dom',
+ exec: nodeFromDataToString
+ },
+ {
+ item: 'converter',
+ from: 'undefined',
+ to: 'dom',
+ exec: function(data, conversionContext) {
+ return util.createElement(conversionContext.document, 'span');
+ }
+ },
+ {
+ item: 'converter',
+ from: 'json',
+ to: 'view',
+ exec: function(json, context) {
+ var html = JSON.stringify(json, null, '&#160;').replace(/\n/g, '<br/>');
+ return {
+ html: '<pre>' + html + '</pre>'
+ };
+ }
+ },
+ {
+ item: 'converter',
+ from: 'number',
+ to: 'string',
+ exec: function(data) { return '' + data; }
+ },
+ {
+ item: 'converter',
+ from: 'boolean',
+ to: 'string',
+ exec: function(data) { return '' + data; }
+ },
+ {
+ item: 'converter',
+ from: 'undefined',
+ to: 'string',
+ exec: function(data) { return ''; }
+ },
+ {
+ item: 'converter',
+ from: 'json',
+ to: 'string',
+ exec: function(json, conversionContext) {
+ return JSON.stringify(json, null, ' ');
+ }
+ }
+];
diff --git a/devtools/shared/gcli/source/lib/gcli/converters/converters.js b/devtools/shared/gcli/source/lib/gcli/converters/converters.js
new file mode 100644
index 000000000..c054871d6
--- /dev/null
+++ b/devtools/shared/gcli/source/lib/gcli/converters/converters.js
@@ -0,0 +1,280 @@
+/*
+ * 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');
+
+// It's probably easiest to read this bottom to top
+
+/**
+ * Best guess at creating a DOM element from random data
+ */
+var fallbackDomConverter = {
+ from: '*',
+ to: 'dom',
+ exec: function(data, conversionContext) {
+ return conversionContext.document.createTextNode(data || '');
+ }
+};
+
+/**
+ * Best guess at creating a string from random data
+ */
+var fallbackStringConverter = {
+ from: '*',
+ to: 'string',
+ exec: function(data, conversionContext) {
+ return data == null ? '' : data.toString();
+ }
+};
+
+/**
+ * Convert a view object to a DOM element
+ */
+var viewDomConverter = {
+ item: 'converter',
+ from: 'view',
+ to: 'dom',
+ exec: function(view, conversionContext) {
+ if (!view.isView) {
+ view = conversionContext.createView(view);
+ }
+ return view.toDom(conversionContext.document);
+ }
+};
+
+/**
+ * Convert a view object to a string
+ */
+var viewStringConverter = {
+ item: 'converter',
+ from: 'view',
+ to: 'string',
+ exec: function(view, conversionContext) {
+ if (!view.isView) {
+ view = conversionContext.createView(view);
+ }
+ return view.toDom(conversionContext.document).textContent;
+ }
+};
+
+/**
+ * Convert a view object to a string
+ */
+var stringViewStringConverter = {
+ item: 'converter',
+ from: 'stringView',
+ to: 'string',
+ exec: function(view, conversionContext) {
+ if (!view.isView) {
+ view = conversionContext.createView(view);
+ }
+ return view.toDom(conversionContext.document).textContent;
+ }
+};
+
+/**
+ * Convert an exception to a DOM element
+ */
+var errorDomConverter = {
+ item: 'converter',
+ from: 'error',
+ to: 'dom',
+ exec: function(ex, conversionContext) {
+ var node = util.createElement(conversionContext.document, 'p');
+ node.className = 'gcli-error';
+ node.textContent = errorStringConverter.exec(ex, conversionContext);
+ return node;
+ }
+};
+
+/**
+ * Convert an exception to a string
+ */
+var errorStringConverter = {
+ item: 'converter',
+ from: 'error',
+ to: 'string',
+ exec: function(ex, conversionContext) {
+ if (typeof ex === 'string') {
+ return ex;
+ }
+ if (ex instanceof Error) {
+ return '' + ex;
+ }
+ if (typeof ex.message === 'string') {
+ return ex.message;
+ }
+ return '' + ex;
+ }
+};
+
+/**
+ * Create a new converter by using 2 converters, one after the other
+ */
+function getChainConverter(first, second) {
+ if (first.to !== second.from) {
+ throw new Error('Chain convert impossible: ' + first.to + '!=' + second.from);
+ }
+ return {
+ from: first.from,
+ to: second.to,
+ exec: function(data, conversionContext) {
+ var intermediate = first.exec(data, conversionContext);
+ return second.exec(intermediate, conversionContext);
+ }
+ };
+}
+
+/**
+ * A manager for the registered Converters
+ */
+function Converters() {
+ // This is where we cache the converters that we know about
+ this._registered = {
+ from: {}
+ };
+}
+
+/**
+ * Add a new converter to the cache
+ */
+Converters.prototype.add = function(converter) {
+ var fromMatch = this._registered.from[converter.from];
+ if (fromMatch == null) {
+ fromMatch = {};
+ this._registered.from[converter.from] = fromMatch;
+ }
+
+ fromMatch[converter.to] = converter;
+};
+
+/**
+ * Remove an existing converter from the cache
+ */
+Converters.prototype.remove = function(converter) {
+ var fromMatch = this._registered.from[converter.from];
+ if (fromMatch == null) {
+ return;
+ }
+
+ if (fromMatch[converter.to] === converter) {
+ fromMatch[converter.to] = null;
+ }
+};
+
+/**
+ * Work out the best converter that we've got, for a given conversion.
+ */
+Converters.prototype.get = function(from, to) {
+ var fromMatch = this._registered.from[from];
+ if (fromMatch == null) {
+ return this._getFallbackConverter(from, to);
+ }
+
+ var converter = fromMatch[to];
+ if (converter == null) {
+ // Someone is going to love writing a graph search algorithm to work out
+ // the smallest number of conversions, or perhaps the least 'lossy'
+ // conversion but for now the only 2 step conversions which we are going to
+ // special case are foo->view->dom and foo->stringView->string.
+ if (to === 'dom') {
+ converter = fromMatch.view;
+ if (converter != null) {
+ return getChainConverter(converter, viewDomConverter);
+ }
+ }
+
+ if (to === 'string') {
+ converter = fromMatch.stringView;
+ if (converter != null) {
+ return getChainConverter(converter, stringViewStringConverter);
+ }
+ converter = fromMatch.view;
+ if (converter != null) {
+ return getChainConverter(converter, viewStringConverter);
+ }
+ }
+
+ return this._getFallbackConverter(from, to);
+ }
+ return converter;
+};
+
+/**
+ * Get all the registered converters. Most for debugging
+ */
+Converters.prototype.getAll = function() {
+ return Object.keys(this._registered.from).map(function(name) {
+ return this._registered.from[name];
+ }.bind(this));
+};
+
+/**
+ * Helper for get to pick the best fallback converter
+ */
+Converters.prototype._getFallbackConverter = function(from, to) {
+ console.error('No converter from ' + from + ' to ' + to + '. Using fallback');
+
+ if (to === 'dom') {
+ return fallbackDomConverter;
+ }
+
+ if (to === 'string') {
+ return fallbackStringConverter;
+ }
+
+ throw new Error('No conversion possible from ' + from + ' to ' + to + '.');
+};
+
+/**
+ * Convert some data from one type to another
+ * @param data The object to convert
+ * @param from The type of the data right now
+ * @param to The type that we would like the data in
+ * @param conversionContext An execution context (i.e. simplified requisition)
+ * which is often required for access to a document, or createView function
+ */
+Converters.prototype.convert = function(data, from, to, conversionContext) {
+ try {
+ if (from === to) {
+ return Promise.resolve(data);
+ }
+
+ var converter = this.get(from, to);
+ return host.exec(function() {
+ return converter.exec(data, conversionContext);
+ }.bind(this));
+ }
+ catch (ex) {
+ var converter = this.get('error', to);
+ return host.exec(function() {
+ return converter.exec(ex, conversionContext);
+ }.bind(this));
+ }
+};
+
+exports.Converters = Converters;
+
+/**
+ * Items for export
+ */
+exports.items = [
+ viewDomConverter, viewStringConverter, stringViewStringConverter,
+ errorDomConverter, errorStringConverter
+];
diff --git a/devtools/shared/gcli/source/lib/gcli/converters/html.js b/devtools/shared/gcli/source/lib/gcli/converters/html.js
new file mode 100644
index 000000000..2dea0eb82
--- /dev/null
+++ b/devtools/shared/gcli/source/lib/gcli/converters/html.js
@@ -0,0 +1,47 @@
+/*
+ * 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');
+
+/**
+ * 'html' means a string containing HTML markup. We use innerHTML to inject
+ * this into a DOM which has security implications, so this module will not
+ * be used in all implementations.
+ */
+exports.items = [
+ {
+ item: 'converter',
+ from: 'html',
+ to: 'dom',
+ exec: function(html, conversionContext) {
+ var div = util.createElement(conversionContext.document, 'div');
+ div.innerHTML = html;
+ return div;
+ }
+ },
+ {
+ item: 'converter',
+ from: 'html',
+ to: 'string',
+ exec: function(html, conversionContext) {
+ var div = util.createElement(conversionContext.document, 'div');
+ div.innerHTML = html;
+ return div.textContent;
+ }
+ }
+];
diff --git a/devtools/shared/gcli/source/lib/gcli/converters/moz.build b/devtools/shared/gcli/source/lib/gcli/converters/moz.build
new file mode 100644
index 000000000..d3a649197
--- /dev/null
+++ b/devtools/shared/gcli/source/lib/gcli/converters/moz.build
@@ -0,0 +1,12 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+DevToolsModules(
+ 'basic.js',
+ 'converters.js',
+ 'html.js',
+ 'terminal.js',
+)
diff --git a/devtools/shared/gcli/source/lib/gcli/converters/terminal.js b/devtools/shared/gcli/source/lib/gcli/converters/terminal.js
new file mode 100644
index 000000000..a2406c689
--- /dev/null
+++ b/devtools/shared/gcli/source/lib/gcli/converters/terminal.js
@@ -0,0 +1,56 @@
+/*
+ * 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');
+
+/**
+ * A 'terminal' object is a string or an array of strings, which are typically
+ * the output from a shell command
+ */
+exports.items = [
+ {
+ item: 'converter',
+ from: 'terminal',
+ to: 'dom',
+ createTextArea: function(text, conversionContext) {
+ var node = util.createElement(conversionContext.document, 'textarea');
+ node.classList.add('gcli-row-subterminal');
+ node.readOnly = true;
+ node.textContent = text;
+ return node;
+ },
+ exec: function(data, conversionContext) {
+ if (Array.isArray(data)) {
+ var node = util.createElement(conversionContext.document, 'div');
+ data.forEach(function(member) {
+ node.appendChild(this.createTextArea(member, conversionContext));
+ });
+ return node;
+ }
+ return this.createTextArea(data);
+ }
+ },
+ {
+ item: 'converter',
+ from: 'terminal',
+ to: 'string',
+ exec: function(data, conversionContext) {
+ return Array.isArray(data) ? data.join('') : '' + data;
+ }
+ }
+];
diff --git a/devtools/shared/gcli/source/lib/gcli/fields/delegate.js b/devtools/shared/gcli/source/lib/gcli/fields/delegate.js
new file mode 100644
index 000000000..a2fa508f0
--- /dev/null
+++ b/devtools/shared/gcli/source/lib/gcli/fields/delegate.js
@@ -0,0 +1,96 @@
+/*
+ * 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 Field = require('./fields').Field;
+
+/**
+ * A field that works with delegate types by delaying resolution until that
+ * last possible time
+ */
+function DelegateField(type, options) {
+ Field.call(this, type, options);
+ this.options = options;
+
+ this.element = util.createElement(this.document, 'div');
+ this.update();
+
+ this.onFieldChange = util.createEvent('DelegateField.onFieldChange');
+}
+
+DelegateField.prototype = Object.create(Field.prototype);
+
+DelegateField.prototype.update = function() {
+ var subtype = this.type.getType(this.options.requisition.executionContext);
+ if (typeof subtype.parse !== 'function') {
+ subtype = this.options.requisition.system.types.createType(subtype);
+ }
+
+ // It's not clear that we can compare subtypes in this way.
+ // Perhaps we need a type.equals(...) function
+ if (subtype === this.subtype) {
+ return;
+ }
+
+ if (this.field) {
+ this.field.destroy();
+ }
+
+ this.subtype = subtype;
+ var fields = this.options.requisition.system.fields;
+ this.field = fields.get(subtype, this.options);
+
+ util.clearElement(this.element);
+ this.element.appendChild(this.field.element);
+};
+
+DelegateField.claim = function(type, context) {
+ return type.isDelegate ? Field.MATCH : Field.NO_MATCH;
+};
+
+DelegateField.prototype.destroy = function() {
+ this.element = undefined;
+ this.options = undefined;
+ if (this.field) {
+ this.field.destroy();
+ }
+ this.subtype = undefined;
+ Field.prototype.destroy.call(this);
+};
+
+DelegateField.prototype.setConversion = function(conversion) {
+ this.field.setConversion(conversion);
+};
+
+DelegateField.prototype.getConversion = function() {
+ return this.field.getConversion();
+};
+
+Object.defineProperty(DelegateField.prototype, 'isImportant', {
+ get: function() {
+ return this.field.isImportant;
+ },
+ enumerable: true
+});
+
+/**
+ * Exported items
+ */
+exports.items = [
+ DelegateField
+];
diff --git a/devtools/shared/gcli/source/lib/gcli/fields/fields.js b/devtools/shared/gcli/source/lib/gcli/fields/fields.js
new file mode 100644
index 000000000..c97184731
--- /dev/null
+++ b/devtools/shared/gcli/source/lib/gcli/fields/fields.js
@@ -0,0 +1,245 @@
+/*
+ * 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');
+
+/**
+ * A Field is a way to get input for a single parameter.
+ * This class is designed to be inherited from. It's important that all
+ * subclasses have a similar constructor signature because they are created
+ * via Fields.get(...)
+ * @param type The type to use in conversions
+ * @param options A set of properties to help fields configure themselves:
+ * - document: The document we use in calling createElement
+ * - requisition: The requisition that we're attached to
+ */
+function Field(type, options) {
+ this.type = type;
+ this.document = options.document;
+ this.requisition = options.requisition;
+}
+
+/**
+ * Enable registration of fields using addItems
+ */
+Field.prototype.item = 'field';
+
+/**
+ * Subclasses should assign their element with the DOM node that gets added
+ * to the 'form'. It doesn't have to be an input node, just something that
+ * contains it.
+ */
+Field.prototype.element = undefined;
+
+/**
+ * Called from the outside to indicate that the command line has changed and
+ * the field should update itself
+ */
+Field.prototype.update = function() {
+};
+
+/**
+ * Indicates that this field should drop any resources that it has created
+ */
+Field.prototype.destroy = function() {
+ this.messageElement = undefined;
+ this.document = undefined;
+ this.requisition = undefined;
+};
+
+// Note: We could/should probably change Fields from working with Conversions
+// to working with Arguments (Tokens), which makes for less calls to parse()
+
+/**
+ * Update this field display with the value from this conversion.
+ * Subclasses should provide an implementation of this function.
+ */
+Field.prototype.setConversion = function(conversion) {
+ throw new Error('Field should not be used directly');
+};
+
+/**
+ * Extract a conversion from the values in this field.
+ * Subclasses should provide an implementation of this function.
+ */
+Field.prototype.getConversion = function() {
+ throw new Error('Field should not be used directly');
+};
+
+/**
+ * Set the element where messages and validation errors will be displayed
+ * @see setMessage()
+ */
+Field.prototype.setMessageElement = function(element) {
+ this.messageElement = element;
+};
+
+/**
+ * Display a validation message in the UI
+ */
+Field.prototype.setMessage = function(message) {
+ if (this.messageElement) {
+ util.setTextContent(this.messageElement, message || '');
+ }
+};
+
+/**
+ * Some fields contain information that is more important to the user, for
+ * example error messages and completion menus.
+ */
+Field.prototype.isImportant = false;
+
+/**
+ * 'static/abstract' method to allow implementations of Field to lay a claim
+ * to a type. This allows claims of various strength to be weighted up.
+ * See the Field.*MATCH values.
+ */
+Field.claim = function(type, context) {
+ throw new Error('Field should not be used directly');
+};
+
+/**
+ * How good a match is a field for a given type
+ */
+Field.MATCH = 3; // Good match
+Field.DEFAULT = 2; // A default match
+Field.BASIC = 1; // OK in an emergency. i.e. assume Strings
+Field.NO_MATCH = 0; // This field can't help with the given type
+
+exports.Field = Field;
+
+
+/**
+ * A manager for the registered Fields
+ */
+function Fields() {
+ // Internal array of known fields
+ this._fieldCtors = [];
+}
+
+/**
+ * Add a field definition by field constructor
+ * @param fieldCtor Constructor function of new Field
+ */
+Fields.prototype.add = function(fieldCtor) {
+ if (typeof fieldCtor !== 'function') {
+ console.error('fields.add erroring on ', fieldCtor);
+ throw new Error('fields.add requires a Field constructor');
+ }
+ this._fieldCtors.push(fieldCtor);
+};
+
+/**
+ * Remove a Field definition
+ * @param field A previously registered field, specified either with a field
+ * name or from the field name
+ */
+Fields.prototype.remove = function(field) {
+ if (typeof field !== 'string') {
+ this._fieldCtors = this._fieldCtors.filter(function(test) {
+ return test !== field;
+ });
+ }
+ else if (field instanceof Field) {
+ this.remove(field.name);
+ }
+ else {
+ console.error('fields.remove erroring on ', field);
+ throw new Error('fields.remove requires an instance of Field');
+ }
+};
+
+/**
+ * Find the best possible matching field from the specification of the type
+ * of field required.
+ * @param type An instance of Type that we will represent
+ * @param options A set of properties that we should attempt to match, and use
+ * in the construction of the new field object:
+ * - document: The document to use in creating new elements
+ * - requisition: The requisition we're monitoring,
+ * @return A newly constructed field that best matches the input options
+ */
+Fields.prototype.get = function(type, options) {
+ var FieldConstructor;
+ var highestClaim = -1;
+ this._fieldCtors.forEach(function(fieldCtor) {
+ var context = (options.requisition == null) ?
+ null : options.requisition.executionContext;
+ var claim = fieldCtor.claim(type, context);
+ if (claim > highestClaim) {
+ highestClaim = claim;
+ FieldConstructor = fieldCtor;
+ }
+ });
+
+ if (!FieldConstructor) {
+ console.error('Unknown field type ', type, ' in ', this._fieldCtors);
+ throw new Error('Can\'t find field for ' + type);
+ }
+
+ if (highestClaim < Field.DEFAULT) {
+ return new BlankField(type, options);
+ }
+
+ return new FieldConstructor(type, options);
+};
+
+/**
+ * Get all the registered fields. Most for debugging
+ */
+Fields.prototype.getAll = function() {
+ return this._fieldCtors.slice();
+};
+
+exports.Fields = Fields;
+
+/**
+ * For use with delegate types that do not yet have anything to resolve to.
+ * BlankFields are not for general use.
+ */
+function BlankField(type, options) {
+ Field.call(this, type, options);
+
+ this.element = util.createElement(this.document, 'div');
+
+ this.onFieldChange = util.createEvent('BlankField.onFieldChange');
+}
+
+BlankField.prototype = Object.create(Field.prototype);
+
+BlankField.claim = function(type, context) {
+ return type.name === 'blank' ? Field.MATCH : Field.NO_MATCH;
+};
+
+BlankField.prototype.destroy = function() {
+ this.element = undefined;
+ Field.prototype.destroy.call(this);
+};
+
+BlankField.prototype.setConversion = function(conversion) {
+ this.setMessage(conversion.message);
+};
+
+BlankField.prototype.getConversion = function() {
+ return this.type.parseString('', this.requisition.executionContext);
+};
+
+/**
+ * Items for export
+ */
+exports.items = [ BlankField ];
diff --git a/devtools/shared/gcli/source/lib/gcli/fields/moz.build b/devtools/shared/gcli/source/lib/gcli/fields/moz.build
new file mode 100644
index 000000000..74fa1cc95
--- /dev/null
+++ b/devtools/shared/gcli/source/lib/gcli/fields/moz.build
@@ -0,0 +1,11 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+DevToolsModules(
+ 'delegate.js',
+ 'fields.js',
+ 'selection.js',
+)
diff --git a/devtools/shared/gcli/source/lib/gcli/fields/selection.js b/devtools/shared/gcli/source/lib/gcli/fields/selection.js
new file mode 100644
index 000000000..4f5885777
--- /dev/null
+++ b/devtools/shared/gcli/source/lib/gcli/fields/selection.js
@@ -0,0 +1,124 @@
+/*
+ * 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 Menu = require('../ui/menu').Menu;
+
+var Argument = require('../types/types').Argument;
+var Conversion = require('../types/types').Conversion;
+var Field = require('./fields').Field;
+
+/**
+ * A field that allows selection of one of a number of options
+ */
+function SelectionField(type, options) {
+ Field.call(this, type, options);
+
+ this.arg = new Argument();
+
+ this.menu = new Menu({
+ document: this.document,
+ maxPredictions: Conversion.maxPredictions
+ });
+ this.element = this.menu.element;
+
+ this.onFieldChange = util.createEvent('SelectionField.onFieldChange');
+
+ // i.e. Register this.onItemClick as the default action for a menu click
+ this.menu.onItemClick.add(this.itemClicked, this);
+}
+
+SelectionField.prototype = Object.create(Field.prototype);
+
+SelectionField.claim = function(type, context) {
+ if (context == null) {
+ return Field.NO_MATCH;
+ }
+ return type.getType(context).hasPredictions ? Field.DEFAULT : Field.NO_MATCH;
+};
+
+SelectionField.prototype.destroy = function() {
+ this.menu.onItemClick.remove(this.itemClicked, this);
+ this.menu.destroy();
+ this.menu = undefined;
+ this.element = undefined;
+ Field.prototype.destroy.call(this);
+};
+
+SelectionField.prototype.setConversion = function(conversion) {
+ this.arg = conversion.arg;
+ this.setMessage(conversion.message);
+
+ var context = this.requisition.executionContext;
+ conversion.getPredictions(context).then(function(predictions) {
+ var items = predictions.map(function(prediction) {
+ // If the prediction value is an 'item' (that is an object with a name and
+ // description) then use that, otherwise use the prediction itself, because
+ // at least that has a name.
+ return prediction.value && prediction.value.description ?
+ prediction.value :
+ prediction;
+ }, this);
+ if (this.menu != null) {
+ this.menu.show(items, conversion.arg.text);
+ }
+ }.bind(this)).catch(util.errorHandler);
+};
+
+SelectionField.prototype.itemClicked = function(ev) {
+ var arg = new Argument(ev.name, '', ' ');
+ var context = this.requisition.executionContext;
+
+ this.type.parse(arg, context).then(function(conversion) {
+ this.onFieldChange({ conversion: conversion });
+ this.setMessage(conversion.message);
+ }.bind(this)).catch(util.errorHandler);
+};
+
+SelectionField.prototype.getConversion = function() {
+ // This tweaks the prefix/suffix of the argument to fit
+ this.arg = this.arg.beget({ text: this.input.value });
+ return this.type.parse(this.arg, this.requisition.executionContext);
+};
+
+/**
+ * Allow the terminal to use RETURN to chose the current menu item when
+ * it can't execute the command line
+ * @return true if an item was 'clicked', false otherwise
+ */
+SelectionField.prototype.selectChoice = function() {
+ var selected = this.menu.selected;
+ if (selected == null) {
+ return false;
+ }
+
+ this.itemClicked({ name: selected });
+ return true;
+};
+
+Object.defineProperty(SelectionField.prototype, 'isImportant', {
+ get: function() {
+ return this.type.name !== 'command';
+ },
+ enumerable: true
+});
+
+/**
+ * Allow registration and de-registration.
+ */
+exports.items = [ SelectionField ];
diff --git a/devtools/shared/gcli/source/lib/gcli/index.js b/devtools/shared/gcli/source/lib/gcli/index.js
new file mode 100644
index 000000000..0b889b63d
--- /dev/null
+++ b/devtools/shared/gcli/source/lib/gcli/index.js
@@ -0,0 +1,29 @@
+/*
+ * 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 Cc = require('chrome').Cc;
+var Ci = require('chrome').Ci;
+
+
+var prefSvc = Cc['@mozilla.org/preferences-service;1']
+ .getService(Ci.nsIPrefService);
+var prefBranch = prefSvc.getBranch(null).QueryInterface(Ci.nsIPrefBranch2);
+
+exports.hiddenByChromePref = function() {
+ return !prefBranch.prefHasUserValue('devtools.chrome.enabled');
+};
diff --git a/devtools/shared/gcli/source/lib/gcli/l10n.js b/devtools/shared/gcli/source/lib/gcli/l10n.js
new file mode 100644
index 000000000..4d3f36595
--- /dev/null
+++ b/devtools/shared/gcli/source/lib/gcli/l10n.js
@@ -0,0 +1,74 @@
+/*
+ * 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 Cc = require("chrome").Cc;
+var Ci = require("chrome").Ci;
+
+var prefSvc = Cc["@mozilla.org/preferences-service;1"].getService(Ci.nsIPrefService);
+var prefBranch = prefSvc.getBranch(null).QueryInterface(Ci.nsIPrefBranch);
+
+const {LocalizationHelper} = require("devtools/shared/l10n");
+const L10N = new LocalizationHelper("devtools/shared/locales/gclicommands.properties");
+
+/**
+ * Lookup a string in the GCLI string bundle
+ */
+exports.lookup = function (name) {
+ try {
+ return L10N.getStr(name);
+ } catch (ex) {
+ throw new Error("Failure in lookup('" + name + "')");
+ }
+};
+
+/**
+ * An alternative to lookup().
+ * <code>l10n.lookup("BLAH") === l10n.propertyLookup.BLAH</code>
+ * This is particularly nice for templates because you can pass
+ * <code>l10n:l10n.propertyLookup</code> in the template data and use it
+ * like <code>${l10n.BLAH}</code>
+ */
+exports.propertyLookup = new Proxy({}, {
+ get: function (rcvr, name) {
+ return exports.lookup(name);
+ }
+});
+
+/**
+ * Lookup a string in the GCLI string bundle
+ */
+exports.lookupFormat = function (name, swaps) {
+ try {
+ return L10N.getFormatStr(name, ...swaps);
+ } catch (ex) {
+ throw new Error("Failure in lookupFormat('" + name + "')");
+ }
+};
+
+/**
+ * Allow GCLI users to be hidden by the "devtools.chrome.enabled" pref.
+ * Use it in commands like this:
+ * <pre>
+ * name: "somecommand",
+ * hidden: l10n.hiddenByChromePref(),
+ * exec: function (args, context) { ... }
+ * </pre>
+ */
+exports.hiddenByChromePref = function () {
+ return !prefBranch.getBoolPref("devtools.chrome.enabled");
+};
diff --git a/devtools/shared/gcli/source/lib/gcli/languages/command.html b/devtools/shared/gcli/source/lib/gcli/languages/command.html
new file mode 100644
index 000000000..45b367332
--- /dev/null
+++ b/devtools/shared/gcli/source/lib/gcli/languages/command.html
@@ -0,0 +1,14 @@
+
+<div>
+ <div class="gcli-row-in" save="${rowinEle}" aria-live="assertive"
+ onclick="${onclick}" ondblclick="${ondblclick}"
+ data-command="${output.canonical}">
+ <span
+ save="${promptEle}"
+ class="gcli-row-prompt ${promptClass}">:</span><span
+ class="gcli-row-in-typed">${output.typed}</span>
+ <div class="gcli-row-throbber" save="${throbEle}"></div>
+ </div>
+ <div class="gcli-row-out" aria-live="assertive" save="${rowoutEle}">
+ </div>
+</div>
diff --git a/devtools/shared/gcli/source/lib/gcli/languages/command.js b/devtools/shared/gcli/source/lib/gcli/languages/command.js
new file mode 100644
index 000000000..58357ce2b
--- /dev/null
+++ b/devtools/shared/gcli/source/lib/gcli/languages/command.js
@@ -0,0 +1,563 @@
+/*
+ * 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 domtemplate = require('../util/domtemplate');
+var host = require('../util/host');
+
+var Status = require('../types/types').Status;
+var cli = require('../cli');
+var Requisition = require('../cli').Requisition;
+var CommandAssignment = require('../cli').CommandAssignment;
+var intro = require('../ui/intro');
+
+var RESOLVED = Promise.resolve(true);
+
+/**
+ * Various ways in which we need to manipulate the caret/selection position.
+ * A value of null means we're not expecting a change
+ */
+var Caret = exports.Caret = {
+ /**
+ * We are expecting changes, but we don't need to move the cursor
+ */
+ NO_CHANGE: 0,
+
+ /**
+ * We want the entire input area to be selected
+ */
+ SELECT_ALL: 1,
+
+ /**
+ * The whole input has changed - push the cursor to the end
+ */
+ TO_END: 2,
+
+ /**
+ * A part of the input has changed - push the cursor to the end of the
+ * changed section
+ */
+ TO_ARG_END: 3
+};
+
+/**
+ * Shared promise for loading command.html
+ */
+var commandHtmlPromise;
+
+var commandLanguage = exports.commandLanguage = {
+ // Language implementation for GCLI commands
+ item: 'language',
+ name: 'commands',
+ prompt: ':',
+ proportionalFonts: true,
+
+ constructor: function(terminal) {
+ this.terminal = terminal;
+ this.document = terminal.document;
+ this.focusManager = terminal.focusManager;
+
+ var options = this.terminal.options;
+ this.requisition = options.requisition;
+ if (this.requisition == null) {
+ if (options.environment == null) {
+ options.environment = {};
+ options.environment.document = options.document || this.document;
+ options.environment.window = options.environment.document.defaultView;
+ }
+
+ this.requisition = new Requisition(terminal.system, options);
+ }
+
+ // We also keep track of the last known arg text for the current assignment
+ this.lastText = undefined;
+
+ // Used to effect caret changes. See _processCaretChange()
+ this._caretChange = null;
+
+ // We keep track of which assignment the cursor is in
+ this.assignment = this.requisition.getAssignmentAt(0);
+
+ if (commandHtmlPromise == null) {
+ commandHtmlPromise = host.staticRequire(module, './command.html');
+ }
+
+ return commandHtmlPromise.then(function(commandHtml) {
+ this.commandDom = host.toDom(this.document, commandHtml);
+
+ this.requisition.commandOutputManager.onOutput.add(this.outputted, this);
+ var mapping = cli.getMapping(this.requisition.executionContext);
+ mapping.terminal = this.terminal;
+
+ this.requisition.onExternalUpdate.add(this.textChanged, this);
+
+ return this;
+ }.bind(this));
+ },
+
+ destroy: function() {
+ var mapping = cli.getMapping(this.requisition.executionContext);
+ delete mapping.terminal;
+
+ this.requisition.commandOutputManager.onOutput.remove(this.outputted, this);
+ this.requisition.onExternalUpdate.remove(this.textChanged, this);
+
+ this.terminal = undefined;
+ this.requisition = undefined;
+ this.commandDom = undefined;
+ },
+
+ textChanged: function() {
+ if (this.terminal == null) {
+ return; // This can happen post-destroy()
+ }
+
+ if (this.terminal._caretChange == null) {
+ // We weren't expecting a change so this was requested by the hint system
+ // we should move the cursor to the end of the 'changed section', and the
+ // best we can do for that right now is the end of the current argument.
+ this.terminal._caretChange = Caret.TO_ARG_END;
+ }
+
+ var newStr = this.requisition.toString();
+ var input = this.terminal.getInputState();
+
+ input.typed = newStr;
+ this._processCaretChange(input);
+
+ // We don't update terminal._previousValue. Should we?
+ // Shouldn't this really be a function of terminal?
+ if (this.terminal.inputElement.value !== newStr) {
+ this.terminal.inputElement.value = newStr;
+ }
+ this.terminal.onInputChange({ inputState: input });
+
+ // We get here for minor things like whitespace change in arg prefix,
+ // so we ignore anything but an actual value change.
+ if (this.assignment.arg.text === this.lastText) {
+ return;
+ }
+
+ this.lastText = this.assignment.arg.text;
+
+ this.terminal.field.update();
+ this.terminal.field.setConversion(this.assignment.conversion);
+ util.setTextContent(this.terminal.descriptionEle, this.description);
+ },
+
+ // Called internally whenever we think that the current assignment might
+ // have changed, typically on mouse-clicks or key presses.
+ caretMoved: function(start) {
+ if (!this.requisition.isUpToDate()) {
+ return;
+ }
+ var newAssignment = this.requisition.getAssignmentAt(start);
+ if (newAssignment == null) {
+ return;
+ }
+
+ if (this.assignment !== newAssignment) {
+ if (this.assignment.param.type.onLeave) {
+ this.assignment.param.type.onLeave(this.assignment);
+ }
+
+ // This can be kicked off either by requisition doing an assign or by
+ // terminal noticing a cursor movement out of a command, so we should
+ // check that this really is a new assignment
+ var isNew = (this.assignment !== newAssignment);
+
+ this.assignment = newAssignment;
+ this.terminal.updateCompletion().catch(util.errorHandler);
+
+ if (isNew) {
+ this.updateHints();
+ }
+
+ if (this.assignment.param.type.onEnter) {
+ this.assignment.param.type.onEnter(this.assignment);
+ }
+ }
+ else {
+ if (this.assignment && this.assignment.param.type.onChange) {
+ this.assignment.param.type.onChange(this.assignment);
+ }
+ }
+
+ // Warning: compare the logic here with the logic in fieldChanged, which
+ // is slightly different. They should probably be the same
+ var error = (this.assignment.status === Status.ERROR);
+ this.focusManager.setError(error);
+ },
+
+ // Called whenever the assignment that we're providing help with changes
+ updateHints: function() {
+ this.lastText = this.assignment.arg.text;
+
+ var field = this.terminal.field;
+ if (field) {
+ field.onFieldChange.remove(this.terminal.fieldChanged, this.terminal);
+ field.destroy();
+ }
+
+ var fields = this.terminal.system.fields;
+ field = this.terminal.field = fields.get(this.assignment.param.type, {
+ document: this.terminal.document,
+ requisition: this.requisition
+ });
+
+ this.focusManager.setImportantFieldFlag(field.isImportant);
+
+ field.onFieldChange.add(this.terminal.fieldChanged, this.terminal);
+ field.setConversion(this.assignment.conversion);
+
+ // Filled in by the template process
+ this.terminal.errorEle = undefined;
+ this.terminal.descriptionEle = undefined;
+
+ var contents = this.terminal.tooltipTemplate.cloneNode(true);
+ domtemplate.template(contents, this.terminal, {
+ blankNullUndefined: true,
+ stack: 'terminal.html#tooltip'
+ });
+
+ util.clearElement(this.terminal.tooltipElement);
+ this.terminal.tooltipElement.appendChild(contents);
+ this.terminal.tooltipElement.style.display = 'block';
+
+ field.setMessageElement(this.terminal.errorEle);
+ },
+
+ /**
+ * See also handleDownArrow for some symmetry
+ */
+ handleUpArrow: function() {
+ // If the user is on a valid value, then we increment the value, but if
+ // they've typed something that's not right we page through predictions
+ if (this.assignment.getStatus() === Status.VALID) {
+ return this.requisition.nudge(this.assignment, 1).then(function() {
+ this.textChanged();
+ this.focusManager.onInputChange();
+ return true;
+ }.bind(this));
+ }
+
+ return Promise.resolve(false);
+ },
+
+ /**
+ * See also handleUpArrow for some symmetry
+ */
+ handleDownArrow: function() {
+ if (this.assignment.getStatus() === Status.VALID) {
+ return this.requisition.nudge(this.assignment, -1).then(function() {
+ this.textChanged();
+ this.focusManager.onInputChange();
+ return true;
+ }.bind(this));
+ }
+
+ return Promise.resolve(false);
+ },
+
+ /**
+ * RETURN checks status and might exec
+ */
+ handleReturn: function(input) {
+ // Deny RETURN unless the command might work
+ if (this.requisition.status !== Status.VALID) {
+ return Promise.resolve(false);
+ }
+
+ this.terminal.history.add(input);
+ this.terminal.unsetChoice().catch(util.errorHandler);
+
+ this.terminal._previousValue = this.terminal.inputElement.value;
+ this.terminal.inputElement.value = '';
+
+ return this.requisition.exec().then(function() {
+ this.textChanged();
+ return true;
+ }.bind(this));
+ },
+
+ /**
+ * Warning: We get TAB events for more than just the user pressing TAB in our
+ * input element.
+ */
+ handleTab: function() {
+ // It's possible for TAB to not change the input, in which case the
+ // textChanged event will not fire, and the caret move will not be
+ // processed. So we check that this is done first
+ this.terminal._caretChange = Caret.TO_ARG_END;
+ var inputState = this.terminal.getInputState();
+ this._processCaretChange(inputState);
+
+ this.terminal._previousValue = this.terminal.inputElement.value;
+
+ // The changes made by complete may happen asynchronously, so after the
+ // the call to complete() we should avoid making changes before the end
+ // of the event loop
+ var index = this.terminal.getChoiceIndex();
+ return this.requisition.complete(inputState.cursor, index).then(function(updated) {
+ // Abort UI changes if this UI update has been overtaken
+ if (!updated) {
+ return RESOLVED;
+ }
+ this.textChanged();
+ return this.terminal.unsetChoice();
+ }.bind(this));
+ },
+
+ /**
+ * The input text has changed in some way.
+ */
+ handleInput: function(value) {
+ this.terminal._caretChange = Caret.NO_CHANGE;
+
+ return this.requisition.update(value).then(function(updated) {
+ // Abort UI changes if this UI update has been overtaken
+ if (!updated) {
+ return RESOLVED;
+ }
+ this.textChanged();
+ return this.terminal.unsetChoice();
+ }.bind(this));
+ },
+
+ /**
+ * Counterpart to |setInput| for moving the cursor.
+ * @param cursor A JS object shaped like { start: x, end: y }
+ */
+ setCursor: function(cursor) {
+ this._caretChange = Caret.NO_CHANGE;
+ this._processCaretChange({
+ typed: this.terminal.inputElement.value,
+ cursor: cursor
+ });
+ },
+
+ /**
+ * If this._caretChange === Caret.TO_ARG_END, we alter the input object to move
+ * the selection start to the end of the current argument.
+ * @param input An object shaped like { typed:'', cursor: { start:0, end:0 }}
+ */
+ _processCaretChange: function(input) {
+ var start, end;
+ switch (this._caretChange) {
+ case Caret.SELECT_ALL:
+ start = 0;
+ end = input.typed.length;
+ break;
+
+ case Caret.TO_END:
+ start = input.typed.length;
+ end = input.typed.length;
+ break;
+
+ case Caret.TO_ARG_END:
+ // There could be a fancy way to do this involving assignment/arg math
+ // but it doesn't seem easy, so we cheat a move the cursor to just before
+ // the next space, or the end of the input
+ start = input.cursor.start;
+ do {
+ start++;
+ }
+ while (start < input.typed.length && input.typed[start - 1] !== ' ');
+
+ end = start;
+ break;
+
+ default:
+ start = input.cursor.start;
+ end = input.cursor.end;
+ break;
+ }
+
+ start = (start > input.typed.length) ? input.typed.length : start;
+ end = (end > input.typed.length) ? input.typed.length : end;
+
+ var newInput = {
+ typed: input.typed,
+ cursor: { start: start, end: end }
+ };
+
+ if (this.terminal.inputElement.selectionStart !== start) {
+ this.terminal.inputElement.selectionStart = start;
+ }
+ if (this.terminal.inputElement.selectionEnd !== end) {
+ this.terminal.inputElement.selectionEnd = end;
+ }
+
+ this.caretMoved(start);
+
+ this._caretChange = null;
+ return newInput;
+ },
+
+ /**
+ * Calculate the properties required by the template process for completer.html
+ */
+ getCompleterTemplateData: function() {
+ var input = this.terminal.getInputState();
+ var start = input.cursor.start;
+ var index = this.terminal.getChoiceIndex();
+
+ return this.requisition.getStateData(start, index).then(function(data) {
+ // Calculate the statusMarkup required to show wavy lines underneath the
+ // input text (like that of an inline spell-checker) which used by the
+ // template process for completer.html
+ // i.e. s/space/&nbsp/g in the string (for HTML display) and status to an
+ // appropriate class name (i.e. lower cased, prefixed with gcli-in-)
+ data.statusMarkup.forEach(function(member) {
+ member.string = member.string.replace(/ /g, '\u00a0'); // i.e. &nbsp;
+ member.className = 'gcli-in-' + member.status.toString().toLowerCase();
+ }, this);
+
+ return data;
+ });
+ },
+
+ /**
+ * Called by the onFieldChange event (via the terminal) on the current Field
+ */
+ fieldChanged: function(ev) {
+ this.requisition.setAssignment(this.assignment, ev.conversion.arg,
+ { matchPadding: true }).then(function() {
+ this.textChanged();
+ }.bind(this));
+
+ var isError = ev.conversion.message != null && ev.conversion.message !== '';
+ this.focusManager.setError(isError);
+ },
+
+ /**
+ * Monitor for new command executions
+ */
+ outputted: function(ev) {
+ if (ev.output.hidden) {
+ return;
+ }
+
+ var template = this.commandDom.cloneNode(true);
+ var templateOptions = { stack: 'terminal.html#outputView' };
+
+ var context = this.requisition.conversionContext;
+ var data = {
+ onclick: context.update,
+ ondblclick: context.updateExec,
+ language: this,
+ output: ev.output,
+ promptClass: (ev.output.error ? 'gcli-row-error' : '') +
+ (ev.output.completed ? ' gcli-row-complete' : ''),
+ // Elements attached to this by template().
+ rowinEle: null,
+ rowoutEle: null,
+ throbEle: null,
+ promptEle: null
+ };
+
+ domtemplate.template(template, data, templateOptions);
+
+ ev.output.promise.then(function() {
+ var document = data.rowoutEle.ownerDocument;
+
+ if (ev.output.completed) {
+ data.promptEle.classList.add('gcli-row-complete');
+ }
+ if (ev.output.error) {
+ data.promptEle.classList.add('gcli-row-error');
+ }
+
+ util.clearElement(data.rowoutEle);
+
+ return ev.output.convert('dom', context).then(function(node) {
+ this.terminal.scrollToBottom();
+ data.throbEle.style.display = ev.output.completed ? 'none' : 'block';
+
+ if (node == null) {
+ data.promptEle.classList.add('gcli-row-error');
+ // TODO: show some error to the user
+ }
+
+ this._linksToNewTab(node);
+ data.rowoutEle.appendChild(node);
+
+ var event = document.createEvent('Event');
+ event.initEvent('load', true, true);
+ event.addedElement = node;
+ node.dispatchEvent(event);
+ }.bind(this));
+ }.bind(this)).catch(console.error);
+
+ this.terminal.addElement(data.rowinEle);
+ this.terminal.addElement(data.rowoutEle);
+ this.terminal.scrollToBottom();
+
+ this.focusManager.outputted();
+ },
+
+ /**
+ * Find elements with href attributes and add a target=_blank so opened links
+ * will open in a new window
+ */
+ _linksToNewTab: function(element) {
+ var links = element.querySelectorAll('*[href]');
+ for (var i = 0; i < links.length; i++) {
+ links[i].setAttribute('target', '_blank');
+ }
+ return element;
+ },
+
+ /**
+ * Show a short introduction to this language
+ */
+ showIntro: function() {
+ intro.maybeShowIntro(this.requisition.commandOutputManager,
+ this.requisition.conversionContext);
+ },
+};
+
+/**
+ * The description (displayed at the top of the hint area) should be blank if
+ * we're entering the CommandAssignment (because it's obvious) otherwise it's
+ * the parameter description.
+ */
+Object.defineProperty(commandLanguage, 'description', {
+ get: function() {
+ if (this.assignment == null || (
+ this.assignment instanceof CommandAssignment &&
+ this.assignment.value == null)) {
+ return '';
+ }
+
+ return this.assignment.param.manual || this.assignment.param.description;
+ },
+ enumerable: true
+});
+
+/**
+ * Present an error message to the hint popup
+ */
+Object.defineProperty(commandLanguage, 'message', {
+ get: function() {
+ return this.assignment.conversion.message;
+ },
+ enumerable: true
+});
+
+exports.items = [ commandLanguage ];
diff --git a/devtools/shared/gcli/source/lib/gcli/languages/javascript.js b/devtools/shared/gcli/source/lib/gcli/languages/javascript.js
new file mode 100644
index 000000000..229cdd4ff
--- /dev/null
+++ b/devtools/shared/gcli/source/lib/gcli/languages/javascript.js
@@ -0,0 +1,86 @@
+/*
+ * 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 host = require('../util/host');
+var prism = require('../util/prism').Prism;
+
+function isMultiline(text) {
+ return typeof text === 'string' && text.indexOf('\n') > -1;
+}
+
+exports.items = [
+ {
+ // Language implementation for Javascript
+ item: 'language',
+ name: 'javascript',
+ prompt: '>',
+
+ constructor: function(terminal) {
+ this.document = terminal.document;
+ this.focusManager = terminal.focusManager;
+
+ this.updateHints();
+ },
+
+ destroy: function() {
+ this.document = undefined;
+ },
+
+ exec: function(input) {
+ return this.evaluate(input).then(function(response) {
+ var output = (response.exception != null) ?
+ response.exception.class :
+ response.output;
+
+ var isSameString = typeof output === 'string' &&
+ input.substr(1, input.length - 2) === output;
+ var isSameOther = typeof output !== 'string' &&
+ input === '' + output;
+
+ // Place strings in quotes
+ if (typeof output === 'string' && response.exception == null) {
+ if (output.indexOf('\'') === -1) {
+ output = '\'' + output + '\'';
+ }
+ else {
+ output = output.replace(/\\/, '\\').replace(/"/, '"').replace(/'/, '\'');
+ output = '"' + output + '"';
+ }
+ }
+
+ var line;
+ if (isSameString || isSameOther || output === undefined) {
+ line = input;
+ }
+ else if (isMultiline(output)) {
+ line = input + '\n/*\n' + output + '\n*/';
+ }
+ else {
+ line = input + ' // ' + output;
+ }
+
+ var grammar = prism.languages[this.name];
+ return prism.highlight(line, grammar, this.name);
+ }.bind(this));
+ },
+
+ evaluate: function(input) {
+ return host.script.evaluate(input);
+ }
+ }
+];
diff --git a/devtools/shared/gcli/source/lib/gcli/languages/languages.js b/devtools/shared/gcli/source/lib/gcli/languages/languages.js
new file mode 100644
index 000000000..3444c9a8f
--- /dev/null
+++ b/devtools/shared/gcli/source/lib/gcli/languages/languages.js
@@ -0,0 +1,179 @@
+/*
+ * 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 RESOLVED = Promise.resolve(true);
+
+/**
+ * This is the base implementation for all languages
+ */
+var baseLanguage = {
+ item: 'language',
+ name: undefined,
+
+ constructor: function(terminal) {
+ },
+
+ destroy: function() {
+ },
+
+ updateHints: function() {
+ util.clearElement(this.terminal.tooltipElement);
+ },
+
+ description: '',
+ message: '',
+ caretMoved: function() {},
+
+ handleUpArrow: function() {
+ return Promise.resolve(false);
+ },
+
+ handleDownArrow: function() {
+ return Promise.resolve(false);
+ },
+
+ handleTab: function() {
+ return this.terminal.unsetChoice().then(function() {
+ return RESOLVED;
+ }, util.errorHandler);
+ },
+
+ handleInput: function(input) {
+ if (input === ':') {
+ return this.terminal.setInput('').then(function() {
+ return this.terminal.pushLanguage('commands');
+ }.bind(this));
+ }
+
+ return this.terminal.unsetChoice().then(function() {
+ return RESOLVED;
+ }, util.errorHandler);
+ },
+
+ handleReturn: function(input) {
+ var rowoutEle = this.document.createElement('pre');
+ rowoutEle.classList.add('gcli-row-out');
+ rowoutEle.classList.add('gcli-row-script');
+ rowoutEle.setAttribute('aria-live', 'assertive');
+
+ return this.exec(input).then(function(line) {
+ rowoutEle.innerHTML = line;
+
+ this.terminal.addElement(rowoutEle);
+ this.terminal.scrollToBottom();
+
+ this.focusManager.outputted();
+
+ this.terminal.unsetChoice().catch(util.errorHandler);
+ this.terminal.inputElement.value = '';
+ }.bind(this));
+ },
+
+ setCursor: function(cursor) {
+ this.terminal.inputElement.selectionStart = cursor.start;
+ this.terminal.inputElement.selectionEnd = cursor.end;
+ },
+
+ getCompleterTemplateData: function() {
+ return Promise.resolve({
+ statusMarkup: [
+ {
+ string: this.terminal.inputElement.value.replace(/ /g, '\u00a0'), // i.e. &nbsp;
+ className: 'gcli-in-valid'
+ }
+ ],
+ unclosedJs: false,
+ directTabText: '',
+ arrowTabText: '',
+ emptyParameters: ''
+ });
+ },
+
+ showIntro: function() {
+ },
+
+ exec: function(input) {
+ throw new Error('Missing implementation of handleReturn() or exec() ' + this.name);
+ }
+};
+
+/**
+ * A manager for the registered Languages
+ */
+function Languages() {
+ // This is where we cache the languages that we know about
+ this._registered = {};
+}
+
+/**
+ * Add a new language to the cache
+ */
+Languages.prototype.add = function(language) {
+ this._registered[language.name] = language;
+};
+
+/**
+ * Remove an existing language from the cache
+ */
+Languages.prototype.remove = function(language) {
+ var name = typeof language === 'string' ? language : language.name;
+ delete this._registered[name];
+};
+
+/**
+ * Get access to the list of known languages
+ */
+Languages.prototype.getAll = function() {
+ return Object.keys(this._registered).map(function(name) {
+ return this._registered[name];
+ }.bind(this));
+};
+
+/**
+ * Find a previously registered language
+ */
+Languages.prototype.createLanguage = function(name, terminal) {
+ if (name == null) {
+ name = Object.keys(this._registered)[0];
+ }
+
+ var language = (typeof name === 'string') ? this._registered[name] : name;
+ if (!language) {
+ console.error('Known languages: ' + Object.keys(this._registered).join(', '));
+ throw new Error('Unknown language: \'' + name + '\'');
+ }
+
+ // clone 'type'
+ var newInstance = {};
+ util.copyProperties(baseLanguage, newInstance);
+ util.copyProperties(language, newInstance);
+
+ if (typeof newInstance.constructor === 'function') {
+ var reply = newInstance.constructor(terminal);
+ return Promise.resolve(reply).then(function() {
+ return newInstance;
+ });
+ }
+ else {
+ return Promise.resolve(newInstance);
+ }
+};
+
+exports.Languages = Languages;
diff --git a/devtools/shared/gcli/source/lib/gcli/languages/moz.build b/devtools/shared/gcli/source/lib/gcli/languages/moz.build
new file mode 100644
index 000000000..e1828a51f
--- /dev/null
+++ b/devtools/shared/gcli/source/lib/gcli/languages/moz.build
@@ -0,0 +1,12 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+DevToolsModules(
+ 'command.html',
+ 'command.js',
+ 'javascript.js',
+ 'languages.js',
+)
diff --git a/devtools/shared/gcli/source/lib/gcli/moz.build b/devtools/shared/gcli/source/lib/gcli/moz.build
new file mode 100644
index 000000000..7b1e6dd2a
--- /dev/null
+++ b/devtools/shared/gcli/source/lib/gcli/moz.build
@@ -0,0 +1,13 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+DevToolsModules(
+ 'cli.js',
+ 'index.js',
+ 'l10n.js',
+ 'settings.js',
+ 'system.js',
+)
diff --git a/devtools/shared/gcli/source/lib/gcli/mozui/completer.js b/devtools/shared/gcli/source/lib/gcli/mozui/completer.js
new file mode 100644
index 000000000..fd9a74732
--- /dev/null
+++ b/devtools/shared/gcli/source/lib/gcli/mozui/completer.js
@@ -0,0 +1,151 @@
+/*
+ * 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 domtemplate = require('../util/domtemplate');
+
+var completerHtml =
+ '<description\n' +
+ ' xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">\n' +
+ ' <loop foreach="member in ${statusMarkup}">\n' +
+ ' <label class="${member.className}" value="${member.string}"></label>\n' +
+ ' </loop>\n' +
+ ' <label class="gcli-in-ontab" value="${directTabText}"/>\n' +
+ ' <label class="gcli-in-todo" foreach="param in ${emptyParameters}" value="${param}"/>\n' +
+ ' <label class="gcli-in-ontab" value="${arrowTabText}"/>\n' +
+ ' <label class="gcli-in-closebrace" if="${unclosedJs}" value="}"/>\n' +
+ '</description>\n';
+
+/**
+ * Completer is an 'input-like' element that sits an input element annotating
+ * it with visual goodness.
+ * @param components Object that links to other UI components. GCLI provided:
+ * - requisition: A GCLI Requisition object whose state is monitored
+ * - element: Element to use as root
+ * - autoResize: (default=false): Should we attempt to sync the dimensions of
+ * the complete element with the input element.
+ */
+function Completer(components) {
+ this.requisition = components.requisition;
+ this.input = { typed: '', cursor: { start: 0, end: 0 } };
+ this.choice = 0;
+
+ this.element = components.element;
+ this.element.classList.add('gcli-in-complete');
+ this.element.setAttribute('tabindex', '-1');
+ this.element.setAttribute('aria-live', 'polite');
+
+ this.document = this.element.ownerDocument;
+
+ this.inputter = components.inputter;
+
+ this.inputter.onInputChange.add(this.update, this);
+ this.inputter.onAssignmentChange.add(this.update, this);
+ this.inputter.onChoiceChange.add(this.update, this);
+
+ this.autoResize = components.autoResize;
+ if (this.autoResize) {
+ this.inputter.onResize.add(this.resized, this);
+
+ var dimensions = this.inputter.getDimensions();
+ if (dimensions) {
+ this.resized(dimensions);
+ }
+ }
+
+ this.template = host.toDom(this.document, completerHtml);
+ // We want the spans to line up without the spaces in the template
+ util.removeWhitespace(this.template, true);
+
+ this.update();
+}
+
+/**
+ * Avoid memory leaks
+ */
+Completer.prototype.destroy = function() {
+ this.inputter.onInputChange.remove(this.update, this);
+ this.inputter.onAssignmentChange.remove(this.update, this);
+ this.inputter.onChoiceChange.remove(this.update, this);
+
+ if (this.autoResize) {
+ this.inputter.onResize.remove(this.resized, this);
+ }
+
+ this.document = undefined;
+ this.element = undefined;
+ this.template = undefined;
+ this.inputter = undefined;
+};
+
+/**
+ * Ensure that the completion element is the same size and the inputter element
+ */
+Completer.prototype.resized = function(ev) {
+ this.element.style.top = ev.top + 'px';
+ this.element.style.height = ev.height + 'px';
+ this.element.style.lineHeight = ev.height + 'px';
+ this.element.style.left = ev.left + 'px';
+ this.element.style.width = ev.width + 'px';
+};
+
+/**
+ * Bring the completion element up to date with what the requisition says
+ */
+Completer.prototype.update = function(ev) {
+ this.choice = (ev && ev.choice != null) ? ev.choice : 0;
+
+ this._getCompleterTemplateData().then(function(data) {
+ if (this.template == null) {
+ return; // destroy() has been called
+ }
+
+ var template = this.template.cloneNode(true);
+ domtemplate.template(template, data, { stack: 'completer.html' });
+
+ util.clearElement(this.element);
+ while (template.hasChildNodes()) {
+ this.element.appendChild(template.firstChild);
+ }
+ }.bind(this));
+};
+
+/**
+ * Calculate the properties required by the template process for completer.html
+ */
+Completer.prototype._getCompleterTemplateData = function() {
+ var input = this.inputter.getInputState();
+ var start = input.cursor.start;
+
+ return this.requisition.getStateData(start, this.choice).then(function(data) {
+ // Calculate the statusMarkup required to show wavy lines underneath the
+ // input text (like that of an inline spell-checker) which used by the
+ // template process for completer.html
+ // i.e. s/space/&nbsp/g in the string (for HTML display) and status to an
+ // appropriate class name (i.e. lower cased, prefixed with gcli-in-)
+ data.statusMarkup.forEach(function(member) {
+ member.string = member.string.replace(/ /g, '\u00a0'); // i.e. &nbsp;
+ member.className = 'gcli-in-' + member.status.toString().toLowerCase();
+ }, this);
+
+ return data;
+ });
+};
+
+exports.Completer = Completer;
diff --git a/devtools/shared/gcli/source/lib/gcli/mozui/inputter.js b/devtools/shared/gcli/source/lib/gcli/mozui/inputter.js
new file mode 100644
index 000000000..3810c2e8c
--- /dev/null
+++ b/devtools/shared/gcli/source/lib/gcli/mozui/inputter.js
@@ -0,0 +1,657 @@
+/*
+ * 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 KeyEvent = require('../util/util').KeyEvent;
+
+var Status = require('../types/types').Status;
+var History = require('../ui/history').History;
+
+var RESOLVED = Promise.resolve(true);
+
+/**
+ * A wrapper to take care of the functions concerning an input element
+ * @param components Object that links to other UI components. GCLI provided:
+ * - requisition
+ * - focusManager
+ * - element
+ */
+function Inputter(components) {
+ this.requisition = components.requisition;
+ this.focusManager = components.focusManager;
+
+ this.element = components.element;
+ this.element.classList.add('gcli-in-input');
+ this.element.spellcheck = false;
+
+ this.document = this.element.ownerDocument;
+
+ // Used to distinguish focus from TAB in CLI. See onKeyUp()
+ this.lastTabDownAt = 0;
+
+ // Used to effect caret changes. See _processCaretChange()
+ this._caretChange = null;
+
+ // Ensure that TAB/UP/DOWN isn't handled by the browser
+ this.onKeyDown = this.onKeyDown.bind(this);
+ this.onKeyUp = this.onKeyUp.bind(this);
+ this.element.addEventListener('keydown', this.onKeyDown, false);
+ this.element.addEventListener('keyup', this.onKeyUp, false);
+
+ // Setup History
+ this.history = new History();
+ this._scrollingThroughHistory = false;
+
+ // Used when we're selecting which prediction to complete with
+ this._choice = null;
+ this.onChoiceChange = util.createEvent('Inputter.onChoiceChange');
+
+ // Cursor position affects hint severity
+ this.onMouseUp = this.onMouseUp.bind(this);
+ this.element.addEventListener('mouseup', this.onMouseUp, false);
+
+ if (this.focusManager) {
+ this.focusManager.addMonitoredElement(this.element, 'input');
+ }
+
+ // Initially an asynchronous completion isn't in-progress
+ this._completed = RESOLVED;
+
+ this.textChanged = this.textChanged.bind(this);
+
+ this.outputted = this.outputted.bind(this);
+ this.requisition.commandOutputManager.onOutput.add(this.outputted, this);
+
+ this.assignment = this.requisition.getAssignmentAt(0);
+ this.onAssignmentChange = util.createEvent('Inputter.onAssignmentChange');
+ this.onInputChange = util.createEvent('Inputter.onInputChange');
+
+ this.onResize = util.createEvent('Inputter.onResize');
+ this.onWindowResize = this.onWindowResize.bind(this);
+ this.document.defaultView.addEventListener('resize', this.onWindowResize, false);
+ this.requisition.onExternalUpdate.add(this.textChanged, this);
+
+ this._previousValue = undefined;
+ this.requisition.update(this.element.value || '');
+}
+
+/**
+ * Avoid memory leaks
+ */
+Inputter.prototype.destroy = function() {
+ this.document.defaultView.removeEventListener('resize', this.onWindowResize, false);
+
+ this.requisition.commandOutputManager.onOutput.remove(this.outputted, this);
+ this.requisition.onExternalUpdate.remove(this.textChanged, this);
+ if (this.focusManager) {
+ this.focusManager.removeMonitoredElement(this.element, 'input');
+ }
+
+ this.element.removeEventListener('mouseup', this.onMouseUp, false);
+ this.element.removeEventListener('keydown', this.onKeyDown, false);
+ this.element.removeEventListener('keyup', this.onKeyUp, false);
+
+ this.history.destroy();
+
+ if (this.style) {
+ this.style.parentNode.removeChild(this.style);
+ this.style = undefined;
+ }
+
+ this.textChanged = undefined;
+ this.outputted = undefined;
+ this.onMouseUp = undefined;
+ this.onKeyDown = undefined;
+ this.onKeyUp = undefined;
+ this.onWindowResize = undefined;
+ this.tooltip = undefined;
+ this.document = undefined;
+ this.element = undefined;
+};
+
+/**
+ * Make ourselves visually similar to the input element, and make the input
+ * element transparent so our background shines through
+ */
+Inputter.prototype.onWindowResize = function() {
+ // Mochitest sometimes causes resize after shutdown. See Bug 743190
+ if (!this.element) {
+ return;
+ }
+
+ this.onResize(this.getDimensions());
+};
+
+/**
+ * Make ourselves visually similar to the input element, and make the input
+ * element transparent so our background shines through
+ */
+Inputter.prototype.getDimensions = function() {
+ var fixedLoc = {};
+ var currentElement = this.element.parentNode;
+ while (currentElement && currentElement.nodeName !== '#document') {
+ var style = this.document.defaultView.getComputedStyle(currentElement, '');
+ if (style) {
+ var position = style.getPropertyValue('position');
+ if (position === 'absolute' || position === 'fixed') {
+ var bounds = currentElement.getBoundingClientRect();
+ fixedLoc.top = bounds.top;
+ fixedLoc.left = bounds.left;
+ break;
+ }
+ }
+ currentElement = currentElement.parentNode;
+ }
+
+ var rect = this.element.getBoundingClientRect();
+ return {
+ top: rect.top - (fixedLoc.top || 0) + 1,
+ height: rect.bottom - rect.top - 1,
+ left: rect.left - (fixedLoc.left || 0) + 2,
+ width: rect.right - rect.left
+ };
+};
+
+/**
+ * Pass 'outputted' events on to the focus manager
+ */
+Inputter.prototype.outputted = function() {
+ if (this.focusManager) {
+ this.focusManager.outputted();
+ }
+};
+
+/**
+ * Handler for the input-element.onMouseUp event
+ */
+Inputter.prototype.onMouseUp = function(ev) {
+ this._checkAssignment();
+};
+
+/**
+ * Function called when we think the text might have changed
+ */
+Inputter.prototype.textChanged = function() {
+ if (!this.document) {
+ return; // This can happen post-destroy()
+ }
+
+ if (this._caretChange == null) {
+ // We weren't expecting a change so this was requested by the hint system
+ // we should move the cursor to the end of the 'changed section', and the
+ // best we can do for that right now is the end of the current argument.
+ this._caretChange = Caret.TO_ARG_END;
+ }
+
+ var newStr = this.requisition.toString();
+ var input = this.getInputState();
+
+ input.typed = newStr;
+ this._processCaretChange(input);
+
+ if (this.element.value !== newStr) {
+ this.element.value = newStr;
+ }
+ this.onInputChange({ inputState: input });
+
+ this.tooltip.textChanged();
+};
+
+/**
+ * Various ways in which we need to manipulate the caret/selection position.
+ * A value of null means we're not expecting a change
+ */
+var Caret = {
+ /**
+ * We are expecting changes, but we don't need to move the cursor
+ */
+ NO_CHANGE: 0,
+
+ /**
+ * We want the entire input area to be selected
+ */
+ SELECT_ALL: 1,
+
+ /**
+ * The whole input has changed - push the cursor to the end
+ */
+ TO_END: 2,
+
+ /**
+ * A part of the input has changed - push the cursor to the end of the
+ * changed section
+ */
+ TO_ARG_END: 3
+};
+
+/**
+ * If this._caretChange === Caret.TO_ARG_END, we alter the input object to move
+ * the selection start to the end of the current argument.
+ * @param input An object shaped like { typed:'', cursor: { start:0, end:0 }}
+ */
+Inputter.prototype._processCaretChange = function(input) {
+ var start, end;
+ switch (this._caretChange) {
+ case Caret.SELECT_ALL:
+ start = 0;
+ end = input.typed.length;
+ break;
+
+ case Caret.TO_END:
+ start = input.typed.length;
+ end = input.typed.length;
+ break;
+
+ case Caret.TO_ARG_END:
+ // There could be a fancy way to do this involving assignment/arg math
+ // but it doesn't seem easy, so we cheat a move the cursor to just before
+ // the next space, or the end of the input
+ start = input.cursor.start;
+ do {
+ start++;
+ }
+ while (start < input.typed.length && input.typed[start - 1] !== ' ');
+
+ end = start;
+ break;
+
+ default:
+ start = input.cursor.start;
+ end = input.cursor.end;
+ break;
+ }
+
+ start = (start > input.typed.length) ? input.typed.length : start;
+ end = (end > input.typed.length) ? input.typed.length : end;
+
+ var newInput = {
+ typed: input.typed,
+ cursor: { start: start, end: end }
+ };
+
+ if (this.element.selectionStart !== start) {
+ this.element.selectionStart = start;
+ }
+ if (this.element.selectionEnd !== end) {
+ this.element.selectionEnd = end;
+ }
+
+ this._checkAssignment(start);
+
+ this._caretChange = null;
+ return newInput;
+};
+
+/**
+ * To be called internally whenever we think that the current assignment might
+ * have changed, typically on mouse-clicks or key presses.
+ * @param start Optional - if specified, the cursor position to use in working
+ * out the current assignment. This is needed because setting the element
+ * selection start is only recognised when the event loop has finished
+ */
+Inputter.prototype._checkAssignment = function(start) {
+ if (start == null) {
+ start = this.element.selectionStart;
+ }
+ if (!this.requisition.isUpToDate()) {
+ return;
+ }
+ var newAssignment = this.requisition.getAssignmentAt(start);
+ if (newAssignment == null) {
+ return;
+ }
+ if (this.assignment !== newAssignment) {
+ if (this.assignment.param.type.onLeave) {
+ this.assignment.param.type.onLeave(this.assignment);
+ }
+
+ this.assignment = newAssignment;
+ this.onAssignmentChange({ assignment: this.assignment });
+
+ if (this.assignment.param.type.onEnter) {
+ this.assignment.param.type.onEnter(this.assignment);
+ }
+ }
+ else {
+ if (this.assignment && this.assignment.param.type.onChange) {
+ this.assignment.param.type.onChange(this.assignment);
+ }
+ }
+
+ // This is slightly nasty - the focusManager generally relies on people
+ // telling it what it needs to know (which makes sense because the event
+ // system to do it with events would be unnecessarily complex). However
+ // requisition doesn't know about the focusManager either. So either one
+ // needs to know about the other, or a third-party needs to break the
+ // deadlock. These 2 lines are all we're quibbling about, so for now we hack
+ if (this.focusManager) {
+ var error = (this.assignment.status === Status.ERROR);
+ this.focusManager.setError(error);
+ }
+};
+
+/**
+ * Set the input field to a value, for external use.
+ * This function updates the data model. It sets the caret to the end of the
+ * input. It does not make any similarity checks so calling this function with
+ * it's current value resets the cursor position.
+ * It does not execute the input or affect the history.
+ * This function should not be called internally, by Inputter and never as a
+ * result of a keyboard event on this.element or bug 676520 could be triggered.
+ */
+Inputter.prototype.setInput = function(str) {
+ this._caretChange = Caret.TO_END;
+ return this.requisition.update(str).then(function(updated) {
+ this.textChanged();
+ return updated;
+ }.bind(this));
+};
+
+/**
+ * Counterpart to |setInput| for moving the cursor.
+ * @param cursor An object shaped like { start: x, end: y }
+ */
+Inputter.prototype.setCursor = function(cursor) {
+ this._caretChange = Caret.NO_CHANGE;
+ this._processCaretChange({ typed: this.element.value, cursor: cursor });
+ return RESOLVED;
+};
+
+/**
+ * Focus the input element
+ */
+Inputter.prototype.focus = function() {
+ this.element.focus();
+ this._checkAssignment();
+};
+
+/**
+ * Ensure certain keys (arrows, tab, etc) that we would like to handle
+ * are not handled by the browser
+ */
+Inputter.prototype.onKeyDown = function(ev) {
+ if (ev.keyCode === KeyEvent.DOM_VK_UP || ev.keyCode === KeyEvent.DOM_VK_DOWN) {
+ ev.preventDefault();
+ return;
+ }
+
+ // The following keys do not affect the state of the command line so we avoid
+ // informing the focusManager about keyboard events that involve these keys
+ if (ev.keyCode === KeyEvent.DOM_VK_F1 ||
+ ev.keyCode === KeyEvent.DOM_VK_ESCAPE ||
+ ev.keyCode === KeyEvent.DOM_VK_UP ||
+ ev.keyCode === KeyEvent.DOM_VK_DOWN) {
+ return;
+ }
+
+ if (this.focusManager) {
+ this.focusManager.onInputChange();
+ }
+
+ if (ev.keyCode === KeyEvent.DOM_VK_TAB) {
+ this.lastTabDownAt = 0;
+ if (!ev.shiftKey) {
+ ev.preventDefault();
+ // Record the timestamp of this TAB down so onKeyUp can distinguish
+ // focus from TAB in the CLI.
+ this.lastTabDownAt = ev.timeStamp;
+ }
+ if (ev.metaKey || ev.altKey || ev.crtlKey) {
+ if (this.document.commandDispatcher) {
+ this.document.commandDispatcher.advanceFocus();
+ }
+ else {
+ this.element.blur();
+ }
+ }
+ }
+};
+
+/**
+ * Handler for use with DOM events, which just calls the promise enabled
+ * handleKeyUp function but checks the exit state of the promise so we know
+ * if something went wrong.
+ */
+Inputter.prototype.onKeyUp = function(ev) {
+ this.handleKeyUp(ev).catch(util.errorHandler);
+};
+
+/**
+ * The main keyboard processing loop
+ * @return A promise that resolves (to undefined) when the actions kicked off
+ * by this handler are completed.
+ */
+Inputter.prototype.handleKeyUp = function(ev) {
+ if (this.focusManager && ev.keyCode === KeyEvent.DOM_VK_F1) {
+ this.focusManager.helpRequest();
+ return RESOLVED;
+ }
+
+ if (this.focusManager && ev.keyCode === KeyEvent.DOM_VK_ESCAPE) {
+ this.focusManager.removeHelp();
+ return RESOLVED;
+ }
+
+ if (ev.keyCode === KeyEvent.DOM_VK_UP) {
+ return this._handleUpArrow();
+ }
+
+ if (ev.keyCode === KeyEvent.DOM_VK_DOWN) {
+ return this._handleDownArrow();
+ }
+
+ if (ev.keyCode === KeyEvent.DOM_VK_RETURN) {
+ return this._handleReturn();
+ }
+
+ if (ev.keyCode === KeyEvent.DOM_VK_TAB && !ev.shiftKey) {
+ return this._handleTab(ev);
+ }
+
+ if (this._previousValue === this.element.value) {
+ return RESOLVED;
+ }
+
+ this._scrollingThroughHistory = false;
+ this._caretChange = Caret.NO_CHANGE;
+
+ this._completed = this.requisition.update(this.element.value);
+ this._previousValue = this.element.value;
+
+ return this._completed.then(function() {
+ // Abort UI changes if this UI update has been overtaken
+ if (this._previousValue === this.element.value) {
+ this._choice = null;
+ this.textChanged();
+ this.onChoiceChange({ choice: this._choice });
+ }
+ }.bind(this));
+};
+
+/**
+ * See also _handleDownArrow for some symmetry
+ */
+Inputter.prototype._handleUpArrow = function() {
+ if (this.tooltip && this.tooltip.isMenuShowing) {
+ this.changeChoice(-1);
+ return RESOLVED;
+ }
+
+ if (this.element.value === '' || this._scrollingThroughHistory) {
+ this._scrollingThroughHistory = true;
+ return this.requisition.update(this.history.backward()).then(function(updated) {
+ this.textChanged();
+ return updated;
+ }.bind(this));
+ }
+
+ // If the user is on a valid value, then we increment the value, but if
+ // they've typed something that's not right we page through predictions
+ if (this.assignment.getStatus() === Status.VALID) {
+ return this.requisition.nudge(this.assignment, 1).then(function() {
+ // See notes on focusManager.onInputChange in onKeyDown
+ this.textChanged();
+ if (this.focusManager) {
+ this.focusManager.onInputChange();
+ }
+ }.bind(this));
+ }
+
+ this.changeChoice(-1);
+ return RESOLVED;
+};
+
+/**
+ * See also _handleUpArrow for some symmetry
+ */
+Inputter.prototype._handleDownArrow = function() {
+ if (this.tooltip && this.tooltip.isMenuShowing) {
+ this.changeChoice(+1);
+ return RESOLVED;
+ }
+
+ if (this.element.value === '' || this._scrollingThroughHistory) {
+ this._scrollingThroughHistory = true;
+ return this.requisition.update(this.history.forward()).then(function(updated) {
+ this.textChanged();
+ return updated;
+ }.bind(this));
+ }
+
+ // See notes above for the UP key
+ if (this.assignment.getStatus() === Status.VALID) {
+ return this.requisition.nudge(this.assignment, -1).then(function() {
+ // See notes on focusManager.onInputChange in onKeyDown
+ this.textChanged();
+ if (this.focusManager) {
+ this.focusManager.onInputChange();
+ }
+ }.bind(this));
+ }
+
+ this.changeChoice(+1);
+ return RESOLVED;
+};
+
+/**
+ * RETURN checks status and might exec
+ */
+Inputter.prototype._handleReturn = function() {
+ // Deny RETURN unless the command might work
+ if (this.requisition.status === Status.VALID) {
+ this._scrollingThroughHistory = false;
+ this.history.add(this.element.value);
+
+ return this.requisition.exec().then(function() {
+ this.textChanged();
+ }.bind(this));
+ }
+
+ // If we can't execute the command, but there is a menu choice to use
+ // then use it.
+ if (!this.tooltip.selectChoice()) {
+ this.focusManager.setError(true);
+ }
+
+ this._choice = null;
+ return RESOLVED;
+};
+
+/**
+ * Warning: We get TAB events for more than just the user pressing TAB in our
+ * input element.
+ */
+Inputter.prototype._handleTab = function(ev) {
+ // Being able to complete 'nothing' is OK if there is some context, but
+ // when there is nothing on the command line it just looks bizarre.
+ var hasContents = (this.element.value.length > 0);
+
+ // If the TAB keypress took the cursor from another field to this one,
+ // then they get the keydown/keypress, and we get the keyup. In this
+ // case we don't want to do any completion.
+ // If the time of the keydown/keypress of TAB was close (i.e. within
+ // 1 second) to the time of the keyup then we assume that we got them
+ // both, and do the completion.
+ if (hasContents && this.lastTabDownAt + 1000 > ev.timeStamp) {
+ // It's possible for TAB to not change the input, in which case the caret
+ // move will not be processed. So we check that this is done first
+ this._caretChange = Caret.TO_ARG_END;
+ var inputState = this.getInputState();
+ this._processCaretChange(inputState);
+
+ if (this._choice == null) {
+ this._choice = 0;
+ }
+
+ // The changes made by complete may happen asynchronously, so after the
+ // the call to complete() we should avoid making changes before the end
+ // of the event loop
+ this._completed = this.requisition.complete(inputState.cursor,
+ this._choice);
+ this._previousValue = this.element.value;
+ }
+ this.lastTabDownAt = 0;
+ this._scrollingThroughHistory = false;
+
+ return this._completed.then(function(updated) {
+ // Abort UI changes if this UI update has been overtaken
+ if (updated) {
+ this.textChanged();
+ this._choice = null;
+ this.onChoiceChange({ choice: this._choice });
+ }
+ }.bind(this));
+};
+
+/**
+ * Used by onKeyUp for UP/DOWN to change the current choice from an options
+ * menu.
+ */
+Inputter.prototype.changeChoice = function(amount) {
+ if (this._choice == null) {
+ this._choice = 0;
+ }
+ // There's an annoying up is down thing here, the menu is presented
+ // with the zeroth index at the top working down, so the UP arrow needs
+ // pick the choice below because we're working down
+ this._choice += amount;
+ this.onChoiceChange({ choice: this._choice });
+};
+
+/**
+ * Pull together an input object, which may include XUL hacks
+ */
+Inputter.prototype.getInputState = function() {
+ var input = {
+ typed: this.element.value,
+ cursor: {
+ start: this.element.selectionStart,
+ end: this.element.selectionEnd
+ }
+ };
+
+ // Workaround for potential XUL bug 676520 where textbox gives incorrect
+ // values for its content
+ if (input.typed == null) {
+ input = { typed: '', cursor: { start: 0, end: 0 } };
+ }
+
+ return input;
+};
+
+exports.Inputter = Inputter;
diff --git a/devtools/shared/gcli/source/lib/gcli/mozui/moz.build b/devtools/shared/gcli/source/lib/gcli/mozui/moz.build
new file mode 100644
index 000000000..af76e0d99
--- /dev/null
+++ b/devtools/shared/gcli/source/lib/gcli/mozui/moz.build
@@ -0,0 +1,11 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+DevToolsModules(
+ 'completer.js',
+ 'inputter.js',
+ 'tooltip.js',
+)
diff --git a/devtools/shared/gcli/source/lib/gcli/mozui/tooltip.js b/devtools/shared/gcli/source/lib/gcli/mozui/tooltip.js
new file mode 100644
index 000000000..f72900a80
--- /dev/null
+++ b/devtools/shared/gcli/source/lib/gcli/mozui/tooltip.js
@@ -0,0 +1,298 @@
+/*
+ * 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 domtemplate = require('../util/domtemplate');
+
+var CommandAssignment = require('../cli').CommandAssignment;
+
+var tooltipHtml =
+ '<div class="gcli-tt" aria-live="polite">\n' +
+ ' <div class="gcli-tt-description" save="${descriptionEle}">${description}</div>\n' +
+ ' ${field.element}\n' +
+ ' <div class="gcli-tt-error" save="${errorEle}">${assignment.conversion.message}</div>\n' +
+ ' <div class="gcli-tt-highlight" save="${highlightEle}"></div>\n' +
+ '</div>';
+
+/**
+ * A widget to display an inline dialog which allows the user to fill out
+ * the arguments to a command.
+ * @param components Object that links to other UI components. GCLI provided:
+ * - requisition: The Requisition to fill out
+ * - inputter: An instance of Inputter
+ * - focusManager: Component to manage hiding/showing this element
+ * - panelElement (optional): The element to show/hide on visibility events
+ * - element: The root element to populate
+ */
+function Tooltip(components) {
+ this.inputter = components.inputter;
+ this.requisition = components.requisition;
+ this.focusManager = components.focusManager;
+
+ this.element = components.element;
+ this.element.classList.add('gcliterm-tooltip');
+ this.document = this.element.ownerDocument;
+
+ this.panelElement = components.panelElement;
+ if (this.panelElement) {
+ this.panelElement.classList.add('gcli-panel-hide');
+ this.focusManager.onVisibilityChange.add(this.visibilityChanged, this);
+ }
+ this.focusManager.addMonitoredElement(this.element, 'tooltip');
+
+ // We cache the fields we create so we can destroy them later
+ this.fields = [];
+
+ this.template = host.toDom(this.document, tooltipHtml);
+ this.templateOptions = { blankNullUndefined: true, stack: 'tooltip.html' };
+
+ this.inputter.onChoiceChange.add(this.choiceChanged, this);
+ this.inputter.onAssignmentChange.add(this.assignmentChanged, this);
+
+ // We keep a track of which assignment the cursor is in
+ this.assignment = undefined;
+ this.assignmentChanged({ assignment: this.inputter.assignment });
+
+ // We also keep track of the last known arg text for the current assignment
+ this.lastText = undefined;
+}
+
+/**
+ * Avoid memory leaks
+ */
+Tooltip.prototype.destroy = function() {
+ this.inputter.onAssignmentChange.remove(this.assignmentChanged, this);
+ this.inputter.onChoiceChange.remove(this.choiceChanged, this);
+
+ if (this.panelElement) {
+ this.focusManager.onVisibilityChange.remove(this.visibilityChanged, this);
+ }
+ this.focusManager.removeMonitoredElement(this.element, 'tooltip');
+
+ if (this.style) {
+ this.style.parentNode.removeChild(this.style);
+ this.style = undefined;
+ }
+
+ this.field.onFieldChange.remove(this.fieldChanged, this);
+ this.field.destroy();
+
+ this.lastText = undefined;
+ this.assignment = undefined;
+
+ this.errorEle = undefined;
+ this.descriptionEle = undefined;
+ this.highlightEle = undefined;
+
+ this.document = undefined;
+ this.element = undefined;
+ this.panelElement = undefined;
+ this.template = undefined;
+};
+
+/**
+ * The inputter acts on UP/DOWN if there is a menu showing
+ */
+Object.defineProperty(Tooltip.prototype, 'isMenuShowing', {
+ get: function() {
+ return this.focusManager.isTooltipVisible &&
+ this.field != null &&
+ this.field.menu != null;
+ },
+ enumerable: true
+});
+
+/**
+ * Called whenever the assignment that we're providing help with changes
+ */
+Tooltip.prototype.assignmentChanged = function(ev) {
+ // This can be kicked off either by requisition doing an assign or by
+ // inputter noticing a cursor movement out of a command, so we should check
+ // that this really is a new assignment
+ if (this.assignment === ev.assignment) {
+ return;
+ }
+
+ this.assignment = ev.assignment;
+ this.lastText = this.assignment.arg.text;
+
+ if (this.field) {
+ this.field.onFieldChange.remove(this.fieldChanged, this);
+ this.field.destroy();
+ }
+
+ this.field = this.requisition.system.fields.get(this.assignment.param.type, {
+ document: this.document,
+ requisition: this.requisition
+ });
+
+ this.focusManager.setImportantFieldFlag(this.field.isImportant);
+
+ this.field.onFieldChange.add(this.fieldChanged, this);
+ this.field.setConversion(this.assignment.conversion);
+
+ // Filled in by the template process
+ this.errorEle = undefined;
+ this.descriptionEle = undefined;
+ this.highlightEle = undefined;
+
+ var contents = this.template.cloneNode(true);
+ domtemplate.template(contents, this, this.templateOptions);
+ util.clearElement(this.element);
+ this.element.appendChild(contents);
+ this.element.style.display = 'block';
+
+ this.field.setMessageElement(this.errorEle);
+
+ this._updatePosition();
+};
+
+/**
+ * Forward the event to the current field
+ */
+Tooltip.prototype.choiceChanged = function(ev) {
+ if (this.field && this.field.menu) {
+ var conversion = this.assignment.conversion;
+ var context = this.requisition.executionContext;
+ conversion.constrainPredictionIndex(context, ev.choice).then(function(choice) {
+ this.field.menu._choice = choice;
+ this.field.menu._updateHighlight();
+ }.bind(this)).catch(util.errorHandler);
+ }
+};
+
+/**
+ * Allow the inputter to use RETURN to chose the current menu item when
+ * it can't execute the command line
+ * @return true if there was a selection to use, false otherwise
+ */
+Tooltip.prototype.selectChoice = function(ev) {
+ if (this.field && this.field.selectChoice) {
+ return this.field.selectChoice();
+ }
+ return false;
+};
+
+/**
+ * Called by the onFieldChange event on the current Field
+ */
+Tooltip.prototype.fieldChanged = function(ev) {
+ this.requisition.setAssignment(this.assignment, ev.conversion.arg,
+ { matchPadding: true });
+
+ var isError = ev.conversion.message != null && ev.conversion.message !== '';
+ this.focusManager.setError(isError);
+
+ // Nasty hack, the inputter won't know about the text change yet, so it will
+ // get it's calculations wrong. We need to wait until the current set of
+ // changes has had a chance to propagate
+ this.document.defaultView.setTimeout(function() {
+ this.inputter.focus();
+ }.bind(this), 10);
+};
+
+/**
+ * Called by the Inputter when the text changes
+ */
+Tooltip.prototype.textChanged = function() {
+ // We get here for minor things like whitespace change in arg prefix,
+ // so we ignore anything but an actual value change.
+ if (this.assignment.arg.text === this.lastText) {
+ return;
+ }
+
+ this.lastText = this.assignment.arg.text;
+
+ this.field.setConversion(this.assignment.conversion);
+ util.setTextContent(this.descriptionEle, this.description);
+
+ this._updatePosition();
+};
+
+/**
+ * Called to move the tooltip to the correct horizontal position
+ */
+Tooltip.prototype._updatePosition = function() {
+ var dimensions = this.getDimensionsOfAssignment();
+
+ // 10 is roughly the width of a char
+ if (this.panelElement) {
+ this.panelElement.style.left = (dimensions.start * 10) + 'px';
+ }
+
+ this.focusManager.updatePosition(dimensions);
+};
+
+/**
+ * Returns a object containing 'start' and 'end' properties which identify the
+ * number of pixels from the left hand edge of the input element that represent
+ * the text portion of the current assignment.
+ */
+Tooltip.prototype.getDimensionsOfAssignment = function() {
+ var before = '';
+ var assignments = this.requisition.getAssignments(true);
+ for (var i = 0; i < assignments.length; i++) {
+ if (assignments[i] === this.assignment) {
+ break;
+ }
+ before += assignments[i].toString();
+ }
+ before += this.assignment.arg.prefix;
+
+ var startChar = before.length;
+ before += this.assignment.arg.text;
+ var endChar = before.length;
+
+ return { start: startChar, end: endChar };
+};
+
+/**
+ * The description (displayed at the top of the hint area) should be blank if
+ * we're entering the CommandAssignment (because it's obvious) otherwise it's
+ * the parameter description.
+ */
+Object.defineProperty(Tooltip.prototype, 'description', {
+ get: function() {
+ if (this.assignment instanceof CommandAssignment &&
+ this.assignment.value == null) {
+ return '';
+ }
+
+ return this.assignment.param.manual || this.assignment.param.description;
+ },
+ enumerable: true
+});
+
+/**
+ * Tweak CSS to show/hide the output
+ */
+Tooltip.prototype.visibilityChanged = function(ev) {
+ if (!this.panelElement) {
+ return;
+ }
+
+ if (ev.tooltipVisible) {
+ this.panelElement.classList.remove('gcli-panel-hide');
+ }
+ else {
+ this.panelElement.classList.add('gcli-panel-hide');
+ }
+};
+
+exports.Tooltip = Tooltip;
diff --git a/devtools/shared/gcli/source/lib/gcli/settings.js b/devtools/shared/gcli/source/lib/gcli/settings.js
new file mode 100644
index 000000000..29e608cbd
--- /dev/null
+++ b/devtools/shared/gcli/source/lib/gcli/settings.js
@@ -0,0 +1,284 @@
+/*
+ * 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 imports = {};
+
+var Cc = require('chrome').Cc;
+var Ci = require('chrome').Ci;
+var Cu = require('chrome').Cu;
+
+var XPCOMUtils = Cu.import('resource://gre/modules/XPCOMUtils.jsm', {}).XPCOMUtils;
+var Services = require("Services");
+
+XPCOMUtils.defineLazyGetter(imports, 'prefBranch', function() {
+ var prefService = Cc['@mozilla.org/preferences-service;1']
+ .getService(Ci.nsIPrefService);
+ return prefService.getBranch(null).QueryInterface(Ci.nsIPrefBranch2);
+});
+
+XPCOMUtils.defineLazyGetter(imports, 'supportsString', function() {
+ return Cc['@mozilla.org/supports-string;1']
+ .createInstance(Ci.nsISupportsString);
+});
+
+var util = require('./util/util');
+
+/**
+ * All local settings have this prefix when used in Firefox
+ */
+var DEVTOOLS_PREFIX = 'devtools.gcli.';
+
+/**
+ * A manager for the registered Settings
+ */
+function Settings(types, settingValues) {
+ this._types = types;
+
+ if (settingValues != null) {
+ throw new Error('settingValues is not supported when writing to prefs');
+ }
+
+ // Collection of preferences for sorted access
+ this._settingsAll = [];
+
+ // Collection of preferences for fast indexed access
+ this._settingsMap = new Map();
+
+ // Flag so we know if we've read the system preferences
+ this._hasReadSystem = false;
+
+ // Event for use to detect when the list of settings changes
+ this.onChange = util.createEvent('Settings.onChange');
+}
+
+/**
+ * Load system prefs if they've not been loaded already
+ * @return true
+ */
+Settings.prototype._readSystem = function() {
+ if (this._hasReadSystem) {
+ return;
+ }
+
+ imports.prefBranch.getChildList('').forEach(function(name) {
+ var setting = new Setting(this, name);
+ this._settingsAll.push(setting);
+ this._settingsMap.set(name, setting);
+ }.bind(this));
+
+ this._settingsAll.sort(function(s1, s2) {
+ return s1.name.localeCompare(s2.name);
+ }.bind(this));
+
+ this._hasReadSystem = true;
+};
+
+/**
+ * Get an array containing all known Settings filtered to match the given
+ * filter (string) at any point in the name of the setting
+ */
+Settings.prototype.getAll = function(filter) {
+ this._readSystem();
+
+ if (filter == null) {
+ return this._settingsAll;
+ }
+
+ return this._settingsAll.filter(function(setting) {
+ return setting.name.indexOf(filter) !== -1;
+ }.bind(this));
+};
+
+/**
+ * Add a new setting
+ */
+Settings.prototype.add = function(prefSpec) {
+ var setting = new Setting(this, prefSpec);
+
+ if (this._settingsMap.has(setting.name)) {
+ // Once exists already, we're going to need to replace it in the array
+ for (var i = 0; i < this._settingsAll.length; i++) {
+ if (this._settingsAll[i].name === setting.name) {
+ this._settingsAll[i] = setting;
+ }
+ }
+ }
+
+ this._settingsMap.set(setting.name, setting);
+ this.onChange({ added: setting.name });
+
+ return setting;
+};
+
+/**
+ * Getter for an existing setting. Generally use of this function should be
+ * avoided. Systems that define a setting should export it if they wish it to
+ * be available to the outside, or not otherwise. Use of this function breaks
+ * that boundary and also hides dependencies. Acceptable uses include testing
+ * and embedded uses of GCLI that pre-define all settings (e.g. Firefox)
+ * @param name The name of the setting to fetch
+ * @return The found Setting object, or undefined if the setting was not found
+ */
+Settings.prototype.get = function(name) {
+ // We might be able to give the answer without needing to read all system
+ // settings if this is an internal setting
+ var found = this._settingsMap.get(name);
+ if (!found) {
+ found = this._settingsMap.get(DEVTOOLS_PREFIX + name);
+ }
+
+ if (found) {
+ return found;
+ }
+
+ if (this._hasReadSystem) {
+ return undefined;
+ }
+ else {
+ this._readSystem();
+ found = this._settingsMap.get(name);
+ if (!found) {
+ found = this._settingsMap.get(DEVTOOLS_PREFIX + name);
+ }
+ return found;
+ }
+};
+
+/**
+ * Remove a setting. A no-op in this case
+ */
+Settings.prototype.remove = function() {
+};
+
+exports.Settings = Settings;
+
+/**
+ * A class to wrap up the properties of a Setting.
+ * @see toolkit/components/viewconfig/content/config.js
+ */
+function Setting(settings, prefSpec) {
+ this._settings = settings;
+ if (typeof prefSpec === 'string') {
+ // We're coming from getAll() i.e. a full listing of prefs
+ this.name = prefSpec;
+ this.description = '';
+ }
+ else {
+ // A specific addition by GCLI
+ this.name = DEVTOOLS_PREFIX + prefSpec.name;
+
+ if (prefSpec.ignoreTypeDifference !== true && prefSpec.type) {
+ if (this.type.name !== prefSpec.type) {
+ throw new Error('Locally declared type (' + prefSpec.type + ') != ' +
+ 'Mozilla declared type (' + this.type.name + ') for ' + this.name);
+ }
+ }
+
+ this.description = prefSpec.description;
+ }
+
+ this.onChange = util.createEvent('Setting.onChange');
+}
+
+/**
+ * Reset this setting to it's initial default value
+ */
+Setting.prototype.setDefault = function() {
+ imports.prefBranch.clearUserPref(this.name);
+ Services.prefs.savePrefFile(null);
+};
+
+/**
+ * What type is this property: boolean/integer/string?
+ */
+Object.defineProperty(Setting.prototype, 'type', {
+ get: function() {
+ switch (imports.prefBranch.getPrefType(this.name)) {
+ case imports.prefBranch.PREF_BOOL:
+ return this._settings._types.createType('boolean');
+
+ case imports.prefBranch.PREF_INT:
+ return this._settings._types.createType('number');
+
+ case imports.prefBranch.PREF_STRING:
+ return this._settings._types.createType('string');
+
+ default:
+ throw new Error('Unknown type for ' + this.name);
+ }
+ },
+ enumerable: true
+});
+
+/**
+ * What type is this property: boolean/integer/string?
+ */
+Object.defineProperty(Setting.prototype, 'value', {
+ get: function() {
+ switch (imports.prefBranch.getPrefType(this.name)) {
+ case imports.prefBranch.PREF_BOOL:
+ return imports.prefBranch.getBoolPref(this.name);
+
+ case imports.prefBranch.PREF_INT:
+ return imports.prefBranch.getIntPref(this.name);
+
+ case imports.prefBranch.PREF_STRING:
+ var value = imports.prefBranch.getComplexValue(this.name,
+ Ci.nsISupportsString).data;
+ // In case of a localized string
+ if (/^chrome:\/\/.+\/locale\/.+\.properties/.test(value)) {
+ value = imports.prefBranch.getComplexValue(this.name,
+ Ci.nsIPrefLocalizedString).data;
+ }
+ return value;
+
+ default:
+ throw new Error('Invalid value for ' + this.name);
+ }
+ },
+
+ set: function(value) {
+ if (imports.prefBranch.prefIsLocked(this.name)) {
+ throw new Error('Locked preference ' + this.name);
+ }
+
+ switch (imports.prefBranch.getPrefType(this.name)) {
+ case imports.prefBranch.PREF_BOOL:
+ imports.prefBranch.setBoolPref(this.name, value);
+ break;
+
+ case imports.prefBranch.PREF_INT:
+ imports.prefBranch.setIntPref(this.name, value);
+ break;
+
+ case imports.prefBranch.PREF_STRING:
+ imports.supportsString.data = value;
+ imports.prefBranch.setComplexValue(this.name,
+ Ci.nsISupportsString,
+ imports.supportsString);
+ break;
+
+ default:
+ throw new Error('Invalid value for ' + this.name);
+ }
+
+ Services.prefs.savePrefFile(null);
+ },
+
+ enumerable: true
+});
diff --git a/devtools/shared/gcli/source/lib/gcli/system.js b/devtools/shared/gcli/source/lib/gcli/system.js
new file mode 100644
index 000000000..5a4719b8d
--- /dev/null
+++ b/devtools/shared/gcli/source/lib/gcli/system.js
@@ -0,0 +1,370 @@
+/*
+ * 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 Commands = require('./commands/commands').Commands;
+var Connectors = require('./connectors/connectors').Connectors;
+var Converters = require('./converters/converters').Converters;
+var Fields = require('./fields/fields').Fields;
+var Languages = require('./languages/languages').Languages;
+var Settings = require('./settings').Settings;
+var Types = require('./types/types').Types;
+
+/**
+ * This is the heart of the API that we expose to the outside.
+ * @param options Object that customizes how the system acts. Valid properties:
+ * - commands, connectors, converters, fields, languages, settings, types:
+ * Custom configured manager objects for these item types
+ * - location: a system with a location will ignore commands that don't have a
+ * matching runAt property. This is principly for client/server setups where
+ * we import commands from the server to the client, so a system with
+ * `{ location: 'client' }` will silently ignore commands with
+ * `{ runAt: 'server' }`. Any system without a location will accept commands
+ * with any runAt property (including none).
+ */
+exports.createSystem = function(options) {
+ options = options || {};
+ var location = options.location;
+
+ // The plural/singular thing may make you want to scream, but it allows us
+ // to say components[getItemType(item)], so a lookup here (and below) saves
+ // multiple lookups in the middle of the code
+ var components = {
+ connector: options.connectors || new Connectors(),
+ converter: options.converters || new Converters(),
+ field: options.fields || new Fields(),
+ language: options.languages || new Languages(),
+ type: options.types || new Types()
+ };
+ components.setting = new Settings(components.type);
+ components.command = new Commands(components.type, location);
+
+ var getItemType = function(item) {
+ if (item.item) {
+ return item.item;
+ }
+ // Some items are registered using the constructor so we need to check
+ // the prototype for the the type of the item
+ return (item.prototype && item.prototype.item) ?
+ item.prototype.item : 'command';
+ };
+
+ var addItem = function(item) {
+ try {
+ components[getItemType(item)].add(item);
+ }
+ catch (ex) {
+ if (item != null) {
+ console.error('While adding: ' + item.name);
+ }
+ throw ex;
+ }
+ };
+
+ var removeItem = function(item) {
+ components[getItemType(item)].remove(item);
+ };
+
+ /**
+ * loadableModules is a lookup of names to module loader functions (like
+ * the venerable 'require') to which we can pass a name and get back a
+ * JS object (or a promise of a JS object). This allows us to have custom
+ * loaders to get stuff from the filesystem etc.
+ */
+ var loadableModules = {};
+
+ /**
+ * loadedModules is a lookup by name of the things returned by the functions
+ * in loadableModules so we can track what we need to unload / reload.
+ */
+ var loadedModules = {};
+
+ var unloadModule = function(name) {
+ var existingModule = loadedModules[name];
+ if (existingModule != null) {
+ existingModule.items.forEach(removeItem);
+ }
+ delete loadedModules[name];
+ };
+
+ var loadModule = function(name) {
+ var existingModule = loadedModules[name];
+ unloadModule(name);
+
+ // And load the new items
+ try {
+ var loader = loadableModules[name];
+ return Promise.resolve(loader(name)).then(function(newModule) {
+ if (existingModule === newModule) {
+ return;
+ }
+
+ if (newModule == null) {
+ throw 'Module \'' + name + '\' not found';
+ }
+
+ if (newModule.items == null || typeof newModule.items.forEach !== 'function') {
+ console.log('Exported properties: ' + Object.keys(newModule).join(', '));
+ throw 'Module \'' + name + '\' has no \'items\' array export';
+ }
+
+ newModule.items.forEach(addItem);
+
+ loadedModules[name] = newModule;
+ });
+ }
+ catch (ex) {
+ console.error('Failed to load module ' + name + ': ' + ex);
+ console.error(ex.stack);
+
+ return Promise.resolve();
+ }
+ };
+
+ var pendingChanges = false;
+
+ var system = {
+ addItems: function(items) {
+ items.forEach(addItem);
+ },
+
+ removeItems: function(items) {
+ items.forEach(removeItem);
+ },
+
+ addItemsByModule: function(names, options) {
+ var promises = [];
+
+ options = options || {};
+ if (!options.delayedLoad) {
+ // We could be about to add many commands, just report the change once
+ this.commands.onCommandsChange.holdFire();
+ }
+
+ if (typeof names === 'string') {
+ names = [ names ];
+ }
+ names.forEach(function(name) {
+ if (options.loader == null) {
+ options.loader = function(name) {
+ return require(name);
+ };
+ }
+ loadableModules[name] = options.loader;
+
+ if (options.delayedLoad) {
+ pendingChanges = true;
+ }
+ else {
+ promises.push(loadModule(name).catch(console.error));
+ }
+ });
+
+ if (options.delayedLoad) {
+ return Promise.resolve();
+ }
+ else {
+ return Promise.all(promises).then(function() {
+ this.commands.onCommandsChange.resumeFire();
+ }.bind(this));
+ }
+ },
+
+ removeItemsByModule: function(name) {
+ this.commands.onCommandsChange.holdFire();
+
+ delete loadableModules[name];
+ unloadModule(name);
+
+ this.commands.onCommandsChange.resumeFire();
+ },
+
+ load: function() {
+ if (!pendingChanges) {
+ return Promise.resolve();
+ }
+ this.commands.onCommandsChange.holdFire();
+
+ // clone loadedModules, so we can remove what is left at the end
+ var modules = Object.keys(loadedModules).map(function(name) {
+ return loadedModules[name];
+ });
+
+ var promises = Object.keys(loadableModules).map(function(name) {
+ delete modules[name];
+ return loadModule(name).catch(console.error);
+ });
+
+ Object.keys(modules).forEach(unloadModule);
+ pendingChanges = false;
+
+ return Promise.all(promises).then(function() {
+ this.commands.onCommandsChange.resumeFire();
+ }.bind(this));
+ },
+
+ destroy: function() {
+ this.commands.onCommandsChange.holdFire();
+
+ Object.keys(loadedModules).forEach(function(name) {
+ unloadModule(name);
+ });
+
+ this.commands.onCommandsChange.resumeFire();
+ },
+
+ toString: function() {
+ return 'System [' +
+ 'commands:' + components.command.getAll().length + ', ' +
+ 'connectors:' + components.connector.getAll().length + ', ' +
+ 'converters:' + components.converter.getAll().length + ', ' +
+ 'fields:' + components.field.getAll().length + ', ' +
+ 'settings:' + components.setting.getAll().length + ', ' +
+ 'types:' + components.type.getTypeNames().length + ']';
+ }
+ };
+
+ Object.defineProperty(system, 'commands', {
+ get: function() { return components.command; },
+ enumerable: true
+ });
+
+ Object.defineProperty(system, 'connectors', {
+ get: function() { return components.connector; },
+ enumerable: true
+ });
+
+ Object.defineProperty(system, 'converters', {
+ get: function() { return components.converter; },
+ enumerable: true
+ });
+
+ Object.defineProperty(system, 'fields', {
+ get: function() { return components.field; },
+ enumerable: true
+ });
+
+ Object.defineProperty(system, 'languages', {
+ get: function() { return components.language; },
+ enumerable: true
+ });
+
+ Object.defineProperty(system, 'settings', {
+ get: function() { return components.setting; },
+ enumerable: true
+ });
+
+ Object.defineProperty(system, 'types', {
+ get: function() { return components.type; },
+ enumerable: true
+ });
+
+ return system;
+};
+
+/**
+ * Connect a local system with another at the other end of a connector
+ * @param system System to which we're adding commands
+ * @param front Front which allows access to the remote system from which we
+ * import commands
+ * @param customProps Array of strings specifying additional properties defined
+ * on remote commands that should be considered part of the metadata for the
+ * commands imported into the local system
+ */
+exports.connectFront = function(system, front, customProps) {
+ system._handleCommandsChanged = function() {
+ syncItems(system, front, customProps).catch(util.errorHandler);
+ };
+ front.on('commands-changed', system._handleCommandsChanged);
+
+ return syncItems(system, front, customProps);
+};
+
+/**
+ * Undo the effect of #connectFront
+ */
+exports.disconnectFront = function(system, front) {
+ front.off('commands-changed', system._handleCommandsChanged);
+ system._handleCommandsChanged = undefined;
+ removeItemsFromFront(system, front);
+};
+
+/**
+ * Remove the items in this system that came from a previous sync action, and
+ * re-add them. See connectFront() for explanation of properties
+ */
+function syncItems(system, front, customProps) {
+ return front.specs(customProps).then(function(specs) {
+ removeItemsFromFront(system, front);
+
+ var remoteItems = addLocalFunctions(specs, front);
+ system.addItems(remoteItems);
+
+ return system;
+ });
+};
+
+/**
+ * Take the data from the 'specs' command (or the 'commands-changed' event) and
+ * add function to proxy the execution back over the front
+ */
+function addLocalFunctions(specs, front) {
+ // Inject an 'exec' function into the commands, and the front into
+ // all the remote types
+ specs.forEach(function(commandSpec) {
+ // HACK: Tack the front to the command so we know how to remove it
+ // in removeItemsFromFront() below
+ commandSpec.front = front;
+
+ // Tell the type instances for a command how to contact their counterparts
+ // Don't confuse this with setting the front on the commandSpec which is
+ // about associating a proxied command with it's source for later removal.
+ // This is actually going to be used by the type
+ commandSpec.params.forEach(function(param) {
+ if (typeof param.type !== 'string') {
+ param.type.front = front;
+ }
+ });
+
+ if (!commandSpec.isParent) {
+ commandSpec.exec = function(args, context) {
+ var typed = (context.prefix ? context.prefix + ' ' : '') + context.typed;
+ return front.execute(typed).then(function(reply) {
+ var typedData = context.typedData(reply.type, reply.data);
+ return reply.isError ? Promise.reject(typedData) : typedData;
+ });
+ };
+ }
+
+ commandSpec.isProxy = true;
+ });
+
+ return specs;
+}
+
+/**
+ * Go through all the commands removing any that are associated with the
+ * given front. The method of association is the hack in addLocalFunctions.
+ */
+function removeItemsFromFront(system, front) {
+ system.commands.getAll().forEach(function(command) {
+ if (command.front === front) {
+ system.commands.remove(command);
+ }
+ });
+}
diff --git a/devtools/shared/gcli/source/lib/gcli/types/array.js b/devtools/shared/gcli/source/lib/gcli/types/array.js
new file mode 100644
index 000000000..381bd0b80
--- /dev/null
+++ b/devtools/shared/gcli/source/lib/gcli/types/array.js
@@ -0,0 +1,80 @@
+/*
+ * 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 ArrayConversion = require('./types').ArrayConversion;
+var ArrayArgument = require('./types').ArrayArgument;
+
+exports.items = [
+ {
+ // A set of objects of the same type
+ item: 'type',
+ name: 'array',
+ subtype: undefined,
+
+ constructor: function() {
+ if (!this.subtype) {
+ console.error('Array.typeSpec is missing subtype. Assuming string.' +
+ this.name);
+ this.subtype = 'string';
+ }
+ this.subtype = this.types.createType(this.subtype);
+ },
+
+ getSpec: function(commandName, paramName) {
+ return {
+ name: 'array',
+ subtype: this.subtype.getSpec(commandName, paramName),
+ };
+ },
+
+ stringify: function(values, context) {
+ if (values == null) {
+ return '';
+ }
+ // BUG 664204: Check for strings with spaces and add quotes
+ return values.join(' ');
+ },
+
+ parse: function(arg, context) {
+ if (arg.type !== 'ArrayArgument') {
+ console.error('non ArrayArgument to ArrayType.parse', arg);
+ throw new Error('non ArrayArgument to ArrayType.parse');
+ }
+
+ // Parse an argument to a conversion
+ // Hack alert. ArrayConversion needs to be able to answer questions about
+ // the status of individual conversions in addition to the overall state.
+ // |subArg.conversion| allows us to do that easily.
+ var subArgParse = function(subArg) {
+ return this.subtype.parse(subArg, context).then(function(conversion) {
+ subArg.conversion = conversion;
+ return conversion;
+ }.bind(this));
+ }.bind(this);
+
+ var conversionPromises = arg.getArguments().map(subArgParse);
+ return Promise.all(conversionPromises).then(function(conversions) {
+ return new ArrayConversion(conversions, arg);
+ });
+ },
+
+ getBlank: function(context) {
+ return new ArrayConversion([], new ArrayArgument());
+ }
+ },
+];
diff --git a/devtools/shared/gcli/source/lib/gcli/types/boolean.js b/devtools/shared/gcli/source/lib/gcli/types/boolean.js
new file mode 100644
index 000000000..01f5f5022
--- /dev/null
+++ b/devtools/shared/gcli/source/lib/gcli/types/boolean.js
@@ -0,0 +1,62 @@
+/*
+ * 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 Status = require('./types').Status;
+var Conversion = require('./types').Conversion;
+var BlankArgument = require('./types').BlankArgument;
+var SelectionType = require('./selection').SelectionType;
+
+exports.items = [
+ {
+ // 'boolean' type
+ item: 'type',
+ name: 'boolean',
+ parent: 'selection',
+
+ getSpec: function() {
+ return 'boolean';
+ },
+
+ lookup: [
+ { name: 'false', value: false },
+ { name: 'true', value: true }
+ ],
+
+ parse: function(arg, context) {
+ if (arg.type === 'TrueNamedArgument') {
+ return Promise.resolve(new Conversion(true, arg));
+ }
+ if (arg.type === 'FalseNamedArgument') {
+ return Promise.resolve(new Conversion(false, arg));
+ }
+ return SelectionType.prototype.parse.call(this, arg, context);
+ },
+
+ stringify: function(value, context) {
+ if (value == null) {
+ return '';
+ }
+ return '' + value;
+ },
+
+ getBlank: function(context) {
+ return new Conversion(false, new BlankArgument(), Status.VALID, '',
+ Promise.resolve(this.lookup));
+ }
+ }
+];
diff --git a/devtools/shared/gcli/source/lib/gcli/types/command.js b/devtools/shared/gcli/source/lib/gcli/types/command.js
new file mode 100644
index 000000000..779aa77ab
--- /dev/null
+++ b/devtools/shared/gcli/source/lib/gcli/types/command.js
@@ -0,0 +1,255 @@
+/*
+ * 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 l10n = require('../util/l10n');
+var spell = require('../util/spell');
+var SelectionType = require('./selection').SelectionType;
+var Status = require('./types').Status;
+var Conversion = require('./types').Conversion;
+var cli = require('../cli');
+
+exports.items = [
+ {
+ // Select from the available parameters to a command
+ item: 'type',
+ name: 'param',
+ parent: 'selection',
+ stringifyProperty: 'name',
+ requisition: undefined,
+ isIncompleteName: undefined,
+
+ getSpec: function() {
+ throw new Error('param type is not remotable');
+ },
+
+ lookup: function() {
+ return exports.getDisplayedParamLookup(this.requisition);
+ },
+
+ parse: function(arg, context) {
+ if (this.isIncompleteName) {
+ return SelectionType.prototype.parse.call(this, arg, context);
+ }
+ else {
+ var message = l10n.lookup('cliUnusedArg');
+ return Promise.resolve(new Conversion(undefined, arg, Status.ERROR, message));
+ }
+ }
+ },
+ {
+ // Select from the available commands
+ // This is very similar to a SelectionType, however the level of hackery in
+ // SelectionType to make it handle Commands correctly was to high, so we
+ // simplified.
+ // If you are making changes to this code, you should check there too.
+ item: 'type',
+ name: 'command',
+ parent: 'selection',
+ stringifyProperty: 'name',
+ allowNonExec: true,
+
+ getSpec: function() {
+ return {
+ name: 'command',
+ allowNonExec: this.allowNonExec
+ };
+ },
+
+ lookup: function(context) {
+ var commands = cli.getMapping(context).requisition.system.commands;
+ return exports.getCommandLookup(commands);
+ },
+
+ parse: function(arg, context) {
+ var conversion = exports.parse(context, arg, this.allowNonExec);
+ return Promise.resolve(conversion);
+ }
+ }
+];
+
+exports.getDisplayedParamLookup = function(requisition) {
+ var displayedParams = [];
+ var command = requisition.commandAssignment.value;
+ if (command != null) {
+ command.params.forEach(function(param) {
+ var arg = requisition.getAssignment(param.name).arg;
+ if (!param.isPositionalAllowed && arg.type === 'BlankArgument') {
+ displayedParams.push({ name: '--' + param.name, value: param });
+ }
+ });
+ }
+ return displayedParams;
+};
+
+exports.parse = function(context, arg, allowNonExec) {
+ var commands = cli.getMapping(context).requisition.system.commands;
+ var lookup = exports.getCommandLookup(commands);
+ var predictions = exports.findPredictions(arg, lookup);
+ return exports.convertPredictions(commands, arg, allowNonExec, predictions);
+};
+
+exports.getCommandLookup = function(commands) {
+ var sorted = commands.getAll().sort(function(c1, c2) {
+ return c1.name.localeCompare(c2.name);
+ });
+ return sorted.map(function(command) {
+ return { name: command.name, value: command };
+ });
+};
+
+exports.findPredictions = function(arg, lookup) {
+ var predictions = [];
+ var i, option;
+ var maxPredictions = Conversion.maxPredictions;
+ var match = arg.text.toLowerCase();
+
+ // Add an option to our list of predicted options
+ var addToPredictions = function(option) {
+ if (arg.text.length === 0) {
+ // If someone hasn't typed anything, we only show top level commands in
+ // the menu. i.e. sub-commands (those with a space in their name) are
+ // excluded. We do this to keep the list at an overview level.
+ if (option.name.indexOf(' ') === -1) {
+ predictions.push(option);
+ }
+ }
+ else {
+ // If someone has typed something, then we exclude parent commands
+ // (those without an exec). We do this because the user is drilling
+ // down and doesn't need the summary level.
+ if (option.value.exec != null) {
+ predictions.push(option);
+ }
+ }
+ };
+
+ // If the arg has a suffix then we're kind of 'done'. Only an exact
+ // match will do.
+ if (arg.suffix.match(/ +/)) {
+ for (i = 0; i < lookup.length && predictions.length < maxPredictions; i++) {
+ option = lookup[i];
+ if (option.name === arg.text ||
+ option.name.indexOf(arg.text + ' ') === 0) {
+ addToPredictions(option);
+ }
+ }
+
+ return predictions;
+ }
+
+ // Cache lower case versions of all the option names
+ for (i = 0; i < lookup.length; i++) {
+ option = lookup[i];
+ if (option._gcliLowerName == null) {
+ option._gcliLowerName = option.name.toLowerCase();
+ }
+ }
+
+ // Exact hidden matches. If 'hidden: true' then we only allow exact matches
+ // All the tests after here check that !option.value.hidden
+ for (i = 0; i < lookup.length && predictions.length < maxPredictions; i++) {
+ option = lookup[i];
+ if (option.name === arg.text) {
+ addToPredictions(option);
+ }
+ }
+
+ // Start with prefix matching
+ for (i = 0; i < lookup.length && predictions.length < maxPredictions; i++) {
+ option = lookup[i];
+ if (option._gcliLowerName.indexOf(match) === 0 && !option.value.hidden) {
+ if (predictions.indexOf(option) === -1) {
+ addToPredictions(option);
+ }
+ }
+ }
+
+ // Try infix matching if we get less half max matched
+ if (predictions.length < (maxPredictions / 2)) {
+ for (i = 0; i < lookup.length && predictions.length < maxPredictions; i++) {
+ option = lookup[i];
+ if (option._gcliLowerName.indexOf(match) !== -1 && !option.value.hidden) {
+ if (predictions.indexOf(option) === -1) {
+ addToPredictions(option);
+ }
+ }
+ }
+ }
+
+ // Try fuzzy matching if we don't get a prefix match
+ if (predictions.length === 0) {
+ var names = [];
+ lookup.forEach(function(opt) {
+ if (!opt.value.hidden) {
+ names.push(opt.name);
+ }
+ });
+ var corrected = spell.correct(match, names);
+ if (corrected) {
+ lookup.forEach(function(opt) {
+ if (opt.name === corrected) {
+ predictions.push(opt);
+ }
+ });
+ }
+ }
+
+ return predictions;
+};
+
+exports.convertPredictions = function(commands, arg, allowNonExec, predictions) {
+ var command = commands.get(arg.text);
+ // Helper function - Commands like 'context' work best with parent
+ // commands which are not executable. However obviously to execute a
+ // command, it needs an exec function.
+ var execWhereNeeded = (allowNonExec ||
+ (command != null && typeof command.exec === 'function'));
+
+ var isExact = command && command.name === arg.text &&
+ execWhereNeeded && predictions.length === 1;
+ var alternatives = isExact ? [] : predictions;
+
+ if (command) {
+ var status = execWhereNeeded ? Status.VALID : Status.INCOMPLETE;
+ return new Conversion(command, arg, status, '', alternatives);
+ }
+
+ if (predictions.length === 0) {
+ var msg = l10n.lookupFormat('typesSelectionNomatch', [ arg.text ]);
+ return new Conversion(undefined, arg, Status.ERROR, msg, alternatives);
+ }
+
+ command = predictions[0].value;
+
+ if (predictions.length === 1) {
+ // Is it an exact match of an executable command,
+ // or just the only possibility?
+ if (command.name === arg.text && execWhereNeeded) {
+ return new Conversion(command, arg, Status.VALID, '');
+ }
+
+ return new Conversion(undefined, arg, Status.INCOMPLETE, '', alternatives);
+ }
+
+ // It's valid if the text matches, even if there are several options
+ if (predictions[0].name === arg.text) {
+ return new Conversion(command, arg, Status.VALID, '', alternatives);
+ }
+
+ return new Conversion(undefined, arg, Status.INCOMPLETE, '', alternatives);
+};
diff --git a/devtools/shared/gcli/source/lib/gcli/types/date.js b/devtools/shared/gcli/source/lib/gcli/types/date.js
new file mode 100644
index 000000000..f05569724
--- /dev/null
+++ b/devtools/shared/gcli/source/lib/gcli/types/date.js
@@ -0,0 +1,248 @@
+/*
+ * 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 l10n = require('../util/l10n');
+var Status = require('./types').Status;
+var Conversion = require('./types').Conversion;
+
+/**
+ * Helper for stringify() to left pad a single digit number with a single '0'
+ * so 1 -> '01', 42 -> '42', etc.
+ */
+function pad(number) {
+ var r = String(number);
+ return r.length === 1 ? '0' + r : r;
+}
+
+/**
+ * Utility to convert a string to a date, throwing if the date can't be
+ * parsed rather than having an invalid date
+ */
+function toDate(str) {
+ var millis = Date.parse(str);
+ if (isNaN(millis)) {
+ throw new Error(l10n.lookupFormat('typesDateNan', [ str ]));
+ }
+ return new Date(millis);
+}
+
+/**
+ * Is |thing| a valid date?
+ * @see http://stackoverflow.com/questions/1353684/detecting-an-invalid-date-date-instance-in-javascript
+ */
+function isDate(thing) {
+ return Object.prototype.toString.call(thing) === '[object Date]'
+ && !isNaN(thing.getTime());
+}
+
+exports.items = [
+ {
+ // ECMA 5.1 §15.9.1.1
+ // @see http://stackoverflow.com/questions/11526504/minimum-and-maximum-date
+ item: 'type',
+ name: 'date',
+ step: 1,
+ min: new Date(-8640000000000000),
+ max: new Date(8640000000000000),
+
+ constructor: function() {
+ this._origMin = this.min;
+ if (this.min != null) {
+ if (typeof this.min === 'string') {
+ this.min = toDate(this.min);
+ }
+ else if (isDate(this.min) || typeof this.min === 'function') {
+ this.min = this.min;
+ }
+ else {
+ throw new Error('date min value must be one of string/date/function');
+ }
+ }
+
+ this._origMax = this.max;
+ if (this.max != null) {
+ if (typeof this.max === 'string') {
+ this.max = toDate(this.max);
+ }
+ else if (isDate(this.max) || typeof this.max === 'function') {
+ this.max = this.max;
+ }
+ else {
+ throw new Error('date max value must be one of string/date/function');
+ }
+ }
+ },
+
+ getSpec: function() {
+ var spec = {
+ name: 'date'
+ };
+ if (this.step !== 1) {
+ spec.step = this.step;
+ }
+ if (this._origMax != null) {
+ spec.max = this._origMax;
+ }
+ if (this._origMin != null) {
+ spec.min = this._origMin;
+ }
+ return spec;
+ },
+
+ stringify: function(value, context) {
+ if (!isDate(value)) {
+ return '';
+ }
+
+ var str = pad(value.getFullYear()) + '-' +
+ pad(value.getMonth() + 1) + '-' +
+ pad(value.getDate());
+
+ // Only add in the time if it's not midnight
+ if (value.getHours() !== 0 || value.getMinutes() !== 0 ||
+ value.getSeconds() !== 0 || value.getMilliseconds() !== 0) {
+
+ // What string should we use to separate the date from the time?
+ // There are 3 options:
+ // 'T': This is the standard from ISO8601. i.e. 2013-05-20T11:05
+ // The good news - it's a standard. The bad news - it's weird and
+ // alien to many if not most users
+ // ' ': This looks nicest, but needs escaping (which GCLI will do
+ // automatically) so it would look like: '2013-05-20 11:05'
+ // Good news: looks best, bad news: on completion we place the
+ // cursor after the final ', breaking repeated increment/decrement
+ // '\ ': It's possible that we could find a way to use a \ to escape
+ // the space, so the output would look like: 2013-05-20\ 11:05
+ // This would involve changes to a number of parts, and is
+ // probably too complex a solution for this problem for now
+ // In the short term I'm going for ' ', and raising the priority of
+ // cursor positioning on actions like increment/decrement/tab.
+
+ str += ' ' + pad(value.getHours());
+ str += ':' + pad(value.getMinutes());
+
+ // Only add in seconds/milliseconds if there is anything to report
+ if (value.getSeconds() !== 0 || value.getMilliseconds() !== 0) {
+ str += ':' + pad(value.getSeconds());
+ if (value.getMilliseconds() !== 0) {
+ var milliVal = (value.getUTCMilliseconds() / 1000).toFixed(3);
+ str += '.' + String(milliVal).slice(2, 5);
+ }
+ }
+ }
+
+ return str;
+ },
+
+ getMax: function(context) {
+ if (typeof this.max === 'function') {
+ return this._max(context);
+ }
+ if (isDate(this.max)) {
+ return this.max;
+ }
+ return undefined;
+ },
+
+ getMin: function(context) {
+ if (typeof this.min === 'function') {
+ return this._min(context);
+ }
+ if (isDate(this.min)) {
+ return this.min;
+ }
+ return undefined;
+ },
+
+ parse: function(arg, context) {
+ var value;
+
+ if (arg.text.replace(/\s/g, '').length === 0) {
+ return Promise.resolve(new Conversion(undefined, arg, Status.INCOMPLETE, ''));
+ }
+
+ // Lots of room for improvement here: 1h ago, in two days, etc.
+ // Should "1h ago" dynamically update the step?
+ if (arg.text.toLowerCase() === 'now' ||
+ arg.text.toLowerCase() === 'today') {
+ value = new Date();
+ }
+ else if (arg.text.toLowerCase() === 'yesterday') {
+ value = new Date();
+ value.setDate(value.getDate() - 1);
+ }
+ else if (arg.text.toLowerCase() === 'tomorrow') {
+ value = new Date();
+ value.setDate(value.getDate() + 1);
+ }
+ else {
+ // So now actual date parsing.
+ // Javascript dates are a mess. Like the default date libraries in most
+ // common languages, but with added browser weirdness.
+ // There is an argument for saying that the user will expect dates to
+ // be formatted as JavaScript dates, except that JS dates are of
+ // themselves very unexpected.
+ // See http://blog.dygraphs.com/2012/03/javascript-and-dates-what-mess.html
+
+ // The timezone used by Date.parse depends on whether or not the string
+ // can be interpreted as ISO-8601, so "2000-01-01" is not the same as
+ // "2000/01/01" (unless your TZ aligns with UTC) because the first is
+ // ISO-8601 and therefore assumed to be UTC, where the latter is
+ // assumed to be in the local timezone.
+
+ // First, if the user explicitly includes a 'Z' timezone marker, then
+ // we assume they know what they are doing with timezones. ISO-8601
+ // uses 'Z' as a marker for 'Zulu time', zero hours offset i.e. UTC
+ if (arg.text.indexOf('Z') !== -1) {
+ value = new Date(arg.text);
+ }
+ else {
+ // Now we don't want the browser to assume ISO-8601 and therefore use
+ // UTC so we replace the '-' with '/'
+ value = new Date(arg.text.replace(/-/g, '/'));
+ }
+
+ if (isNaN(value.getTime())) {
+ var msg = l10n.lookupFormat('typesDateNan', [ arg.text ]);
+ return Promise.resolve(new Conversion(undefined, arg, Status.ERROR, msg));
+ }
+ }
+
+ return Promise.resolve(new Conversion(value, arg));
+ },
+
+ nudge: function(value, by, context) {
+ if (!isDate(value)) {
+ return new Date();
+ }
+
+ var newValue = new Date(value);
+ newValue.setDate(value.getDate() + (by * this.step));
+
+ if (newValue < this.getMin(context)) {
+ return this.getMin(context);
+ }
+ else if (newValue > this.getMax(context)) {
+ return this.getMax();
+ }
+ else {
+ return newValue;
+ }
+ }
+ }
+];
diff --git a/devtools/shared/gcli/source/lib/gcli/types/delegate.js b/devtools/shared/gcli/source/lib/gcli/types/delegate.js
new file mode 100644
index 000000000..978718231
--- /dev/null
+++ b/devtools/shared/gcli/source/lib/gcli/types/delegate.js
@@ -0,0 +1,158 @@
+/*
+ * 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 Conversion = require('./types').Conversion;
+var Status = require('./types').Status;
+var BlankArgument = require('./types').BlankArgument;
+
+/**
+ * The types we expose for registration
+ */
+exports.items = [
+ // A type for "we don't know right now, but hope to soon"
+ {
+ item: 'type',
+ name: 'delegate',
+
+ getSpec: function(commandName, paramName) {
+ return {
+ name: 'delegate',
+ param: paramName
+ };
+ },
+
+ // Child types should implement this method to return an instance of the type
+ // that should be used. If no type is available, or some sort of temporary
+ // placeholder is required, BlankType can be used.
+ delegateType: undefined,
+
+ stringify: function(value, context) {
+ return this.getType(context).then(function(delegated) {
+ return delegated.stringify(value, context);
+ }.bind(this));
+ },
+
+ parse: function(arg, context) {
+ return this.getType(context).then(function(delegated) {
+ return delegated.parse(arg, context);
+ }.bind(this));
+ },
+
+ nudge: function(value, by, context) {
+ return this.getType(context).then(function(delegated) {
+ return delegated.nudge ?
+ delegated.nudge(value, by, context) :
+ undefined;
+ }.bind(this));
+ },
+
+ getType: function(context) {
+ if (this.delegateType === undefined) {
+ return Promise.resolve(this.types.createType('blank'));
+ }
+
+ var type = this.delegateType(context);
+ if (typeof type.parse !== 'function') {
+ type = this.types.createType(type);
+ }
+ return Promise.resolve(type);
+ },
+
+ // DelegateType is designed to be inherited from, so DelegateField needs a
+ // way to check if something works like a delegate without using 'name'
+ isDelegate: true,
+
+ // Technically we perhaps should proxy this, except that properties are
+ // inherently synchronous, so we can't. It doesn't seem important enough to
+ // change the function definition to accommodate this right now
+ isImportant: false
+ },
+ {
+ item: 'type',
+ name: 'remote',
+ paramName: undefined,
+ blankIsValid: false,
+ hasPredictions: true,
+
+ getSpec: function(commandName, paramName) {
+ return {
+ name: 'remote',
+ commandName: commandName,
+ paramName: paramName,
+ blankIsValid: this.blankIsValid
+ };
+ },
+
+ getBlank: function(context) {
+ if (this.blankIsValid) {
+ return new Conversion({ stringified: '' },
+ new BlankArgument(), Status.VALID);
+ }
+ else {
+ return new Conversion(undefined, new BlankArgument(),
+ Status.INCOMPLETE, '');
+ }
+ },
+
+ stringify: function(value, context) {
+ if (value == null) {
+ return '';
+ }
+ // remote types are client only, and we don't attempt to transfer value
+ // objects to the client (we can't be sure the are jsonable) so it is a
+ // bit strange to be asked to stringify a value object, however since
+ // parse creates a Conversion with a (fake) value object we might be
+ // asked to stringify that. We can stringify fake value objects.
+ if (typeof value.stringified === 'string') {
+ return value.stringified;
+ }
+ throw new Error('Can\'t stringify that value');
+ },
+
+ parse: function(arg, context) {
+ return this.front.parseType(context.typed, this.paramName).then(function(json) {
+ var status = Status.fromString(json.status);
+ return new Conversion(undefined, arg, status, json.message, json.predictions);
+ }.bind(this));
+ },
+
+ nudge: function(value, by, context) {
+ return this.front.nudgeType(context.typed, by, this.paramName).then(function(json) {
+ return { stringified: json.arg };
+ }.bind(this));
+ }
+ },
+ // 'blank' is a type for use with DelegateType when we don't know yet.
+ // It should not be used anywhere else.
+ {
+ item: 'type',
+ name: 'blank',
+
+ getSpec: function(commandName, paramName) {
+ return 'blank';
+ },
+
+ stringify: function(value, context) {
+ return '';
+ },
+
+ parse: function(arg, context) {
+ return Promise.resolve(new Conversion(undefined, arg));
+ }
+ }
+];
diff --git a/devtools/shared/gcli/source/lib/gcli/types/file.js b/devtools/shared/gcli/source/lib/gcli/types/file.js
new file mode 100644
index 000000000..004f0108c
--- /dev/null
+++ b/devtools/shared/gcli/source/lib/gcli/types/file.js
@@ -0,0 +1,96 @@
+/*
+ * 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';
+
+/*
+ * The file type is a bit of a spiders-web, but there isn't a nice solution
+ * yet. The core of the problem is that the modules used by Firefox and NodeJS
+ * intersect with the modules used by the web, but not each other. Except here.
+ * So we have to do something fancy to get the sharing but not mess up the web.
+ *
+ * This file requires 'gcli/types/fileparser', and there are 4 implementations
+ * of this:
+ * - '/lib/gcli/types/fileparser.js', the default web version that uses XHR to
+ * talk to the node server
+ * - '/lib/server/gcli/types/fileparser.js', an NodeJS stub, and ...
+ * - '/mozilla/gcli/types/fileparser.js', the Firefox implementation both of
+ * these are shims which import
+ * - 'gcli/util/fileparser', does the real work, except the actual file access
+ *
+ * The file access comes from the 'gcli/util/filesystem' module, and there are
+ * 2 implementations of this:
+ * - '/lib/server/gcli/util/filesystem.js', which uses NodeJS APIs
+ * - '/mozilla/gcli/util/filesystem.js', which uses OS.File APIs
+ */
+
+var fileparser = require('./fileparser');
+var Conversion = require('./types').Conversion;
+
+exports.items = [
+ {
+ item: 'type',
+ name: 'file',
+
+ filetype: 'any', // One of 'file', 'directory', 'any'
+ existing: 'maybe', // Should be one of 'yes', 'no', 'maybe'
+ matches: undefined, // RegExp to match the file part of the path
+
+ hasPredictions: true,
+
+ constructor: function() {
+ if (this.filetype !== 'any' && this.filetype !== 'file' &&
+ this.filetype !== 'directory') {
+ throw new Error('filetype must be one of [any|file|directory]');
+ }
+
+ if (this.existing !== 'yes' && this.existing !== 'no' &&
+ this.existing !== 'maybe') {
+ throw new Error('existing must be one of [yes|no|maybe]');
+ }
+ },
+
+ getSpec: function(commandName, paramName) {
+ return {
+ name: 'remote',
+ commandName: commandName,
+ paramName: paramName
+ };
+ },
+
+ stringify: function(file) {
+ if (file == null) {
+ return '';
+ }
+
+ return file.toString();
+ },
+
+ parse: function(arg, context) {
+ var options = {
+ filetype: this.filetype,
+ existing: this.existing,
+ matches: this.matches
+ };
+ var promise = fileparser.parse(context, arg.text, options);
+
+ return promise.then(function(reply) {
+ return new Conversion(reply.value, arg, reply.status,
+ reply.message, reply.predictor);
+ });
+ }
+ }
+];
diff --git a/devtools/shared/gcli/source/lib/gcli/types/fileparser.js b/devtools/shared/gcli/source/lib/gcli/types/fileparser.js
new file mode 100644
index 000000000..5db86dc66
--- /dev/null
+++ b/devtools/shared/gcli/source/lib/gcli/types/fileparser.js
@@ -0,0 +1,19 @@
+/*
+ * 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';
+
+exports.parse = require('../util/fileparser').parse;
diff --git a/devtools/shared/gcli/source/lib/gcli/types/javascript.js b/devtools/shared/gcli/source/lib/gcli/types/javascript.js
new file mode 100644
index 000000000..71324ef2a
--- /dev/null
+++ b/devtools/shared/gcli/source/lib/gcli/types/javascript.js
@@ -0,0 +1,522 @@
+/*
+ * 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 l10n = require('../util/l10n');
+
+var Conversion = require('./types').Conversion;
+var Type = require('./types').Type;
+var Status = require('./types').Status;
+
+/**
+ * 'javascript' handles scripted input
+ */
+function JavascriptType(typeSpec) {
+}
+
+JavascriptType.prototype = Object.create(Type.prototype);
+
+JavascriptType.prototype.getSpec = function(commandName, paramName) {
+ return {
+ name: 'remote',
+ paramName: paramName
+ };
+};
+
+JavascriptType.prototype.stringify = function(value, context) {
+ if (value == null) {
+ return '';
+ }
+ return value;
+};
+
+/**
+ * When sorting out completions, there is no point in displaying millions of
+ * matches - this the number of matches that we aim for
+ */
+JavascriptType.MAX_COMPLETION_MATCHES = 10;
+
+JavascriptType.prototype.parse = function(arg, context) {
+ var typed = arg.text;
+ var scope = (context.environment.window == null) ?
+ null : context.environment.window;
+
+ // No input is undefined
+ if (typed === '') {
+ return Promise.resolve(new Conversion(undefined, arg, Status.INCOMPLETE));
+ }
+ // Just accept numbers
+ if (!isNaN(parseFloat(typed)) && isFinite(typed)) {
+ return Promise.resolve(new Conversion(typed, arg));
+ }
+ // Just accept constants like true/false/null/etc
+ if (typed.trim().match(/(null|undefined|NaN|Infinity|true|false)/)) {
+ return Promise.resolve(new Conversion(typed, arg));
+ }
+
+ // Analyze the input text and find the beginning of the last part that
+ // should be completed.
+ var beginning = this._findCompletionBeginning(typed);
+
+ // There was an error analyzing the string.
+ if (beginning.err) {
+ return Promise.resolve(new Conversion(typed, arg, Status.ERROR, beginning.err));
+ }
+
+ // If the current state is ParseState.COMPLEX, then we can't do completion.
+ // so bail out now
+ if (beginning.state === ParseState.COMPLEX) {
+ return Promise.resolve(new Conversion(typed, arg));
+ }
+
+ // If the current state is not ParseState.NORMAL, then we are inside of a
+ // string which means that no completion is possible.
+ if (beginning.state !== ParseState.NORMAL) {
+ return Promise.resolve(new Conversion(typed, arg, Status.INCOMPLETE, ''));
+ }
+
+ var completionPart = typed.substring(beginning.startPos);
+ var properties = completionPart.split('.');
+ var matchProp;
+ var prop;
+
+ if (properties.length > 1) {
+ matchProp = properties.pop().trimLeft();
+ for (var i = 0; i < properties.length; i++) {
+ prop = properties[i].trim();
+
+ // We can't complete on null.foo, so bail out
+ if (scope == null) {
+ return Promise.resolve(new Conversion(typed, arg, Status.ERROR,
+ l10n.lookup('jstypeParseScope')));
+ }
+
+ if (prop === '') {
+ return Promise.resolve(new Conversion(typed, arg, Status.INCOMPLETE, ''));
+ }
+
+ // Check if prop is a getter function on 'scope'. Functions can change
+ // other stuff so we can't execute them to get the next object. Stop here.
+ if (this._isSafeProperty(scope, prop)) {
+ return Promise.resolve(new Conversion(typed, arg));
+ }
+
+ try {
+ scope = scope[prop];
+ }
+ catch (ex) {
+ // It would be nice to be able to report this error in some way but
+ // as it can happen just when someone types '{sessionStorage.', it
+ // almost doesn't really count as an error, so we ignore it
+ return Promise.resolve(new Conversion(typed, arg, Status.VALID, ''));
+ }
+ }
+ }
+ else {
+ matchProp = properties[0].trimLeft();
+ }
+
+ // If the reason we just stopped adjusting the scope was a non-simple string,
+ // then we're not sure if the input is valid or invalid, so accept it
+ if (prop && !prop.match(/^[0-9A-Za-z]*$/)) {
+ return Promise.resolve(new Conversion(typed, arg));
+ }
+
+ // However if the prop was a simple string, it is an error
+ if (scope == null) {
+ var msg = l10n.lookupFormat('jstypeParseMissing', [ prop ]);
+ return Promise.resolve(new Conversion(typed, arg, Status.ERROR, msg));
+ }
+
+ // If the thing we're looking for isn't a simple string, then we're not going
+ // to find it, but we're not sure if it's valid or invalid, so accept it
+ if (!matchProp.match(/^[0-9A-Za-z]*$/)) {
+ return Promise.resolve(new Conversion(typed, arg));
+ }
+
+ // Skip Iterators and Generators.
+ if (this._isIteratorOrGenerator(scope)) {
+ return Promise.resolve(new Conversion(typed, arg));
+ }
+
+ var matchLen = matchProp.length;
+ var prefix = matchLen === 0 ? typed : typed.slice(0, -matchLen);
+ var status = Status.INCOMPLETE;
+ var message = '';
+
+ // We really want an array of matches (for sorting) but it's easier to
+ // detect existing members if we're using a map initially
+ var matches = {};
+
+ // We only display a maximum of MAX_COMPLETION_MATCHES, so there is no point
+ // in digging up the prototype chain for matches that we're never going to
+ // use. Initially look for matches directly on the object itself and then
+ // look up the chain to find more
+ var distUpPrototypeChain = 0;
+ var root = scope;
+ try {
+ while (root != null &&
+ Object.keys(matches).length < JavascriptType.MAX_COMPLETION_MATCHES) {
+
+ /* jshint loopfunc:true */
+ Object.keys(root).forEach(function(property) {
+ // Only add matching properties. Also, as we're walking up the
+ // prototype chain, properties on 'higher' prototypes don't override
+ // similarly named properties lower down
+ if (property.indexOf(matchProp) === 0 && !(property in matches)) {
+ matches[property] = {
+ prop: property,
+ distUpPrototypeChain: distUpPrototypeChain
+ };
+ }
+ });
+
+ distUpPrototypeChain++;
+ root = Object.getPrototypeOf(root);
+ }
+ }
+ catch (ex) {
+ return Promise.resolve(new Conversion(typed, arg, Status.INCOMPLETE, ''));
+ }
+
+ // Convert to an array for sorting, and while we're at it, note if we got
+ // an exact match so we know that this input is valid
+ matches = Object.keys(matches).map(function(property) {
+ if (property === matchProp) {
+ status = Status.VALID;
+ }
+ return matches[property];
+ });
+
+ // The sort keys are:
+ // - Being on the object itself, not in the prototype chain
+ // - The lack of existence of a vendor prefix
+ // - The name
+ matches.sort(function(m1, m2) {
+ if (m1.distUpPrototypeChain !== m2.distUpPrototypeChain) {
+ return m1.distUpPrototypeChain - m2.distUpPrototypeChain;
+ }
+ // Push all vendor prefixes to the bottom of the list
+ return isVendorPrefixed(m1.prop) ?
+ (isVendorPrefixed(m2.prop) ? m1.prop.localeCompare(m2.prop) : 1) :
+ (isVendorPrefixed(m2.prop) ? -1 : m1.prop.localeCompare(m2.prop));
+ });
+
+ // Trim to size. There is a bug for doing a better job of finding matches
+ // (bug 682694), but in the mean time there is a performance problem
+ // associated with creating a large number of DOM nodes that few people will
+ // ever read, so trim ...
+ if (matches.length > JavascriptType.MAX_COMPLETION_MATCHES) {
+ matches = matches.slice(0, JavascriptType.MAX_COMPLETION_MATCHES - 1);
+ }
+
+ // Decorate the matches with:
+ // - a description
+ // - a value (for the menu) and,
+ // - an incomplete flag which reports if we should assume that the user isn't
+ // going to carry on the JS expression with this input so far
+ var predictions = matches.map(function(match) {
+ var description;
+ var incomplete = true;
+
+ if (this._isSafeProperty(scope, match.prop)) {
+ description = '(property getter)';
+ }
+ else {
+ try {
+ var value = scope[match.prop];
+
+ if (typeof value === 'function') {
+ description = '(function)';
+ }
+ else if (typeof value === 'boolean' || typeof value === 'number') {
+ description = '= ' + value;
+ incomplete = false;
+ }
+ else if (typeof value === 'string') {
+ if (value.length > 40) {
+ value = value.substring(0, 37) + '…';
+ }
+ description = '= \'' + value + '\'';
+ incomplete = false;
+ }
+ else {
+ description = '(' + typeof value + ')';
+ }
+ }
+ catch (ex) {
+ description = '(' + l10n.lookup('jstypeParseError') + ')';
+ }
+ }
+
+ return {
+ name: prefix + match.prop,
+ value: {
+ name: prefix + match.prop,
+ description: description
+ },
+ description: description,
+ incomplete: incomplete
+ };
+ }, this);
+
+ if (predictions.length === 0) {
+ status = Status.ERROR;
+ message = l10n.lookupFormat('jstypeParseMissing', [ matchProp ]);
+ }
+
+ // If the match is the only one possible, and its VALID, predict nothing
+ if (predictions.length === 1 && status === Status.VALID) {
+ predictions = [];
+ }
+
+ return Promise.resolve(new Conversion(typed, arg, status, message,
+ Promise.resolve(predictions)));
+};
+
+/**
+ * Does the given property have a prefix that indicates that it is vendor
+ * specific?
+ */
+function isVendorPrefixed(name) {
+ return name.indexOf('moz') === 0 ||
+ name.indexOf('webkit') === 0 ||
+ name.indexOf('ms') === 0;
+}
+
+/**
+ * Constants used in return value of _findCompletionBeginning()
+ */
+var ParseState = {
+ /**
+ * We have simple input like window.foo, without any punctuation that makes
+ * completion prediction be confusing or wrong
+ */
+ NORMAL: 0,
+
+ /**
+ * The cursor is in some Javascript that makes completion hard to predict,
+ * like console.log(
+ */
+ COMPLEX: 1,
+
+ /**
+ * The cursor is inside single quotes (')
+ */
+ QUOTE: 2,
+
+ /**
+ * The cursor is inside single quotes (")
+ */
+ DQUOTE: 3
+};
+
+var OPEN_BODY = '{[('.split('');
+var CLOSE_BODY = '}])'.split('');
+var OPEN_CLOSE_BODY = {
+ '{': '}',
+ '[': ']',
+ '(': ')'
+};
+
+/**
+ * How we distinguish between simple and complex JS input. We attempt
+ * completion against simple JS.
+ */
+var simpleChars = /[a-zA-Z0-9.]/;
+
+/**
+ * Analyzes a given string to find the last statement that is interesting for
+ * later completion.
+ * @param text A string to analyze
+ * @return If there was an error in the string detected, then a object like
+ * { err: 'ErrorMesssage' }
+ * is returned, otherwise a object like
+ * {
+ * state: ParseState.NORMAL|ParseState.QUOTE|ParseState.DQUOTE,
+ * startPos: index of where the last statement begins
+ * }
+ */
+JavascriptType.prototype._findCompletionBeginning = function(text) {
+ var bodyStack = [];
+
+ var state = ParseState.NORMAL;
+ var start = 0;
+ var c;
+ var complex = false;
+
+ for (var i = 0; i < text.length; i++) {
+ c = text[i];
+ if (!simpleChars.test(c)) {
+ complex = true;
+ }
+
+ switch (state) {
+ // Normal JS state.
+ case ParseState.NORMAL:
+ if (c === '"') {
+ state = ParseState.DQUOTE;
+ }
+ else if (c === '\'') {
+ state = ParseState.QUOTE;
+ }
+ else if (c === ';') {
+ start = i + 1;
+ }
+ else if (c === ' ') {
+ start = i + 1;
+ }
+ else if (OPEN_BODY.indexOf(c) != -1) {
+ bodyStack.push({
+ token: c,
+ start: start
+ });
+ start = i + 1;
+ }
+ else if (CLOSE_BODY.indexOf(c) != -1) {
+ var last = bodyStack.pop();
+ if (!last || OPEN_CLOSE_BODY[last.token] != c) {
+ return { err: l10n.lookup('jstypeBeginSyntax') };
+ }
+ if (c === '}') {
+ start = i + 1;
+ }
+ else {
+ start = last.start;
+ }
+ }
+ break;
+
+ // Double quote state > " <
+ case ParseState.DQUOTE:
+ if (c === '\\') {
+ i ++;
+ }
+ else if (c === '\n') {
+ return { err: l10n.lookup('jstypeBeginUnterm') };
+ }
+ else if (c === '"') {
+ state = ParseState.NORMAL;
+ }
+ break;
+
+ // Single quote state > ' <
+ case ParseState.QUOTE:
+ if (c === '\\') {
+ i ++;
+ }
+ else if (c === '\n') {
+ return { err: l10n.lookup('jstypeBeginUnterm') };
+ }
+ else if (c === '\'') {
+ state = ParseState.NORMAL;
+ }
+ break;
+ }
+ }
+
+ if (state === ParseState.NORMAL && complex) {
+ state = ParseState.COMPLEX;
+ }
+
+ return {
+ state: state,
+ startPos: start
+ };
+};
+
+/**
+ * Return true if the passed object is either an iterator or a generator, and
+ * false otherwise
+ * @param obj The object to check
+ */
+JavascriptType.prototype._isIteratorOrGenerator = function(obj) {
+ if (obj === null) {
+ return false;
+ }
+
+ if (typeof aObject === 'object') {
+ if (typeof obj.__iterator__ === 'function' ||
+ obj.constructor && obj.constructor.name === 'Iterator') {
+ return true;
+ }
+
+ try {
+ var str = obj.toString();
+ if (typeof obj.next === 'function' &&
+ str.indexOf('[object Generator') === 0) {
+ return true;
+ }
+ }
+ catch (ex) {
+ // window.history.next throws in the typeof check above.
+ return false;
+ }
+ }
+
+ return false;
+};
+
+/**
+ * Would calling 'scope[prop]' cause the invocation of a non-native (i.e. user
+ * defined) function property?
+ * Since calling functions can have side effects, it's only safe to do that if
+ * explicitly requested, rather than because we're trying things out for the
+ * purposes of completion.
+ */
+JavascriptType.prototype._isSafeProperty = function(scope, prop) {
+ if (typeof scope !== 'object') {
+ return false;
+ }
+
+ // Walk up the prototype chain of 'scope' looking for a property descriptor
+ // for 'prop'
+ var propDesc;
+ while (scope) {
+ try {
+ propDesc = Object.getOwnPropertyDescriptor(scope, prop);
+ if (propDesc) {
+ break;
+ }
+ }
+ catch (ex) {
+ // Native getters throw here. See bug 520882.
+ if (ex.name === 'NS_ERROR_XPC_BAD_CONVERT_JS' ||
+ ex.name === 'NS_ERROR_XPC_BAD_OP_ON_WN_PROTO') {
+ return false;
+ }
+ return true;
+ }
+ scope = Object.getPrototypeOf(scope);
+ }
+
+ if (!propDesc) {
+ return false;
+ }
+
+ if (!propDesc.get) {
+ return false;
+ }
+
+ // The property is safe if 'get' isn't a function or if the function has a
+ // prototype (in which case it's native)
+ return typeof propDesc.get !== 'function' || 'prototype' in propDesc.get;
+};
+
+JavascriptType.prototype.name = 'javascript';
+
+exports.items = [ JavascriptType ];
diff --git a/devtools/shared/gcli/source/lib/gcli/types/moz.build b/devtools/shared/gcli/source/lib/gcli/types/moz.build
new file mode 100644
index 000000000..dc3063594
--- /dev/null
+++ b/devtools/shared/gcli/source/lib/gcli/types/moz.build
@@ -0,0 +1,25 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+DevToolsModules(
+ 'array.js',
+ 'boolean.js',
+ 'command.js',
+ 'date.js',
+ 'delegate.js',
+ 'file.js',
+ 'fileparser.js',
+ 'javascript.js',
+ 'node.js',
+ 'number.js',
+ 'resource.js',
+ 'selection.js',
+ 'setting.js',
+ 'string.js',
+ 'types.js',
+ 'union.js',
+ 'url.js',
+)
diff --git a/devtools/shared/gcli/source/lib/gcli/types/node.js b/devtools/shared/gcli/source/lib/gcli/types/node.js
new file mode 100644
index 000000000..2f71704e3
--- /dev/null
+++ b/devtools/shared/gcli/source/lib/gcli/types/node.js
@@ -0,0 +1,201 @@
+/*
+ * 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 Highlighter = require('../util/host').Highlighter;
+var l10n = require('../util/l10n');
+var util = require('../util/util');
+var Status = require('./types').Status;
+var Conversion = require('./types').Conversion;
+var BlankArgument = require('./types').BlankArgument;
+
+/**
+ * Helper functions to be attached to the prototypes of NodeType and
+ * NodeListType to allow terminal to tell us which nodes should be highlighted
+ */
+function onEnter(assignment) {
+ // TODO: GCLI doesn't support passing a context to notifications of cursor
+ // position, so onEnter/onLeave/onChange are disabled below until we fix this
+ assignment.highlighter = new Highlighter(context.environment.window.document);
+ assignment.highlighter.nodelist = assignment.conversion.matches;
+}
+
+/** @see #onEnter() */
+function onLeave(assignment) {
+ if (!assignment.highlighter) {
+ return;
+ }
+
+ assignment.highlighter.destroy();
+ delete assignment.highlighter;
+}
+/** @see #onEnter() */
+function onChange(assignment) {
+ if (assignment.conversion.matches == null) {
+ return;
+ }
+ if (!assignment.highlighter) {
+ return;
+ }
+
+ assignment.highlighter.nodelist = assignment.conversion.matches;
+}
+
+/**
+ * The exported 'node' and 'nodelist' types
+ */
+exports.items = [
+ {
+ // The 'node' type is a CSS expression that refers to a single node
+ item: 'type',
+ name: 'node',
+
+ getSpec: function(commandName, paramName) {
+ return {
+ name: 'remote',
+ commandName: commandName,
+ paramName: paramName
+ };
+ },
+
+ stringify: function(value, context) {
+ if (value == null) {
+ return '';
+ }
+ return value.__gcliQuery || 'Error';
+ },
+
+ parse: function(arg, context) {
+ var reply;
+
+ if (arg.text === '') {
+ reply = new Conversion(undefined, arg, Status.INCOMPLETE);
+ }
+ else {
+ var nodes;
+ try {
+ nodes = context.environment.window.document.querySelectorAll(arg.text);
+ if (nodes.length === 0) {
+ reply = new Conversion(undefined, arg, Status.INCOMPLETE,
+ l10n.lookup('nodeParseNone'));
+ }
+ else if (nodes.length === 1) {
+ var node = nodes.item(0);
+ node.__gcliQuery = arg.text;
+
+ reply = new Conversion(node, arg, Status.VALID, '');
+ }
+ else {
+ var msg = l10n.lookupFormat('nodeParseMultiple', [ nodes.length ]);
+ reply = new Conversion(undefined, arg, Status.ERROR, msg);
+ }
+
+ reply.matches = nodes;
+ }
+ catch (ex) {
+ reply = new Conversion(undefined, arg, Status.ERROR,
+ l10n.lookup('nodeParseSyntax'));
+ }
+ }
+
+ return Promise.resolve(reply);
+ },
+
+ // onEnter: onEnter,
+ // onLeave: onLeave,
+ // onChange: onChange
+ },
+ {
+ // The 'nodelist' type is a CSS expression that refers to a node list
+ item: 'type',
+ name: 'nodelist',
+
+ // The 'allowEmpty' option ensures that we do not complain if the entered
+ // CSS selector is valid, but does not match any nodes. There is some
+ // overlap between this option and 'defaultValue'. What the user wants, in
+ // most cases, would be to use 'defaultText' (i.e. what is typed rather than
+ // the value that it represents). However this isn't a concept that exists
+ // yet and should probably be a part of GCLI if/when it does.
+ // All NodeListTypes have an automatic defaultValue of an empty NodeList so
+ // they can easily be used in named parameters.
+ allowEmpty: false,
+
+ constructor: function() {
+ if (typeof this.allowEmpty !== 'boolean') {
+ throw new Error('Legal values for allowEmpty are [true|false]');
+ }
+ },
+
+ getSpec: function(commandName, paramName) {
+ return {
+ name: 'remote',
+ commandName: commandName,
+ paramName: paramName,
+ blankIsValid: true
+ };
+ },
+
+ getBlank: function(context) {
+ var emptyNodeList = [];
+ if (context != null && context.environment.window != null) {
+ var doc = context.environment.window.document;
+ emptyNodeList = util.createEmptyNodeList(doc);
+ }
+ return new Conversion(emptyNodeList, new BlankArgument(), Status.VALID);
+ },
+
+ stringify: function(value, context) {
+ if (value == null) {
+ return '';
+ }
+ return value.__gcliQuery || 'Error';
+ },
+
+ parse: function(arg, context) {
+ var reply;
+ try {
+ if (arg.text === '') {
+ reply = new Conversion(undefined, arg, Status.INCOMPLETE);
+ }
+ else {
+ var nodes = context.environment.window.document.querySelectorAll(arg.text);
+
+ if (nodes.length === 0 && !this.allowEmpty) {
+ reply = new Conversion(undefined, arg, Status.INCOMPLETE,
+ l10n.lookup('nodeParseNone'));
+ }
+ else {
+ nodes.__gcliQuery = arg.text;
+ reply = new Conversion(nodes, arg, Status.VALID, '');
+ }
+
+ reply.matches = nodes;
+ }
+ }
+ catch (ex) {
+ reply = new Conversion(undefined, arg, Status.ERROR,
+ l10n.lookup('nodeParseSyntax'));
+ }
+
+ return Promise.resolve(reply);
+ },
+
+ // onEnter: onEnter,
+ // onLeave: onLeave,
+ // onChange: onChange
+ }
+];
diff --git a/devtools/shared/gcli/source/lib/gcli/types/number.js b/devtools/shared/gcli/source/lib/gcli/types/number.js
new file mode 100644
index 000000000..4c67e5807
--- /dev/null
+++ b/devtools/shared/gcli/source/lib/gcli/types/number.js
@@ -0,0 +1,181 @@
+/*
+ * 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 l10n = require('../util/l10n');
+var Status = require('./types').Status;
+var Conversion = require('./types').Conversion;
+
+exports.items = [
+ {
+ // 'number' type
+ // Has custom max / min / step values to control increment and decrement
+ // and a boolean allowFloat property to clamp values to integers
+ item: 'type',
+ name: 'number',
+
+ allowFloat: false,
+ max: undefined,
+ min: undefined,
+ step: 1,
+
+ constructor: function() {
+ if (!this.allowFloat &&
+ (this._isFloat(this.min) ||
+ this._isFloat(this.max) ||
+ this._isFloat(this.step))) {
+ throw new Error('allowFloat is false, but non-integer values given in type spec');
+ }
+ },
+
+ getSpec: function() {
+ var spec = {
+ name: 'number'
+ };
+ if (this.step !== 1) {
+ spec.step = this.step;
+ }
+ if (this.max != null) {
+ spec.max = this.max;
+ }
+ if (this.min != null) {
+ spec.min = this.min;
+ }
+ if (this.allowFloat) {
+ spec.allowFloat = true;
+ }
+ return (Object.keys(spec).length === 1) ? 'number' : spec;
+ },
+
+ stringify: function(value, context) {
+ if (value == null) {
+ return '';
+ }
+ return '' + value;
+ },
+
+ getMin: function(context) {
+ if (this.min != null) {
+ if (typeof this.min === 'function') {
+ return this.min(context);
+ }
+ if (typeof this.min === 'number') {
+ return this.min;
+ }
+ }
+ return undefined;
+ },
+
+ getMax: function(context) {
+ if (this.max != null) {
+ if (typeof this.max === 'function') {
+ return this.max(context);
+ }
+ if (typeof this.max === 'number') {
+ return this.max;
+ }
+ }
+ return undefined;
+ },
+
+ parse: function(arg, context) {
+ var msg;
+ if (arg.text.replace(/^\s*-?/, '').length === 0) {
+ return Promise.resolve(new Conversion(undefined, arg, Status.INCOMPLETE, ''));
+ }
+
+ if (!this.allowFloat && (arg.text.indexOf('.') !== -1)) {
+ msg = l10n.lookupFormat('typesNumberNotInt2', [ arg.text ]);
+ return Promise.resolve(new Conversion(undefined, arg, Status.ERROR, msg));
+ }
+
+ var value;
+ if (this.allowFloat) {
+ value = parseFloat(arg.text);
+ }
+ else {
+ value = parseInt(arg.text, 10);
+ }
+
+ if (isNaN(value)) {
+ msg = l10n.lookupFormat('typesNumberNan', [ arg.text ]);
+ return Promise.resolve(new Conversion(undefined, arg, Status.ERROR, msg));
+ }
+
+ var max = this.getMax(context);
+ if (max != null && value > max) {
+ msg = l10n.lookupFormat('typesNumberMax', [ value, max ]);
+ return Promise.resolve(new Conversion(undefined, arg, Status.ERROR, msg));
+ }
+
+ var min = this.getMin(context);
+ if (min != null && value < min) {
+ msg = l10n.lookupFormat('typesNumberMin', [ value, min ]);
+ return Promise.resolve(new Conversion(undefined, arg, Status.ERROR, msg));
+ }
+
+ return Promise.resolve(new Conversion(value, arg));
+ },
+
+ nudge: function(value, by, context) {
+ if (typeof value !== 'number' || isNaN(value)) {
+ if (by < 0) {
+ return this.getMax(context) || 1;
+ }
+ else {
+ var min = this.getMin(context);
+ return min != null ? min : 0;
+ }
+ }
+
+ var newValue = value + (by * this.step);
+
+ // Snap to the nearest incremental of the step
+ if (by < 0) {
+ newValue = Math.ceil(newValue / this.step) * this.step;
+ }
+ else {
+ newValue = Math.floor(newValue / this.step) * this.step;
+ if (this.getMax(context) == null) {
+ return newValue;
+ }
+ }
+ return this._boundsCheck(newValue, context);
+ },
+
+ // Return the input value so long as it is within the max/min bounds.
+ // If it is lower than the minimum, return the minimum. If it is bigger
+ // than the maximum then return the maximum.
+ _boundsCheck: function(value, context) {
+ var min = this.getMin(context);
+ if (min != null && value < min) {
+ return min;
+ }
+ var max = this.getMax(context);
+ if (max != null && value > max) {
+ return max;
+ }
+ return value;
+ },
+
+ // Return true if the given value is a finite number and not an integer,
+ // else return false.
+ _isFloat: function(value) {
+ return ((typeof value === 'number') && isFinite(value) && (value % 1 !== 0));
+ }
+ }
+];
diff --git a/devtools/shared/gcli/source/lib/gcli/types/resource.js b/devtools/shared/gcli/source/lib/gcli/types/resource.js
new file mode 100644
index 000000000..cd1984824
--- /dev/null
+++ b/devtools/shared/gcli/source/lib/gcli/types/resource.js
@@ -0,0 +1,270 @@
+/*
+ * 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';
+
+exports.clearResourceCache = function() {
+ ResourceCache.clear();
+};
+
+/**
+ * Resources are bits of CSS and JavaScript that the page either includes
+ * directly or as a result of reading some remote resource.
+ * Resource should not be used directly, but instead through a sub-class like
+ * CssResource or ScriptResource.
+ */
+function Resource(name, type, inline, element) {
+ this.name = name;
+ this.type = type;
+ this.inline = inline;
+ this.element = element;
+}
+
+/**
+ * Get the contents of the given resource as a string.
+ * The base Resource leaves this unimplemented.
+ */
+Resource.prototype.loadContents = function() {
+ throw new Error('not implemented');
+};
+
+Resource.TYPE_SCRIPT = 'text/javascript';
+Resource.TYPE_CSS = 'text/css';
+
+/**
+ * A CssResource provides an implementation of Resource that works for both
+ * [style] elements and [link type='text/css'] elements in the [head].
+ */
+function CssResource(domSheet) {
+ this.name = domSheet.href;
+ if (!this.name) {
+ this.name = domSheet.ownerNode && domSheet.ownerNode.id ?
+ 'css#' + domSheet.ownerNode.id :
+ 'inline-css';
+ }
+
+ this.inline = (domSheet.href == null);
+ this.type = Resource.TYPE_CSS;
+ this.element = domSheet;
+}
+
+CssResource.prototype = Object.create(Resource.prototype);
+
+CssResource.prototype.loadContents = function() {
+ return new Promise(function(resolve, reject) {
+ resolve(this.element.ownerNode.innerHTML);
+ }.bind(this));
+};
+
+CssResource._getAllStyles = function(context) {
+ var resources = [];
+ if (context.environment.window == null) {
+ return resources;
+ }
+
+ var doc = context.environment.window.document;
+ Array.prototype.forEach.call(doc.styleSheets, function(domSheet) {
+ CssResource._getStyle(domSheet, resources);
+ });
+
+ dedupe(resources, function(clones) {
+ for (var i = 0; i < clones.length; i++) {
+ clones[i].name = clones[i].name + '-' + i;
+ }
+ });
+
+ return resources;
+};
+
+CssResource._getStyle = function(domSheet, resources) {
+ var resource = ResourceCache.get(domSheet);
+ if (!resource) {
+ resource = new CssResource(domSheet);
+ ResourceCache.add(domSheet, resource);
+ }
+ resources.push(resource);
+
+ // Look for imported stylesheets
+ try {
+ Array.prototype.forEach.call(domSheet.cssRules, function(domRule) {
+ if (domRule.type == CSSRule.IMPORT_RULE && domRule.styleSheet) {
+ CssResource._getStyle(domRule.styleSheet, resources);
+ }
+ }, this);
+ }
+ catch (ex) {
+ // For system stylesheets
+ }
+};
+
+/**
+ * A ScriptResource provides an implementation of Resource that works for
+ * [script] elements (both with a src attribute, and used directly).
+ */
+function ScriptResource(scriptNode) {
+ this.name = scriptNode.src;
+ if (!this.name) {
+ this.name = scriptNode.id ?
+ 'script#' + scriptNode.id :
+ 'inline-script';
+ }
+
+ this.inline = (scriptNode.src === '' || scriptNode.src == null);
+ this.type = Resource.TYPE_SCRIPT;
+ this.element = scriptNode;
+}
+
+ScriptResource.prototype = Object.create(Resource.prototype);
+
+ScriptResource.prototype.loadContents = function() {
+ return new Promise(function(resolve, reject) {
+ if (this.inline) {
+ resolve(this.element.innerHTML);
+ }
+ else {
+ // It would be good if there was a better way to get the script source
+ var xhr = new XMLHttpRequest();
+ xhr.onreadystatechange = function() {
+ if (xhr.readyState !== xhr.DONE) {
+ return;
+ }
+ resolve(xhr.responseText);
+ };
+ xhr.open('GET', this.element.src, true);
+ xhr.send();
+ }
+ }.bind(this));
+};
+
+ScriptResource._getAllScripts = function(context) {
+ if (context.environment.window == null) {
+ return [];
+ }
+
+ var doc = context.environment.window.document;
+ var scriptNodes = doc.querySelectorAll('script');
+ var resources = Array.prototype.map.call(scriptNodes, function(scriptNode) {
+ var resource = ResourceCache.get(scriptNode);
+ if (!resource) {
+ resource = new ScriptResource(scriptNode);
+ ResourceCache.add(scriptNode, resource);
+ }
+ return resource;
+ });
+
+ dedupe(resources, function(clones) {
+ for (var i = 0; i < clones.length; i++) {
+ clones[i].name = clones[i].name + '-' + i;
+ }
+ });
+
+ return resources;
+};
+
+/**
+ * Find resources with the same name, and call onDupe to change the names
+ */
+function dedupe(resources, onDupe) {
+ // first create a map of name->[array of resources with same name]
+ var names = {};
+ resources.forEach(function(scriptResource) {
+ if (names[scriptResource.name] == null) {
+ names[scriptResource.name] = [];
+ }
+ names[scriptResource.name].push(scriptResource);
+ });
+
+ // Call the de-dupe function for each set of dupes
+ Object.keys(names).forEach(function(name) {
+ var clones = names[name];
+ if (clones.length > 1) {
+ onDupe(clones);
+ }
+ });
+}
+
+/**
+ * A quick cache of resources against nodes
+ * TODO: Potential memory leak when the target document has css or script
+ * resources repeatedly added and removed. Solution might be to use a weak
+ * hash map or some such.
+ */
+var ResourceCache = {
+ _cached: [],
+
+ /**
+ * Do we already have a resource that was created for the given node
+ */
+ get: function(node) {
+ for (var i = 0; i < ResourceCache._cached.length; i++) {
+ if (ResourceCache._cached[i].node === node) {
+ return ResourceCache._cached[i].resource;
+ }
+ }
+ return null;
+ },
+
+ /**
+ * Add a resource for a given node
+ */
+ add: function(node, resource) {
+ ResourceCache._cached.push({ node: node, resource: resource });
+ },
+
+ /**
+ * Drop all cache entries. Helpful to prevent memory leaks
+ */
+ clear: function() {
+ ResourceCache._cached = [];
+ }
+};
+
+/**
+ * The resource type itself
+ */
+exports.items = [
+ {
+ item: 'type',
+ name: 'resource',
+ parent: 'selection',
+ cacheable: false,
+ include: null,
+
+ constructor: function() {
+ if (this.include !== Resource.TYPE_SCRIPT &&
+ this.include !== Resource.TYPE_CSS &&
+ this.include != null) {
+ throw new Error('invalid include property: ' + this.include);
+ }
+ },
+
+ lookup: function(context) {
+ var resources = [];
+ if (this.include !== Resource.TYPE_SCRIPT) {
+ Array.prototype.push.apply(resources,
+ CssResource._getAllStyles(context));
+ }
+ if (this.include !== Resource.TYPE_CSS) {
+ Array.prototype.push.apply(resources,
+ ScriptResource._getAllScripts(context));
+ }
+
+ return Promise.resolve(resources.map(function(resource) {
+ return { name: resource.name, value: resource };
+ }));
+ }
+ }
+];
diff --git a/devtools/shared/gcli/source/lib/gcli/types/selection.js b/devtools/shared/gcli/source/lib/gcli/types/selection.js
new file mode 100644
index 000000000..0e64c8fa2
--- /dev/null
+++ b/devtools/shared/gcli/source/lib/gcli/types/selection.js
@@ -0,0 +1,389 @@
+/*
+ * 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 l10n = require('../util/l10n');
+var spell = require('../util/spell');
+var Type = require('./types').Type;
+var Status = require('./types').Status;
+var Conversion = require('./types').Conversion;
+var BlankArgument = require('./types').BlankArgument;
+
+/**
+ * A selection allows the user to pick a value from known set of options.
+ * An option is made up of a name (which is what the user types) and a value
+ * (which is passed to exec)
+ * @param typeSpec Object containing properties that describe how this
+ * selection functions. Properties include:
+ * - lookup: An array of objects, one for each option, which contain name and
+ * value properties. lookup can be a function which returns this array
+ * - data: An array of strings - alternative to 'lookup' where the valid values
+ * are strings. i.e. there is no mapping between what is typed and the value
+ * that is used by the program
+ * - stringifyProperty: Conversion from value to string is generally a process
+ * of looking through all the valid options for a matching value, and using
+ * the associated name. However the name maybe available directly from the
+ * value using a property lookup. Setting 'stringifyProperty' allows
+ * SelectionType to take this shortcut.
+ * - cacheable: If lookup is a function, then we normally assume that
+ * the values fetched can change. Setting 'cacheable:true' enables internal
+ * caching.
+ */
+function SelectionType(typeSpec) {
+ if (typeSpec) {
+ Object.keys(typeSpec).forEach(function(key) {
+ this[key] = typeSpec[key];
+ }, this);
+ }
+
+ if (this.name !== 'selection' &&
+ this.lookup == null && this.data == null) {
+ throw new Error(this.name + ' has no lookup or data');
+ }
+
+ this._dataToLookup = this._dataToLookup.bind(this);
+}
+
+SelectionType.prototype = Object.create(Type.prototype);
+
+SelectionType.prototype.getSpec = function(commandName, paramName) {
+ var spec = { name: 'selection' };
+ if (this.lookup != null && typeof this.lookup !== 'function') {
+ spec.lookup = this.lookup;
+ }
+ if (this.data != null && typeof this.data !== 'function') {
+ spec.data = this.data;
+ }
+ if (this.stringifyProperty != null) {
+ spec.stringifyProperty = this.stringifyProperty;
+ }
+ if (this.cacheable) {
+ spec.cacheable = true;
+ }
+ if (typeof this.lookup === 'function' || typeof this.data === 'function') {
+ spec.commandName = commandName;
+ spec.paramName = paramName;
+ spec.remoteLookup = true;
+ }
+ return spec;
+};
+
+SelectionType.prototype.stringify = function(value, context) {
+ if (value == null) {
+ return '';
+ }
+ if (this.stringifyProperty != null) {
+ return value[this.stringifyProperty];
+ }
+
+ return this.getLookup(context).then(function(lookup) {
+ var name = null;
+ lookup.some(function(item) {
+ if (item.value === value) {
+ name = item.name;
+ return true;
+ }
+ return false;
+ }, this);
+ return name;
+ }.bind(this));
+};
+
+/**
+ * If typeSpec contained cacheable:true then calls to parse() work on cached
+ * data. clearCache() enables the cache to be cleared.
+ */
+SelectionType.prototype.clearCache = function() {
+ this._cachedLookup = undefined;
+};
+
+/**
+ * There are several ways to get selection data. This unifies them into one
+ * single function.
+ * @return An array of objects with name and value properties.
+ */
+SelectionType.prototype.getLookup = function(context) {
+ if (this._cachedLookup != null) {
+ return this._cachedLookup;
+ }
+
+ var reply;
+
+ if (this.remoteLookup) {
+ reply = this.front.getSelectionLookup(this.commandName, this.paramName);
+ reply = resolve(reply, context);
+ }
+ else if (typeof this.lookup === 'function') {
+ reply = resolve(this.lookup.bind(this), context);
+ }
+ else if (this.lookup != null) {
+ reply = resolve(this.lookup, context);
+ }
+ else if (this.data != null) {
+ reply = resolve(this.data, context).then(this._dataToLookup);
+ }
+ else {
+ throw new Error(this.name + ' has no lookup or data');
+ }
+
+ if (this.cacheable) {
+ this._cachedLookup = reply;
+ }
+
+ if (reply == null) {
+ console.error(arguments);
+ }
+ return reply;
+};
+
+/**
+ * Both 'lookup' and 'data' properties (see docs on SelectionType constructor)
+ * in addition to being real data can be a function or a promise, or even a
+ * function which returns a promise of real data, etc. This takes a thing and
+ * returns a promise of actual values.
+ */
+function resolve(thing, context) {
+ return Promise.resolve(thing).then(function(resolved) {
+ if (typeof resolved === 'function') {
+ return resolve(resolved(context), context);
+ }
+ return resolved;
+ });
+}
+
+/**
+ * Selection can be provided with either a lookup object (in the 'lookup'
+ * property) or an array of strings (in the 'data' property). Internally we
+ * always use lookup, so we need a way to convert a 'data' array to a lookup.
+ */
+SelectionType.prototype._dataToLookup = function(data) {
+ if (!Array.isArray(data)) {
+ throw new Error('data for ' + this.name + ' resolved to non-array');
+ }
+
+ return data.map(function(option) {
+ return { name: option, value: option };
+ });
+};
+
+/**
+ * Return a list of possible completions for the given arg.
+ * @param arg The initial input to match
+ * @return A trimmed array of string:value pairs
+ */
+exports.findPredictions = function(arg, lookup) {
+ var predictions = [];
+ var i, option;
+ var maxPredictions = Conversion.maxPredictions;
+ var match = arg.text.toLowerCase();
+
+ // If the arg has a suffix then we're kind of 'done'. Only an exact match
+ // will do.
+ if (arg.suffix.length > 0) {
+ for (i = 0; i < lookup.length && predictions.length < maxPredictions; i++) {
+ option = lookup[i];
+ if (option.name === arg.text) {
+ predictions.push(option);
+ }
+ }
+
+ return predictions;
+ }
+
+ // Cache lower case versions of all the option names
+ for (i = 0; i < lookup.length; i++) {
+ option = lookup[i];
+ if (option._gcliLowerName == null) {
+ option._gcliLowerName = option.name.toLowerCase();
+ }
+ }
+
+ // Exact hidden matches. If 'hidden: true' then we only allow exact matches
+ // All the tests after here check that !isHidden(option)
+ for (i = 0; i < lookup.length && predictions.length < maxPredictions; i++) {
+ option = lookup[i];
+ if (option.name === arg.text) {
+ predictions.push(option);
+ }
+ }
+
+ // Start with prefix matching
+ for (i = 0; i < lookup.length && predictions.length < maxPredictions; i++) {
+ option = lookup[i];
+ if (option._gcliLowerName.indexOf(match) === 0 && !isHidden(option)) {
+ if (predictions.indexOf(option) === -1) {
+ predictions.push(option);
+ }
+ }
+ }
+
+ // Try infix matching if we get less half max matched
+ if (predictions.length < (maxPredictions / 2)) {
+ for (i = 0; i < lookup.length && predictions.length < maxPredictions; i++) {
+ option = lookup[i];
+ if (option._gcliLowerName.indexOf(match) !== -1 && !isHidden(option)) {
+ if (predictions.indexOf(option) === -1) {
+ predictions.push(option);
+ }
+ }
+ }
+ }
+
+ // Try fuzzy matching if we don't get a prefix match
+ if (predictions.length === 0) {
+ var names = [];
+ lookup.forEach(function(opt) {
+ if (!isHidden(opt)) {
+ names.push(opt.name);
+ }
+ });
+ var corrected = spell.correct(match, names);
+ if (corrected) {
+ lookup.forEach(function(opt) {
+ if (opt.name === corrected) {
+ predictions.push(opt);
+ }
+ }, this);
+ }
+ }
+
+ return predictions;
+};
+
+SelectionType.prototype.parse = function(arg, context) {
+ return Promise.resolve(this.getLookup(context)).then(function(lookup) {
+ var predictions = exports.findPredictions(arg, lookup);
+ return exports.convertPredictions(arg, predictions);
+ }.bind(this));
+};
+
+/**
+ * Decide what sort of conversion to return based on the available predictions
+ * and how they match the passed arg
+ */
+exports.convertPredictions = function(arg, predictions) {
+ if (predictions.length === 0) {
+ var msg = l10n.lookupFormat('typesSelectionNomatch', [ arg.text ]);
+ return new Conversion(undefined, arg, Status.ERROR, msg,
+ Promise.resolve(predictions));
+ }
+
+ if (predictions[0].name === arg.text) {
+ var value = predictions[0].value;
+ return new Conversion(value, arg, Status.VALID, '',
+ Promise.resolve(predictions));
+ }
+
+ return new Conversion(undefined, arg, Status.INCOMPLETE, '',
+ Promise.resolve(predictions));
+};
+
+/**
+ * Checking that an option is hidden involves messing in properties on the
+ * value right now (which isn't a good idea really) we really should be marking
+ * that on the option, so this encapsulates the problem
+ */
+function isHidden(option) {
+ return option.hidden === true ||
+ (option.value != null && option.value.hidden);
+}
+
+SelectionType.prototype.getBlank = function(context) {
+ var predictFunc = function(context2) {
+ return Promise.resolve(this.getLookup(context2)).then(function(lookup) {
+ return lookup.filter(function(option) {
+ return !isHidden(option);
+ }).slice(0, Conversion.maxPredictions - 1);
+ });
+ }.bind(this);
+
+ return new Conversion(undefined, new BlankArgument(), Status.INCOMPLETE, '',
+ predictFunc);
+};
+
+/**
+ * Increment and decrement are confusing for selections. +1 is -1 and -1 is +1.
+ * Given an array e.g. [ 'a', 'b', 'c' ] with the current selection on 'b',
+ * displayed to the user in the natural way, i.e.:
+ *
+ * 'a'
+ * 'b' <- highlighted as current value
+ * 'c'
+ *
+ * Pressing the UP arrow should take us to 'a', which decrements this index
+ * (compare pressing UP on a number which would increment the number)
+ *
+ * So for selections, we treat +1 as -1 and -1 as +1.
+ */
+SelectionType.prototype.nudge = function(value, by, context) {
+ return this.getLookup(context).then(function(lookup) {
+ var index = this._findValue(lookup, value);
+ if (index === -1) {
+ if (by < 0) {
+ // We're supposed to be doing a decrement (which means +1), but the
+ // value isn't found, so we reset the index to the top of the list
+ // which is index 0
+ index = 0;
+ }
+ else {
+ // For an increment operation when there is nothing to start from, we
+ // want to start from the top, i.e. index 0, so the value before we
+ // 'increment' (see note above) must be 1.
+ index = 1;
+ }
+ }
+
+ // This is where we invert the sense of up/down (see doc comment)
+ index -= by;
+
+ if (index >= lookup.length) {
+ index = 0;
+ }
+ return lookup[index].value;
+ }.bind(this));
+};
+
+/**
+ * Walk through an array of { name:.., value:... } objects looking for a
+ * matching value (using strict equality), returning the matched index (or -1
+ * if not found).
+ * @param lookup Array of objects with name/value properties to search through
+ * @param value The value to search for
+ * @return The index at which the match was found, or -1 if no match was found
+ */
+SelectionType.prototype._findValue = function(lookup, value) {
+ var index = -1;
+ for (var i = 0; i < lookup.length; i++) {
+ var pair = lookup[i];
+ if (pair.value === value) {
+ index = i;
+ break;
+ }
+ }
+ return index;
+};
+
+/**
+ * This is how we indicate to SelectionField that we have predictions that
+ * might work in a menu.
+ */
+SelectionType.prototype.hasPredictions = true;
+
+SelectionType.prototype.name = 'selection';
+
+exports.SelectionType = SelectionType;
+exports.items = [ SelectionType ];
diff --git a/devtools/shared/gcli/source/lib/gcli/types/setting.js b/devtools/shared/gcli/source/lib/gcli/types/setting.js
new file mode 100644
index 000000000..26c6f4063
--- /dev/null
+++ b/devtools/shared/gcli/source/lib/gcli/types/setting.js
@@ -0,0 +1,62 @@
+/*
+ * 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';
+
+exports.items = [
+ {
+ // A type for selecting a known setting
+ item: 'type',
+ name: 'setting',
+ parent: 'selection',
+ cacheable: true,
+ lookup: function(context) {
+ var settings = context.system.settings;
+
+ // Lazily add a settings.onChange listener to clear the cache
+ if (!this._registeredListener) {
+ settings.onChange.add(function(ev) {
+ this.clearCache();
+ }, this);
+ this._registeredListener = true;
+ }
+
+ return settings.getAll().map(function(setting) {
+ return { name: setting.name, value: setting };
+ });
+ }
+ },
+ {
+ // A type for entering the value of a known setting
+ // Customizations:
+ // - settingParamName The name of the setting parameter so we can customize
+ // the type that we are expecting to read
+ item: 'type',
+ name: 'settingValue',
+ parent: 'delegate',
+ settingParamName: 'setting',
+ delegateType: function(context) {
+ if (context != null) {
+ var setting = context.getArgsObject()[this.settingParamName];
+ if (setting != null) {
+ return setting.type;
+ }
+ }
+
+ return 'blank';
+ }
+ }
+];
diff --git a/devtools/shared/gcli/source/lib/gcli/types/string.js b/devtools/shared/gcli/source/lib/gcli/types/string.js
new file mode 100644
index 000000000..a3aebacad
--- /dev/null
+++ b/devtools/shared/gcli/source/lib/gcli/types/string.js
@@ -0,0 +1,92 @@
+/*
+ * 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 Status = require('./types').Status;
+var Conversion = require('./types').Conversion;
+
+exports.items = [
+ {
+ // 'string' the most basic string type where all we need to do is to take
+ // care of converting escaped characters like \t, \n, etc.
+ // For the full list see
+ // https://developer.mozilla.org/en-US/docs/JavaScript/Guide/Values,_variables,_and_literals
+ // The exception is that we ignore \b because replacing '\b' characters in
+ // stringify() with their escaped version injects '\\b' all over the place
+ // and the need to support \b seems low)
+ // Customizations:
+ // allowBlank: Allow a blank string to be counted as valid
+ item: 'type',
+ name: 'string',
+ allowBlank: false,
+
+ getSpec: function() {
+ return this.allowBlank ?
+ { name: 'string', allowBlank: true } :
+ 'string';
+ },
+
+ stringify: function(value, context) {
+ if (value == null) {
+ return '';
+ }
+
+ return value
+ .replace(/\\/g, '\\\\')
+ .replace(/\f/g, '\\f')
+ .replace(/\n/g, '\\n')
+ .replace(/\r/g, '\\r')
+ .replace(/\t/g, '\\t')
+ .replace(/\v/g, '\\v')
+ .replace(/\n/g, '\\n')
+ .replace(/\r/g, '\\r')
+ .replace(/ /g, '\\ ')
+ .replace(/'/g, '\\\'')
+ .replace(/"/g, '\\"')
+ .replace(/{/g, '\\{')
+ .replace(/}/g, '\\}');
+ },
+
+ parse: function(arg, context) {
+ if (!this.allowBlank && (arg.text == null || arg.text === '')) {
+ return Promise.resolve(new Conversion(undefined, arg, Status.INCOMPLETE, ''));
+ }
+
+ // The string '\\' (i.e. an escaped \ (represented here as '\\\\' because it
+ // is double escaped)) is first converted to a private unicode character and
+ // then at the end from \uF000 to a single '\' to avoid the string \\n being
+ // converted first to \n and then to a <LF>
+ var value = arg.text
+ .replace(/\\\\/g, '\uF000')
+ .replace(/\\f/g, '\f')
+ .replace(/\\n/g, '\n')
+ .replace(/\\r/g, '\r')
+ .replace(/\\t/g, '\t')
+ .replace(/\\v/g, '\v')
+ .replace(/\\n/g, '\n')
+ .replace(/\\r/g, '\r')
+ .replace(/\\ /g, ' ')
+ .replace(/\\'/g, '\'')
+ .replace(/\\"/g, '"')
+ .replace(/\\{/g, '{')
+ .replace(/\\}/g, '}')
+ .replace(/\uF000/g, '\\');
+
+ return Promise.resolve(new Conversion(value, arg));
+ }
+ }
+];
diff --git a/devtools/shared/gcli/source/lib/gcli/types/types.js b/devtools/shared/gcli/source/lib/gcli/types/types.js
new file mode 100644
index 000000000..ed5a93d54
--- /dev/null
+++ b/devtools/shared/gcli/source/lib/gcli/types/types.js
@@ -0,0 +1,1146 @@
+/*
+ * 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');
+
+/**
+ * We record where in the input string an argument comes so we can report
+ * errors against those string positions.
+ * @param text The string (trimmed) that contains the argument
+ * @param prefix Knowledge of quotation marks and whitespace used prior to the
+ * text in the input string allows us to re-generate the original input from
+ * the arguments.
+ * @param suffix Any quotation marks and whitespace used after the text.
+ * Whitespace is normally placed in the prefix to the succeeding argument, but
+ * can be used here when this is the last argument.
+ * @constructor
+ */
+function Argument(text, prefix, suffix) {
+ if (text === undefined) {
+ this.text = '';
+ this.prefix = '';
+ this.suffix = '';
+ }
+ else {
+ this.text = text;
+ this.prefix = prefix !== undefined ? prefix : '';
+ this.suffix = suffix !== undefined ? suffix : '';
+ }
+}
+
+Argument.prototype.type = 'Argument';
+
+/**
+ * Return the result of merging these arguments.
+ * case and some of the arguments are in quotation marks?
+ */
+Argument.prototype.merge = function(following) {
+ // Is it possible that this gets called when we're merging arguments
+ // for the single string?
+ return new Argument(
+ this.text + this.suffix + following.prefix + following.text,
+ this.prefix, following.suffix);
+};
+
+/**
+ * Returns a new Argument like this one but with various items changed.
+ * @param options Values to use in creating a new Argument.
+ * Warning: some implementations of beget make additions to the options
+ * argument. You should be aware of this in the unlikely event that you want to
+ * reuse 'options' arguments.
+ * Properties:
+ * - text: The new text value
+ * - prefixSpace: Should the prefix be altered to begin with a space?
+ * - prefixPostSpace: Should the prefix be altered to end with a space?
+ * - suffixSpace: Should the suffix be altered to end with a space?
+ * - type: Constructor to use in creating new instances. Default: Argument
+ * - dontQuote: Should we avoid adding prefix/suffix quotes when the text value
+ * has a space? Needed when we're completing a sub-command.
+ */
+Argument.prototype.beget = function(options) {
+ var text = this.text;
+ var prefix = this.prefix;
+ var suffix = this.suffix;
+
+ if (options.text != null) {
+ text = options.text;
+
+ // We need to add quotes when the replacement string has spaces or is empty
+ if (!options.dontQuote) {
+ var needsQuote = text.indexOf(' ') >= 0 || text.length === 0;
+ var hasQuote = /['"]$/.test(prefix);
+ if (needsQuote && !hasQuote) {
+ prefix = prefix + '\'';
+ suffix = '\'' + suffix;
+ }
+ }
+ }
+
+ if (options.prefixSpace && prefix.charAt(0) !== ' ') {
+ prefix = ' ' + prefix;
+ }
+
+ if (options.prefixPostSpace && prefix.charAt(prefix.length - 1) !== ' ') {
+ prefix = prefix + ' ';
+ }
+
+ if (options.suffixSpace && suffix.charAt(suffix.length - 1) !== ' ') {
+ suffix = suffix + ' ';
+ }
+
+ if (text === this.text && suffix === this.suffix && prefix === this.prefix) {
+ return this;
+ }
+
+ var ArgumentType = options.type || Argument;
+ return new ArgumentType(text, prefix, suffix);
+};
+
+/**
+ * We need to keep track of which assignment we've been assigned to
+ */
+Object.defineProperty(Argument.prototype, 'assignment', {
+ get: function() { return this._assignment; },
+ set: function(assignment) { this._assignment = assignment; },
+ enumerable: true
+});
+
+/**
+ * Sub-classes of Argument are collections of arguments, getArgs() gets access
+ * to the members of the collection in order to do things like re-create input
+ * command lines. For the simple Argument case it's just an array containing
+ * only this.
+ */
+Argument.prototype.getArgs = function() {
+ return [ this ];
+};
+
+/**
+ * We define equals to mean all arg properties are strict equals.
+ * Used by Conversion.argEquals and Conversion.equals and ultimately
+ * Assignment.equals to avoid reporting a change event when a new conversion
+ * is assigned.
+ */
+Argument.prototype.equals = function(that) {
+ if (this === that) {
+ return true;
+ }
+ if (that == null || !(that instanceof Argument)) {
+ return false;
+ }
+
+ return this.text === that.text &&
+ this.prefix === that.prefix && this.suffix === that.suffix;
+};
+
+/**
+ * Helper when we're putting arguments back together
+ */
+Argument.prototype.toString = function() {
+ // BUG 664207: We should re-escape escaped characters
+ // But can we do that reliably?
+ return this.prefix + this.text + this.suffix;
+};
+
+/**
+ * Merge an array of arguments into a single argument.
+ * All Arguments in the array are expected to have the same emitter
+ */
+Argument.merge = function(argArray, start, end) {
+ start = (start === undefined) ? 0 : start;
+ end = (end === undefined) ? argArray.length : end;
+
+ var joined;
+ for (var i = start; i < end; i++) {
+ var arg = argArray[i];
+ if (!joined) {
+ joined = arg;
+ }
+ else {
+ joined = joined.merge(arg);
+ }
+ }
+ return joined;
+};
+
+/**
+ * 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(Argument.prototype, '_summaryJson', {
+ get: function() {
+ var assignStatus = this.assignment == null ?
+ 'null' :
+ this.assignment.param.name;
+ return '<' + this.prefix + ':' + this.text + ':' + this.suffix + '>' +
+ ' (a=' + assignStatus + ',' + ' t=' + this.type + ')';
+ },
+ enumerable: true
+});
+
+exports.Argument = Argument;
+
+
+/**
+ * BlankArgument is a marker that the argument wasn't typed but is there to
+ * fill a slot. Assignments begin with their arg set to a BlankArgument.
+ */
+function BlankArgument() {
+ this.text = '';
+ this.prefix = '';
+ this.suffix = '';
+}
+
+BlankArgument.prototype = Object.create(Argument.prototype);
+
+BlankArgument.prototype.type = 'BlankArgument';
+
+exports.BlankArgument = BlankArgument;
+
+
+/**
+ * ScriptArgument is a marker that the argument is designed to be JavaScript.
+ * It also implements the special rules that spaces after the { or before the
+ * } are part of the pre/suffix rather than the content, and that they are
+ * never 'blank' so they can be used by Requisition._split() and not raise an
+ * ERROR status due to being blank.
+ */
+function ScriptArgument(text, prefix, suffix) {
+ this.text = text !== undefined ? text : '';
+ this.prefix = prefix !== undefined ? prefix : '';
+ this.suffix = suffix !== undefined ? suffix : '';
+
+ ScriptArgument._moveSpaces(this);
+}
+
+ScriptArgument.prototype = Object.create(Argument.prototype);
+
+ScriptArgument.prototype.type = 'ScriptArgument';
+
+/**
+ * Private/Dangerous: Alters a ScriptArgument to move the spaces at the start
+ * or end of the 'text' into the prefix/suffix. With a string, " a " is 3 chars
+ * long, but with a ScriptArgument, { a } is only one char long.
+ * Arguments are generally supposed to be immutable, so this method should only
+ * be called on a ScriptArgument that isn't exposed to the outside world yet.
+ */
+ScriptArgument._moveSpaces = function(arg) {
+ while (arg.text.charAt(0) === ' ') {
+ arg.prefix = arg.prefix + ' ';
+ arg.text = arg.text.substring(1);
+ }
+
+ while (arg.text.charAt(arg.text.length - 1) === ' ') {
+ arg.suffix = ' ' + arg.suffix;
+ arg.text = arg.text.slice(0, -1);
+ }
+};
+
+/**
+ * As Argument.beget that implements the space rule documented in the ctor.
+ */
+ScriptArgument.prototype.beget = function(options) {
+ options.type = ScriptArgument;
+ var begotten = Argument.prototype.beget.call(this, options);
+ ScriptArgument._moveSpaces(begotten);
+ return begotten;
+};
+
+exports.ScriptArgument = ScriptArgument;
+
+
+/**
+ * Commands like 'echo' with a single string argument, and used with the
+ * special format like: 'echo a b c' effectively have a number of arguments
+ * merged together.
+ */
+function MergedArgument(args, start, end) {
+ if (!Array.isArray(args)) {
+ throw new Error('args is not an array of Arguments');
+ }
+
+ if (start === undefined) {
+ this.args = args;
+ }
+ else {
+ this.args = args.slice(start, end);
+ }
+
+ var arg = Argument.merge(this.args);
+ this.text = arg.text;
+ this.prefix = arg.prefix;
+ this.suffix = arg.suffix;
+}
+
+MergedArgument.prototype = Object.create(Argument.prototype);
+
+MergedArgument.prototype.type = 'MergedArgument';
+
+/**
+ * Keep track of which assignment we've been assigned to, and allow the
+ * original args to do the same.
+ */
+Object.defineProperty(MergedArgument.prototype, 'assignment', {
+ get: function() { return this._assignment; },
+ set: function(assignment) {
+ this._assignment = assignment;
+
+ this.args.forEach(function(arg) {
+ arg.assignment = assignment;
+ }, this);
+ },
+ enumerable: true
+});
+
+MergedArgument.prototype.getArgs = function() {
+ return this.args;
+};
+
+MergedArgument.prototype.equals = function(that) {
+ if (this === that) {
+ return true;
+ }
+ if (that == null || !(that instanceof MergedArgument)) {
+ return false;
+ }
+
+ // We might need to add a check that args is the same here
+
+ return this.text === that.text &&
+ this.prefix === that.prefix && this.suffix === that.suffix;
+};
+
+exports.MergedArgument = MergedArgument;
+
+
+/**
+ * TrueNamedArguments are for when we have an argument like --verbose which
+ * has a boolean value, and thus the opposite of '--verbose' is ''.
+ */
+function TrueNamedArgument(arg) {
+ this.arg = arg;
+ this.text = arg.text;
+ this.prefix = arg.prefix;
+ this.suffix = arg.suffix;
+}
+
+TrueNamedArgument.prototype = Object.create(Argument.prototype);
+
+TrueNamedArgument.prototype.type = 'TrueNamedArgument';
+
+Object.defineProperty(TrueNamedArgument.prototype, 'assignment', {
+ get: function() { return this._assignment; },
+ set: function(assignment) {
+ this._assignment = assignment;
+
+ if (this.arg) {
+ this.arg.assignment = assignment;
+ }
+ },
+ enumerable: true
+});
+
+TrueNamedArgument.prototype.getArgs = function() {
+ return [ this.arg ];
+};
+
+TrueNamedArgument.prototype.equals = function(that) {
+ if (this === that) {
+ return true;
+ }
+ if (that == null || !(that instanceof TrueNamedArgument)) {
+ return false;
+ }
+
+ return this.text === that.text &&
+ this.prefix === that.prefix && this.suffix === that.suffix;
+};
+
+/**
+ * As Argument.beget that rebuilds nameArg and valueArg
+ */
+TrueNamedArgument.prototype.beget = function(options) {
+ if (options.text) {
+ console.error('Can\'t change text of a TrueNamedArgument', this, options);
+ }
+
+ options.type = TrueNamedArgument;
+ var begotten = Argument.prototype.beget.call(this, options);
+ begotten.arg = new Argument(begotten.text, begotten.prefix, begotten.suffix);
+ return begotten;
+};
+
+exports.TrueNamedArgument = TrueNamedArgument;
+
+
+/**
+ * FalseNamedArguments are for when we don't have an argument like --verbose
+ * which has a boolean value, and thus the opposite of '' is '--verbose'.
+ */
+function FalseNamedArgument() {
+ this.text = '';
+ this.prefix = '';
+ this.suffix = '';
+}
+
+FalseNamedArgument.prototype = Object.create(Argument.prototype);
+
+FalseNamedArgument.prototype.type = 'FalseNamedArgument';
+
+FalseNamedArgument.prototype.getArgs = function() {
+ return [ ];
+};
+
+FalseNamedArgument.prototype.equals = function(that) {
+ if (this === that) {
+ return true;
+ }
+ if (that == null || !(that instanceof FalseNamedArgument)) {
+ return false;
+ }
+
+ return this.text === that.text &&
+ this.prefix === that.prefix && this.suffix === that.suffix;
+};
+
+exports.FalseNamedArgument = FalseNamedArgument;
+
+
+/**
+ * A named argument is for cases where we have input in one of the following
+ * formats:
+ * <ul>
+ * <li>--param value
+ * <li>-p value
+ * </ul>
+ * We model this as a normal argument but with a long prefix.
+ *
+ * There are 2 ways to construct a NamedArgument. One using 2 Arguments which
+ * are taken to be the argument for the name (e.g. '--param') and one for the
+ * value to assign to that parameter.
+ * Alternatively, you can pass in the text/prefix/suffix values in the same
+ * way as an Argument is constructed. If you do this then you are expected to
+ * assign to nameArg and valueArg before exposing the new NamedArgument.
+ */
+function NamedArgument() {
+ if (typeof arguments[0] === 'string') {
+ this.nameArg = null;
+ this.valueArg = null;
+ this.text = arguments[0];
+ this.prefix = arguments[1];
+ this.suffix = arguments[2];
+ }
+ else if (arguments[1] == null) {
+ this.nameArg = arguments[0];
+ this.valueArg = null;
+ this.text = '';
+ this.prefix = this.nameArg.toString();
+ this.suffix = '';
+ }
+ else {
+ this.nameArg = arguments[0];
+ this.valueArg = arguments[1];
+ this.text = this.valueArg.text;
+ this.prefix = this.nameArg.toString() + this.valueArg.prefix;
+ this.suffix = this.valueArg.suffix;
+ }
+}
+
+NamedArgument.prototype = Object.create(Argument.prototype);
+
+NamedArgument.prototype.type = 'NamedArgument';
+
+Object.defineProperty(NamedArgument.prototype, 'assignment', {
+ get: function() { return this._assignment; },
+ set: function(assignment) {
+ this._assignment = assignment;
+
+ this.nameArg.assignment = assignment;
+ if (this.valueArg != null) {
+ this.valueArg.assignment = assignment;
+ }
+ },
+ enumerable: true
+});
+
+NamedArgument.prototype.getArgs = function() {
+ return this.valueArg ? [ this.nameArg, this.valueArg ] : [ this.nameArg ];
+};
+
+NamedArgument.prototype.equals = function(that) {
+ if (this === that) {
+ return true;
+ }
+ if (that == null) {
+ return false;
+ }
+
+ if (!(that instanceof NamedArgument)) {
+ return false;
+ }
+
+ // We might need to add a check that nameArg and valueArg are the same
+
+ return this.text === that.text &&
+ this.prefix === that.prefix && this.suffix === that.suffix;
+};
+
+/**
+ * As Argument.beget that rebuilds nameArg and valueArg
+ */
+NamedArgument.prototype.beget = function(options) {
+ options.type = NamedArgument;
+ var begotten = Argument.prototype.beget.call(this, options);
+
+ // Cut the prefix into |whitespace|non-whitespace|whitespace+quote so we can
+ // rebuild nameArg and valueArg from the parts
+ var matches = /^([\s]*)([^\s]*)([\s]*['"]?)$/.exec(begotten.prefix);
+
+ if (this.valueArg == null && begotten.text === '') {
+ begotten.nameArg = new Argument(matches[2], matches[1], matches[3]);
+ begotten.valueArg = null;
+ }
+ else {
+ begotten.nameArg = new Argument(matches[2], matches[1], '');
+ begotten.valueArg = new Argument(begotten.text, matches[3], begotten.suffix);
+ }
+
+ return begotten;
+};
+
+exports.NamedArgument = NamedArgument;
+
+
+/**
+ * An argument the groups together a number of plain arguments together so they
+ * can be jointly assigned to a single array parameter
+ */
+function ArrayArgument() {
+ this.args = [];
+}
+
+ArrayArgument.prototype = Object.create(Argument.prototype);
+
+ArrayArgument.prototype.type = 'ArrayArgument';
+
+ArrayArgument.prototype.addArgument = function(arg) {
+ this.args.push(arg);
+};
+
+ArrayArgument.prototype.addArguments = function(args) {
+ Array.prototype.push.apply(this.args, args);
+};
+
+ArrayArgument.prototype.getArguments = function() {
+ return this.args;
+};
+
+Object.defineProperty(ArrayArgument.prototype, 'assignment', {
+ get: function() { return this._assignment; },
+ set: function(assignment) {
+ this._assignment = assignment;
+
+ this.args.forEach(function(arg) {
+ arg.assignment = assignment;
+ }, this);
+ },
+ enumerable: true
+});
+
+ArrayArgument.prototype.getArgs = function() {
+ return this.args;
+};
+
+ArrayArgument.prototype.equals = function(that) {
+ if (this === that) {
+ return true;
+ }
+ if (that == null) {
+ return false;
+ }
+
+ if (that.type !== 'ArrayArgument') {
+ return false;
+ }
+
+ if (this.args.length !== that.args.length) {
+ return false;
+ }
+
+ for (var i = 0; i < this.args.length; i++) {
+ if (!this.args[i].equals(that.args[i])) {
+ return false;
+ }
+ }
+
+ return true;
+};
+
+/**
+ * Helper when we're putting arguments back together
+ */
+ArrayArgument.prototype.toString = function() {
+ return '{' + this.args.map(function(arg) {
+ return arg.toString();
+ }, this).join(',') + '}';
+};
+
+exports.ArrayArgument = ArrayArgument;
+
+/**
+ * Some types can detect validity, that is to say they can distinguish between
+ * valid and invalid values.
+ * We might want to change these constants to be numbers for better performance
+ */
+var Status = {
+ /**
+ * The conversion process worked without any problem, and the value is
+ * valid. There are a number of failure states, so the best way to check
+ * for failure is (x !== Status.VALID)
+ */
+ VALID: {
+ toString: function() { return 'VALID'; },
+ valueOf: function() { return 0; }
+ },
+
+ /**
+ * A conversion process failed, however it was noted that the string
+ * provided to 'parse()' could be VALID by the addition of more characters,
+ * so the typing may not be actually incorrect yet, just unfinished.
+ * @see Status.ERROR
+ */
+ INCOMPLETE: {
+ toString: function() { return 'INCOMPLETE'; },
+ valueOf: function() { return 1; }
+ },
+
+ /**
+ * The conversion process did not work, the value should be null and a
+ * reason for failure should have been provided. In addition some
+ * completion values may be available.
+ * @see Status.INCOMPLETE
+ */
+ ERROR: {
+ toString: function() { return 'ERROR'; },
+ valueOf: function() { return 2; }
+ },
+
+ /**
+ * A combined status is the worser of the provided statuses. The statuses
+ * can be provided either as a set of arguments or a single array
+ */
+ combine: function() {
+ var combined = Status.VALID;
+ for (var i = 0; i < arguments.length; i++) {
+ var status = arguments[i];
+ if (Array.isArray(status)) {
+ status = Status.combine.apply(null, status);
+ }
+ if (status > combined) {
+ combined = status;
+ }
+ }
+ return combined;
+ },
+
+ fromString: function(str) {
+ switch (str) {
+ case Status.VALID.toString():
+ return Status.VALID;
+ case Status.INCOMPLETE.toString():
+ return Status.INCOMPLETE;
+ case Status.ERROR.toString():
+ return Status.ERROR;
+ default:
+ throw new Error('\'' + str + '\' is not a status');
+ }
+ }
+};
+
+exports.Status = Status;
+
+
+/**
+ * The type.parse() method converts an Argument into a value, Conversion is
+ * a wrapper to that value.
+ * Conversion is needed to collect a number of properties related to that
+ * conversion in one place, i.e. to handle errors and provide traceability.
+ * @param value The result of the conversion. null if status == VALID
+ * @param arg The data from which the conversion was made
+ * @param status See the Status values [VALID|INCOMPLETE|ERROR] defined above.
+ * The default status is Status.VALID.
+ * @param message If status=ERROR, there should be a message to describe the
+ * error. A message is not needed unless for other statuses, but could be
+ * present for any status including VALID (in the case where we want to note a
+ * warning, for example).
+ * See BUG 664676: GCLI conversion error messages should be localized
+ * @param predictions If status=INCOMPLETE, there could be predictions as to
+ * the options available to complete the input.
+ * We generally expect there to be about 7 predictions (to match human list
+ * comprehension ability) however it is valid to provide up to about 20,
+ * or less. It is the job of the predictor to decide a smart cut-off.
+ * For example if there are 4 very good matches and 4 very poor ones,
+ * probably only the 4 very good matches should be presented.
+ * The predictions are presented either as an array of prediction objects or as
+ * a function which returns this array when called with no parameters.
+ * Each prediction object has the following shape:
+ * {
+ * name: '...', // textual completion. i.e. what the cli uses
+ * value: { ... }, // value behind the textual completion
+ * incomplete: true // this completion is only partial (optional)
+ * }
+ * The 'incomplete' property could be used to denote a valid completion which
+ * could have sub-values (e.g. for tree navigation).
+ */
+function Conversion(value, arg, status, message, predictions) {
+ if (arg == null) {
+ throw new Error('Missing arg');
+ }
+
+ if (predictions != null && typeof predictions !== 'function' &&
+ !Array.isArray(predictions) && typeof predictions.then !== 'function') {
+ throw new Error('predictions exists but is not a promise, function or array');
+ }
+
+ if (status === Status.ERROR && !message) {
+ throw new Error('Conversion has status=ERROR but no message');
+ }
+
+ this.value = value;
+ this.arg = arg;
+ this._status = status || Status.VALID;
+ this.message = message;
+ this.predictions = predictions;
+}
+
+/**
+ * Ensure that all arguments that are part of this conversion know what they
+ * are assigned to.
+ * @param assignment The Assignment (param/conversion link) to inform the
+ * argument about.
+ */
+Object.defineProperty(Conversion.prototype, 'assignment', {
+ get: function() { return this.arg.assignment; },
+ set: function(assignment) { this.arg.assignment = assignment; },
+ enumerable: true
+});
+
+/**
+ * Work out if there is information provided in the contained argument.
+ */
+Conversion.prototype.isDataProvided = function() {
+ return this.arg.type !== 'BlankArgument';
+};
+
+/**
+ * 2 conversions are equal if and only if their args are equal (argEquals) and
+ * their values are equal (valueEquals).
+ * @param that The conversion object to compare against.
+ */
+Conversion.prototype.equals = function(that) {
+ if (this === that) {
+ return true;
+ }
+ if (that == null) {
+ return false;
+ }
+ return this.valueEquals(that) && this.argEquals(that);
+};
+
+/**
+ * Check that the value in this conversion is strict equal to the value in the
+ * provided conversion.
+ * @param that The conversion to compare values with
+ */
+Conversion.prototype.valueEquals = function(that) {
+ return that != null && this.value === that.value;
+};
+
+/**
+ * Check that the argument in this conversion is equal to the value in the
+ * provided conversion as defined by the argument (i.e. arg.equals).
+ * @param that The conversion to compare arguments with
+ */
+Conversion.prototype.argEquals = function(that) {
+ return that == null ? false : this.arg.equals(that.arg);
+};
+
+/**
+ * Accessor for the status of this conversion
+ */
+Conversion.prototype.getStatus = function(arg) {
+ return this._status;
+};
+
+/**
+ * Defined by the toString() value provided by the argument
+ */
+Conversion.prototype.toString = function() {
+ return this.arg.toString();
+};
+
+/**
+ * If status === INCOMPLETE, then we may be able to provide predictions as to
+ * how the argument can be completed.
+ * @return An array of items, or a promise of an array of items, where each
+ * item is an object with the following properties:
+ * - name (mandatory): Displayed to the user, and typed in. No whitespace
+ * - description (optional): Short string for display in a tool-tip
+ * - manual (optional): Longer description which details usage
+ * - incomplete (optional): Indicates that the prediction if used should not
+ * be considered necessarily sufficient, which typically will mean that the
+ * UI should not append a space to the completion
+ * - value (optional): If a value property is present, this will be used as the
+ * value of the conversion, otherwise the item itself will be used.
+ */
+Conversion.prototype.getPredictions = function(context) {
+ if (typeof this.predictions === 'function') {
+ return this.predictions(context);
+ }
+ return Promise.resolve(this.predictions || []);
+};
+
+/**
+ * Return a promise of an index constrained by the available predictions.
+ * i.e. (index % predicitons.length)
+ * This code can probably be removed when the Firefox developer toolbar isn't
+ * needed any more.
+ */
+Conversion.prototype.constrainPredictionIndex = function(context, index) {
+ if (index == null) {
+ return Promise.resolve();
+ }
+
+ return this.getPredictions(context).then(function(value) {
+ if (value.length === 0) {
+ return undefined;
+ }
+
+ index = index % value.length;
+ if (index < 0) {
+ index = value.length + index;
+ }
+ return index;
+ }.bind(this));
+};
+
+/**
+ * Constant to allow everyone to agree on the maximum number of predictions
+ * that should be provided. We actually display 1 less than this number.
+ */
+Conversion.maxPredictions = 9;
+
+exports.Conversion = Conversion;
+
+
+/**
+ * ArrayConversion is a special Conversion, needed because arrays are converted
+ * member by member rather then as a whole, which means we can track the
+ * conversion if individual array elements. So an ArrayConversion acts like a
+ * normal Conversion (which is needed as Assignment requires a Conversion) but
+ * it can also be devolved into a set of Conversions for each array member.
+ */
+function ArrayConversion(conversions, arg) {
+ this.arg = arg;
+ this.conversions = conversions;
+ this.value = conversions.map(function(conversion) {
+ return conversion.value;
+ }, this);
+
+ this._status = Status.combine(conversions.map(function(conversion) {
+ return conversion.getStatus();
+ }));
+
+ // This message is just for reporting errors like "not enough values"
+ // rather that for problems with individual values.
+ this.message = '';
+
+ // Predictions are generally provided by individual values
+ this.predictions = [];
+}
+
+ArrayConversion.prototype = Object.create(Conversion.prototype);
+
+Object.defineProperty(ArrayConversion.prototype, 'assignment', {
+ get: function() { return this._assignment; },
+ set: function(assignment) {
+ this._assignment = assignment;
+
+ this.conversions.forEach(function(conversion) {
+ conversion.assignment = assignment;
+ }, this);
+ },
+ enumerable: true
+});
+
+ArrayConversion.prototype.getStatus = function(arg) {
+ if (arg && arg.conversion) {
+ return arg.conversion.getStatus();
+ }
+ return this._status;
+};
+
+ArrayConversion.prototype.isDataProvided = function() {
+ return this.conversions.length > 0;
+};
+
+ArrayConversion.prototype.valueEquals = function(that) {
+ if (that == null) {
+ return false;
+ }
+
+ if (!(that instanceof ArrayConversion)) {
+ throw new Error('Can\'t compare values with non ArrayConversion');
+ }
+
+ if (this.value === that.value) {
+ return true;
+ }
+
+ if (this.value.length !== that.value.length) {
+ return false;
+ }
+
+ for (var i = 0; i < this.conversions.length; i++) {
+ if (!this.conversions[i].valueEquals(that.conversions[i])) {
+ return false;
+ }
+ }
+
+ return true;
+};
+
+ArrayConversion.prototype.toString = function() {
+ return '[ ' + this.conversions.map(function(conversion) {
+ return conversion.toString();
+ }, this).join(', ') + ' ]';
+};
+
+exports.ArrayConversion = ArrayConversion;
+
+
+/**
+ * Most of our types are 'static' e.g. there is only one type of 'string',
+ * however some types like 'selection' and 'delegate' are customizable.
+ * The basic Type type isn't useful, but does provide documentation about what
+ * types do.
+ */
+function Type() {
+}
+
+/**
+ * Get a JSONable data structure that entirely describes this type.
+ * commandName and paramName are the names of the command and parameter which
+ * we are remoting to help the server get back to the remoted action.
+ */
+Type.prototype.getSpec = function(commandName, paramName) {
+ throw new Error('Not implemented');
+};
+
+/**
+ * Convert the given <tt>value</tt> to a string representation.
+ * Where possible, there should be round-tripping between values and their
+ * string representations.
+ * @param value The object to convert into a string
+ * @param context An ExecutionContext to allow basic Requisition access
+ */
+Type.prototype.stringify = function(value, context) {
+ throw new Error('Not implemented');
+};
+
+/**
+ * Convert the given <tt>arg</tt> to an instance of this type.
+ * Where possible, there should be round-tripping between values and their
+ * string representations.
+ * @param arg An instance of <tt>Argument</tt> to convert.
+ * @param context An ExecutionContext to allow basic Requisition access
+ * @return Conversion
+ */
+Type.prototype.parse = function(arg, context) {
+ throw new Error('Not implemented');
+};
+
+/**
+ * A convenience method for times when you don't have an argument to parse
+ * but instead have a string.
+ * @see #parse(arg)
+ */
+Type.prototype.parseString = function(str, context) {
+ return this.parse(new Argument(str), context);
+};
+
+/**
+ * The plug-in system, and other things need to know what this type is
+ * called. The name alone is not enough to fully specify a type. Types like
+ * 'selection' and 'delegate' need extra data, however this function returns
+ * only the name, not the extra data.
+ */
+Type.prototype.name = undefined;
+
+/**
+ * If there is some concept of a lower or higher value, return it,
+ * otherwise return undefined.
+ * @param by number indicating how much to nudge by, usually +1 or -1 which is
+ * caused by the user pressing the UP/DOWN keys with the cursor in this type
+ */
+Type.prototype.nudge = function(value, by, context) {
+ return undefined;
+};
+
+/**
+ * The 'blank value' of most types is 'undefined', but there are exceptions;
+ * This allows types to specify a better conversion from empty string than
+ * 'undefined'.
+ * 2 known examples of this are boolean -> false and array -> []
+ */
+Type.prototype.getBlank = function(context) {
+ return new Conversion(undefined, new BlankArgument(), Status.INCOMPLETE, '');
+};
+
+/**
+ * This is something of a hack for the benefit of DelegateType which needs to
+ * be able to lie about it's type for fields to accept it as one of their own.
+ * Sub-types can ignore this unless they're DelegateType.
+ * @param context An ExecutionContext to allow basic Requisition access
+ */
+Type.prototype.getType = function(context) {
+ return this;
+};
+
+/**
+ * addItems allows registrations of a number of things. This allows it to know
+ * what type of item, and how it should be registered.
+ */
+Type.prototype.item = 'type';
+
+exports.Type = Type;
+
+/**
+ * 'Types' represents a registry of types
+ */
+function Types() {
+ // Invariant: types[name] = type.name
+ this._registered = {};
+}
+
+exports.Types = Types;
+
+/**
+ * Get an array of the names of registered types
+ */
+Types.prototype.getTypeNames = function() {
+ return Object.keys(this._registered);
+};
+
+/**
+ * Add a new type to the list available to the system.
+ * You can pass 2 things to this function - either an instance of Type, in
+ * which case we return this instance when #getType() is called with a 'name'
+ * that matches type.name.
+ * Also you can pass in a constructor (i.e. function) in which case when
+ * #getType() is called with a 'name' that matches Type.prototype.name we will
+ * pass the typeSpec into this constructor.
+ */
+Types.prototype.add = function(type) {
+ if (typeof type === 'object') {
+ if (!type.name) {
+ throw new Error('All registered types must have a name');
+ }
+
+ if (type instanceof Type) {
+ this._registered[type.name] = type;
+ }
+ else {
+ var name = type.name;
+ var parent = type.parent;
+ type.name = parent;
+ delete type.parent;
+
+ this._registered[name] = this.createType(type);
+
+ type.name = name;
+ type.parent = parent;
+ }
+ }
+ else if (typeof type === 'function') {
+ if (!type.prototype.name) {
+ throw new Error('All registered types must have a name');
+ }
+ this._registered[type.prototype.name] = type;
+ }
+ else {
+ throw new Error('Unknown type: ' + type);
+ }
+};
+
+/**
+ * Remove a type from the list available to the system
+ */
+Types.prototype.remove = function(type) {
+ delete this._registered[type.name];
+};
+
+/**
+ * Find a previously registered type
+ */
+Types.prototype.createType = function(typeSpec) {
+ if (typeof typeSpec === 'string') {
+ typeSpec = { name: typeSpec };
+ }
+
+ if (typeof typeSpec !== 'object') {
+ throw new Error('Can\'t extract type from ' + typeSpec);
+ }
+
+ var NewTypeCtor, newType;
+ if (typeSpec.name == null || typeSpec.name == 'type') {
+ NewTypeCtor = Type;
+ }
+ else {
+ NewTypeCtor = this._registered[typeSpec.name];
+ }
+
+ if (!NewTypeCtor) {
+ console.error('Known types: ' + Object.keys(this._registered).join(', '));
+ throw new Error('Unknown type: \'' + typeSpec.name + '\'');
+ }
+
+ if (typeof NewTypeCtor === 'function') {
+ newType = new NewTypeCtor(typeSpec);
+ }
+ else {
+ // clone 'type'
+ newType = {};
+ util.copyProperties(NewTypeCtor, newType);
+ }
+
+ // Copy the properties of typeSpec onto the new type
+ util.copyProperties(typeSpec, newType);
+
+ // Several types need special powers to create child types
+ newType.types = this;
+
+ if (typeof NewTypeCtor !== 'function') {
+ if (typeof newType.constructor === 'function') {
+ newType.constructor();
+ }
+ }
+
+ return newType;
+};
diff --git a/devtools/shared/gcli/source/lib/gcli/types/union.js b/devtools/shared/gcli/source/lib/gcli/types/union.js
new file mode 100644
index 000000000..c98d3411b
--- /dev/null
+++ b/devtools/shared/gcli/source/lib/gcli/types/union.js
@@ -0,0 +1,117 @@
+/*
+ * Copyright 2014, 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 l10n = require('../util/l10n');
+var Conversion = require('./types').Conversion;
+var Status = require('./types').Status;
+
+exports.items = [
+ {
+ // The union type allows for a combination of different parameter types.
+ item: 'type',
+ name: 'union',
+ hasPredictions: true,
+
+ constructor: function() {
+ // Get the properties of the type. Later types in the list should always
+ // be more general, so 'catch all' types like string must be last
+ this.alternatives = this.alternatives.map(function(typeData) {
+ return this.types.createType(typeData);
+ }.bind(this));
+ },
+
+ getSpec: function(command, param) {
+ var spec = { name: 'union', alternatives: [] };
+ this.alternatives.forEach(function(type) {
+ spec.alternatives.push(type.getSpec(command, param));
+ }.bind(this));
+ return spec;
+ },
+
+ stringify: function(value, context) {
+ if (value == null) {
+ return '';
+ }
+
+ var type = this.alternatives.find(function(typeData) {
+ return typeData.name === value.type;
+ });
+
+ return type.stringify(value[value.type], context);
+ },
+
+ parse: function(arg, context) {
+ var conversionPromises = this.alternatives.map(function(type) {
+ return type.parse(arg, context);
+ }.bind(this));
+
+ return Promise.all(conversionPromises).then(function(conversions) {
+ // Find a list of the predictions made by any conversion
+ var predictionPromises = conversions.map(function(conversion) {
+ return conversion.getPredictions(context);
+ }.bind(this));
+
+ return Promise.all(predictionPromises).then(function(allPredictions) {
+ // Take one prediction from each set of predictions, ignoring
+ // duplicates, until we've got up to Conversion.maxPredictions
+ var maxIndex = allPredictions.reduce(function(prev, prediction) {
+ return Math.max(prev, prediction.length);
+ }.bind(this), 0);
+ var predictions = [];
+
+ indexLoop:
+ for (var index = 0; index < maxIndex; index++) {
+ for (var p = 0; p <= allPredictions.length; p++) {
+ if (predictions.length >= Conversion.maxPredictions) {
+ break indexLoop;
+ }
+
+ if (allPredictions[p] != null) {
+ var prediction = allPredictions[p][index];
+ if (prediction != null && predictions.indexOf(prediction) === -1) {
+ predictions.push(prediction);
+ }
+ }
+ }
+ }
+
+ var bestStatus = Status.ERROR;
+ var value;
+ for (var i = 0; i < conversions.length; i++) {
+ var conversion = conversions[i];
+ var thisStatus = conversion.getStatus(arg);
+ if (thisStatus < bestStatus) {
+ bestStatus = thisStatus;
+ }
+ if (bestStatus === Status.VALID) {
+ var type = this.alternatives[i].name;
+ value = { type: type };
+ value[type] = conversion.value;
+ break;
+ }
+ }
+
+ var msg = (bestStatus === Status.VALID) ?
+ '' :
+ l10n.lookupFormat('typesSelectionNomatch', [ arg.text ]);
+ return new Conversion(value, arg, bestStatus, msg, predictions);
+ }.bind(this));
+ }.bind(this));
+ },
+ }
+];
diff --git a/devtools/shared/gcli/source/lib/gcli/types/url.js b/devtools/shared/gcli/source/lib/gcli/types/url.js
new file mode 100644
index 000000000..73895d66b
--- /dev/null
+++ b/devtools/shared/gcli/source/lib/gcli/types/url.js
@@ -0,0 +1,86 @@
+/*
+ * 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 host = require('../util/host');
+var Status = require('./types').Status;
+var Conversion = require('./types').Conversion;
+
+exports.items = [
+ {
+ item: 'type',
+ name: 'url',
+
+ getSpec: function() {
+ return 'url';
+ },
+
+ stringify: function(value, context) {
+ if (value == null) {
+ return '';
+ }
+ return value.href;
+ },
+
+ parse: function(arg, context) {
+ var conversion;
+
+ try {
+ var url = host.createUrl(arg.text);
+ conversion = new Conversion(url, arg);
+ }
+ catch (ex) {
+ var predictions = [];
+ var status = Status.ERROR;
+
+ // Maybe the URL was missing a scheme?
+ if (arg.text.indexOf('://') === -1) {
+ [ 'http', 'https' ].forEach(function(scheme) {
+ try {
+ var http = host.createUrl(scheme + '://' + arg.text);
+ predictions.push({ name: http.href, value: http });
+ }
+ catch (ex) {
+ // Ignore
+ }
+ }.bind(this));
+
+ // Try to create a URL with the current page as a base ref
+ if ('window' in context.environment) {
+ try {
+ var base = context.environment.window.location.href;
+ var localized = host.createUrl(arg.text, base);
+ predictions.push({ name: localized.href, value: localized });
+ }
+ catch (ex) {
+ // Ignore
+ }
+ }
+ }
+
+ if (predictions.length > 0) {
+ status = Status.INCOMPLETE;
+ }
+
+ conversion = new Conversion(undefined, arg, status,
+ ex.message, predictions);
+ }
+
+ return Promise.resolve(conversion);
+ }
+ }
+];
diff --git a/devtools/shared/gcli/source/lib/gcli/ui/focus.js b/devtools/shared/gcli/source/lib/gcli/ui/focus.js
new file mode 100644
index 000000000..6d3761cca
--- /dev/null
+++ b/devtools/shared/gcli/source/lib/gcli/ui/focus.js
@@ -0,0 +1,403 @@
+/*
+ * 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 l10n = require('../util/l10n');
+
+/**
+ * Record how much help the user wants from the tooltip
+ */
+var Eagerness = {
+ NEVER: 1,
+ SOMETIMES: 2,
+ ALWAYS: 3
+};
+
+/**
+ * Export the eagerHelper setting
+ */
+exports.items = [
+ {
+ item: 'setting',
+ name: 'eagerHelper',
+ type: {
+ name: 'selection',
+ lookup: [
+ { name: 'never', value: Eagerness.NEVER },
+ { name: 'sometimes', value: Eagerness.SOMETIMES },
+ { name: 'always', value: Eagerness.ALWAYS }
+ ]
+ },
+ defaultValue: Eagerness.SOMETIMES,
+ description: l10n.lookup('eagerHelperDesc'),
+ ignoreTypeDifference: true
+ }
+];
+
+/**
+ * FocusManager solves the problem of tracking focus among a set of nodes.
+ * The specific problem we are solving is when the hint element must be visible
+ * if either the command line or any of the inputs in the hint element has the
+ * focus, and invisible at other times, without hiding and showing the hint
+ * element even briefly as the focus changes between them.
+ * It does this simply by postponing the hide events by 250ms to see if
+ * something else takes focus.
+ */
+function FocusManager(document, settings) {
+ if (document == null) {
+ throw new Error('document == null');
+ }
+
+ this.document = document;
+ this.settings = settings;
+ this.debug = false;
+ this.blurDelay = 150;
+ this.window = this.document.defaultView;
+
+ this._blurDelayTimeout = null; // Result of setTimeout in delaying a blur
+ this._monitoredElements = []; // See addMonitoredElement()
+
+ this._isError = false;
+ this._hasFocus = false;
+ this._helpRequested = false;
+ this._recentOutput = false;
+
+ this.onVisibilityChange = util.createEvent('FocusManager.onVisibilityChange');
+
+ this._focused = this._focused.bind(this);
+ if (this.document.addEventListener) {
+ this.document.addEventListener('focus', this._focused, true);
+ }
+
+ var eagerHelper = this.settings.get('eagerHelper');
+ eagerHelper.onChange.add(this._eagerHelperChanged, this);
+
+ this.isTooltipVisible = undefined;
+ this.isOutputVisible = undefined;
+ this._checkShow();
+}
+
+/**
+ * Avoid memory leaks
+ */
+FocusManager.prototype.destroy = function() {
+ var eagerHelper = this.settings.get('eagerHelper');
+ eagerHelper.onChange.remove(this._eagerHelperChanged, this);
+
+ this.document.removeEventListener('focus', this._focused, true);
+
+ for (var i = 0; i < this._monitoredElements.length; i++) {
+ var monitor = this._monitoredElements[i];
+ console.error('Hanging monitored element: ', monitor.element);
+
+ monitor.element.removeEventListener('focus', monitor.onFocus, true);
+ monitor.element.removeEventListener('blur', monitor.onBlur, true);
+ }
+
+ if (this._blurDelayTimeout) {
+ this.window.clearTimeout(this._blurDelayTimeout);
+ this._blurDelayTimeout = null;
+ }
+
+ this._focused = undefined;
+ this.document = undefined;
+ this.settings = undefined;
+ this.window = undefined;
+};
+
+/**
+ * The easy way to include an element in the set of things that are part of the
+ * aggregate focus. Using [add|remove]MonitoredElement() is a simpler way of
+ * option than calling report[Focus|Blur]()
+ * @param element The element on which to track focus|blur events
+ * @param where Optional source string for debugging only
+ */
+FocusManager.prototype.addMonitoredElement = function(element, where) {
+ if (this.debug) {
+ console.log('FocusManager.addMonitoredElement(' + (where || 'unknown') + ')');
+ }
+
+ var monitor = {
+ element: element,
+ where: where,
+ onFocus: function() { this._reportFocus(where); }.bind(this),
+ onBlur: function() { this._reportBlur(where); }.bind(this)
+ };
+
+ element.addEventListener('focus', monitor.onFocus, true);
+ element.addEventListener('blur', monitor.onBlur, true);
+
+ if (this.document.activeElement === element) {
+ this._reportFocus(where);
+ }
+
+ this._monitoredElements.push(monitor);
+};
+
+/**
+ * Undo the effects of addMonitoredElement()
+ * @param element The element to stop tracking
+ * @param where Optional source string for debugging only
+ */
+FocusManager.prototype.removeMonitoredElement = function(element, where) {
+ if (this.debug) {
+ console.log('FocusManager.removeMonitoredElement(' + (where || 'unknown') + ')');
+ }
+
+ this._monitoredElements = this._monitoredElements.filter(function(monitor) {
+ if (monitor.element === element) {
+ element.removeEventListener('focus', monitor.onFocus, true);
+ element.removeEventListener('blur', monitor.onBlur, true);
+ return false;
+ }
+ return true;
+ });
+};
+
+/**
+ * Monitor for new command executions
+ */
+FocusManager.prototype.updatePosition = function(dimensions) {
+ var ev = {
+ tooltipVisible: this.isTooltipVisible,
+ outputVisible: this.isOutputVisible,
+ dimensions: dimensions
+ };
+ this.onVisibilityChange(ev);
+};
+
+/**
+ * Monitor for new command executions
+ */
+FocusManager.prototype.outputted = function() {
+ this._recentOutput = true;
+ this._helpRequested = false;
+ this._checkShow();
+};
+
+/**
+ * We take a focus event anywhere to be an indication that we might be about
+ * to lose focus
+ */
+FocusManager.prototype._focused = function() {
+ this._reportBlur('document');
+};
+
+/**
+ * Some component has received a 'focus' event. This sets the internal status
+ * straight away and informs the listeners
+ * @param where Optional source string for debugging only
+ */
+FocusManager.prototype._reportFocus = function(where) {
+ if (this.debug) {
+ console.log('FocusManager._reportFocus(' + (where || 'unknown') + ')');
+ }
+
+ if (this._blurDelayTimeout) {
+ if (this.debug) {
+ console.log('FocusManager.cancelBlur');
+ }
+ this.window.clearTimeout(this._blurDelayTimeout);
+ this._blurDelayTimeout = null;
+ }
+
+ if (!this._hasFocus) {
+ this._hasFocus = true;
+ }
+ this._checkShow();
+};
+
+/**
+ * Some component has received a 'blur' event. This waits for a while to see if
+ * we are going to get any subsequent 'focus' events and then sets the internal
+ * status and informs the listeners
+ * @param where Optional source string for debugging only
+ */
+FocusManager.prototype._reportBlur = function(where) {
+ if (this.debug) {
+ console.log('FocusManager._reportBlur(' + where + ')');
+ }
+
+ if (this._hasFocus) {
+ if (this._blurDelayTimeout) {
+ if (this.debug) {
+ console.log('FocusManager.blurPending');
+ }
+ return;
+ }
+
+ this._blurDelayTimeout = this.window.setTimeout(function() {
+ if (this.debug) {
+ console.log('FocusManager.blur');
+ }
+ this._hasFocus = false;
+ this._checkShow();
+ this._blurDelayTimeout = null;
+ }.bind(this), this.blurDelay);
+ }
+};
+
+/**
+ * The setting has changed
+ */
+FocusManager.prototype._eagerHelperChanged = function() {
+ this._checkShow();
+};
+
+/**
+ * The terminal tells us about keyboard events so we can decide to delay
+ * showing the tooltip element
+ */
+FocusManager.prototype.onInputChange = function() {
+ this._recentOutput = false;
+ this._checkShow();
+};
+
+/**
+ * Generally called for something like a F1 key press, when the user explicitly
+ * wants help
+ */
+FocusManager.prototype.helpRequest = function() {
+ if (this.debug) {
+ console.log('FocusManager.helpRequest');
+ }
+
+ this._helpRequested = true;
+ this._recentOutput = false;
+ this._checkShow();
+};
+
+/**
+ * Generally called for something like a ESC key press, when the user explicitly
+ * wants to get rid of the help
+ */
+FocusManager.prototype.removeHelp = function() {
+ if (this.debug) {
+ console.log('FocusManager.removeHelp');
+ }
+
+ this._importantFieldFlag = false;
+ this._isError = false;
+ this._helpRequested = false;
+ this._recentOutput = false;
+ this._checkShow();
+};
+
+/**
+ * Set to true whenever a field thinks it's output is important
+ */
+FocusManager.prototype.setImportantFieldFlag = function(flag) {
+ if (this.debug) {
+ console.log('FocusManager.setImportantFieldFlag', flag);
+ }
+ this._importantFieldFlag = flag;
+ this._checkShow();
+};
+
+/**
+ * Set to true whenever a field thinks it's output is important
+ */
+FocusManager.prototype.setError = function(isError) {
+ if (this.debug) {
+ console.log('FocusManager._isError', isError);
+ }
+ this._isError = isError;
+ this._checkShow();
+};
+
+/**
+ * Helper to compare the current showing state with the value calculated by
+ * _shouldShow() and take appropriate action
+ */
+FocusManager.prototype._checkShow = function() {
+ var fire = false;
+ var ev = {
+ tooltipVisible: this.isTooltipVisible,
+ outputVisible: this.isOutputVisible
+ };
+
+ var showTooltip = this._shouldShowTooltip();
+ if (this.isTooltipVisible !== showTooltip.visible) {
+ ev.tooltipVisible = this.isTooltipVisible = showTooltip.visible;
+ fire = true;
+ }
+
+ var showOutput = this._shouldShowOutput();
+ if (this.isOutputVisible !== showOutput.visible) {
+ ev.outputVisible = this.isOutputVisible = showOutput.visible;
+ fire = true;
+ }
+
+ if (fire) {
+ if (this.debug) {
+ console.log('FocusManager.onVisibilityChange', ev);
+ }
+ this.onVisibilityChange(ev);
+ }
+};
+
+/**
+ * Calculate if we should be showing or hidden taking into account all the
+ * available inputs
+ */
+FocusManager.prototype._shouldShowTooltip = function() {
+ var eagerHelper = this.settings.get('eagerHelper');
+ if (eagerHelper.value === Eagerness.NEVER) {
+ return { visible: false, reason: 'eagerHelperNever' };
+ }
+
+ if (eagerHelper.value === Eagerness.ALWAYS) {
+ return { visible: true, reason: 'eagerHelperAlways' };
+ }
+
+ if (!this._hasFocus) {
+ return { visible: false, reason: 'notHasFocus' };
+ }
+
+ if (this._isError) {
+ return { visible: true, reason: 'isError' };
+ }
+
+ if (this._helpRequested) {
+ return { visible: true, reason: 'helpRequested' };
+ }
+
+ if (this._importantFieldFlag) {
+ return { visible: true, reason: 'importantFieldFlag' };
+ }
+
+ return { visible: false, reason: 'default' };
+};
+
+/**
+ * Calculate if we should be showing or hidden taking into account all the
+ * available inputs
+ */
+FocusManager.prototype._shouldShowOutput = function() {
+ if (!this._hasFocus) {
+ return { visible: false, reason: 'notHasFocus' };
+ }
+
+ if (this._recentOutput) {
+ return { visible: true, reason: 'recentOutput' };
+ }
+
+ return { visible: false, reason: 'default' };
+};
+
+exports.FocusManager = FocusManager;
diff --git a/devtools/shared/gcli/source/lib/gcli/ui/history.js b/devtools/shared/gcli/source/lib/gcli/ui/history.js
new file mode 100644
index 000000000..a9d4b868c
--- /dev/null
+++ b/devtools/shared/gcli/source/lib/gcli/ui/history.js
@@ -0,0 +1,71 @@
+/*
+ * 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';
+
+/**
+ * A History object remembers commands that have been entered in the past and
+ * provides an API for accessing them again.
+ * See Bug 681340: Search through history (like C-r in bash)?
+ */
+function History() {
+ // This is the actual buffer where previous commands are kept.
+ // 'this._buffer[0]' should always be equal the empty string. This is so
+ // that when you try to go in to the "future", you will just get an empty
+ // command.
+ this._buffer = [''];
+
+ // This is an index in to the history buffer which points to where we
+ // currently are in the history.
+ this._current = 0;
+}
+
+/**
+ * Avoid memory leaks
+ */
+History.prototype.destroy = function() {
+ this._buffer = undefined;
+};
+
+/**
+ * Record and save a new command in the history.
+ */
+History.prototype.add = function(command) {
+ this._buffer.splice(1, 0, command);
+ this._current = 0;
+};
+
+/**
+ * Get the next (newer) command from history.
+ */
+History.prototype.forward = function() {
+ if (this._current > 0 ) {
+ this._current--;
+ }
+ return this._buffer[this._current];
+};
+
+/**
+ * Get the previous (older) item from history.
+ */
+History.prototype.backward = function() {
+ if (this._current < this._buffer.length - 1) {
+ this._current++;
+ }
+ return this._buffer[this._current];
+};
+
+exports.History = History;
diff --git a/devtools/shared/gcli/source/lib/gcli/ui/intro.js b/devtools/shared/gcli/source/lib/gcli/ui/intro.js
new file mode 100644
index 000000000..9abf51db6
--- /dev/null
+++ b/devtools/shared/gcli/source/lib/gcli/ui/intro.js
@@ -0,0 +1,90 @@
+/*
+ * 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 l10n = require('../util/l10n');
+var Output = require('../cli').Output;
+var view = require('./view');
+
+/**
+ * Record if the user has clicked on 'Got It!'
+ */
+exports.items = [
+ {
+ item: 'setting',
+ name: 'hideIntro',
+ type: 'boolean',
+ description: l10n.lookup('hideIntroDesc'),
+ defaultValue: false
+ }
+];
+
+/**
+ * Called when the UI is ready to add a welcome message to the output
+ */
+exports.maybeShowIntro = function (commandOutputManager, conversionContext,
+ outputPanel) {
+ var hideIntro = conversionContext.system.settings.get('hideIntro');
+ if (hideIntro.value) {
+ return;
+ }
+
+ var output = new Output(conversionContext);
+ output.type = 'view';
+ commandOutputManager.onOutput({ output: output });
+
+ var viewData = this.createView(null, conversionContext, true, outputPanel);
+
+ output.complete({ isTypedData: true, type: 'view', data: viewData });
+};
+
+/**
+ * Called when the UI is ready to add a welcome message to the output
+ */
+exports.createView = function (ignoreArgs, conversionContext, showHideButton,
+ outputPanel) {
+ return view.createView({
+ html:
+ '<div save="${mainDiv}">\n' +
+ ' <p>${l10n.introTextOpening3}</p>\n' +
+ '\n' +
+ ' <p>\n' +
+ ' ${l10n.introTextCommands}\n' +
+ ' <span class="gcli-out-shortcut" onclick="${onclick}"\n' +
+ ' ondblclick="${ondblclick}"\n' +
+ ' data-command="help">help</span>${l10n.introTextKeys2}\n' +
+ ' <code>${l10n.introTextF1Escape}</code>.\n' +
+ ' </p>\n' +
+ '\n' +
+ ' <button onclick="${onGotIt}"\n' +
+ ' if="${showHideButton}">${l10n.introTextGo}</button>\n' +
+ '</div>',
+ options: { stack: 'intro.html' },
+ data: {
+ l10n: l10n.propertyLookup,
+ onclick: conversionContext.update,
+ ondblclick: conversionContext.updateExec,
+ showHideButton: showHideButton,
+ onGotIt: function(ev) {
+ var settings = conversionContext.system.settings;
+ var hideIntro = settings.get('hideIntro');
+ hideIntro.value = true;
+ outputPanel.remove();
+ }
+ }
+ });
+};
diff --git a/devtools/shared/gcli/source/lib/gcli/ui/menu.css b/devtools/shared/gcli/source/lib/gcli/ui/menu.css
new file mode 100644
index 000000000..913ee1eec
--- /dev/null
+++ b/devtools/shared/gcli/source/lib/gcli/ui/menu.css
@@ -0,0 +1,69 @@
+
+.gcli-menu {
+ overflow: hidden;
+ font-size: 90%;
+}
+
+.gcli-menu:not(:first-of-type) {
+ padding-top: 5px;
+}
+
+.gcli-menu-vert {
+ white-space: nowrap;
+ max-width: 22em;
+ display: inline-flex;
+ padding-inline-end: 20px;
+ -webkit-padding-end: 20px;
+}
+
+.gcli-menu-names {
+ white-space: nowrap;
+ flex-grow: 0;
+ flex-shrink: 0;
+}
+
+.gcli-menu-descs {
+ flex-grow: 1;
+ flex-shrink: 1;
+}
+
+.gcli-menu-name,
+.gcli-menu-desc {
+ white-space: nowrap;
+}
+
+.gcli-menu-name {
+ padding-inline-start: 2px;
+ -webkit-padding-start: 2px;
+ padding-inline-end: 8px;
+ -webkit-padding-end: 8px;
+}
+
+.gcli-menu-desc {
+ padding-inline-end: 2px;
+ -webkit-padding-end: 2px;
+ color: #777;
+ text-overflow: ellipsis;
+ overflow: hidden;
+}
+
+.gcli-menu-name:hover,
+.gcli-menu-desc:hover {
+ background-color: rgba(0, 0, 0, 0.05);
+}
+
+.gcli-menu-highlight,
+.gcli-menu-highlight.gcli-menu-option:hover {
+ background-color: rgba(0, 0, 0, 0.1);
+}
+
+.gcli-menu-typed {
+ color: #FF6600;
+}
+
+.gcli-menu-more {
+ font-size: 80%;
+ width: 8em;
+ display: inline-flex;
+ vertical-align: bottom;
+}
diff --git a/devtools/shared/gcli/source/lib/gcli/ui/menu.html b/devtools/shared/gcli/source/lib/gcli/ui/menu.html
new file mode 100644
index 000000000..ab6a690f4
--- /dev/null
+++ b/devtools/shared/gcli/source/lib/gcli/ui/menu.html
@@ -0,0 +1,20 @@
+
+<div>
+ <div class="gcli-menu-template" aria-live="polite">
+ <div class="gcli-menu-names">
+ <div class="gcli-menu-name"
+ foreach="item in ${items}"
+ data-name="${item.name}"
+ onclick="${onItemClickInternal}"
+ title="${item.manual}">${item.highlight}</div>
+ </div>
+ <div class="gcli-menu-descs">
+ <div class="gcli-menu-desc"
+ foreach="item in ${items}"
+ data-name="${item.name}"
+ onclick="${onItemClickInternal}"
+ title="${item.manual}">${item.description}</div>
+ </div>
+ </div>
+ <div class="gcli-menu-more" if="${hasMore}">${l10n.fieldMenuMore}</div>
+</div>
diff --git a/devtools/shared/gcli/source/lib/gcli/ui/menu.js b/devtools/shared/gcli/source/lib/gcli/ui/menu.js
new file mode 100644
index 000000000..52b415384
--- /dev/null
+++ b/devtools/shared/gcli/source/lib/gcli/ui/menu.js
@@ -0,0 +1,328 @@
+/*
+ * 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 l10n = require('../util/l10n');
+var domtemplate = require('../util/domtemplate');
+var host = require('../util/host');
+
+/**
+ * Shared promises for loading resource files
+ */
+var menuCssPromise;
+var menuHtmlPromise;
+
+/**
+ * Menu is a display of the commands that are possible given the state of a
+ * requisition.
+ * @param options A way to customize the menu display.
+ * - document: The document to use in creating widgets
+ * - maxPredictions (default=8): The maximum predictions to show at one time
+ * If more are requested, a message will be displayed asking the user to
+ * continue typing to narrow the list of options
+ */
+function Menu(options) {
+ options = options || {};
+ this.document = options.document || document;
+ this.maxPredictions = options.maxPredictions || 8;
+
+ // Keep track of any highlighted items
+ this._choice = null;
+
+ // FF can be really hard to debug if doc is null, so we check early on
+ if (!this.document) {
+ throw new Error('No document');
+ }
+
+ this.element = util.createElement(this.document, 'div');
+ this.element.classList.add('gcli-menu');
+
+ if (menuCssPromise == null) {
+ menuCssPromise = host.staticRequire(module, './menu.css');
+ }
+ menuCssPromise.then(function(menuCss) {
+ // Pull the HTML into the DOM, but don't add it to the document
+ if (menuCss != null) {
+ util.importCss(menuCss, this.document, 'gcli-menu');
+ }
+ }.bind(this), console.error);
+
+ this.templateOptions = { blankNullUndefined: true, stack: 'menu.html' };
+ if (menuHtmlPromise == null) {
+ menuHtmlPromise = host.staticRequire(module, './menu.html');
+ }
+ menuHtmlPromise.then(function(menuHtml) {
+ if (this.document == null) {
+ return; // destroy() has been called
+ }
+
+ this.template = host.toDom(this.document, menuHtml);
+ }.bind(this), console.error);
+
+ // Contains the items that should be displayed
+ this.items = [];
+
+ this.onItemClick = util.createEvent('Menu.onItemClick');
+}
+
+/**
+ * Allow the template engine to get at localization strings
+ */
+Menu.prototype.l10n = l10n.propertyLookup;
+
+/**
+ * Avoid memory leaks
+ */
+Menu.prototype.destroy = function() {
+ this.element = undefined;
+ this.template = undefined;
+ this.document = undefined;
+ this.items = undefined;
+};
+
+/**
+ * The default is to do nothing when someone clicks on the menu.
+ * This is called from template.html
+ * @param ev The click event from the browser
+ */
+Menu.prototype.onItemClickInternal = function(ev) {
+ var name = ev.currentTarget.getAttribute('data-name');
+ if (!name) {
+ var named = ev.currentTarget.querySelector('[data-name]');
+ name = named.getAttribute('data-name');
+ }
+ this.onItemClick({ name: name });
+};
+
+/**
+ * Act as though someone clicked on the selected item
+ */
+Menu.prototype.clickSelected = function() {
+ this.onItemClick({ name: this.selected });
+};
+
+/**
+ * What is the currently selected item?
+ */
+Object.defineProperty(Menu.prototype, 'isSelected', {
+ get: function() {
+ return this.selected != null;
+ },
+ enumerable: true
+});
+
+/**
+ * What is the currently selected item?
+ */
+Object.defineProperty(Menu.prototype, 'selected', {
+ get: function() {
+ var item = this.element.querySelector('.gcli-menu-name.gcli-menu-highlight');
+ if (!item) {
+ return null;
+ }
+ return item.textContent;
+ },
+ enumerable: true
+});
+
+/**
+ * Display a number of items in the menu (or hide the menu if there is nothing
+ * to display)
+ * @param items The items to show in the menu
+ * @param match Matching text to highlight in the output
+ */
+Menu.prototype.show = function(items, match) {
+ // If the HTML hasn't loaded yet then just don't show a menu
+ if (this.template == null) {
+ return;
+ }
+
+ this.items = items.filter(function(item) {
+ return item.hidden === undefined || item.hidden !== true;
+ }.bind(this));
+
+ this.items = this.items.map(function(item) {
+ return getHighlightingProxy(item, match, this.template.ownerDocument);
+ }.bind(this));
+
+ if (this.items.length === 0) {
+ this.element.style.display = 'none';
+ return;
+ }
+
+ if (this.items.length >= this.maxPredictions) {
+ this.items.splice(-1);
+ this.hasMore = true;
+ }
+ else {
+ this.hasMore = false;
+ }
+
+ var options = this.template.cloneNode(true);
+ domtemplate.template(options, this, this.templateOptions);
+
+ util.clearElement(this.element);
+ this.element.appendChild(options);
+
+ this.element.style.display = 'block';
+};
+
+var MAX_ITEMS = 3;
+
+/**
+ * Takes an array of items and cuts it into an array of arrays to help us
+ * to place the items into columns.
+ * The inner arrays will have at most MAX_ITEMS in them, with the number of
+ * outer arrays expanding to accommodate.
+ */
+Object.defineProperty(Menu.prototype, 'itemsSubdivided', {
+ get: function() {
+ var reply = [];
+
+ var taken = 0;
+ while (taken < this.items.length) {
+ reply.push(this.items.slice(taken, taken + MAX_ITEMS));
+ taken += MAX_ITEMS;
+ }
+
+ return reply;
+ },
+ enumerable: true
+});
+
+/**
+ * Create a proxy around an item that highlights matching text
+ */
+function getHighlightingProxy(item, match, document) {
+ var proxy = {};
+ Object.defineProperties(proxy, {
+ highlight: {
+ get: function() {
+ if (!match) {
+ return item.name;
+ }
+
+ var value = item.name;
+ var startMatch = value.indexOf(match);
+ if (startMatch === -1) {
+ return value;
+ }
+
+ var before = value.substr(0, startMatch);
+ var after = value.substr(startMatch + match.length);
+ var parent = util.createElement(document, 'span');
+ parent.appendChild(document.createTextNode(before));
+ var highlight = util.createElement(document, 'span');
+ highlight.classList.add('gcli-menu-typed');
+ highlight.appendChild(document.createTextNode(match));
+ parent.appendChild(highlight);
+ parent.appendChild(document.createTextNode(after));
+ return parent;
+ },
+ enumerable: true
+ },
+
+ name: {
+ value: item.name,
+ enumerable: true
+ },
+
+ manual: {
+ value: item.manual,
+ enumerable: true
+ },
+
+ description: {
+ value: item.description,
+ enumerable: true
+ }
+ });
+ return proxy;
+}
+
+/**
+ * @return {int} current choice index
+ */
+Menu.prototype.getChoiceIndex = function() {
+ return this._choice == null ? 0 : this._choice;
+};
+
+/**
+ * Highlight the next (for by=1) or previous (for by=-1) option
+ */
+Menu.prototype.nudgeChoice = function(by) {
+ if (this._choice == null) {
+ this._choice = 0;
+ }
+
+ // There's an annoying up is down thing here, the menu is presented
+ // with the zeroth index at the top working down, so the UP arrow needs
+ // pick the choice below because we're working down
+ this._choice -= by;
+ this._updateHighlight();
+};
+
+/**
+ * Highlight nothing
+ */
+Menu.prototype.unsetChoice = function() {
+ this._choice = null;
+ this._updateHighlight();
+};
+
+/**
+ * Internal option to update the currently highlighted option
+ */
+Menu.prototype._updateHighlight = function() {
+ var names = this.element.querySelectorAll('.gcli-menu-name');
+ var descs = this.element.querySelectorAll('.gcli-menu-desc');
+ for (var i = 0; i < names.length; i++) {
+ names[i].classList.remove('gcli-menu-highlight');
+ }
+ for (i = 0; i < descs.length; i++) {
+ descs[i].classList.remove('gcli-menu-highlight');
+ }
+
+ if (this._choice == null || names.length === 0) {
+ return;
+ }
+
+ var index = this._choice % names.length;
+ if (index < 0) {
+ index = names.length + index;
+ }
+
+ names.item(index).classList.add('gcli-menu-highlight');
+ descs.item(index).classList.add('gcli-menu-highlight');
+};
+
+/**
+ * Hide the menu
+ */
+Menu.prototype.hide = function() {
+ this.element.style.display = 'none';
+};
+
+/**
+ * Change how much vertical space this menu can take up
+ */
+Menu.prototype.setMaxHeight = function(height) {
+ this.element.style.maxHeight = height + 'px';
+};
+
+exports.Menu = Menu;
diff --git a/devtools/shared/gcli/source/lib/gcli/ui/moz.build b/devtools/shared/gcli/source/lib/gcli/ui/moz.build
new file mode 100644
index 000000000..70ac666f0
--- /dev/null
+++ b/devtools/shared/gcli/source/lib/gcli/ui/moz.build
@@ -0,0 +1,15 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+DevToolsModules(
+ 'focus.js',
+ 'history.js',
+ 'intro.js',
+ 'menu.css',
+ 'menu.html',
+ 'menu.js',
+ 'view.js',
+)
diff --git a/devtools/shared/gcli/source/lib/gcli/ui/view.js b/devtools/shared/gcli/source/lib/gcli/ui/view.js
new file mode 100644
index 000000000..193fb2d96
--- /dev/null
+++ b/devtools/shared/gcli/source/lib/gcli/ui/view.js
@@ -0,0 +1,87 @@
+/*
+ * 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 domtemplate = require('../util/domtemplate');
+
+
+/**
+ * We want to avoid commands having to create DOM structures because that's
+ * messy and because we're going to need to have command output displayed in
+ * different documents. A View is a way to wrap an HTML template (for
+ * domtemplate) in with the data and options to render the template, so anyone
+ * can later run the template in the context of any document.
+ * View also cuts out a chunk of boiler place code.
+ * @param options The information needed to create the DOM from HTML. Includes:
+ * - html (required): The HTML source, probably from a call to require
+ * - options (default={}): The domtemplate options. See domtemplate for details
+ * - data (default={}): The data to domtemplate. See domtemplate for details.
+ * - css (default=none): Some CSS to be added to the final document. If 'css'
+ * is used, use of cssId is strongly recommended.
+ * - cssId (default=none): An ID to prevent multiple CSS additions. See
+ * util.importCss for more details.
+ * @return An object containing a single function 'appendTo()' which runs the
+ * template adding the result to the specified element. Takes 2 parameters:
+ * - element (required): the element to add to
+ * - clear (default=false): if clear===true then remove all pre-existing
+ * children of 'element' before appending the results of this template.
+ */
+exports.createView = function(options) {
+ if (options.html == null) {
+ throw new Error('options.html is missing');
+ }
+
+ return {
+ /**
+ * RTTI. Yeah.
+ */
+ isView: true,
+
+ /**
+ * Run the template against the document to which element belongs.
+ * @param element The element to append the result to
+ * @param clear Set clear===true to remove all children of element
+ */
+ appendTo: function(element, clear) {
+ // Strict check on the off-chance that we later think of other options
+ // and want to replace 'clear' with an 'options' parameter, but want to
+ // support backwards compat.
+ if (clear === true) {
+ util.clearElement(element);
+ }
+
+ element.appendChild(this.toDom(element.ownerDocument));
+ },
+
+ /**
+ * Actually convert the view data into a DOM suitable to be appended to
+ * an element
+ * @param document to use in realizing the template
+ */
+ toDom: function(document) {
+ if (options.css) {
+ util.importCss(options.css, document, options.cssId);
+ }
+
+ var child = host.toDom(document, options.html);
+ domtemplate.template(child, options.data || {}, options.options || {});
+ return child;
+ }
+ };
+};
diff --git a/devtools/shared/gcli/source/lib/gcli/util/domtemplate.js b/devtools/shared/gcli/source/lib/gcli/util/domtemplate.js
new file mode 100644
index 000000000..d8979db3b
--- /dev/null
+++ b/devtools/shared/gcli/source/lib/gcli/util/domtemplate.js
@@ -0,0 +1,20 @@
+/*
+ * 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 {template} = require("devtools/shared/gcli/templater");
+exports.template = template;
diff --git a/devtools/shared/gcli/source/lib/gcli/util/fileparser.js b/devtools/shared/gcli/source/lib/gcli/util/fileparser.js
new file mode 100644
index 000000000..4c470e638
--- /dev/null
+++ b/devtools/shared/gcli/source/lib/gcli/util/fileparser.js
@@ -0,0 +1,281 @@
+/*
+ * 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');
+var l10n = require('./l10n');
+var spell = require('./spell');
+var filesystem = require('./filesystem');
+var Status = require('../types/types').Status;
+
+/*
+ * An implementation of the functions that call the filesystem, designed to
+ * support the file type.
+ */
+
+/**
+ * Helper for the parse() function from the file type.
+ * See gcli/util/filesystem.js for details
+ */
+exports.parse = function(context, typed, options) {
+ return filesystem.stat(typed).then(function(stats) {
+ // The 'save-as' case - the path should not exist but does
+ if (options.existing === 'no' && stats.exists) {
+ return {
+ value: undefined,
+ status: Status.INCOMPLETE,
+ message: l10n.lookupFormat('fileErrExists', [ typed ]),
+ predictor: undefined // No predictions that we can give here
+ };
+ }
+
+ if (stats.exists) {
+ // The path exists - check it's the correct file type ...
+ if (options.filetype === 'file' && !stats.isFile) {
+ return {
+ value: undefined,
+ status: Status.INCOMPLETE,
+ message: l10n.lookupFormat('fileErrIsNotFile', [ typed ]),
+ predictor: getPredictor(typed, options)
+ };
+ }
+
+ if (options.filetype === 'directory' && !stats.isDir) {
+ return {
+ value: undefined,
+ status: Status.INCOMPLETE,
+ message: l10n.lookupFormat('fileErrIsNotDirectory', [ typed ]),
+ predictor: getPredictor(typed, options)
+ };
+ }
+
+ // ... and that it matches any 'match' RegExp
+ if (options.matches != null && !options.matches.test(typed)) {
+ return {
+ value: undefined,
+ status: Status.INCOMPLETE,
+ message: l10n.lookupFormat('fileErrDoesntMatch',
+ [ typed, options.source ]),
+ predictor: getPredictor(typed, options)
+ };
+ }
+ }
+ else {
+ if (options.existing === 'yes') {
+ // We wanted something that exists, but it doesn't. But we don't know
+ // if the path so far is an ERROR or just INCOMPLETE
+ var parentName = filesystem.dirname(typed);
+ return filesystem.stat(parentName).then(function(stats) {
+ return {
+ value: undefined,
+ status: stats.isDir ? Status.INCOMPLETE : Status.ERROR,
+ message: l10n.lookupFormat('fileErrNotExists', [ typed ]),
+ predictor: getPredictor(typed, options)
+ };
+ });
+ }
+ }
+
+ // We found no problems
+ return {
+ value: typed,
+ status: Status.VALID,
+ message: undefined,
+ predictor: getPredictor(typed, options)
+ };
+ });
+};
+
+var RANK_OPTIONS = { noSort: true, prefixZero: true };
+
+/**
+ * We want to be able to turn predictions off in Firefox
+ */
+exports.supportsPredictions = false;
+
+/**
+ * Get a function which creates predictions of files that match the given
+ * path
+ */
+function getPredictor(typed, options) {
+ if (!exports.supportsPredictions) {
+ return undefined;
+ }
+
+ return function() {
+ var allowFile = (options.filetype !== 'directory');
+ var parts = filesystem.split(typed);
+
+ var absolute = (typed.indexOf('/') === 0);
+ var roots;
+ if (absolute) {
+ roots = [ { name: '/', dist: 0, original: '/' } ];
+ }
+ else {
+ roots = dirHistory.getCommonDirectories().map(function(root) {
+ return { name: root, dist: 0, original: root };
+ });
+ }
+
+ // Add each part of the typed pathname onto each of the roots in turn,
+ // Finding options from each of those paths, and using these options as
+ // our roots for the next part
+ var partsAdded = util.promiseEach(parts, function(part, index) {
+
+ var partsSoFar = filesystem.join.apply(filesystem, parts.slice(0, index + 1));
+
+ // We allow this file matches in this pass if we're allowed files at all
+ // (i.e this isn't 'cd') and if this is the last part of the path
+ var allowFileForPart = (allowFile && index >= parts.length - 1);
+
+ var rootsPromise = util.promiseEach(roots, function(root) {
+
+ // Extend each roots to a list of all the files in each of the roots
+ var matchFile = allowFileForPart ? options.matches : null;
+ var promise = filesystem.ls(root.name, matchFile);
+
+ var onSuccess = function(entries) {
+ // Unless this is the final part filter out the non-directories
+ if (!allowFileForPart) {
+ entries = entries.filter(function(entry) {
+ return entry.isDir;
+ });
+ }
+ var entryMap = {};
+ entries.forEach(function(entry) {
+ entryMap[entry.pathname] = entry;
+ });
+ return entryMap;
+ };
+
+ var onError = function(err) {
+ // We expect errors due to the path not being a directory, not being
+ // accessible, or removed since the call to 'readdir'
+ return {};
+ };
+
+ promise = promise.then(onSuccess, onError);
+
+ // We want to compare all the directory entries with the original root
+ // plus the partsSoFar
+ var compare = filesystem.join(root.original, partsSoFar);
+
+ return promise.then(function(entryMap) {
+
+ var ranks = spell.rank(compare, Object.keys(entryMap), RANK_OPTIONS);
+ // penalize each path by the distance of it's parent
+ ranks.forEach(function(rank) {
+ rank.original = root.original;
+ rank.stats = entryMap[rank.name];
+ });
+ return ranks;
+ });
+ });
+
+ return rootsPromise.then(function(data) {
+ // data is an array of arrays of ranking objects. Squash down.
+ data = data.reduce(function(prev, curr) {
+ return prev.concat(curr);
+ }, []);
+
+ data.sort(function(r1, r2) {
+ return r1.dist - r2.dist;
+ });
+
+ // Trim, but by how many?
+ // If this is the last run through, we want to present the user with
+ // a sensible set of predictions. Otherwise we want to trim the tree
+ // to a reasonable set of matches, so we're happy with 1
+ // We look through x +/- 3 roots, and find the one with the biggest
+ // distance delta, and cut below that
+ // x=5 for the last time through, and x=8 otherwise
+ var isLast = index >= parts.length - 1;
+ var start = isLast ? 1 : 5;
+ var end = isLast ? 7 : 10;
+
+ var maxDeltaAt = start;
+ var maxDelta = data[start].dist - data[start - 1].dist;
+
+ for (var i = start + 1; i < end; i++) {
+ var delta = data[i].dist - data[i - 1].dist;
+ if (delta >= maxDelta) {
+ maxDelta = delta;
+ maxDeltaAt = i;
+ }
+ }
+
+ // Update the list of roots for the next time round
+ roots = data.slice(0, maxDeltaAt);
+ });
+ });
+
+ return partsAdded.then(function() {
+ var predictions = roots.map(function(root) {
+ var isFile = root.stats && root.stats.isFile;
+ var isDir = root.stats && root.stats.isDir;
+
+ var name = root.name;
+ if (isDir && name.charAt(name.length) !== filesystem.sep) {
+ name += filesystem.sep;
+ }
+
+ return {
+ name: name,
+ incomplete: !(allowFile && isFile),
+ isFile: isFile, // Added for describe, below
+ dist: root.dist, // TODO: Remove - added for debug in describe
+ };
+ });
+
+ return util.promiseEach(predictions, function(prediction) {
+ if (!prediction.isFile) {
+ prediction.description = '(' + prediction.dist + ')';
+ prediction.dist = undefined;
+ prediction.isFile = undefined;
+ return prediction;
+ }
+
+ return filesystem.describe(prediction.name).then(function(description) {
+ prediction.description = description;
+ prediction.dist = undefined;
+ prediction.isFile = undefined;
+ return prediction;
+ });
+ });
+ });
+ };
+}
+
+// =============================================================================
+
+/*
+ * The idea is that we maintain a list of 'directories that the user is
+ * interested in'. We store directories in a most-frequently-used cache
+ * of some description.
+ * But for now we're just using / and ~/
+ */
+var dirHistory = {
+ getCommonDirectories: function() {
+ return [
+ filesystem.sep, // i.e. the root directory
+ filesystem.home // i.e. the users home directory
+ ];
+ },
+ addCommonDirectory: function(ignore) {
+ // Not implemented yet
+ }
+};
diff --git a/devtools/shared/gcli/source/lib/gcli/util/filesystem.js b/devtools/shared/gcli/source/lib/gcli/util/filesystem.js
new file mode 100644
index 000000000..a7b22a8f7
--- /dev/null
+++ b/devtools/shared/gcli/source/lib/gcli/util/filesystem.js
@@ -0,0 +1,130 @@
+/*
+ * 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 Cu = require('chrome').Cu;
+var Cc = require('chrome').Cc;
+var Ci = require('chrome').Ci;
+
+var OS = Cu.import('resource://gre/modules/osfile.jsm', {}).OS;
+
+/**
+ * A set of functions that don't really belong in 'fs' (because they're not
+ * really universal in scope) but also kind of do (because they're not specific
+ * to GCLI
+ */
+
+exports.join = OS.Path.join;
+exports.sep = OS.Path.sep;
+exports.dirname = OS.Path.dirname;
+
+// On B2G, there is no home folder
+var home = null;
+try {
+ var dirService = Cc['@mozilla.org/file/directory_service;1']
+ .getService(Ci.nsIProperties);
+ home = dirService.get('Home', Ci.nsIFile).path;
+} catch(e) {}
+exports.home = home;
+
+if ('winGetDrive' in OS.Path) {
+ exports.sep = '\\';
+}
+else {
+ exports.sep = '/';
+}
+
+/**
+ * Split a path into its components.
+ * @param pathname (string) The part to cut up
+ * @return An array of path components
+ */
+exports.split = function(pathname) {
+ return OS.Path.split(pathname).components;
+};
+
+/**
+ * @param pathname string, path of an existing directory
+ * @param matches optional regular expression - filter output to include only
+ * the files that match the regular expression. The regexp is applied to the
+ * filename only not to the full path
+ * @return A promise of an array of stat objects for each member of the
+ * directory pointed to by ``pathname``, each containing 2 extra properties:
+ * - pathname: The full pathname of the file
+ * - filename: The final filename part of the pathname
+ */
+exports.ls = function(pathname, matches) {
+ var iterator = new OS.File.DirectoryIterator(pathname);
+ var entries = [];
+
+ var iteratePromise = iterator.forEach(function(entry) {
+ entries.push({
+ exists: true,
+ isDir: entry.isDir,
+ isFile: !entry.isFile,
+ filename: entry.name,
+ pathname: entry.path
+ });
+ });
+
+ return iteratePromise.then(function onSuccess() {
+ iterator.close();
+ return entries;
+ },
+ function onFailure(reason) {
+ iterator.close();
+ throw reason;
+ }
+ );
+};
+
+/**
+ * stat() is annoying because it considers stat('/doesnt/exist') to be an
+ * error, when the point of stat() is to *find* *out*. So this wrapper just
+ * converts 'ENOENT' i.e. doesn't exist to { exists:false } and adds
+ * exists:true to stat blocks from existing paths
+ */
+exports.stat = function(pathname) {
+ var onResolve = function(stats) {
+ return {
+ exists: true,
+ isDir: stats.isDir,
+ isFile: !stats.isFile
+ };
+ };
+
+ var onReject = function(err) {
+ if (err instanceof OS.File.Error && err.becauseNoSuchFile) {
+ return {
+ exists: false,
+ isDir: false,
+ isFile: false
+ };
+ }
+ throw err;
+ };
+
+ return OS.File.stat(pathname).then(onResolve, onReject);
+};
+
+/**
+ * We may read the first line of a file to describe it?
+ * Right now, however, we do nothing.
+ */
+exports.describe = function(pathname) {
+ return Promise.resolve('');
+};
diff --git a/devtools/shared/gcli/source/lib/gcli/util/host.js b/devtools/shared/gcli/source/lib/gcli/util/host.js
new file mode 100644
index 000000000..00fefa4f6
--- /dev/null
+++ b/devtools/shared/gcli/source/lib/gcli/util/host.js
@@ -0,0 +1,230 @@
+/*
+ * 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 Cc = require('chrome').Cc;
+var Ci = require('chrome').Ci;
+
+var { Task } = require("devtools/shared/task");
+
+var util = require('./util');
+
+function Highlighter(document) {
+ this._document = document;
+ this._nodes = util.createEmptyNodeList(this._document);
+}
+
+Object.defineProperty(Highlighter.prototype, 'nodelist', {
+ set: function(nodes) {
+ Array.prototype.forEach.call(this._nodes, this._unhighlightNode, this);
+ this._nodes = (nodes == null) ?
+ util.createEmptyNodeList(this._document) :
+ nodes;
+ Array.prototype.forEach.call(this._nodes, this._highlightNode, this);
+ },
+ get: function() {
+ return this._nodes;
+ },
+ enumerable: true
+});
+
+Highlighter.prototype.destroy = function() {
+ this.nodelist = null;
+};
+
+Highlighter.prototype._highlightNode = function(node) {
+ // Enable when the highlighter rewrite is done
+};
+
+Highlighter.prototype._unhighlightNode = function(node) {
+ // Enable when the highlighter rewrite is done
+};
+
+exports.Highlighter = Highlighter;
+
+/**
+ * See docs in lib/gcli/util/host.js
+ */
+exports.exec = function(task) {
+ return Task.spawn(task);
+};
+
+/**
+ * The URL API is new enough that we need specific platform help
+ */
+exports.createUrl = function(uristr, base) {
+ return new URL(uristr, base);
+};
+
+/**
+ * Load some HTML into the given document and return a DOM element.
+ * This utility assumes that the html has a single root (other than whitespace)
+ */
+exports.toDom = function(document, html) {
+ var div = util.createElement(document, 'div');
+ util.setContents(div, html);
+ return div.children[0];
+};
+
+/**
+ * When dealing with module paths on windows we want to use the unix
+ * directory separator rather than the windows one, so we avoid using
+ * OS.Path.dirname, and use unix version on all platforms.
+ */
+var resourceDirName = function(path) {
+ var index = path.lastIndexOf('/');
+ if (index == -1) {
+ return '.';
+ }
+ while (index >= 0 && path[index] == '/') {
+ --index;
+ }
+ return path.slice(0, index + 1);
+};
+
+/**
+ * Asynchronously load a text resource
+ * @see lib/gcli/util/host.js
+ */
+exports.staticRequire = function(requistingModule, name) {
+ if (name.match(/\.css$/)) {
+ return Promise.resolve('');
+ }
+ else {
+ return new Promise(function(resolve, reject) {
+ var filename = resourceDirName(requistingModule.id) + '/' + name;
+ filename = filename.replace(/\/\.\//g, '/');
+ filename = 'resource://devtools/shared/gcli/source/lib/' + filename;
+
+ var xhr = Cc['@mozilla.org/xmlextras/xmlhttprequest;1']
+ .createInstance(Ci.nsIXMLHttpRequest);
+
+ xhr.onload = function onload() {
+ resolve(xhr.responseText);
+ }.bind(this);
+
+ xhr.onabort = xhr.onerror = xhr.ontimeout = function(err) {
+ reject(err);
+ }.bind(this);
+
+ xhr.open('GET', filename);
+ xhr.send();
+ }.bind(this));
+ }
+};
+
+/**
+ * A group of functions to help scripting. Small enough that it doesn't need
+ * a separate module (it's basically a wrapper around 'eval' in some contexts)
+ */
+var client;
+var target;
+var consoleActor;
+var webConsoleClient;
+
+exports.script = { };
+
+exports.script.onOutput = util.createEvent('Script.onOutput');
+
+/**
+ * Setup the environment to eval JavaScript
+ */
+exports.script.useTarget = function(tgt) {
+ target = tgt;
+
+ // Local debugging needs to make the target remote.
+ var targetPromise = target.isRemote ?
+ Promise.resolve(target) :
+ target.makeRemote();
+
+ return targetPromise.then(function() {
+ return new Promise(function(resolve, reject) {
+ client = target._client;
+
+ client.addListener('pageError', function(packet) {
+ if (packet.from === consoleActor) {
+ // console.log('pageError', packet.pageError);
+ exports.script.onOutput({
+ level: 'exception',
+ message: packet.exception.class
+ });
+ }
+ });
+
+ client.addListener('consoleAPICall', function(type, packet) {
+ if (packet.from === consoleActor) {
+ var data = packet.message;
+
+ var ev = {
+ level: data.level,
+ arguments: data.arguments,
+ };
+
+ if (data.filename !== 'debugger eval code') {
+ ev.source = {
+ filename: data.filename,
+ lineNumber: data.lineNumber,
+ functionName: data.functionName
+ };
+ }
+
+ exports.script.onOutput(ev);
+ }
+ });
+
+ consoleActor = target._form.consoleActor;
+
+ var onAttach = function(response, wcc) {
+ webConsoleClient = wcc;
+
+ if (response.error != null) {
+ reject(response);
+ }
+ else {
+ resolve(response);
+ }
+
+ // TODO: add _onTabNavigated code?
+ };
+
+ var listeners = [ 'PageError', 'ConsoleAPI' ];
+ client.attachConsole(consoleActor, listeners, onAttach);
+ }.bind(this));
+ });
+};
+
+/**
+ * Execute some JavaScript
+ */
+exports.script.evaluate = function(javascript) {
+ return new Promise(function(resolve, reject) {
+ var onResult = function(response) {
+ var output = response.result;
+ if (typeof output === 'object' && output.type === 'undefined') {
+ output = undefined;
+ }
+
+ resolve({
+ input: response.input,
+ output: output,
+ exception: response.exception
+ });
+ };
+
+ webConsoleClient.evaluateJS(javascript, onResult, {});
+ }.bind(this));
+};
diff --git a/devtools/shared/gcli/source/lib/gcli/util/l10n.js b/devtools/shared/gcli/source/lib/gcli/util/l10n.js
new file mode 100644
index 000000000..6d0c7c8f4
--- /dev/null
+++ b/devtools/shared/gcli/source/lib/gcli/util/l10n.js
@@ -0,0 +1,80 @@
+/*
+ * 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";
+
+const {LocalizationHelper} = require("devtools/shared/l10n");
+const L10N = new LocalizationHelper("devtools/shared/locales/gcli.properties");
+
+/*
+ * Not supported when embedded - we"re doing things the Mozilla way not the
+ * require.js way.
+ */
+exports.registerStringsSource = function (modulePath) {
+ throw new Error("registerStringsSource is not available in mozilla");
+};
+
+exports.unregisterStringsSource = function (modulePath) {
+ throw new Error("unregisterStringsSource is not available in mozilla");
+};
+
+exports.lookupSwap = function (key, swaps) {
+ throw new Error("lookupSwap is not available in mozilla");
+};
+
+exports.lookupPlural = function (key, ord, swaps) {
+ throw new Error("lookupPlural is not available in mozilla");
+};
+
+exports.getPreferredLocales = function () {
+ return [ "root" ];
+};
+
+/** @see lookup() in lib/gcli/util/l10n.js */
+exports.lookup = function (key) {
+ try {
+ // Our memory leak hunter walks reachable objects trying to work out what
+ // type of thing they are using object.constructor.name. If that causes
+ // problems then we can avoid the unknown-key-exception with the following:
+ /*
+ if (key === "constructor") {
+ return { name: "l10n-mem-leak-defeat" };
+ }
+ */
+
+ return L10N.getStr(key);
+ } catch (ex) {
+ console.error("Failed to lookup ", key, ex);
+ return key;
+ }
+};
+
+/** @see propertyLookup in lib/gcli/util/l10n.js */
+exports.propertyLookup = new Proxy({}, {
+ get: function (rcvr, name) {
+ return exports.lookup(name);
+ }
+});
+
+/** @see lookupFormat in lib/gcli/util/l10n.js */
+exports.lookupFormat = function (key, swaps) {
+ try {
+ return L10N.getFormatStr(key, ...swaps);
+ } catch (ex) {
+ console.error("Failed to format ", key, ex);
+ return key;
+ }
+};
diff --git a/devtools/shared/gcli/source/lib/gcli/util/legacy.js b/devtools/shared/gcli/source/lib/gcli/util/legacy.js
new file mode 100644
index 000000000..07b0fd71a
--- /dev/null
+++ b/devtools/shared/gcli/source/lib/gcli/util/legacy.js
@@ -0,0 +1,147 @@
+/*
+ * 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';
+
+/**
+ * Fake a console for IE9
+ */
+if (typeof window !== 'undefined' && window.console == null) {
+ window.console = {};
+}
+'debug,log,warn,error,trace,group,groupEnd'.split(',').forEach(function(f) {
+ if (typeof window !== 'undefined' && !window.console[f]) {
+ window.console[f] = function() {};
+ }
+});
+
+/**
+ * Fake Element.classList for IE9
+ * Based on https://gist.github.com/1381839 by Devon Govett
+ */
+if (typeof document !== 'undefined' && typeof HTMLElement !== 'undefined' &&
+ !('classList' in document.documentElement) && Object.defineProperty) {
+ Object.defineProperty(HTMLElement.prototype, 'classList', {
+ get: function() {
+ var self = this;
+ function update(fn) {
+ return function(value) {
+ var classes = self.className.split(/\s+/);
+ var index = classes.indexOf(value);
+ fn(classes, index, value);
+ self.className = classes.join(' ');
+ };
+ }
+
+ var ret = {
+ add: update(function(classes, index, value) {
+ ~index || classes.push(value);
+ }),
+ remove: update(function(classes, index) {
+ ~index && classes.splice(index, 1);
+ }),
+ toggle: update(function(classes, index, value) {
+ ~index ? classes.splice(index, 1) : classes.push(value);
+ }),
+ contains: function(value) {
+ return !!~self.className.split(/\s+/).indexOf(value);
+ },
+ item: function(i) {
+ return self.className.split(/\s+/)[i] || null;
+ }
+ };
+
+ Object.defineProperty(ret, 'length', {
+ get: function() {
+ return self.className.split(/\s+/).length;
+ }
+ });
+
+ return ret;
+ }
+ });
+}
+
+/**
+ * Array.find
+ * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/find
+ */
+if (!Array.prototype.find) {
+ Object.defineProperty(Array.prototype, 'find', {
+ enumerable: false,
+ configurable: true,
+ writable: true,
+ value: function(predicate) {
+ if (this == null) {
+ throw new TypeError('Array.prototype.find called on null or undefined');
+ }
+ if (typeof predicate !== 'function') {
+ throw new TypeError('predicate must be a function');
+ }
+ var list = Object(this);
+ var length = list.length >>> 0;
+ var thisArg = arguments[1];
+ var value;
+
+ for (var i = 0; i < length; i++) {
+ if (i in list) {
+ value = list[i];
+ if (predicate.call(thisArg, value, i, list)) {
+ return value;
+ }
+ }
+ }
+ return undefined;
+ }
+ });
+}
+
+/**
+ * String.prototype.trimLeft is non-standard, but it works in Firefox,
+ * Chrome and Opera. It's easiest to create a shim here.
+ */
+if (!String.prototype.trimLeft) {
+ String.prototype.trimLeft = function() {
+ return String(this).replace(/\s*$/, '');
+ };
+}
+
+/**
+ * Polyfil taken from
+ * https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Function/bind
+ */
+if (!Function.prototype.bind) {
+ Function.prototype.bind = function(oThis) {
+ if (typeof this !== 'function') {
+ // closest thing possible to the ECMAScript 5 internal IsCallable function
+ throw new TypeError('Function.prototype.bind - what is trying to be bound is not callable');
+ }
+
+ var aArgs = Array.prototype.slice.call(arguments, 1),
+ fToBind = this,
+ fNOP = function () {},
+ fBound = function () {
+ return fToBind.apply(this instanceof fNOP && oThis
+ ? this
+ : oThis,
+ aArgs.concat(Array.prototype.slice.call(arguments)));
+ };
+
+ fNOP.prototype = this.prototype;
+ fBound.prototype = new fNOP();
+ return fBound;
+ };
+}
diff --git a/devtools/shared/gcli/source/lib/gcli/util/moz.build b/devtools/shared/gcli/source/lib/gcli/util/moz.build
new file mode 100644
index 000000000..0fdeb96ec
--- /dev/null
+++ b/devtools/shared/gcli/source/lib/gcli/util/moz.build
@@ -0,0 +1,17 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+DevToolsModules(
+ 'domtemplate.js',
+ 'fileparser.js',
+ 'filesystem.js',
+ 'host.js',
+ 'l10n.js',
+ 'legacy.js',
+ 'prism.js',
+ 'spell.js',
+ 'util.js',
+)
diff --git a/devtools/shared/gcli/source/lib/gcli/util/prism.js b/devtools/shared/gcli/source/lib/gcli/util/prism.js
new file mode 100644
index 000000000..6f457cf23
--- /dev/null
+++ b/devtools/shared/gcli/source/lib/gcli/util/prism.js
@@ -0,0 +1,361 @@
+/**
+ * Prism: Lightweight, robust, elegant syntax highlighting
+ * MIT license http://www.opensource.org/licenses/mit-license.php/
+ * @author Lea Verou http://lea.verou.me
+ */
+
+'use strict';
+
+// Private helper vars
+var lang = /\blang(?:uage)?-(?!\*)(\w+)\b/i;
+
+var Prism = exports.Prism = {
+ util: {
+ type: function (o) {
+ return Object.prototype.toString.call(o).match(/\[object (\w+)\]/)[1];
+ },
+
+ // Deep clone a language definition (e.g. to extend it)
+ clone: function (o) {
+ var type = Prism.util.type(o);
+
+ switch (type) {
+ case 'Object':
+ var clone = {};
+
+ for (var key in o) {
+ if (o.hasOwnProperty(key)) {
+ clone[key] = Prism.util.clone(o[key]);
+ }
+ }
+
+ return clone;
+
+ case 'Array':
+ return o.slice();
+ }
+
+ return o;
+ }
+ },
+
+ languages: {
+ extend: function (id, redef) {
+ var lang = Prism.util.clone(Prism.languages[id]);
+
+ for (var key in redef) {
+ lang[key] = redef[key];
+ }
+
+ return lang;
+ },
+
+ // Insert a token before another token in a language literal
+ insertBefore: function (inside, before, insert, root) {
+ root = root || Prism.languages;
+ var grammar = root[inside];
+ var ret = {};
+
+ for (var token in grammar) {
+
+ if (grammar.hasOwnProperty(token)) {
+
+ if (token == before) {
+
+ for (var newToken in insert) {
+
+ if (insert.hasOwnProperty(newToken)) {
+ ret[newToken] = insert[newToken];
+ }
+ }
+ }
+
+ ret[token] = grammar[token];
+ }
+ }
+
+ root[inside] = ret;
+ return ret;
+ },
+
+ // Traverse a language definition with Depth First Search
+ DFS: function(o, callback) {
+ for (var i in o) {
+ callback.call(o, i, o[i]);
+
+ if (Prism.util.type(o) === 'Object') {
+ Prism.languages.DFS(o[i], callback);
+ }
+ }
+ }
+ },
+
+ highlightAll: function(async, callback) {
+ var elements = document.querySelectorAll('code[class*="language-"], [class*="language-"] code, code[class*="lang-"], [class*="lang-"] code');
+
+ elements.forEach(function(element) {
+ Prism.highlightElement(element, async === true, callback);
+ });
+ },
+
+ highlightElement: function(element, async, callback) {
+ // Find language
+ var language;
+ var grammar;
+
+ var parent = element;
+ while (parent && !lang.test(parent.className)) {
+ parent = parent.parentNode;
+ }
+
+ if (parent) {
+ language = (parent.className.match(lang) || [,''])[1];
+ grammar = Prism.languages[language];
+ }
+
+ if (!grammar) {
+ return;
+ }
+
+ // Set language on the element, if not present
+ element.className = element.className.replace(lang, '').replace(/\s+/g, ' ') + ' language-' + language;
+
+ // Set language on the parent, for styling
+ parent = element.parentNode;
+
+ if (/pre/i.test(parent.nodeName)) {
+ parent.className = parent.className.replace(lang, '').replace(/\s+/g, ' ') + ' language-' + language;
+ }
+
+ var code = element.textContent;
+
+ if (!code) {
+ return;
+ }
+
+ code = code.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/\u00a0/g, ' ');
+
+ var env = {
+ element: element,
+ language: language,
+ grammar: grammar,
+ code: code
+ };
+
+ Prism.hooks.run('before-highlight', env);
+
+ env.highlightedCode = Prism.highlight(env.code, env.grammar, env.language);
+
+ Prism.hooks.run('before-insert', env);
+
+ env.element.innerHTML = env.highlightedCode;
+
+ if (callback) {
+ callback.call(element);
+ }
+
+ Prism.hooks.run('after-highlight', env);
+ },
+
+ highlight: function (text, grammar, language) {
+ return Token.stringify(Prism.tokenize(text, grammar), language);
+ },
+
+ tokenize: function(text, grammar, language) {
+ var Token = Prism.Token;
+
+ var strarr = [text];
+
+ var rest = grammar.rest;
+ var token;
+ if (rest) {
+ for (token in rest) {
+ grammar[token] = rest[token];
+ }
+
+ delete grammar.rest;
+ }
+
+ tokenloop:
+ for (token in grammar) {
+ if (!grammar.hasOwnProperty(token) || !grammar[token]) {
+ continue;
+ }
+
+ var pattern = grammar[token],
+ inside = pattern.inside,
+ lookbehind = !!pattern.lookbehind,
+ lookbehindLength = 0;
+
+ pattern = pattern.pattern || pattern;
+
+ for (var i=0; i<strarr.length; i++) { // Don’t cache length as it changes during the loop
+
+ var str = strarr[i];
+
+ if (strarr.length > text.length) {
+ // Something went terribly wrong, ABORT, ABORT!
+ break tokenloop;
+ }
+
+ if (str instanceof Token) {
+ continue;
+ }
+
+ pattern.lastIndex = 0;
+
+ var match = pattern.exec(str);
+
+ if (match) {
+ if (lookbehind) {
+ lookbehindLength = match[1].length;
+ }
+
+ var from = match.index - 1 + lookbehindLength;
+ match = match[0].slice(lookbehindLength);
+ var len = match.length;
+ var to = from + len;
+ var before = str.slice(0, from + 1);
+ var after = str.slice(to + 1);
+
+ var args = [i, 1];
+
+ if (before) {
+ args.push(before);
+ }
+
+ var wrapped = new Token(token, inside? Prism.tokenize(match, inside) : match);
+
+ args.push(wrapped);
+
+ if (after) {
+ args.push(after);
+ }
+
+ Array.prototype.splice.apply(strarr, args);
+ }
+ }
+ }
+
+ return strarr;
+ },
+
+ hooks: {
+ all: {},
+
+ add: function (name, callback) {
+ var hooks = Prism.hooks.all;
+
+ hooks[name] = hooks[name] || [];
+
+ hooks[name].push(callback);
+ },
+
+ run: function (name, env) {
+ var callbacks = Prism.hooks.all[name];
+
+ if (!callbacks || !callbacks.length) {
+ return;
+ }
+
+ callbacks.forEach(function(callback) {
+ callback(env);
+ });
+ }
+ }
+};
+
+var Token = Prism.Token = function(type, content) {
+ this.type = type;
+ this.content = content;
+};
+
+Token.stringify = function(o, language, parent) {
+ if (typeof o == 'string') {
+ return o;
+ }
+
+ if (Object.prototype.toString.call(o) == '[object Array]') {
+ return o.map(function(element) {
+ return Token.stringify(element, language, o);
+ }).join('');
+ }
+
+ var env = {
+ type: o.type,
+ content: Token.stringify(o.content, language, parent),
+ tag: 'span',
+ classes: ['token', o.type],
+ attributes: {},
+ language: language,
+ parent: parent
+ };
+
+ if (env.type == 'comment') {
+ env.attributes.spellcheck = 'true';
+ }
+
+ Prism.hooks.run('wrap', env);
+
+ var attributes = '';
+
+ for (var name in env.attributes) {
+ attributes += name + '="' + (env.attributes[name] || '') + '"';
+ }
+
+ return '<' + env.tag + ' class="' + env.classes.join(' ') + '" ' + attributes + '>' + env.content + '</' + env.tag + '>';
+};
+
+Prism.languages.clike = {
+ 'comment': {
+ pattern: /(^|[^\\])(\/\*[\w\W]*?\*\/|(^|[^:])\/\/.*?(\r?\n|$))/g,
+ lookbehind: true
+ },
+ 'string': /("|')(\\?.)*?\1/g,
+ 'class-name': {
+ pattern: /((?:(?:class|interface|extends|implements|trait|instanceof|new)\s+)|(?:catch\s+\())[a-z0-9_\.\\]+/ig,
+ lookbehind: true,
+ inside: {
+ punctuation: /(\.|\\)/
+ }
+ },
+ 'keyword': /\b(if|else|while|do|for|return|in|instanceof|function|new|try|throw|catch|finally|null|break|continue)\b/g,
+ 'boolean': /\b(true|false)\b/g,
+ 'function': {
+ pattern: /[a-z0-9_]+\(/ig,
+ inside: {
+ punctuation: /\(/
+ }
+ },
+ 'number': /\b-?(0x[\dA-Fa-f]+|\d*\.?\d+([Ee]-?\d+)?)\b/g,
+ 'operator': /[-+]{1,2}|!|&lt;=?|>=?|={1,3}|(&amp;){1,2}|\|?\||\?|\*|\/|\~|\^|\%/g,
+ 'ignore': /&(lt|gt|amp);/gi,
+ 'punctuation': /[{}[\];(),.:]/g
+};
+
+Prism.languages.javascript = Prism.languages.extend('clike', {
+ 'keyword': /\b(var|let|if|else|while|do|for|return|in|instanceof|function|new|with|typeof|try|throw|catch|finally|null|break|continue)\b/g,
+ 'number': /\b-?(0x[\dA-Fa-f]+|\d*\.?\d+([Ee]-?\d+)?|NaN|-?Infinity)\b/g
+});
+
+Prism.languages.insertBefore('javascript', 'keyword', {
+ 'regex': {
+ pattern: /(^|[^/])\/(?!\/)(\[.+?]|\\.|[^/\r\n])+\/[gim]{0,3}(?=\s*($|[\r\n,.;})]))/g,
+ lookbehind: true
+ }
+});
+
+if (Prism.languages.markup) {
+ Prism.languages.insertBefore('markup', 'tag', {
+ 'script': {
+ pattern: /(&lt;|<)script[\w\W]*?(>|&gt;)[\w\W]*?(&lt;|<)\/script(>|&gt;)/ig,
+ inside: {
+ 'tag': {
+ pattern: /(&lt;|<)script[\w\W]*?(>|&gt;)|(&lt;|<)\/script(>|&gt;)/ig,
+ inside: Prism.languages.markup.tag.inside
+ },
+ rest: Prism.languages.javascript
+ }
+ }
+ });
+}
diff --git a/devtools/shared/gcli/source/lib/gcli/util/spell.js b/devtools/shared/gcli/source/lib/gcli/util/spell.js
new file mode 100644
index 000000000..f16724f2a
--- /dev/null
+++ b/devtools/shared/gcli/source/lib/gcli/util/spell.js
@@ -0,0 +1,197 @@
+/*
+ * 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';
+
+/*
+ * A spell-checker based on Damerau-Levenshtein distance.
+ */
+
+var CASE_CHANGE_COST = 1;
+var INSERTION_COST = 10;
+var DELETION_COST = 10;
+var SWAP_COST = 10;
+var SUBSTITUTION_COST = 20;
+var MAX_EDIT_DISTANCE = 40;
+
+/**
+ * Compute Damerau-Levenshtein Distance, with a modification to allow a low
+ * case-change cost (1/10th of a swap-cost)
+ * @see http://en.wikipedia.org/wiki/Damerau%E2%80%93Levenshtein_distance
+ */
+var distance = exports.distance = function(wordi, wordj) {
+ var wordiLen = wordi.length;
+ var wordjLen = wordj.length;
+
+ // We only need to store three rows of our dynamic programming matrix.
+ // (Without swap, it would have been two.)
+ var row0 = new Array(wordiLen+1);
+ var row1 = new Array(wordiLen+1);
+ var row2 = new Array(wordiLen+1);
+ var tmp;
+
+ var i, j;
+
+ // The distance between the empty string and a string of size i is the cost
+ // of i insertions.
+ for (i = 0; i <= wordiLen; i++) {
+ row1[i] = i * INSERTION_COST;
+ }
+
+ // Row-by-row, we're computing the edit distance between substrings wordi[0..i]
+ // and wordj[0..j].
+ for (j = 1; j <= wordjLen; j++) {
+ // Edit distance between wordi[0..0] and wordj[0..j] is the cost of j
+ // insertions.
+ row0[0] = j * INSERTION_COST;
+
+ for (i = 1; i <= wordiLen; i++) {
+ // Handle deletion, insertion and substitution: we can reach each cell
+ // from three other cells corresponding to those three operations. We
+ // want the minimum cost.
+ var dc = row0[i - 1] + DELETION_COST;
+ var ic = row1[i] + INSERTION_COST;
+ var sc0;
+ if (wordi[i-1] === wordj[j-1]) {
+ sc0 = 0;
+ }
+ else {
+ if (wordi[i-1].toLowerCase() === wordj[j-1].toLowerCase()) {
+ sc0 = CASE_CHANGE_COST;
+ }
+ else {
+ sc0 = SUBSTITUTION_COST;
+ }
+ }
+ var sc = row1[i-1] + sc0;
+
+ row0[i] = Math.min(dc, ic, sc);
+
+ // We handle swap too, eg. distance between help and hlep should be 1. If
+ // we find such a swap, there's a chance to update row0[1] to be lower.
+ if (i > 1 && j > 1 && wordi[i-1] === wordj[j-2] && wordj[j-1] === wordi[i-2]) {
+ row0[i] = Math.min(row0[i], row2[i-2] + SWAP_COST);
+ }
+ }
+
+ tmp = row2;
+ row2 = row1;
+ row1 = row0;
+ row0 = tmp;
+ }
+
+ return row1[wordiLen];
+};
+
+/**
+ * As distance() except that we say that if word is a prefix of name then we
+ * only count the case changes. This allows us to use words that can be
+ * completed by typing as more likely than short words
+ */
+var distancePrefix = exports.distancePrefix = function(word, name) {
+ var dist = 0;
+
+ for (var i = 0; i < word.length; i++) {
+ if (name[i] !== word[i]) {
+ if (name[i].toLowerCase() === word[i].toLowerCase()) {
+ dist++;
+ }
+ else {
+ // name does not start with word, even ignoring case, use
+ // Damerau-Levenshtein
+ return exports.distance(word, name);
+ }
+ }
+ }
+
+ return dist;
+};
+
+/**
+ * A function that returns the correction for the specified word.
+ */
+exports.correct = function(word, names) {
+ if (names.length === 0) {
+ return undefined;
+ }
+
+ var distances = {};
+ var sortedCandidates;
+
+ names.forEach(function(candidate) {
+ distances[candidate] = exports.distance(word, candidate);
+ });
+
+ sortedCandidates = names.sort(function(worda, wordb) {
+ if (distances[worda] !== distances[wordb]) {
+ return distances[worda] - distances[wordb];
+ }
+ else {
+ // if the score is the same, always return the first string
+ // in the lexicographical order
+ return worda < wordb;
+ }
+ });
+
+ if (distances[sortedCandidates[0]] <= MAX_EDIT_DISTANCE) {
+ return sortedCandidates[0];
+ }
+ else {
+ return undefined;
+ }
+};
+
+/**
+ * Return a ranked list of matches:
+ *
+ * spell.rank('fred', [ 'banana', 'fred', 'ed', 'red' ]);
+ * ↓
+ * [
+ * { name: 'fred', dist: 0 },
+ * { name: 'red', dist: 1 },
+ * { name: 'ed', dist: 2 },
+ * { name: 'banana', dist: 10 },
+ * ]
+ *
+ * @param word The string that we're comparing names against
+ * @param names An array of strings to compare word against
+ * @param options Comparison options:
+ * - noSort: Do not sort the output by distance
+ * - prefixZero: Count prefix matches as edit distance 0 (i.e. word='bana' and
+ * names=['banana'], would return { name:'banana': dist: 0 }) This is useful
+ * if someone is typing the matches and may not have finished yet
+ */
+exports.rank = function(word, names, options) {
+ options = options || {};
+
+ var reply = names.map(function(name) {
+ // If any name starts with the word then the distance is based on the
+ // number of case changes rather than Damerau-Levenshtein
+ var algo = options.prefixZero ? distancePrefix : distance;
+ return {
+ name: name,
+ dist: algo(word, name)
+ };
+ });
+
+ if (!options.noSort) {
+ reply = reply.sort(function(d1, d2) {
+ return d1.dist - d2.dist;
+ });
+ }
+
+ return reply;
+};
diff --git a/devtools/shared/gcli/source/lib/gcli/util/util.js b/devtools/shared/gcli/source/lib/gcli/util/util.js
new file mode 100644
index 000000000..065bf36c0
--- /dev/null
+++ b/devtools/shared/gcli/source/lib/gcli/util/util.js
@@ -0,0 +1,685 @@
+/*
+ * 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';
+
+/*
+ * A number of DOM manipulation and event handling utilities.
+ */
+
+//------------------------------------------------------------------------------
+
+var eventDebug = false;
+
+/**
+ * Patch up broken console API from node
+ */
+if (eventDebug) {
+ if (console.group == null) {
+ console.group = function() { console.log(arguments); };
+ }
+ if (console.groupEnd == null) {
+ console.groupEnd = function() { console.log(arguments); };
+ }
+}
+
+/**
+ * Useful way to create a name for a handler, used in createEvent()
+ */
+function nameFunction(handler) {
+ var scope = handler.scope ? handler.scope.constructor.name + '.' : '';
+ var name = handler.func.name;
+ if (name) {
+ return scope + name;
+ }
+ for (var prop in handler.scope) {
+ if (handler.scope[prop] === handler.func) {
+ return scope + prop;
+ }
+ }
+ return scope + handler.func;
+}
+
+/**
+ * Create an event.
+ * For use as follows:
+ *
+ * function Hat() {
+ * this.putOn = createEvent('Hat.putOn');
+ * ...
+ * }
+ * Hat.prototype.adorn = function(person) {
+ * this.putOn({ hat: hat, person: person });
+ * ...
+ * }
+ *
+ * var hat = new Hat();
+ * hat.putOn.add(function(ev) {
+ * console.log('The hat ', ev.hat, ' has is worn by ', ev.person);
+ * }, scope);
+ *
+ * @param name Optional name to help with debugging
+ */
+exports.createEvent = function(name) {
+ var handlers = [];
+ var fireHoldCount = 0;
+ var heldEvents = [];
+ var eventCombiner;
+
+ /**
+ * This is how the event is triggered.
+ * @param ev The event object to be passed to the event listeners
+ */
+ var event = function(ev) {
+ if (fireHoldCount > 0) {
+ heldEvents.push(ev);
+ if (eventDebug) {
+ console.log('Held fire: ' + name, ev);
+ }
+ return;
+ }
+
+ if (eventDebug) {
+ console.group('Fire: ' + name + ' to ' + handlers.length + ' listeners', ev);
+ }
+
+ // Use for rather than forEach because it step debugs better, which is
+ // important for debugging events
+ for (var i = 0; i < handlers.length; i++) {
+ var handler = handlers[i];
+ if (eventDebug) {
+ console.log(nameFunction(handler));
+ }
+ handler.func.call(handler.scope, ev);
+ }
+
+ if (eventDebug) {
+ console.groupEnd();
+ }
+ };
+
+ /**
+ * Add a new handler function
+ * @param func The function to call when this event is triggered
+ * @param scope Optional 'this' object for the function call
+ */
+ event.add = function(func, scope) {
+ if (typeof func !== 'function') {
+ throw new Error(name + ' add(func,...), 1st param is ' + typeof func);
+ }
+
+ if (eventDebug) {
+ console.log('Adding listener to ' + name);
+ }
+
+ handlers.push({ func: func, scope: scope });
+ };
+
+ /**
+ * Remove a handler function added through add(). Both func and scope must
+ * be strict equals (===) the values used in the call to add()
+ * @param func The function to call when this event is triggered
+ * @param scope Optional 'this' object for the function call
+ */
+ event.remove = function(func, scope) {
+ if (eventDebug) {
+ console.log('Removing listener from ' + name);
+ }
+
+ var found = false;
+ handlers = handlers.filter(function(test) {
+ var match = (test.func === func && test.scope === scope);
+ if (match) {
+ found = true;
+ }
+ return !match;
+ });
+ if (!found) {
+ console.warn('Handler not found. Attached to ' + name);
+ }
+ };
+
+ /**
+ * Remove all handlers.
+ * Reset the state of this event back to it's post create state
+ */
+ event.removeAll = function() {
+ handlers = [];
+ };
+
+ /**
+ * Fire an event just once using a promise.
+ */
+ event.once = function() {
+ if (arguments.length !== 0) {
+ throw new Error('event.once uses promise return values');
+ }
+
+ return new Promise(function(resolve, reject) {
+ var handler = function(arg) {
+ event.remove(handler);
+ resolve(arg);
+ };
+
+ event.add(handler);
+ });
+ };
+
+ /**
+ * Temporarily prevent this event from firing.
+ * @see resumeFire(ev)
+ */
+ event.holdFire = function() {
+ if (eventDebug) {
+ console.group('Holding fire: ' + name);
+ }
+
+ fireHoldCount++;
+ };
+
+ /**
+ * Resume firing events.
+ * If there are heldEvents, then we fire one event to cover them all. If an
+ * event combining function has been provided then we use that to combine the
+ * events. Otherwise the last held event is used.
+ * @see holdFire()
+ */
+ event.resumeFire = function() {
+ if (eventDebug) {
+ console.groupEnd('Resume fire: ' + name);
+ }
+
+ if (fireHoldCount === 0) {
+ throw new Error('fireHoldCount === 0 during resumeFire on ' + name);
+ }
+
+ fireHoldCount--;
+ if (heldEvents.length === 0) {
+ return;
+ }
+
+ if (heldEvents.length === 1) {
+ event(heldEvents[0]);
+ }
+ else {
+ var first = heldEvents[0];
+ var last = heldEvents[heldEvents.length - 1];
+ if (eventCombiner) {
+ event(eventCombiner(first, last, heldEvents));
+ }
+ else {
+ event(last);
+ }
+ }
+
+ heldEvents = [];
+ };
+
+ /**
+ * When resumeFire has a number of events to combine, by default it just
+ * picks the last, however you can provide an eventCombiner which returns a
+ * combined event.
+ * eventCombiners will be passed 3 parameters:
+ * - first The first event to be held
+ * - last The last event to be held
+ * - all An array containing all the held events
+ * The return value from an eventCombiner is expected to be an event object
+ */
+ Object.defineProperty(event, 'eventCombiner', {
+ set: function(newEventCombiner) {
+ if (typeof newEventCombiner !== 'function') {
+ throw new Error('eventCombiner is not a function');
+ }
+ eventCombiner = newEventCombiner;
+ },
+
+ enumerable: true
+ });
+
+ return event;
+};
+
+//------------------------------------------------------------------------------
+
+/**
+ * promiseEach is roughly like Array.forEach except that the action is taken to
+ * be something that completes asynchronously, returning a promise, so we wait
+ * for the action to complete for each array element before moving onto the
+ * next.
+ * @param array An array of objects to enumerate
+ * @param action A function to call for each member of the array
+ * @param scope Optional object to use as 'this' for the function calls
+ * @return A promise which is resolved (with an array of resolution values)
+ * when all the array members have been passed to the action function, and
+ * rejected as soon as any of the action function calls fails 
+ */
+exports.promiseEach = function(array, action, scope) {
+ if (array.length === 0) {
+ return Promise.resolve([]);
+ }
+
+ var allReply = [];
+ var promise = Promise.resolve();
+
+ array.forEach(function(member, i) {
+ promise = promise.then(function() {
+ var reply = action.call(scope, member, i, array);
+ return Promise.resolve(reply).then(function(data) {
+ allReply[i] = data;
+ });
+ });
+ });
+
+ return promise.then(function() {
+ return allReply;
+ });
+};
+
+/**
+ * Catching errors from promises isn't as simple as:
+ * promise.then(handler, console.error);
+ * for a number of reasons:
+ * - chrome's console doesn't have bound functions (why?)
+ * - we don't get stack traces out from console.error(ex);
+ */
+exports.errorHandler = function(ex) {
+ if (ex instanceof Error) {
+ // V8 weirdly includes the exception message in the stack
+ if (ex.stack.indexOf(ex.message) !== -1) {
+ console.error(ex.stack);
+ }
+ else {
+ console.error('' + ex);
+ console.error(ex.stack);
+ }
+ }
+ else {
+ console.error(ex);
+ }
+};
+
+
+//------------------------------------------------------------------------------
+
+/**
+ * Copy the properties from one object to another in a way that preserves
+ * function properties as functions rather than copying the calculated value
+ * as copy time
+ */
+exports.copyProperties = function(src, dest) {
+ for (var key in src) {
+ var descriptor;
+ var obj = src;
+ while (true) {
+ descriptor = Object.getOwnPropertyDescriptor(obj, key);
+ if (descriptor != null) {
+ break;
+ }
+ obj = Object.getPrototypeOf(obj);
+ if (obj == null) {
+ throw new Error('Can\'t find descriptor of ' + key);
+ }
+ }
+
+ if ('value' in descriptor) {
+ dest[key] = src[key];
+ }
+ else if ('get' in descriptor) {
+ Object.defineProperty(dest, key, {
+ get: descriptor.get,
+ set: descriptor.set,
+ enumerable: descriptor.enumerable
+ });
+ }
+ else {
+ throw new Error('Don\'t know how to copy ' + key + ' property.');
+ }
+ }
+};
+
+//------------------------------------------------------------------------------
+
+/**
+ * XHTML namespace
+ */
+exports.NS_XHTML = 'http://www.w3.org/1999/xhtml';
+
+/**
+ * XUL namespace
+ */
+exports.NS_XUL = 'http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul';
+
+/**
+ * Create an HTML or XHTML element depending on whether the document is HTML
+ * or XML based. Where HTML/XHTML elements are distinguished by whether they
+ * are created using doc.createElementNS('http://www.w3.org/1999/xhtml', tag)
+ * or doc.createElement(tag)
+ * If you want to create a XUL element then you don't have a problem knowing
+ * what namespace you want.
+ * @param doc The document in which to create the element
+ * @param tag The name of the tag to create
+ * @returns The created element
+ */
+exports.createElement = function(doc, tag) {
+ if (exports.isXmlDocument(doc)) {
+ return doc.createElementNS(exports.NS_XHTML, tag);
+ }
+ else {
+ return doc.createElement(tag);
+ }
+};
+
+/**
+ * Remove all the child nodes from this node
+ * @param elem The element that should have it's children removed
+ */
+exports.clearElement = function(elem) {
+ while (elem.hasChildNodes()) {
+ elem.removeChild(elem.firstChild);
+ }
+};
+
+var isAllWhitespace = /^\s*$/;
+
+/**
+ * Iterate over the children of a node looking for TextNodes that have only
+ * whitespace content and remove them.
+ * This utility is helpful when you have a template which contains whitespace
+ * so it looks nice, but where the whitespace interferes with the rendering of
+ * the page
+ * @param elem The element which should have blank whitespace trimmed
+ * @param deep Should this node removal include child elements
+ */
+exports.removeWhitespace = function(elem, deep) {
+ var i = 0;
+ while (i < elem.childNodes.length) {
+ var child = elem.childNodes.item(i);
+ if (child.nodeType === 3 /*Node.TEXT_NODE*/ &&
+ isAllWhitespace.test(child.textContent)) {
+ elem.removeChild(child);
+ }
+ else {
+ if (deep && child.nodeType === 1 /*Node.ELEMENT_NODE*/) {
+ exports.removeWhitespace(child, deep);
+ }
+ i++;
+ }
+ }
+};
+
+/**
+ * Create a style element in the document head, and add the given CSS text to
+ * it.
+ * @param cssText The CSS declarations to append
+ * @param doc The document element to work from
+ * @param id Optional id to assign to the created style tag. If the id already
+ * exists on the document, we do not add the CSS again.
+ */
+exports.importCss = function(cssText, doc, id) {
+ if (!cssText) {
+ return undefined;
+ }
+
+ doc = doc || document;
+
+ if (!id) {
+ id = 'hash-' + hash(cssText);
+ }
+
+ var found = doc.getElementById(id);
+ if (found) {
+ if (found.tagName.toLowerCase() !== 'style') {
+ console.error('Warning: importCss passed id=' + id +
+ ', but that pre-exists (and isn\'t a style tag)');
+ }
+ return found;
+ }
+
+ var style = exports.createElement(doc, 'style');
+ style.id = id;
+ style.appendChild(doc.createTextNode(cssText));
+
+ var head = doc.getElementsByTagName('head')[0] || doc.documentElement;
+ head.appendChild(style);
+
+ return style;
+};
+
+/**
+ * Simple hash function which happens to match Java's |String.hashCode()|
+ * Done like this because I we don't need crypto-security, but do need speed,
+ * and I don't want to spend a long time working on it.
+ * @see http://werxltd.com/wp/2010/05/13/javascript-implementation-of-javas-string-hashcode-method/
+ */
+function hash(str) {
+ var h = 0;
+ if (str.length === 0) {
+ return h;
+ }
+ for (var i = 0; i < str.length; i++) {
+ var character = str.charCodeAt(i);
+ h = ((h << 5) - h) + character;
+ h = h & h; // Convert to 32bit integer
+ }
+ return h;
+}
+
+/**
+ * Shortcut for clearElement/createTextNode/appendChild to make up for the lack
+ * of standards around textContent/innerText
+ */
+exports.setTextContent = function(elem, text) {
+ exports.clearElement(elem);
+ var child = elem.ownerDocument.createTextNode(text);
+ elem.appendChild(child);
+};
+
+/**
+ * There are problems with innerHTML on XML documents, so we need to do a dance
+ * using document.createRange().createContextualFragment() when in XML mode
+ */
+exports.setContents = function(elem, contents) {
+ if (typeof HTMLElement !== 'undefined' && contents instanceof HTMLElement) {
+ exports.clearElement(elem);
+ elem.appendChild(contents);
+ return;
+ }
+
+ if ('innerHTML' in elem) {
+ elem.innerHTML = contents;
+ }
+ else {
+ try {
+ var ns = elem.ownerDocument.documentElement.namespaceURI;
+ if (!ns) {
+ ns = exports.NS_XHTML;
+ }
+ exports.clearElement(elem);
+ contents = '<div xmlns="' + ns + '">' + contents + '</div>';
+ var range = elem.ownerDocument.createRange();
+ var child = range.createContextualFragment(contents).firstChild;
+ while (child.hasChildNodes()) {
+ elem.appendChild(child.firstChild);
+ }
+ }
+ catch (ex) {
+ console.error('Bad XHTML', ex);
+ console.trace();
+ throw ex;
+ }
+ }
+};
+
+/**
+ * How to detect if we're in an XML document.
+ * In a Mozilla we check that document.xmlVersion = null, however in Chrome
+ * we use document.contentType = undefined.
+ * @param doc The document element to work from (defaulted to the global
+ * 'document' if missing
+ */
+exports.isXmlDocument = function(doc) {
+ doc = doc || document;
+ // Best test for Firefox
+ if (doc.contentType && doc.contentType != 'text/html') {
+ return true;
+ }
+ // Best test for Chrome
+ if (doc.xmlVersion != null) {
+ return true;
+ }
+ return false;
+};
+
+/**
+ * We'd really like to be able to do 'new NodeList()'
+ */
+exports.createEmptyNodeList = function(doc) {
+ if (doc.createDocumentFragment) {
+ return doc.createDocumentFragment().childNodes;
+ }
+ return doc.querySelectorAll('x>:root');
+};
+
+//------------------------------------------------------------------------------
+
+/**
+ * Keyboard handling is a mess. http://unixpapa.com/js/key.html
+ * It would be good to use DOM L3 Keyboard events,
+ * http://www.w3.org/TR/2010/WD-DOM-Level-3-Events-20100907/#events-keyboardevents
+ * however only Webkit supports them, and there isn't a shim on Modernizr:
+ * https://github.com/Modernizr/Modernizr/wiki/HTML5-Cross-browser-Polyfills
+ * and when the code that uses this KeyEvent was written, nothing was clear,
+ * so instead, we're using this unmodern shim:
+ * http://stackoverflow.com/questions/5681146/chrome-10-keyevent-or-something-similar-to-firefoxs-keyevent
+ * See BUG 664991: GCLI's keyboard handling should be updated to use DOM-L3
+ * https://bugzilla.mozilla.org/show_bug.cgi?id=664991
+ */
+exports.KeyEvent = {
+ DOM_VK_CANCEL: 3,
+ DOM_VK_HELP: 6,
+ DOM_VK_BACK_SPACE: 8,
+ DOM_VK_TAB: 9,
+ DOM_VK_CLEAR: 12,
+ DOM_VK_RETURN: 13,
+ DOM_VK_SHIFT: 16,
+ DOM_VK_CONTROL: 17,
+ DOM_VK_ALT: 18,
+ DOM_VK_PAUSE: 19,
+ DOM_VK_CAPS_LOCK: 20,
+ DOM_VK_ESCAPE: 27,
+ DOM_VK_SPACE: 32,
+ DOM_VK_PAGE_UP: 33,
+ DOM_VK_PAGE_DOWN: 34,
+ DOM_VK_END: 35,
+ DOM_VK_HOME: 36,
+ DOM_VK_LEFT: 37,
+ DOM_VK_UP: 38,
+ DOM_VK_RIGHT: 39,
+ DOM_VK_DOWN: 40,
+ DOM_VK_PRINTSCREEN: 44,
+ DOM_VK_INSERT: 45,
+ DOM_VK_DELETE: 46,
+ DOM_VK_0: 48,
+ DOM_VK_1: 49,
+ DOM_VK_2: 50,
+ DOM_VK_3: 51,
+ DOM_VK_4: 52,
+ DOM_VK_5: 53,
+ DOM_VK_6: 54,
+ DOM_VK_7: 55,
+ DOM_VK_8: 56,
+ DOM_VK_9: 57,
+ DOM_VK_SEMICOLON: 59,
+ DOM_VK_EQUALS: 61,
+ DOM_VK_A: 65,
+ DOM_VK_B: 66,
+ DOM_VK_C: 67,
+ DOM_VK_D: 68,
+ DOM_VK_E: 69,
+ DOM_VK_F: 70,
+ DOM_VK_G: 71,
+ DOM_VK_H: 72,
+ DOM_VK_I: 73,
+ DOM_VK_J: 74,
+ DOM_VK_K: 75,
+ DOM_VK_L: 76,
+ DOM_VK_M: 77,
+ DOM_VK_N: 78,
+ DOM_VK_O: 79,
+ DOM_VK_P: 80,
+ DOM_VK_Q: 81,
+ DOM_VK_R: 82,
+ DOM_VK_S: 83,
+ DOM_VK_T: 84,
+ DOM_VK_U: 85,
+ DOM_VK_V: 86,
+ DOM_VK_W: 87,
+ DOM_VK_X: 88,
+ DOM_VK_Y: 89,
+ DOM_VK_Z: 90,
+ DOM_VK_CONTEXT_MENU: 93,
+ DOM_VK_NUMPAD0: 96,
+ DOM_VK_NUMPAD1: 97,
+ DOM_VK_NUMPAD2: 98,
+ DOM_VK_NUMPAD3: 99,
+ DOM_VK_NUMPAD4: 100,
+ DOM_VK_NUMPAD5: 101,
+ DOM_VK_NUMPAD6: 102,
+ DOM_VK_NUMPAD7: 103,
+ DOM_VK_NUMPAD8: 104,
+ DOM_VK_NUMPAD9: 105,
+ DOM_VK_MULTIPLY: 106,
+ DOM_VK_ADD: 107,
+ DOM_VK_SEPARATOR: 108,
+ DOM_VK_SUBTRACT: 109,
+ DOM_VK_DECIMAL: 110,
+ DOM_VK_DIVIDE: 111,
+ DOM_VK_F1: 112,
+ DOM_VK_F2: 113,
+ DOM_VK_F3: 114,
+ DOM_VK_F4: 115,
+ DOM_VK_F5: 116,
+ DOM_VK_F6: 117,
+ DOM_VK_F7: 118,
+ DOM_VK_F8: 119,
+ DOM_VK_F9: 120,
+ DOM_VK_F10: 121,
+ DOM_VK_F11: 122,
+ DOM_VK_F12: 123,
+ DOM_VK_F13: 124,
+ DOM_VK_F14: 125,
+ DOM_VK_F15: 126,
+ DOM_VK_F16: 127,
+ DOM_VK_F17: 128,
+ DOM_VK_F18: 129,
+ DOM_VK_F19: 130,
+ DOM_VK_F20: 131,
+ DOM_VK_F21: 132,
+ DOM_VK_F22: 133,
+ DOM_VK_F23: 134,
+ DOM_VK_F24: 135,
+ DOM_VK_NUM_LOCK: 144,
+ DOM_VK_SCROLL_LOCK: 145,
+ DOM_VK_COMMA: 188,
+ DOM_VK_PERIOD: 190,
+ DOM_VK_SLASH: 191,
+ DOM_VK_BACK_QUOTE: 192,
+ DOM_VK_OPEN_BRACKET: 219,
+ DOM_VK_BACK_SLASH: 220,
+ DOM_VK_CLOSE_BRACKET: 221,
+ DOM_VK_QUOTE: 222,
+ DOM_VK_META: 224
+};