summaryrefslogtreecommitdiffstats
path: root/devtools/shared/gcli
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/shared/gcli')
-rw-r--r--devtools/shared/gcli/commands/addon.js320
-rw-r--r--devtools/shared/gcli/commands/appcache.js186
-rw-r--r--devtools/shared/gcli/commands/calllog.js219
-rw-r--r--devtools/shared/gcli/commands/cmd.js178
-rw-r--r--devtools/shared/gcli/commands/cookie.js300
-rw-r--r--devtools/shared/gcli/commands/csscoverage.js201
-rw-r--r--devtools/shared/gcli/commands/folder.js77
-rw-r--r--devtools/shared/gcli/commands/highlight.js158
-rw-r--r--devtools/shared/gcli/commands/index.js179
-rw-r--r--devtools/shared/gcli/commands/inject.js86
-rw-r--r--devtools/shared/gcli/commands/jsb.js134
-rw-r--r--devtools/shared/gcli/commands/listen.js106
-rw-r--r--devtools/shared/gcli/commands/mdn.js83
-rw-r--r--devtools/shared/gcli/commands/measure.js112
-rw-r--r--devtools/shared/gcli/commands/media.js56
-rw-r--r--devtools/shared/gcli/commands/moz.build30
-rw-r--r--devtools/shared/gcli/commands/pagemod.js276
-rw-r--r--devtools/shared/gcli/commands/paintflashing.js201
-rw-r--r--devtools/shared/gcli/commands/qsa.js24
-rw-r--r--devtools/shared/gcli/commands/restart.js77
-rw-r--r--devtools/shared/gcli/commands/rulers.js110
-rw-r--r--devtools/shared/gcli/commands/screenshot.js579
-rw-r--r--devtools/shared/gcli/commands/security.js328
-rw-r--r--devtools/shared/gcli/moz.build23
-rw-r--r--devtools/shared/gcli/source/LICENSE202
-rw-r--r--devtools/shared/gcli/source/docs/design.md102
-rw-r--r--devtools/shared/gcli/source/docs/developing-gcli.md213
-rw-r--r--devtools/shared/gcli/source/docs/index.md150
-rw-r--r--devtools/shared/gcli/source/docs/running-tests.md60
-rw-r--r--devtools/shared/gcli/source/docs/writing-commands.md757
-rw-r--r--devtools/shared/gcli/source/docs/writing-tests.md20
-rw-r--r--devtools/shared/gcli/source/docs/writing-types.md106
-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
-rw-r--r--devtools/shared/gcli/templater.js602
104 files changed, 21260 insertions, 0 deletions
diff --git a/devtools/shared/gcli/commands/addon.js b/devtools/shared/gcli/commands/addon.js
new file mode 100644
index 000000000..9a38142a3
--- /dev/null
+++ b/devtools/shared/gcli/commands/addon.js
@@ -0,0 +1,320 @@
+/* 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/. */
+
+"use strict";
+
+/**
+ * You can't require the AddonManager in a child process, but GCLI wants to
+ * check for 'items' in all processes, so we return empty array if the
+ * AddonManager is not available
+ */
+function getAddonManager() {
+ try {
+ return {
+ AddonManager: require("resource://gre/modules/AddonManager.jsm").AddonManager,
+ addonManagerActive: true
+ };
+ }
+ catch (ex) {
+ // Fake up an AddonManager just enough to let the file load
+ return {
+ AddonManager: {
+ getAllAddons() {},
+ getAddonsByTypes() {}
+ },
+ addonManagerActive: false
+ };
+ }
+}
+
+const { Cc, Ci, Cu } = require("chrome");
+const { AddonManager, addonManagerActive } = getAddonManager();
+const l10n = require("gcli/l10n");
+
+const BRAND_SHORT_NAME = Cc["@mozilla.org/intl/stringbundle;1"]
+ .getService(Ci.nsIStringBundleService)
+ .createBundle("chrome://branding/locale/brand.properties")
+ .GetStringFromName("brandShortName");
+
+/**
+ * Takes a function that uses a callback as its last parameter, and returns a
+ * new function that returns a promise instead.
+ * This should probably live in async-util
+ */
+const promiseify = function(scope, functionWithLastParamCallback) {
+ return (...args) => {
+ return new Promise(resolve => {
+ args.push((...results) => {
+ resolve(results.length > 1 ? results : results[0]);
+ });
+ functionWithLastParamCallback.apply(scope, args);
+ });
+ }
+};
+
+// Convert callback based functions to promise based ones
+const getAllAddons = promiseify(AddonManager, AddonManager.getAllAddons);
+const getAddonsByTypes = promiseify(AddonManager, AddonManager.getAddonsByTypes);
+
+/**
+ * Return a string array containing the pending operations on an addon
+ */
+function pendingOperations(addon) {
+ let allOperations = [
+ "PENDING_ENABLE", "PENDING_DISABLE", "PENDING_UNINSTALL",
+ "PENDING_INSTALL", "PENDING_UPGRADE"
+ ];
+ return allOperations.reduce(function(operations, opName) {
+ return addon.pendingOperations & AddonManager[opName] ?
+ operations.concat(opName) :
+ operations;
+ }, []);
+}
+
+var items = [
+ {
+ item: "type",
+ name: "addon",
+ parent: "selection",
+ stringifyProperty: "name",
+ cacheable: true,
+ constructor: function() {
+ // Tell GCLI to clear the cache of addons when one is added or removed
+ let listener = {
+ onInstalled: addon => { this.clearCache(); },
+ onUninstalled: addon => { this.clearCache(); },
+ };
+ AddonManager.addAddonListener(listener);
+ },
+ lookup: function() {
+ return getAllAddons().then(addons => {
+ return addons.map(addon => {
+ let name = addon.name + " " + addon.version;
+ name = name.trim().replace(/\s/g, "_");
+ return { name: name, value: addon };
+ });
+ });
+ }
+ },
+ {
+ name: "addon",
+ description: l10n.lookup("addonDesc")
+ },
+ {
+ name: "addon list",
+ description: l10n.lookup("addonListDesc"),
+ returnType: "addonsInfo",
+ params: [{
+ name: "type",
+ type: {
+ name: "selection",
+ data: [ "dictionary", "extension", "locale", "plugin", "theme", "all" ]
+ },
+ defaultValue: "all",
+ description: l10n.lookup("addonListTypeDesc")
+ }],
+ exec: function(args, context) {
+ let types = (args.type === "all") ? null : [ args.type ];
+ return getAddonsByTypes(types).then(addons => {
+ addons = addons.map(function(addon) {
+ return {
+ name: addon.name,
+ version: addon.version,
+ isActive: addon.isActive,
+ pendingOperations: pendingOperations(addon)
+ };
+ });
+ return { addons: addons, type: args.type };
+ });
+ }
+ },
+ {
+ item: "converter",
+ from: "addonsInfo",
+ to: "view",
+ exec: function(addonsInfo, context) {
+ if (!addonsInfo.addons.length) {
+ return context.createView({
+ html: "<p>${message}</p>",
+ data: { message: l10n.lookup("addonNoneOfType") }
+ });
+ }
+
+ let headerLookups = {
+ "dictionary": "addonListDictionaryHeading",
+ "extension": "addonListExtensionHeading",
+ "locale": "addonListLocaleHeading",
+ "plugin": "addonListPluginHeading",
+ "theme": "addonListThemeHeading",
+ "all": "addonListAllHeading"
+ };
+ let header = l10n.lookup(headerLookups[addonsInfo.type] ||
+ "addonListUnknownHeading");
+
+ let operationLookups = {
+ "PENDING_ENABLE": "addonPendingEnable",
+ "PENDING_DISABLE": "addonPendingDisable",
+ "PENDING_UNINSTALL": "addonPendingUninstall",
+ "PENDING_INSTALL": "addonPendingInstall",
+ "PENDING_UPGRADE": "addonPendingUpgrade"
+ };
+ function lookupOperation(opName) {
+ let lookupName = operationLookups[opName];
+ return lookupName ? l10n.lookup(lookupName) : opName;
+ }
+
+ function arrangeAddons(addons) {
+ let enabledAddons = [];
+ let disabledAddons = [];
+ addons.forEach(function(addon) {
+ if (addon.isActive) {
+ enabledAddons.push(addon);
+ } else {
+ disabledAddons.push(addon);
+ }
+ });
+
+ function compareAddonNames(nameA, nameB) {
+ return String.localeCompare(nameA.name, nameB.name);
+ }
+ enabledAddons.sort(compareAddonNames);
+ disabledAddons.sort(compareAddonNames);
+
+ return enabledAddons.concat(disabledAddons);
+ }
+
+ function isActiveForToggle(addon) {
+ return (addon.isActive && ~~addon.pendingOperations.indexOf("PENDING_DISABLE"));
+ }
+
+ return context.createView({
+ html:
+ "<table>" +
+ " <caption>${header}</caption>" +
+ " <tbody>" +
+ " <tr foreach='addon in ${addons}'" +
+ " class=\"gcli-addon-${addon.status}\">" +
+ " <td>${addon.name} ${addon.version}</td>" +
+ " <td>${addon.pendingOperations}</td>" +
+ " <td>" +
+ " <span class='gcli-out-shortcut'" +
+ " data-command='addon ${addon.toggleActionName} ${addon.label}'" +
+ " onclick='${onclick}' ondblclick='${ondblclick}'" +
+ " >${addon.toggleActionMessage}</span>" +
+ " </td>" +
+ " </tr>" +
+ " </tbody>" +
+ "</table>",
+ data: {
+ header: header,
+ addons: arrangeAddons(addonsInfo.addons).map(function(addon) {
+ return {
+ name: addon.name,
+ label: addon.name.replace(/\s/g, "_") +
+ (addon.version ? "_" + addon.version : ""),
+ status: addon.isActive ? "enabled" : "disabled",
+ version: addon.version,
+ pendingOperations: addon.pendingOperations.length ?
+ (" (" + l10n.lookup("addonPending") + ": "
+ + addon.pendingOperations.map(lookupOperation).join(", ")
+ + ")") :
+ "",
+ toggleActionName: isActiveForToggle(addon) ? "disable": "enable",
+ toggleActionMessage: isActiveForToggle(addon) ?
+ l10n.lookup("addonListOutDisable") :
+ l10n.lookup("addonListOutEnable")
+ };
+ }),
+ onclick: context.update,
+ ondblclick: context.updateExec
+ }
+ });
+ }
+ },
+ {
+ item: "command",
+ runAt: "client",
+ name: "addon enable",
+ description: l10n.lookup("addonEnableDesc"),
+ params: [
+ {
+ name: "addon",
+ type: "addon",
+ description: l10n.lookup("addonNameDesc")
+ }
+ ],
+ exec: function(args, context) {
+ let name = (args.addon.name + " " + args.addon.version).trim();
+ if (args.addon.userDisabled) {
+ args.addon.userDisabled = false;
+ return l10n.lookupFormat("addonEnabled", [ name ]);
+ }
+
+ return l10n.lookupFormat("addonAlreadyEnabled", [ name ]);
+ }
+ },
+ {
+ item: "command",
+ runAt: "client",
+ name: "addon disable",
+ description: l10n.lookup("addonDisableDesc"),
+ params: [
+ {
+ name: "addon",
+ type: "addon",
+ description: l10n.lookup("addonNameDesc")
+ }
+ ],
+ exec: function(args, context) {
+ // If the addon is not disabled or is set to "click to play" then
+ // disable it. Otherwise display the message "Add-on is already
+ // disabled."
+ let name = (args.addon.name + " " + args.addon.version).trim();
+ if (!args.addon.userDisabled ||
+ args.addon.userDisabled === AddonManager.STATE_ASK_TO_ACTIVATE) {
+ args.addon.userDisabled = true;
+ return l10n.lookupFormat("addonDisabled", [ name ]);
+ }
+
+ return l10n.lookupFormat("addonAlreadyDisabled", [ name ]);
+ }
+ },
+ {
+ item: "command",
+ runAt: "client",
+ name: "addon ctp",
+ description: l10n.lookup("addonCtpDesc"),
+ params: [
+ {
+ name: "addon",
+ type: "addon",
+ description: l10n.lookup("addonNameDesc")
+ }
+ ],
+ exec: function(args, context) {
+ let name = (args.addon.name + " " + args.addon.version).trim();
+ if (args.addon.type !== "plugin") {
+ return l10n.lookupFormat("addonCantCtp", [ name ]);
+ }
+
+ if (!args.addon.userDisabled ||
+ args.addon.userDisabled === true) {
+ args.addon.userDisabled = AddonManager.STATE_ASK_TO_ACTIVATE;
+
+ if (args.addon.userDisabled !== AddonManager.STATE_ASK_TO_ACTIVATE) {
+ // Some plugins (e.g. OpenH264 shipped with Firefox) cannot be set to
+ // click-to-play. Handle this.
+
+ return l10n.lookupFormat("addonNoCtp", [ name ]);
+ }
+
+ return l10n.lookupFormat("addonCtp", [ name ]);
+ }
+
+ return l10n.lookupFormat("addonAlreadyCtp", [ name ]);
+ }
+ }
+];
+
+exports.items = addonManagerActive ? items : [];
diff --git a/devtools/shared/gcli/commands/appcache.js b/devtools/shared/gcli/commands/appcache.js
new file mode 100644
index 000000000..0789fb5a0
--- /dev/null
+++ b/devtools/shared/gcli/commands/appcache.js
@@ -0,0 +1,186 @@
+/* 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/. */
+
+"use strict";
+
+const l10n = require("gcli/l10n");
+
+loader.lazyImporter(this, "AppCacheUtils", "resource://devtools/client/shared/AppCacheUtils.jsm");
+
+exports.items = [
+ {
+ item: "command",
+ name: "appcache",
+ description: l10n.lookup("appCacheDesc")
+ },
+ {
+ item: "command",
+ runAt: "server",
+ name: "appcache validate",
+ description: l10n.lookup("appCacheValidateDesc"),
+ manual: l10n.lookup("appCacheValidateManual"),
+ returnType: "appcacheerrors",
+ params: [{
+ group: "options",
+ params: [
+ {
+ type: "string",
+ name: "uri",
+ description: l10n.lookup("appCacheValidateUriDesc"),
+ defaultValue: null,
+ }
+ ]
+ }],
+ exec: function(args, context) {
+ let utils;
+ let deferred = context.defer();
+
+ if (args.uri) {
+ utils = new AppCacheUtils(args.uri);
+ } else {
+ utils = new AppCacheUtils(context.environment.document);
+ }
+
+ utils.validateManifest().then(function(errors) {
+ deferred.resolve([errors, utils.manifestURI || "-"]);
+ });
+
+ return deferred.promise;
+ }
+ },
+ {
+ item: "converter",
+ from: "appcacheerrors",
+ to: "view",
+ exec: function([errors, manifestURI], context) {
+ if (errors.length == 0) {
+ return context.createView({
+ html: "<span>" + l10n.lookup("appCacheValidatedSuccessfully") + "</span>"
+ });
+ }
+
+ return context.createView({
+ html:
+ "<div>" +
+ " <h4>Manifest URI: ${manifestURI}</h4>" +
+ " <ol>" +
+ " <li foreach='error in ${errors}'>${error.msg}</li>" +
+ " </ol>" +
+ "</div>",
+ data: {
+ errors: errors,
+ manifestURI: manifestURI
+ }
+ });
+ }
+ },
+ {
+ item: "command",
+ runAt: "server",
+ name: "appcache clear",
+ description: l10n.lookup("appCacheClearDesc"),
+ manual: l10n.lookup("appCacheClearManual"),
+ exec: function(args, context) {
+ let utils = new AppCacheUtils(args.uri);
+ utils.clearAll();
+
+ return l10n.lookup("appCacheClearCleared");
+ }
+ },
+ {
+ item: "command",
+ runAt: "server",
+ name: "appcache list",
+ description: l10n.lookup("appCacheListDesc"),
+ manual: l10n.lookup("appCacheListManual"),
+ returnType: "appcacheentries",
+ params: [{
+ group: "options",
+ params: [
+ {
+ type: "string",
+ name: "search",
+ description: l10n.lookup("appCacheListSearchDesc"),
+ defaultValue: null,
+ },
+ ]
+ }],
+ exec: function(args, context) {
+ let utils = new AppCacheUtils();
+ return utils.listEntries(args.search);
+ }
+ },
+ {
+ item: "converter",
+ from: "appcacheentries",
+ to: "view",
+ exec: function(entries, context) {
+ return context.createView({
+ html: "" +
+ "<ul class='gcli-appcache-list'>" +
+ " <li foreach='entry in ${entries}'>" +
+ " <table class='gcli-appcache-detail'>" +
+ " <tr>" +
+ " <td>" + l10n.lookup("appCacheListKey") + "</td>" +
+ " <td>${entry.key}</td>" +
+ " </tr>" +
+ " <tr>" +
+ " <td>" + l10n.lookup("appCacheListFetchCount") + "</td>" +
+ " <td>${entry.fetchCount}</td>" +
+ " </tr>" +
+ " <tr>" +
+ " <td>" + l10n.lookup("appCacheListLastFetched") + "</td>" +
+ " <td>${entry.lastFetched}</td>" +
+ " </tr>" +
+ " <tr>" +
+ " <td>" + l10n.lookup("appCacheListLastModified") + "</td>" +
+ " <td>${entry.lastModified}</td>" +
+ " </tr>" +
+ " <tr>" +
+ " <td>" + l10n.lookup("appCacheListExpirationTime") + "</td>" +
+ " <td>${entry.expirationTime}</td>" +
+ " </tr>" +
+ " <tr>" +
+ " <td>" + l10n.lookup("appCacheListDataSize") + "</td>" +
+ " <td>${entry.dataSize}</td>" +
+ " </tr>" +
+ " <tr>" +
+ " <td>" + l10n.lookup("appCacheListDeviceID") + "</td>" +
+ " <td>${entry.deviceID} <span class='gcli-out-shortcut' " +
+ "onclick='${onclick}' ondblclick='${ondblclick}' " +
+ "data-command='appcache viewentry ${entry.key}'" +
+ ">" + l10n.lookup("appCacheListViewEntry") + "</span>" +
+ " </td>" +
+ " </tr>" +
+ " </table>" +
+ " </li>" +
+ "</ul>",
+ data: {
+ entries: entries,
+ onclick: context.update,
+ ondblclick: context.updateExec
+ }
+ });
+ }
+ },
+ {
+ item: "command",
+ runAt: "server",
+ name: "appcache viewentry",
+ description: l10n.lookup("appCacheViewEntryDesc"),
+ manual: l10n.lookup("appCacheViewEntryManual"),
+ params: [
+ {
+ type: "string",
+ name: "key",
+ description: l10n.lookup("appCacheViewEntryKey"),
+ defaultValue: null,
+ }
+ ],
+ exec: function(args, context) {
+ let utils = new AppCacheUtils();
+ return utils.viewEntry(args.key);
+ }
+ }
+];
diff --git a/devtools/shared/gcli/commands/calllog.js b/devtools/shared/gcli/commands/calllog.js
new file mode 100644
index 000000000..c0f21aeab
--- /dev/null
+++ b/devtools/shared/gcli/commands/calllog.js
@@ -0,0 +1,219 @@
+/* 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/. */
+
+"use strict";
+
+const { Cu } = require("chrome");
+const l10n = require("gcli/l10n");
+const gcli = require("gcli/index");
+const Debugger = require("Debugger");
+
+loader.lazyRequireGetter(this, "gDevTools", "devtools/client/framework/devtools", true);
+loader.lazyRequireGetter(this, "TargetFactory", "devtools/client/framework/target", true);
+
+var debuggers = [];
+var chromeDebuggers = [];
+var sandboxes = [];
+
+exports.items = [
+ {
+ name: "calllog",
+ description: l10n.lookup("calllogDesc")
+ },
+ {
+ item: "command",
+ runAt: "client",
+ name: "calllog start",
+ description: l10n.lookup("calllogStartDesc"),
+
+ exec: function(args, context) {
+ let contentWindow = context.environment.window;
+
+ let dbg = new Debugger(contentWindow);
+ dbg.onEnterFrame = function(frame) {
+ // BUG 773652 - Make the output from the GCLI calllog command nicer
+ contentWindow.console.log("Method call: " + this.callDescription(frame));
+ }.bind(this);
+
+ debuggers.push(dbg);
+
+ let gBrowser = context.environment.chromeDocument.defaultView.gBrowser;
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+ gDevTools.showToolbox(target, "webconsole");
+
+ return l10n.lookup("calllogStartReply");
+ },
+
+ callDescription: function(frame) {
+ let name = "<anonymous>";
+ if (frame.callee.name) {
+ name = frame.callee.name;
+ }
+ else {
+ let desc = frame.callee.getOwnPropertyDescriptor("displayName");
+ if (desc && desc.value && typeof desc.value == "string") {
+ name = desc.value;
+ }
+ }
+
+ let args = frame.arguments.map(this.valueToString).join(", ");
+ return name + "(" + args + ")";
+ },
+
+ valueToString: function(value) {
+ if (typeof value !== "object" || value === null) {
+ return uneval(value);
+ }
+ return "[object " + value.class + "]";
+ }
+ },
+ {
+ item: "command",
+ runAt: "client",
+ name: "calllog stop",
+ description: l10n.lookup("calllogStopDesc"),
+
+ exec: function(args, context) {
+ let numDebuggers = debuggers.length;
+ if (numDebuggers == 0) {
+ return l10n.lookup("calllogStopNoLogging");
+ }
+
+ for (let dbg of debuggers) {
+ dbg.onEnterFrame = undefined;
+ }
+ debuggers = [];
+
+ return l10n.lookupFormat("calllogStopReply", [ numDebuggers ]);
+ }
+ },
+ {
+ item: "command",
+ runAt: "client",
+ name: "calllog chromestart",
+ description: l10n.lookup("calllogChromeStartDesc"),
+ get hidden() {
+ return gcli.hiddenByChromePref();
+ },
+ params: [
+ {
+ name: "sourceType",
+ type: {
+ name: "selection",
+ data: ["content-variable", "chrome-variable", "jsm", "javascript"]
+ }
+ },
+ {
+ name: "source",
+ type: "string",
+ description: l10n.lookup("calllogChromeSourceTypeDesc"),
+ manual: l10n.lookup("calllogChromeSourceTypeManual"),
+ }
+ ],
+ exec: function(args, context) {
+ let globalObj;
+ let contentWindow = context.environment.window;
+
+ if (args.sourceType == "jsm") {
+ try {
+ globalObj = Cu.import(args.source, {});
+ } catch (e) {
+ return l10n.lookup("callLogChromeInvalidJSM");
+ }
+ } else if (args.sourceType == "content-variable") {
+ if (args.source in contentWindow) {
+ globalObj = Cu.getGlobalForObject(contentWindow[args.source]);
+ } else {
+ throw new Error(l10n.lookup("callLogChromeVarNotFoundContent"));
+ }
+ } else if (args.sourceType == "chrome-variable") {
+ let chromeWin = context.environment.chromeDocument.defaultView;
+ if (args.source in chromeWin) {
+ globalObj = Cu.getGlobalForObject(chromeWin[args.source]);
+ } else {
+ return l10n.lookup("callLogChromeVarNotFoundChrome");
+ }
+ } else {
+ let chromeWin = context.environment.chromeDocument.defaultView;
+ let sandbox = new Cu.Sandbox(chromeWin,
+ {
+ sandboxPrototype: chromeWin,
+ wantXrays: false,
+ sandboxName: "gcli-cmd-calllog-chrome"
+ });
+ let returnVal;
+ try {
+ returnVal = Cu.evalInSandbox(args.source, sandbox, "ECMAv5");
+ sandboxes.push(sandbox);
+ } catch(e) {
+ // We need to save the message before cleaning up else e contains a dead
+ // object.
+ let msg = l10n.lookup("callLogChromeEvalException") + ": " + e;
+ Cu.nukeSandbox(sandbox);
+ return msg;
+ }
+
+ if (typeof returnVal == "undefined") {
+ return l10n.lookup("callLogChromeEvalNeedsObject");
+ }
+
+ globalObj = Cu.getGlobalForObject(returnVal);
+ }
+
+ let dbg = new Debugger(globalObj);
+ chromeDebuggers.push(dbg);
+
+ dbg.onEnterFrame = function(frame) {
+ // BUG 773652 - Make the output from the GCLI calllog command nicer
+ contentWindow.console.log(l10n.lookup("callLogChromeMethodCall") +
+ ": " + this.callDescription(frame));
+ }.bind(this);
+
+ let gBrowser = context.environment.chromeDocument.defaultView.gBrowser;
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+ gDevTools.showToolbox(target, "webconsole");
+
+ return l10n.lookup("calllogChromeStartReply");
+ },
+
+ valueToString: function(value) {
+ if (typeof value !== "object" || value === null)
+ return uneval(value);
+ return "[object " + value.class + "]";
+ },
+
+ callDescription: function(frame) {
+ let name = frame.callee.name || l10n.lookup("callLogChromeAnonFunction");
+ let args = frame.arguments.map(this.valueToString).join(", ");
+ return name + "(" + args + ")";
+ }
+ },
+ {
+ item: "command",
+ runAt: "client",
+ name: "calllog chromestop",
+ description: l10n.lookup("calllogChromeStopDesc"),
+ get hidden() {
+ return gcli.hiddenByChromePref();
+ },
+ exec: function(args, context) {
+ let numDebuggers = chromeDebuggers.length;
+ if (numDebuggers == 0) {
+ return l10n.lookup("calllogChromeStopNoLogging");
+ }
+
+ for (let dbg of chromeDebuggers) {
+ dbg.onEnterFrame = undefined;
+ dbg.enabled = false;
+ }
+ for (let sandbox of sandboxes) {
+ Cu.nukeSandbox(sandbox);
+ }
+ chromeDebuggers = [];
+ sandboxes = [];
+
+ return l10n.lookupFormat("calllogChromeStopReply", [ numDebuggers ]);
+ }
+ }
+];
diff --git a/devtools/shared/gcli/commands/cmd.js b/devtools/shared/gcli/commands/cmd.js
new file mode 100644
index 000000000..1777ed960
--- /dev/null
+++ b/devtools/shared/gcli/commands/cmd.js
@@ -0,0 +1,178 @@
+/* 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/. */
+
+"use strict";
+
+const { Cc, Ci, Cu } = require("chrome");
+
+const { OS } = Cu.import("resource://gre/modules/osfile.jsm", {});
+const { TextEncoder, TextDecoder } = Cu.import('resource://gre/modules/commonjs/toolkit/loader.js', {});
+const gcli = require("gcli/index");
+const l10n = require("gcli/l10n");
+
+loader.lazyGetter(this, "prefBranch", function() {
+ let prefService = Cc["@mozilla.org/preferences-service;1"].getService(Ci.nsIPrefService);
+ return prefService.getBranch(null).QueryInterface(Ci.nsIPrefBranch2);
+});
+
+loader.lazyGetter(this, "supportsString", function() {
+ return Cc["@mozilla.org/supports-string;1"].createInstance(Ci.nsISupportsString);
+});
+
+loader.lazyImporter(this, "NetUtil", "resource://gre/modules/NetUtil.jsm");
+
+const PREF_DIR = "devtools.commands.dir";
+
+/**
+ * Load all the .mozcmd files in the directory pointed to by PREF_DIR
+ * @return A promise of an array of items suitable for gcli.addItems or
+ * using in gcli.addItemsByModule
+ */
+function loadItemsFromMozDir() {
+ let dirName = prefBranch.getComplexValue(PREF_DIR,
+ Ci.nsISupportsString).data.trim();
+ if (dirName == "") {
+ return Promise.resolve([]);
+ }
+
+ // replaces ~ with the home directory path in unix and windows
+ if (dirName.indexOf("~") == 0) {
+ let dirService = Cc["@mozilla.org/file/directory_service;1"]
+ .getService(Ci.nsIProperties);
+ let homeDirFile = dirService.get("Home", Ci.nsIFile);
+ let homeDir = homeDirFile.path;
+ dirName = dirName.substr(1);
+ dirName = homeDir + dirName;
+ }
+
+ // statPromise resolves to nothing if dirName is a directory, or it
+ // rejects with an error message otherwise
+ let statPromise = OS.File.stat(dirName);
+ statPromise = statPromise.then(
+ function onSuccess(stat) {
+ if (!stat.isDir) {
+ throw new Error("'" + dirName + "' is not a directory.");
+ }
+ },
+ function onFailure(reason) {
+ if (reason instanceof OS.File.Error && reason.becauseNoSuchFile) {
+ throw new Error("'" + dirName + "' does not exist.");
+ } else {
+ throw reason;
+ }
+ }
+ );
+
+ // We need to return (a promise of) an array of items from the *.mozcmd
+ // files in dirName (which we can assume to be a valid directory now)
+ return statPromise.then(() => {
+ let itemPromises = [];
+
+ let iterator = new OS.File.DirectoryIterator(dirName);
+ let iterPromise = iterator.forEach(entry => {
+ if (entry.name.match(/.*\.mozcmd$/) && !entry.isDir) {
+ itemPromises.push(loadCommandFile(entry));
+ }
+ });
+
+ return iterPromise.then(() => {
+ iterator.close();
+ return Promise.all(itemPromises).then((itemsArray) => {
+ return itemsArray.reduce((prev, curr) => {
+ return prev.concat(curr);
+ }, []);
+ });
+ }, reason => { iterator.close(); throw reason; });
+ });
+}
+
+exports.mozDirLoader = function(name) {
+ return loadItemsFromMozDir().then(items => {
+ return { items: items };
+ });
+};
+
+/**
+ * Load the commands from a single file
+ * @param OS.File.DirectoryIterator.Entry entry The DirectoryIterator
+ * Entry of the file containing the commands that we should read
+ */
+function loadCommandFile(entry) {
+ let readPromise = OS.File.read(entry.path);
+ return readPromise = readPromise.then(array => {
+ let decoder = new TextDecoder();
+ let source = decoder.decode(array);
+ var principal = Cc["@mozilla.org/systemprincipal;1"]
+ .createInstance(Ci.nsIPrincipal);
+
+ let sandbox = new Cu.Sandbox(principal, {
+ sandboxName: entry.path
+ });
+ let data = Cu.evalInSandbox(source, sandbox, "1.8", entry.name, 1);
+
+ if (!Array.isArray(data)) {
+ console.error("Command file '" + entry.name + "' does not have top level array.");
+ return;
+ }
+
+ return data;
+ });
+}
+
+exports.items = [
+ {
+ name: "cmd",
+ get hidden() {
+ return !prefBranch.prefHasUserValue(PREF_DIR);
+ },
+ description: l10n.lookup("cmdDesc")
+ },
+ {
+ item: "command",
+ runAt: "client",
+ name: "cmd refresh",
+ description: l10n.lookup("cmdRefreshDesc"),
+ get hidden() {
+ return !prefBranch.prefHasUserValue(PREF_DIR);
+ },
+ exec: function(args, context) {
+ gcli.load();
+
+ let dirName = prefBranch.getComplexValue(PREF_DIR,
+ Ci.nsISupportsString).data.trim();
+ return l10n.lookupFormat("cmdStatus3", [ dirName ]);
+ }
+ },
+ {
+ item: "command",
+ runAt: "client",
+ name: "cmd setdir",
+ description: l10n.lookup("cmdSetdirDesc"),
+ manual: l10n.lookup("cmdSetdirManual3"),
+ params: [
+ {
+ name: "directory",
+ description: l10n.lookup("cmdSetdirDirectoryDesc"),
+ type: {
+ name: "file",
+ filetype: "directory",
+ existing: "yes"
+ },
+ defaultValue: null
+ }
+ ],
+ returnType: "string",
+ get hidden() {
+ return true; // !prefBranch.prefHasUserValue(PREF_DIR);
+ },
+ exec: function(args, context) {
+ supportsString.data = args.directory;
+ prefBranch.setComplexValue(PREF_DIR, Ci.nsISupportsString, supportsString);
+
+ gcli.load();
+
+ return l10n.lookupFormat("cmdStatus3", [ args.directory ]);
+ }
+ }
+];
diff --git a/devtools/shared/gcli/commands/cookie.js b/devtools/shared/gcli/commands/cookie.js
new file mode 100644
index 000000000..f1680042f
--- /dev/null
+++ b/devtools/shared/gcli/commands/cookie.js
@@ -0,0 +1,300 @@
+/* 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/. */
+
+"use strict";
+
+/**
+ * XXX: bug 1221488 is required to make these commands run on the server.
+ * If we want these commands to run on remote devices/connections, they need to
+ * run on the server (runAt=server). Unfortunately, cookie commands not only
+ * need to run on the server, they also need to access to the parent process to
+ * retrieve and manipulate cookies via nsICookieManager2.
+ * However, server-running commands have no way of accessing the parent process
+ * for now.
+ *
+ * So, because these cookie commands, as of today, only run in the developer
+ * toolbar (the gcli command bar), and because this toolbar is only available on
+ * a local Firefox desktop tab (not in webide or the browser toolbox), we can
+ * make the commands run on the client.
+ * This way, they'll always run in the parent process.
+ */
+
+const { Ci, Cc } = require("chrome");
+const l10n = require("gcli/l10n");
+const { XPCOMUtils } = require("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyGetter(this, "cookieMgr", function() {
+ return Cc["@mozilla.org/cookiemanager;1"].getService(Ci.nsICookieManager2);
+});
+
+/**
+ * Check host value and remove port part as it is not used
+ * for storing cookies.
+ *
+ * Parameter will usually be `new URL(context.environment.target.url).host`
+ */
+function sanitizeHost(host) {
+ if (host == null || host == "") {
+ throw new Error(l10n.lookup("cookieListOutNonePage"));
+ }
+ return host.split(":")[0];
+}
+
+/**
+ * The cookie 'expires' value needs converting into something more readable.
+ *
+ * And the unit of expires is sec, the unit that in argument of Date() needs
+ * millisecond.
+ */
+function translateExpires(expires) {
+ if (expires == 0) {
+ return l10n.lookup("cookieListOutSession");
+ }
+
+ let expires_msec = expires * 1000;
+
+ return (new Date(expires_msec)).toLocaleString();
+}
+
+/**
+ * Check if a given cookie matches a given host
+ */
+function isCookieAtHost(cookie, host) {
+ if (cookie.host == null) {
+ return host == null;
+ }
+ if (cookie.host.startsWith(".")) {
+ return ("." + host).endsWith(cookie.host);
+ }
+ if (cookie.host === "") {
+ return host.startsWith("file://" + cookie.path);
+ }
+ return cookie.host == host;
+}
+
+exports.items = [
+ {
+ name: "cookie",
+ description: l10n.lookup("cookieDesc"),
+ manual: l10n.lookup("cookieManual")
+ },
+ {
+ item: "command",
+ runAt: "client",
+ name: "cookie list",
+ description: l10n.lookup("cookieListDesc"),
+ manual: l10n.lookup("cookieListManual"),
+ returnType: "cookies",
+ exec: function(args, context) {
+ if (context.environment.target.isRemote) {
+ throw new Error("The cookie gcli commands only work in a local tab, " +
+ "see bug 1221488");
+ }
+ let host = new URL(context.environment.target.url).host;
+ let contentWindow = context.environment.window;
+ host = sanitizeHost(host);
+ let enm = cookieMgr.getCookiesFromHost(host, contentWindow.document.
+ nodePrincipal.
+ originAttributes);
+
+ let cookies = [];
+ while (enm.hasMoreElements()) {
+ let cookie = enm.getNext().QueryInterface(Ci.nsICookie);
+ if (isCookieAtHost(cookie, host)) {
+ cookies.push({
+ host: cookie.host,
+ name: cookie.name,
+ value: cookie.value,
+ path: cookie.path,
+ expires: cookie.expires,
+ secure: cookie.secure,
+ httpOnly: cookie.httpOnly,
+ sameDomain: cookie.sameDomain
+ });
+ }
+ }
+
+ return cookies;
+ }
+ },
+ {
+ item: "command",
+ runAt: "client",
+ name: "cookie remove",
+ description: l10n.lookup("cookieRemoveDesc"),
+ manual: l10n.lookup("cookieRemoveManual"),
+ params: [
+ {
+ name: "name",
+ type: "string",
+ description: l10n.lookup("cookieRemoveKeyDesc"),
+ }
+ ],
+ exec: function(args, context) {
+ if (context.environment.target.isRemote) {
+ throw new Error("The cookie gcli commands only work in a local tab, " +
+ "see bug 1221488");
+ }
+ let host = new URL(context.environment.target.url).host;
+ let contentWindow = context.environment.window;
+ host = sanitizeHost(host);
+ let enm = cookieMgr.getCookiesFromHost(host, contentWindow.document.
+ nodePrincipal.
+ originAttributes);
+
+ while (enm.hasMoreElements()) {
+ let cookie = enm.getNext().QueryInterface(Ci.nsICookie);
+ if (isCookieAtHost(cookie, host)) {
+ if (cookie.name == args.name) {
+ cookieMgr.remove(cookie.host, cookie.name, cookie.path,
+ false, cookie.originAttributes);
+ }
+ }
+ }
+ }
+ },
+ {
+ item: "converter",
+ from: "cookies",
+ to: "view",
+ exec: function(cookies, context) {
+ if (cookies.length == 0) {
+ let host = new URL(context.environment.target.url).host;
+ host = sanitizeHost(host);
+ let msg = l10n.lookupFormat("cookieListOutNoneHost", [ host ]);
+ return context.createView({ html: "<span>" + msg + "</span>" });
+ }
+
+ for (let cookie of cookies) {
+ cookie.expires = translateExpires(cookie.expires);
+
+ let noAttrs = !cookie.secure && !cookie.httpOnly && !cookie.sameDomain;
+ cookie.attrs = (cookie.secure ? "secure" : " ") +
+ (cookie.httpOnly ? "httpOnly" : " ") +
+ (cookie.sameDomain ? "sameDomain" : " ") +
+ (noAttrs ? l10n.lookup("cookieListOutNone") : " ");
+ }
+
+ return context.createView({
+ html:
+ "<ul class='gcli-cookielist-list'>" +
+ " <li foreach='cookie in ${cookies}'>" +
+ " <div>${cookie.name}=${cookie.value}</div>" +
+ " <table class='gcli-cookielist-detail'>" +
+ " <tr>" +
+ " <td>" + l10n.lookup("cookieListOutHost") + "</td>" +
+ " <td>${cookie.host}</td>" +
+ " </tr>" +
+ " <tr>" +
+ " <td>" + l10n.lookup("cookieListOutPath") + "</td>" +
+ " <td>${cookie.path}</td>" +
+ " </tr>" +
+ " <tr>" +
+ " <td>" + l10n.lookup("cookieListOutExpires") + "</td>" +
+ " <td>${cookie.expires}</td>" +
+ " </tr>" +
+ " <tr>" +
+ " <td>" + l10n.lookup("cookieListOutAttributes") + "</td>" +
+ " <td>${cookie.attrs}</td>" +
+ " </tr>" +
+ " <tr><td colspan='2'>" +
+ " <span class='gcli-out-shortcut' onclick='${onclick}'" +
+ " data-command='cookie set ${cookie.name} '" +
+ " >" + l10n.lookup("cookieListOutEdit") + "</span>" +
+ " <span class='gcli-out-shortcut'" +
+ " onclick='${onclick}' ondblclick='${ondblclick}'" +
+ " data-command='cookie remove ${cookie.name}'" +
+ " >" + l10n.lookup("cookieListOutRemove") + "</span>" +
+ " </td></tr>" +
+ " </table>" +
+ " </li>" +
+ "</ul>",
+ data: {
+ options: { allowEval: true },
+ cookies: cookies,
+ onclick: context.update,
+ ondblclick: context.updateExec
+ }
+ });
+ }
+ },
+ {
+ item: "command",
+ runAt: "client",
+ name: "cookie set",
+ description: l10n.lookup("cookieSetDesc"),
+ manual: l10n.lookup("cookieSetManual"),
+ params: [
+ {
+ name: "name",
+ type: "string",
+ description: l10n.lookup("cookieSetKeyDesc")
+ },
+ {
+ name: "value",
+ type: "string",
+ description: l10n.lookup("cookieSetValueDesc")
+ },
+ {
+ group: l10n.lookup("cookieSetOptionsDesc"),
+ params: [
+ {
+ name: "path",
+ type: { name: "string", allowBlank: true },
+ defaultValue: "/",
+ description: l10n.lookup("cookieSetPathDesc")
+ },
+ {
+ name: "domain",
+ type: "string",
+ defaultValue: null,
+ description: l10n.lookup("cookieSetDomainDesc")
+ },
+ {
+ name: "secure",
+ type: "boolean",
+ description: l10n.lookup("cookieSetSecureDesc")
+ },
+ {
+ name: "httpOnly",
+ type: "boolean",
+ description: l10n.lookup("cookieSetHttpOnlyDesc")
+ },
+ {
+ name: "session",
+ type: "boolean",
+ description: l10n.lookup("cookieSetSessionDesc")
+ },
+ {
+ name: "expires",
+ type: "string",
+ defaultValue: "Jan 17, 2038",
+ description: l10n.lookup("cookieSetExpiresDesc")
+ },
+ ]
+ }
+ ],
+ exec: function(args, context) {
+ if (context.environment.target.isRemote) {
+ throw new Error("The cookie gcli commands only work in a local tab, " +
+ "see bug 1221488");
+ }
+ let host = new URL(context.environment.target.url).host;
+ host = sanitizeHost(host);
+ let time = Date.parse(args.expires) / 1000;
+ let contentWindow = context.environment.window;
+ cookieMgr.add(args.domain ? "." + args.domain : host,
+ args.path ? args.path : "/",
+ args.name,
+ args.value,
+ args.secure,
+ args.httpOnly,
+ args.session,
+ time,
+ contentWindow.document.
+ nodePrincipal.
+ originAttributes);
+ }
+ }
+];
diff --git a/devtools/shared/gcli/commands/csscoverage.js b/devtools/shared/gcli/commands/csscoverage.js
new file mode 100644
index 000000000..ebbf0baca
--- /dev/null
+++ b/devtools/shared/gcli/commands/csscoverage.js
@@ -0,0 +1,201 @@
+/* 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/. */
+
+"use strict";
+
+const { Cc, Ci } = require("chrome");
+
+const domtemplate = require("gcli/util/domtemplate");
+const csscoverage = require("devtools/shared/fronts/csscoverage");
+const l10n = csscoverage.l10n;
+
+loader.lazyRequireGetter(this, "gDevTools", "devtools/client/framework/devtools", true);
+
+loader.lazyImporter(this, "Chart", "resource://devtools/client/shared/widgets/Chart.jsm");
+
+/**
+ * The commands/converters for GCLI
+ */
+exports.items = [
+ {
+ name: "csscoverage",
+ hidden: true,
+ description: l10n.lookup("csscoverageDesc"),
+ },
+ {
+ item: "command",
+ runAt: "client",
+ name: "csscoverage start",
+ hidden: true,
+ description: l10n.lookup("csscoverageStartDesc2"),
+ params: [
+ {
+ name: "noreload",
+ type: "boolean",
+ description: l10n.lookup("csscoverageStartNoReloadDesc"),
+ manual: l10n.lookup("csscoverageStartNoReloadManual")
+ }
+ ],
+ exec: function*(args, context) {
+ let usage = yield csscoverage.getUsage(context.environment.target);
+ if (usage == null) {
+ throw new Error(l10n.lookup("csscoverageNoRemoteError"));
+ }
+ yield usage.start(context.environment.chromeWindow,
+ context.environment.target, args.noreload);
+ }
+ },
+ {
+ item: "command",
+ runAt: "client",
+ name: "csscoverage stop",
+ hidden: true,
+ description: l10n.lookup("csscoverageStopDesc2"),
+ exec: function*(args, context) {
+ let target = context.environment.target;
+ let usage = yield csscoverage.getUsage(target);
+ if (usage == null) {
+ throw new Error(l10n.lookup("csscoverageNoRemoteError"));
+ }
+ yield usage.stop();
+ yield gDevTools.showToolbox(target, "styleeditor");
+ }
+ },
+ {
+ item: "command",
+ runAt: "client",
+ name: "csscoverage oneshot",
+ hidden: true,
+ description: l10n.lookup("csscoverageOneShotDesc2"),
+ exec: function*(args, context) {
+ let target = context.environment.target;
+ let usage = yield csscoverage.getUsage(target);
+ if (usage == null) {
+ throw new Error(l10n.lookup("csscoverageNoRemoteError"));
+ }
+ yield usage.oneshot();
+ yield gDevTools.showToolbox(target, "styleeditor");
+ }
+ },
+ {
+ item: "command",
+ runAt: "client",
+ name: "csscoverage toggle",
+ hidden: true,
+ description: l10n.lookup("csscoverageToggleDesc2"),
+ state: {
+ isChecked: function(target) {
+ return csscoverage.getUsage(target).then(usage => {
+ return usage.isRunning();
+ });
+ },
+ onChange: function(target, handler) {
+ csscoverage.getUsage(target).then(usage => {
+ this.handler = ev => { handler("state-change", ev); };
+ usage.on("state-change", this.handler);
+ });
+ },
+ offChange: function(target, handler) {
+ csscoverage.getUsage(target).then(usage => {
+ usage.off("state-change", this.handler);
+ this.handler = undefined;
+ });
+ },
+ },
+ exec: function*(args, context) {
+ let target = context.environment.target;
+ let usage = yield csscoverage.getUsage(target);
+ if (usage == null) {
+ throw new Error(l10n.lookup("csscoverageNoRemoteError"));
+ }
+
+ yield usage.toggle(context.environment.chromeWindow,
+ context.environment.target);
+ }
+ },
+ {
+ item: "command",
+ runAt: "client",
+ name: "csscoverage report",
+ hidden: true,
+ description: l10n.lookup("csscoverageReportDesc2"),
+ exec: function*(args, context) {
+ let usage = yield csscoverage.getUsage(context.environment.target);
+ if (usage == null) {
+ throw new Error(l10n.lookup("csscoverageNoRemoteError"));
+ }
+
+ return {
+ isTypedData: true,
+ type: "csscoveragePageReport",
+ data: yield usage.createPageReport()
+ };
+ }
+ },
+ {
+ item: "converter",
+ from: "csscoveragePageReport",
+ to: "dom",
+ exec: function*(csscoveragePageReport, context) {
+ let target = context.environment.target;
+
+ let toolbox = yield gDevTools.showToolbox(target, "styleeditor");
+ let panel = toolbox.getCurrentPanel();
+
+ let host = panel._panelDoc.querySelector(".csscoverage-report");
+ let templ = panel._panelDoc.querySelector(".csscoverage-template");
+
+ templ = templ.cloneNode(true);
+ templ.hidden = false;
+
+ let data = {
+ preload: csscoveragePageReport.preload,
+ unused: csscoveragePageReport.unused,
+ summary: csscoveragePageReport.summary,
+ onback: () => {
+ // The back button clears and hides .csscoverage-report
+ while (host.hasChildNodes()) {
+ host.removeChild(host.firstChild);
+ }
+ host.hidden = true;
+ }
+ };
+
+ let addOnClick = rule => {
+ rule.onclick = () => {
+ panel.selectStyleSheet(rule.url, rule.start.line);
+ };
+ };
+
+ data.preload.forEach(page => {
+ page.rules.forEach(addOnClick);
+ });
+ data.unused.forEach(page => {
+ page.rules.forEach(addOnClick);
+ });
+
+ let options = { allowEval: true, stack: "styleeditor.xul" };
+ domtemplate.template(templ, data, options);
+
+ while (templ.hasChildNodes()) {
+ host.appendChild(templ.firstChild);
+ }
+
+ // Create a new chart.
+ let container = host.querySelector(".csscoverage-report-chart");
+ let chart = Chart.PieTable(panel._panelDoc, {
+ diameter: 200, // px
+ title: "CSS Usage",
+ data: [
+ { size: data.summary.preload, label: "Used Preload" },
+ { size: data.summary.used, label: "Used" },
+ { size: data.summary.unused, label: "Unused" }
+ ]
+ });
+ container.appendChild(chart.node);
+
+ host.hidden = false;
+ }
+ }
+];
diff --git a/devtools/shared/gcli/commands/folder.js b/devtools/shared/gcli/commands/folder.js
new file mode 100644
index 000000000..22a51420d
--- /dev/null
+++ b/devtools/shared/gcli/commands/folder.js
@@ -0,0 +1,77 @@
+/* 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/. */
+
+"use strict";
+
+const { Cc, Ci, Cu, CC } = require("chrome");
+const Services = require("Services");
+const l10n = require("gcli/l10n");
+const dirService = Cc["@mozilla.org/file/directory_service;1"]
+ .getService(Ci.nsIProperties);
+
+function showFolder(aPath) {
+ let nsLocalFile = CC("@mozilla.org/file/local;1", "nsILocalFile",
+ "initWithPath");
+
+ try {
+ let file = new nsLocalFile(aPath);
+
+ if (file.exists()) {
+ file.reveal();
+ return l10n.lookupFormat("folderOpenDirResult", [aPath]);
+ } else {
+ return l10n.lookup("folderInvalidPath");
+ }
+ } catch (e) {
+ return l10n.lookup("folderInvalidPath");
+ }
+}
+
+exports.items = [
+ {
+ name: "folder",
+ description: l10n.lookup("folderDesc")
+ },
+ {
+ item: "command",
+ runAt: "client",
+ name: "folder open",
+ description: l10n.lookup("folderOpenDesc"),
+ params: [
+ {
+ name: "path",
+ type: { name: "string", allowBlank: true },
+ defaultValue: "~",
+ description: l10n.lookup("folderOpenDir")
+ }
+ ],
+ returnType: "string",
+ exec: function(args, context) {
+ let dirName = args.path;
+
+ // replaces ~ with the home directory path in unix and windows
+ if (dirName.indexOf("~") == 0) {
+ let homeDirFile = dirService.get("Home", Ci.nsIFile);
+ let homeDir = homeDirFile.path;
+ dirName = dirName.substr(1);
+ dirName = homeDir + dirName;
+ }
+
+ return showFolder(dirName);
+ }
+ },
+ {
+ item: "command",
+ runAt: "client",
+ name: "folder openprofile",
+ description: l10n.lookup("folderOpenProfileDesc"),
+ returnType: "string",
+ exec: function(args, context) {
+ // Get the profile directory.
+ let currProfD = Services.dirsvc.get("ProfD", Ci.nsIFile);
+ let profileDir = currProfD.path;
+ return showFolder(profileDir);
+ }
+ }
+];
diff --git a/devtools/shared/gcli/commands/highlight.js b/devtools/shared/gcli/commands/highlight.js
new file mode 100644
index 000000000..cc2353b3b
--- /dev/null
+++ b/devtools/shared/gcli/commands/highlight.js
@@ -0,0 +1,158 @@
+/* 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/. */
+
+"use strict";
+
+const l10n = require("gcli/l10n");
+require("devtools/server/actors/inspector");
+const {
+ BoxModelHighlighter,
+ HighlighterEnvironment
+} = require("devtools/server/actors/highlighters");
+
+const {PluralForm} = require("devtools/shared/plural-form");
+const {LocalizationHelper} = require("devtools/shared/l10n");
+const L10N = new LocalizationHelper("devtools/shared/locales/gclicommands.properties");
+
+// How many maximum nodes can be highlighted in parallel
+const MAX_HIGHLIGHTED_ELEMENTS = 100;
+
+// Store the environment object used to create highlighters so it can be
+// destroyed later.
+var highlighterEnv;
+
+// Stores the highlighters instances so they can be destroyed later.
+// also export them so tests can access those and assert they got created
+// correctly.
+exports.highlighters = [];
+
+/**
+ * Destroy all existing highlighters
+ */
+function unhighlightAll() {
+ for (let highlighter of exports.highlighters) {
+ highlighter.destroy();
+ }
+ exports.highlighters.length = 0;
+
+ if (highlighterEnv) {
+ highlighterEnv.destroy();
+ highlighterEnv = null;
+ }
+}
+
+exports.items = [
+ {
+ item: "command",
+ runAt: "server",
+ name: "highlight",
+ description: l10n.lookup("highlightDesc"),
+ manual: l10n.lookup("highlightManual"),
+ params: [
+ {
+ name: "selector",
+ type: "nodelist",
+ description: l10n.lookup("highlightSelectorDesc"),
+ manual: l10n.lookup("highlightSelectorManual")
+ },
+ {
+ group: l10n.lookup("highlightOptionsDesc"),
+ params: [
+ {
+ name: "hideguides",
+ type: "boolean",
+ description: l10n.lookup("highlightHideGuidesDesc"),
+ manual: l10n.lookup("highlightHideGuidesManual")
+ },
+ {
+ name: "showinfobar",
+ type: "boolean",
+ description: l10n.lookup("highlightShowInfoBarDesc"),
+ manual: l10n.lookup("highlightShowInfoBarManual")
+ },
+ {
+ name: "showall",
+ type: "boolean",
+ description: l10n.lookup("highlightShowAllDesc"),
+ manual: l10n.lookup("highlightShowAllManual")
+ },
+ {
+ name: "region",
+ type: {
+ name: "selection",
+ data: ["content", "padding", "border", "margin"]
+ },
+ description: l10n.lookup("highlightRegionDesc"),
+ manual: l10n.lookup("highlightRegionManual"),
+ defaultValue: "border"
+ },
+ {
+ name: "fill",
+ type: "string",
+ description: l10n.lookup("highlightFillDesc"),
+ manual: l10n.lookup("highlightFillManual"),
+ defaultValue: null
+ },
+ {
+ name: "keep",
+ type: "boolean",
+ description: l10n.lookup("highlightKeepDesc"),
+ manual: l10n.lookup("highlightKeepManual")
+ }
+ ]
+ }
+ ],
+ exec: function(args, context) {
+ // Remove all existing highlighters unless told otherwise
+ if (!args.keep) {
+ unhighlightAll();
+ }
+
+ let env = context.environment;
+ highlighterEnv = new HighlighterEnvironment();
+ highlighterEnv.initFromWindow(env.window);
+
+ // Unhighlight on navigate
+ highlighterEnv.once("will-navigate", unhighlightAll);
+
+ let i = 0;
+ for (let node of args.selector) {
+ if (!args.showall && i >= MAX_HIGHLIGHTED_ELEMENTS) {
+ break;
+ }
+
+ let highlighter = new BoxModelHighlighter(highlighterEnv);
+ if (args.fill) {
+ highlighter.regionFill[args.region] = args.fill;
+ }
+ highlighter.show(node, {
+ region: args.region,
+ hideInfoBar: !args.showinfobar,
+ hideGuides: args.hideguides,
+ showOnly: args.region
+ });
+ exports.highlighters.push(highlighter);
+ i++;
+ }
+
+ let highlightText = L10N.getStr("highlightOutputConfirm2");
+ let output = PluralForm.get(args.selector.length, highlightText)
+ .replace("%1$S", args.selector.length);
+ if (args.selector.length > i) {
+ output = l10n.lookupFormat("highlightOutputMaxReached",
+ ["" + args.selector.length, "" + i]);
+ }
+
+ return output;
+ }
+ },
+ {
+ item: "command",
+ runAt: "server",
+ name: "unhighlight",
+ description: l10n.lookup("unhighlightDesc"),
+ manual: l10n.lookup("unhighlightManual"),
+ exec: unhighlightAll
+ }
+];
diff --git a/devtools/shared/gcli/commands/index.js b/devtools/shared/gcli/commands/index.js
new file mode 100644
index 000000000..8fe77482e
--- /dev/null
+++ b/devtools/shared/gcli/commands/index.js
@@ -0,0 +1,179 @@
+/* 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/. */
+
+"use strict";
+
+const { createSystem, connectFront, disconnectFront } = require("gcli/system");
+const { GcliFront } = require("devtools/shared/fronts/gcli");
+
+/**
+ * This is the basic list of modules that should be loaded into each
+ * requisition instance whether server side or client side
+ */
+exports.baseModules = [
+ "gcli/types/delegate",
+ "gcli/types/selection",
+ "gcli/types/array",
+
+ "gcli/types/boolean",
+ "gcli/types/command",
+ "gcli/types/date",
+ "gcli/types/file",
+ "gcli/types/javascript",
+ "gcli/types/node",
+ "gcli/types/number",
+ "gcli/types/resource",
+ "gcli/types/setting",
+ "gcli/types/string",
+ "gcli/types/union",
+ "gcli/types/url",
+
+ "gcli/fields/fields",
+ "gcli/fields/delegate",
+ "gcli/fields/selection",
+
+ "gcli/ui/focus",
+ "gcli/ui/intro",
+
+ "gcli/converters/converters",
+ "gcli/converters/basic",
+ "gcli/converters/terminal",
+
+ "gcli/languages/command",
+ "gcli/languages/javascript",
+
+ "gcli/commands/clear",
+ "gcli/commands/context",
+ "gcli/commands/help",
+ "gcli/commands/pref",
+];
+
+/**
+ * Some commands belong to a tool (see getToolModules). This is a list of the
+ * modules that are *not* owned by a tool.
+ */
+exports.devtoolsModules = [
+ "devtools/shared/gcli/commands/addon",
+ "devtools/shared/gcli/commands/appcache",
+ "devtools/shared/gcli/commands/calllog",
+ "devtools/shared/gcli/commands/cmd",
+ "devtools/shared/gcli/commands/cookie",
+ "devtools/shared/gcli/commands/csscoverage",
+ "devtools/shared/gcli/commands/folder",
+ "devtools/shared/gcli/commands/highlight",
+ "devtools/shared/gcli/commands/inject",
+ "devtools/shared/gcli/commands/jsb",
+ "devtools/shared/gcli/commands/listen",
+ "devtools/shared/gcli/commands/mdn",
+ "devtools/shared/gcli/commands/measure",
+ "devtools/shared/gcli/commands/media",
+ "devtools/shared/gcli/commands/pagemod",
+ "devtools/shared/gcli/commands/paintflashing",
+ "devtools/shared/gcli/commands/qsa",
+ "devtools/shared/gcli/commands/restart",
+ "devtools/shared/gcli/commands/rulers",
+ "devtools/shared/gcli/commands/screenshot",
+ "devtools/shared/gcli/commands/security",
+];
+
+/**
+ * Register commands from tools with 'command: [ "some/module" ]' definitions.
+ * The map/reduce incantation squashes the array of arrays to a single array.
+ */
+try {
+ const { defaultTools } = require("devtools/client/definitions");
+ exports.devtoolsToolModules = defaultTools.map(def => def.commands || [])
+ .reduce((prev, curr) => prev.concat(curr), []);
+} catch (e) {
+ // "devtools/client/definitions" is only accessible from Firefox
+ exports.devtoolsToolModules = [];
+}
+
+/**
+ * Register commands from toolbox buttons with 'command: [ "some/module" ]'
+ * definitions. The map/reduce incantation squashes the array of arrays to a
+ * single array.
+ */
+try {
+ const { ToolboxButtons } = require("devtools/client/definitions");
+ exports.devtoolsButtonModules = ToolboxButtons.map(def => def.commands || [])
+ .reduce((prev, curr) => prev.concat(curr), []);
+} catch (e) {
+ // "devtools/client/definitions" is only accessible from Firefox
+ exports.devtoolsButtonModules = [];
+}
+
+/**
+ * Add modules to a system for use in a content process (but don't call load)
+ */
+exports.addAllItemsByModule = function(system) {
+ system.addItemsByModule(exports.baseModules, { delayedLoad: true });
+ system.addItemsByModule(exports.devtoolsModules, { delayedLoad: true });
+ system.addItemsByModule(exports.devtoolsToolModules, { delayedLoad: true });
+ system.addItemsByModule(exports.devtoolsButtonModules, { delayedLoad: true });
+
+ const { mozDirLoader } = require("devtools/shared/gcli/commands/cmd");
+ system.addItemsByModule("mozcmd", { delayedLoad: true, loader: mozDirLoader });
+};
+
+/**
+ * This is WeakMap<Target, Links> where Links is an object that looks like
+ * { refs: number, promise: Promise<System>, front: GcliFront }
+ */
+var linksForTarget = new WeakMap();
+
+/**
+ * The toolbox uses the following properties on a command to allow it to be
+ * added to the toolbox toolbar
+ */
+var customProperties = [ "buttonId", "buttonClass", "tooltipText" ];
+
+/**
+ * Create a system which connects to a GCLI in a remote target
+ * @return Promise<System> for the given target
+ */
+exports.getSystem = function(target) {
+ const existingLinks = linksForTarget.get(target);
+ if (existingLinks != null) {
+ existingLinks.refs++;
+ return existingLinks.promise;
+ }
+
+ const system = createSystem({ location: "client" });
+
+ exports.addAllItemsByModule(system);
+
+ // Load the client system
+ const links = {
+ refs: 1,
+ system,
+ promise: system.load().then(() => {
+ return GcliFront.create(target).then(front => {
+ links.front = front;
+ return connectFront(system, front, customProperties).then(() => system);
+ });
+ })
+ };
+
+ linksForTarget.set(target, links);
+ return links.promise;
+};
+
+/**
+ * Someone that called getSystem doesn't need it any more, so decrement the
+ * count of users of the system for that target, and destroy if needed
+ */
+exports.releaseSystem = function(target) {
+ const links = linksForTarget.get(target);
+ if (links == null) {
+ throw new Error("releaseSystem called for unknown target");
+ }
+
+ links.refs--;
+ if (links.refs === 0) {
+ disconnectFront(links.system, links.front);
+ links.system.destroy();
+ linksForTarget.delete(target);
+ }
+};
diff --git a/devtools/shared/gcli/commands/inject.js b/devtools/shared/gcli/commands/inject.js
new file mode 100644
index 000000000..85e995eed
--- /dev/null
+++ b/devtools/shared/gcli/commands/inject.js
@@ -0,0 +1,86 @@
+/* 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/. */
+
+"use strict";
+
+const Services = require("Services");
+const { listenOnce } = require("devtools/shared/async-utils");
+const l10n = require("gcli/l10n");
+
+exports.items = [
+ {
+ item: "command",
+ runAt: "server",
+ name: "inject",
+ description: l10n.lookup("injectDesc"),
+ manual: l10n.lookup("injectManual2"),
+ params: [{
+ name: "library",
+ type: {
+ name: "union",
+ alternatives: [
+ {
+ name: "selection",
+ lookup: [
+ {
+ name: "jQuery",
+ value: {
+ name: "jQuery",
+ src: Services.prefs.getCharPref("devtools.gcli.jquerySrc")
+ }
+ },
+ {
+ name: "lodash",
+ value: {
+ name: "lodash",
+ src: Services.prefs.getCharPref("devtools.gcli.lodashSrc")
+ }
+ },
+ {
+ name: "underscore",
+ value: {
+ name: "underscore",
+ src: Services.prefs.getCharPref("devtools.gcli.underscoreSrc")
+ }
+ }
+ ]
+ },
+ {
+ name: "url"
+ }
+ ]
+ },
+ description: l10n.lookup("injectLibraryDesc")
+ }],
+ exec: function*(args, context) {
+ let document = context.environment.document;
+ let library = args.library;
+ let name = (library.type === "selection") ?
+ library.selection.name : library.url;
+ let src = (library.type === "selection") ?
+ library.selection.src : library.url;
+
+ if (context.environment.window.location.protocol == "https:") {
+ src = src.replace(/^http:/, "https:");
+ }
+
+ try {
+ // Check if URI is valid
+ Services.io.newURI(src, null, null);
+ } catch(e) {
+ return l10n.lookupFormat("injectFailed", [name]);
+ }
+
+ let newSource = document.createElement("script");
+ newSource.setAttribute("src", src);
+
+ let loadPromise = listenOnce(newSource, "load");
+ document.head.appendChild(newSource);
+
+ yield loadPromise;
+
+ return l10n.lookupFormat("injectLoaded", [name]);
+ }
+ }
+];
diff --git a/devtools/shared/gcli/commands/jsb.js b/devtools/shared/gcli/commands/jsb.js
new file mode 100644
index 000000000..b56e079d2
--- /dev/null
+++ b/devtools/shared/gcli/commands/jsb.js
@@ -0,0 +1,134 @@
+/* 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/. */
+
+"use strict";
+
+const { Cc, Ci, Cu } = require("chrome");
+const l10n = require("gcli/l10n");
+const XMLHttpRequest = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"];
+
+loader.lazyImporter(this, "Preferences", "resource://gre/modules/Preferences.jsm");
+loader.lazyImporter(this, "ScratchpadManager", "resource://devtools/client/scratchpad/scratchpad-manager.jsm");
+
+loader.lazyRequireGetter(this, "beautify", "devtools/shared/jsbeautify/beautify");
+
+exports.items = [
+ {
+ item: "command",
+ runAt: "client",
+ name: "jsb",
+ description: l10n.lookup("jsbDesc"),
+ returnValue:"string",
+ params: [
+ {
+ name: "url",
+ type: "string",
+ description: l10n.lookup("jsbUrlDesc")
+ },
+ {
+ group: l10n.lookup("jsbOptionsDesc"),
+ params: [
+ {
+ name: "indentSize",
+ type: "number",
+ description: l10n.lookup("jsbIndentSizeDesc"),
+ manual: l10n.lookup("jsbIndentSizeManual"),
+ defaultValue: Preferences.get("devtools.editor.tabsize", 2),
+ },
+ {
+ name: "indentChar",
+ type: {
+ name: "selection",
+ lookup: [
+ { name: "space", value: " " },
+ { name: "tab", value: "\t" }
+ ]
+ },
+ description: l10n.lookup("jsbIndentCharDesc"),
+ manual: l10n.lookup("jsbIndentCharManual"),
+ defaultValue: " ",
+ },
+ {
+ name: "doNotPreserveNewlines",
+ type: "boolean",
+ description: l10n.lookup("jsbDoNotPreserveNewlinesDesc")
+ },
+ {
+ name: "preserveMaxNewlines",
+ type: "number",
+ description: l10n.lookup("jsbPreserveMaxNewlinesDesc"),
+ manual: l10n.lookup("jsbPreserveMaxNewlinesManual"),
+ defaultValue: -1
+ },
+ {
+ name: "jslintHappy",
+ type: "boolean",
+ description: l10n.lookup("jsbJslintHappyDesc"),
+ manual: l10n.lookup("jsbJslintHappyManual")
+ },
+ {
+ name: "braceStyle",
+ type: {
+ name: "selection",
+ data: ["collapse", "expand", "end-expand", "expand-strict"]
+ },
+ description: l10n.lookup("jsbBraceStyleDesc2"),
+ manual: l10n.lookup("jsbBraceStyleManual2"),
+ defaultValue: "collapse"
+ },
+ {
+ name: "noSpaceBeforeConditional",
+ type: "boolean",
+ description: l10n.lookup("jsbNoSpaceBeforeConditionalDesc")
+ },
+ {
+ name: "unescapeStrings",
+ type: "boolean",
+ description: l10n.lookup("jsbUnescapeStringsDesc"),
+ manual: l10n.lookup("jsbUnescapeStringsManual")
+ }
+ ]
+ }
+ ],
+ exec: function(args, context) {
+ let opts = {
+ indent_size: args.indentSize,
+ indent_char: args.indentChar,
+ preserve_newlines: !args.doNotPreserveNewlines,
+ max_preserve_newlines: args.preserveMaxNewlines == -1 ?
+ undefined : args.preserveMaxNewlines,
+ jslint_happy: args.jslintHappy,
+ brace_style: args.braceStyle,
+ space_before_conditional: !args.noSpaceBeforeConditional,
+ unescape_strings: args.unescapeStrings
+ };
+
+ let xhr = new XMLHttpRequest();
+
+ let deferred = context.defer();
+
+ xhr.onreadystatechange = function() {
+ if (xhr.readyState == 4) {
+ if (xhr.status == 200 || xhr.status == 0) {
+ let result = beautify.js(xhr.responseText, opts);
+
+ ScratchpadManager.openScratchpad({text: result});
+
+ deferred.resolve();
+ } else {
+ deferred.reject("Unable to load page to beautify: " + args.url + " " +
+ xhr.status + " " + xhr.statusText);
+ }
+ };
+ }
+ try {
+ xhr.open("GET", args.url, true);
+ xhr.send(null);
+ } catch(e) {
+ return l10n.lookup("jsbInvalidURL");
+ }
+ return deferred.promise;
+ }
+ }
+];
diff --git a/devtools/shared/gcli/commands/listen.js b/devtools/shared/gcli/commands/listen.js
new file mode 100644
index 000000000..7878577fb
--- /dev/null
+++ b/devtools/shared/gcli/commands/listen.js
@@ -0,0 +1,106 @@
+/* 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/. */
+
+"use strict";
+
+const { Cc, Ci } = require("chrome");
+const Services = require("Services");
+const l10n = require("gcli/l10n");
+const { XPCOMUtils } = require("resource://gre/modules/XPCOMUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "DevToolsLoader",
+ "resource://devtools/shared/Loader.jsm");
+
+const BRAND_SHORT_NAME = Cc["@mozilla.org/intl/stringbundle;1"]
+ .getService(Ci.nsIStringBundleService)
+ .createBundle("chrome://branding/locale/brand.properties")
+ .GetStringFromName("brandShortName");
+
+XPCOMUtils.defineLazyGetter(this, "debuggerServer", () => {
+ // Create a separate loader instance, so that we can be sure to receive
+ // a separate instance of the DebuggingServer from the rest of the
+ // devtools. This allows us to safely use the tools against even the
+ // actors and DebuggingServer itself, especially since we can mark
+ // serverLoader as invisible to the debugger (unlike the usual loader
+ // settings).
+ let serverLoader = new DevToolsLoader();
+ serverLoader.invisibleToDebugger = true;
+ let { DebuggerServer: debuggerServer } = serverLoader.require("devtools/server/main");
+ debuggerServer.init();
+ debuggerServer.addBrowserActors();
+ debuggerServer.allowChromeProcess = !l10n.hiddenByChromePref();
+ return debuggerServer;
+});
+
+exports.items = [
+ {
+ item: "command",
+ runAt: "client",
+ name: "listen",
+ description: l10n.lookup("listenDesc"),
+ manual: l10n.lookupFormat("listenManual2", [ BRAND_SHORT_NAME ]),
+ params: [
+ {
+ name: "port",
+ type: "number",
+ get defaultValue() {
+ return Services.prefs.getIntPref("devtools.debugger.remote-port");
+ },
+ description: l10n.lookup("listenPortDesc"),
+ },
+ {
+ name: "protocol",
+ get defaultValue() {
+ let webSocket = Services.prefs
+ .getBoolPref("devtools.debugger.remote-websocket");
+ let protocol;
+ if (webSocket === true) {
+ protocol = "websocket";
+ } else {
+ protocol = "mozilla-rdp";
+ }
+ return protocol;
+ },
+ type: {
+ name: "selection",
+ data: [ "mozilla-rdp", "websocket"],
+ },
+ description: l10n.lookup("listenProtocolDesc"),
+ },
+ ],
+ exec: function (args, context) {
+ var listener = debuggerServer.createListener();
+ if (!listener) {
+ throw new Error(l10n.lookup("listenDisabledOutput"));
+ }
+
+ let webSocket = false;
+ if (args.protocol === "websocket") {
+ webSocket = true;
+ } else if (args.protocol === "mozilla-rdp") {
+ webSocket = false;
+ }
+
+ listener.portOrPath = args.port;
+ listener.webSocket = webSocket;
+ listener.open();
+
+ if (debuggerServer.initialized) {
+ return l10n.lookupFormat("listenInitOutput", [ "" + args.port ]);
+ }
+
+ return l10n.lookup("listenNoInitOutput");
+ },
+ },
+ {
+ item: "command",
+ runAt: "client",
+ name: "unlisten",
+ description: l10n.lookup("unlistenDesc"),
+ manual: l10n.lookup("unlistenManual"),
+ exec: function (args, context) {
+ debuggerServer.closeAllListeners();
+ return l10n.lookup("unlistenOutput");
+ }
+ }
+];
diff --git a/devtools/shared/gcli/commands/mdn.js b/devtools/shared/gcli/commands/mdn.js
new file mode 100644
index 000000000..57e582e40
--- /dev/null
+++ b/devtools/shared/gcli/commands/mdn.js
@@ -0,0 +1,83 @@
+/* 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/. */
+
+"use strict";
+
+const l10n = require("gcli/l10n");
+
+var MdnDocsWidget;
+try {
+ MdnDocsWidget = require("devtools/client/shared/widgets/MdnDocsWidget");
+} catch (e) {
+ // DevTools MdnDocsWidget only available in Firefox Desktop
+}
+
+exports.items = [{
+ name: "mdn",
+ description: l10n.lookup("mdnDesc")
+}, {
+ item: "command",
+ runAt: "client",
+ name: "mdn css",
+ description: l10n.lookup("mdnCssDesc"),
+ returnType: "cssPropertyOutput",
+ params: [{
+ name: "property",
+ type: { name: "string" },
+ defaultValue: null,
+ description: l10n.lookup("mdnCssProp")
+ }],
+ exec: function(args) {
+ if (!MdnDocsWidget) {
+ return null;
+ }
+
+ return MdnDocsWidget.getCssDocs(args.property).then(result => {
+ return {
+ data: result,
+ url: MdnDocsWidget.PAGE_LINK_URL + args.property,
+ property: args.property
+ };
+ }, error => {
+ return { error, property: args.property };
+ });
+ }
+}, {
+ item: "converter",
+ from: "cssPropertyOutput",
+ to: "dom",
+ exec: function(result, context) {
+ let propertyName = result.property;
+
+ let document = context.document;
+ let root = document.createElement("div");
+
+ if (result.error) {
+ // The css property specified doesn't exist.
+ root.appendChild(document.createTextNode(
+ l10n.lookupFormat("mdnCssPropertyNotFound", [ propertyName ]) +
+ " (" + result.error + ")"));
+ } else {
+ let title = document.createElement("h2");
+ title.textContent = propertyName;
+ root.appendChild(title);
+
+ let link = document.createElement("p");
+ link.classList.add("gcli-mdn-url");
+ link.textContent = l10n.lookup("mdnCssVisitPage");
+ root.appendChild(link);
+
+ link.addEventListener("click", () => {
+ let mainWindow = context.environment.chromeWindow;
+ mainWindow.openUILinkIn(result.url, "tab");
+ });
+
+ let summary = document.createElement("p");
+ summary.textContent = result.data.summary;
+ root.appendChild(summary);
+ }
+
+ return root;
+ }
+}];
diff --git a/devtools/shared/gcli/commands/measure.js b/devtools/shared/gcli/commands/measure.js
new file mode 100644
index 000000000..7f6233a95
--- /dev/null
+++ b/devtools/shared/gcli/commands/measure.js
@@ -0,0 +1,112 @@
+/* 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/. */
+ /* globals getOuterId, getBrowserForTab */
+
+"use strict";
+
+const EventEmitter = require("devtools/shared/event-emitter");
+const eventEmitter = new EventEmitter();
+const events = require("sdk/event/core");
+
+loader.lazyRequireGetter(this, "getOuterId", "sdk/window/utils", true);
+loader.lazyRequireGetter(this, "getBrowserForTab", "sdk/tabs/utils", true);
+
+const l10n = require("gcli/l10n");
+require("devtools/server/actors/inspector");
+const { MeasuringToolHighlighter, HighlighterEnvironment } =
+ require("devtools/server/actors/highlighters");
+
+const highlighters = new WeakMap();
+const visibleHighlighters = new Set();
+
+const isCheckedFor = (tab) =>
+ tab ? visibleHighlighters.has(getBrowserForTab(tab).outerWindowID) : false;
+
+exports.items = [
+ // The client measure command is used to maintain the toolbar button state
+ // only and redirects to the server command to actually toggle the measuring
+ // tool (see `measure_server` below).
+ {
+ name: "measure",
+ runAt: "client",
+ description: l10n.lookup("measureDesc"),
+ manual: l10n.lookup("measureManual"),
+ buttonId: "command-button-measure",
+ buttonClass: "command-button command-button-invertable",
+ tooltipText: l10n.lookup("measureTooltip"),
+ state: {
+ isChecked: ({_tab}) => isCheckedFor(_tab),
+ onChange: (target, handler) => eventEmitter.on("changed", handler),
+ offChange: (target, handler) => eventEmitter.off("changed", handler)
+ },
+ exec: function*(args, context) {
+ let { target } = context.environment;
+
+ // Pipe the call to the server command.
+ let response = yield context.updateExec("measure_server");
+ let { visible, id } = response.data;
+
+ if (visible) {
+ visibleHighlighters.add(id);
+ } else {
+ visibleHighlighters.delete(id);
+ }
+
+ eventEmitter.emit("changed", { target });
+
+ // Toggle off the button when the page navigates because the measuring
+ // tool is removed automatically by the MeasuringToolHighlighter on the
+ // server then.
+ let onNavigate = () => {
+ visibleHighlighters.delete(id);
+ eventEmitter.emit("changed", { target });
+ };
+ target.off("will-navigate", onNavigate);
+ target.once("will-navigate", onNavigate);
+ }
+ },
+ // The server measure command is hidden by default, it's just used by the
+ // client command.
+ {
+ name: "measure_server",
+ runAt: "server",
+ hidden: true,
+ returnType: "highlighterVisibility",
+ exec: function(args, context) {
+ let env = context.environment;
+ let { document } = env;
+ let id = getOuterId(env.window);
+
+ // Calling the command again after the measuring tool has been shown once,
+ // hides it.
+ if (highlighters.has(document)) {
+ let { highlighter } = highlighters.get(document);
+ highlighter.destroy();
+ return {visible: false, id};
+ }
+
+ // Otherwise, display the measuring tool.
+ let environment = new HighlighterEnvironment();
+ environment.initFromWindow(env.window);
+ let highlighter = new MeasuringToolHighlighter(environment);
+
+ // Store the instance of the measuring tool highlighter for this document
+ // so we can hide it later.
+ highlighters.set(document, { highlighter, environment });
+
+ // Listen to the highlighter's destroy event which may happen if the
+ // window is refreshed or closed with the measuring tool shown.
+ events.once(highlighter, "destroy", () => {
+ if (highlighters.has(document)) {
+ let { environment } = highlighters.get(document);
+ environment.destroy();
+ highlighters.delete(document);
+ }
+ });
+
+ highlighter.show();
+ return {visible: true, id};
+ }
+ }
+];
diff --git a/devtools/shared/gcli/commands/media.js b/devtools/shared/gcli/commands/media.js
new file mode 100644
index 000000000..908e9eb5e
--- /dev/null
+++ b/devtools/shared/gcli/commands/media.js
@@ -0,0 +1,56 @@
+/* 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/. */
+
+"use strict";
+
+const {Ci} = require("chrome");
+const l10n = require("gcli/l10n");
+
+function getContentViewer(context) {
+ let {window} = context.environment;
+ return window.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDocShell)
+ .contentViewer;
+}
+
+exports.items = [
+ {
+ name: "media",
+ description: l10n.lookup("mediaDesc")
+ },
+ {
+ item: "command",
+ runAt: "server",
+ name: "media emulate",
+ description: l10n.lookup("mediaEmulateDesc"),
+ manual: l10n.lookup("mediaEmulateManual"),
+ params: [
+ {
+ name: "type",
+ description: l10n.lookup("mediaEmulateType"),
+ type: {
+ name: "selection",
+ data: [
+ "braille", "embossed", "handheld", "print", "projection",
+ "screen", "speech", "tty", "tv"
+ ]
+ }
+ }
+ ],
+ exec: function(args, context) {
+ let contentViewer = getContentViewer(context);
+ contentViewer.emulateMedium(args.type);
+ }
+ },
+ {
+ item: "command",
+ runAt: "server",
+ name: "media reset",
+ description: l10n.lookup("mediaResetDesc"),
+ exec: function(args, context) {
+ let contentViewer = getContentViewer(context);
+ contentViewer.stopEmulatingMedium();
+ }
+ }
+];
diff --git a/devtools/shared/gcli/commands/moz.build b/devtools/shared/gcli/commands/moz.build
new file mode 100644
index 000000000..e5f3fe91c
--- /dev/null
+++ b/devtools/shared/gcli/commands/moz.build
@@ -0,0 +1,30 @@
+# -*- 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(
+ 'addon.js',
+ 'appcache.js',
+ 'calllog.js',
+ 'cmd.js',
+ 'cookie.js',
+ 'csscoverage.js',
+ 'folder.js',
+ 'highlight.js',
+ 'index.js',
+ 'inject.js',
+ 'jsb.js',
+ 'listen.js',
+ 'mdn.js',
+ 'measure.js',
+ 'media.js',
+ 'pagemod.js',
+ 'paintflashing.js',
+ 'qsa.js',
+ 'restart.js',
+ 'rulers.js',
+ 'screenshot.js',
+ 'security.js',
+)
diff --git a/devtools/shared/gcli/commands/pagemod.js b/devtools/shared/gcli/commands/pagemod.js
new file mode 100644
index 000000000..184ab1ea3
--- /dev/null
+++ b/devtools/shared/gcli/commands/pagemod.js
@@ -0,0 +1,276 @@
+/* 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/. */
+
+"use strict";
+
+const { Cc, Ci, Cu } = require("chrome");
+const l10n = require("gcli/l10n");
+
+exports.items = [
+ {
+ name: "pagemod",
+ description: l10n.lookup("pagemodDesc"),
+ },
+ {
+ item: "command",
+ runAt: "server",
+ name: "pagemod replace",
+ description: l10n.lookup("pagemodReplaceDesc"),
+ params: [
+ {
+ name: "search",
+ type: "string",
+ description: l10n.lookup("pagemodReplaceSearchDesc"),
+ },
+ {
+ name: "replace",
+ type: "string",
+ description: l10n.lookup("pagemodReplaceReplaceDesc"),
+ },
+ {
+ name: "ignoreCase",
+ type: "boolean",
+ description: l10n.lookup("pagemodReplaceIgnoreCaseDesc"),
+ },
+ {
+ name: "selector",
+ type: "string",
+ description: l10n.lookup("pagemodReplaceSelectorDesc"),
+ defaultValue: "*:not(script):not(style):not(embed):not(object):not(frame):not(iframe):not(frameset)",
+ },
+ {
+ name: "root",
+ type: "node",
+ description: l10n.lookup("pagemodReplaceRootDesc"),
+ defaultValue: null,
+ },
+ {
+ name: "attrOnly",
+ type: "boolean",
+ description: l10n.lookup("pagemodReplaceAttrOnlyDesc"),
+ },
+ {
+ name: "contentOnly",
+ type: "boolean",
+ description: l10n.lookup("pagemodReplaceContentOnlyDesc"),
+ },
+ {
+ name: "attributes",
+ type: "string",
+ description: l10n.lookup("pagemodReplaceAttributesDesc"),
+ defaultValue: null,
+ },
+ ],
+ // Make a given string safe to use in a regular expression.
+ escapeRegex: function(aString) {
+ return aString.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&");
+ },
+ exec: function(args, context) {
+ let searchTextNodes = !args.attrOnly;
+ let searchAttributes = !args.contentOnly;
+ let regexOptions = args.ignoreCase ? "ig" : "g";
+ let search = new RegExp(this.escapeRegex(args.search), regexOptions);
+ let attributeRegex = null;
+ if (args.attributes) {
+ attributeRegex = new RegExp(args.attributes, regexOptions);
+ }
+
+ let root = args.root || context.environment.document;
+ let elements = root.querySelectorAll(args.selector);
+ elements = Array.prototype.slice.call(elements);
+
+ let replacedTextNodes = 0;
+ let replacedAttributes = 0;
+
+ function replaceAttribute() {
+ replacedAttributes++;
+ return args.replace;
+ }
+ function replaceTextNode() {
+ replacedTextNodes++;
+ return args.replace;
+ }
+
+ for (let i = 0; i < elements.length; i++) {
+ let element = elements[i];
+ if (searchTextNodes) {
+ for (let y = 0; y < element.childNodes.length; y++) {
+ let node = element.childNodes[y];
+ if (node.nodeType == node.TEXT_NODE) {
+ node.textContent = node.textContent.replace(search, replaceTextNode);
+ }
+ }
+ }
+
+ if (searchAttributes) {
+ if (!element.attributes) {
+ continue;
+ }
+ for (let y = 0; y < element.attributes.length; y++) {
+ let attr = element.attributes[y];
+ if (!attributeRegex || attributeRegex.test(attr.name)) {
+ attr.value = attr.value.replace(search, replaceAttribute);
+ }
+ }
+ }
+ }
+
+ return l10n.lookupFormat("pagemodReplaceResult",
+ [elements.length, replacedTextNodes,
+ replacedAttributes]);
+ }
+ },
+ {
+ name: "pagemod remove",
+ description: l10n.lookup("pagemodRemoveDesc"),
+ },
+ {
+ item: "command",
+ runAt: "server",
+ name: "pagemod remove element",
+ description: l10n.lookup("pagemodRemoveElementDesc"),
+ params: [
+ {
+ name: "search",
+ type: "string",
+ description: l10n.lookup("pagemodRemoveElementSearchDesc"),
+ },
+ {
+ name: "root",
+ type: "node",
+ description: l10n.lookup("pagemodRemoveElementRootDesc"),
+ defaultValue: null,
+ },
+ {
+ name: "stripOnly",
+ type: "boolean",
+ description: l10n.lookup("pagemodRemoveElementStripOnlyDesc"),
+ },
+ {
+ name: "ifEmptyOnly",
+ type: "boolean",
+ description: l10n.lookup("pagemodRemoveElementIfEmptyOnlyDesc"),
+ },
+ ],
+ exec: function(args, context) {
+ let root = args.root || context.environment.document;
+ let elements = Array.prototype.slice.call(root.querySelectorAll(args.search));
+
+ let removed = 0;
+ for (let i = 0; i < elements.length; i++) {
+ let element = elements[i];
+ let parentNode = element.parentNode;
+ if (!parentNode || !element.removeChild) {
+ continue;
+ }
+ if (args.stripOnly) {
+ while (element.hasChildNodes()) {
+ parentNode.insertBefore(element.childNodes[0], element);
+ }
+ }
+ if (!args.ifEmptyOnly || !element.hasChildNodes()) {
+ element.parentNode.removeChild(element);
+ removed++;
+ }
+ }
+
+ return l10n.lookupFormat("pagemodRemoveElementResultMatchedAndRemovedElements",
+ [elements.length, removed]);
+ }
+ },
+ {
+ item: "command",
+ runAt: "server",
+ name: "pagemod remove attribute",
+ description: l10n.lookup("pagemodRemoveAttributeDesc"),
+ params: [
+ {
+ name: "searchAttributes",
+ type: "string",
+ description: l10n.lookup("pagemodRemoveAttributeSearchAttributesDesc"),
+ },
+ {
+ name: "searchElements",
+ type: "string",
+ description: l10n.lookup("pagemodRemoveAttributeSearchElementsDesc"),
+ },
+ {
+ name: "root",
+ type: "node",
+ description: l10n.lookup("pagemodRemoveAttributeRootDesc"),
+ defaultValue: null,
+ },
+ {
+ name: "ignoreCase",
+ type: "boolean",
+ description: l10n.lookup("pagemodRemoveAttributeIgnoreCaseDesc"),
+ },
+ ],
+ exec: function(args, context) {
+ let root = args.root || context.environment.document;
+ let regexOptions = args.ignoreCase ? "ig" : "g";
+ let attributeRegex = new RegExp(args.searchAttributes, regexOptions);
+ let elements = root.querySelectorAll(args.searchElements);
+ elements = Array.prototype.slice.call(elements);
+
+ let removed = 0;
+ for (let i = 0; i < elements.length; i++) {
+ let element = elements[i];
+ if (!element.attributes) {
+ continue;
+ }
+
+ var attrs = Array.prototype.slice.call(element.attributes);
+ for (let y = 0; y < attrs.length; y++) {
+ let attr = attrs[y];
+ if (attributeRegex.test(attr.name)) {
+ element.removeAttribute(attr.name);
+ removed++;
+ }
+ }
+ }
+
+ return l10n.lookupFormat("pagemodRemoveAttributeResult",
+ [elements.length, removed]);
+ }
+ },
+ // This command allows the user to export the page to HTML after DOM changes
+ {
+ name: "export",
+ description: l10n.lookup("exportDesc"),
+ },
+ {
+ item: "command",
+ runAt: "server",
+ name: "export html",
+ description: l10n.lookup("exportHtmlDesc"),
+ params: [
+ {
+ name: "destination",
+ type: {
+ name: "selection",
+ data: [ "window", "stdout", "clipboard" ]
+ },
+ defaultValue: "window"
+ }
+ ],
+ exec: function(args, context) {
+ let html = context.environment.document.documentElement.outerHTML;
+ if (args.destination === "stdout") {
+ return html;
+ }
+
+ if (args.desination === "clipboard") {
+ let clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"]
+ .getService(Ci.nsIClipboardHelper);
+ clipboard.copyString(url);
+ return '';
+ }
+
+ let url = "data:text/plain;charset=utf8," + encodeURIComponent(html);
+ context.environment.window.open(url);
+ return '';
+ }
+ }
+];
diff --git a/devtools/shared/gcli/commands/paintflashing.js b/devtools/shared/gcli/commands/paintflashing.js
new file mode 100644
index 000000000..7e21911a7
--- /dev/null
+++ b/devtools/shared/gcli/commands/paintflashing.js
@@ -0,0 +1,201 @@
+/* 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/. */
+
+"use strict";
+
+const { Ci } = require("chrome");
+loader.lazyRequireGetter(this, "getOuterId", "sdk/window/utils", true);
+loader.lazyRequireGetter(this, "getBrowserForTab", "sdk/tabs/utils", true);
+
+var telemetry;
+try {
+ const Telemetry = require("devtools/client/shared/telemetry");
+ telemetry = new Telemetry();
+} catch(e) {
+ // DevTools Telemetry module only available in Firefox
+}
+
+const EventEmitter = require("devtools/shared/event-emitter");
+const eventEmitter = new EventEmitter();
+
+const gcli = require("gcli/index");
+const l10n = require("gcli/l10n");
+
+const enabledPaintFlashing = new Set();
+
+const isCheckedFor = (tab) =>
+ tab ? enabledPaintFlashing.has(getBrowserForTab(tab).outerWindowID) : false;
+
+/**
+ * Fire events and telemetry when paintFlashing happens
+ */
+function onPaintFlashingChanged(target, state) {
+ const { flashing, id } = state;
+
+ if (flashing) {
+ enabledPaintFlashing.add(id);
+ } else {
+ enabledPaintFlashing.delete(id);
+ }
+
+ eventEmitter.emit("changed", { target: target });
+ function fireChange() {
+ eventEmitter.emit("changed", { target: target });
+ }
+
+ target.off("navigate", fireChange);
+ target.once("navigate", fireChange);
+
+ if (!telemetry) {
+ return;
+ }
+ if (flashing) {
+ telemetry.toolOpened("paintflashing");
+ } else {
+ telemetry.toolClosed("paintflashing");
+ }
+}
+
+/**
+ * Alter the paintFlashing state of a window and report on the new value.
+ * This works with chrome or content windows.
+ *
+ * This is a bizarre method that you could argue should be broken up into
+ * separate getter and setter functions, however keeping it as one helps
+ * to simplify the commands below.
+ *
+ * @param state {string} One of:
+ * - "on" which does window.paintFlashing = true
+ * - "off" which does window.paintFlashing = false
+ * - "toggle" which does window.paintFlashing = !window.paintFlashing
+ * - "query" which does nothing
+ * @return The new value of the window.paintFlashing flag
+ */
+function setPaintFlashing(window, state) {
+ const winUtils = window.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindowUtils);
+
+ if (!["on", "off", "toggle", "query"].includes(state)) {
+ throw new Error(`Unsupported state: ${state}`);
+ }
+
+ if (state === "on") {
+ winUtils.paintFlashing = true;
+ } else if (state === "off") {
+ winUtils.paintFlashing = false;
+ } else if (state === "toggle") {
+ winUtils.paintFlashing = !winUtils.paintFlashing;
+ }
+
+ return winUtils.paintFlashing;
+}
+
+exports.items = [
+ {
+ name: "paintflashing",
+ description: l10n.lookup("paintflashingDesc")
+ },
+ {
+ item: "command",
+ runAt: "client",
+ name: "paintflashing on",
+ description: l10n.lookup("paintflashingOnDesc"),
+ manual: l10n.lookup("paintflashingManual"),
+ params: [{
+ group: "options",
+ params: [
+ {
+ type: "boolean",
+ name: "chrome",
+ get hidden() {
+ return gcli.hiddenByChromePref();
+ },
+ description: l10n.lookup("paintflashingChromeDesc"),
+ }
+ ]
+ }],
+ exec: function*(args, context) {
+ if (!args.chrome) {
+ const output = yield context.updateExec("paintflashing_server --state on");
+
+ onPaintFlashingChanged(context.environment.target, output.data);
+ } else {
+ setPaintFlashing(context.environment.chromeWindow, "on");
+ }
+ }
+ },
+ {
+ item: "command",
+ runAt: "client",
+ name: "paintflashing off",
+ description: l10n.lookup("paintflashingOffDesc"),
+ manual: l10n.lookup("paintflashingManual"),
+ params: [{
+ group: "options",
+ params: [
+ {
+ type: "boolean",
+ name: "chrome",
+ get hidden() {
+ return gcli.hiddenByChromePref();
+ },
+ description: l10n.lookup("paintflashingChromeDesc"),
+ }
+ ]
+ }],
+ exec: function*(args, context) {
+ if (!args.chrome) {
+ const output = yield context.updateExec("paintflashing_server --state off");
+
+ onPaintFlashingChanged(context.environment.target, output.data);
+ } else {
+ setPaintFlashing(context.environment.chromeWindow, "off");
+ }
+ }
+ },
+ {
+ item: "command",
+ runAt: "client",
+ name: "paintflashing toggle",
+ hidden: true,
+ buttonId: "command-button-paintflashing",
+ buttonClass: "command-button command-button-invertable",
+ state: {
+ isChecked: ({_tab}) => isCheckedFor(_tab),
+ onChange: (_, handler) => eventEmitter.on("changed", handler),
+ offChange: (_, handler) => eventEmitter.off("changed", handler),
+ },
+ tooltipText: l10n.lookup("paintflashingTooltip"),
+ description: l10n.lookup("paintflashingToggleDesc"),
+ manual: l10n.lookup("paintflashingManual"),
+ exec: function*(args, context) {
+ const output = yield context.updateExec("paintflashing_server --state toggle");
+
+ onPaintFlashingChanged(context.environment.target, output.data);
+ }
+ },
+ {
+ item: "command",
+ runAt: "server",
+ name: "paintflashing_server",
+ hidden: true,
+ params: [
+ {
+ name: "state",
+ type: {
+ name: "selection",
+ data: [ "on", "off", "toggle", "query" ]
+ }
+ },
+ ],
+ returnType: "paintFlashingState",
+ exec: function(args, context) {
+ let { window } = context.environment;
+ let id = getOuterId(window);
+ let flashing = setPaintFlashing(window, args.state);
+
+ return { flashing, id };
+ }
+ }
+];
diff --git a/devtools/shared/gcli/commands/qsa.js b/devtools/shared/gcli/commands/qsa.js
new file mode 100644
index 000000000..939991f18
--- /dev/null
+++ b/devtools/shared/gcli/commands/qsa.js
@@ -0,0 +1,24 @@
+/* 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/. */
+
+"use strict";
+
+const l10n = require("gcli/l10n");
+
+exports.items = [
+ {
+ item: "command",
+ runAt: "server",
+ name: "qsa",
+ description: l10n.lookup("qsaDesc"),
+ params: [{
+ name: "query",
+ type: "nodelist",
+ description: l10n.lookup("qsaQueryDesc")
+ }],
+ exec: function(args, context) {
+ return args.query.length;
+ }
+ }
+];
diff --git a/devtools/shared/gcli/commands/restart.js b/devtools/shared/gcli/commands/restart.js
new file mode 100644
index 000000000..cf0e688d3
--- /dev/null
+++ b/devtools/shared/gcli/commands/restart.js
@@ -0,0 +1,77 @@
+/* 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/. */
+
+"use strict";
+
+const { Cc, Ci, Cu } = require("chrome");
+const l10n = require("gcli/l10n");
+const Services = require("Services");
+
+const BRAND_SHORT_NAME = Cc["@mozilla.org/intl/stringbundle;1"]
+ .getService(Ci.nsIStringBundleService)
+ .createBundle("chrome://branding/locale/brand.properties")
+ .GetStringFromName("brandShortName");
+
+/**
+ * Restart command
+ *
+ * @param boolean nocache
+ * Disables loading content from cache upon restart.
+ *
+ * Examples :
+ * >> restart
+ * - restarts browser immediately
+ * >> restart --nocache
+ * - restarts immediately and starts Firefox without using cache
+ */
+exports.items = [
+ {
+ item: "command",
+ runAt: "client",
+ name: "restart",
+ description: l10n.lookupFormat("restartBrowserDesc", [ BRAND_SHORT_NAME ]),
+ params: [{
+ group: l10n.lookup("restartBrowserGroupOptions"),
+ params: [
+ {
+ name: "nocache",
+ type: "boolean",
+ description: l10n.lookup("restartBrowserNocacheDesc")
+ },
+ {
+ name: "safemode",
+ type: "boolean",
+ description: l10n.lookup("restartBrowserSafemodeDesc")
+ }
+ ]
+ }],
+ returnType: "string",
+ exec: function Restart(args, context) {
+ let canceled = Cc["@mozilla.org/supports-PRBool;1"]
+ .createInstance(Ci.nsISupportsPRBool);
+ Services.obs.notifyObservers(canceled, "quit-application-requested", "restart");
+ if (canceled.data) {
+ return l10n.lookup("restartBrowserRequestCancelled");
+ }
+
+ // disable loading content from cache.
+ if (args.nocache) {
+ Services.appinfo.invalidateCachesOnRestart();
+ }
+
+ const appStartup = Cc["@mozilla.org/toolkit/app-startup;1"]
+ .getService(Ci.nsIAppStartup);
+
+ if (args.safemode) {
+ // restart in safemode
+ appStartup.restartInSafeMode(Ci.nsIAppStartup.eAttemptQuit);
+ } else {
+ // restart normally
+ appStartup.quit(Ci.nsIAppStartup.eAttemptQuit | Ci.nsIAppStartup.eRestart);
+ }
+
+ return l10n.lookupFormat("restartBrowserRestarting", [ BRAND_SHORT_NAME ]);
+ }
+ }
+];
diff --git a/devtools/shared/gcli/commands/rulers.js b/devtools/shared/gcli/commands/rulers.js
new file mode 100644
index 000000000..121e975bc
--- /dev/null
+++ b/devtools/shared/gcli/commands/rulers.js
@@ -0,0 +1,110 @@
+/* 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/. */
+/* globals getBrowserForTab */
+
+"use strict";
+
+const EventEmitter = require("devtools/shared/event-emitter");
+const eventEmitter = new EventEmitter();
+const events = require("sdk/event/core");
+loader.lazyRequireGetter(this, "getOuterId", "sdk/window/utils", true);
+loader.lazyRequireGetter(this, "getBrowserForTab", "sdk/tabs/utils", true);
+
+const l10n = require("gcli/l10n");
+require("devtools/server/actors/inspector");
+const { RulersHighlighter, HighlighterEnvironment } =
+ require("devtools/server/actors/highlighters");
+
+const highlighters = new WeakMap();
+const visibleHighlighters = new Set();
+
+const isCheckedFor = (tab) =>
+ tab ? visibleHighlighters.has(getBrowserForTab(tab).outerWindowID) : false;
+
+exports.items = [
+ // The client rulers command is used to maintain the toolbar button state only
+ // and redirects to the server command to actually toggle the rulers (see
+ // rulers_server below).
+ {
+ name: "rulers",
+ runAt: "client",
+ description: l10n.lookup("rulersDesc"),
+ manual: l10n.lookup("rulersManual"),
+ buttonId: "command-button-rulers",
+ buttonClass: "command-button command-button-invertable",
+ tooltipText: l10n.lookup("rulersTooltip"),
+ state: {
+ isChecked: ({_tab}) => isCheckedFor(_tab),
+ onChange: (target, handler) => eventEmitter.on("changed", handler),
+ offChange: (target, handler) => eventEmitter.off("changed", handler)
+ },
+ exec: function*(args, context) {
+ let { target } = context.environment;
+
+ // Pipe the call to the server command.
+ let response = yield context.updateExec("rulers_server");
+ let { visible, id } = response.data;
+
+ if (visible) {
+ visibleHighlighters.add(id);
+ } else {
+ visibleHighlighters.delete(id);
+ }
+
+ eventEmitter.emit("changed", { target });
+
+ // Toggle off the button when the page navigates because the rulers are
+ // removed automatically by the RulersHighlighter on the server then.
+ let onNavigate = () => {
+ visibleHighlighters.delete(id);
+ eventEmitter.emit("changed", { target });
+ };
+ target.off("will-navigate", onNavigate);
+ target.once("will-navigate", onNavigate);
+ }
+ },
+ // The server rulers command is hidden by default, it's just used by the
+ // client command.
+ {
+ name: "rulers_server",
+ runAt: "server",
+ hidden: true,
+ returnType: "highlighterVisibility",
+ exec: function(args, context) {
+ let env = context.environment;
+ let { document } = env;
+ let id = getOuterId(env.window);
+
+ // Calling the command again after the rulers have been shown once hides
+ // them.
+ if (highlighters.has(document)) {
+ let { highlighter } = highlighters.get(document);
+ highlighter.destroy();
+ return {visible: false, id};
+ }
+
+ // Otherwise, display the rulers.
+ let environment = new HighlighterEnvironment();
+ environment.initFromWindow(env.window);
+ let highlighter = new RulersHighlighter(environment);
+
+ // Store the instance of the rulers highlighter for this document so we
+ // can hide it later.
+ highlighters.set(document, { highlighter, environment });
+
+ // Listen to the highlighter's destroy event which may happen if the
+ // window is refreshed or closed with the rulers shown.
+ events.once(highlighter, "destroy", () => {
+ if (highlighters.has(document)) {
+ let { environment } = highlighters.get(document);
+ environment.destroy();
+ highlighters.delete(document);
+ }
+ });
+
+ highlighter.show();
+ return {visible: true, id};
+ }
+ }
+];
diff --git a/devtools/shared/gcli/commands/screenshot.js b/devtools/shared/gcli/commands/screenshot.js
new file mode 100644
index 000000000..e2f38b6d9
--- /dev/null
+++ b/devtools/shared/gcli/commands/screenshot.js
@@ -0,0 +1,579 @@
+/* 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/. */
+
+"use strict";
+
+const { Cc, Ci, Cr, Cu } = require("chrome");
+const l10n = require("gcli/l10n");
+const Services = require("Services");
+const { NetUtil } = require("resource://gre/modules/NetUtil.jsm");
+const { getRect } = require("devtools/shared/layout/utils");
+const promise = require("promise");
+const defer = require("devtools/shared/defer");
+const { Task } = require("devtools/shared/task");
+
+loader.lazyImporter(this, "Downloads", "resource://gre/modules/Downloads.jsm");
+loader.lazyImporter(this, "OS", "resource://gre/modules/osfile.jsm");
+loader.lazyImporter(this, "FileUtils", "resource://gre/modules/FileUtils.jsm");
+loader.lazyImporter(this, "PrivateBrowsingUtils",
+ "resource://gre/modules/PrivateBrowsingUtils.jsm");
+
+const BRAND_SHORT_NAME = Cc["@mozilla.org/intl/stringbundle;1"]
+ .getService(Ci.nsIStringBundleService)
+ .createBundle("chrome://branding/locale/brand.properties")
+ .GetStringFromName("brandShortName");
+
+// String used as an indication to generate default file name in the following
+// format: "Screen Shot yyyy-mm-dd at HH.MM.SS.png"
+const FILENAME_DEFAULT_VALUE = " ";
+
+/*
+ * There are 2 commands and 1 converter here. The 2 commands are nearly
+ * identical except that one runs on the client and one in the server.
+ *
+ * The server command is hidden, and is designed to be called from the client
+ * command.
+ */
+
+/**
+ * Both commands have the same initial filename parameter
+ */
+const filenameParam = {
+ name: "filename",
+ type: {
+ name: "file",
+ filetype: "file",
+ existing: "maybe",
+ },
+ defaultValue: FILENAME_DEFAULT_VALUE,
+ description: l10n.lookup("screenshotFilenameDesc"),
+ manual: l10n.lookup("screenshotFilenameManual")
+};
+
+/**
+ * Both commands have the same set of standard optional parameters
+ */
+const standardParams = {
+ group: l10n.lookup("screenshotGroupOptions"),
+ params: [
+ {
+ name: "clipboard",
+ type: "boolean",
+ description: l10n.lookup("screenshotClipboardDesc"),
+ manual: l10n.lookup("screenshotClipboardManual")
+ },
+ {
+ name: "imgur",
+ type: "boolean",
+ description: l10n.lookup("screenshotImgurDesc"),
+ manual: l10n.lookup("screenshotImgurManual")
+ },
+ {
+ name: "delay",
+ type: { name: "number", min: 0 },
+ defaultValue: 0,
+ description: l10n.lookup("screenshotDelayDesc"),
+ manual: l10n.lookup("screenshotDelayManual")
+ },
+ {
+ name: "dpr",
+ type: { name: "number", min: 0, allowFloat: true },
+ defaultValue: 0,
+ description: l10n.lookup("screenshotDPRDesc"),
+ manual: l10n.lookup("screenshotDPRManual")
+ },
+ {
+ name: "fullpage",
+ type: "boolean",
+ description: l10n.lookup("screenshotFullPageDesc"),
+ manual: l10n.lookup("screenshotFullPageManual")
+ },
+ {
+ name: "selector",
+ type: "node",
+ defaultValue: null,
+ description: l10n.lookup("inspectNodeDesc"),
+ manual: l10n.lookup("inspectNodeManual")
+ }
+ ]
+};
+
+exports.items = [
+ {
+ /**
+ * Format an 'imageSummary' (as output by the screenshot command).
+ * An 'imageSummary' is a simple JSON object that looks like this:
+ *
+ * {
+ * destinations: [ "..." ], // Required array of descriptions of the
+ * // locations of the result image (the command
+ * // can have multiple outputs)
+ * data: "...", // Optional Base64 encoded image data
+ * width:1024, height:768, // Dimensions of the image data, required
+ * // if data != null
+ * filename: "...", // If set, clicking the image will open the
+ * // folder containing the given file
+ * href: "...", // If set, clicking the image will open the
+ * // link in a new tab
+ * }
+ */
+ item: "converter",
+ from: "imageSummary",
+ to: "dom",
+ exec: function(imageSummary, context) {
+ const document = context.document;
+ const root = document.createElement("div");
+
+ // Add a line to the result for each destination
+ imageSummary.destinations.forEach(destination => {
+ const title = document.createElement("div");
+ title.textContent = destination;
+ root.appendChild(title);
+ });
+
+ // Add the thumbnail image
+ if (imageSummary.data != null) {
+ const image = context.document.createElement("div");
+ const previewHeight = parseInt(256 * imageSummary.height / imageSummary.width);
+ const style = "" +
+ "width: 256px;" +
+ "height: " + previewHeight + "px;" +
+ "max-height: 256px;" +
+ "background-image: url('" + imageSummary.data + "');" +
+ "background-size: 256px " + previewHeight + "px;" +
+ "margin: 4px;" +
+ "display: block;";
+ image.setAttribute("style", style);
+ root.appendChild(image);
+ }
+
+ // Click handler
+ if (imageSummary.href || imageSummary.filename) {
+ root.style.cursor = "pointer";
+ root.addEventListener("click", () => {
+ if (imageSummary.href) {
+ let mainWindow = context.environment.chromeWindow;
+ mainWindow.openUILinkIn(imageSummary.href, "tab");
+ } else if (imageSummary.filename) {
+ const file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsILocalFile);
+ file.initWithPath(imageSummary.filename);
+ file.reveal();
+ }
+ });
+ }
+
+ return root;
+ }
+ },
+ {
+ item: "command",
+ runAt: "client",
+ name: "screenshot",
+ description: l10n.lookup("screenshotDesc"),
+ manual: l10n.lookup("screenshotManual"),
+ returnType: "imageSummary",
+ buttonId: "command-button-screenshot",
+ buttonClass: "command-button command-button-invertable",
+ tooltipText: l10n.lookup("screenshotTooltipPage"),
+ params: [
+ filenameParam,
+ standardParams,
+ ],
+ exec: function (args, context) {
+ // Re-execute the command on the server
+ const command = context.typed.replace(/^screenshot/, "screenshot_server");
+ let capture = context.updateExec(command).then(output => {
+ return output.error ? Promise.reject(output.data) : output.data;
+ });
+
+ simulateCameraEffect(context.environment.chromeDocument, "shutter");
+ return capture.then(saveScreenshot.bind(null, args, context));
+ },
+ },
+ {
+ item: "command",
+ runAt: "server",
+ name: "screenshot_server",
+ hidden: true,
+ returnType: "imageSummary",
+ params: [ filenameParam, standardParams ],
+ exec: function (args, context) {
+ return captureScreenshot(args, context.environment.document);
+ },
+ }
+];
+
+/**
+ * This function is called to simulate camera effects
+ */
+function simulateCameraEffect(document, effect) {
+ let window = document.defaultView;
+ if (effect === "shutter") {
+ const audioCamera = new window.Audio("resource://devtools/client/themes/audio/shutter.wav");
+ audioCamera.play();
+ }
+ if (effect == "flash") {
+ const frames = Cu.cloneInto({ opacity: [ 0, 1 ] }, window);
+ document.documentElement.animate(frames, 500);
+ }
+}
+
+/**
+ * This function simply handles the --delay argument before calling
+ * createScreenshotData
+ */
+function captureScreenshot(args, document) {
+ if (args.delay > 0) {
+ return new Promise((resolve, reject) => {
+ document.defaultView.setTimeout(() => {
+ createScreenshotData(document, args).then(resolve, reject);
+ }, args.delay * 1000);
+ });
+ }
+ else {
+ return createScreenshotData(document, args);
+ }
+}
+
+/**
+ * There are several possible destinations for the screenshot, SKIP is used
+ * in saveScreenshot() whenever one of them is not used
+ */
+const SKIP = Promise.resolve();
+
+/**
+ * Save the captured screenshot to one of several destinations.
+ */
+function saveScreenshot(args, context, reply) {
+ const fileNeeded = args.filename != FILENAME_DEFAULT_VALUE ||
+ (!args.imgur && !args.clipboard);
+
+ return Promise.all([
+ args.clipboard ? saveToClipboard(context, reply) : SKIP,
+ args.imgur ? uploadToImgur(reply) : SKIP,
+ fileNeeded ? saveToFile(context, reply) : SKIP,
+ ]).then(() => reply);
+}
+
+/**
+ * This does the dirty work of creating a base64 string out of an
+ * area of the browser window
+ */
+function createScreenshotData(document, args) {
+ const window = document.defaultView;
+ let left = 0;
+ let top = 0;
+ let width;
+ let height;
+ const currentX = window.scrollX;
+ const currentY = window.scrollY;
+
+ let filename = getFilename(args.filename);
+
+ if (args.fullpage) {
+ // Bug 961832: GCLI screenshot shows fixed position element in wrong
+ // position if we don't scroll to top
+ window.scrollTo(0,0);
+ width = window.innerWidth + window.scrollMaxX - window.scrollMinX;
+ height = window.innerHeight + window.scrollMaxY - window.scrollMinY;
+ filename = filename.replace(".png", "-fullpage.png");
+ }
+ else if (args.selector) {
+ ({ top, left, width, height } = getRect(window, args.selector, window));
+ }
+ else {
+ left = window.scrollX;
+ top = window.scrollY;
+ width = window.innerWidth;
+ height = window.innerHeight;
+ }
+
+ // Only adjust for scrollbars when considering the full window
+ if (!args.selector) {
+ const winUtils = window.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindowUtils);
+ const scrollbarHeight = {};
+ const scrollbarWidth = {};
+ winUtils.getScrollbarSize(false, scrollbarWidth, scrollbarHeight);
+ width -= scrollbarWidth.value;
+ height -= scrollbarHeight.value;
+ }
+
+ const canvas = document.createElementNS("http://www.w3.org/1999/xhtml", "canvas");
+ const ctx = canvas.getContext("2d");
+ const ratio = args.dpr ? args.dpr : window.devicePixelRatio;
+ canvas.width = width * ratio;
+ canvas.height = height * ratio;
+ ctx.scale(ratio, ratio);
+ ctx.drawWindow(window, left, top, width, height, "#fff");
+ const data = canvas.toDataURL("image/png", "");
+
+ // See comment above on bug 961832
+ if (args.fullpage) {
+ window.scrollTo(currentX, currentY);
+ }
+
+ simulateCameraEffect(document, "flash");
+
+ return Promise.resolve({
+ destinations: [],
+ data: data,
+ height: height,
+ width: width,
+ filename: filename,
+ });
+}
+
+/**
+ * We may have a filename specified in args, or we might have to generate
+ * one.
+ */
+function getFilename(defaultName) {
+ // Create a name for the file if not present
+ if (defaultName != FILENAME_DEFAULT_VALUE) {
+ return defaultName;
+ }
+
+ const date = new Date();
+ let dateString = date.getFullYear() + "-" + (date.getMonth() + 1) +
+ "-" + date.getDate();
+ dateString = dateString.split("-").map(function(part) {
+ if (part.length == 1) {
+ part = "0" + part;
+ }
+ return part;
+ }).join("-");
+
+ const timeString = date.toTimeString().replace(/:/g, ".").split(" ")[0];
+ return l10n.lookupFormat("screenshotGeneratedFilename",
+ [ dateString, timeString ]) + ".png";
+}
+
+/**
+ * Save the image data to the clipboard. This returns a promise, so it can
+ * be treated exactly like imgur / file processing, but it's really sync
+ * for now.
+ */
+function saveToClipboard(context, reply) {
+ try {
+ const channel = NetUtil.newChannel({
+ uri: reply.data,
+ loadUsingSystemPrincipal: true,
+ contentPolicyType: Ci.nsIContentPolicy.TYPE_INTERNAL_IMAGE
+ });
+ const input = channel.open2();
+
+ const loadContext = context.environment.chromeWindow
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebNavigation)
+ .QueryInterface(Ci.nsILoadContext);
+
+ const imgTools = Cc["@mozilla.org/image/tools;1"]
+ .getService(Ci.imgITools);
+
+ const container = {};
+ imgTools.decodeImageData(input, channel.contentType, container);
+
+ const wrapped = Cc["@mozilla.org/supports-interface-pointer;1"]
+ .createInstance(Ci.nsISupportsInterfacePointer);
+ wrapped.data = container.value;
+
+ const trans = Cc["@mozilla.org/widget/transferable;1"]
+ .createInstance(Ci.nsITransferable);
+ trans.init(loadContext);
+ trans.addDataFlavor(channel.contentType);
+ trans.setTransferData(channel.contentType, wrapped, -1);
+
+ const clip = Cc["@mozilla.org/widget/clipboard;1"]
+ .getService(Ci.nsIClipboard);
+ clip.setData(trans, null, Ci.nsIClipboard.kGlobalClipboard);
+
+ reply.destinations.push(l10n.lookup("screenshotCopied"));
+ }
+ catch (ex) {
+ console.error(ex);
+ reply.destinations.push(l10n.lookup("screenshotErrorCopying"));
+ }
+
+ return Promise.resolve();
+}
+
+/**
+ * Upload screenshot data to Imgur, returning a promise of a URL (as a string)
+ */
+function uploadToImgur(reply) {
+ return new Promise((resolve, reject) => {
+ const xhr = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"]
+ .createInstance(Ci.nsIXMLHttpRequest);
+ const fd = Cc["@mozilla.org/files/formdata;1"]
+ .createInstance(Ci.nsIDOMFormData);
+ fd.append("image", reply.data.split(",")[1]);
+ fd.append("type", "base64");
+ fd.append("title", reply.filename);
+
+ const postURL = Services.prefs.getCharPref("devtools.gcli.imgurUploadURL");
+ const clientID = "Client-ID " + Services.prefs.getCharPref("devtools.gcli.imgurClientID");
+
+ xhr.open("POST", postURL);
+ xhr.setRequestHeader("Authorization", clientID);
+ xhr.send(fd);
+ xhr.responseType = "json";
+
+ xhr.onreadystatechange = function() {
+ if (xhr.readyState == 4) {
+ if (xhr.status == 200) {
+ reply.href = xhr.response.data.link;
+ reply.destinations.push(l10n.lookupFormat("screenshotImgurUploaded",
+ [ reply.href ]));
+ } else {
+ reply.destinations.push(l10n.lookup("screenshotImgurError"));
+ }
+
+ resolve();
+ }
+ };
+ });
+}
+
+/**
+ * Progress listener that forwards calls to a transfer object.
+ *
+ * This is used below in saveToFile to forward progress updates from the
+ * nsIWebBrowserPersist object that does the actual saving to the nsITransfer
+ * which just represents the operation for the Download Manager. This keeps the
+ * Download Manager updated on saving progress and completion, so that it gives
+ * visual feedback from the downloads toolbar button when the save is done.
+ *
+ * It also allows the browser window to show auth prompts if needed (should not
+ * be needed for saving screenshots).
+ *
+ * This code is borrowed directly from contentAreaUtils.js.
+ */
+function DownloadListener(win, transfer) {
+ this.window = win;
+ this.transfer = transfer;
+
+ // For most method calls, forward to the transfer object.
+ for (let name in transfer) {
+ if (name != "QueryInterface" &&
+ name != "onStateChange") {
+ this[name] = (...args) => transfer[name].apply(transfer, args);
+ }
+ }
+
+ // Allow saveToFile to await completion for error handling
+ this._completedDeferred = defer();
+ this.completed = this._completedDeferred.promise;
+}
+
+DownloadListener.prototype = {
+ QueryInterface: function(iid) {
+ if (iid.equals(Ci.nsIInterfaceRequestor) ||
+ iid.equals(Ci.nsIWebProgressListener) ||
+ iid.equals(Ci.nsIWebProgressListener2) ||
+ iid.equals(Ci.nsISupports)) {
+ return this;
+ }
+ throw Cr.NS_ERROR_NO_INTERFACE;
+ },
+
+ getInterface: function(iid) {
+ if (iid.equals(Ci.nsIAuthPrompt) ||
+ iid.equals(Ci.nsIAuthPrompt2)) {
+ let ww = Cc["@mozilla.org/embedcomp/window-watcher;1"]
+ .getService(Ci.nsIPromptFactory);
+ return ww.getPrompt(this.window, iid);
+ }
+
+ throw Cr.NS_ERROR_NO_INTERFACE;
+ },
+
+ onStateChange: function(webProgress, request, state, status) {
+ // Check if the download has completed
+ if ((state & Ci.nsIWebProgressListener.STATE_STOP) &&
+ (state & Ci.nsIWebProgressListener.STATE_IS_NETWORK)) {
+ if (status == Cr.NS_OK) {
+ this._completedDeferred.resolve();
+ } else {
+ this._completedDeferred.reject();
+ }
+ }
+
+ this.transfer.onStateChange.apply(this.transfer, arguments);
+ }
+};
+
+/**
+ * Save the screenshot data to disk, returning a promise which is resolved on
+ * completion.
+ */
+var saveToFile = Task.async(function*(context, reply) {
+ let document = context.environment.chromeDocument;
+ let window = context.environment.chromeWindow;
+
+ // Check there is a .png extension to filename
+ if (!reply.filename.match(/.png$/i)) {
+ reply.filename += ".png";
+ }
+
+ let downloadsDir = yield Downloads.getPreferredDownloadsDirectory();
+ let downloadsDirExists = yield OS.File.exists(downloadsDir);
+ if (downloadsDirExists) {
+ // If filename is absolute, it will override the downloads directory and
+ // still be applied as expected.
+ reply.filename = OS.Path.join(downloadsDir, reply.filename);
+ }
+
+ let sourceURI = Services.io.newURI(reply.data, null, null);
+ let targetFile = new FileUtils.File(reply.filename);
+ let targetFileURI = Services.io.newFileURI(targetFile);
+
+ // Create download and track its progress.
+ // This is adapted from saveURL in contentAreaUtils.js, but simplified greatly
+ // and modified to allow saving to arbitrary paths on disk. Using these
+ // objects as opposed to just writing with OS.File allows us to tie into the
+ // download manager to record a download entry and to get visual feedback from
+ // the downloads toolbar button when the save is done.
+ const nsIWBP = Ci.nsIWebBrowserPersist;
+ const flags = nsIWBP.PERSIST_FLAGS_REPLACE_EXISTING_FILES |
+ nsIWBP.PERSIST_FLAGS_FORCE_ALLOW_COOKIES |
+ nsIWBP.PERSIST_FLAGS_BYPASS_CACHE |
+ nsIWBP.PERSIST_FLAGS_AUTODETECT_APPLY_CONVERSION;
+ let isPrivate =
+ PrivateBrowsingUtils.isContentWindowPrivate(document.defaultView);
+ let persist = Cc["@mozilla.org/embedding/browser/nsWebBrowserPersist;1"]
+ .createInstance(Ci.nsIWebBrowserPersist);
+ persist.persistFlags = flags;
+ let tr = Cc["@mozilla.org/transfer;1"].createInstance(Ci.nsITransfer);
+ tr.init(sourceURI,
+ targetFileURI,
+ "",
+ null,
+ null,
+ null,
+ persist,
+ isPrivate);
+ let listener = new DownloadListener(window, tr);
+ persist.progressListener = listener;
+ persist.savePrivacyAwareURI(sourceURI,
+ null,
+ document.documentURIObject,
+ Ci.nsIHttpChannel
+ .REFERRER_POLICY_NO_REFERRER_WHEN_DOWNGRADE,
+ null,
+ null,
+ targetFileURI,
+ isPrivate);
+
+ try {
+ // Await successful completion of the save via the listener
+ yield listener.completed;
+ reply.destinations.push(l10n.lookup("screenshotSavedToFile") +
+ ` "${reply.filename}"`);
+ } catch (ex) {
+ console.error(ex);
+ reply.destinations.push(l10n.lookup("screenshotErrorSavingToFile") + " " +
+ reply.filename);
+ }
+});
diff --git a/devtools/shared/gcli/commands/security.js b/devtools/shared/gcli/commands/security.js
new file mode 100644
index 000000000..eeed03c61
--- /dev/null
+++ b/devtools/shared/gcli/commands/security.js
@@ -0,0 +1,328 @@
+/* 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/. */
+
+/**
+ * The Security devtool supports the following arguments:
+ * * Security CSP
+ * Provides feedback about the current CSP
+ *
+ * * Security referrer
+ * Provides information about the current referrer policy
+ */
+
+"use strict";
+
+const { Cc, Ci, Cu, CC } = require("chrome");
+const l10n = require("gcli/l10n");
+const CSP = Cc["@mozilla.org/cspcontext;1"].getService(Ci.nsIContentSecurityPolicy);
+
+const GOOD_IMG_SRC = "chrome://browser/content/gcli_sec_good.svg";
+const MOD_IMG_SRC = "chrome://browser/content/gcli_sec_moderate.svg";
+const BAD_IMG_SRC = "chrome://browser/content/gcli_sec_bad.svg";
+
+
+// special handling within policy
+const POLICY_REPORT_ONLY = "report-only"
+
+// special handling of directives
+const DIR_UPGRADE_INSECURE = "upgrade-insecure-requests";
+const DIR_BLOCK_ALL_MIXED_CONTENT = "block-all-mixed-content";
+
+// special handling of sources
+const SRC_UNSAFE_INLINE = "'unsafe-inline'";
+const SRC_UNSAFE_EVAL = "'unsafe-eval'";
+
+const WILDCARD_MSG = l10n.lookup("securityCSPRemWildCard");
+const XSS_WARNING_MSG = l10n.lookup("securityCSPPotentialXSS");
+const NO_CSP_ON_PAGE_MSG = l10n.lookup("securityCSPNoCSPOnPage");
+const CONTENT_SECURITY_POLICY_MSG = l10n.lookup("securityCSPHeaderOnPage");
+const CONTENT_SECURITY_POLICY_REPORT_ONLY_MSG = l10n.lookup("securityCSPROHeaderOnPage");
+
+const NEXT_URI_HEADER = l10n.lookup("securityReferrerNextURI");
+const CALCULATED_REFERRER_HEADER = l10n.lookup("securityReferrerCalculatedReferrer");
+/* The official names from the W3C Referrer Policy Draft http://www.w3.org/TR/referrer-policy/ */
+const REFERRER_POLICY_NAMES = [ "None When Downgrade (default)", "None", "Origin Only", "Origin When Cross-Origin", "Unsafe URL" ];
+
+exports.items = [
+ {
+ // --- General Security information
+ name: "security",
+ description: l10n.lookup("securityPrivacyDesc"),
+ manual: l10n.lookup("securityManual")
+ },
+ {
+ // --- CSP specific Security information
+ item: "command",
+ runAt: "server",
+ name: "security csp",
+ description: l10n.lookup("securityCSPDesc"),
+ manual: l10n.lookup("securityCSPManual"),
+ returnType: "securityCSPInfo",
+ exec: function(args, context) {
+
+ var cspJSON = context.environment.document.nodePrincipal.cspJSON;
+ var cspOBJ = JSON.parse(cspJSON);
+
+ var outPolicies = [];
+
+ var policies = cspOBJ["csp-policies"];
+
+ // loop over all the different policies
+ for (var csp in policies) {
+ var curPolicy = policies[csp];
+
+ // loop over all the directive-values within that policy
+ var outDirectives = [];
+ var outHeader = CONTENT_SECURITY_POLICY_MSG;
+ for (var dir in curPolicy) {
+ var curDir = curPolicy[dir];
+
+ // when iterating properties within the obj we might also
+ // encounter the 'report-only' flag, which is not a csp directive.
+ if (dir === POLICY_REPORT_ONLY) {
+ outHeader = curPolicy[POLICY_REPORT_ONLY] === true ?
+ CONTENT_SECURITY_POLICY_REPORT_ONLY_MSG :
+ CONTENT_SECURITY_POLICY_MSG;
+ continue;
+ }
+
+ // loop over all the directive-sources within that directive
+ var outSrcs = [];
+
+ // special case handling for the directives
+ // upgrade-insecure-requests and block-all-mixed-content
+ // which do not include any srcs
+ if (dir === DIR_UPGRADE_INSECURE ||
+ dir === DIR_BLOCK_ALL_MIXED_CONTENT) {
+ outSrcs.push({
+ icon: GOOD_IMG_SRC,
+ src: "", // no src
+ desc: "" // no description
+ });
+ }
+
+ for (var src in curDir) {
+ var curSrc = curDir[src];
+
+ // the default icon and descritpion of the directive-src
+ var outIcon = GOOD_IMG_SRC;
+ var outDesc = "";
+
+ if (curSrc.indexOf("*") > -1) {
+ outIcon = MOD_IMG_SRC;
+ outDesc = WILDCARD_MSG;
+ }
+ if (curSrc == SRC_UNSAFE_INLINE || curSrc == SRC_UNSAFE_EVAL) {
+ outIcon = BAD_IMG_SRC;
+ outDesc = XSS_WARNING_MSG;
+ }
+ outSrcs.push({
+ icon: outIcon,
+ src: curSrc,
+ desc: outDesc
+ });
+ }
+ // append info about that directive to the directives array
+ outDirectives.push({
+ dirValue: dir,
+ dirSrc: outSrcs
+ });
+ }
+ // append info about the policy to the policies array
+ outPolicies.push({
+ header: outHeader,
+ directives: outDirectives
+ });
+ }
+ return outPolicies;
+ }
+ },
+ {
+ item: "converter",
+ from: "securityCSPInfo",
+ to: "view",
+ exec: function(cspInfo, context) {
+ var url = context.environment.target.url;
+
+ if (cspInfo.length == 0) {
+ return context.createView({
+ html:
+ "<table class='gcli-csp-detail' cellspacing='10' valign='top'>" +
+ " <tr>" +
+ " <td> <img src='chrome://browser/content/gcli_sec_bad.svg' width='20px' /> </td> " +
+ " <td>" + NO_CSP_ON_PAGE_MSG + " <b>" + url + "</b></td>" +
+ " </tr>" +
+ "</table>"});
+ }
+
+ return context.createView({
+ html:
+ "<table class='gcli-csp-detail' cellspacing='10' valign='top'>" +
+ // iterate all policies
+ " <tr foreach='csp in ${cspinfo}' >" +
+ " <td> ${csp.header} <b>" + url + "</b><br/><br/>" +
+ " <table class='gcli-csp-dir-detail' valign='top'>" +
+ // >> iterate all directives
+ " <tr foreach='dir in ${csp.directives}' >" +
+ " <td valign='top'> ${dir.dirValue} </td>" +
+ " <td valign='top'>" +
+ " <table class='gcli-csp-src-detail' valign='top'>" +
+ // >> >> iterate all srs
+ " <tr foreach='src in ${dir.dirSrc}' >" +
+ " <td valign='center' width='20px'> <img src= \"${src.icon}\" width='20px' /> </td> " +
+ " <td valign='center' width='200px'> ${src.src} </td>" +
+ " <td valign='center'> ${src.desc} </td>" +
+ " </tr>" +
+ " </table>" +
+ " </td>" +
+ " </tr>" +
+ " </table>" +
+ " </td>" +
+ " </tr>" +
+ "</table>",
+ data: {
+ cspinfo: cspInfo,
+ }
+ });
+ }
+ },
+ {
+ // --- Referrer Policy specific Security information
+ item: "command",
+ runAt: "server",
+ name: "security referrer",
+ description: l10n.lookup("securityReferrerPolicyDesc"),
+ manual: l10n.lookup("securityReferrerPolicyManual"),
+ returnType: "securityReferrerPolicyInfo",
+ exec: function(args, context) {
+ var doc = context.environment.document;
+
+ var referrerPolicy = doc.referrerPolicy;
+
+ var pageURI = doc.documentURIObject;
+ var sameDomainReferrer = "";
+ var otherDomainReferrer = "";
+ var downgradeReferrer = "";
+ var otherDowngradeReferrer = "";
+ var origin = pageURI.prePath;
+
+ switch (referrerPolicy) {
+ case Ci.nsIHttpChannel.REFERRER_POLICY_NO_REFERRER:
+ // sends no referrer
+ sameDomainReferrer
+ = otherDomainReferrer
+ = downgradeReferrer
+ = otherDowngradeReferrer
+ = "(no referrer)";
+ break;
+ case Ci.nsIHttpChannel.REFERRER_POLICY_ORIGIN:
+ // only sends the origin of the referring URL
+ sameDomainReferrer
+ = otherDomainReferrer
+ = downgradeReferrer
+ = otherDowngradeReferrer
+ = origin;
+ break;
+ case Ci.nsIHttpChannel.REFERRER_POLICY_ORIGIN_WHEN_XORIGIN:
+ // same as default, but reduced to ORIGIN when cross-origin.
+ sameDomainReferrer = pageURI.spec;
+ otherDomainReferrer
+ = downgradeReferrer
+ = otherDowngradeReferrer
+ = origin;
+ break;
+ case Ci.nsIHttpChannel.REFERRER_POLICY_UNSAFE_URL:
+ // always sends the referrer, even on downgrade.
+ sameDomainReferrer
+ = otherDomainReferrer
+ = downgradeReferrer
+ = otherDowngradeReferrer
+ = pageURI.spec;
+ break;
+ case Ci.nsIHttpChannel.REFERRER_POLICY_NO_REFERRER_WHEN_DOWNGRADE:
+ // default state, doesn't send referrer from https->http
+ sameDomainReferrer = otherDomainReferrer = pageURI.spec;
+ downgradeReferrer = otherDowngradeReferrer = "(no referrer)";
+ break;
+ default:
+ // this is a new referrer policy which we do not know about
+ sameDomainReferrer
+ = otherDomainReferrer
+ = downgradeReferrer
+ = otherDowngradeReferrer
+ = "(unknown Referrer Policy)";
+ break;
+ }
+
+ var sameDomainUri = origin + "/*";
+
+ var referrerUrls = [
+ // add the referrer uri 'referrer' we would send when visiting 'uri'
+ {
+ uri: pageURI.scheme+'://example.com/',
+ referrer: otherDomainReferrer,
+ description: l10n.lookup('securityReferrerPolicyOtherDomain')},
+ {
+ uri: sameDomainUri,
+ referrer: sameDomainReferrer,
+ description: l10n.lookup('securityReferrerPolicySameDomain')}
+ ];
+
+ if (pageURI.schemeIs('https')) {
+ // add the referrer we would send on downgrading http->https
+ if (sameDomainReferrer != downgradeReferrer) {
+ referrerUrls.push({
+ uri: "http://"+pageURI.hostPort+"/*",
+ referrer: downgradeReferrer,
+ description:
+ l10n.lookup('securityReferrerPolicySameDomainDowngrade')
+ });
+ }
+ if (otherDomainReferrer != otherDowngradeReferrer) {
+ referrerUrls.push({
+ uri: "http://example.com/",
+ referrer: otherDowngradeReferrer,
+ description:
+ l10n.lookup('securityReferrerPolicyOtherDomainDowngrade')
+ });
+ }
+ }
+
+ return {
+ header: l10n.lookupFormat("securityReferrerPolicyReportHeader",
+ [pageURI.spec]),
+ policyName: REFERRER_POLICY_NAMES[referrerPolicy],
+ urls: referrerUrls
+ }
+ }
+ },
+ {
+ item: "converter",
+ from: "securityReferrerPolicyInfo",
+ to: "view",
+ exec: function(referrerPolicyInfo, context) {
+ return context.createView({
+ html:
+ "<div class='gcli-referrer-policy'>" +
+ " <strong> ${rpi.header} </strong> <br />" +
+ " ${rpi.policyName} <br />" +
+ " <table class='gcli-referrer-policy-detail' cellspacing='10' >" +
+ " <tr>" +
+ " <th> " + NEXT_URI_HEADER + " </th>" +
+ " <th> " + CALCULATED_REFERRER_HEADER + " </th>" +
+ " </tr>" +
+ // iterate all policies
+ " <tr foreach='nextURI in ${rpi.urls}' >" +
+ " <td> ${nextURI.description} (e.g., ${nextURI.uri}) </td>" +
+ " <td> ${nextURI.referrer} </td>" +
+ " </tr>" +
+ " </table>" +
+ "</div>",
+ data: {
+ rpi: referrerPolicyInfo,
+ }
+ });
+ }
+ }
+];
diff --git a/devtools/shared/gcli/moz.build b/devtools/shared/gcli/moz.build
new file mode 100644
index 000000000..c1015569f
--- /dev/null
+++ b/devtools/shared/gcli/moz.build
@@ -0,0 +1,23 @@
+# -*- 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/.
+
+DIRS += [
+ 'commands',
+ 'source/lib/gcli',
+ 'source/lib/gcli/connectors',
+ 'source/lib/gcli/converters',
+ 'source/lib/gcli/commands',
+ 'source/lib/gcli/fields',
+ 'source/lib/gcli/languages',
+ 'source/lib/gcli/mozui',
+ 'source/lib/gcli/types',
+ 'source/lib/gcli/ui',
+ 'source/lib/gcli/util',
+]
+
+DevToolsModules(
+ 'templater.js'
+)
diff --git a/devtools/shared/gcli/source/LICENSE b/devtools/shared/gcli/source/LICENSE
new file mode 100644
index 000000000..d64569567
--- /dev/null
+++ b/devtools/shared/gcli/source/LICENSE
@@ -0,0 +1,202 @@
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ 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.
diff --git a/devtools/shared/gcli/source/docs/design.md b/devtools/shared/gcli/source/docs/design.md
new file mode 100644
index 000000000..5e3c0a2f3
--- /dev/null
+++ b/devtools/shared/gcli/source/docs/design.md
@@ -0,0 +1,102 @@
+
+# The Design of GCLI
+
+## Design Goals
+
+GCLI should be:
+
+- primarily for technical users.
+- as fast as a traditional CLI. It should be possible to put your head down,
+ and look at the keyboard and use GCLI 'blind' at full speed without making
+ mistakes.
+- principled about the way it encourages people to build commands. There is
+ benefit from unifying the underlying concepts.
+- automatically helpful.
+
+GCLI should not attempt to:
+
+- convert existing GUI users to a CLI.
+- use natural language input. The closest we should get to natural language is
+ thinking of commands as ```verb noun --adjective```.
+- gain a touch based interface. Whilst it's possible (even probable) that touch
+ can provide further benefits to command line users, that can wait while we
+ catch up with 1985.
+- slavishly follow the syntax of existing commands, predictability is more
+ important.
+- be a programming language. Shell scripts are mini programming languages but
+ we have JavaScript sat just next door. It's better to integrate than compete.
+
+
+## Design Challenges
+
+What has changed since 1970 that might cause us to make changes to the design
+of the command line?
+
+
+### Connection limitations
+
+Unix pre-dates the Internet and treats almost everything as a file. Since the
+Internet it could be more useful to use URIs as ways to identify sources of data.
+
+
+### Memory limitations
+
+Modern computers have something like 6 orders of magnitude more memory than the
+PDP-7 on which Unix was developed. Innovations like stdin/stdout and pipes are
+ways to connect systems without long-term storage of the results. The ability
+to store results for some time (potentially in more than one format)
+significantly reduces the need for these concepts. We should make the results
+of past commands addressable for re-use at a later time.
+
+There are a number of possible policies for eviction of items from the history.
+We should investigate options other than a simple stack.
+
+
+### Multi-tasking limitations
+
+Multi-tasking was a problem in 1970; the problem was getting a computer to do
+many jobs on 1 core. Today the problem is getting a computer to do one job on
+many cores. However we're stuck with this legacy in 2 ways. Firstly that the
+default is to force everything to wait until the previous job is finished, but
+more importantly that output from parallel jobs frequently collides
+
+ $ find / -ctime 5d -print &
+ $ find / -uid 0 -print &
+ // good luck working out what came from where
+
+ $ tail -f logfile.txt &
+ $ vi main.c
+ // have a nice time editing that file
+
+GCLI should allow commands to be asynchronous and will provide UI elements to
+inform the user of job completion. It will also keep asynchronous command
+output contained within it's own display area.
+
+
+### Output limitations
+
+The PDP-7 had a teletype. There is something like 4 orders of magnitude more
+information that can be displayed on a modern display than a 80x24 character
+based console. We can use this flexibility to provide better help to the user
+in entering their command.
+
+The additional display richness can also allow interaction with result output.
+Command output can include links to follow-up commands, and even ask for
+additional input. (e.g. "your search returned zero results do you want to try
+again with a different search string")
+
+There is no reason why output must be static. For example, it could be
+informative to see the results of an "ls" command alter given changes made by
+subsequent commands. (It should be noted that there are times when historical
+information is important too)
+
+
+### Integration limitations
+
+In 1970, command execution meant retrieving a program from storage, and running
+it. This required minimal interaction between the command line processor and
+the program being run, and was good for resource constrained systems.
+This lack of interaction resulted in the processing of command line arguments
+being done everywhere, when the task was better suited to command line.
+We should provide metadata about the commands being run, to allow the command
+line to process, interpret and provide help on the input.
diff --git a/devtools/shared/gcli/source/docs/developing-gcli.md b/devtools/shared/gcli/source/docs/developing-gcli.md
new file mode 100644
index 000000000..113712655
--- /dev/null
+++ b/devtools/shared/gcli/source/docs/developing-gcli.md
@@ -0,0 +1,213 @@
+
+# Developing GCLI
+
+## About the code
+
+The majority of the GCLI source is stored in the ``lib`` directory.
+
+The ``docs`` directory contains documentation.
+The ``scripts`` directory contains RequireJS that GCLI uses.
+The ``build`` directory contains files used when creating builds.
+The ``mozilla`` directory contains the mercurial patch queue of patches to apply
+to mozilla-central.
+The ``selenium-tests`` directory contains selenium web-page integration tests.
+
+The source in the ``lib`` directory is split into 4 sections:
+
+- ``lib/demo`` contains commands used in the demo page. It is not needed except
+ for demo purposes.
+- ``lib/test`` contains a small test harness for testing GCLI.
+- ``lib/gclitest`` contains tests that run in the test harness
+- ``lib/gcli`` contains the actual meat
+
+GCLI is split into a UI portion and a Model/Controller portion.
+
+
+## The GCLI Model
+
+The heart of GCLI is a ``Requisition``, which is an AST for the input. A
+``Requisition`` is a command that we'd like to execute, and we're filling out
+all the inputs required to execute the command.
+
+A ``Requisition`` has a ``Command`` that is to be executed. Each Command has a
+number of ``Parameter``s, each of which has a name and a type as detailed
+above.
+
+As you type, your input is split into ``Argument``s, which are then assigned to
+``Parameter``s using ``Assignment``s. Each ``Assignment`` has a ``Conversion``
+which stores the input argument along with the value that is was converted into
+according to the type of the parameter.
+
+There are special assignments called ``CommandAssignment`` which the
+``Requisition`` uses to link to the command to execute, and
+``UnassignedAssignment``used to store arguments that do not have a parameter
+to be assigned to.
+
+
+## The GCLI UI
+
+There are several components of the GCLI UI. Each can have a script portion,
+some template HTML and a CSS file. The template HTML is processed by
+``domtemplate`` before use.
+
+DomTemplate is fully documented in [it's own repository]
+(https://github.com/joewalker/domtemplate).
+
+The components are:
+
+- ``Inputter`` controls the input field, processing special keyboard events and
+ making sure that it stays in sync with the Requisition.
+- ``Completer`` updates a div that is located behind the input field and used
+ to display completion advice and hint highlights. It is stored in
+ completer.js.
+- ``Display`` is responsible for containing the popup hints that are displayed
+ above the command line. Typically Display contains a Hinter and a RequestsView
+ although these are not both required. Display itself is optional, and isn't
+ planned for use in the first release of GCLI in Firefox.
+- ``Hinter`` Is used to display input hints. It shows either a Menu or an
+ ArgFetch component depending on the state of the Requisition
+- ``Menu`` is used initially to select the command to be executed. It can act
+ somewhat like the Start menu on windows.
+- ``ArgFetch`` Once the command to be executed has been selected, ArgFetch
+ shows a 'dialog' allowing the user to enter the parameters to the selected
+ command.
+- ``RequestsView`` Contains a set of ``RequestView`` components, each of which
+ displays a command that has been invoked. RequestsView is a poor name, and
+ should better be called ReportView
+
+ArgFetch displays a number of Fields. There are fields for most of the Types
+discussed earlier. See 'Writing Fields' above for more information.
+
+
+## Testing
+
+GCLI contains 2 test suites:
+
+- JS level testing is run with the ``test`` command. The tests are located in
+ ``lib/gclitest`` and they use the test runner in ``lib/test``. This is fairly
+ comprehensive, however it does not do UI level testing.
+ If writing a new test it needs to be registered in ``lib/gclitest/index``.
+ For an example of how to write tests, see ``lib/gclitest/testSplit.js``.
+ The test functions are implemented in ``lib/test/assert``.
+- Browser integration tests are included in ``browser_webconsole_gcli_*.js``,
+ in ``toolkit/components/console/hudservice/tests/browser``. These are
+ run with the rest of the Mozilla test suite.
+
+
+## Coding Conventions
+
+The coding conventions for the GCLI project come from the Bespin/Skywriter and
+Ace projects. They are roughly [Crockford]
+(http://javascript.crockford.com/code.html) with a few exceptions and
+additions:
+
+* ``var`` does not need to be at the top of each function, we'd like to move
+ to ``let`` when it's generally available, and ``let`` doesn't have the same
+ semantic twists as ``var``.
+
+* Strings are generally enclosed in single quotes.
+
+* ``eval`` is to be avoided, but we don't declare it evil.
+
+The [Google JavaScript conventions]
+(https://google-styleguide.googlecode.com/svn/trunk/javascriptguide.xml) are
+more detailed, we tend to deviate in:
+
+* Custom exceptions: We generally just use ``throw new Error('message');``
+
+* Multi-level prototype hierarchies: Allowed; we don't have ``goog.inherits()``
+
+* ``else`` begins on a line by itself:
+
+ if (thing) {
+ doThis();
+ }
+ else {
+ doThat();
+ }
+
+
+## Startup
+
+Internally GCLI modules have ``startup()``/``shutdown()`` functions which are
+called on module init from the top level ``index.js`` of that 'package'.
+
+In order to initialize a package all that is needed is to require the package
+index (e.g. ``require('package/index')``).
+
+The ``shutdown()`` function was useful when GCLI was used in Bespin as part of
+dynamic registration/de-registration. It is not known if this feature will be
+useful in the future. So it has not been entirely removed, it may be at some
+future date.
+
+
+## Running the Unit Tests
+
+Start the GCLI static server:
+
+ cd path/to/gcli
+ node gcli.js
+
+Now point your browser to http://localhost:9999/localtest.html. When the page
+loads the tests will be automatically run outputting to the console, or you can
+enter the ``test`` command to run the unit tests.
+
+
+## Contributing Code
+
+Please could you do the following to help minimize the amount of rework that we
+do:
+
+1. Check the unit tests run correctly (see **Running the Unit Tests** above)
+2. Check the code follows the style guide. At a minimum it should look like the
+ code around it. For more detailed notes, see **Coding Conventions** above
+3. Help me review your work by using good commit comments. Which means 2 things
+ * Well formatted messages, i.e. 50 char summary including bug tag, followed
+ by a blank line followed by a more in-depth message wrapped to 72 chars
+ per line. This is basically the format used by the Linux Kernel. See the
+ [commit log](https://github.com/joewalker/gcli/commits/master) for
+ examples. The be extra helpful, please use the "shortdesc-BUGNUM: " if
+ possible which also helps in reviews.
+ * Commit your changes as a story. Make it easy for me to understand the
+ changes that you've made.
+4. Sign your work. To improve tracking of who did what, we follow the sign-off
+ procedure used in the Linux Kernel.
+ The sign-off is a simple line at the end of the explanation for the
+ patch, which certifies that you wrote it or otherwise have the right to
+ pass it on as an open-source patch. The rules are pretty simple: if you
+ can certify the below:
+
+ Developer's Certificate of Origin 1.1
+
+ By making a contribution to this project, I certify that:
+
+ (a) The contribution was created in whole or in part by me and I
+ have the right to submit it under the open source license
+ indicated in the file; or
+
+ (b) The contribution is based upon previous work that, to the best
+ of my knowledge, is covered under an appropriate open source
+ license and I have the right under that license to submit that
+ work with modifications, whether created in whole or in part
+ by me, under the same open source license (unless I am
+ permitted to submit under a different license), as indicated
+ in the file; or
+
+ (c) The contribution was provided directly to me by some other
+ person who certified (a), (b) or (c) and I have not modified
+ it.
+
+ (d) I understand and agree that this project and the contribution
+ are public and that a record of the contribution (including all
+ personal information I submit with it, including my sign-off) is
+ maintained indefinitely and may be redistributed consistent with
+ this project or the open source license(s) involved.
+
+ then you just add a line saying
+
+ Signed-off-by: Random J Developer <random@developer.example.org>
+
+ using your real name (sorry, no pseudonyms or anonymous contributions.)
+
+Thanks for wanting to contribute code.
+
diff --git a/devtools/shared/gcli/source/docs/index.md b/devtools/shared/gcli/source/docs/index.md
new file mode 100644
index 000000000..dce112e6d
--- /dev/null
+++ b/devtools/shared/gcli/source/docs/index.md
@@ -0,0 +1,150 @@
+
+# About GCLI
+
+## GCLI is a Graphical Command Line Interpreter.
+
+GCLI is a command line for modern computers. When command lines were invented,
+computers were resource-limited, disconnected systems with slow multi-tasking
+and poor displays. The design of the Unix CLI made sense in 1970, but over 40
+years on, considering the pace of change, there are many improvements we can
+make.
+
+CLIs generally suffer from poor discoverability; It's hard when faced with a
+blank command line to work out what to do. As a result the majority of programs
+today use purely graphical user interfaces, however in doing so, they lose some
+of the benefits of CLIs. CLIs are still used because generally, in the hands of
+a skilled user they are faster, and have a wider range of available options.
+
+GCLI attempts to get the best of the GUI world and the CLI world to produce
+something that is both easy to use and learn as well as fast and powerful.
+
+GCLI has a type system to help ensure that users are inputting valid commands
+and to enable us to provide sensible context sensitive help. GCLI provides
+integration with JavaScript rather than being an alternative (like CoffeeScript).
+
+
+## History
+
+GCLI was born as part of the
+[Bespin](http://ajaxian.com/archives/canvas-for-a-text-editor) project and was
+[discussed at the time](http://j.mp/bespin-cli). The command line component
+survived the rename of Bepsin to Skywriter and the merger with Ace, got a name
+of it's own (Cockpit) which didn't last long before the project was named GCLI.
+It is now being used in the Firefox's web console where it doesn't have a
+separate identity but it's still called GCLI outside of Firefox. It is also
+used in [Eclipse Orion](http://www.eclipse.org/orion/).
+
+
+## Environments
+
+GCLI is designed to work in a number of environments:
+
+1. As a component of Firefox developer tools.
+2. As an adjunct to Orion/Ace and other online editors.
+3. As a plugin to any web-page wishing to provide its own set of commands.
+4. As part of a standalone web browser extension with it's own set of commands.
+
+
+## Related Pages
+
+Other sources of GCLI documentation:
+
+- [Writing Commands](writing-commands.md)
+- [Writing Types](writing-types.md)
+- [Developing GCLI](developing-gcli.md)
+- [Writing Tests](writing-tests.md) / [Running Tests](running-tests.md)
+- [The Design of GCLI](design.md)
+- Source
+ - The most up-to-date source is in [this Github repository](https://github.com/joewalker/gcli/).
+ - When a feature is 'done' it's merged into the [Mozilla clone](https://github.com/mozilla/gcli/).
+ - From which it flows into [Mozilla Central](https://hg.mozilla.org/mozilla-central/file/tip/devtools/client/commandline).
+- [Demo of GCLI](http://mozilla.github.com/gcli/) with an arbitrary set of demo
+ commands
+- Other Documentation
+ - [Embedding docs](https://github.com/mozilla/gcli/blob/master/docs/index.md)
+ - [Status page](http://mozilla.github.com/devtools/2011/status.html#gcli)
+
+
+## Accessibility
+
+GCLI uses ARIA roles to guide a screen-reader as to the important sections to
+voice. We welcome [feedback on how these roles are implemented](https://bugzilla.mozilla.org/enter_bug.cgi?product=Firefox&component=Developer+Tools:+Graphic+Commandline+and+Toolbar&rep_platform=All&op_sys=All&short_desc=GCLI).
+
+The command line uses TAB as a method of completing current input, this
+prevents use of TAB for keyboard navigation. Instead of using TAB to move to
+the next field you can use F6. In addition to F6, ALT+TAB, CTRL+TAB, META+TAB
+make an attempt to move the focus on. How well this works depends on your
+OS/browser combination.
+
+
+## Embedding GCLI
+
+There are 3 basic steps in using GCLI in your system.
+
+1. Import a GCLI JavaScript file.
+ For serious use of GCLI you are likely to be creating a custom build (see
+ below) however if you just want to have a quick play, you can use
+ ``gcli-uncompressed.js`` from [the gh-pages branch of GCLI]
+ (https://github.com/mozilla/gcli/tree/gh-pages)
+ Just place the following wherever you place your script files.
+
+ <script src="path/to/gcli-uncompressed.js" type="text/javascript"></script>
+
+2. Having imported GCLI, we need to tell it where to display. The simplest
+ method is to include an elements with the id of ``gcli-input`` and
+ ``gcli-display``.
+
+ <input id="gcli-input" type="text"/>
+ <div id="gcli-display"></div>
+
+3. Tell GCLI what commands to make available. See the sections on Writing
+ Commands, Writing Types and Writing Fields for more information.
+
+ GCLI uses the CommonJS AMD format for it's files, so a 'require' statement
+ is needed to get started.
+
+ require([ 'gcli/index' ], function(gcli) {
+ gcli.add(...); // Register custom commands/types/etc
+ gcli.createTerminal(); // Create a user interface
+ });
+
+ The createTerminal() function takes an ``options`` objects which allows
+ customization. At the current time the documentation of these object is left
+ to the source.
+
+
+## Backwards Compatibility
+
+The goals of the GCLI project are:
+
+- Aim for very good backwards compatibility with code required from an
+ 'index' module. This means we will not break code without a cycle of
+ deprecation warnings.
+
+ There are currently 3 'index' modules:
+ - gcli/index (all you need to get started with GCLI)
+ - demo/index (a number of demo commands)
+ - gclitest/index (GCLI test suite)
+
+ Code from these modules uses the module pattern to prevent access to internal
+ functions, so in essence, if you can get to it from an index module, you
+ should be ok.
+
+- We try to avoid needless change to other modules, however we don't make any
+ promises, and don't provide a deprecation cycle.
+
+ Code from other modules uses classes rather than modules, so member variables
+ are exposed. Many classes mark private members using the `_underscorePrefix`
+ pattern. Particular care should be taken if access is needed to a private
+ member.
+
+
+## Creating Custom Builds
+
+GCLI uses [DryIce](https://github.com/mozilla/dryice) to create custom builds.
+If dryice is installed (``npm install .``) then you can create a built
+version of GCLI simply using ``node gcli.js standard``. DryIce supplies a custom
+module loader to replace RequireJS for built applications.
+
+The build will be output to the ``built`` directory. The directory will be
+created if it doesn't exist.
diff --git a/devtools/shared/gcli/source/docs/running-tests.md b/devtools/shared/gcli/source/docs/running-tests.md
new file mode 100644
index 000000000..378c40837
--- /dev/null
+++ b/devtools/shared/gcli/source/docs/running-tests.md
@@ -0,0 +1,60 @@
+
+# Running Tests
+
+GCLI has a test suite that can be run in a number of different environments.
+Some of the tests don't work in all environments. These should be automatically
+skipped when not applicable.
+
+
+## Web
+
+Running a limited set of test from the web is the easiest. Simply load
+'localtest.html' and the unit tests should be run automatically, with results
+displayed on the console. Tests can be re-run using the 'test' command.
+
+It also creates a function 'testCommands()' to be run at a JS prompt, which
+enables the test commands for debugging purposes.
+
+
+## Firefox
+
+GCLI's test suite integrates with Mochitest and runs automatically on each test
+run. Dryice packages the tests to format them for the Firefox build system.
+
+For more information about running Mochitest on Firefox (including GCLI) see
+[the MDN, Mochitest docs](https://developer.mozilla.org/en/Mochitest)
+
+
+# Node
+
+Running the test suite under node can be done as follows:
+
+ $ node gcli.js test
+
+Or, using the `test` command:
+
+ $ node gcli.js
+ Serving GCLI to http://localhost:9999/
+ This is also a limited GCLI prompt.
+ Type 'help' for a list of commands, CTRL+C twice to exit:
+ : test
+
+ testCli: Pass (funcs=9, checks=208)
+ testCompletion: Pass (funcs=1, checks=139)
+ testExec: Pass (funcs=1, checks=133)
+ testHistory: Pass (funcs=3, checks=13)
+ ....
+
+ Summary: Pass (951 checks)
+
+
+# Travis CI
+
+GCLI check-ins are automatically tested by [Travis CI](https://travis-ci.org/joewalker/gcli).
+
+
+# Test Case Generation
+
+GCLI can generate test cases automagically. Load ```localtest.html```, type a
+command to be tested into GCLI, and the press F2. GCLI will output to the
+console a template test case for the entered command.
diff --git a/devtools/shared/gcli/source/docs/writing-commands.md b/devtools/shared/gcli/source/docs/writing-commands.md
new file mode 100644
index 000000000..e73050279
--- /dev/null
+++ b/devtools/shared/gcli/source/docs/writing-commands.md
@@ -0,0 +1,757 @@
+
+# Writing Commands
+
+## Basics
+
+GCLI has opinions about how commands should be written, and it encourages you
+to do The Right Thing. The opinions are based on helping users convert their
+intentions to commands and commands to what's actually going to happen.
+
+- Related commands should be sub-commands of a parent command. One of the goals
+ of GCLI is to support a large number of commands without things becoming
+ confusing, this will require some sort of namespacing or there will be
+ many people wanting to implement the ``add`` command. This style of
+ writing commands has become common place in Unix as the number of commands
+ has gone up.
+ The ```context``` command allows users to focus on a parent command, promoting
+ its sub-commands above others.
+
+- Each command should do exactly and only one thing. An example of a Unix
+ command that breaks this principle is the ``tar`` command
+
+ $ tar -zcf foo.tar.gz .
+ $ tar -zxf foo.tar.gz .
+
+ These 2 commands do exactly opposite things. Many a file has died as a result
+ of a x/c typo. In GCLI this would be better expressed:
+
+ $ tar create foo.tar.gz -z .
+ $ tar extract foo.tar.gz -z .
+
+ There may be commands (like tar) which have enough history behind them
+ that we shouldn't force everyone to re-learn a new syntax. The can be achieved
+ by having a single string parameter and parsing the input in the command)
+
+- Avoid errors. We try to avoid the user having to start again with a command
+ due to some problem. The majority of problems are simple typos which we can
+ catch using command metadata, but there are 2 things command authors can do
+ to prevent breakage.
+
+ - Where possible avoid the need to validate command line parameters in the
+ exec function. This can be done by good parameter design (see 'do exactly
+ and only one thing' above)
+
+ - If there is an obvious fix for an unpredictable problem, offer the
+ solution in the command output. So rather than use request.error (see
+ Request Object below) output some HTML which contains a link to a fixed
+ command line.
+
+Currently these concepts are not enforced at a code level, but they could be in
+the future.
+
+
+## How commands work
+
+This is how to create a basic ``greet`` command:
+
+ gcli.addItems([{
+ name: 'greet',
+ description: 'Show a greeting',
+ params: [
+ {
+ name: 'name',
+ type: 'string',
+ description: 'The name to greet'
+ }
+ ],
+ returnType: 'string',
+ exec: function(args, context) {
+ return 'Hello, ' + args.name;
+ }
+ }]);
+
+This command is used as follows:
+
+ : greet Joe
+ Hello, Joe
+
+Some terminology that isn't always obvious: a function has 'parameters', and
+when you call a function, you pass 'arguments' to it.
+
+
+## Internationalization (i18n)
+
+There are several ways that GCLI commands can be localized. The best method
+depends on what context you are writing your command for.
+
+### Firefox Embedding
+
+GCLI supports Mozilla style localization. To add a command that will only ever
+be used embedded in Firefox, this is the way to go. Your strings should be
+stored in ``toolkit/locales/en-US/chrome/global/devtools/gclicommands.properties``,
+And you should access them using ``let l10n = require("gcli/l10n")`` and then
+``l10n.lookup(...)`` or ``l10n.lookupFormat()``
+
+For examples of existing commands, take a look in
+``devtools/client/webconsole/GcliCommands.jsm``, which contains most of the
+current GCLI commands. If you will be adding a number of new commands, then
+consider starting a new JSM.
+
+Your command will then look something like this:
+
+ gcli.addItems([{
+ name: 'greet',
+ description: gcli.lookup("greetDesc")
+ ...
+ }]);
+
+### Web Commands
+
+There are 2 ways to provide translated strings for web use. The first is to
+supply the translated strings in the description:
+
+ gcli.addItems([{
+ name: 'greet',
+ description: {
+ 'root': 'Show a greeting',
+ 'fr-fr': 'Afficher un message d'accueil',
+ 'de-de': 'Zeige einen GruƟ',
+ 'gk-gk': 'Ī•Ī¼Ļ†Ī¬Ī½Ī¹ĻƒĪ· Ī­Ī½Ī± Ļ‡Ī±Ī¹ĻĪµĻ„Ī¹ĻƒĪ¼ĻŒ',
+ ...
+ }
+ ...
+ }]);
+
+Each description should contain at least a 'root' entry which is the
+default if no better match is found. This method has the benefit of being
+compact and simple, however it has the significant drawback of being wasteful
+of memory and bandwidth to transmit and store a significant number of strings,
+the majority of which will never be used.
+
+More efficient is to supply a lookup key and ask GCLI to lookup the key from an
+appropriate localized strings file:
+
+ gcli.addItems([{
+ name: 'greet',
+ description: { 'key': 'demoGreetingDesc' }
+ ...
+ }]);
+
+For web usage, the central store of localized strings is
+``lib/gcli/nls/strings.js``. Other string files can be added using the
+``l10n.registerStringsSource(...)`` function.
+
+This method can be used both in Firefox and on the Web (see the help command
+for an example). However this method has the drawback that it will not work
+with DryIce built files until we fix bug 683844.
+
+
+## Default argument values
+
+The ``greet`` command requires the entry of the ``name`` parameter. This
+parameter can be made optional with the addition of a ``defaultValue`` to the
+parameter:
+
+ gcli.addItems([{
+ name: 'greet',
+ description: 'Show a message to someone',
+ params: [
+ {
+ name: 'name',
+ type: 'string',
+ description: 'The name to greet',
+ defaultValue: 'World!'
+ }
+ ],
+ returnType: 'string',
+ exec: function(args, context) {
+ return "Hello, " + args.name;
+ }
+ }]);
+
+Now we can also use the ``greet`` command as follows:
+
+ : greet
+ Hello, World!
+
+
+## Positional vs. named arguments
+
+Arguments can be entered either positionally or as named arguments. Generally
+users will prefer to type the positional version, however the named alternative
+can be more self documenting.
+
+For example, we can also invoke the greet command as follows:
+
+ : greet --name Joe
+ Hello, Joe
+
+
+## Short argument names
+
+GCLI allows you to specify a 'short' character for any parameter:
+
+ gcli.addItems([{
+ name: 'greet',
+ params: [
+ {
+ name: 'name',
+ short: 'n',
+ type: 'string',
+ ...
+ }
+ ],
+ ...
+ }]);
+
+This is used as follows:
+
+ : greet -n Fred
+ Hello, Fred
+
+Currently GCLI does not allow short parameter merging (i.e. ```ls -la```)
+however this is planned.
+
+
+## Parameter types
+
+Initially the available types are:
+
+- string
+- boolean
+- number
+- selection
+- delegate
+- date
+- array
+- file
+- node
+- nodelist
+- resource
+- command
+- setting
+
+This list can be extended. See [Writing Types](writing-types.md) on types for
+more information.
+
+The following examples assume the following definition of the ```greet```
+command:
+
+ gcli.addItems([{
+ name: 'greet',
+ params: [
+ { name: 'name', type: 'string' },
+ { name: 'repeat', type: 'number' }
+ ],
+ ...
+ }]);
+
+Parameters can be specified either with named arguments:
+
+ : greet --name Joe --repeat 2
+
+And sometimes positionally:
+
+ : greet Joe 2
+
+Parameters can be specified positionally if they are considered 'important'.
+Unimportant parameters must be specified with a named argument.
+
+Named arguments can be specified anywhere on the command line (after the
+command itself) however positional arguments must be in order. So
+these examples are the same:
+
+ : greet --name Joe --repeat 2
+ : greet --repeat 2 --name Joe
+
+However (obviously) these are not the same:
+
+ : greet Joe 2
+ : greet 2 Joe
+
+(The second would be an error because 'Joe' is not a number).
+
+Named arguments are assigned first, then the remaining arguments are assigned
+to the remaining parameters. So the following is valid and unambiguous:
+
+ : greet 2 --name Joe
+
+Positional parameters quickly become unwieldy with long parameter lists so we
+recommend only having 2 or 3 important parameters. GCLI provides hints for
+important parameters more obviously than unimportant ones.
+
+Parameters are 'important' if they are not in a parameter group. The easiest way
+to achieve this is to use the ```option: true``` property.
+
+For example, using:
+
+ gcli.addItems([{
+ name: 'greet',
+ params: [
+ { name: 'name', type: 'string' },
+ { name: 'repeat', type: 'number', option: true, defaultValue: 1 }
+ ],
+ ...
+ }]);
+
+Would mean that this is an error
+
+ : greet Joe 2
+
+You would instead need to do the following:
+
+ : greet Joe --repeat 2
+
+For more on parameter groups, see below.
+
+In addition to being 'important' and 'unimportant' parameters can also be
+optional. If is possible to be important and optional, but it is not possible
+to be unimportant and non-optional.
+
+Parameters are optional if they either:
+- Have a ```defaultValue``` property
+- Are of ```type=boolean``` (boolean arguments automatically default to being false)
+
+There is currently no way to make parameters mutually exclusive.
+
+
+## Selection types
+
+Parameters can have a type of ``selection``. For example:
+
+ gcli.addItems([{
+ name: 'greet',
+ params: [
+ { name: 'name', ... },
+ {
+ name: 'lang',
+ description: 'In which language should we greet',
+ type: { name: 'selection', data: [ 'en', 'fr', 'de', 'es', 'gk' ] },
+ defaultValue: 'en'
+ }
+ ],
+ ...
+ }]);
+
+GCLI will enforce that the value of ``arg.lang`` was one of the values
+specified. Alternatively ``data`` can be a function which returns an array of
+strings.
+
+The ``data`` property is useful when the underlying type is a string but it
+doesn't work when the underlying type is something else. For this use the
+``lookup`` property as follows:
+
+ type: {
+ name: 'selection',
+ lookup: {
+ 'en': Locale.EN,
+ 'fr': Locale.FR,
+ ...
+ }
+ },
+
+Similarly, ``lookup`` can be a function returning the data of this type.
+
+
+## Number types
+
+Number types are mostly self explanatory, they have one special property which
+is the ability to specify upper and lower bounds for the number:
+
+ gcli.addItems([{
+ name: 'volume',
+ params: [
+ {
+ name: 'vol',
+ description: 'How loud should we go',
+ type: { name: 'number', min: 0, max: 11 }
+ }
+ ],
+ ...
+ }]);
+
+You can also specify a ``step`` property which specifies by what amount we
+should increment and decrement the values. The ``min``, ``max``, and ``step``
+properties are used by the command line when up and down are pressed and in
+the input type of a dialog generated from this command.
+
+
+## Delegate types
+
+Delegate types are needed when the type of some parameter depends on the type
+of another parameter. For example:
+
+ : set height 100
+ : set name "Joe Walker"
+
+We can achieve this as follows:
+
+ gcli.addItems([{
+ name: 'set',
+ params: [
+ {
+ name: 'setting',
+ type: { name: 'selection', values: [ 'height', 'name' ] }
+ },
+ {
+ name: 'value',
+ type: {
+ name: 'delegate',
+ delegateType: function() { ... }
+ }
+ }
+ ],
+ ...
+ }]);
+
+Several details are left out of this example, like how the delegateType()
+function knows what the current setting is. See the ``pref`` command for an
+example.
+
+
+## Array types
+
+Parameters can have a type of ``array``. For example:
+
+ gcli.addItems([{
+ name: 'greet',
+ params: [
+ {
+ name: 'names',
+ type: { name: 'array', subtype: 'string' },
+ description: 'The names to greet',
+ defaultValue: [ 'World!' ]
+ }
+ ],
+ ...
+ exec: function(args, context) {
+ return "Hello, " + args.names.join(', ') + '.';
+ }
+ }]);
+
+This would be used as follows:
+
+ : greet Fred Jim Shiela
+ Hello, Fred, Jim, Shiela.
+
+Or using named arguments:
+
+ : greet --names Fred --names Jim --names Shiela
+ Hello, Fred, Jim, Shiela.
+
+There can only be one ungrouped parameter with an array type, and it must be
+at the end of the list of parameters (i.e. just before any parameter groups).
+This avoids confusion as to which parameter an argument should be assigned.
+
+
+## Sub-commands
+
+It is common for commands to be groups into those with similar functionality.
+Examples include virtually all VCS commands, ``apt-get``, etc. There are many
+examples of commands that should be structured as in a sub-command style -
+``tar`` being the obvious example, but others include ``crontab``.
+
+Groups of commands are specified with the top level command not having an
+exec function:
+
+ gcli.addItems([
+ {
+ name: 'tar',
+ description: 'Commands to manipulate archives',
+ },
+ {
+ name: 'tar create',
+ description: 'Create a new archive',
+ exec: function(args, context) { ... },
+ ...
+ },
+ {
+ name: 'tar extract',
+ description: 'Extract from an archive',
+ exec: function(args, context) { ... },
+ ...
+ }
+ ]);
+
+
+## Parameter groups
+
+Parameters can be grouped into sections.
+
+There are 3 ways to assign a parameter to a group.
+
+The simplest uses ```option: true``` to put a parameter into the default
+'Options' group:
+
+ gcli.addItems([{
+ name: 'greet',
+ params: [
+ { name: 'repeat', type: 'number', option: true }
+ ],
+ ...
+ }]);
+
+The ```option``` property can also take a string to use an alternative parameter
+group:
+
+ gcli.addItems([{
+ name: 'greet',
+ params: [
+ { name: 'repeat', type: 'number', option: 'Advanced' }
+ ],
+ ...
+ }]);
+
+An example of how this can be useful is 'git' which categorizes parameters into
+'porcelain' and 'plumbing'.
+
+Finally, parameters can be grouped together as follows:
+
+ gcli.addItems([{
+ name: 'greet',
+ params: [
+ { name: 'name', type: 'string', description: 'The name to greet' },
+ {
+ group: 'Advanced Options',
+ params: [
+ { name: 'repeat', type: 'number', defaultValue: 1 },
+ { name: 'debug', type: 'boolean' }
+ ]
+ }
+ ],
+ ...
+ }]);
+
+This could be used as follows:
+
+ : greet Joe --repeat 2 --debug
+ About to send greeting
+ Hello, Joe
+ Hello, Joe
+ Done!
+
+Parameter groups must come after non-grouped parameters 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.
+
+
+## Command metadata
+
+Each command should have the following properties:
+
+- A string ``name``.
+- A short ``description`` string. Generally no more than 20 characters without
+ a terminating period/fullstop.
+- A function to ``exec``ute. (Optional for the parent containing sub-commands)
+ See below for more details.
+
+And optionally the following extra properties:
+
+- A declaration of the accepted ``params``.
+- A ``hidden`` property to stop the command showing up in requests for help.
+- A ``context`` property which defines the scope of the function that we're
+ calling. Rather than simply call ``exec()``, we do ``exec.call(context)``.
+- A ``manual`` property which allows a fuller description of the purpose of the
+ command.
+- A ``returnType`` specifying how we should handle the value returned from the
+ exec function.
+
+The ``params`` property is an array of objects, one for each parameter. Each
+parameter object should have the following 3 properties:
+
+- A string ``name``.
+- A short string ``description`` as for the command.
+- A ``type`` which refers to an existing Type (see Writing Types).
+
+Optionally each parameter can have these properties:
+
+- A ``defaultValue`` (which should be in the type specified in ``type``).
+ The defaultValue will be used when there is no argument supplied for this
+ parameter on the command line.
+ If the parameter has a ``defaultValue``, other than ``undefined`` then the
+ parameter is optional, and if unspecified on the command line, the matching
+ argument will have this value when the function is called.
+ If ``defaultValue`` is missing, or if it is set to ``undefined``, then the
+ system will ensure that a value is provided before anything is executed.
+ There are 2 special cases:
+ - If the type is ``selection``, then defaultValue must not be undefined.
+ The defaultValue must either be ``null`` (meaning that a value must be
+ supplied by the user) or one of the selection values.
+ - If the type is ``boolean``, then ``defaultValue:false`` is implied and
+ can't be changed. Boolean toggles are assumed to be off by default, and
+ should be named to match.
+- A ``manual`` property for parameters is exactly analogous to the ``manual``
+ property for commands - descriptive text that is longer than than 20
+ characters.
+
+
+## The Command Function (exec)
+
+The parameters to the exec function are designed to be useful when you have a
+large number of parameters, and to give direct access to the environment (if
+used).
+
+ gcli.addItems([{
+ name: 'echo',
+ description: 'The message to display.',
+ params: [
+ {
+ name: 'message',
+ type: 'string',
+ description: 'The message to display.'
+ }
+ ],
+ returnType: 'string',
+ exec: function(args, context) {
+ return args.message;
+ }
+ }]);
+
+The ``args`` object contains the values specified on the params section and
+provided on the command line. In this example it would contain the message for
+display as ``args.message``.
+
+The ``context`` object has the following signature:
+
+ {
+ environment: ..., // environment object passed to createTerminal()
+ exec: ..., // function to execute a command
+ update: ..., // function to alter the text of the input area
+ createView: ..., // function to help creating rich output
+ defer: ..., // function to create a deferred promise
+ }
+
+The ``environment`` object is opaque to GCLI. It can be used for providing
+arbitrary data to your commands about their environment. It is most useful
+when more than one command line exists on a page with similar commands in both
+which should act in their own ways.
+An example use for ``environment`` would be a page with several tabs, each
+containing an editor with a command line. Commands executed in those editors
+should apply to the relevant editor.
+The ``environment`` object is passed to GCLI at startup (probably in the
+``createTerminal()`` function).
+
+The ``document`` object is also passed to GCLI at startup. In some environments
+(e.g. embedded in Firefox) there is no global ``document``. This object
+provides a way to create DOM nodes.
+
+``defer()`` allows commands to execute asynchronously.
+
+
+## Returning data
+
+The command meta-data specifies the type of data returned by the command using
+the ``returnValue`` setting.
+
+``returnValue`` processing is currently functioning, but incomplete, and being
+tracked in [Bug 657595](http://bugzil.la/657595). Currently you should specify
+a ``returnType`` of ``string`` or ``html``. If using HTML, you can return
+either an HTML string or a DOM node.
+
+In the future, JSON will be strongly encouraged as the return type, with some
+formatting functions to convert the JSON to HTML.
+
+Asynchronous output is achieved using a promise created from the ``context``
+parameter: ``context.defer()``.
+
+Some examples of this is practice:
+
+ { returnType: "string" }
+ ...
+ return "example";
+
+GCLI interprets the output as a plain string. It will be escaped before display
+and available as input to other commands as a plain string.
+
+ { returnType: "html" }
+ ...
+ return "<p>Hello</p>";
+
+GCLI will interpret this as HTML, and parse it for display.
+
+ { returnType: "dom" }
+ ...
+ return util.createElement(context.document, 'div');
+
+``util.createElement`` is a utility to ensure use of the XHTML namespace in XUL
+and other XML documents. In an HTML document it's functionally equivalent to
+``context.document.createElement('div')``. If your command is likely to be used
+in Firefox or another XML environment, you should use it. You can import it
+with ``var util = require('util/util');``.
+
+GCLI will use the returned HTML element as returned. See notes on ``context``
+above.
+
+ { returnType: "number" }
+ ...
+ return 42;
+
+GCLI will display the element in a similar way to a string, but it the value
+will be available to future commands as a number.
+
+ { returnType: "date" }
+ ...
+ return new Date();
+
+ { returnType: "file" }
+ ...
+ return new File();
+
+Both these examples return data as a given type, for which a converter will
+be required before the value can be displayed. The type system is likely to
+change before this is finalized. Please contact the author for more
+information.
+
+ { returnType: "string" }
+ ...
+ var deferred = context.defer();
+ setTimeout(function() {
+ deferred.resolve("hello");
+ }, 500);
+ return deferred.promise;
+
+Errors can be signaled by throwing an exception. GCLI will display the message
+property (or the toString() value if there is no message property). (However
+see *3 principles for writing commands* above for ways to avoid doing this).
+
+
+## Specifying Types
+
+Types are generally specified by a simple string, e.g. ``'string'``. For most
+types this is enough detail. There are a number of exceptions:
+
+* Array types. We declare a parameter to be an array of things using ``[]``,
+ for example: ``number[]``.
+* Selection types. There are 3 ways to specify the options in a selection:
+ * Using a lookup map
+
+ type: {
+ name: 'selection',
+ lookup: { one:1, two:2, three:3 }
+ }
+
+ (The boolean type is effectively just a selection that uses
+ ``lookup:{ 'true': true, 'false': false }``)
+
+ * Using given strings
+
+ type: {
+ name: 'selection',
+ data: [ 'left', 'center', 'right' ]
+ }
+
+ * Using named objects, (objects with a ``name`` property)
+
+ type: {
+ name: 'selection',
+ data: [
+ { name: 'Google', url: 'http://www.google.com/' },
+ { name: 'Microsoft', url: 'http://www.microsoft.com/' },
+ { name: 'Yahoo', url: 'http://www.yahoo.com/' }
+ ]
+ }
+
+* Delegate type. It is generally best to inherit from Delegate in order to
+ provide a customization of this type. See settingValue for an example.
+
+See below for more information.
diff --git a/devtools/shared/gcli/source/docs/writing-tests.md b/devtools/shared/gcli/source/docs/writing-tests.md
new file mode 100644
index 000000000..4d42142cd
--- /dev/null
+++ b/devtools/shared/gcli/source/docs/writing-tests.md
@@ -0,0 +1,20 @@
+
+# Writing Tests
+
+There are several sources of GCLI tests and several environments in which they
+are run.
+
+The majority of GCLI tests are stored in
+[this repository](https://github.com/joewalker/gcli/) in files named like
+```./lib/gclitest/test*.js```. These tests run in Firefox, Chrome, Opera,
+and NodeJS/JsDom
+
+See [Running Tests](running-tests.md) for further details.
+
+GCLI comes with a generic unit test harness (in ```./lib/test/```) and a
+set of helpers for creating GCLI tests (in ```./lib/gclitest/helpers.js```).
+
+# GCLI tests in Firefox
+
+The build process converts the GCLI tests to run under Mochitest inside the
+Firefox unit tests. It also adds some
diff --git a/devtools/shared/gcli/source/docs/writing-types.md b/devtools/shared/gcli/source/docs/writing-types.md
new file mode 100644
index 000000000..ed2f75438
--- /dev/null
+++ b/devtools/shared/gcli/source/docs/writing-types.md
@@ -0,0 +1,106 @@
+
+# Writing Types
+
+Commands are a fundamental building block because they are what the users
+directly interacts with, however they are built on ``Type``s. There are a
+number of built in types:
+
+* string. This is a JavaScript string
+* number. A JavaScript number
+* boolean. A JavaScript boolean
+* selection. This is an selection from a number of alternatives
+* delegate. This type could change depending on other factors, but is well
+ defined when one of the conversion routines is called.
+
+There are a number of additional types defined by Pilot and GCLI as
+extensions to the ``selection`` and ``delegate`` types
+
+* setting. One of the defined settings
+* settingValue. A value that can be applied to an associated setting.
+* command. One of the defined commands
+
+Most of our types are 'static' e.g. there is only one type of 'string', however
+some types like 'selection' and 'delegate' are customizable.
+
+All types must inherit from Type and have the following methods:
+
+ /**
+ * Convert the given <tt>value</tt> to a string representation.
+ * Where possible, there should be round-tripping between values and their
+ * string representations.
+ */
+ stringify: function(value) { return 'string version of value'; },
+
+ /**
+ * Convert the given <tt>str</tt> to an instance of this type.
+ * Where possible, there should be round-tripping between values and their
+ * string representations.
+ * @return Conversion
+ */
+ parse: function(str) { return new Conversion(...); },
+
+ /**
+ * 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.
+ * <p>In old bespin, equality was based on the name. This may turn out to be
+ * important in Ace too.
+ */
+ name: 'example',
+
+In addition, defining the following function can be helpful, although Type
+contains default implementations:
+
+* nudge(value, by)
+
+Type, Conversion and Status are all declared by commands.js.
+
+The values produced by the parse function can be of any type, but if you are
+producing your own, you are strongly encouraged to include properties called
+``name`` and ``description`` where it makes sense. There are a number of
+places in GCLI where the UI will be able to provide better help to users if
+your values include these properties.
+
+
+# Writing Fields
+
+Fields are visual representations of types. For simple types like string it is
+enough to use ``<input type=...>``, however more complex types we may wish to
+provide a custom widget to allow the user to enter values of the given type.
+
+This is an example of a very simple new password field type:
+
+ function PasswordField(doc) {
+ this.doc = doc;
+ }
+
+ PasswordField.prototype = Object.create(Field.prototype);
+
+ PasswordField.prototype.createElement = function(assignment) {
+ this.assignment = assignment;
+ this.input = dom.createElement(this.doc, 'input');
+ this.input.type = 'password';
+ this.input.value = assignment.arg ? assignment.arg.text : '';
+
+ this.onKeyup = function() {
+ this.assignment.setValue(this.input.value);
+ }.bind(this);
+ this.input.addEventListener('keyup', this.onKeyup, false);
+
+ this.onChange = function() {
+ this.input.value = this.assignment.arg.text;
+ };
+ this.assignment.onAssignmentChange.add(this.onChange, this);
+
+ return this.input;
+ };
+
+ PasswordField.prototype.destroy = function() {
+ this.input.removeEventListener('keyup', this.onKeyup, false);
+ this.assignment.onAssignmentChange.remove(this.onChange, this);
+ };
+
+ PasswordField.claim = function(type) {
+ return type.name === 'password' ? Field.claim.MATCH : Field.claim.NO_MATCH;
+ };
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
+};
diff --git a/devtools/shared/gcli/templater.js b/devtools/shared/gcli/templater.js
new file mode 100644
index 000000000..c5e01b6cf
--- /dev/null
+++ b/devtools/shared/gcli/templater.js
@@ -0,0 +1,602 @@
+/*
+ * 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";
+
+/* globals document */
+
+/**
+ * For full documentation, see:
+ * https://github.com/mozilla/domtemplate/blob/master/README.md
+ */
+
+/**
+ * Begin a new templating process.
+ * @param node A DOM element or string referring to an element's id
+ * @param data Data to use in filling out the template
+ * @param options Options to customize the template processing. One of:
+ * - allowEval: boolean (default false) Basic template interpolations are
+ * either property paths (e.g. ${a.b.c.d}), or if allowEval=true then we
+ * allow arbitrary JavaScript
+ * - stack: string or array of strings (default empty array) The template
+ * engine maintains a stack of tasks to help debug where it is. This allows
+ * this stack to be prefixed with a template name
+ * - blankNullUndefined: By default DOMTemplate exports null and undefined
+ * values using the strings 'null' and 'undefined', which can be helpful for
+ * debugging, but can introduce unnecessary extra logic in a template to
+ * convert null/undefined to ''. By setting blankNullUndefined:true, this
+ * conversion is handled by DOMTemplate
+ */
+var template = function (node, data, options) {
+ let state = {
+ options: options || {},
+ // We keep a track of the nodes that we've passed through so we can keep
+ // data.__element pointing to the correct node
+ nodes: []
+ };
+
+ state.stack = state.options.stack;
+
+ if (!Array.isArray(state.stack)) {
+ if (typeof state.stack === "string") {
+ state.stack = [ options.stack ];
+ } else {
+ state.stack = [];
+ }
+ }
+
+ processNode(state, node, data);
+};
+
+if (typeof exports !== "undefined") {
+ exports.template = template;
+}
+this.template = template;
+
+/**
+ * Helper for the places where we need to act asynchronously and keep track of
+ * where we are right now
+ */
+function cloneState(state) {
+ return {
+ options: state.options,
+ stack: state.stack.slice(),
+ nodes: state.nodes.slice()
+ };
+}
+
+/**
+ * Regex used to find ${...} sections in some text.
+ * Performance note: This regex uses ( and ) to capture the 'script' for
+ * further processing. Not all of the uses of this regex use this feature so
+ * if use of the capturing group is a performance drain then we should split
+ * this regex in two.
+ */
+var TEMPLATE_REGION = /\$\{([^}]*)\}/g;
+
+/**
+ * Recursive function to walk the tree processing the attributes as it goes.
+ * @param node the node to process. If you pass a string in instead of a DOM
+ * element, it is assumed to be an id for use with document.getElementById()
+ * @param data the data to use for node processing.
+ */
+function processNode(state, node, data) {
+ if (typeof node === "string") {
+ node = document.getElementById(node);
+ }
+ if (data == null) {
+ data = {};
+ }
+ state.stack.push(node.nodeName + (node.id ? "#" + node.id : ""));
+ let pushedNode = false;
+ try {
+ // Process attributes
+ if (node.attributes && node.attributes.length) {
+ // We need to handle 'foreach' and 'if' first because they might stop
+ // some types of processing from happening, and foreach must come first
+ // because it defines new data on which 'if' might depend.
+ if (node.hasAttribute("foreach")) {
+ processForEach(state, node, data);
+ return;
+ }
+ if (node.hasAttribute("if")) {
+ if (!processIf(state, node, data)) {
+ return;
+ }
+ }
+ // Only make the node available once we know it's not going away
+ state.nodes.push(data.__element);
+ data.__element = node;
+ pushedNode = true;
+ // It's good to clean up the attributes when we've processed them,
+ // but if we do it straight away, we mess up the array index
+ let attrs = Array.prototype.slice.call(node.attributes);
+ for (let i = 0; i < attrs.length; i++) {
+ let value = attrs[i].value;
+ let name = attrs[i].name;
+
+ state.stack.push(name);
+ try {
+ if (name === "save") {
+ // Save attributes are a setter using the node
+ value = stripBraces(state, value);
+ property(state, value, data, node);
+ node.removeAttribute("save");
+ } else if (name.substring(0, 2) === "on") {
+ // If this attribute value contains only an expression
+ if (value.substring(0, 2) === "${" && value.slice(-1) === "}" &&
+ value.indexOf("${", 2) === -1) {
+ value = stripBraces(state, value);
+ let func = property(state, value, data);
+ if (typeof func === "function") {
+ node.removeAttribute(name);
+ let capture = node.hasAttribute("capture" + name.substring(2));
+ node.addEventListener(name.substring(2), func, capture);
+ if (capture) {
+ node.removeAttribute("capture" + name.substring(2));
+ }
+ } else {
+ // Attribute value is not a function - use as a DOM-L0 string
+ node.setAttribute(name, func);
+ }
+ } else {
+ // Attribute value is not a single expression use as DOM-L0
+ node.setAttribute(name, processString(state, value, data));
+ }
+ } else {
+ node.removeAttribute(name);
+ // Remove '_' prefix of attribute names so the DOM won't try
+ // to use them before we've processed the template
+ if (name.charAt(0) === "_") {
+ name = name.substring(1);
+ }
+
+ // Async attributes can only work if the whole attribute is async
+ let replacement;
+ if (value.indexOf("${") === 0 &&
+ value.charAt(value.length - 1) === "}") {
+ replacement = envEval(state, value.slice(2, -1), data, value);
+ if (replacement && typeof replacement.then === "function") {
+ node.setAttribute(name, "");
+ /* jshint loopfunc:true */
+ replacement.then(function (newValue) {
+ node.setAttribute(name, newValue);
+ }).then(null, console.error);
+ } else {
+ if (state.options.blankNullUndefined && replacement == null) {
+ replacement = "";
+ }
+ node.setAttribute(name, replacement);
+ }
+ } else {
+ node.setAttribute(name, processString(state, value, data));
+ }
+ }
+ } finally {
+ state.stack.pop();
+ }
+ }
+ }
+
+ // Loop through our children calling processNode. First clone them, so the
+ // set of nodes that we visit will be unaffected by additions or removals.
+ let childNodes = Array.prototype.slice.call(node.childNodes);
+ for (let j = 0; j < childNodes.length; j++) {
+ processNode(state, childNodes[j], data);
+ }
+
+ /* 3 === Node.TEXT_NODE */
+ if (node.nodeType === 3) {
+ processTextNode(state, node, data);
+ }
+ } finally {
+ if (pushedNode) {
+ data.__element = state.nodes.pop();
+ }
+ state.stack.pop();
+ }
+}
+
+/**
+ * Handle attribute values where the output can only be a string
+ */
+function processString(state, value, data) {
+ return value.replace(TEMPLATE_REGION, function (path) {
+ let insert = envEval(state, path.slice(2, -1), data, value);
+ return state.options.blankNullUndefined && insert == null ? "" : insert;
+ });
+}
+
+/**
+ * Handle <x if="${...}">
+ * @param node An element with an 'if' attribute
+ * @param data The data to use with envEval()
+ * @returns true if processing should continue, false otherwise
+ */
+function processIf(state, node, data) {
+ state.stack.push("if");
+ try {
+ let originalValue = node.getAttribute("if");
+ let value = stripBraces(state, originalValue);
+ let recurse = true;
+ try {
+ let reply = envEval(state, value, data, originalValue);
+ recurse = !!reply;
+ } catch (ex) {
+ handleError(state, "Error with '" + value + "'", ex);
+ recurse = false;
+ }
+ if (!recurse) {
+ node.parentNode.removeChild(node);
+ }
+ node.removeAttribute("if");
+ return recurse;
+ } finally {
+ state.stack.pop();
+ }
+}
+
+/**
+ * Handle <x foreach="param in ${array}"> and the special case of
+ * <loop foreach="param in ${array}">.
+ * This function is responsible for extracting what it has to do from the
+ * attributes, and getting the data to work on (including resolving promises
+ * in getting the array). It delegates to processForEachLoop to actually
+ * unroll the data.
+ * @param node An element with a 'foreach' attribute
+ * @param data The data to use with envEval()
+ */
+function processForEach(state, node, data) {
+ state.stack.push("foreach");
+ try {
+ let originalValue = node.getAttribute("foreach");
+ let value = originalValue;
+
+ let paramName = "param";
+ if (value.charAt(0) === "$") {
+ // No custom loop variable name. Use the default: 'param'
+ value = stripBraces(state, value);
+ } else {
+ // Extract the loop variable name from 'NAME in ${ARRAY}'
+ let nameArr = value.split(" in ");
+ paramName = nameArr[0].trim();
+ value = stripBraces(state, nameArr[1].trim());
+ }
+ node.removeAttribute("foreach");
+ try {
+ let evaled = envEval(state, value, data, originalValue);
+ let cState = cloneState(state);
+ handleAsync(evaled, node, function (reply, siblingNode) {
+ processForEachLoop(cState, reply, node, siblingNode, data, paramName);
+ });
+ node.parentNode.removeChild(node);
+ } catch (ex) {
+ handleError(state, "Error with " + value + "'", ex);
+ }
+ } finally {
+ state.stack.pop();
+ }
+}
+
+/**
+ * Called by processForEach to handle looping over the data in a foreach loop.
+ * This works with both arrays and objects.
+ * Calls processForEachMember() for each member of 'set'
+ * @param set The object containing the data to loop over
+ * @param templNode The node to copy for each set member
+ * @param sibling The sibling node to which we add things
+ * @param data the data to use for node processing
+ * @param paramName foreach loops have a name for the parameter currently being
+ * processed. The default is 'param'. e.g. <loop foreach="param in ${x}">...
+ */
+function processForEachLoop(state, set, templNode, sibling, data, paramName) {
+ if (Array.isArray(set)) {
+ set.forEach(function (member, i) {
+ processForEachMember(state, member, templNode, sibling,
+ data, paramName, "" + i);
+ });
+ } else {
+ for (let member in set) {
+ if (set.hasOwnProperty(member)) {
+ processForEachMember(state, member, templNode, sibling,
+ data, paramName, member);
+ }
+ }
+ }
+}
+
+/**
+ * Called by processForEachLoop() to resolve any promises in the array (the
+ * array itself can also be a promise, but that is resolved by
+ * processForEach()). Handle <LOOP> elements (which are taken out of the DOM),
+ * clone the template node, and pass the processing on to processNode().
+ * @param member The data item to use in templating
+ * @param templNode The node to copy for each set member
+ * @param siblingNode The parent node to which we add things
+ * @param data the data to use for node processing
+ * @param paramName The name given to 'member' by the foreach attribute
+ * @param frame A name to push on the stack for debugging
+ */
+function processForEachMember(state, member, templNode, siblingNode, data,
+ paramName, frame) {
+ state.stack.push(frame);
+ try {
+ let cState = cloneState(state);
+ handleAsync(member, siblingNode, function (reply, node) {
+ // Clone data because we can't be sure that we can safely mutate it
+ let newData = Object.create(null);
+ Object.keys(data).forEach(function (key) {
+ newData[key] = data[key];
+ });
+ newData[paramName] = reply;
+ if (node.parentNode != null) {
+ let clone;
+ if (templNode.nodeName.toLowerCase() === "loop") {
+ for (let i = 0; i < templNode.childNodes.length; i++) {
+ clone = templNode.childNodes[i].cloneNode(true);
+ node.parentNode.insertBefore(clone, node);
+ processNode(cState, clone, newData);
+ }
+ } else {
+ clone = templNode.cloneNode(true);
+ clone.removeAttribute("foreach");
+ node.parentNode.insertBefore(clone, node);
+ processNode(cState, clone, newData);
+ }
+ }
+ });
+ } finally {
+ state.stack.pop();
+ }
+}
+
+/**
+ * Take a text node and replace it with another text node with the ${...}
+ * sections parsed out. We replace the node by altering node.parentNode but
+ * we could probably use a DOM Text API to achieve the same thing.
+ * @param node The Text node to work on
+ * @param data The data to use in calls to envEval()
+ */
+function processTextNode(state, node, data) {
+ // Replace references in other attributes
+ let value = node.data;
+ // We can't use the string.replace() with function trick (see generic
+ // attribute processing in processNode()) because we need to support
+ // functions that return DOM nodes, so we can't have the conversion to a
+ // string.
+ // Instead we process the string as an array of parts. In order to split
+ // the string up, we first replace '${' with '\uF001$' and '}' with '\uF002'
+ // We can then split using \uF001 or \uF002 to get an array of strings
+ // where scripts are prefixed with $.
+ // \uF001 and \uF002 are just unicode chars reserved for private use.
+ value = value.replace(TEMPLATE_REGION, "\uF001$$$1\uF002");
+ // Split a string using the unicode chars F001 and F002.
+ let parts = value.split(/\uF001|\uF002/);
+ if (parts.length > 1) {
+ parts.forEach(function (part) {
+ if (part === null || part === undefined || part === "") {
+ return;
+ }
+ if (part.charAt(0) === "$") {
+ part = envEval(state, part.slice(1), data, node.data);
+ }
+ let cState = cloneState(state);
+ handleAsync(part, node, function (reply, siblingNode) {
+ let doc = siblingNode.ownerDocument;
+ if (reply == null) {
+ reply = cState.options.blankNullUndefined ? "" : "" + reply;
+ }
+ if (typeof reply.cloneNode === "function") {
+ // i.e. if (reply instanceof Element) { ...
+ reply = maybeImportNode(cState, reply, doc);
+ siblingNode.parentNode.insertBefore(reply, siblingNode);
+ } else if (typeof reply.item === "function" && reply.length) {
+ // NodeLists can be live, in which case maybeImportNode can
+ // remove them from the document, and thus the NodeList, which in
+ // turn breaks iteration. So first we clone the list
+ let list = Array.prototype.slice.call(reply, 0);
+ list.forEach(function (child) {
+ let imported = maybeImportNode(cState, child, doc);
+ siblingNode.parentNode.insertBefore(imported, siblingNode);
+ });
+ } else {
+ // if thing isn't a DOM element then wrap its string value in one
+ reply = doc.createTextNode(reply.toString());
+ siblingNode.parentNode.insertBefore(reply, siblingNode);
+ }
+ });
+ });
+ node.parentNode.removeChild(node);
+ }
+}
+
+/**
+ * Return node or a import of node, if it's not in the given document
+ * @param node The node that we want to be properly owned
+ * @param doc The document that the given node should belong to
+ * @return A node that belongs to the given document
+ */
+function maybeImportNode(state, node, doc) {
+ return node.ownerDocument === doc ? node : doc.importNode(node, true);
+}
+
+/**
+ * A function to handle the fact that some nodes can be promises, so we check
+ * and resolve if needed using a marker node to keep our place before calling
+ * an inserter function.
+ * @param thing The object which could be real data or a promise of real data
+ * we use it directly if it's not a promise, or resolve it if it is.
+ * @param siblingNode The element before which we insert new elements.
+ * @param inserter The function to to the insertion. If thing is not a promise
+ * then handleAsync() is just 'inserter(thing, siblingNode)'
+ */
+function handleAsync(thing, siblingNode, inserter) {
+ if (thing != null && typeof thing.then === "function") {
+ // Placeholder element to be replaced once we have the real data
+ let tempNode = siblingNode.ownerDocument.createElement("span");
+ siblingNode.parentNode.insertBefore(tempNode, siblingNode);
+ thing.then(function (delayed) {
+ inserter(delayed, tempNode);
+ if (tempNode.parentNode != null) {
+ tempNode.parentNode.removeChild(tempNode);
+ }
+ }).then(null, function (error) {
+ console.error(error.stack);
+ });
+ } else {
+ inserter(thing, siblingNode);
+ }
+}
+
+/**
+ * Warn of string does not begin '${' and end '}'
+ * @param str the string to check.
+ * @return The string stripped of ${ and }, or untouched if it does not match
+ */
+function stripBraces(state, str) {
+ if (!str.match(TEMPLATE_REGION)) {
+ handleError(state, "Expected " + str + " to match ${...}");
+ return str;
+ }
+ return str.slice(2, -1);
+}
+
+/**
+ * Combined getter and setter that works with a path through some data set.
+ * For example:
+ * <ul>
+ * <li>property(state, 'a.b', { a: { b: 99 }}); // returns 99
+ * <li>property(state, 'a', { a: { b: 99 }}); // returns { b: 99 }
+ * <li>property(state, 'a', { a: { b: 99 }}, 42); // returns 99 and alters the
+ * input data to be { a: { b: 42 }}
+ * </ul>
+ * @param path An array of strings indicating the path through the data, or
+ * a string to be cut into an array using <tt>split('.')</tt>
+ * @param data the data to use for node processing
+ * @param newValue (optional) If defined, this value will replace the
+ * original value for the data at the path specified.
+ * @return The value pointed to by <tt>path</tt> before any
+ * <tt>newValue</tt> is applied.
+ */
+function property(state, path, data, newValue) {
+ try {
+ if (typeof path === "string") {
+ path = path.split(".");
+ }
+ let value = data[path[0]];
+ if (path.length === 1) {
+ if (newValue !== undefined) {
+ data[path[0]] = newValue;
+ }
+ if (typeof value === "function") {
+ return value.bind(data);
+ }
+ return value;
+ }
+ if (!value) {
+ handleError(state, "\"" + path[0] + "\" is undefined");
+ return null;
+ }
+ return property(state, path.slice(1), value, newValue);
+ } catch (ex) {
+ handleError(state, "Path error with '" + path + "'", ex);
+ return "${" + path + "}";
+ }
+}
+
+/**
+ * Like eval, but that creates a context of the variables in <tt>env</tt> in
+ * which the script is evaluated.
+ * @param script The string to be evaluated.
+ * @param data The environment in which to eval the script.
+ * @param frame Optional debugging string in case of failure.
+ * @return The return value of the script, or the error message if the script
+ * execution failed.
+ */
+function envEval(state, script, data, frame) {
+ try {
+ state.stack.push(frame.replace(/\s+/g, " "));
+ // Detect if a script is capable of being interpreted using property()
+ if (/^[_a-zA-Z0-9.]*$/.test(script)) {
+ return property(state, script, data);
+ }
+ if (!state.options.allowEval) {
+ handleError(state, "allowEval is not set, however '" + script + "'" +
+ " can not be resolved using a simple property path.");
+ return "${" + script + "}";
+ }
+
+ // What we're looking to do is basically:
+ // with(data) { return eval(script); }
+ // except in strict mode where 'with' is banned.
+ // So we create a function which has a parameter list the same as the
+ // keys in 'data' and with 'script' as its function body.
+ // We then call this function with the values in 'data'
+ let keys = allKeys(data);
+ let func = Function.apply(null, keys.concat("return " + script));
+
+ let values = keys.map((key) => data[key]);
+ return func.apply(null, values);
+
+ // TODO: The 'with' method is different from the code above in the value
+ // of 'this' when calling functions. For example:
+ // envEval(state, 'foo()', { foo: function () { return this; } }, ...);
+ // The global for 'foo' when using 'with' is the data object. However the
+ // code above, the global is null. (Using 'func.apply(data, values)'
+ // changes 'this' in the 'foo()' frame, but not in the inside the body
+ // of 'foo', so that wouldn't help)
+ } catch (ex) {
+ handleError(state, "Template error evaluating '" + script + "'", ex);
+ return "${" + script + "}";
+ } finally {
+ state.stack.pop();
+ }
+}
+
+/**
+ * Object.keys() that respects the prototype chain
+ */
+function allKeys(data) {
+ let keys = [];
+ for (let key in data) {
+ keys.push(key);
+ }
+ return keys;
+}
+
+/**
+ * A generic way of reporting errors, for easy overloading in different
+ * environments.
+ * @param message the error message to report.
+ * @param ex optional associated exception.
+ */
+function handleError(state, message, ex) {
+ logError(message + " (In: " + state.stack.join(" > ") + ")");
+ if (ex) {
+ logError(ex);
+ }
+}
+
+/**
+ * A generic way of reporting errors, for easy overloading in different
+ * environments.
+ * @param message the error message to report.
+ */
+function logError(message) {
+ console.error(message);
+}
+
+exports.template = template;