mirror of
https://repo.dactyloidae.xyz/Dactyloidae/UXP.git
synced 2026-09-05 07:18:39 +09:00
import FIREFOX_52_6_0esr_RELEASE from mozilla-esr52 hg repo
This commit is contained in:
commit
dcd9973243
150858 changed files with 23884658 additions and 0 deletions
320
devtools/shared/gcli/commands/addon.js
Normal file
320
devtools/shared/gcli/commands/addon.js
Normal file
|
|
@ -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 : [];
|
||||
186
devtools/shared/gcli/commands/appcache.js
Normal file
186
devtools/shared/gcli/commands/appcache.js
Normal file
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
];
|
||||
219
devtools/shared/gcli/commands/calllog.js
Normal file
219
devtools/shared/gcli/commands/calllog.js
Normal file
|
|
@ -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 ]);
|
||||
}
|
||||
}
|
||||
];
|
||||
178
devtools/shared/gcli/commands/cmd.js
Normal file
178
devtools/shared/gcli/commands/cmd.js
Normal file
|
|
@ -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 ]);
|
||||
}
|
||||
}
|
||||
];
|
||||
300
devtools/shared/gcli/commands/cookie.js
Normal file
300
devtools/shared/gcli/commands/cookie.js
Normal file
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
];
|
||||
201
devtools/shared/gcli/commands/csscoverage.js
Normal file
201
devtools/shared/gcli/commands/csscoverage.js
Normal file
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
];
|
||||
77
devtools/shared/gcli/commands/folder.js
Normal file
77
devtools/shared/gcli/commands/folder.js
Normal file
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
];
|
||||
158
devtools/shared/gcli/commands/highlight.js
Normal file
158
devtools/shared/gcli/commands/highlight.js
Normal file
|
|
@ -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
|
||||
}
|
||||
];
|
||||
179
devtools/shared/gcli/commands/index.js
Normal file
179
devtools/shared/gcli/commands/index.js
Normal file
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
86
devtools/shared/gcli/commands/inject.js
Normal file
86
devtools/shared/gcli/commands/inject.js
Normal file
|
|
@ -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]);
|
||||
}
|
||||
}
|
||||
];
|
||||
134
devtools/shared/gcli/commands/jsb.js
Normal file
134
devtools/shared/gcli/commands/jsb.js
Normal file
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
];
|
||||
106
devtools/shared/gcli/commands/listen.js
Normal file
106
devtools/shared/gcli/commands/listen.js
Normal file
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
];
|
||||
83
devtools/shared/gcli/commands/mdn.js
Normal file
83
devtools/shared/gcli/commands/mdn.js
Normal file
|
|
@ -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;
|
||||
}
|
||||
}];
|
||||
112
devtools/shared/gcli/commands/measure.js
Normal file
112
devtools/shared/gcli/commands/measure.js
Normal file
|
|
@ -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};
|
||||
}
|
||||
}
|
||||
];
|
||||
56
devtools/shared/gcli/commands/media.js
Normal file
56
devtools/shared/gcli/commands/media.js
Normal file
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
];
|
||||
30
devtools/shared/gcli/commands/moz.build
Normal file
30
devtools/shared/gcli/commands/moz.build
Normal file
|
|
@ -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',
|
||||
)
|
||||
276
devtools/shared/gcli/commands/pagemod.js
Normal file
276
devtools/shared/gcli/commands/pagemod.js
Normal file
|
|
@ -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 '';
|
||||
}
|
||||
}
|
||||
];
|
||||
201
devtools/shared/gcli/commands/paintflashing.js
Normal file
201
devtools/shared/gcli/commands/paintflashing.js
Normal file
|
|
@ -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 };
|
||||
}
|
||||
}
|
||||
];
|
||||
24
devtools/shared/gcli/commands/qsa.js
Normal file
24
devtools/shared/gcli/commands/qsa.js
Normal file
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
];
|
||||
77
devtools/shared/gcli/commands/restart.js
Normal file
77
devtools/shared/gcli/commands/restart.js
Normal file
|
|
@ -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 ]);
|
||||
}
|
||||
}
|
||||
];
|
||||
110
devtools/shared/gcli/commands/rulers.js
Normal file
110
devtools/shared/gcli/commands/rulers.js
Normal file
|
|
@ -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};
|
||||
}
|
||||
}
|
||||
];
|
||||
579
devtools/shared/gcli/commands/screenshot.js
Normal file
579
devtools/shared/gcli/commands/screenshot.js
Normal file
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
328
devtools/shared/gcli/commands/security.js
Normal file
328
devtools/shared/gcli/commands/security.js
Normal file
|
|
@ -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,
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
];
|
||||
23
devtools/shared/gcli/moz.build
Normal file
23
devtools/shared/gcli/moz.build
Normal file
|
|
@ -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'
|
||||
)
|
||||
202
devtools/shared/gcli/source/LICENSE
Normal file
202
devtools/shared/gcli/source/LICENSE
Normal file
|
|
@ -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.
|
||||
102
devtools/shared/gcli/source/docs/design.md
Normal file
102
devtools/shared/gcli/source/docs/design.md
Normal file
|
|
@ -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.
|
||||
213
devtools/shared/gcli/source/docs/developing-gcli.md
Normal file
213
devtools/shared/gcli/source/docs/developing-gcli.md
Normal file
|
|
@ -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.
|
||||
|
||||
150
devtools/shared/gcli/source/docs/index.md
Normal file
150
devtools/shared/gcli/source/docs/index.md
Normal file
|
|
@ -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.
|
||||
60
devtools/shared/gcli/source/docs/running-tests.md
Normal file
60
devtools/shared/gcli/source/docs/running-tests.md
Normal file
|
|
@ -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.
|
||||
757
devtools/shared/gcli/source/docs/writing-commands.md
Normal file
757
devtools/shared/gcli/source/docs/writing-commands.md
Normal file
|
|
@ -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.
|
||||
20
devtools/shared/gcli/source/docs/writing-tests.md
Normal file
20
devtools/shared/gcli/source/docs/writing-tests.md
Normal file
|
|
@ -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
|
||||
106
devtools/shared/gcli/source/docs/writing-types.md
Normal file
106
devtools/shared/gcli/source/docs/writing-types.md
Normal file
|
|
@ -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;
|
||||
};
|
||||
2209
devtools/shared/gcli/source/lib/gcli/cli.js
Normal file
2209
devtools/shared/gcli/source/lib/gcli/cli.js
Normal file
File diff suppressed because it is too large
Load diff
59
devtools/shared/gcli/source/lib/gcli/commands/clear.js
Normal file
59
devtools/shared/gcli/source/lib/gcli/commands/clear.js
Normal file
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
];
|
||||
570
devtools/shared/gcli/source/lib/gcli/commands/commands.js
Normal file
570
devtools/shared/gcli/source/lib/gcli/commands/commands.js
Normal file
|
|
@ -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;
|
||||
62
devtools/shared/gcli/source/lib/gcli/commands/context.js
Normal file
62
devtools/shared/gcli/source/lib/gcli/commands/context.js
Normal file
|
|
@ -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 ];
|
||||
387
devtools/shared/gcli/source/lib/gcli/commands/help.js
Normal file
387
devtools/shared/gcli/source/lib/gcli/commands/help.js
Normal file
|
|
@ -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} → ${command.description}\n' +
|
||||
'</loop></pre>',
|
||||
options: { allowEval: true, stack: 'commandsData->stringView' },
|
||||
data: getHelpListData(commandsData, context)
|
||||
};
|
||||
}
|
||||
}
|
||||
];
|
||||
68
devtools/shared/gcli/source/lib/gcli/commands/mocks.js
Normal file
68
devtools/shared/gcli/source/lib/gcli/commands/mocks.js
Normal file
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
];
|
||||
16
devtools/shared/gcli/source/lib/gcli/commands/moz.build
Normal file
16
devtools/shared/gcli/source/lib/gcli/commands/moz.build
Normal file
|
|
@ -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',
|
||||
)
|
||||
93
devtools/shared/gcli/source/lib/gcli/commands/pref.js
Normal file
93
devtools/shared/gcli/source/lib/gcli/commands/pref.js
Normal file
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
];
|
||||
214
devtools/shared/gcli/source/lib/gcli/commands/preflist.js
Normal file
214
devtools/shared/gcli/source/lib/gcli/commands/preflist.js
Normal file
|
|
@ -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 ];
|
||||
215
devtools/shared/gcli/source/lib/gcli/commands/test.js
Normal file
215
devtools/shared/gcli/source/lib/gcli/commands/test.js
Normal file
|
|
@ -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' }
|
||||
};
|
||||
}
|
||||
}
|
||||
];
|
||||
157
devtools/shared/gcli/source/lib/gcli/connectors/connectors.js
Normal file
157
devtools/shared/gcli/source/lib/gcli/connectors/connectors.js
Normal file
|
|
@ -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;
|
||||
|
|
@ -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',
|
||||
)
|
||||
94
devtools/shared/gcli/source/lib/gcli/converters/basic.js
Normal file
94
devtools/shared/gcli/source/lib/gcli/converters/basic.js
Normal file
|
|
@ -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, ' ').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, ' ');
|
||||
}
|
||||
}
|
||||
];
|
||||
280
devtools/shared/gcli/source/lib/gcli/converters/converters.js
Normal file
280
devtools/shared/gcli/source/lib/gcli/converters/converters.js
Normal file
|
|
@ -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
|
||||
];
|
||||
47
devtools/shared/gcli/source/lib/gcli/converters/html.js
Normal file
47
devtools/shared/gcli/source/lib/gcli/converters/html.js
Normal file
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
];
|
||||
12
devtools/shared/gcli/source/lib/gcli/converters/moz.build
Normal file
12
devtools/shared/gcli/source/lib/gcli/converters/moz.build
Normal file
|
|
@ -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',
|
||||
)
|
||||
56
devtools/shared/gcli/source/lib/gcli/converters/terminal.js
Normal file
56
devtools/shared/gcli/source/lib/gcli/converters/terminal.js
Normal file
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
];
|
||||
96
devtools/shared/gcli/source/lib/gcli/fields/delegate.js
Normal file
96
devtools/shared/gcli/source/lib/gcli/fields/delegate.js
Normal file
|
|
@ -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
|
||||
];
|
||||
245
devtools/shared/gcli/source/lib/gcli/fields/fields.js
Normal file
245
devtools/shared/gcli/source/lib/gcli/fields/fields.js
Normal file
|
|
@ -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 ];
|
||||
11
devtools/shared/gcli/source/lib/gcli/fields/moz.build
Normal file
11
devtools/shared/gcli/source/lib/gcli/fields/moz.build
Normal file
|
|
@ -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',
|
||||
)
|
||||
124
devtools/shared/gcli/source/lib/gcli/fields/selection.js
Normal file
124
devtools/shared/gcli/source/lib/gcli/fields/selection.js
Normal file
|
|
@ -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 ];
|
||||
29
devtools/shared/gcli/source/lib/gcli/index.js
Normal file
29
devtools/shared/gcli/source/lib/gcli/index.js
Normal file
|
|
@ -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');
|
||||
};
|
||||
74
devtools/shared/gcli/source/lib/gcli/l10n.js
Normal file
74
devtools/shared/gcli/source/lib/gcli/l10n.js
Normal file
|
|
@ -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");
|
||||
};
|
||||
14
devtools/shared/gcli/source/lib/gcli/languages/command.html
Normal file
14
devtools/shared/gcli/source/lib/gcli/languages/command.html
Normal file
|
|
@ -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>
|
||||
563
devtools/shared/gcli/source/lib/gcli/languages/command.js
Normal file
563
devtools/shared/gcli/source/lib/gcli/languages/command.js
Normal file
|
|
@ -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/ /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.
|
||||
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 ];
|
||||
86
devtools/shared/gcli/source/lib/gcli/languages/javascript.js
Normal file
86
devtools/shared/gcli/source/lib/gcli/languages/javascript.js
Normal file
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
];
|
||||
179
devtools/shared/gcli/source/lib/gcli/languages/languages.js
Normal file
179
devtools/shared/gcli/source/lib/gcli/languages/languages.js
Normal file
|
|
@ -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.
|
||||
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;
|
||||
12
devtools/shared/gcli/source/lib/gcli/languages/moz.build
Normal file
12
devtools/shared/gcli/source/lib/gcli/languages/moz.build
Normal file
|
|
@ -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',
|
||||
)
|
||||
13
devtools/shared/gcli/source/lib/gcli/moz.build
Normal file
13
devtools/shared/gcli/source/lib/gcli/moz.build
Normal file
|
|
@ -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',
|
||||
)
|
||||
151
devtools/shared/gcli/source/lib/gcli/mozui/completer.js
Normal file
151
devtools/shared/gcli/source/lib/gcli/mozui/completer.js
Normal file
|
|
@ -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/ /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.
|
||||
member.className = 'gcli-in-' + member.status.toString().toLowerCase();
|
||||
}, this);
|
||||
|
||||
return data;
|
||||
});
|
||||
};
|
||||
|
||||
exports.Completer = Completer;
|
||||
657
devtools/shared/gcli/source/lib/gcli/mozui/inputter.js
Normal file
657
devtools/shared/gcli/source/lib/gcli/mozui/inputter.js
Normal file
|
|
@ -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;
|
||||
11
devtools/shared/gcli/source/lib/gcli/mozui/moz.build
Normal file
11
devtools/shared/gcli/source/lib/gcli/mozui/moz.build
Normal file
|
|
@ -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',
|
||||
)
|
||||
298
devtools/shared/gcli/source/lib/gcli/mozui/tooltip.js
Normal file
298
devtools/shared/gcli/source/lib/gcli/mozui/tooltip.js
Normal file
|
|
@ -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;
|
||||
284
devtools/shared/gcli/source/lib/gcli/settings.js
Normal file
284
devtools/shared/gcli/source/lib/gcli/settings.js
Normal file
|
|
@ -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
|
||||
});
|
||||
370
devtools/shared/gcli/source/lib/gcli/system.js
Normal file
370
devtools/shared/gcli/source/lib/gcli/system.js
Normal file
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
80
devtools/shared/gcli/source/lib/gcli/types/array.js
Normal file
80
devtools/shared/gcli/source/lib/gcli/types/array.js
Normal file
|
|
@ -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());
|
||||
}
|
||||
},
|
||||
];
|
||||
62
devtools/shared/gcli/source/lib/gcli/types/boolean.js
Normal file
62
devtools/shared/gcli/source/lib/gcli/types/boolean.js
Normal file
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
];
|
||||
255
devtools/shared/gcli/source/lib/gcli/types/command.js
Normal file
255
devtools/shared/gcli/source/lib/gcli/types/command.js
Normal file
|
|
@ -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);
|
||||
};
|
||||
248
devtools/shared/gcli/source/lib/gcli/types/date.js
Normal file
248
devtools/shared/gcli/source/lib/gcli/types/date.js
Normal file
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
];
|
||||
158
devtools/shared/gcli/source/lib/gcli/types/delegate.js
Normal file
158
devtools/shared/gcli/source/lib/gcli/types/delegate.js
Normal file
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
];
|
||||
96
devtools/shared/gcli/source/lib/gcli/types/file.js
Normal file
96
devtools/shared/gcli/source/lib/gcli/types/file.js
Normal file
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
];
|
||||
19
devtools/shared/gcli/source/lib/gcli/types/fileparser.js
Normal file
19
devtools/shared/gcli/source/lib/gcli/types/fileparser.js
Normal file
|
|
@ -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;
|
||||
522
devtools/shared/gcli/source/lib/gcli/types/javascript.js
Normal file
522
devtools/shared/gcli/source/lib/gcli/types/javascript.js
Normal file
|
|
@ -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 ];
|
||||
25
devtools/shared/gcli/source/lib/gcli/types/moz.build
Normal file
25
devtools/shared/gcli/source/lib/gcli/types/moz.build
Normal file
|
|
@ -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',
|
||||
)
|
||||
201
devtools/shared/gcli/source/lib/gcli/types/node.js
Normal file
201
devtools/shared/gcli/source/lib/gcli/types/node.js
Normal file
|
|
@ -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
|
||||
}
|
||||
];
|
||||
181
devtools/shared/gcli/source/lib/gcli/types/number.js
Normal file
181
devtools/shared/gcli/source/lib/gcli/types/number.js
Normal file
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
];
|
||||
270
devtools/shared/gcli/source/lib/gcli/types/resource.js
Normal file
270
devtools/shared/gcli/source/lib/gcli/types/resource.js
Normal file
|
|
@ -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 };
|
||||
}));
|
||||
}
|
||||
}
|
||||
];
|
||||
389
devtools/shared/gcli/source/lib/gcli/types/selection.js
Normal file
389
devtools/shared/gcli/source/lib/gcli/types/selection.js
Normal file
|
|
@ -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 ];
|
||||
62
devtools/shared/gcli/source/lib/gcli/types/setting.js
Normal file
62
devtools/shared/gcli/source/lib/gcli/types/setting.js
Normal file
|
|
@ -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';
|
||||
}
|
||||
}
|
||||
];
|
||||
92
devtools/shared/gcli/source/lib/gcli/types/string.js
Normal file
92
devtools/shared/gcli/source/lib/gcli/types/string.js
Normal file
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
];
|
||||
1146
devtools/shared/gcli/source/lib/gcli/types/types.js
Normal file
1146
devtools/shared/gcli/source/lib/gcli/types/types.js
Normal file
File diff suppressed because it is too large
Load diff
117
devtools/shared/gcli/source/lib/gcli/types/union.js
Normal file
117
devtools/shared/gcli/source/lib/gcli/types/union.js
Normal file
|
|
@ -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));
|
||||
},
|
||||
}
|
||||
];
|
||||
86
devtools/shared/gcli/source/lib/gcli/types/url.js
Normal file
86
devtools/shared/gcli/source/lib/gcli/types/url.js
Normal file
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
];
|
||||
403
devtools/shared/gcli/source/lib/gcli/ui/focus.js
Normal file
403
devtools/shared/gcli/source/lib/gcli/ui/focus.js
Normal file
|
|
@ -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;
|
||||
71
devtools/shared/gcli/source/lib/gcli/ui/history.js
Normal file
71
devtools/shared/gcli/source/lib/gcli/ui/history.js
Normal file
|
|
@ -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;
|
||||
90
devtools/shared/gcli/source/lib/gcli/ui/intro.js
Normal file
90
devtools/shared/gcli/source/lib/gcli/ui/intro.js
Normal file
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
69
devtools/shared/gcli/source/lib/gcli/ui/menu.css
Normal file
69
devtools/shared/gcli/source/lib/gcli/ui/menu.css
Normal file
|
|
@ -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;
|
||||
}
|
||||
20
devtools/shared/gcli/source/lib/gcli/ui/menu.html
Normal file
20
devtools/shared/gcli/source/lib/gcli/ui/menu.html
Normal file
|
|
@ -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>
|
||||
328
devtools/shared/gcli/source/lib/gcli/ui/menu.js
Normal file
328
devtools/shared/gcli/source/lib/gcli/ui/menu.js
Normal file
|
|
@ -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;
|
||||
15
devtools/shared/gcli/source/lib/gcli/ui/moz.build
Normal file
15
devtools/shared/gcli/source/lib/gcli/ui/moz.build
Normal file
|
|
@ -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',
|
||||
)
|
||||
87
devtools/shared/gcli/source/lib/gcli/ui/view.js
Normal file
87
devtools/shared/gcli/source/lib/gcli/ui/view.js
Normal file
|
|
@ -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;
|
||||
}
|
||||
};
|
||||
};
|
||||
20
devtools/shared/gcli/source/lib/gcli/util/domtemplate.js
Normal file
20
devtools/shared/gcli/source/lib/gcli/util/domtemplate.js
Normal file
|
|
@ -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;
|
||||
281
devtools/shared/gcli/source/lib/gcli/util/fileparser.js
Normal file
281
devtools/shared/gcli/source/lib/gcli/util/fileparser.js
Normal file
|
|
@ -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
|
||||
}
|
||||
};
|
||||
130
devtools/shared/gcli/source/lib/gcli/util/filesystem.js
Normal file
130
devtools/shared/gcli/source/lib/gcli/util/filesystem.js
Normal file
|
|
@ -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('');
|
||||
};
|
||||
230
devtools/shared/gcli/source/lib/gcli/util/host.js
Normal file
230
devtools/shared/gcli/source/lib/gcli/util/host.js
Normal file
|
|
@ -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));
|
||||
};
|
||||
80
devtools/shared/gcli/source/lib/gcli/util/l10n.js
Normal file
80
devtools/shared/gcli/source/lib/gcli/util/l10n.js
Normal file
|
|
@ -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;
|
||||
}
|
||||
};
|
||||
147
devtools/shared/gcli/source/lib/gcli/util/legacy.js
Normal file
147
devtools/shared/gcli/source/lib/gcli/util/legacy.js
Normal file
|
|
@ -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;
|
||||
};
|
||||
}
|
||||
17
devtools/shared/gcli/source/lib/gcli/util/moz.build
Normal file
17
devtools/shared/gcli/source/lib/gcli/util/moz.build
Normal file
|
|
@ -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',
|
||||
)
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue