summaryrefslogtreecommitdiff
path: root/browser/devtools/shared
diff options
context:
space:
mode:
Diffstat (limited to 'browser/devtools/shared')
-rw-r--r--browser/devtools/shared/AppCacheUtils.jsm630
-rw-r--r--browser/devtools/shared/AutocompletePopup.jsm500
-rw-r--r--browser/devtools/shared/DOMHelpers.jsm124
-rw-r--r--browser/devtools/shared/DeveloperToolbar.jsm1248
-rw-r--r--browser/devtools/shared/FloatingScrollbars.jsm126
-rw-r--r--browser/devtools/shared/Jsbeautify.jsm1303
-rw-r--r--browser/devtools/shared/LayoutHelpers.jsm384
-rw-r--r--browser/devtools/shared/Makefile.in18
-rw-r--r--browser/devtools/shared/Parser.jsm2293
-rw-r--r--browser/devtools/shared/SplitView.jsm302
-rw-r--r--browser/devtools/shared/event-emitter.js118
-rw-r--r--browser/devtools/shared/inplace-editor.js851
-rw-r--r--browser/devtools/shared/moz.build7
-rw-r--r--browser/devtools/shared/splitview.css98
-rw-r--r--browser/devtools/shared/telemetry.js259
-rw-r--r--browser/devtools/shared/test/Makefile.in42
-rw-r--r--browser/devtools/shared/test/browser_eventemitter_basic.js80
-rw-r--r--browser/devtools/shared/test/browser_layoutHelpers.html25
-rw-r--r--browser/devtools/shared/test/browser_layoutHelpers.js99
-rw-r--r--browser/devtools/shared/test/browser_layoutHelpers_iframe.html19
-rw-r--r--browser/devtools/shared/test/browser_require_basic.js140
-rw-r--r--browser/devtools/shared/test/browser_telemetry_buttonsandsidebar.js179
-rw-r--r--browser/devtools/shared/test/browser_telemetry_toolboxtabs_inspector.js110
-rw-r--r--browser/devtools/shared/test/browser_telemetry_toolboxtabs_jsdebugger.js110
-rw-r--r--browser/devtools/shared/test/browser_telemetry_toolboxtabs_jsprofiler.js110
-rw-r--r--browser/devtools/shared/test/browser_telemetry_toolboxtabs_netmonitor.js110
-rw-r--r--browser/devtools/shared/test/browser_telemetry_toolboxtabs_options.js110
-rw-r--r--browser/devtools/shared/test/browser_telemetry_toolboxtabs_styleeditor.js110
-rw-r--r--browser/devtools/shared/test/browser_telemetry_toolboxtabs_webconsole.js110
-rw-r--r--browser/devtools/shared/test/browser_templater_basic.html13
-rw-r--r--browser/devtools/shared/test/browser_templater_basic.js288
-rw-r--r--browser/devtools/shared/test/browser_toolbar_basic.html35
-rw-r--r--browser/devtools/shared/test/browser_toolbar_basic.js74
-rw-r--r--browser/devtools/shared/test/browser_toolbar_tooltip.js54
-rw-r--r--browser/devtools/shared/test/browser_toolbar_webconsole_errors_count.html32
-rw-r--r--browser/devtools/shared/test/browser_toolbar_webconsole_errors_count.js245
-rw-r--r--browser/devtools/shared/test/head.js121
-rw-r--r--browser/devtools/shared/test/leakhunt.js170
-rw-r--r--browser/devtools/shared/test/moz.build7
-rw-r--r--browser/devtools/shared/test/unit/test_undoStack.js98
-rw-r--r--browser/devtools/shared/test/unit/xpcshell.ini6
-rw-r--r--browser/devtools/shared/theme-switching.js74
-rw-r--r--browser/devtools/shared/undo.js206
-rw-r--r--browser/devtools/shared/widgets/BreadcrumbsWidget.jsm227
-rw-r--r--browser/devtools/shared/widgets/SideMenuWidget.jsm621
-rw-r--r--browser/devtools/shared/widgets/VariablesView.jsm3166
-rw-r--r--browser/devtools/shared/widgets/VariablesView.xul16
-rw-r--r--browser/devtools/shared/widgets/VariablesViewController.jsm350
-rw-r--r--browser/devtools/shared/widgets/ViewHelpers.jsm1606
-rw-r--r--browser/devtools/shared/widgets/widgets.css59
50 files changed, 17083 insertions, 0 deletions
diff --git a/browser/devtools/shared/AppCacheUtils.jsm b/browser/devtools/shared/AppCacheUtils.jsm
new file mode 100644
index 000000000..0c3579084
--- /dev/null
+++ b/browser/devtools/shared/AppCacheUtils.jsm
@@ -0,0 +1,630 @@
+/* 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/. */
+
+/**
+ * validateManifest() warns of the following errors:
+ * - No manifest specified in page
+ * - Manifest is not utf-8
+ * - Manifest mimetype not text/cache-manifest
+ * - Manifest does not begin with "CACHE MANIFEST"
+ * - Page modified since appcache last changed
+ * - Duplicate entries
+ * - Conflicting entries e.g. in both CACHE and NETWORK sections or in cache
+ * but blocked by FALLBACK namespace
+ * - Detect referenced files that are not available
+ * - Detect referenced files that have cache-control set to no-store
+ * - Wildcards used in a section other than NETWORK
+ * - Spaces in URI not replaced with %20
+ * - Completely invalid URIs
+ * - Too many dot dot slash operators
+ * - SETTINGS section is valid
+ * - Invalid section name
+ * - etc.
+ */
+
+"use strict";
+
+const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+
+let { XPCOMUtils } = Cu.import("resource://gre/modules/XPCOMUtils.jsm", {});
+let { Services } = Cu.import("resource://gre/modules/Services.jsm", {});
+let { Promise } = Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js", {});
+
+this.EXPORTED_SYMBOLS = ["AppCacheUtils"];
+
+function AppCacheUtils(documentOrUri) {
+ this._parseManifest = this._parseManifest.bind(this);
+
+ if (documentOrUri) {
+ if (typeof documentOrUri == "string") {
+ this.uri = documentOrUri;
+ }
+ if (/HTMLDocument/.test(documentOrUri.toString())) {
+ this.doc = documentOrUri;
+ }
+ }
+}
+
+AppCacheUtils.prototype = {
+ get cachePath() {
+ return "";
+ },
+
+ validateManifest: function ACU_validateManifest() {
+ let deferred = Promise.defer();
+ this.errors = [];
+ // Check for missing manifest.
+ this._getManifestURI().then(manifestURI => {
+ this.manifestURI = manifestURI;
+
+ if (!this.manifestURI) {
+ this._addError(0, "noManifest");
+ deferred.resolve(this.errors);
+ }
+
+ this._getURIInfo(this.manifestURI).then(uriInfo => {
+ this._parseManifest(uriInfo).then(() => {
+ // Sort errors by line number.
+ this.errors.sort(function(a, b) {
+ return a.line - b.line;
+ });
+ deferred.resolve(this.errors);
+ });
+ });
+ });
+
+ return deferred.promise;
+ },
+
+ _parseManifest: function ACU__parseManifest(uriInfo) {
+ let deferred = Promise.defer();
+ let manifestName = uriInfo.name;
+ let manifestLastModified = new Date(uriInfo.responseHeaders["Last-Modified"]);
+
+ if (uriInfo.charset.toLowerCase() != "utf-8") {
+ this._addError(0, "notUTF8", uriInfo.charset);
+ }
+
+ if (uriInfo.mimeType != "text/cache-manifest") {
+ this._addError(0, "badMimeType", uriInfo.mimeType);
+ }
+
+ let parser = new ManifestParser(uriInfo.text, this.manifestURI);
+ let parsed = parser.parse();
+
+ if (parsed.errors.length > 0) {
+ this.errors.push.apply(this.errors, parsed.errors);
+ }
+
+ // Check for duplicate entries.
+ let dupes = {};
+ for (let parsedUri of parsed.uris) {
+ dupes[parsedUri.uri] = dupes[parsedUri.uri] || [];
+ dupes[parsedUri.uri].push({
+ line: parsedUri.line,
+ section: parsedUri.section,
+ original: parsedUri.original
+ });
+ }
+ for (let [uri, value] of Iterator(dupes)) {
+ if (value.length > 1) {
+ this._addError(0, "duplicateURI", uri, JSON.stringify(value));
+ }
+ }
+
+ // Loop through network entries making sure that fallback and cache don't
+ // contain uris starting with the network uri.
+ for (let neturi of parsed.uris) {
+ if (neturi.section == "NETWORK") {
+ for (let parsedUri of parsed.uris) {
+ if (parsedUri.uri.startsWith(neturi.uri)) {
+ this._addError(neturi.line, "networkBlocksURI", neturi.line,
+ neturi.original, parsedUri.line, parsedUri.original,
+ parsedUri.section);
+ }
+ }
+ }
+ }
+
+ // Loop through fallback entries making sure that fallback and cache don't
+ // contain uris starting with the network uri.
+ for (let fb of parsed.fallbacks) {
+ for (let parsedUri of parsed.uris) {
+ if (parsedUri.uri.startsWith(fb.namespace)) {
+ this._addError(fb.line, "fallbackBlocksURI", fb.line,
+ fb.original, parsedUri.line, parsedUri.original,
+ parsedUri.section);
+ }
+ }
+ }
+
+ // Check that all resources exist and that their cach-control headers are
+ // not set to no-store.
+ let current = -1;
+ for (let i = 0, len = parsed.uris.length; i < len; i++) {
+ let parsedUri = parsed.uris[i];
+ this._getURIInfo(parsedUri.uri).then(uriInfo => {
+ current++;
+
+ if (uriInfo.success) {
+ // Check that the resource was not modified after the manifest was last
+ // modified. If it was then the manifest file should be refreshed.
+ let resourceLastModified =
+ new Date(uriInfo.responseHeaders["Last-Modified"]);
+
+ if (manifestLastModified < resourceLastModified) {
+ this._addError(parsedUri.line, "fileChangedButNotManifest",
+ uriInfo.name, manifestName, parsedUri.line);
+ }
+
+ // If cache-control: no-store the file will not be added to the
+ // appCache.
+ if (uriInfo.nocache) {
+ this._addError(parsedUri.line, "cacheControlNoStore",
+ parsedUri.original, parsedUri.line);
+ }
+ } else {
+ this._addError(parsedUri.line, "notAvailable",
+ parsedUri.original, parsedUri.line);
+ }
+
+ if (current == len - 1) {
+ deferred.resolve();
+ }
+ });
+ }
+
+ return deferred.promise;
+ },
+
+ _getURIInfo: function ACU__getURIInfo(uri) {
+ let inputStream = Cc["@mozilla.org/scriptableinputstream;1"]
+ .createInstance(Ci.nsIScriptableInputStream);
+ let deferred = Promise.defer();
+ let channelCharset = "";
+ let buffer = "";
+ let channel = Services.io.newChannel(uri, null, null);
+
+ // Avoid the cache:
+ channel.loadFlags |= Ci.nsIRequest.LOAD_BYPASS_CACHE;
+ channel.loadFlags |= Ci.nsIRequest.INHIBIT_CACHING;
+
+ channel.asyncOpen({
+ onStartRequest: function (request, context) {
+ // This empty method is needed in order for onDataAvailable to be
+ // called.
+ },
+
+ onDataAvailable: function (request, context, stream, offset, count) {
+ request.QueryInterface(Ci.nsIHttpChannel);
+ inputStream.init(stream);
+ buffer = buffer.concat(inputStream.read(count));
+ },
+
+ onStopRequest: function onStartRequest(request, context, statusCode) {
+ if (statusCode == 0) {
+ request.QueryInterface(Ci.nsIHttpChannel);
+
+ let result = {
+ name: request.name,
+ success: request.requestSucceeded,
+ status: request.responseStatus + " - " + request.responseStatusText,
+ charset: request.contentCharset || "utf-8",
+ mimeType: request.contentType,
+ contentLength: request.contentLength,
+ nocache: request.isNoCacheResponse() || request.isNoStoreResponse(),
+ prePath: request.URI.prePath + "/",
+ text: buffer
+ };
+
+ result.requestHeaders = {};
+ request.visitRequestHeaders(function(header, value) {
+ result.requestHeaders[header] = value;
+ });
+
+ result.responseHeaders = {};
+ request.visitResponseHeaders(function(header, value) {
+ result.responseHeaders[header] = value;
+ });
+
+ deferred.resolve(result);
+ } else {
+ deferred.resolve({
+ name: request.name,
+ success: false
+ });
+ }
+ }
+ }, null);
+ return deferred.promise;
+ },
+
+ listEntries: function ACU_show(searchTerm) {
+ if (!Services.prefs.getBoolPref("browser.cache.disk.enable")) {
+ throw new Error(l10n.GetStringFromName("cacheDisabled"));
+ }
+
+ let entries = [];
+
+ Services.cache.visitEntries({
+ visitDevice: function(deviceID, deviceInfo) {
+ return true;
+ },
+
+ visitEntry: function(deviceID, entryInfo) {
+ if (entryInfo.deviceID == "offline") {
+ let entry = {};
+ let lowerKey = entryInfo.key.toLowerCase();
+
+ if (searchTerm && lowerKey.indexOf(searchTerm.toLowerCase()) == -1) {
+ return true;
+ }
+
+ for (let [key, value] of Iterator(entryInfo)) {
+ if (key == "QueryInterface") {
+ continue;
+ }
+ if (key == "clientID") {
+ entry.key = entryInfo.key;
+ }
+ if (key == "expirationTime" || key == "lastFetched" || key == "lastModified") {
+ value = new Date(value * 1000);
+ }
+ entry[key] = value;
+ }
+ entries.push(entry);
+ }
+ return true;
+ }
+ });
+
+ if (entries.length == 0) {
+ throw new Error(l10n.GetStringFromName("noResults"));
+ }
+ return entries;
+ },
+
+ viewEntry: function ACU_viewEntry(key) {
+ let uri;
+
+ Services.cache.visitEntries({
+ visitDevice: function(deviceID, deviceInfo) {
+ return true;
+ },
+
+ visitEntry: function(deviceID, entryInfo) {
+ if (entryInfo.deviceID == "offline" && entryInfo.key == key) {
+ uri = "about:cache-entry?client=" + entryInfo.clientID +
+ "&sb=1&key=" + entryInfo.key;
+ return false;
+ }
+ return true;
+ }
+ });
+
+ if (uri) {
+ let wm = Cc["@mozilla.org/appshell/window-mediator;1"]
+ .getService(Ci.nsIWindowMediator);
+ let win = wm.getMostRecentWindow("navigator:browser");
+ win.gBrowser.selectedTab = win.gBrowser.addTab(uri);
+ } else {
+ return l10n.GetStringFromName("entryNotFound");
+ }
+ },
+
+ clearAll: function ACU_clearAll() {
+ Services.cache.evictEntries(Ci.nsICache.STORE_OFFLINE);
+ },
+
+ _getManifestURI: function ACU__getManifestURI() {
+ let deferred = Promise.defer();
+
+ let getURI = node => {
+ let htmlNode = this.doc.querySelector("html[manifest]");
+ if (htmlNode) {
+ let pageUri = this.doc.location ? this.doc.location.href : this.uri;
+ let origin = pageUri.substr(0, pageUri.lastIndexOf("/") + 1);
+ return origin + htmlNode.getAttribute("manifest");
+ }
+ };
+
+ if (this.doc) {
+ let uri = getURI(this.doc);
+ return Promise.resolve(uri);
+ } else {
+ this._getURIInfo(this.uri).then(uriInfo => {
+ if (uriInfo.success) {
+ let html = uriInfo.text;
+ let parser = _DOMParser;
+ this.doc = parser.parseFromString(html, "text/html");
+ let uri = getURI(this.doc);
+ deferred.resolve(uri);
+ } else {
+ this.errors.push({
+ line: 0,
+ msg: l10n.GetStringFromName("invalidURI")
+ });
+ }
+ });
+ }
+ return deferred.promise;
+ },
+
+ _addError: function ACU__addError(line, l10nString, ...params) {
+ let msg;
+
+ if (params) {
+ msg = l10n.formatStringFromName(l10nString, params, params.length);
+ } else {
+ msg = l10n.GetStringFromName(l10nString);
+ }
+
+ this.errors.push({
+ line: line,
+ msg: msg
+ });
+ },
+};
+
+/**
+ * We use our own custom parser because we need far more detailed information
+ * than the system manifest parser provides.
+ *
+ * @param {String} manifestText
+ * The text content of the manifest file.
+ * @param {String} manifestURI
+ * The URI of the manifest file. This is used in calculating the path of
+ * relative URIs.
+ */
+function ManifestParser(manifestText, manifestURI) {
+ this.manifestText = manifestText;
+ this.origin = manifestURI.substr(0, manifestURI.lastIndexOf("/") + 1)
+ .replace(" ", "%20");
+}
+
+ManifestParser.prototype = {
+ parse: function OCIMP_parse() {
+ let lines = this.manifestText.split(/\r?\n/);
+ let fallbacks = this.fallbacks = [];
+ let settings = this.settings = [];
+ let errors = this.errors = [];
+ let uris = this.uris = [];
+
+ this.currSection = "CACHE";
+
+ for (let i = 0; i < lines.length; i++) {
+ let text = this.text = lines[i].replace(/^\s+|\s+$/g);
+ this.currentLine = i + 1;
+
+ if (i == 0 && text != "CACHE MANIFEST") {
+ this._addError(1, "firstLineMustBeCacheManifest", 1);
+ }
+
+ // Ignore comments
+ if (/^#/.test(text) || !text.length) {
+ continue;
+ }
+
+ if (text == "CACHE MANIFEST") {
+ if (this.currentLine != 1) {
+ this._addError(this.currentLine, "cacheManifestOnlyFirstLine2",
+ this.currentLine);
+ }
+ continue;
+ }
+
+ if (this._maybeUpdateSectionName()) {
+ continue;
+ }
+
+ switch (this.currSection) {
+ case "CACHE":
+ case "NETWORK":
+ this.parseLine();
+ break;
+ case "FALLBACK":
+ this.parseFallbackLine();
+ break;
+ case "SETTINGS":
+ this.parseSettingsLine();
+ break;
+ }
+ }
+
+ return {
+ uris: uris,
+ fallbacks: fallbacks,
+ settings: settings,
+ errors: errors
+ };
+ },
+
+ parseLine: function OCIMP_parseLine() {
+ let text = this.text;
+
+ if (text.indexOf("*") != -1) {
+ if (this.currSection != "NETWORK" || text.length != 1) {
+ this._addError(this.currentLine, "asteriskInWrongSection2",
+ this.currSection, this.currentLine);
+ return;
+ }
+ }
+
+ if (/\s/.test(text)) {
+ this._addError(this.currentLine, "escapeSpaces", this.currentLine);
+ text = text.replace(/\s/g, "%20")
+ }
+
+ if (text[0] == "/") {
+ if (text.substr(0, 4) == "/../") {
+ this._addError(this.currentLine, "slashDotDotSlashBad", this.currentLine);
+ } else {
+ this.uris.push(this._wrapURI(this.origin + text.substring(1)));
+ }
+ } else if (text.substr(0, 2) == "./") {
+ this.uris.push(this._wrapURI(this.origin + text.substring(2)));
+ } else if (text.substr(0, 4) == "http") {
+ this.uris.push(this._wrapURI(text));
+ } else {
+ let origin = this.origin;
+ let path = text;
+
+ while (path.substr(0, 3) == "../" && /^https?:\/\/.*?\/.*?\//.test(origin)) {
+ let trimIdx = origin.substr(0, origin.length - 1).lastIndexOf("/") + 1;
+ origin = origin.substr(0, trimIdx);
+ path = path.substr(3);
+ }
+
+ if (path.substr(0, 3) == "../") {
+ this._addError(this.currentLine, "tooManyDotDotSlashes", this.currentLine);
+ return;
+ }
+
+ if (/^https?:\/\//.test(path)) {
+ this.uris.push(this._wrapURI(path));
+ return;
+ }
+ this.uris.push(this._wrapURI(origin + path));
+ }
+ },
+
+ parseFallbackLine: function OCIMP_parseFallbackLine() {
+ let split = this.text.split(/\s+/);
+ let origURI = this.text;
+
+ if (split.length != 2) {
+ this._addError(this.currentLine, "fallbackUseSpaces", this.currentLine);
+ return;
+ }
+
+ let [ namespace, fallback ] = split;
+
+ if (namespace.indexOf("*") != -1) {
+ this._addError(this.currentLine, "fallbackAsterisk2", this.currentLine);
+ }
+
+ if (/\s/.test(namespace)) {
+ this._addError(this.currentLine, "escapeSpaces", this.currentLine);
+ namespace = namespace.replace(/\s/g, "%20")
+ }
+
+ if (namespace.substr(0, 4) == "/../") {
+ this._addError(this.currentLine, "slashDotDotSlashBad", this.currentLine);
+ }
+
+ if (namespace.substr(0, 2) == "./") {
+ namespace = this.origin + namespace.substring(2);
+ }
+
+ if (namespace.substr(0, 4) != "http") {
+ let origin = this.origin;
+ let path = namespace;
+
+ while (path.substr(0, 3) == "../" && /^https?:\/\/.*?\/.*?\//.test(origin)) {
+ let trimIdx = origin.substr(0, origin.length - 1).lastIndexOf("/") + 1;
+ origin = origin.substr(0, trimIdx);
+ path = path.substr(3);
+ }
+
+ if (path.substr(0, 3) == "../") {
+ this._addError(this.currentLine, "tooManyDotDotSlashes", this.currentLine);
+ }
+
+ if (/^https?:\/\//.test(path)) {
+ namespace = path;
+ } else {
+ if (path[0] == "/") {
+ path = path.substring(1);
+ }
+ namespace = origin + path;
+ }
+ }
+
+ this.text = fallback;
+ this.parseLine();
+
+ this.fallbacks.push({
+ line: this.currentLine,
+ original: origURI,
+ namespace: namespace,
+ fallback: fallback
+ });
+ },
+
+ parseSettingsLine: function OCIMP_parseSettingsLine() {
+ let text = this.text;
+
+ if (this.settings.length == 1 || !/prefer-online|fast/.test(text)) {
+ this._addError(this.currentLine, "settingsBadValue", this.currentLine);
+ return;
+ }
+
+ switch (text) {
+ case "prefer-online":
+ this.settings.push(this._wrapURI(text));
+ break;
+ case "fast":
+ this.settings.push(this._wrapURI(text));
+ break;
+ }
+ },
+
+ _wrapURI: function OCIMP__wrapURI(uri) {
+ return {
+ section: this.currSection,
+ line: this.currentLine,
+ uri: uri,
+ original: this.text
+ };
+ },
+
+ _addError: function OCIMP__addError(line, l10nString, ...params) {
+ let msg;
+
+ if (params) {
+ msg = l10n.formatStringFromName(l10nString, params, params.length);
+ } else {
+ msg = l10n.GetStringFromName(l10nString);
+ }
+
+ this.errors.push({
+ line: line,
+ msg: msg
+ });
+ },
+
+ _maybeUpdateSectionName: function OCIMP__maybeUpdateSectionName() {
+ let text = this.text;
+
+ if (text == text.toUpperCase() && text.charAt(text.length - 1) == ":") {
+ text = text.substr(0, text.length - 1);
+
+ switch (text) {
+ case "CACHE":
+ case "NETWORK":
+ case "FALLBACK":
+ case "SETTINGS":
+ this.currSection = text;
+ return true;
+ default:
+ this._addError(this.currentLine,
+ "invalidSectionName", text, this.currentLine);
+ return false;
+ }
+ }
+ },
+};
+
+XPCOMUtils.defineLazyGetter(this, "l10n", function() Services.strings
+ .createBundle("chrome://browser/locale/devtools/appcacheutils.properties"));
+
+XPCOMUtils.defineLazyGetter(this, "appcacheservice", function() {
+ return Cc["@mozilla.org/network/application-cache-service;1"]
+ .getService(Ci.nsIApplicationCacheService);
+
+});
+
+XPCOMUtils.defineLazyGetter(this, "_DOMParser", function() {
+ return Cc["@mozilla.org/xmlextras/domparser;1"].createInstance(Ci.nsIDOMParser);
+});
diff --git a/browser/devtools/shared/AutocompletePopup.jsm b/browser/devtools/shared/AutocompletePopup.jsm
new file mode 100644
index 000000000..523a13e6c
--- /dev/null
+++ b/browser/devtools/shared/AutocompletePopup.jsm
@@ -0,0 +1,500 @@
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* 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/. */
+
+const Cu = Components.utils;
+
+// The XUL and XHTML namespace.
+const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+this.EXPORTED_SYMBOLS = ["AutocompletePopup"];
+
+/**
+ * Autocomplete popup UI implementation.
+ *
+ * @constructor
+ * @param nsIDOMDocument aDocument
+ * The document you want the popup attached to.
+ * @param Object aOptions
+ * An object consiting any of the following options:
+ * - panelId {String} The id for the popup panel.
+ * - listBoxId {String} The id for the richlistbox inside the panel.
+ * - position {String} The position for the popup panel.
+ * - theme {String} String related to the theme of the popup.
+ * - autoSelect {Boolean} Boolean to allow the first entry of the popup
+ * panel to be automatically selected when the popup shows.
+ * - fixedWidth {Boolean} Boolean to control dynamic width of the popup.
+ * - direction {String} The direction of the text in the panel. rtl or ltr
+ * - onSelect {String} The select event handler for the richlistbox
+ * - onClick {String} The click event handler for the richlistbox.
+ * - onKeypress {String} The keypress event handler for the richlistitems.
+ */
+this.AutocompletePopup =
+function AutocompletePopup(aDocument, aOptions = {})
+{
+ this._document = aDocument;
+
+ this.fixedWidth = aOptions.fixedWidth || false;
+ this.autoSelect = aOptions.autoSelect || false;
+ this.position = aOptions.position || "after_start";
+ this.direction = aOptions.direction || "ltr";
+
+ this.onSelect = aOptions.onSelect;
+ this.onClick = aOptions.onClick;
+ this.onKeypress = aOptions.onKeypress;
+
+ let id = aOptions.panelId || "devtools_autoCompletePopup";
+ let theme = aOptions.theme || "dark";
+ // Reuse the existing popup elements.
+ this._panel = this._document.getElementById(id);
+ if (!this._panel) {
+ this._panel = this._document.createElementNS(XUL_NS, "panel");
+ this._panel.setAttribute("id", id);
+ this._panel.className = "devtools-autocomplete-popup " + theme + "-theme";
+
+ this._panel.setAttribute("noautofocus", "true");
+ this._panel.setAttribute("level", "top");
+ if (!aOptions.onKeypress) {
+ this._panel.setAttribute("ignorekeys", "true");
+ }
+
+ let mainPopupSet = this._document.getElementById("mainPopupSet");
+ if (mainPopupSet) {
+ mainPopupSet.appendChild(this._panel);
+ }
+ else {
+ this._document.documentElement.appendChild(this._panel);
+ }
+ this._list = null;
+ }
+ else {
+ this._list = this._panel.firstChild;
+ }
+
+ if (!this._list) {
+ this._list = this._document.createElementNS(XUL_NS, "richlistbox");
+ this._panel.appendChild(this._list);
+
+ // Open and hide the panel, so we initialize the API of the richlistbox.
+ this._panel.openPopup(null, this.position, 0, 0);
+ this._panel.hidePopup();
+ }
+
+ this._list.setAttribute("flex", "1");
+ this._list.setAttribute("seltype", "single");
+
+ if (aOptions.listBoxId) {
+ this._list.setAttribute("id", aOptions.listBoxId);
+ }
+ this._list.className = "devtools-autocomplete-listbox " + theme + "-theme";
+
+ if (this.onSelect) {
+ this._list.addEventListener("select", this.onSelect, false);
+ }
+
+ if (this.onClick) {
+ this._list.addEventListener("click", this.onClick, false);
+ }
+
+ if (this.onKeypress) {
+ this._list.addEventListener("keypress", this.onKeypress, false);
+ }
+}
+
+AutocompletePopup.prototype = {
+ _document: null,
+ _panel: null,
+ _list: null,
+
+ // Event handlers.
+ onSelect: null,
+ onClick: null,
+ onKeypress: null,
+
+ /**
+ * Open the autocomplete popup panel.
+ *
+ * @param nsIDOMNode aAnchor
+ * Optional node to anchor the panel to.
+ */
+ openPopup: function AP_openPopup(aAnchor)
+ {
+ this._panel.openPopup(aAnchor, this.position, 0, 0);
+
+ if (this.autoSelect) {
+ this.selectFirstItem();
+ }
+ if (!this.fixedWidth) {
+ this._updateSize();
+ }
+ },
+
+ /**
+ * Hide the autocomplete popup panel.
+ */
+ hidePopup: function AP_hidePopup()
+ {
+ this._panel.hidePopup();
+ },
+
+ /**
+ * Check if the autocomplete popup is open.
+ */
+ get isOpen() {
+ return this._panel.state == "open";
+ },
+
+ /**
+ * Destroy the object instance. Please note that the panel DOM elements remain
+ * in the DOM, because they might still be in use by other instances of the
+ * same code. It is the responsability of the client code to perform DOM
+ * cleanup.
+ */
+ destroy: function AP_destroy()
+ {
+ if (this.isOpen) {
+ this.hidePopup();
+ }
+ this.clearItems();
+
+ if (this.onSelect) {
+ this._list.removeEventListener("select", this.onSelect, false);
+ }
+
+ if (this.onClick) {
+ this._list.removeEventListener("click", this.onClick, false);
+ }
+
+ if (this.onKeypress) {
+ this._list.removeEventListener("keypress", this.onKeypress, false);
+ }
+
+ this._document = null;
+ this._list = null;
+ this._panel = null;
+ },
+
+ /**
+ * Get the autocomplete items array.
+ *
+ * @param Number aIndex The index of the item what is wanted.
+ *
+ * @return The autocomplete item at index aIndex.
+ */
+ getItemAtIndex: function AP_getItemAtIndex(aIndex)
+ {
+ return this._list.getItemAtIndex(aIndex)._autocompleteItem;
+ },
+
+ /**
+ * Get the autocomplete items array.
+ *
+ * @return array
+ * The array of autocomplete items.
+ */
+ getItems: function AP_getItems()
+ {
+ let items = [];
+
+ Array.forEach(this._list.childNodes, function(aItem) {
+ items.push(aItem._autocompleteItem);
+ });
+
+ return items;
+ },
+
+ /**
+ * Set the autocomplete items list, in one go.
+ *
+ * @param array aItems
+ * The list of items you want displayed in the popup list.
+ */
+ setItems: function AP_setItems(aItems)
+ {
+ this.clearItems();
+ aItems.forEach(this.appendItem, this);
+
+ // Make sure that the new content is properly fitted by the XUL richlistbox.
+ if (this.isOpen) {
+ if (this.autoSelect) {
+ this.selectFirstItem();
+ }
+ if (!this.fixedWidth) {
+ this._updateSize();
+ }
+ }
+ },
+
+ /**
+ * Selects the first item of the richlistbox. Note that first item here is the
+ * item closes to the input element, which means that 0th index if position is
+ * below, and last index if position is above.
+ */
+ selectFirstItem: function AP_selectFirstItem()
+ {
+ if (this.position.contains("before")) {
+ this.selectedIndex = this.itemCount - 1;
+ }
+ else {
+ this.selectedIndex = 0;
+ }
+ },
+
+ /**
+ * Update the panel size to fit the content.
+ *
+ * @private
+ */
+ _updateSize: function AP__updateSize()
+ {
+ // We need the dispatch to allow the content to reflow. Attempting to
+ // update the richlistbox size too early does not work.
+ Services.tm.currentThread.dispatch({ run: () => {
+ if (!this._panel) {
+ return;
+ }
+ this._list.width = this._panel.clientWidth + this._scrollbarWidth;
+ // Height change is required, otherwise the panel is drawn at an offset
+ // the first time.
+ this._list.height = this._list.clientHeight;
+ // This brings the panel back at right position.
+ this._list.top = 0;
+ // Changing panel height might make the selected item out of view, so
+ // bring it back to view.
+ this._list.ensureIndexIsVisible(this._list.selectedIndex);
+ }}, 0);
+ },
+
+ /**
+ * Clear all the items from the autocomplete list.
+ */
+ clearItems: function AP_clearItems()
+ {
+ // Reset the selectedIndex to -1 before clearing the list
+ this.selectedIndex = -1;
+
+ while (this._list.hasChildNodes()) {
+ this._list.removeChild(this._list.firstChild);
+ }
+
+ if (!this.fixedWidth) {
+ // Reset the panel and list dimensions. New dimensions are calculated when
+ // a new set of items is added to the autocomplete popup.
+ this._list.width = "";
+ this._list.height = "";
+ this._panel.width = "";
+ this._panel.height = "";
+ this._panel.top = "";
+ this._panel.left = "";
+ }
+ },
+
+ /**
+ * Getter for the index of the selected item.
+ *
+ * @type number
+ */
+ get selectedIndex() {
+ return this._list.selectedIndex;
+ },
+
+ /**
+ * Setter for the selected index.
+ *
+ * @param number aIndex
+ * The number (index) of the item you want to select in the list.
+ */
+ set selectedIndex(aIndex) {
+ this._list.selectedIndex = aIndex;
+ if (this.isOpen && this._list.ensureIndexIsVisible) {
+ this._list.ensureIndexIsVisible(this._list.selectedIndex);
+ }
+ },
+
+ /**
+ * Getter for the selected item.
+ * @type object
+ */
+ get selectedItem() {
+ return this._list.selectedItem ?
+ this._list.selectedItem._autocompleteItem : null;
+ },
+
+ /**
+ * Setter for the selected item.
+ *
+ * @param object aItem
+ * The object you want selected in the list.
+ */
+ set selectedItem(aItem) {
+ this._list.selectedItem = this._findListItem(aItem);
+ if (this.isOpen) {
+ this._list.ensureIndexIsVisible(this._list.selectedIndex);
+ }
+ },
+
+ /**
+ * Append an item into the autocomplete list.
+ *
+ * @param object aItem
+ * The item you want appended to the list.
+ * The item object can have the following properties:
+ * - label {String} Property which is used as the displayed value.
+ * - preLabel {String} [Optional] The String that will be displayed
+ * before the label indicating that this is the already
+ * present text in the input box, and label is the text
+ * that will be auto completed. When this property is
+ * present, |preLabel.length| starting characters will be
+ * removed from label.
+ * - count {Number} [Optional] The number to represent the count of
+ * autocompleted label.
+ */
+ appendItem: function AP_appendItem(aItem)
+ {
+ let listItem = this._document.createElementNS(XUL_NS, "richlistitem");
+ if (this.direction) {
+ listItem.setAttribute("dir", this.direction);
+ }
+ let label = this._document.createElementNS(XUL_NS, "label");
+ label.setAttribute("value", aItem.label);
+ label.setAttribute("class", "autocomplete-value");
+ if (aItem.preLabel) {
+ let preDesc = this._document.createElementNS(XUL_NS, "label");
+ preDesc.setAttribute("value", aItem.preLabel);
+ preDesc.setAttribute("class", "initial-value");
+ listItem.appendChild(preDesc);
+ label.setAttribute("value", aItem.label.slice(aItem.preLabel.length));
+ }
+ listItem.appendChild(label);
+ if (aItem.count && aItem.count > 1) {
+ let countDesc = this._document.createElementNS(XUL_NS, "label");
+ countDesc.setAttribute("value", aItem.count);
+ countDesc.setAttribute("flex", "1");
+ countDesc.setAttribute("class", "autocomplete-count");
+ listItem.appendChild(countDesc);
+ }
+ listItem._autocompleteItem = aItem;
+
+ this._list.appendChild(listItem);
+ },
+
+ /**
+ * Find the richlistitem element that belongs to an item.
+ *
+ * @private
+ *
+ * @param object aItem
+ * The object you want found in the list.
+ *
+ * @return nsIDOMNode|null
+ * The nsIDOMNode that belongs to the given item object. This node is
+ * the richlistitem element.
+ */
+ _findListItem: function AP__findListItem(aItem)
+ {
+ for (let i = 0; i < this._list.childNodes.length; i++) {
+ let child = this._list.childNodes[i];
+ if (child._autocompleteItem == aItem) {
+ return child;
+ }
+ }
+ return null;
+ },
+
+ /**
+ * Remove an item from the popup list.
+ *
+ * @param object aItem
+ * The item you want removed.
+ */
+ removeItem: function AP_removeItem(aItem)
+ {
+ let item = this._findListItem(aItem);
+ if (!item) {
+ throw new Error("Item not found!");
+ }
+ this._list.removeChild(item);
+ },
+
+ /**
+ * Getter for the number of items in the popup.
+ * @type number
+ */
+ get itemCount() {
+ return this._list.childNodes.length;
+ },
+
+ /**
+ * Select the next item in the list.
+ *
+ * @return object
+ * The newly selected item object.
+ */
+ selectNextItem: function AP_selectNextItem()
+ {
+ if (this.selectedIndex < (this.itemCount - 1)) {
+ this.selectedIndex++;
+ }
+ else {
+ this.selectedIndex = -1;
+ }
+
+ return this.selectedItem;
+ },
+
+ /**
+ * Select the previous item in the list.
+ *
+ * @return object
+ * The newly selected item object.
+ */
+ selectPreviousItem: function AP_selectPreviousItem()
+ {
+ if (this.selectedIndex > -1) {
+ this.selectedIndex--;
+ }
+ else {
+ this.selectedIndex = this.itemCount - 1;
+ }
+
+ return this.selectedItem;
+ },
+
+ /**
+ * Focuses the richlistbox.
+ */
+ focus: function AP_focus()
+ {
+ this._list.focus();
+ },
+
+ /**
+ * Determine the scrollbar width in the current document.
+ *
+ * @private
+ */
+ get _scrollbarWidth()
+ {
+ if (this.__scrollbarWidth) {
+ return this.__scrollbarWidth;
+ }
+
+ let hbox = this._document.createElementNS(XUL_NS, "hbox");
+ hbox.setAttribute("style", "height: 0%; overflow: hidden");
+
+ let scrollbar = this._document.createElementNS(XUL_NS, "scrollbar");
+ scrollbar.setAttribute("orient", "vertical");
+ hbox.appendChild(scrollbar);
+
+ this._document.documentElement.appendChild(hbox);
+ this.__scrollbarWidth = scrollbar.clientWidth;
+ this._document.documentElement.removeChild(hbox);
+
+ return this.__scrollbarWidth;
+ },
+};
+
diff --git a/browser/devtools/shared/DOMHelpers.jsm b/browser/devtools/shared/DOMHelpers.jsm
new file mode 100644
index 000000000..754632ff9
--- /dev/null
+++ b/browser/devtools/shared/DOMHelpers.jsm
@@ -0,0 +1,124 @@
+/* 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/. */
+
+this.EXPORTED_SYMBOLS = ["DOMHelpers"];
+
+/**
+ * DOMHelpers
+ * Makes DOM traversal easier. Goes through iframes.
+ *
+ * @constructor
+ * @param nsIDOMWindow aWindow
+ * The content window, owning the document to traverse.
+ */
+this.DOMHelpers = function DOMHelpers(aWindow) {
+ this.window = aWindow;
+};
+
+DOMHelpers.prototype = {
+ getParentObject: function Helpers_getParentObject(node)
+ {
+ let parentNode = node ? node.parentNode : null;
+
+ if (!parentNode) {
+ // Documents have no parentNode; Attr, Document, DocumentFragment, Entity,
+ // and Notation. top level windows have no parentNode
+ if (node && node == this.window.Node.DOCUMENT_NODE) {
+ // document type
+ if (node.defaultView) {
+ let embeddingFrame = node.defaultView.frameElement;
+ if (embeddingFrame)
+ return embeddingFrame.parentNode;
+ }
+ }
+ // a Document object without a parentNode or window
+ return null; // top level has no parent
+ }
+
+ if (parentNode.nodeType == this.window.Node.DOCUMENT_NODE) {
+ if (parentNode.defaultView) {
+ return parentNode.defaultView.frameElement;
+ }
+ // parent is document element, but no window at defaultView.
+ return null;
+ }
+
+ if (!parentNode.localName)
+ return null;
+
+ return parentNode;
+ },
+
+ getChildObject: function Helpers_getChildObject(node, index, previousSibling,
+ showTextNodesWithWhitespace)
+ {
+ if (!node)
+ return null;
+
+ if (node.contentDocument) {
+ // then the node is a frame
+ if (index == 0) {
+ return node.contentDocument.documentElement; // the node's HTMLElement
+ }
+ return null;
+ }
+
+ if (node.getSVGDocument) {
+ let svgDocument = node.getSVGDocument();
+ if (svgDocument) {
+ // then the node is a frame
+ if (index == 0) {
+ return svgDocument.documentElement; // the node's SVGElement
+ }
+ return null;
+ }
+ }
+
+ let child = null;
+ if (previousSibling) // then we are walking
+ child = this.getNextSibling(previousSibling);
+ else
+ child = this.getFirstChild(node);
+
+ if (showTextNodesWithWhitespace)
+ return child;
+
+ for (; child; child = this.getNextSibling(child)) {
+ if (!this.isWhitespaceText(child))
+ return child;
+ }
+
+ return null; // we have no children worth showing.
+ },
+
+ getFirstChild: function Helpers_getFirstChild(node)
+ {
+ let SHOW_ALL = Components.interfaces.nsIDOMNodeFilter.SHOW_ALL;
+ this.treeWalker = node.ownerDocument.createTreeWalker(node,
+ SHOW_ALL, null);
+ return this.treeWalker.firstChild();
+ },
+
+ getNextSibling: function Helpers_getNextSibling(node)
+ {
+ let next = this.treeWalker.nextSibling();
+
+ if (!next)
+ delete this.treeWalker;
+
+ return next;
+ },
+
+ isWhitespaceText: function Helpers_isWhitespaceText(node)
+ {
+ return node.nodeType == this.window.Node.TEXT_NODE &&
+ !/[^\s]/.exec(node.nodeValue);
+ },
+
+ destroy: function Helpers_destroy()
+ {
+ delete this.window;
+ delete this.treeWalker;
+ }
+};
diff --git a/browser/devtools/shared/DeveloperToolbar.jsm b/browser/devtools/shared/DeveloperToolbar.jsm
new file mode 100644
index 000000000..f5b19139c
--- /dev/null
+++ b/browser/devtools/shared/DeveloperToolbar.jsm
@@ -0,0 +1,1248 @@
+/* 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";
+
+this.EXPORTED_SYMBOLS = [ "DeveloperToolbar", "CommandUtils" ];
+
+const NS_XHTML = "http://www.w3.org/1999/xhtml";
+const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource:///modules/devtools/Commands.jsm");
+
+const Node = Ci.nsIDOMNode;
+
+XPCOMUtils.defineLazyModuleGetter(this, "console",
+ "resource://gre/modules/devtools/Console.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "gcli",
+ "resource://gre/modules/devtools/gcli.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "CmdCommands",
+ "resource:///modules/devtools/BuiltinCommands.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "ConsoleServiceListener",
+ "resource://gre/modules/devtools/WebConsoleUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "PluralForm",
+ "resource://gre/modules/PluralForm.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "devtools",
+ "resource://gre/modules/devtools/Loader.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "require",
+ "resource://gre/modules/devtools/Require.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "EventEmitter",
+ "resource:///modules/devtools/shared/event-emitter.js");
+
+XPCOMUtils.defineLazyGetter(this, "prefBranch", function() {
+ let prefService = Cc["@mozilla.org/preferences-service;1"]
+ .getService(Ci.nsIPrefService);
+ return prefService.getBranch(null)
+ .QueryInterface(Ci.nsIPrefBranch2);
+});
+
+XPCOMUtils.defineLazyGetter(this, "toolboxStrings", function () {
+ return Services.strings.createBundle("chrome://browser/locale/devtools/toolbox.properties");
+});
+
+let Telemetry = devtools.require("devtools/shared/telemetry");
+
+const converters = require("gcli/converters");
+
+/**
+ * A collection of utilities to help working with commands
+ */
+let CommandUtils = {
+ /**
+ * Read a toolbarSpec from preferences
+ * @param aPref The name of the preference to read
+ */
+ getCommandbarSpec: function CU_getCommandbarSpec(aPref) {
+ let value = prefBranch.getComplexValue(aPref, Ci.nsISupportsString).data;
+ return JSON.parse(value);
+ },
+
+ /**
+ * A toolbarSpec is an array of buttonSpecs. A buttonSpec is an array of
+ * strings each of which is a GCLI command (including args if needed).
+ *
+ * Warning: this method uses the unload event of the window that owns the
+ * buttons that are of type checkbox. this means that we don't properly
+ * unregister event handlers until the window is destroyed.
+ */
+ createButtons: function CU_createButtons(toolbarSpec, target, document, requisition) {
+ let reply = [];
+
+ toolbarSpec.forEach(function(buttonSpec) {
+ let button = document.createElement("toolbarbutton");
+ reply.push(button);
+
+ if (typeof buttonSpec == "string") {
+ buttonSpec = { typed: buttonSpec };
+ }
+ // Ask GCLI to parse the typed string (doesn't execute it)
+ requisition.update(buttonSpec.typed);
+
+ // Ignore invalid commands
+ let command = requisition.commandAssignment.value;
+ if (command == null) {
+ // TODO: Have a broken icon
+ // button.icon = 'Broken';
+ button.setAttribute("label", "X");
+ button.setAttribute("tooltip", "Unknown command: " + buttonSpec.typed);
+ button.setAttribute("disabled", "true");
+ }
+ else {
+ if (command.buttonId != null) {
+ button.id = command.buttonId;
+ }
+ if (command.buttonClass != null) {
+ button.className = command.buttonClass;
+ }
+ if (command.tooltipText != null) {
+ button.setAttribute("tooltiptext", command.tooltipText);
+ }
+ else if (command.description != null) {
+ button.setAttribute("tooltiptext", command.description);
+ }
+
+ button.addEventListener("click", function() {
+ requisition.update(buttonSpec.typed);
+ //if (requisition.getStatus() == Status.VALID) {
+ requisition.exec();
+ /*
+ }
+ else {
+ console.error('incomplete commands not yet supported');
+ }
+ */
+ }, false);
+
+ // Allow the command button to be toggleable
+ if (command.state) {
+ button.setAttribute("autocheck", false);
+ let onChange = function(event, eventTab) {
+ if (eventTab == target.tab) {
+ if (command.state.isChecked(target)) {
+ button.setAttribute("checked", true);
+ }
+ else if (button.hasAttribute("checked")) {
+ button.removeAttribute("checked");
+ }
+ }
+ };
+ command.state.onChange(target, onChange);
+ onChange(null, target.tab);
+ document.defaultView.addEventListener("unload", function() {
+ command.state.offChange(target, onChange);
+ }, false);
+ }
+ }
+ });
+
+ requisition.update('');
+
+ return reply;
+ },
+
+ /**
+ * A helper function to create the environment object that is passed to
+ * GCLI commands.
+ */
+ createEnvironment: function(chromeDocument, contentDocument) {
+ let environment = {
+ chromeDocument: chromeDocument,
+ chromeWindow: chromeDocument.defaultView,
+
+ document: contentDocument,
+ window: contentDocument != null ? contentDocument.defaultView : undefined
+ };
+
+ Object.defineProperty(environment, "target", {
+ get: function() {
+ let tab = chromeDocument.defaultView.getBrowser().selectedTab;
+ return devtools.TargetFactory.forTab(tab);
+ },
+ enumerable: true
+ });
+
+ return environment;
+ },
+};
+
+this.CommandUtils = CommandUtils;
+
+/**
+ * Due to a number of panel bugs we need a way to check if we are running on
+ * Linux. See the comments for TooltipPanel and OutputPanel for further details.
+ *
+ * When bug 780102 is fixed all isLinux checks can be removed and we can revert
+ * to using panels.
+ */
+XPCOMUtils.defineLazyGetter(this, "isLinux", function () {
+ return OS == "Linux";
+});
+
+XPCOMUtils.defineLazyGetter(this, "OS", function () {
+ let os = Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime).OS;
+ return os;
+});
+
+/**
+ * A component to manage the global developer toolbar, which contains a GCLI
+ * and buttons for various developer tools.
+ * @param aChromeWindow The browser window to which this toolbar is attached
+ * @param aToolbarElement See browser.xul:<toolbar id="developer-toolbar">
+ */
+this.DeveloperToolbar = function DeveloperToolbar(aChromeWindow, aToolbarElement)
+{
+ this._chromeWindow = aChromeWindow;
+
+ this._element = aToolbarElement;
+ this._element.hidden = true;
+ this._doc = this._element.ownerDocument;
+
+ this._telemetry = new Telemetry();
+ this._lastState = NOTIFICATIONS.HIDE;
+ this._pendingShowCallback = undefined;
+ this._pendingHide = false;
+ this._errorsCount = {};
+ this._warningsCount = {};
+ this._errorListeners = {};
+ this._errorCounterButton = this._doc
+ .getElementById("developer-toolbar-toolbox-button");
+ this._errorCounterButton._defaultTooltipText =
+ this._errorCounterButton.getAttribute("tooltiptext");
+
+ EventEmitter.decorate(this);
+
+ try {
+ CmdCommands.refreshAutoCommands(aChromeWindow);
+ }
+ catch (ex) {
+ console.error(ex);
+ }
+}
+
+/**
+ * Inspector notifications dispatched through the nsIObserverService
+ */
+const NOTIFICATIONS = {
+ /** DeveloperToolbar.show() has been called, and we're working on it */
+ LOAD: "developer-toolbar-load",
+
+ /** DeveloperToolbar.show() has completed */
+ SHOW: "developer-toolbar-show",
+
+ /** DeveloperToolbar.hide() has been called */
+ HIDE: "developer-toolbar-hide"
+};
+
+/**
+ * Attach notification constants to the object prototype so tests etc can
+ * use them without needing to import anything
+ */
+DeveloperToolbar.prototype.NOTIFICATIONS = NOTIFICATIONS;
+
+/**
+ * Is the toolbar open?
+ */
+Object.defineProperty(DeveloperToolbar.prototype, 'visible', {
+ get: function DT_visible() {
+ return !this._element.hidden;
+ },
+ enumerable: true
+});
+
+let _gSequenceId = 0;
+
+/**
+ * Getter for a unique ID.
+ */
+Object.defineProperty(DeveloperToolbar.prototype, 'sequenceId', {
+ get: function DT_visible() {
+ return _gSequenceId++;
+ },
+ enumerable: true
+});
+
+/**
+ * Called from browser.xul in response to menu-click or keyboard shortcut to
+ * toggle the toolbar
+ */
+DeveloperToolbar.prototype.toggle = function DT_toggle()
+{
+ if (this.visible) {
+ this.hide();
+ } else {
+ this.show(true);
+ }
+};
+
+/**
+ * Called from browser.xul in response to menu-click or keyboard shortcut to
+ * toggle the toolbar
+ */
+DeveloperToolbar.prototype.focus = function DT_focus()
+{
+ if (this.visible) {
+ this._input.focus();
+ } else {
+ this.show(true);
+ }
+};
+
+/**
+ * Called from browser.xul in response to menu-click or keyboard shortcut to
+ * toggle the toolbar
+ */
+DeveloperToolbar.prototype.focusToggle = function DT_focusToggle()
+{
+ if (this.visible) {
+ // If we have focus then the active element is the HTML input contained
+ // inside the xul input element
+ let active = this._chromeWindow.document.activeElement;
+ let position = this._input.compareDocumentPosition(active);
+ if (position & Node.DOCUMENT_POSITION_CONTAINED_BY) {
+ this.hide();
+ }
+ else {
+ this._input.focus();
+ }
+ } else {
+ this.show(true);
+ }
+};
+
+/**
+ * Even if the user has not clicked on 'Got it' in the intro, we only show it
+ * once per session.
+ * Warning this is slightly messed up because this.DeveloperToolbar is not the
+ * same as this.DeveloperToolbar when in browser.js context.
+ */
+DeveloperToolbar.introShownThisSession = false;
+
+/**
+ * Show the developer toolbar
+ * @param aCallback show events can be asynchronous. If supplied aCallback will
+ * be called when the DeveloperToolbar is visible
+ */
+DeveloperToolbar.prototype.show = function DT_show(aFocus, aCallback)
+{
+ if (this._lastState != NOTIFICATIONS.HIDE) {
+ return;
+ }
+
+ Services.prefs.setBoolPref("devtools.toolbar.visible", true);
+
+ this._telemetry.toolOpened("developertoolbar");
+
+ this._notify(NOTIFICATIONS.LOAD);
+ this._pendingShowCallback = aCallback;
+ this._pendingHide = false;
+
+ let checkLoad = function() {
+ if (this.tooltipPanel && this.tooltipPanel.loaded &&
+ this.outputPanel && this.outputPanel.loaded) {
+ this._onload(aFocus);
+ }
+ }.bind(this);
+
+ this._input = this._doc.querySelector(".gclitoolbar-input-node");
+ this.tooltipPanel = new TooltipPanel(this._doc, this._input, checkLoad);
+ this.outputPanel = new OutputPanel(this, checkLoad);
+};
+
+/**
+ * Initializing GCLI can only be done when we've got content windows to write
+ * to, so this needs to be done asynchronously.
+ */
+DeveloperToolbar.prototype._onload = function DT_onload(aFocus)
+{
+ this._doc.getElementById("Tools:DevToolbar").setAttribute("checked", "true");
+
+ let contentDocument = this._chromeWindow.getBrowser().contentDocument;
+
+ this.display = gcli.createDisplay({
+ contentDocument: contentDocument,
+ chromeDocument: this._doc,
+ chromeWindow: this._chromeWindow,
+ hintElement: this.tooltipPanel.hintElement,
+ inputElement: this._input,
+ completeElement: this._doc.querySelector(".gclitoolbar-complete-node"),
+ backgroundElement: this._doc.querySelector(".gclitoolbar-stack-node"),
+ outputDocument: this.outputPanel.document,
+ environment: CommandUtils.createEnvironment(this._doc, contentDocument),
+ tooltipClass: 'gcliterm-tooltip',
+ eval: null,
+ scratchpad: null
+ });
+
+ this.display.focusManager.addMonitoredElement(this.outputPanel._frame);
+ this.display.focusManager.addMonitoredElement(this._element);
+
+ this.display.onVisibilityChange.add(this.outputPanel._visibilityChanged,
+ this.outputPanel);
+ this.display.onVisibilityChange.add(this.tooltipPanel._visibilityChanged,
+ this.tooltipPanel);
+ this.display.onOutput.add(this.outputPanel._outputChanged, this.outputPanel);
+
+ let tabbrowser = this._chromeWindow.getBrowser();
+ tabbrowser.tabContainer.addEventListener("TabSelect", this, false);
+ tabbrowser.tabContainer.addEventListener("TabClose", this, false);
+ tabbrowser.addEventListener("load", this, true);
+ tabbrowser.addEventListener("beforeunload", this, true);
+
+ this._initErrorsCount(tabbrowser.selectedTab);
+
+ this._element.hidden = false;
+
+ if (aFocus) {
+ this._input.focus();
+ }
+
+ this._notify(NOTIFICATIONS.SHOW);
+ if (this._pendingShowCallback) {
+ this._pendingShowCallback.call();
+ this._pendingShowCallback = undefined;
+ }
+
+ // If a hide event happened while we were loading, then we need to hide.
+ // We could make this check earlier, but then cleanup would be complex so
+ // we're being inefficient for now.
+ if (this._pendingHide) {
+ this.hide();
+ return;
+ }
+
+ if (!DeveloperToolbar.introShownThisSession) {
+ this.display.maybeShowIntro();
+ DeveloperToolbar.introShownThisSession = true;
+ }
+};
+
+/**
+ * Initialize the listeners needed for tracking the number of errors for a given
+ * tab.
+ *
+ * @private
+ * @param nsIDOMNode aTab the xul:tab for which you want to track the number of
+ * errors.
+ */
+DeveloperToolbar.prototype._initErrorsCount = function DT__initErrorsCount(aTab)
+{
+ let tabId = aTab.linkedPanel;
+ if (tabId in this._errorsCount) {
+ this._updateErrorsCount();
+ return;
+ }
+
+ let window = aTab.linkedBrowser.contentWindow;
+ let listener = new ConsoleServiceListener(window, {
+ onConsoleServiceMessage: this._onPageError.bind(this, tabId),
+ });
+ listener.init();
+
+ this._errorListeners[tabId] = listener;
+ this._errorsCount[tabId] = 0;
+ this._warningsCount[tabId] = 0;
+
+ let messages = listener.getCachedMessages();
+ messages.forEach(this._onPageError.bind(this, tabId));
+
+ this._updateErrorsCount();
+};
+
+/**
+ * Stop the listeners needed for tracking the number of errors for a given
+ * tab.
+ *
+ * @private
+ * @param nsIDOMNode aTab the xul:tab for which you want to stop tracking the
+ * number of errors.
+ */
+DeveloperToolbar.prototype._stopErrorsCount = function DT__stopErrorsCount(aTab)
+{
+ let tabId = aTab.linkedPanel;
+ if (!(tabId in this._errorsCount) || !(tabId in this._warningsCount)) {
+ this._updateErrorsCount();
+ return;
+ }
+
+ this._errorListeners[tabId].destroy();
+ delete this._errorListeners[tabId];
+ delete this._errorsCount[tabId];
+ delete this._warningsCount[tabId];
+
+ this._updateErrorsCount();
+};
+
+/**
+ * Hide the developer toolbar.
+ */
+DeveloperToolbar.prototype.hide = function DT_hide()
+{
+ if (this._lastState == NOTIFICATIONS.HIDE) {
+ return;
+ }
+
+ if (this._lastState == NOTIFICATIONS.LOAD) {
+ this._pendingHide = true;
+ return;
+ }
+
+ this._element.hidden = true;
+
+ Services.prefs.setBoolPref("devtools.toolbar.visible", false);
+
+ this._doc.getElementById("Tools:DevToolbar").setAttribute("checked", "false");
+ this.destroy();
+
+ this._telemetry.toolClosed("developertoolbar");
+ this._notify(NOTIFICATIONS.HIDE);
+};
+
+/**
+ * Hide the developer toolbar
+ */
+DeveloperToolbar.prototype.destroy = function DT_destroy()
+{
+ if (this._lastState == NOTIFICATIONS.HIDE) {
+ return;
+ }
+
+ let tabbrowser = this._chromeWindow.getBrowser();
+ tabbrowser.tabContainer.removeEventListener("TabSelect", this, false);
+ tabbrowser.tabContainer.removeEventListener("TabClose", this, false);
+ tabbrowser.removeEventListener("load", this, true);
+ tabbrowser.removeEventListener("beforeunload", this, true);
+
+ Array.prototype.forEach.call(tabbrowser.tabs, this._stopErrorsCount, this);
+
+ this.display.focusManager.removeMonitoredElement(this.outputPanel._frame);
+ this.display.focusManager.removeMonitoredElement(this._element);
+
+ this.display.onVisibilityChange.remove(this.outputPanel._visibilityChanged, this.outputPanel);
+ this.display.onVisibilityChange.remove(this.tooltipPanel._visibilityChanged, this.tooltipPanel);
+ this.display.onOutput.remove(this.outputPanel._outputChanged, this.outputPanel);
+ this.display.destroy();
+ this.outputPanel.destroy();
+ this.tooltipPanel.destroy();
+ delete this._input;
+
+ // We could "delete this.display" etc if we have hard-to-track-down memory
+ // leaks as a belt-and-braces approach, however this prevents our DOM node
+ // hunter from looking in all the nooks and crannies, so it's better if we
+ // can be leak-free without
+ /*
+ delete this.display;
+ delete this.outputPanel;
+ delete this.tooltipPanel;
+ */
+
+ this._lastState = NOTIFICATIONS.HIDE;
+};
+
+/**
+ * Utility for sending notifications
+ * @param aTopic a NOTIFICATION constant
+ */
+DeveloperToolbar.prototype._notify = function DT_notify(aTopic)
+{
+ this._lastState = aTopic;
+
+ let data = { toolbar: this };
+ data.wrappedJSObject = data;
+ Services.obs.notifyObservers(data, aTopic, null);
+};
+
+/**
+ * Update various parts of the UI when the current tab changes
+ * @param aEvent
+ */
+DeveloperToolbar.prototype.handleEvent = function DT_handleEvent(aEvent)
+{
+ if (aEvent.type == "TabSelect" || aEvent.type == "load") {
+ if (this.visible) {
+ let contentDocument = this._chromeWindow.getBrowser().contentDocument;
+
+ this.display.reattach({
+ contentDocument: contentDocument,
+ chromeWindow: this._chromeWindow,
+ environment: CommandUtils.createEnvironment(this._doc, contentDocument),
+ });
+
+ if (aEvent.type == "TabSelect") {
+ this._initErrorsCount(aEvent.target);
+ }
+ }
+ }
+ else if (aEvent.type == "TabClose") {
+ this._stopErrorsCount(aEvent.target);
+ }
+ else if (aEvent.type == "beforeunload") {
+ this._onPageBeforeUnload(aEvent);
+ }
+};
+
+/**
+ * Count a page error received for the currently selected tab. This
+ * method counts the JavaScript exceptions received and CSS errors/warnings.
+ *
+ * @private
+ * @param string aTabId the ID of the tab from where the page error comes.
+ * @param object aPageError the page error object received from the
+ * PageErrorListener.
+ */
+DeveloperToolbar.prototype._onPageError =
+function DT__onPageError(aTabId, aPageError)
+{
+ if (aPageError.category == "CSS Parser" ||
+ aPageError.category == "CSS Loader") {
+ return;
+ }
+ if ((aPageError.flags & aPageError.warningFlag) ||
+ (aPageError.flags & aPageError.strictFlag)) {
+ this._warningsCount[aTabId]++;
+ } else {
+ this._errorsCount[aTabId]++;
+ }
+ this._updateErrorsCount(aTabId);
+};
+
+/**
+ * The |beforeunload| event handler. This function resets the errors count when
+ * a different page starts loading.
+ *
+ * @private
+ * @param nsIDOMEvent aEvent the beforeunload DOM event.
+ */
+DeveloperToolbar.prototype._onPageBeforeUnload =
+function DT__onPageBeforeUnload(aEvent)
+{
+ let window = aEvent.target.defaultView;
+ if (window.top !== window) {
+ return;
+ }
+
+ let tabs = this._chromeWindow.getBrowser().tabs;
+ Array.prototype.some.call(tabs, function(aTab) {
+ if (aTab.linkedBrowser.contentWindow === window) {
+ let tabId = aTab.linkedPanel;
+ if (tabId in this._errorsCount || tabId in this._warningsCount) {
+ this._errorsCount[tabId] = 0;
+ this._warningsCount[tabId] = 0;
+ this._updateErrorsCount(tabId);
+ }
+ return true;
+ }
+ return false;
+ }, this);
+};
+
+/**
+ * Update the page errors count displayed in the Web Console button for the
+ * currently selected tab.
+ *
+ * @private
+ * @param string [aChangedTabId] Optional. The tab ID that had its page errors
+ * count changed. If this is provided and it doesn't match the currently
+ * selected tab, then the button is not updated.
+ */
+DeveloperToolbar.prototype._updateErrorsCount =
+function DT__updateErrorsCount(aChangedTabId)
+{
+ let tabId = this._chromeWindow.getBrowser().selectedTab.linkedPanel;
+ if (aChangedTabId && tabId != aChangedTabId) {
+ return;
+ }
+
+ let errors = this._errorsCount[tabId];
+ let warnings = this._warningsCount[tabId];
+ let btn = this._errorCounterButton;
+ if (errors) {
+ let errorsText = toolboxStrings
+ .GetStringFromName("toolboxToggleButton.errors");
+ errorsText = PluralForm.get(errors, errorsText).replace("#1", errors);
+
+ let warningsText = toolboxStrings
+ .GetStringFromName("toolboxToggleButton.warnings");
+ warningsText = PluralForm.get(warnings, warningsText).replace("#1", warnings);
+
+ let tooltiptext = toolboxStrings
+ .formatStringFromName("toolboxToggleButton.tooltip",
+ [errorsText, warningsText], 2);
+
+ btn.setAttribute("error-count", errors);
+ btn.setAttribute("tooltiptext", tooltiptext);
+ } else {
+ btn.removeAttribute("error-count");
+ btn.setAttribute("tooltiptext", btn._defaultTooltipText);
+ }
+
+ this.emit("errors-counter-updated");
+};
+
+/**
+ * Reset the errors counter for the given tab.
+ *
+ * @param nsIDOMElement aTab The xul:tab for which you want to reset the page
+ * errors counters.
+ */
+DeveloperToolbar.prototype.resetErrorsCount =
+function DT_resetErrorsCount(aTab)
+{
+ let tabId = aTab.linkedPanel;
+ if (tabId in this._errorsCount || tabId in this._warningsCount) {
+ this._errorsCount[tabId] = 0;
+ this._warningsCount[tabId] = 0;
+ this._updateErrorsCount(tabId);
+ }
+};
+
+/**
+ * Panel to handle command line output.
+ *
+ * There is a tooltip bug on Windows and OSX that prevents tooltips from being
+ * positioned properly (bug 786975). There is a Gnome panel bug on Linux that
+ * causes ugly focus issues (https://bugzilla.gnome.org/show_bug.cgi?id=621848).
+ * We now use a tooltip on Linux and a panel on OSX & Windows.
+ *
+ * If a panel has no content and no height it is not shown when openPopup is
+ * called on Windows and OSX (bug 692348) ... this prevents the panel from
+ * appearing the first time it is shown. Setting the panel's height to 1px
+ * before calling openPopup works around this issue as we resize it ourselves
+ * anyway.
+ *
+ * @param aChromeDoc document from which we can pull the parts we need.
+ * @param aInput the input element that should get focus.
+ * @param aLoadCallback called when the panel is loaded properly.
+ */
+function OutputPanel(aDevToolbar, aLoadCallback)
+{
+ this._devtoolbar = aDevToolbar;
+ this._input = this._devtoolbar._input;
+ this._toolbar = this._devtoolbar._doc.getElementById("developer-toolbar");
+
+ this._loadCallback = aLoadCallback;
+
+ /*
+ <tooltip|panel id="gcli-output"
+ noautofocus="true"
+ noautohide="true"
+ class="gcli-panel">
+ <html:iframe xmlns:html="http://www.w3.org/1999/xhtml"
+ id="gcli-output-frame"
+ src="chrome://browser/content/devtools/commandlineoutput.xhtml"
+ sandbox="allow-same-origin"/>
+ </tooltip|panel>
+ */
+
+ // TODO: Switch back from tooltip to panel when metacity focus issue is fixed:
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=780102
+ this._panel = this._devtoolbar._doc.createElement(isLinux ? "tooltip" : "panel");
+
+ this._panel.id = "gcli-output";
+ this._panel.classList.add("gcli-panel");
+
+ if (isLinux) {
+ this.canHide = false;
+ this._onpopuphiding = this._onpopuphiding.bind(this);
+ this._panel.addEventListener("popuphiding", this._onpopuphiding, true);
+ } else {
+ this._panel.setAttribute("noautofocus", "true");
+ this._panel.setAttribute("noautohide", "true");
+
+ // Bug 692348: On Windows and OSX if a panel has no content and no height
+ // openPopup fails to display it. Setting the height to 1px alows the panel
+ // to be displayed before has content or a real height i.e. the first time
+ // it is displayed.
+ this._panel.setAttribute("height", "1px");
+ }
+
+ this._toolbar.parentElement.insertBefore(this._panel, this._toolbar);
+
+ this._frame = this._devtoolbar._doc.createElementNS(NS_XHTML, "iframe");
+ this._frame.id = "gcli-output-frame";
+ this._frame.setAttribute("src", "chrome://browser/content/devtools/commandlineoutput.xhtml");
+ this._frame.setAttribute("sandbox", "allow-same-origin");
+ this._panel.appendChild(this._frame);
+
+ this.displayedOutput = undefined;
+
+ this._onload = this._onload.bind(this);
+ this._update = this._update.bind(this);
+ this._frame.addEventListener("load", this._onload, true);
+
+ this.loaded = false;
+}
+
+/**
+ * Wire up the element from the iframe, and inform the _loadCallback.
+ */
+OutputPanel.prototype._onload = function OP_onload()
+{
+ this._frame.removeEventListener("load", this._onload, true);
+ delete this._onload;
+
+ this.document = this._frame.contentDocument;
+
+ this._div = this.document.getElementById("gcli-output-root");
+ this._div.classList.add('gcli-row-out');
+ this._div.setAttribute('aria-live', 'assertive');
+
+ let styles = this._toolbar.ownerDocument.defaultView
+ .getComputedStyle(this._toolbar);
+ this._div.setAttribute("dir", styles.direction);
+
+ this.loaded = true;
+ if (this._loadCallback) {
+ this._loadCallback();
+ delete this._loadCallback;
+ }
+};
+
+/**
+ * Prevent the popup from hiding if it is not permitted via this.canHide.
+ */
+OutputPanel.prototype._onpopuphiding = function OP_onpopuphiding(aEvent)
+{
+ // TODO: When we switch back from tooltip to panel we can remove this hack:
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=780102
+ if (isLinux && !this.canHide) {
+ aEvent.preventDefault();
+ }
+};
+
+/**
+ * Display the OutputPanel.
+ */
+OutputPanel.prototype.show = function OP_show()
+{
+ if (isLinux) {
+ this.canHide = false;
+ }
+
+ // We need to reset the iframe size in order for future size calculations to
+ // be correct
+ this._frame.style.minHeight = this._frame.style.maxHeight = 0;
+ this._frame.style.minWidth = 0;
+
+ this._panel.openPopup(this._input, "before_start", 0, 0, false, false, null);
+ this._resize();
+
+ this._input.focus();
+};
+
+/**
+ * Internal helper to set the height of the output panel to fit the available
+ * content;
+ */
+OutputPanel.prototype._resize = function CLP_resize()
+{
+ if (this._panel == null || this.document == null || !this._panel.state == "closed") {
+ return
+ }
+
+ // Set max panel width to match any content with a max of the width of the
+ // browser window.
+ let maxWidth = this._panel.ownerDocument.documentElement.clientWidth;
+
+ // Adjust max width according to OS.
+ // We'd like to put this in CSS but we can't:
+ // body { width: calc(min(-5px, max-content)); }
+ // #_panel { max-width: -5px; }
+ switch(OS) {
+ case "Linux":
+ maxWidth -= 5;
+ break;
+ case "Darwin":
+ maxWidth -= 25;
+ break;
+ case "WINNT":
+ maxWidth -= 5;
+ break;
+ }
+
+ this.document.body.style.width = "-moz-max-content";
+ let style = this._frame.contentWindow.getComputedStyle(this.document.body);
+ let frameWidth = parseInt(style.width, 10);
+ let width = Math.min(maxWidth, frameWidth);
+ this.document.body.style.width = width + "px";
+
+ // Set the width of the iframe.
+ this._frame.style.minWidth = width + "px";
+ this._panel.style.maxWidth = maxWidth + "px";
+
+ // browserAdjustment is used to correct the panel height according to the
+ // browsers borders etc.
+ const browserAdjustment = 15;
+
+ // Set max panel height to match any content with a max of the height of the
+ // browser window.
+ let maxHeight =
+ this._panel.ownerDocument.documentElement.clientHeight - browserAdjustment;
+ let height = Math.min(maxHeight, this.document.documentElement.scrollHeight);
+
+ // Set the height of the iframe. Setting iframe.height does not work.
+ this._frame.style.minHeight = this._frame.style.maxHeight = height + "px";
+
+ // Set the height and width of the panel to match the iframe.
+ this._panel.sizeTo(width, height);
+
+ // Move the panel to the correct position in the case that it has been
+ // positioned incorrectly.
+ let screenX = this._input.boxObject.screenX;
+ let screenY = this._toolbar.boxObject.screenY;
+ this._panel.moveTo(screenX, screenY - height);
+};
+
+/**
+ * Called by GCLI when a command is executed.
+ */
+OutputPanel.prototype._outputChanged = function OP_outputChanged(aEvent)
+{
+ if (aEvent.output.hidden) {
+ return;
+ }
+
+ this.remove();
+
+ this.displayedOutput = aEvent.output;
+ this.displayedOutput.onClose.add(this.remove, this);
+
+ if (this.displayedOutput.completed) {
+ this._update();
+ }
+ else {
+ this.displayedOutput.promise.then(this._update, this._update)
+ .then(null, console.error);
+ }
+};
+
+/**
+ * Called when displayed Output says it's changed or from outputChanged, which
+ * happens when there is a new displayed Output.
+ */
+OutputPanel.prototype._update = function OP_update()
+{
+ // destroy has been called, bail out
+ if (this._div == null) {
+ return;
+ }
+
+ // Empty this._div
+ while (this._div.hasChildNodes()) {
+ this._div.removeChild(this._div.firstChild);
+ }
+
+ if (this.displayedOutput.data != null) {
+ let requisition = this._devtoolbar.display.requisition;
+ let nodePromise = converters.convert(this.displayedOutput.data,
+ this.displayedOutput.type, 'dom',
+ requisition.conversionContext);
+ nodePromise.then(function(node) {
+ while (this._div.hasChildNodes()) {
+ this._div.removeChild(this._div.firstChild);
+ }
+
+ var links = node.ownerDocument.querySelectorAll('*[href]');
+ for (var i = 0; i < links.length; i++) {
+ links[i].setAttribute('target', '_blank');
+ }
+
+ this._div.appendChild(node);
+ }.bind(this));
+ this.show();
+ }
+};
+
+/**
+ * Detach listeners from the currently displayed Output.
+ */
+OutputPanel.prototype.remove = function OP_remove()
+{
+ if (isLinux) {
+ this.canHide = true;
+ }
+
+ if (this._panel && this._panel.hidePopup) {
+ this._panel.hidePopup();
+ }
+
+ if (this.displayedOutput) {
+ this.displayedOutput.onClose.remove(this.remove, this);
+ delete this.displayedOutput;
+ }
+};
+
+/**
+ * Detach listeners from the currently displayed Output.
+ */
+OutputPanel.prototype.destroy = function OP_destroy()
+{
+ this.remove();
+
+ this._panel.removeEventListener("popuphiding", this._onpopuphiding, true);
+
+ this._panel.removeChild(this._frame);
+ this._toolbar.parentElement.removeChild(this._panel);
+
+ delete this._devtoolbar;
+ delete this._input;
+ delete this._toolbar;
+ delete this._onload;
+ delete this._onpopuphiding;
+ delete this._panel;
+ delete this._frame;
+ delete this._content;
+ delete this._div;
+ delete this.document;
+};
+
+/**
+ * Called by GCLI to indicate that we should show or hide one either the
+ * tooltip panel or the output panel.
+ */
+OutputPanel.prototype._visibilityChanged = function OP_visibilityChanged(aEvent)
+{
+ if (aEvent.outputVisible === true) {
+ // this.show is called by _outputChanged
+ } else {
+ if (isLinux) {
+ this.canHide = true;
+ }
+ this._panel.hidePopup();
+ }
+};
+
+
+/**
+ * Panel to handle tooltips.
+ *
+ * There is a tooltip bug on Windows and OSX that prevents tooltips from being
+ * positioned properly (bug 786975). There is a Gnome panel bug on Linux that
+ * causes ugly focus issues (https://bugzilla.gnome.org/show_bug.cgi?id=621848).
+ * We now use a tooltip on Linux and a panel on OSX & Windows.
+ *
+ * If a panel has no content and no height it is not shown when openPopup is
+ * called on Windows and OSX (bug 692348) ... this prevents the panel from
+ * appearing the first time it is shown. Setting the panel's height to 1px
+ * before calling openPopup works around this issue as we resize it ourselves
+ * anyway.
+ *
+ * @param aChromeDoc document from which we can pull the parts we need.
+ * @param aInput the input element that should get focus.
+ * @param aLoadCallback called when the panel is loaded properly.
+ */
+function TooltipPanel(aChromeDoc, aInput, aLoadCallback)
+{
+ this._input = aInput;
+ this._toolbar = aChromeDoc.getElementById("developer-toolbar");
+ this._dimensions = { start: 0, end: 0 };
+
+ this._onload = this._onload.bind(this);
+ this._loadCallback = aLoadCallback;
+ /*
+ <tooltip|panel id="gcli-tooltip"
+ type="arrow"
+ noautofocus="true"
+ noautohide="true"
+ class="gcli-panel">
+ <html:iframe xmlns:html="http://www.w3.org/1999/xhtml"
+ id="gcli-tooltip-frame"
+ src="chrome://browser/content/devtools/commandlinetooltip.xhtml"
+ flex="1"
+ sandbox="allow-same-origin"/>
+ </tooltip|panel>
+ */
+
+ // TODO: Switch back from tooltip to panel when metacity focus issue is fixed:
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=780102
+ this._panel = aChromeDoc.createElement(isLinux ? "tooltip" : "panel");
+
+ this._panel.id = "gcli-tooltip";
+ this._panel.classList.add("gcli-panel");
+
+ if (isLinux) {
+ this.canHide = false;
+ this._onpopuphiding = this._onpopuphiding.bind(this);
+ this._panel.addEventListener("popuphiding", this._onpopuphiding, true);
+ } else {
+ this._panel.setAttribute("noautofocus", "true");
+ this._panel.setAttribute("noautohide", "true");
+
+ // Bug 692348: On Windows and OSX if a panel has no content and no height
+ // openPopup fails to display it. Setting the height to 1px alows the panel
+ // to be displayed before has content or a real height i.e. the first time
+ // it is displayed.
+ this._panel.setAttribute("height", "1px");
+ }
+
+ this._toolbar.parentElement.insertBefore(this._panel, this._toolbar);
+
+ this._frame = aChromeDoc.createElementNS(NS_XHTML, "iframe");
+ this._frame.id = "gcli-tooltip-frame";
+ this._frame.setAttribute("src", "chrome://browser/content/devtools/commandlinetooltip.xhtml");
+ this._frame.setAttribute("flex", "1");
+ this._frame.setAttribute("sandbox", "allow-same-origin");
+ this._panel.appendChild(this._frame);
+
+ this._frame.addEventListener("load", this._onload, true);
+
+ this.loaded = false;
+}
+
+/**
+ * Wire up the element from the iframe, and inform the _loadCallback.
+ */
+TooltipPanel.prototype._onload = function TP_onload()
+{
+ this._frame.removeEventListener("load", this._onload, true);
+
+ this.document = this._frame.contentDocument;
+ this.hintElement = this.document.getElementById("gcli-tooltip-root");
+ this._connector = this.document.getElementById("gcli-tooltip-connector");
+
+ let styles = this._toolbar.ownerDocument.defaultView
+ .getComputedStyle(this._toolbar);
+ this.hintElement.setAttribute("dir", styles.direction);
+
+ this.loaded = true;
+
+ if (this._loadCallback) {
+ this._loadCallback();
+ delete this._loadCallback;
+ }
+};
+
+/**
+ * Prevent the popup from hiding if it is not permitted via this.canHide.
+ */
+TooltipPanel.prototype._onpopuphiding = function TP_onpopuphiding(aEvent)
+{
+ // TODO: When we switch back from tooltip to panel we can remove this hack:
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=780102
+ if (isLinux && !this.canHide) {
+ aEvent.preventDefault();
+ }
+};
+
+/**
+ * Display the TooltipPanel.
+ */
+TooltipPanel.prototype.show = function TP_show(aDimensions)
+{
+ if (!aDimensions) {
+ aDimensions = { start: 0, end: 0 };
+ }
+ this._dimensions = aDimensions;
+
+ // This is nasty, but displaying the panel causes it to re-flow, which can
+ // change the size it should be, so we need to resize the iframe after the
+ // panel has displayed
+ this._panel.ownerDocument.defaultView.setTimeout(function() {
+ this._resize();
+ }.bind(this), 0);
+
+ if (isLinux) {
+ this.canHide = false;
+ }
+
+ this._resize();
+ this._panel.openPopup(this._input, "before_start", aDimensions.start * 10, 0, false, false, null);
+ this._input.focus();
+};
+
+/**
+ * One option is to spend lots of time taking an average width of characters
+ * in the current font, dynamically, and weighting for the frequency of use of
+ * various characters, or even to render the given string off screen, and then
+ * measure the width.
+ * Or we could do this...
+ */
+const AVE_CHAR_WIDTH = 4.5;
+
+/**
+ * Display the TooltipPanel.
+ */
+TooltipPanel.prototype._resize = function TP_resize()
+{
+ if (this._panel == null || this.document == null || !this._panel.state == "closed") {
+ return
+ }
+
+ let offset = 10 + Math.floor(this._dimensions.start * AVE_CHAR_WIDTH);
+ this._panel.style.marginLeft = offset + "px";
+
+ /*
+ // Bug 744906: UX review - Not sure if we want this code to fatten connector
+ // with param width
+ let width = Math.floor(this._dimensions.end * AVE_CHAR_WIDTH);
+ width = Math.min(width, 100);
+ width = Math.max(width, 10);
+ this._connector.style.width = width + "px";
+ */
+
+ this._frame.height = this.document.body.scrollHeight;
+};
+
+/**
+ * Hide the TooltipPanel.
+ */
+TooltipPanel.prototype.remove = function TP_remove()
+{
+ if (isLinux) {
+ this.canHide = true;
+ }
+ if (this._panel && this._panel.hidePopup) {
+ this._panel.hidePopup();
+ }
+};
+
+/**
+ * Hide the TooltipPanel.
+ */
+TooltipPanel.prototype.destroy = function TP_destroy()
+{
+ this.remove();
+
+ this._panel.removeEventListener("popuphiding", this._onpopuphiding, true);
+
+ this._panel.removeChild(this._frame);
+ this._toolbar.parentElement.removeChild(this._panel);
+
+ delete this._connector;
+ delete this._dimensions;
+ delete this._input;
+ delete this._onload;
+ delete this._onpopuphiding;
+ delete this._panel;
+ delete this._frame;
+ delete this._toolbar;
+ delete this._content;
+ delete this.document;
+ delete this.hintElement;
+};
+
+/**
+ * Called by GCLI to indicate that we should show or hide one either the
+ * tooltip panel or the output panel.
+ */
+TooltipPanel.prototype._visibilityChanged = function TP_visibilityChanged(aEvent)
+{
+ if (aEvent.tooltipVisible === true) {
+ this.show(aEvent.dimensions);
+ } else {
+ if (isLinux) {
+ this.canHide = true;
+ }
+ this._panel.hidePopup();
+ }
+};
diff --git a/browser/devtools/shared/FloatingScrollbars.jsm b/browser/devtools/shared/FloatingScrollbars.jsm
new file mode 100644
index 000000000..a47a784bd
--- /dev/null
+++ b/browser/devtools/shared/FloatingScrollbars.jsm
@@ -0,0 +1,126 @@
+/* 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 { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+
+this.EXPORTED_SYMBOLS = [ "switchToFloatingScrollbars", "switchToNativeScrollbars" ];
+
+Cu.import("resource://gre/modules/Services.jsm");
+
+let URL = Services.io.newURI("chrome://browser/skin/devtools/floating-scrollbars.css", null, null);
+
+let trackedTabs = new WeakMap();
+
+/**
+ * Switch to floating scrollbars, à la mobile.
+ *
+ * @param aTab the targeted tab.
+ *
+ */
+this.switchToFloatingScrollbars = function switchToFloatingScrollbars(aTab) {
+ let mgr = trackedTabs.get(aTab);
+ if (!mgr) {
+ mgr = new ScrollbarManager(aTab);
+ }
+ mgr.switchToFloating();
+}
+
+/**
+ * Switch to original native scrollbars.
+ *
+ * @param aTab the targeted tab.
+ *
+ */
+this.switchToNativeScrollbars = function switchToNativeScrollbars(aTab) {
+ let mgr = trackedTabs.get(aTab);
+ if (mgr) {
+ mgr.reset();
+ }
+}
+
+function ScrollbarManager(aTab) {
+ trackedTabs.set(aTab, this);
+
+ this.attachedTab = aTab;
+ this.attachedBrowser = aTab.linkedBrowser;
+
+ this.reset = this.reset.bind(this);
+ this.switchToFloating = this.switchToFloating.bind(this);
+
+ this.attachedTab.addEventListener("TabClose", this.reset, true);
+ this.attachedBrowser.addEventListener("DOMContentLoaded", this.switchToFloating, true);
+}
+
+ScrollbarManager.prototype = {
+ get win() {
+ return this.attachedBrowser.contentWindow;
+ },
+
+ /*
+ * Change the look of the scrollbars.
+ */
+ switchToFloating: function() {
+ let windows = this.getInnerWindows(this.win);
+ windows.forEach(this.injectStyleSheet);
+ this.forceStyle();
+ },
+
+
+ /*
+ * Reset the look of the scrollbars.
+ */
+ reset: function() {
+ let windows = this.getInnerWindows(this.win);
+ windows.forEach(this.removeStyleSheet);
+ this.forceStyle(this.attachedBrowser);
+ this.attachedBrowser.removeEventListener("DOMContentLoaded", this.switchToFloating, true);
+ this.attachedTab.removeEventListener("TabClose", this.reset, true);
+ trackedTabs.delete(this.attachedTab);
+ },
+
+ /*
+ * Toggle the display property of the window to force the style to be applied.
+ */
+ forceStyle: function() {
+ let parentWindow = this.attachedBrowser.ownerDocument.defaultView;
+ let display = parentWindow.getComputedStyle(this.attachedBrowser).display; // Save display value
+ this.attachedBrowser.style.display = "none";
+ parentWindow.getComputedStyle(this.attachedBrowser).display; // Flush
+ this.attachedBrowser.style.display = display; // Restore
+ },
+
+ /*
+ * return all the window objects present in the hiearchy of a window.
+ */
+ getInnerWindows: function(win) {
+ let iframes = win.document.querySelectorAll("iframe");
+ let innerWindows = [];
+ for (let iframe of iframes) {
+ innerWindows = innerWindows.concat(this.getInnerWindows(iframe.contentWindow));
+ }
+ return [win].concat(innerWindows);
+ },
+
+ /*
+ * Append the new scrollbar style.
+ */
+ injectStyleSheet: function(win) {
+ let winUtils = win.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
+ try {
+ winUtils.loadSheet(URL, win.AGENT_SHEET);
+ }catch(e) {}
+ },
+
+ /*
+ * Remove the injected stylesheet.
+ */
+ removeStyleSheet: function(win) {
+ let winUtils = win.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
+ try {
+ winUtils.removeSheet(URL, win.AGENT_SHEET);
+ }catch(e) {}
+ },
+}
diff --git a/browser/devtools/shared/Jsbeautify.jsm b/browser/devtools/shared/Jsbeautify.jsm
new file mode 100644
index 000000000..4171d5746
--- /dev/null
+++ b/browser/devtools/shared/Jsbeautify.jsm
@@ -0,0 +1,1303 @@
+/*jslint onevar: false, plusplus: false */
+/*jshint curly:true, eqeqeq:true, laxbreak:true, noempty:false */
+/*
+
+ JS Beautifier
+---------------
+
+
+ Written by Einar Lielmanis, <einar@jsbeautifier.org>
+ http://jsbeautifier.org/
+
+ Originally converted to javascript by Vital, <vital76@gmail.com>
+ "End braces on own line" added by Chris J. Shull, <chrisjshull@gmail.com>
+
+ You are free to use this in any way you want, in case you find this useful or working for you.
+
+ Usage:
+ js_beautify(js_source_text);
+ js_beautify(js_source_text, options);
+
+ The options are:
+ indent_size (default 4) - indentation size,
+ indent_char (default space) - character to indent with,
+ preserve_newlines (default true) - whether existing line breaks should be preserved,
+ max_preserve_newlines (default unlimited) - maximum number of line breaks to be preserved in one chunk,
+
+ jslint_happy (default false) - if true, then jslint-stricter mode is enforced.
+
+ jslint_happy !jslint_happy
+ ---------------------------------
+ function () function()
+
+ brace_style (default "collapse") - "collapse" | "expand" | "end-expand" | "expand-strict"
+ put braces on the same line as control statements (default), or put braces on own line (Allman / ANSI style), or just put end braces on own line.
+
+ expand-strict: put brace on own line even in such cases:
+
+ var a =
+ {
+ a: 5,
+ b: 6
+ }
+ This mode may break your scripts - e.g "return { a: 1 }" will be broken into two lines, so beware.
+
+ space_before_conditional (default true) - should the space before conditional statement be added, "if(true)" vs "if (true)",
+
+ unescape_strings (default false) - should printable characters in strings encoded in \xNN notation be unescaped, "example" vs "\x65\x78\x61\x6d\x70\x6c\x65"
+
+ e.g
+
+ js_beautify(js_source_text, {
+ 'indent_size': 1,
+ 'indent_char': '\t'
+ });
+
+
+*/
+
+this.EXPORTED_SYMBOLS = ["js_beautify"];
+
+this.js_beautify = function js_beautify(js_source_text, options) {
+
+ var input, output, token_text, last_type, last_text, last_last_text, last_word, flags, flag_store, indent_string;
+ var whitespace, wordchar, punct, parser_pos, line_starters, digits;
+ var prefix, token_type, do_block_just_closed;
+ var wanted_newline, just_added_newline, n_newlines;
+ var preindent_string = '';
+
+
+ // Some interpreters have unexpected results with foo = baz || bar;
+ options = options ? options : {};
+
+ var opt_brace_style;
+
+ // compatibility
+ if (options.space_after_anon_function !== undefined && options.jslint_happy === undefined) {
+ options.jslint_happy = options.space_after_anon_function;
+ }
+ if (options.braces_on_own_line !== undefined) { //graceful handling of deprecated option
+ opt_brace_style = options.braces_on_own_line ? "expand" : "collapse";
+ }
+ opt_brace_style = options.brace_style ? options.brace_style : (opt_brace_style ? opt_brace_style : "collapse");
+
+
+ var opt_indent_size = options.indent_size ? options.indent_size : 4;
+ var opt_indent_char = options.indent_char ? options.indent_char : ' ';
+ var opt_preserve_newlines = typeof options.preserve_newlines === 'undefined' ? true : options.preserve_newlines;
+ var opt_max_preserve_newlines = typeof options.max_preserve_newlines === 'undefined' ? false : options.max_preserve_newlines;
+ var opt_jslint_happy = options.jslint_happy === 'undefined' ? false : options.jslint_happy;
+ var opt_keep_array_indentation = typeof options.keep_array_indentation === 'undefined' ? false : options.keep_array_indentation;
+ var opt_space_before_conditional = typeof options.space_before_conditional === 'undefined' ? true : options.space_before_conditional;
+ var opt_indent_case = typeof options.indent_case === 'undefined' ? false : options.indent_case;
+ var opt_unescape_strings = typeof options.unescape_strings === 'undefined' ? false : options.unescape_strings;
+
+ just_added_newline = false;
+
+ // cache the source's length.
+ var input_length = js_source_text.length;
+
+ function trim_output(eat_newlines) {
+ eat_newlines = typeof eat_newlines === 'undefined' ? false : eat_newlines;
+ while (output.length && (output[output.length - 1] === ' '
+ || output[output.length - 1] === indent_string
+ || output[output.length - 1] === preindent_string
+ || (eat_newlines && (output[output.length - 1] === '\n' || output[output.length - 1] === '\r')))) {
+ output.pop();
+ }
+ }
+
+ function trim(s) {
+ return s.replace(/^\s\s*|\s\s*$/, '');
+ }
+
+ // we could use just string.split, but
+ // IE doesn't like returning empty strings
+ function split_newlines(s) {
+ return s.split(/\x0d\x0a|\x0a/);
+ }
+
+ function force_newline() {
+ var old_keep_array_indentation = opt_keep_array_indentation;
+ opt_keep_array_indentation = false;
+ print_newline();
+ opt_keep_array_indentation = old_keep_array_indentation;
+ }
+
+ function print_newline(ignore_repeated) {
+
+ flags.eat_next_space = false;
+ if (opt_keep_array_indentation && is_array(flags.mode)) {
+ return;
+ }
+
+ ignore_repeated = typeof ignore_repeated === 'undefined' ? true : ignore_repeated;
+
+ flags.if_line = false;
+ trim_output();
+
+ if (!output.length) {
+ return; // no newline on start of file
+ }
+
+ if (output[output.length - 1] !== "\n" || !ignore_repeated) {
+ just_added_newline = true;
+ output.push("\n");
+ }
+ if (preindent_string) {
+ output.push(preindent_string);
+ }
+ for (var i = 0; i < flags.indentation_level; i += 1) {
+ output.push(indent_string);
+ }
+ if (flags.var_line && flags.var_line_reindented) {
+ output.push(indent_string); // skip space-stuffing, if indenting with a tab
+ }
+ if (flags.case_body) {
+ output.push(indent_string);
+ }
+ }
+
+
+
+ function print_single_space() {
+
+ if (last_type === 'TK_COMMENT') {
+ return print_newline();
+ }
+ if (flags.eat_next_space) {
+ flags.eat_next_space = false;
+ return;
+ }
+ var last_output = ' ';
+ if (output.length) {
+ last_output = output[output.length - 1];
+ }
+ if (last_output !== ' ' && last_output !== '\n' && last_output !== indent_string) { // prevent occassional duplicate space
+ output.push(' ');
+ }
+ }
+
+
+ function print_token() {
+ just_added_newline = false;
+ flags.eat_next_space = false;
+ output.push(token_text);
+ }
+
+ function indent() {
+ flags.indentation_level += 1;
+ }
+
+
+ function remove_indent() {
+ if (output.length && output[output.length - 1] === indent_string) {
+ output.pop();
+ }
+ }
+
+ function set_mode(mode) {
+ if (flags) {
+ flag_store.push(flags);
+ }
+ flags = {
+ previous_mode: flags ? flags.mode : 'BLOCK',
+ mode: mode,
+ var_line: false,
+ var_line_tainted: false,
+ var_line_reindented: false,
+ in_html_comment: false,
+ if_line: false,
+ in_case_statement: false, // switch(..){ INSIDE HERE }
+ in_case: false, // we're on the exact line with "case 0:"
+ case_body: false, // the indented case-action block
+ eat_next_space: false,
+ indentation_baseline: -1,
+ indentation_level: (flags ? flags.indentation_level + (flags.case_body ? 1 : 0) + ((flags.var_line && flags.var_line_reindented) ? 1 : 0) : 0),
+ ternary_depth: 0
+ };
+ }
+
+ function is_array(mode) {
+ return mode === '[EXPRESSION]' || mode === '[INDENTED-EXPRESSION]';
+ }
+
+ function is_expression(mode) {
+ return in_array(mode, ['[EXPRESSION]', '(EXPRESSION)', '(FOR-EXPRESSION)', '(COND-EXPRESSION)']);
+ }
+
+ function restore_mode() {
+ do_block_just_closed = flags.mode === 'DO_BLOCK';
+ if (flag_store.length > 0) {
+ var mode = flags.mode;
+ flags = flag_store.pop();
+ flags.previous_mode = mode;
+ }
+ }
+
+ function all_lines_start_with(lines, c) {
+ for (var i = 0; i < lines.length; i++) {
+ var line = trim(lines[i]);
+ if (line.charAt(0) !== c) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ function is_special_word(word) {
+ return in_array(word, ['case', 'return', 'do', 'if', 'throw', 'else']);
+ }
+
+ function in_array(what, arr) {
+ for (var i = 0; i < arr.length; i += 1) {
+ if (arr[i] === what) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ function look_up(exclude) {
+ var local_pos = parser_pos;
+ var c = input.charAt(local_pos);
+ while (in_array(c, whitespace) && c !== exclude) {
+ local_pos++;
+ if (local_pos >= input_length) {
+ return 0;
+ }
+ c = input.charAt(local_pos);
+ }
+ return c;
+ }
+
+ function get_next_token() {
+ var i;
+ var resulting_string;
+
+ n_newlines = 0;
+
+ if (parser_pos >= input_length) {
+ return ['', 'TK_EOF'];
+ }
+
+ wanted_newline = false;
+
+ var c = input.charAt(parser_pos);
+ parser_pos += 1;
+
+
+ var keep_whitespace = opt_keep_array_indentation && is_array(flags.mode);
+
+ if (keep_whitespace) {
+
+ //
+ // slight mess to allow nice preservation of array indentation and reindent that correctly
+ // first time when we get to the arrays:
+ // var a = [
+ // ....'something'
+ // we make note of whitespace_count = 4 into flags.indentation_baseline
+ // so we know that 4 whitespaces in original source match indent_level of reindented source
+ //
+ // and afterwards, when we get to
+ // 'something,
+ // .......'something else'
+ // we know that this should be indented to indent_level + (7 - indentation_baseline) spaces
+ //
+ var whitespace_count = 0;
+
+ while (in_array(c, whitespace)) {
+
+ if (c === "\n") {
+ trim_output();
+ output.push("\n");
+ just_added_newline = true;
+ whitespace_count = 0;
+ } else {
+ if (c === '\t') {
+ whitespace_count += 4;
+ } else if (c === '\r') {
+ // nothing
+ } else {
+ whitespace_count += 1;
+ }
+ }
+
+ if (parser_pos >= input_length) {
+ return ['', 'TK_EOF'];
+ }
+
+ c = input.charAt(parser_pos);
+ parser_pos += 1;
+
+ }
+ if (flags.indentation_baseline === -1) {
+ flags.indentation_baseline = whitespace_count;
+ }
+
+ if (just_added_newline) {
+ for (i = 0; i < flags.indentation_level + 1; i += 1) {
+ output.push(indent_string);
+ }
+ if (flags.indentation_baseline !== -1) {
+ for (i = 0; i < whitespace_count - flags.indentation_baseline; i++) {
+ output.push(' ');
+ }
+ }
+ }
+
+ } else {
+ while (in_array(c, whitespace)) {
+
+ if (c === "\n") {
+ n_newlines += ((opt_max_preserve_newlines) ? (n_newlines <= opt_max_preserve_newlines) ? 1 : 0 : 1);
+ }
+
+
+ if (parser_pos >= input_length) {
+ return ['', 'TK_EOF'];
+ }
+
+ c = input.charAt(parser_pos);
+ parser_pos += 1;
+
+ }
+
+ if (opt_preserve_newlines) {
+ if (n_newlines > 1) {
+ for (i = 0; i < n_newlines; i += 1) {
+ print_newline(i === 0);
+ just_added_newline = true;
+ }
+ }
+ }
+ wanted_newline = n_newlines > 0;
+ }
+
+
+ if (in_array(c, wordchar)) {
+ if (parser_pos < input_length) {
+ while (in_array(input.charAt(parser_pos), wordchar)) {
+ c += input.charAt(parser_pos);
+ parser_pos += 1;
+ if (parser_pos === input_length) {
+ break;
+ }
+ }
+ }
+
+ // small and surprisingly unugly hack for 1E-10 representation
+ if (parser_pos !== input_length && c.match(/^[0-9]+[Ee]$/) && (input.charAt(parser_pos) === '-' || input.charAt(parser_pos) === '+')) {
+
+ var sign = input.charAt(parser_pos);
+ parser_pos += 1;
+
+ var t = get_next_token();
+ c += sign + t[0];
+ return [c, 'TK_WORD'];
+ }
+
+ if (c === 'in') { // hack for 'in' operator
+ return [c, 'TK_OPERATOR'];
+ }
+ if (wanted_newline && last_type !== 'TK_OPERATOR'
+ && last_type !== 'TK_EQUALS'
+ && !flags.if_line && (opt_preserve_newlines || last_text !== 'var')) {
+ print_newline();
+ }
+ return [c, 'TK_WORD'];
+ }
+
+ if (c === '(' || c === '[') {
+ return [c, 'TK_START_EXPR'];
+ }
+
+ if (c === ')' || c === ']') {
+ return [c, 'TK_END_EXPR'];
+ }
+
+ if (c === '{') {
+ return [c, 'TK_START_BLOCK'];
+ }
+
+ if (c === '}') {
+ return [c, 'TK_END_BLOCK'];
+ }
+
+ if (c === ';') {
+ return [c, 'TK_SEMICOLON'];
+ }
+
+ if (c === '/') {
+ var comment = '';
+ // peek for comment /* ... */
+ var inline_comment = true;
+ if (input.charAt(parser_pos) === '*') {
+ parser_pos += 1;
+ if (parser_pos < input_length) {
+ while (parser_pos < input_length &&
+ ! (input.charAt(parser_pos) === '*' && input.charAt(parser_pos + 1) && input.charAt(parser_pos + 1) === '/')) {
+ c = input.charAt(parser_pos);
+ comment += c;
+ if (c === "\n" || c === "\r") {
+ inline_comment = false;
+ }
+ parser_pos += 1;
+ if (parser_pos >= input_length) {
+ break;
+ }
+ }
+ }
+ parser_pos += 2;
+ if (inline_comment && n_newlines === 0) {
+ return ['/*' + comment + '*/', 'TK_INLINE_COMMENT'];
+ } else {
+ return ['/*' + comment + '*/', 'TK_BLOCK_COMMENT'];
+ }
+ }
+ // peek for comment // ...
+ if (input.charAt(parser_pos) === '/') {
+ comment = c;
+ while (input.charAt(parser_pos) !== '\r' && input.charAt(parser_pos) !== '\n') {
+ comment += input.charAt(parser_pos);
+ parser_pos += 1;
+ if (parser_pos >= input_length) {
+ break;
+ }
+ }
+ if (wanted_newline) {
+ print_newline();
+ }
+ return [comment, 'TK_COMMENT'];
+ }
+
+ }
+
+ if (c === "'" || // string
+ c === '"' || // string
+ (c === '/' &&
+ ((last_type === 'TK_WORD' && is_special_word(last_text)) ||
+ (last_text === ')' && in_array(flags.previous_mode, ['(COND-EXPRESSION)', '(FOR-EXPRESSION)'])) ||
+ (last_type === 'TK_COMMA' || last_type === 'TK_COMMENT' || last_type === 'TK_START_EXPR' || last_type === 'TK_START_BLOCK' || last_type === 'TK_END_BLOCK' || last_type === 'TK_OPERATOR' || last_type === 'TK_EQUALS' || last_type === 'TK_EOF' || last_type === 'TK_SEMICOLON')))) { // regexp
+ var sep = c;
+ var esc = false;
+ var esc1 = 0;
+ var esc2 = 0;
+ resulting_string = c;
+
+ if (parser_pos < input_length) {
+ if (sep === '/') {
+ //
+ // handle regexp separately...
+ //
+ var in_char_class = false;
+ while (esc || in_char_class || input.charAt(parser_pos) !== sep) {
+ resulting_string += input.charAt(parser_pos);
+ if (!esc) {
+ esc = input.charAt(parser_pos) === '\\';
+ if (input.charAt(parser_pos) === '[') {
+ in_char_class = true;
+ } else if (input.charAt(parser_pos) === ']') {
+ in_char_class = false;
+ }
+ } else {
+ esc = false;
+ }
+ parser_pos += 1;
+ if (parser_pos >= input_length) {
+ // incomplete string/rexp when end-of-file reached.
+ // bail out with what had been received so far.
+ return [resulting_string, 'TK_STRING'];
+ }
+ }
+
+ } else {
+ //
+ // and handle string also separately
+ //
+ while (esc || input.charAt(parser_pos) !== sep) {
+ resulting_string += input.charAt(parser_pos);
+ if (esc1 && esc1 >= esc2) {
+ esc1 = parseInt(resulting_string.substr(-esc2), 16);
+ if (esc1 && esc1 >= 0x20 && esc1 <= 0x7e) {
+ esc1 = String.fromCharCode(esc1);
+ resulting_string = resulting_string.substr(0, resulting_string.length - esc2 - 2) + (((esc1 === sep) || (esc1 === '\\')) ? '\\' : '') + esc1;
+ }
+ esc1 = 0;
+ }
+ if (esc1) {
+ esc1++;
+ } else if (!esc) {
+ esc = input.charAt(parser_pos) === '\\';
+ } else {
+ esc = false;
+ if (opt_unescape_strings) {
+ if (input.charAt(parser_pos) === 'x') {
+ esc1++;
+ esc2 = 2;
+ } else if (input.charAt(parser_pos) === 'u') {
+ esc1++;
+ esc2 = 4;
+ }
+ }
+ }
+ parser_pos += 1;
+ if (parser_pos >= input_length) {
+ // incomplete string/rexp when end-of-file reached.
+ // bail out with what had been received so far.
+ return [resulting_string, 'TK_STRING'];
+ }
+ }
+ }
+
+
+
+ }
+
+ parser_pos += 1;
+
+ resulting_string += sep;
+
+ if (sep === '/') {
+ // regexps may have modifiers /regexp/MOD , so fetch those, too
+ while (parser_pos < input_length && in_array(input.charAt(parser_pos), wordchar)) {
+ resulting_string += input.charAt(parser_pos);
+ parser_pos += 1;
+ }
+ }
+ return [resulting_string, 'TK_STRING'];
+ }
+
+ if (c === '#') {
+
+
+ if (output.length === 0 && input.charAt(parser_pos) === '!') {
+ // shebang
+ resulting_string = c;
+ while (parser_pos < input_length && c !== '\n') {
+ c = input.charAt(parser_pos);
+ resulting_string += c;
+ parser_pos += 1;
+ }
+ output.push(trim(resulting_string) + '\n');
+ print_newline();
+ return get_next_token();
+ }
+
+
+
+ // Spidermonkey-specific sharp variables for circular references
+ // https://developer.mozilla.org/En/Sharp_variables_in_JavaScript
+ // http://mxr.mozilla.org/mozilla-central/source/js/src/jsscan.cpp around line 1935
+ var sharp = '#';
+ if (parser_pos < input_length && in_array(input.charAt(parser_pos), digits)) {
+ do {
+ c = input.charAt(parser_pos);
+ sharp += c;
+ parser_pos += 1;
+ } while (parser_pos < input_length && c !== '#' && c !== '=');
+ if (c === '#') {
+ //
+ } else if (input.charAt(parser_pos) === '[' && input.charAt(parser_pos + 1) === ']') {
+ sharp += '[]';
+ parser_pos += 2;
+ } else if (input.charAt(parser_pos) === '{' && input.charAt(parser_pos + 1) === '}') {
+ sharp += '{}';
+ parser_pos += 2;
+ }
+ return [sharp, 'TK_WORD'];
+ }
+ }
+
+ if (c === '<' && input.substring(parser_pos - 1, parser_pos + 3) === '<!--') {
+ parser_pos += 3;
+ c = '<!--';
+ while (input.charAt(parser_pos) !== '\n' && parser_pos < input_length) {
+ c += input.charAt(parser_pos);
+ parser_pos++;
+ }
+ flags.in_html_comment = true;
+ return [c, 'TK_COMMENT'];
+ }
+
+ if (c === '-' && flags.in_html_comment && input.substring(parser_pos - 1, parser_pos + 2) === '-->') {
+ flags.in_html_comment = false;
+ parser_pos += 2;
+ if (wanted_newline) {
+ print_newline();
+ }
+ return ['-->', 'TK_COMMENT'];
+ }
+
+ if (in_array(c, punct)) {
+ while (parser_pos < input_length && in_array(c + input.charAt(parser_pos), punct)) {
+ c += input.charAt(parser_pos);
+ parser_pos += 1;
+ if (parser_pos >= input_length) {
+ break;
+ }
+ }
+
+ if (c === ',') {
+ return [c, 'TK_COMMA'];
+ } else if (c === '=') {
+ return [c, 'TK_EQUALS'];
+ } else {
+ return [c, 'TK_OPERATOR'];
+ }
+ }
+
+ return [c, 'TK_UNKNOWN'];
+ }
+
+ //----------------------------------
+ indent_string = '';
+ while (opt_indent_size > 0) {
+ indent_string += opt_indent_char;
+ opt_indent_size -= 1;
+ }
+
+ while (js_source_text && (js_source_text.charAt(0) === ' ' || js_source_text.charAt(0) === '\t')) {
+ preindent_string += js_source_text.charAt(0);
+ js_source_text = js_source_text.substring(1);
+ }
+ input = js_source_text;
+
+ last_word = ''; // last 'TK_WORD' passed
+ last_type = 'TK_START_EXPR'; // last token type
+ last_text = ''; // last token text
+ last_last_text = ''; // pre-last token text
+ output = [];
+
+ do_block_just_closed = false;
+
+ whitespace = "\n\r\t ".split('');
+ wordchar = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_$'.split('');
+ digits = '0123456789'.split('');
+
+ punct = '+ - * / % & ++ -- = += -= *= /= %= == === != !== > < >= <= >> << >>> >>>= >>= <<= && &= | || ! !! , : ? ^ ^= |= ::';
+ punct += ' <%= <% %> <?= <? ?>'; // try to be a good boy and try not to break the markup language identifiers
+ punct = punct.split(' ');
+
+ // words which should always start on new line.
+ line_starters = 'continue,try,throw,return,var,if,switch,case,default,for,while,break,function'.split(',');
+
+ // states showing if we are currently in expression (i.e. "if" case) - 'EXPRESSION', or in usual block (like, procedure), 'BLOCK'.
+ // some formatting depends on that.
+ flag_store = [];
+ set_mode('BLOCK');
+
+ parser_pos = 0;
+ while (true) {
+ var t = get_next_token();
+ token_text = t[0];
+ token_type = t[1];
+ if (token_type === 'TK_EOF') {
+ break;
+ }
+
+ switch (token_type) {
+
+ case 'TK_START_EXPR':
+
+ if (token_text === '[') {
+
+ if (last_type === 'TK_WORD' || last_text === ')') {
+ // this is array index specifier, break immediately
+ // a[x], fn()[x]
+ if (in_array(last_text, line_starters)) {
+ print_single_space();
+ }
+ set_mode('(EXPRESSION)');
+ print_token();
+ break;
+ }
+
+ if (flags.mode === '[EXPRESSION]' || flags.mode === '[INDENTED-EXPRESSION]') {
+ if (last_last_text === ']' && last_text === ',') {
+ // ], [ goes to new line
+ if (flags.mode === '[EXPRESSION]') {
+ flags.mode = '[INDENTED-EXPRESSION]';
+ if (!opt_keep_array_indentation) {
+ indent();
+ }
+ }
+ set_mode('[EXPRESSION]');
+ if (!opt_keep_array_indentation) {
+ print_newline();
+ }
+ } else if (last_text === '[') {
+ if (flags.mode === '[EXPRESSION]') {
+ flags.mode = '[INDENTED-EXPRESSION]';
+ if (!opt_keep_array_indentation) {
+ indent();
+ }
+ }
+ set_mode('[EXPRESSION]');
+
+ if (!opt_keep_array_indentation) {
+ print_newline();
+ }
+ } else {
+ set_mode('[EXPRESSION]');
+ }
+ } else {
+ set_mode('[EXPRESSION]');
+ }
+
+
+
+ } else {
+ if (last_word === 'for') {
+ set_mode('(FOR-EXPRESSION)');
+ } else if (in_array(last_word, ['if', 'while'])) {
+ set_mode('(COND-EXPRESSION)');
+ } else {
+ set_mode('(EXPRESSION)');
+ }
+ }
+
+ if (last_text === ';' || last_type === 'TK_START_BLOCK') {
+ print_newline();
+ } else if (last_type === 'TK_END_EXPR' || last_type === 'TK_START_EXPR' || last_type === 'TK_END_BLOCK' || last_text === '.') {
+ if (wanted_newline) {
+ print_newline();
+ }
+ // do nothing on (( and )( and ][ and ]( and .(
+ } else if (last_type !== 'TK_WORD' && last_type !== 'TK_OPERATOR') {
+ print_single_space();
+ } else if (last_word === 'function' || last_word === 'typeof') {
+ // function() vs function ()
+ if (opt_jslint_happy) {
+ print_single_space();
+ }
+ } else if (in_array(last_text, line_starters) || last_text === 'catch') {
+ if (opt_space_before_conditional) {
+ print_single_space();
+ }
+ }
+ print_token();
+
+ break;
+
+ case 'TK_END_EXPR':
+ if (token_text === ']') {
+ if (opt_keep_array_indentation) {
+ if (last_text === '}') {
+ // trim_output();
+ // print_newline(true);
+ remove_indent();
+ print_token();
+ restore_mode();
+ break;
+ }
+ } else {
+ if (flags.mode === '[INDENTED-EXPRESSION]') {
+ if (last_text === ']') {
+ restore_mode();
+ print_newline();
+ print_token();
+ break;
+ }
+ }
+ }
+ }
+ restore_mode();
+ print_token();
+ break;
+
+ case 'TK_START_BLOCK':
+
+ if (last_word === 'do') {
+ set_mode('DO_BLOCK');
+ } else {
+ set_mode('BLOCK');
+ }
+ if (opt_brace_style === "expand" || opt_brace_style === "expand-strict") {
+ var empty_braces = false;
+ if (opt_brace_style === "expand-strict") {
+ empty_braces = (look_up() === '}');
+ if (!empty_braces) {
+ print_newline(true);
+ }
+ } else {
+ if (last_type !== 'TK_OPERATOR') {
+ if (last_text === '=' || (is_special_word(last_text) && last_text !== 'else')) {
+ print_single_space();
+ } else {
+ print_newline(true);
+ }
+ }
+ }
+ print_token();
+ if (!empty_braces) {
+ indent();
+ }
+ } else {
+ if (last_type !== 'TK_OPERATOR' && last_type !== 'TK_START_EXPR') {
+ if (last_type === 'TK_START_BLOCK') {
+ print_newline();
+ } else {
+ print_single_space();
+ }
+ } else {
+ // if TK_OPERATOR or TK_START_EXPR
+ if (is_array(flags.previous_mode) && last_text === ',') {
+ if (last_last_text === '}') {
+ // }, { in array context
+ print_single_space();
+ } else {
+ print_newline(); // [a, b, c, {
+ }
+ }
+ }
+ indent();
+ print_token();
+ }
+
+ break;
+
+ case 'TK_END_BLOCK':
+ restore_mode();
+ if (opt_brace_style === "expand" || opt_brace_style === "expand-strict") {
+ if (last_text !== '{') {
+ print_newline();
+ }
+ print_token();
+ } else {
+ if (last_type === 'TK_START_BLOCK') {
+ // nothing
+ if (just_added_newline) {
+ remove_indent();
+ } else {
+ // {}
+ trim_output();
+ }
+ } else {
+ if (is_array(flags.mode) && opt_keep_array_indentation) {
+ // we REALLY need a newline here, but newliner would skip that
+ opt_keep_array_indentation = false;
+ print_newline();
+ opt_keep_array_indentation = true;
+
+ } else {
+ print_newline();
+ }
+ }
+ print_token();
+ }
+ break;
+
+ case 'TK_WORD':
+
+ // no, it's not you. even I have problems understanding how this works
+ // and what does what.
+ if (do_block_just_closed) {
+ // do {} ## while ()
+ print_single_space();
+ print_token();
+ print_single_space();
+ do_block_just_closed = false;
+ break;
+ }
+
+ prefix = 'NONE';
+
+ if (token_text === 'function') {
+ if (flags.var_line && last_type !== 'TK_EQUALS' ) {
+ flags.var_line_reindented = true;
+ }
+ if ((just_added_newline || last_text === ';') && last_text !== '{'
+ && last_type !== 'TK_BLOCK_COMMENT' && last_type !== 'TK_COMMENT') {
+ // make sure there is a nice clean space of at least one blank line
+ // before a new function definition
+ n_newlines = just_added_newline ? n_newlines : 0;
+ if (!opt_preserve_newlines) {
+ n_newlines = 1;
+ }
+
+ for (var i = 0; i < 2 - n_newlines; i++) {
+ print_newline(false);
+ }
+ }
+ if (last_type === 'TK_WORD') {
+ if (last_text === 'get' || last_text === 'set' || last_text === 'new' || last_text === 'return') {
+ print_single_space();
+ } else {
+ print_newline();
+ }
+ } else if (last_type === 'TK_OPERATOR' || last_text === '=') {
+ // foo = function
+ print_single_space();
+ } else if (is_expression(flags.mode)) {
+ //ää print nothing
+ } else {
+ print_newline();
+ }
+
+ print_token();
+ last_word = token_text;
+ break;
+ }
+
+ if (token_text === 'case' || (token_text === 'default' && flags.in_case_statement)) {
+ if (last_text === ':' || flags.case_body) {
+ // switch cases following one another
+ remove_indent();
+ } else {
+ // case statement starts in the same line where switch
+ if (!opt_indent_case) {
+ flags.indentation_level--;
+ }
+ print_newline();
+ if (!opt_indent_case) {
+ flags.indentation_level++;
+ }
+ }
+ print_token();
+ flags.in_case = true;
+ flags.in_case_statement = true;
+ flags.case_body = false;
+ break;
+ }
+
+ if (last_type === 'TK_END_BLOCK') {
+
+ if (!in_array(token_text.toLowerCase(), ['else', 'catch', 'finally'])) {
+ prefix = 'NEWLINE';
+ } else {
+ if (opt_brace_style === "expand" || opt_brace_style === "end-expand" || opt_brace_style === "expand-strict") {
+ prefix = 'NEWLINE';
+ } else {
+ prefix = 'SPACE';
+ print_single_space();
+ }
+ }
+ } else if (last_type === 'TK_SEMICOLON' && (flags.mode === 'BLOCK' || flags.mode === 'DO_BLOCK')) {
+ prefix = 'NEWLINE';
+ } else if (last_type === 'TK_SEMICOLON' && is_expression(flags.mode)) {
+ prefix = 'SPACE';
+ } else if (last_type === 'TK_STRING') {
+ prefix = 'NEWLINE';
+ } else if (last_type === 'TK_WORD') {
+ if (last_text === 'else') {
+ // eat newlines between ...else *** some_op...
+ // won't preserve extra newlines in this place (if any), but don't care that much
+ trim_output(true);
+ }
+ prefix = 'SPACE';
+ } else if (last_type === 'TK_START_BLOCK') {
+ prefix = 'NEWLINE';
+ } else if (last_type === 'TK_END_EXPR') {
+ print_single_space();
+ prefix = 'NEWLINE';
+ }
+
+ if (in_array(token_text, line_starters) && last_text !== ')') {
+ if (last_text === 'else') {
+ prefix = 'SPACE';
+ } else {
+ prefix = 'NEWLINE';
+ }
+
+ }
+
+ if (flags.if_line && last_type === 'TK_END_EXPR') {
+ flags.if_line = false;
+ }
+ if (in_array(token_text.toLowerCase(), ['else', 'catch', 'finally'])) {
+ if (last_type !== 'TK_END_BLOCK' || opt_brace_style === "expand" || opt_brace_style === "end-expand" || opt_brace_style === "expand-strict") {
+ print_newline();
+ } else {
+ trim_output(true);
+ print_single_space();
+ }
+ } else if (prefix === 'NEWLINE') {
+ if (is_special_word(last_text)) {
+ // no newline between 'return nnn'
+ print_single_space();
+ } else if (last_type !== 'TK_END_EXPR') {
+ if ((last_type !== 'TK_START_EXPR' || token_text !== 'var') && last_text !== ':') {
+ // no need to force newline on 'var': for (var x = 0...)
+ if (token_text === 'if' && last_word === 'else' && last_text !== '{') {
+ // no newline for } else if {
+ print_single_space();
+ } else {
+ flags.var_line = false;
+ flags.var_line_reindented = false;
+ print_newline();
+ }
+ }
+ } else if (in_array(token_text, line_starters) && last_text !== ')') {
+ flags.var_line = false;
+ flags.var_line_reindented = false;
+ print_newline();
+ }
+ } else if (is_array(flags.mode) && last_text === ',' && last_last_text === '}') {
+ print_newline(); // }, in lists get a newline treatment
+ } else if (prefix === 'SPACE') {
+ print_single_space();
+ }
+ print_token();
+ last_word = token_text;
+
+ if (token_text === 'var') {
+ flags.var_line = true;
+ flags.var_line_reindented = false;
+ flags.var_line_tainted = false;
+ }
+
+ if (token_text === 'if') {
+ flags.if_line = true;
+ }
+ if (token_text === 'else') {
+ flags.if_line = false;
+ }
+
+ break;
+
+ case 'TK_SEMICOLON':
+
+ print_token();
+ flags.var_line = false;
+ flags.var_line_reindented = false;
+ if (flags.mode === 'OBJECT') {
+ // OBJECT mode is weird and doesn't get reset too well.
+ flags.mode = 'BLOCK';
+ }
+ break;
+
+ case 'TK_STRING':
+
+ if (last_type === 'TK_END_EXPR' && in_array(flags.previous_mode, ['(COND-EXPRESSION)', '(FOR-EXPRESSION)'])) {
+ print_single_space();
+ } else if (last_type === 'TK_COMMENT' || last_type === 'TK_STRING' || last_type === 'TK_START_BLOCK' || last_type === 'TK_END_BLOCK' || last_type === 'TK_SEMICOLON') {
+ print_newline();
+ } else if (last_type === 'TK_WORD') {
+ print_single_space();
+ }
+ print_token();
+ break;
+
+ case 'TK_EQUALS':
+ if (flags.var_line) {
+ // just got an '=' in a var-line, different formatting/line-breaking, etc will now be done
+ flags.var_line_tainted = true;
+ }
+ print_single_space();
+ print_token();
+ print_single_space();
+ break;
+
+ case 'TK_COMMA':
+ if (flags.var_line) {
+ if (is_expression(flags.mode) || last_type === 'TK_END_BLOCK' ) {
+ // do not break on comma, for(var a = 1, b = 2)
+ flags.var_line_tainted = false;
+ }
+ if (flags.var_line_tainted) {
+ print_token();
+ flags.var_line_reindented = true;
+ flags.var_line_tainted = false;
+ print_newline();
+ break;
+ } else {
+ flags.var_line_tainted = false;
+ }
+
+ print_token();
+ print_single_space();
+ break;
+ }
+
+ if (last_type === 'TK_COMMENT') {
+ print_newline();
+ }
+
+ if (last_type === 'TK_END_BLOCK' && flags.mode !== "(EXPRESSION)") {
+ print_token();
+ if (flags.mode === 'OBJECT' && last_text === '}') {
+ print_newline();
+ } else {
+ print_single_space();
+ }
+ } else {
+ if (flags.mode === 'OBJECT') {
+ print_token();
+ print_newline();
+ } else {
+ // EXPR or DO_BLOCK
+ print_token();
+ print_single_space();
+ }
+ }
+ break;
+
+
+ case 'TK_OPERATOR':
+
+ var space_before = true;
+ var space_after = true;
+
+ if (is_special_word(last_text)) {
+ // "return" had a special handling in TK_WORD. Now we need to return the favor
+ print_single_space();
+ print_token();
+ break;
+ }
+
+ // hack for actionscript's import .*;
+ if (token_text === '*' && last_type === 'TK_UNKNOWN' && !last_last_text.match(/^\d+$/)) {
+ print_token();
+ break;
+ }
+
+ if (token_text === ':' && flags.in_case) {
+ if (opt_indent_case) {
+ flags.case_body = true;
+ }
+ print_token(); // colon really asks for separate treatment
+ print_newline();
+ flags.in_case = false;
+ break;
+ }
+
+ if (token_text === '::') {
+ // no spaces around exotic namespacing syntax operator
+ print_token();
+ break;
+ }
+
+ if (in_array(token_text, ['--', '++', '!']) || (in_array(token_text, ['-', '+']) && (in_array(last_type, ['TK_START_BLOCK', 'TK_START_EXPR', 'TK_EQUALS', 'TK_OPERATOR']) || in_array(last_text, line_starters)))) {
+ // unary operators (and binary +/- pretending to be unary) special cases
+
+ space_before = false;
+ space_after = false;
+
+ if (last_text === ';' && is_expression(flags.mode)) {
+ // for (;; ++i)
+ // ^^^
+ space_before = true;
+ }
+ if (last_type === 'TK_WORD' && in_array(last_text, line_starters)) {
+ space_before = true;
+ }
+
+ if (flags.mode === 'BLOCK' && (last_text === '{' || last_text === ';')) {
+ // { foo; --i }
+ // foo(); --bar;
+ print_newline();
+ }
+ } else if (token_text === '.') {
+ // decimal digits or object.property
+ space_before = false;
+
+ } else if (token_text === ':') {
+ if (flags.ternary_depth === 0) {
+ if (flags.mode === 'BLOCK') {
+ flags.mode = 'OBJECT';
+ }
+ space_before = false;
+ } else {
+ flags.ternary_depth -= 1;
+ }
+ } else if (token_text === '?') {
+ flags.ternary_depth += 1;
+ }
+ if (space_before) {
+ print_single_space();
+ }
+
+ print_token();
+
+ if (space_after) {
+ print_single_space();
+ }
+
+ break;
+
+ case 'TK_BLOCK_COMMENT':
+
+ var lines = split_newlines(token_text);
+ var j; // iterator for this case
+
+ if (all_lines_start_with(lines.slice(1), '*')) {
+ // javadoc: reformat and reindent
+ print_newline();
+ output.push(lines[0]);
+ for (j = 1; j < lines.length; j++) {
+ print_newline();
+ output.push(' ');
+ output.push(trim(lines[j]));
+ }
+
+ } else {
+
+ // simple block comment: leave intact
+ if (lines.length > 1) {
+ // multiline comment block starts with a new line
+ print_newline();
+ } else {
+ // single-line /* comment */ stays where it is
+ if (last_type === 'TK_END_BLOCK') {
+ print_newline();
+ } else {
+ print_single_space();
+ }
+
+ }
+
+ for (j = 0; j < lines.length; j++) {
+ output.push(lines[j]);
+ output.push("\n");
+ }
+
+ }
+ if (look_up('\n') !== '\n') {
+ print_newline();
+ }
+ break;
+
+ case 'TK_INLINE_COMMENT':
+ print_single_space();
+ print_token();
+ if (is_expression(flags.mode)) {
+ print_single_space();
+ } else {
+ force_newline();
+ }
+ break;
+
+ case 'TK_COMMENT':
+
+ if (last_text === ',' && !wanted_newline) {
+ trim_output(true);
+ }
+ if (last_type !== 'TK_COMMENT') {
+ if (wanted_newline) {
+ print_newline();
+ } else {
+ print_single_space();
+ }
+ }
+ print_token();
+ print_newline();
+ break;
+
+ case 'TK_UNKNOWN':
+ if (is_special_word(last_text)) {
+ print_single_space();
+ }
+ print_token();
+ break;
+ }
+
+ last_last_text = last_text;
+ last_type = token_type;
+ last_text = token_text;
+ }
+
+ var sweet_code = preindent_string + output.join('').replace(/[\r\n ]+$/, '');
+ return sweet_code;
+
+}
diff --git a/browser/devtools/shared/LayoutHelpers.jsm b/browser/devtools/shared/LayoutHelpers.jsm
new file mode 100644
index 000000000..0c174b92e
--- /dev/null
+++ b/browser/devtools/shared/LayoutHelpers.jsm
@@ -0,0 +1,384 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* 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/. */
+
+const Cu = Components.utils;
+const Ci = Components.interfaces;
+const Cr = Components.results;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Services",
+ "resource://gre/modules/Services.jsm");
+
+XPCOMUtils.defineLazyGetter(this, "PlatformKeys", function() {
+ return Services.strings.createBundle(
+ "chrome://global-platform/locale/platformKeys.properties");
+});
+
+this.EXPORTED_SYMBOLS = ["LayoutHelpers"];
+
+this.LayoutHelpers = LayoutHelpers = {
+
+ /**
+ * Compute the position and the dimensions for the visible portion
+ * of a node, relativalely to the root window.
+ *
+ * @param nsIDOMNode aNode
+ * a DOM element to be highlighted
+ */
+ getDirtyRect: function LH_getDirectyRect(aNode) {
+ let frameWin = aNode.ownerDocument.defaultView;
+ let clientRect = aNode.getBoundingClientRect();
+
+ // Go up in the tree of frames to determine the correct rectangle.
+ // clientRect is read-only, we need to be able to change properties.
+ rect = {top: clientRect.top,
+ left: clientRect.left,
+ width: clientRect.width,
+ height: clientRect.height};
+
+ // We iterate through all the parent windows.
+ while (true) {
+
+ // Does the selection overflow on the right of its window?
+ let diffx = frameWin.innerWidth - (rect.left + rect.width);
+ if (diffx < 0) {
+ rect.width += diffx;
+ }
+
+ // Does the selection overflow on the bottom of its window?
+ let diffy = frameWin.innerHeight - (rect.top + rect.height);
+ if (diffy < 0) {
+ rect.height += diffy;
+ }
+
+ // Does the selection overflow on the left of its window?
+ if (rect.left < 0) {
+ rect.width += rect.left;
+ rect.left = 0;
+ }
+
+ // Does the selection overflow on the top of its window?
+ if (rect.top < 0) {
+ rect.height += rect.top;
+ rect.top = 0;
+ }
+
+ // Selection has been clipped to fit in its own window.
+
+ // Are we in the top-level window?
+ if (frameWin.parent === frameWin || !frameWin.frameElement) {
+ break;
+ }
+
+ // We are in an iframe.
+ // We take into account the parent iframe position and its
+ // offset (borders and padding).
+ let frameRect = frameWin.frameElement.getBoundingClientRect();
+
+ let [offsetTop, offsetLeft] =
+ this.getIframeContentOffset(frameWin.frameElement);
+
+ rect.top += frameRect.top + offsetTop;
+ rect.left += frameRect.left + offsetLeft;
+
+ frameWin = frameWin.parent;
+ }
+
+ return rect;
+ },
+
+ /**
+ * Compute the absolute position and the dimensions of a node, relativalely
+ * to the root window.
+ *
+ * @param nsIDOMNode aNode
+ * a DOM element to get the bounds for
+ * @param nsIWindow aContentWindow
+ * the content window holding the node
+ */
+ getRect: function LH_getRect(aNode, aContentWindow) {
+ let frameWin = aNode.ownerDocument.defaultView;
+ let clientRect = aNode.getBoundingClientRect();
+
+ // Go up in the tree of frames to determine the correct rectangle.
+ // clientRect is read-only, we need to be able to change properties.
+ rect = {top: clientRect.top + aContentWindow.pageYOffset,
+ left: clientRect.left + aContentWindow.pageXOffset,
+ width: clientRect.width,
+ height: clientRect.height};
+
+ // We iterate through all the parent windows.
+ while (true) {
+
+ // Are we in the top-level window?
+ if (frameWin.parent === frameWin || !frameWin.frameElement) {
+ break;
+ }
+
+ // We are in an iframe.
+ // We take into account the parent iframe position and its
+ // offset (borders and padding).
+ let frameRect = frameWin.frameElement.getBoundingClientRect();
+
+ let [offsetTop, offsetLeft] =
+ this.getIframeContentOffset(frameWin.frameElement);
+
+ rect.top += frameRect.top + offsetTop;
+ rect.left += frameRect.left + offsetLeft;
+
+ frameWin = frameWin.parent;
+ }
+
+ return rect;
+ },
+
+ /**
+ * Returns iframe content offset (iframe border + padding).
+ * Note: this function shouldn't need to exist, had the platform provided a
+ * suitable API for determining the offset between the iframe's content and
+ * its bounding client rect. Bug 626359 should provide us with such an API.
+ *
+ * @param aIframe
+ * The iframe.
+ * @returns array [offsetTop, offsetLeft]
+ * offsetTop is the distance from the top of the iframe and the
+ * top of the content document.
+ * offsetLeft is the distance from the left of the iframe and the
+ * left of the content document.
+ */
+ getIframeContentOffset: function LH_getIframeContentOffset(aIframe) {
+ let style = aIframe.contentWindow.getComputedStyle(aIframe, null);
+
+ // In some cases, the computed style is null
+ if (!style) {
+ return [0, 0];
+ }
+
+ let paddingTop = parseInt(style.getPropertyValue("padding-top"));
+ let paddingLeft = parseInt(style.getPropertyValue("padding-left"));
+
+ let borderTop = parseInt(style.getPropertyValue("border-top-width"));
+ let borderLeft = parseInt(style.getPropertyValue("border-left-width"));
+
+ return [borderTop + paddingTop, borderLeft + paddingLeft];
+ },
+
+ /**
+ * Apply the page zoom factor.
+ */
+ getZoomedRect: function LH_getZoomedRect(aWin, aRect) {
+ // get page zoom factor, if any
+ let zoom =
+ aWin.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
+ .getInterface(Components.interfaces.nsIDOMWindowUtils)
+ .fullZoom;
+
+ // adjust rect for zoom scaling
+ let aRectScaled = {};
+ for (let prop in aRect) {
+ aRectScaled[prop] = aRect[prop] * zoom;
+ }
+
+ return aRectScaled;
+ },
+
+
+ /**
+ * Find an element from the given coordinates. This method descends through
+ * frames to find the element the user clicked inside frames.
+ *
+ * @param DOMDocument aDocument the document to look into.
+ * @param integer aX
+ * @param integer aY
+ * @returns Node|null the element node found at the given coordinates.
+ */
+ getElementFromPoint: function LH_elementFromPoint(aDocument, aX, aY) {
+ let node = aDocument.elementFromPoint(aX, aY);
+ if (node && node.contentDocument) {
+ if (node instanceof Ci.nsIDOMHTMLIFrameElement) {
+ let rect = node.getBoundingClientRect();
+
+ // Gap between the iframe and its content window.
+ let [offsetTop, offsetLeft] = LayoutHelpers.getIframeContentOffset(node);
+
+ aX -= rect.left + offsetLeft;
+ aY -= rect.top + offsetTop;
+
+ if (aX < 0 || aY < 0) {
+ // Didn't reach the content document, still over the iframe.
+ return node;
+ }
+ }
+ if (node instanceof Ci.nsIDOMHTMLIFrameElement ||
+ node instanceof Ci.nsIDOMHTMLFrameElement) {
+ let subnode = this.getElementFromPoint(node.contentDocument, aX, aY);
+ if (subnode) {
+ node = subnode;
+ }
+ }
+ }
+ return node;
+ },
+
+ /**
+ * Scroll the document so that the element "elem" appears in the viewport.
+ *
+ * @param Element elem the element that needs to appear in the viewport.
+ * @param bool centered true if you want it centered, false if you want it to
+ * appear on the top of the viewport. It is true by default, and that is
+ * usually what you want.
+ */
+ scrollIntoViewIfNeeded:
+ function LH_scrollIntoViewIfNeeded(elem, centered) {
+ // We want to default to centering the element in the page,
+ // so as to keep the context of the element.
+ centered = centered === undefined? true: !!centered;
+
+ let win = elem.ownerDocument.defaultView;
+ let clientRect = elem.getBoundingClientRect();
+
+ // The following are always from the {top, bottom, left, right}
+ // of the viewport, to the {top, …} of the box.
+ // Think of them as geometrical vectors, it helps.
+ // The origin is at the top left.
+
+ let topToBottom = clientRect.bottom;
+ let bottomToTop = clientRect.top - win.innerHeight;
+ let leftToRight = clientRect.right;
+ let rightToLeft = clientRect.left - win.innerWidth;
+ let xAllowed = true; // We allow one translation on the x axis,
+ let yAllowed = true; // and one on the y axis.
+
+ // Whatever `centered` is, the behavior is the same if the box is
+ // (even partially) visible.
+
+ if ((topToBottom > 0 || !centered) && topToBottom <= elem.offsetHeight) {
+ win.scrollBy(0, topToBottom - elem.offsetHeight);
+ yAllowed = false;
+ } else
+ if ((bottomToTop < 0 || !centered) && bottomToTop >= -elem.offsetHeight) {
+ win.scrollBy(0, bottomToTop + elem.offsetHeight);
+ yAllowed = false;
+ }
+
+ if ((leftToRight > 0 || !centered) && leftToRight <= elem.offsetWidth) {
+ if (xAllowed) {
+ win.scrollBy(leftToRight - elem.offsetWidth, 0);
+ xAllowed = false;
+ }
+ } else
+ if ((rightToLeft < 0 || !centered) && rightToLeft >= -elem.offsetWidth) {
+ if (xAllowed) {
+ win.scrollBy(rightToLeft + elem.offsetWidth, 0);
+ xAllowed = false;
+ }
+ }
+
+ // If we want it centered, and the box is completely hidden,
+ // then we center it explicitly.
+
+ if (centered) {
+
+ if (yAllowed && (topToBottom <= 0 || bottomToTop >= 0)) {
+ win.scroll(win.scrollX,
+ win.scrollY + clientRect.top
+ - (win.innerHeight - elem.offsetHeight) / 2);
+ }
+
+ if (xAllowed && (leftToRight <= 0 || rightToLeft <= 0)) {
+ win.scroll(win.scrollX + clientRect.left
+ - (win.innerWidth - elem.offsetWidth) / 2,
+ win.scrollY);
+ }
+ }
+
+ if (win.parent !== win) {
+ // We are inside an iframe.
+ LH_scrollIntoViewIfNeeded(win.frameElement, centered);
+ }
+ },
+
+ /**
+ * Check if a node and its document are still alive
+ * and attached to the window.
+ *
+ * @param aNode
+ */
+ isNodeConnected: function LH_isNodeConnected(aNode)
+ {
+ try {
+ let connected = (aNode.ownerDocument && aNode.ownerDocument.defaultView &&
+ !(aNode.compareDocumentPosition(aNode.ownerDocument.documentElement) &
+ aNode.DOCUMENT_POSITION_DISCONNECTED));
+ return connected;
+ } catch (e) {
+ // "can't access dead object" error
+ return false;
+ }
+ },
+
+ /**
+ * Prettifies the modifier keys for an element.
+ *
+ * @param Node aElemKey
+ * The key element to get the modifiers from.
+ * @param boolean aAllowCloverleaf
+ * Pass true to use the cloverleaf symbol instead of a descriptive string.
+ * @return string
+ * A prettified and properly separated modifier keys string.
+ */
+ prettyKey: function LH_prettyKey(aElemKey, aAllowCloverleaf)
+ {
+ let elemString = "";
+ let elemMod = aElemKey.getAttribute("modifiers");
+
+ if (elemMod.match("accel")) {
+ if (Services.appinfo.OS == "Darwin") {
+ // XXX bug 779642 Use "Cmd-" literal vs. cloverleaf meta-key until
+ // Orion adds variable height lines.
+ if (!aAllowCloverleaf) {
+ elemString += "Cmd-";
+ } else {
+ elemString += PlatformKeys.GetStringFromName("VK_META") +
+ PlatformKeys.GetStringFromName("MODIFIER_SEPARATOR");
+ }
+ } else {
+ elemString += PlatformKeys.GetStringFromName("VK_CONTROL") +
+ PlatformKeys.GetStringFromName("MODIFIER_SEPARATOR");
+ }
+ }
+ if (elemMod.match("access")) {
+ if (Services.appinfo.OS == "Darwin") {
+ elemString += PlatformKeys.GetStringFromName("VK_CONTROL") +
+ PlatformKeys.GetStringFromName("MODIFIER_SEPARATOR");
+ } else {
+ elemString += PlatformKeys.GetStringFromName("VK_ALT") +
+ PlatformKeys.GetStringFromName("MODIFIER_SEPARATOR");
+ }
+ }
+ if (elemMod.match("shift")) {
+ elemString += PlatformKeys.GetStringFromName("VK_SHIFT") +
+ PlatformKeys.GetStringFromName("MODIFIER_SEPARATOR");
+ }
+ if (elemMod.match("alt")) {
+ elemString += PlatformKeys.GetStringFromName("VK_ALT") +
+ PlatformKeys.GetStringFromName("MODIFIER_SEPARATOR");
+ }
+ if (elemMod.match("ctrl") || elemMod.match("control")) {
+ elemString += PlatformKeys.GetStringFromName("VK_CONTROL") +
+ PlatformKeys.GetStringFromName("MODIFIER_SEPARATOR");
+ }
+ if (elemMod.match("meta")) {
+ elemString += PlatformKeys.GetStringFromName("VK_META") +
+ PlatformKeys.GetStringFromName("MODIFIER_SEPARATOR");
+ }
+
+ return elemString +
+ (aElemKey.getAttribute("keycode").replace(/^.*VK_/, "") ||
+ aElemKey.getAttribute("key")).toUpperCase();
+ }
+};
diff --git a/browser/devtools/shared/Makefile.in b/browser/devtools/shared/Makefile.in
new file mode 100644
index 000000000..94eba0663
--- /dev/null
+++ b/browser/devtools/shared/Makefile.in
@@ -0,0 +1,18 @@
+#
+# 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/.
+
+DEPTH = @DEPTH@
+topsrcdir = @top_srcdir@
+srcdir = @srcdir@
+VPATH = @srcdir@
+
+include $(DEPTH)/config/autoconf.mk
+
+include $(topsrcdir)/config/rules.mk
+
+libs::
+ $(NSINSTALL) $(srcdir)/*.jsm $(FINAL_TARGET)/modules/devtools
+ $(NSINSTALL) $(srcdir)/widgets/*.jsm $(FINAL_TARGET)/modules/devtools
+ $(NSINSTALL) $(srcdir)/*.js $(FINAL_TARGET)/modules/devtools/shared
diff --git a/browser/devtools/shared/Parser.jsm b/browser/devtools/shared/Parser.jsm
new file mode 100644
index 000000000..42b32c07c
--- /dev/null
+++ b/browser/devtools/shared/Parser.jsm
@@ -0,0 +1,2293 @@
+/* -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* 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 = Components.interfaces;
+const Cu = Components.utils;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this,
+ "Reflect", "resource://gre/modules/reflect.jsm");
+
+this.EXPORTED_SYMBOLS = ["Parser"];
+
+/**
+ * A JS parser using the reflection API.
+ */
+this.Parser = function Parser() {
+ this._cache = new Map();
+};
+
+Parser.prototype = {
+ /**
+ * Gets a collection of parser methods for a specified source.
+ *
+ * @param string aUrl [optional]
+ * The source url. The AST nodes will be cached, so you can use this
+ * identifier to avoid parsing the whole source again.
+ * @param string aSource
+ * The source text content.
+ */
+ get: function P_get(aUrl, aSource) {
+ // Try to use the cached AST nodes, to avoid useless parsing operations.
+ if (this._cache.has(aUrl)) {
+ return this._cache.get(aUrl);
+ }
+
+ // The source may not necessarily be JS, in which case we need to extract
+ // all the scripts. Fastest/easiest way is with a regular expression.
+ // Don't worry, the rules of using a <script> tag are really strict,
+ // this will work.
+ let regexp = /<script[^>]*>([^]*?)<\/script\s*>/gim;
+ let syntaxTrees = [];
+ let scriptMatches = [];
+ let scriptMatch;
+
+ if (aSource.match(/^\s*</)) {
+ // First non whitespace character is &lt, so most definitely HTML.
+ while (scriptMatch = regexp.exec(aSource)) {
+ scriptMatches.push(scriptMatch[1]); // Contents are captured at index 1.
+ }
+ }
+
+ // If there are no script matches, send the whole source directly to the
+ // reflection API to generate the AST nodes.
+ if (!scriptMatches.length) {
+ // Reflect.parse throws when encounters a syntax error.
+ try {
+ let nodes = Reflect.parse(aSource);
+ let length = aSource.length;
+ syntaxTrees.push(new SyntaxTree(nodes, aUrl, length));
+ } catch (e) {
+ log(aUrl, e);
+ }
+ }
+ // Generate the AST nodes for each script.
+ else {
+ for (let script of scriptMatches) {
+ // Reflect.parse throws when encounters a syntax error.
+ try {
+ let nodes = Reflect.parse(script);
+ let offset = aSource.indexOf(script);
+ let length = script.length;
+ syntaxTrees.push(new SyntaxTree(nodes, aUrl, length, offset));
+ } catch (e) {
+ log(aUrl, e);
+ }
+ }
+ }
+
+ let pool = new SyntaxTreesPool(syntaxTrees);
+ this._cache.set(aUrl, pool);
+ return pool;
+ },
+
+ /**
+ * Clears all the parsed sources from cache.
+ */
+ clearCache: function P_clearCache() {
+ this._cache.clear();
+ },
+
+ _cache: null
+};
+
+/**
+ * A pool handling a collection of AST nodes generated by the reflection API.
+ *
+ * @param object aSyntaxTrees
+ * A collection of AST nodes generated for a source.
+ */
+function SyntaxTreesPool(aSyntaxTrees) {
+ this._trees = aSyntaxTrees;
+ this._cache = new Map();
+}
+
+SyntaxTreesPool.prototype = {
+ /**
+ * @see SyntaxTree.prototype.getNamedFunctionDefinitions
+ */
+ getNamedFunctionDefinitions: function STP_getNamedFunctionDefinitions(aSubstring) {
+ return this._call("getNamedFunctionDefinitions", aSubstring);
+ },
+
+ /**
+ * @see SyntaxTree.prototype.getFunctionAtLocation
+ */
+ getFunctionAtLocation: function STP_getFunctionAtLocation(aLine, aColumn) {
+ return this._call("getFunctionAtLocation", [aLine, aColumn]);
+ },
+
+ /**
+ * Finds the offset and length of the script containing the specified offset
+ * relative to its parent source.
+ *
+ * @param number aOffset
+ * The offset relative to the parent source.
+ * @return array
+ * The offset and length relative to the enclosing script.
+ */
+ getScriptInfo: function STP_getScriptInfo(aOffset) {
+ for (let { offset, length } of this._trees) {
+ if (offset <= aOffset && offset + length >= aOffset) {
+ return [offset, length];
+ }
+ }
+ return [-1, -1];
+ },
+
+ /**
+ * Handles a request for all known syntax trees.
+ *
+ * @param string aFunction
+ * The function name to call on the SyntaxTree instances.
+ * @param any aParams
+ * Any kind params to pass to the request function.
+ * @return array
+ * The results given by all known syntax trees.
+ */
+ _call: function STP__call(aFunction, aParams) {
+ let results = [];
+ let requestId = aFunction + aParams; // Cache all the things!
+
+ if (this._cache.has(requestId)) {
+ return this._cache.get(requestId);
+ }
+ for (let syntaxTree of this._trees) {
+ try {
+ results.push({
+ sourceUrl: syntaxTree.url,
+ scriptLength: syntaxTree.length,
+ scriptOffset: syntaxTree.offset,
+ parseResults: syntaxTree[aFunction](aParams)
+ });
+ } catch (e) {
+ // Can't guarantee that the tree traversal logic is forever perfect :)
+ // Language features may be added, in which case the recursive methods
+ // need to be updated. If an exception is thrown here, file a bug.
+ log("syntax tree", e);
+ }
+ }
+ this._cache.set(requestId, results);
+ return results;
+ },
+
+ _trees: null,
+ _cache: null
+};
+
+/**
+ * A collection of AST nodes generated by the reflection API.
+ *
+ * @param object aNodes
+ * The AST nodes.
+ * @param string aUrl
+ * The source url.
+ * @param number aLength
+ * The total number of chars of the parsed script in the parent source.
+ * @param number aOffset [optional]
+ * The char offset of the parsed script in the parent source.
+ */
+function SyntaxTree(aNodes, aUrl, aLength, aOffset = 0) {
+ this.AST = aNodes;
+ this.url = aUrl;
+ this.length = aLength;
+ this.offset = aOffset;
+};
+
+SyntaxTree.prototype = {
+ /**
+ * Searches for all function definitions (declarations and expressions)
+ * whose names (or inferred names) contain a string.
+ *
+ * @param string aSubstring
+ * The string to be contained in the function name (or inferred name).
+ * Can be an empty string to match all functions.
+ * @return array
+ * All the matching function declarations and expressions, as
+ * { functionName, functionLocation ... } object hashes.
+ */
+ getNamedFunctionDefinitions: function ST_getNamedFunctionDefinitions(aSubstring) {
+ let lowerCaseToken = aSubstring.toLowerCase();
+ let store = [];
+
+ SyntaxTreeVisitor.walk(this.AST, {
+ /**
+ * Callback invoked for each function declaration node.
+ * @param Node aNode
+ */
+ onFunctionDeclaration: function STW_onFunctionDeclaration(aNode) {
+ let functionName = aNode.id.name;
+ if (functionName.toLowerCase().contains(lowerCaseToken)) {
+ store.push({
+ functionName: functionName,
+ functionLocation: aNode.loc
+ });
+ }
+ },
+
+ /**
+ * Callback invoked for each function expression node.
+ * @param Node aNode
+ */
+ onFunctionExpression: function STW_onFunctionExpression(aNode) {
+ let parent = aNode._parent;
+ let functionName, inferredName, inferredChain, inferredLocation;
+
+ // Function expressions don't necessarily have a name.
+ if (aNode.id) {
+ functionName = aNode.id.name;
+ }
+ // Infer the function's name from an enclosing syntax tree node.
+ if (parent) {
+ let inferredInfo = ParserHelpers.inferFunctionExpressionInfo(aNode);
+ inferredName = inferredInfo.name;
+ inferredChain = inferredInfo.chain;
+ inferredLocation = inferredInfo.loc;
+ }
+ // Current node may be part of a larger assignment expression stack.
+ if (parent.type == "AssignmentExpression") {
+ this.onFunctionExpression(parent);
+ }
+
+ if ((functionName && functionName.toLowerCase().contains(lowerCaseToken)) ||
+ (inferredName && inferredName.toLowerCase().contains(lowerCaseToken))) {
+ store.push({
+ functionName: functionName,
+ functionLocation: aNode.loc,
+ inferredName: inferredName,
+ inferredChain: inferredChain,
+ inferredLocation: inferredLocation
+ });
+ }
+ },
+
+ /**
+ * Callback invoked for each arrow expression node.
+ * @param Node aNode
+ */
+ onArrowExpression: function STW_onArrowExpression(aNode) {
+ let parent = aNode._parent;
+ let inferredName, inferredChain, inferredLocation;
+
+ // Infer the function's name from an enclosing syntax tree node.
+ let inferredInfo = ParserHelpers.inferFunctionExpressionInfo(aNode);
+ inferredName = inferredInfo.name;
+ inferredChain = inferredInfo.chain;
+ inferredLocation = inferredInfo.loc;
+
+ // Current node may be part of a larger assignment expression stack.
+ if (parent.type == "AssignmentExpression") {
+ this.onFunctionExpression(parent);
+ }
+
+ if (inferredName && inferredName.toLowerCase().contains(lowerCaseToken)) {
+ store.push({
+ inferredName: inferredName,
+ inferredChain: inferredChain,
+ inferredLocation: inferredLocation
+ });
+ }
+ }
+ });
+
+ return store;
+ },
+
+ /**
+ * Gets the "new" or "call" expression at the specified location.
+ *
+ * @param number aLine
+ * The line in the source.
+ * @param number aColumn
+ * The column in the source.
+ * @return object
+ * An { functionName, functionLocation } object hash,
+ * or null if nothing is found at the specified location.
+ */
+ getFunctionAtLocation: function STW_getFunctionAtLocation([aLine, aColumn]) {
+ let self = this;
+ let func = null;
+
+ SyntaxTreeVisitor.walk(this.AST, {
+ /**
+ * Callback invoked for each node.
+ * @param Node aNode
+ */
+ onNode: function STW_onNode(aNode) {
+ // Make sure the node is part of a branch that's guaranteed to be
+ // hovered. Otherwise, return true to abruptly halt walking this
+ // syntax tree branch. This is a really efficient optimization.
+ return ParserHelpers.isWithinLines(aNode, aLine);
+ },
+
+ /**
+ * Callback invoked for each identifier node.
+ * @param Node aNode
+ */
+ onIdentifier: function STW_onIdentifier(aNode) {
+ // Make sure the identifier itself is hovered.
+ let hovered = ParserHelpers.isWithinBounds(aNode, aLine, aColumn);
+ if (!hovered) {
+ return;
+ }
+
+ // Make sure the identifier is part of a "new" expression or
+ // "call" expression node.
+ let expression = ParserHelpers.getEnclosingFunctionExpression(aNode);
+ if (!expression) {
+ return;
+ }
+
+ // Found an identifier node that is part of a "new" expression or
+ // "call" expression node. However, it may be an argument, not a callee.
+ if (ParserHelpers.isFunctionCalleeArgument(aNode)) {
+ // It's an argument.
+ if (self.functionIdentifiersCache.has(aNode.name)) {
+ // It's a function as an argument.
+ func = {
+ functionName: aNode.name,
+ functionLocation: aNode.loc || aNode._parent.loc
+ };
+ }
+ return;
+ }
+
+ // Found a valid "new" expression or "call" expression node.
+ func = {
+ functionName: aNode.name,
+ functionLocation: ParserHelpers.getFunctionCalleeInfo(expression).loc
+ };
+
+ // Abruptly halt walking the syntax tree.
+ this.break = true;
+ }
+ });
+
+ return func;
+ },
+
+ /**
+ * Gets all the function identifiers in this syntax tree (both the
+ * function names and their inferred names).
+ *
+ * @return array
+ * An array of strings.
+ */
+ get functionIdentifiersCache() {
+ if (this._functionIdentifiersCache) {
+ return this._functionIdentifiersCache;
+ }
+ let functionDefinitions = this.getNamedFunctionDefinitions("");
+ let functionIdentifiers = new Set();
+
+ for (let { functionName, inferredName } of functionDefinitions) {
+ functionIdentifiers.add(functionName);
+ functionIdentifiers.add(inferredName);
+ }
+ return this._functionIdentifiersCache = functionIdentifiers;
+ },
+
+ AST: null,
+ url: "",
+ length: 0,
+ offset: 0
+};
+
+/**
+ * Parser utility methods.
+ */
+let ParserHelpers = {
+ /**
+ * Checks if a node's bounds contains a specified line.
+ *
+ * @param Node aNode
+ * The node's bounds used as reference.
+ * @param number aLine
+ * The line number to check.
+ * @return boolean
+ * True if the line and column is contained in the node's bounds.
+ */
+ isWithinLines: function PH_isWithinLines(aNode, aLine) {
+ // Not all nodes have location information attached.
+ if (!aNode.loc) {
+ return this.isWithinLines(aNode._parent, aLine);
+ }
+ return aNode.loc.start.line <= aLine && aNode.loc.end.line >= aLine;
+ },
+
+ /**
+ * Checks if a node's bounds contains a specified line and column.
+ *
+ * @param Node aNode
+ * The node's bounds used as reference.
+ * @param number aLine
+ * The line number to check.
+ * @param number aColumn
+ * The column number to check.
+ * @return boolean
+ * True if the line and column is contained in the node's bounds.
+ */
+ isWithinBounds: function PH_isWithinBounds(aNode, aLine, aColumn) {
+ // Not all nodes have location information attached.
+ if (!aNode.loc) {
+ return this.isWithinBounds(aNode._parent, aLine, aColumn);
+ }
+ return aNode.loc.start.line == aLine && aNode.loc.end.line == aLine &&
+ aNode.loc.start.column <= aColumn && aNode.loc.end.column >= aColumn;
+ },
+
+ /**
+ * Try to infer a function expression's name & other details based on the
+ * enclosing VariableDeclarator, AssignmentExpression or ObjectExpression node.
+ *
+ * @param Node aNode
+ * The function expression node to get the name for.
+ * @return object
+ * The inferred function name, or empty string can't infer name,
+ * along with the chain (a generic "context", like a prototype chain)
+ * and location if available.
+ */
+ inferFunctionExpressionInfo: function PH_inferFunctionExpressionInfo(aNode) {
+ let parent = aNode._parent;
+
+ // A function expression may be defined in a variable declarator,
+ // e.g. var foo = function(){}, in which case it is possible to infer
+ // the variable name.
+ if (parent.type == "VariableDeclarator") {
+ return {
+ name: parent.id.name,
+ chain: null,
+ loc: parent.loc
+ };
+ }
+
+ // Function expressions can also be defined in assignment expressions,
+ // e.g. foo = function(){} or foo.bar = function(){}, in which case it is
+ // possible to infer the assignee name ("foo" and "bar" respectively).
+ if (parent.type == "AssignmentExpression") {
+ let assigneeChain = this.getAssignmentExpressionAssigneeChain(parent);
+ let assigneeLeaf = assigneeChain.pop();
+ return {
+ name: assigneeLeaf,
+ chain: assigneeChain,
+ loc: parent.left.loc
+ };
+ }
+
+ // If a function expression is defined in an object expression,
+ // e.g. { foo: function(){} }, then it is possible to infer the name
+ // from the corresponding property.
+ if (parent.type == "ObjectExpression") {
+ let propertyDetails = this.getObjectExpressionPropertyKeyForValue(aNode);
+ let propertyChain = this.getObjectExpressionPropertyChain(parent);
+ let propertyLeaf = propertyDetails.name;
+ return {
+ name: propertyLeaf,
+ chain: propertyChain,
+ loc: propertyDetails.loc
+ };
+ }
+
+ // Can't infer the function expression's name.
+ return {
+ name: "",
+ chain: null,
+ loc: null
+ };
+ },
+
+ /**
+ * Gets details about an object expression's property to which a specified
+ * value is assigned. For example, the node returned for the value 42 in
+ * "{ foo: { bar: 42 } }" is "bar".
+ *
+ * @param Node aNode
+ * The value node assigned to a property in an object expression.
+ * @return object
+ * The details about the assignee property node.
+ */
+ getObjectExpressionPropertyKeyForValue:
+ function PH_getObjectExpressionPropertyKeyForValue(aNode) {
+ let parent = aNode._parent;
+ if (parent.type != "ObjectExpression") {
+ return null;
+ }
+ for (let property of parent.properties) {
+ if (property.value == aNode) {
+ return property.key;
+ }
+ }
+ },
+
+ /**
+ * Gets an object expression property chain to its parent variable declarator.
+ * For example, the chain to "baz" in "foo = { bar: { baz: { } } }" is
+ * ["foo", "bar", "baz"].
+ *
+ * @param Node aNode
+ * The object expression node to begin the scan from.
+ * @param array aStore [optional]
+ * The chain to store the nodes into.
+ * @return array
+ * The chain to the parent variable declarator, as strings.
+ */
+ getObjectExpressionPropertyChain:
+ function PH_getObjectExpressionPropertyChain(aNode, aStore = []) {
+ switch (aNode.type) {
+ case "ObjectExpression":
+ this.getObjectExpressionPropertyChain(aNode._parent, aStore);
+
+ let propertyDetails = this.getObjectExpressionPropertyKeyForValue(aNode);
+ if (propertyDetails) {
+ aStore.push(this.getObjectExpressionPropertyKeyForValue(aNode).name);
+ }
+ break;
+ // Handle "foo.bar = { ... }" since it's commonly used when defining an
+ // object's prototype methods; for example: "Foo.prototype = { ... }".
+ case "AssignmentExpression":
+ this.getAssignmentExpressionAssigneeChain(aNode, aStore);
+ break;
+ // Additionally handle stuff like "foo = bar.baz({ ... })", because it's
+ // commonly used in prototype-based inheritance in many libraries;
+ // for example: "Foo.Bar = Baz.extend({ ... })".
+ case "NewExpression":
+ case "CallExpression":
+ this.getObjectExpressionPropertyChain(aNode._parent, aStore);
+ break;
+ // End of the chain.
+ case "VariableDeclarator":
+ aStore.push(aNode.id.name);
+ break;
+ }
+ return aStore;
+ },
+
+ /**
+ * Gets the assignee property chain in an assignment expression.
+ * For example, the chain in "foo.bar.baz = 42" is ["foo", "bar", "baz"].
+ *
+ * @param Node aNode
+ * The assignment expression node to begin the scan from.
+ * @param array aStore
+ * The chain to store the nodes into.
+ * @param array aStore [optional]
+ * The chain to store the nodes into.
+ * @return array
+ * The full assignee chain, as strings.
+ */
+ getAssignmentExpressionAssigneeChain:
+ function PH_getAssignmentExpressionAssigneeChain(aNode, aStore = []) {
+ switch (aNode.type) {
+ case "AssignmentExpression":
+ this.getAssignmentExpressionAssigneeChain(aNode.left, aStore);
+ break;
+ case "MemberExpression":
+ this.getAssignmentExpressionAssigneeChain(aNode.object, aStore);
+ this.getAssignmentExpressionAssigneeChain(aNode.property, aStore);
+ break;
+ case "ThisExpression":
+ // Such expressions may appear in an assignee chain, for example
+ // "this.foo.bar = baz", however it seems better to ignore such nodes
+ // and limit the chain to ["foo", "bar"].
+ break;
+ case "Identifier":
+ aStore.push(aNode.name);
+ break;
+ }
+ return aStore;
+ },
+
+ /**
+ * Gets the "new" expression or "call" expression containing the specified
+ * node. If the node is not enclosed in either of these expression types,
+ * null is returned.
+ *
+ * @param Node aNode
+ * The child node of an enclosing "new" expression or "call" expression.
+ * @return Node
+ * The enclosing "new" expression or "call" expression node, or
+ * null if nothing is found.
+ */
+ getEnclosingFunctionExpression:
+ function PH_getEnclosingFunctionExpression(aNode) {
+ switch (aNode.type) {
+ case "NewExpression":
+ case "CallExpression":
+ return aNode;
+ case "MemberExpression":
+ case "Identifier":
+ return this.getEnclosingFunctionExpression(aNode._parent);
+ default:
+ return null;
+ }
+ },
+
+ /**
+ * Gets the name and { line, column } location of a "new" expression or
+ * "call" expression's callee node.
+ *
+ * @param Node aNode
+ * The "new" expression or "call" expression to get the callee info for.
+ * @return object
+ * An object containing the name and location as properties, or
+ * null if nothing is found.
+ */
+ getFunctionCalleeInfo: function PH_getFunctionCalleeInfo(aNode) {
+ switch (aNode.type) {
+ case "NewExpression":
+ case "CallExpression":
+ return this.getFunctionCalleeInfo(aNode.callee);
+ case "MemberExpression":
+ return this.getFunctionCalleeInfo(aNode.property);
+ case "Identifier":
+ return {
+ name: aNode.name,
+ loc: aNode.loc || (aNode._parent || {}).loc
+ };
+ default:
+ return null;
+ }
+ },
+
+ /**
+ * Determines if an identifier node is part of a "new" expression or
+ * "call" expression's callee arguments.
+ *
+ * @param Node aNode
+ * The node to determine if part of a function's arguments.
+ * @return boolean
+ * True if the identifier is an argument, false otherwise.
+ */
+ isFunctionCalleeArgument: function PH_isFunctionCalleeArgument(aNode) {
+ if (!aNode._parent) {
+ return false;
+ }
+ switch (aNode._parent.type) {
+ case "NewExpression":
+ case "CallExpression":
+ return aNode._parent.arguments.indexOf(aNode) != -1;
+ default:
+ return this.isFunctionCalleeArgument(aNode._parent);
+ }
+ }
+};
+
+/**
+ * A visitor for a syntax tree generated by the reflection API.
+ * See https://developer.mozilla.org/en-US/docs/SpiderMonkey/Parser_API.
+ *
+ * All node types implement the following interface:
+ * interface Node {
+ * type: string;
+ * loc: SourceLocation | null;
+ * }
+ */
+let SyntaxTreeVisitor = {
+ /**
+ * Walks a syntax tree.
+ *
+ * @param object aTree
+ * The AST nodes generated by the reflection API
+ * @param object aCallbacks
+ * A map of all the callbacks to invoke when passing through certain
+ * types of noes (e.g: onFunctionDeclaration, onBlockStatement etc.).
+ */
+ walk: function STV_walk(aTree, aCallbacks) {
+ this[aTree.type](aTree, aCallbacks);
+ },
+
+ /**
+ * A flag checked on each node in the syntax tree. If true, walking is
+ * abruptly halted.
+ */
+ break: false,
+
+ /**
+ * A complete program source tree.
+ *
+ * interface Program <: Node {
+ * type: "Program";
+ * body: [ Statement ];
+ * }
+ */
+ Program: function STV_Program(aNode, aCallbacks) {
+ if (aCallbacks.onProgram) {
+ aCallbacks.onProgram(aNode);
+ }
+ for (let statement of aNode.body) {
+ this[statement.type](statement, aNode, aCallbacks);
+ }
+ },
+
+ /**
+ * Any statement.
+ *
+ * interface Statement <: Node { }
+ */
+ Statement: function STV_Statement(aNode, aParent, aCallbacks) {
+ aNode._parent = aParent;
+
+ if (this.break) {
+ return;
+ }
+ if (aCallbacks.onNode) {
+ if (aCallbacks.onNode(aNode, aParent) === false) {
+ return;
+ }
+ }
+ if (aCallbacks.onStatement) {
+ aCallbacks.onStatement(aNode);
+ }
+ },
+
+ /**
+ * An empty statement, i.e., a solitary semicolon.
+ *
+ * interface EmptyStatement <: Statement {
+ * type: "EmptyStatement";
+ * }
+ */
+ EmptyStatement: function STV_EmptyStatement(aNode, aParent, aCallbacks) {
+ aNode._parent = aParent;
+
+ if (this.break) {
+ return;
+ }
+ if (aCallbacks.onNode) {
+ if (aCallbacks.onNode(aNode, aParent) === false) {
+ return;
+ }
+ }
+ if (aCallbacks.onEmptyStatement) {
+ aCallbacks.onEmptyStatement(aNode);
+ }
+ },
+
+ /**
+ * A block statement, i.e., a sequence of statements surrounded by braces.
+ *
+ * interface BlockStatement <: Statement {
+ * type: "BlockStatement";
+ * body: [ Statement ];
+ * }
+ */
+ BlockStatement: function STV_BlockStatement(aNode, aParent, aCallbacks) {
+ aNode._parent = aParent;
+
+ if (this.break) {
+ return;
+ }
+ if (aCallbacks.onNode) {
+ if (aCallbacks.onNode(aNode, aParent) === false) {
+ return;
+ }
+ }
+ if (aCallbacks.onBlockStatement) {
+ aCallbacks.onBlockStatement(aNode);
+ }
+ for (let statement of aNode.body) {
+ this[statement.type](statement, aNode, aCallbacks);
+ }
+ },
+
+ /**
+ * An expression statement, i.e., a statement consisting of a single expression.
+ *
+ * interface ExpressionStatement <: Statement {
+ * type: "ExpressionStatement";
+ * expression: Expression;
+ * }
+ */
+ ExpressionStatement: function STV_ExpressionStatement(aNode, aParent, aCallbacks) {
+ aNode._parent = aParent;
+
+ if (this.break) {
+ return;
+ }
+ if (aCallbacks.onNode) {
+ if (aCallbacks.onNode(aNode, aParent) === false) {
+ return;
+ }
+ }
+ if (aCallbacks.onExpressionStatement) {
+ aCallbacks.onExpressionStatement(aNode);
+ }
+ this[aNode.expression.type](aNode.expression, aNode, aCallbacks);
+ },
+
+ /**
+ * An if statement.
+ *
+ * interface IfStatement <: Statement {
+ * type: "IfStatement";
+ * test: Expression;
+ * consequent: Statement;
+ * alternate: Statement | null;
+ * }
+ */
+ IfStatement: function STV_IfStatement(aNode, aParent, aCallbacks) {
+ aNode._parent = aParent;
+
+ if (this.break) {
+ return;
+ }
+ if (aCallbacks.onNode) {
+ if (aCallbacks.onNode(aNode, aParent) === false) {
+ return;
+ }
+ }
+ if (aCallbacks.onIfStatement) {
+ aCallbacks.onIfStatement(aNode);
+ }
+ this[aNode.test.type](aNode.test, aNode, aCallbacks);
+ this[aNode.consequent.type](aNode.consequent, aNode, aCallbacks);
+ if (aNode.alternate) {
+ this[aNode.alternate.type](aNode.alternate, aNode, aCallbacks);
+ }
+ },
+
+ /**
+ * A labeled statement, i.e., a statement prefixed by a break/continue label.
+ *
+ * interface LabeledStatement <: Statement {
+ * type: "LabeledStatement";
+ * label: Identifier;
+ * body: Statement;
+ * }
+ */
+ LabeledStatement: function STV_LabeledStatement(aNode, aParent, aCallbacks) {
+ aNode._parent = aParent;
+
+ if (this.break) {
+ return;
+ }
+ if (aCallbacks.onNode) {
+ if (aCallbacks.onNode(aNode, aParent) === false) {
+ return;
+ }
+ }
+ if (aCallbacks.onLabeledStatement) {
+ aCallbacks.onLabeledStatement(aNode);
+ }
+ this[aNode.label.type](aNode.label, aNode, aCallbacks);
+ this[aNode.body.type](aNode.body, aNode, aCallbacks);
+ },
+
+ /**
+ * A break statement.
+ *
+ * interface BreakStatement <: Statement {
+ * type: "BreakStatement";
+ * label: Identifier | null;
+ * }
+ */
+ BreakStatement: function STV_BreakStatement(aNode, aParent, aCallbacks) {
+ aNode._parent = aParent;
+
+ if (this.break) {
+ return;
+ }
+ if (aCallbacks.onNode) {
+ if (aCallbacks.onNode(aNode, aParent) === false) {
+ return;
+ }
+ }
+ if (aCallbacks.onBreakStatement) {
+ aCallbacks.onBreakStatement(aNode);
+ }
+ if (aNode.label) {
+ this[aNode.label.type](aNode.label, aNode, aCallbacks);
+ }
+ },
+
+ /**
+ * A continue statement.
+ *
+ * interface ContinueStatement <: Statement {
+ * type: "ContinueStatement";
+ * label: Identifier | null;
+ * }
+ */
+ ContinueStatement: function STV_ContinueStatement(aNode, aParent, aCallbacks) {
+ aNode._parent = aParent;
+
+ if (this.break) {
+ return;
+ }
+ if (aCallbacks.onNode) {
+ if (aCallbacks.onNode(aNode, aParent) === false) {
+ return;
+ }
+ }
+ if (aCallbacks.onContinueStatement) {
+ aCallbacks.onContinueStatement(aNode);
+ }
+ if (aNode.label) {
+ this[aNode.label.type](aNode.label, aNode, aCallbacks);
+ }
+ },
+
+ /**
+ * A with statement.
+ *
+ * interface WithStatement <: Statement {
+ * type: "WithStatement";
+ * object: Expression;
+ * body: Statement;
+ * }
+ */
+ WithStatement: function STV_WithStatement(aNode, aParent, aCallbacks) {
+ aNode._parent = aParent;
+
+ if (this.break) {
+ return;
+ }
+ if (aCallbacks.onNode) {
+ if (aCallbacks.onNode(aNode, aParent) === false) {
+ return;
+ }
+ }
+ if (aCallbacks.onWithStatement) {
+ aCallbacks.onWithStatement(aNode);
+ }
+ this[aNode.object.type](aNode.object, aNode, aCallbacks);
+ this[aNode.body.type](aNode.body, aNode, aCallbacks);
+ },
+
+ /**
+ * A switch statement. The lexical flag is metadata indicating whether the
+ * switch statement contains any unnested let declarations (and therefore
+ * introduces a new lexical scope).
+ *
+ * interface SwitchStatement <: Statement {
+ * type: "SwitchStatement";
+ * discriminant: Expression;
+ * cases: [ SwitchCase ];
+ * lexical: boolean;
+ * }
+ */
+ SwitchStatement: function STV_SwitchStatement(aNode, aParent, aCallbacks) {
+ aNode._parent = aParent;
+
+ if (this.break) {
+ return;
+ }
+ if (aCallbacks.onNode) {
+ if (aCallbacks.onNode(aNode, aParent) === false) {
+ return;
+ }
+ }
+ if (aCallbacks.onSwitchStatement) {
+ aCallbacks.onSwitchStatement(aNode);
+ }
+ this[aNode.discriminant.type](aNode.discriminant, aNode, aCallbacks);
+ for (let _case of aNode.cases) {
+ this[_case.type](_case, aNode, aCallbacks);
+ }
+ },
+
+ /**
+ * A return statement.
+ *
+ * interface ReturnStatement <: Statement {
+ * type: "ReturnStatement";
+ * argument: Expression | null;
+ * }
+ */
+ ReturnStatement: function STV_ReturnStatement(aNode, aParent, aCallbacks) {
+ aNode._parent = aParent;
+
+ if (this.break) {
+ return;
+ }
+ if (aCallbacks.onNode) {
+ if (aCallbacks.onNode(aNode, aParent) === false) {
+ return;
+ }
+ }
+ if (aCallbacks.onReturnStatement) {
+ aCallbacks.onReturnStatement(aNode);
+ }
+ if (aNode.argument) {
+ this[aNode.argument.type](aNode.argument, aNode, aCallbacks);
+ }
+ },
+
+ /**
+ * A throw statement.
+ *
+ * interface ThrowStatement <: Statement {
+ * type: "ThrowStatement";
+ * argument: Expression;
+ * }
+ */
+ ThrowStatement: function STV_ThrowStatement(aNode, aParent, aCallbacks) {
+ aNode._parent = aParent;
+
+ if (this.break) {
+ return;
+ }
+ if (aCallbacks.onNode) {
+ if (aCallbacks.onNode(aNode, aParent) === false) {
+ return;
+ }
+ }
+ if (aCallbacks.onThrowStatement) {
+ aCallbacks.onThrowStatement(aNode);
+ }
+ this[aNode.argument.type](aNode.argument, aNode, aCallbacks);
+ },
+
+ /**
+ * A try statement.
+ *
+ * interface TryStatement <: Statement {
+ * type: "TryStatement";
+ * block: BlockStatement;
+ * handler: CatchClause | null;
+ * guardedHandlers: [ CatchClause ];
+ * finalizer: BlockStatement | null;
+ * }
+ */
+ TryStatement: function STV_TryStatement(aNode, aParent, aCallbacks) {
+ aNode._parent = aParent;
+
+ if (this.break) {
+ return;
+ }
+ if (aCallbacks.onNode) {
+ if (aCallbacks.onNode(aNode, aParent) === false) {
+ return;
+ }
+ }
+ if (aCallbacks.onTryStatement) {
+ aCallbacks.onTryStatement(aNode);
+ }
+ this[aNode.block.type](aNode.block, aNode, aCallbacks);
+ if (aNode.handler) {
+ this[aNode.handler.type](aNode.handler, aNode, aCallbacks);
+ }
+ for (let guardedHandler of aNode.guardedHandlers) {
+ this[guardedHandler.type](guardedHandler, aNode, aCallbacks);
+ }
+ if (aNode.finalizer) {
+ this[aNode.finalizer.type](aNode.finalizer, aNode, aCallbacks);
+ }
+ },
+
+ /**
+ * A while statement.
+ *
+ * interface WhileStatement <: Statement {
+ * type: "WhileStatement";
+ * test: Expression;
+ * body: Statement;
+ * }
+ */
+ WhileStatement: function STV_WhileStatement(aNode, aParent, aCallbacks) {
+ aNode._parent = aParent;
+
+ if (this.break) {
+ return;
+ }
+ if (aCallbacks.onNode) {
+ if (aCallbacks.onNode(aNode, aParent) === false) {
+ return;
+ }
+ }
+ if (aCallbacks.onWhileStatement) {
+ aCallbacks.onWhileStatement(aNode);
+ }
+ this[aNode.test.type](aNode.test, aNode, aCallbacks);
+ this[aNode.body.type](aNode.body, aNode, aCallbacks);
+ },
+
+ /**
+ * A do/while statement.
+ *
+ * interface DoWhileStatement <: Statement {
+ * type: "DoWhileStatement";
+ * body: Statement;
+ * test: Expression;
+ * }
+ */
+ DoWhileStatement: function STV_DoWhileStatement(aNode, aParent, aCallbacks) {
+ aNode._parent = aParent;
+
+ if (this.break) {
+ return;
+ }
+ if (aCallbacks.onNode) {
+ if (aCallbacks.onNode(aNode, aParent) === false) {
+ return;
+ }
+ }
+ if (aCallbacks.onDoWhileStatement) {
+ aCallbacks.onDoWhileStatement(aNode);
+ }
+ this[aNode.body.type](aNode.body, aNode, aCallbacks);
+ this[aNode.test.type](aNode.test, aNode, aCallbacks);
+ },
+
+ /**
+ * A for statement.
+ *
+ * interface ForStatement <: Statement {
+ * type: "ForStatement";
+ * init: VariableDeclaration | Expression | null;
+ * test: Expression | null;
+ * update: Expression | null;
+ * body: Statement;
+ * }
+ */
+ ForStatement: function STV_ForStatement(aNode, aParent, aCallbacks) {
+ aNode._parent = aParent;
+
+ if (this.break) {
+ return;
+ }
+ if (aCallbacks.onNode) {
+ if (aCallbacks.onNode(aNode, aParent) === false) {
+ return;
+ }
+ }
+ if (aCallbacks.onForStatement) {
+ aCallbacks.onForStatement(aNode);
+ }
+ if (aNode.init) {
+ this[aNode.init.type](aNode.init, aNode, aCallbacks);
+ }
+ if (aNode.test) {
+ this[aNode.test.type](aNode.test, aNode, aCallbacks);
+ }
+ if (aNode.update) {
+ this[aNode.update.type](aNode.update, aNode, aCallbacks);
+ }
+ this[aNode.body.type](aNode.body, aNode, aCallbacks);
+ },
+
+ /**
+ * A for/in statement, or, if each is true, a for each/in statement.
+ *
+ * interface ForInStatement <: Statement {
+ * type: "ForInStatement";
+ * left: VariableDeclaration | Expression;
+ * right: Expression;
+ * body: Statement;
+ * each: boolean;
+ * }
+ */
+ ForInStatement: function STV_ForInStatement(aNode, aParent, aCallbacks) {
+ aNode._parent = aParent;
+
+ if (this.break) {
+ return;
+ }
+ if (aCallbacks.onNode) {
+ if (aCallbacks.onNode(aNode, aParent) === false) {
+ return;
+ }
+ }
+ if (aCallbacks.onForInStatement) {
+ aCallbacks.onForInStatement(aNode);
+ }
+ this[aNode.left.type](aNode.left, aNode, aCallbacks);
+ this[aNode.right.type](aNode.right, aNode, aCallbacks);
+ this[aNode.body.type](aNode.body, aNode, aCallbacks);
+ },
+
+ /**
+ * A for/of statement.
+ *
+ * interface ForOfStatement <: Statement {
+ * type: "ForOfStatement";
+ * left: VariableDeclaration | Expression;
+ * right: Expression;
+ * body: Statement;
+ * }
+ */
+ ForOfStatement: function STV_ForOfStatement(aNode, aParent, aCallbacks) {
+ aNode._parent = aParent;
+
+ if (this.break) {
+ return;
+ }
+ if (aCallbacks.onNode) {
+ if (aCallbacks.onNode(aNode, aParent) === false) {
+ return;
+ }
+ }
+ if (aCallbacks.onForOfStatement) {
+ aCallbacks.onForOfStatement(aNode);
+ }
+ this[aNode.left.type](aNode.left, aNode, aCallbacks);
+ this[aNode.right.type](aNode.right, aNode, aCallbacks);
+ this[aNode.body.type](aNode.body, aNode, aCallbacks);
+ },
+
+ /**
+ * A let statement.
+ *
+ * interface LetStatement <: Statement {
+ * type: "LetStatement";
+ * head: [ { id: Pattern, init: Expression | null } ];
+ * body: Statement;
+ * }
+ */
+ LetStatement: function STV_LetStatement(aNode, aParent, aCallbacks) {
+ aNode._parent = aParent;
+
+ if (this.break) {
+ return;
+ }
+ if (aCallbacks.onNode) {
+ if (aCallbacks.onNode(aNode, aParent) === false) {
+ return;
+ }
+ }
+ if (aCallbacks.onLetStatement) {
+ aCallbacks.onLetStatement(aNode);
+ }
+ for (let { id, init } of aNode.head) {
+ this[id.type](id, aNode, aCallbacks);
+ if (init) {
+ this[init.type](init, aNode, aCallbacks);
+ }
+ }
+ this[aNode.body.type](aNode.body, aNode, aCallbacks);
+ },
+
+ /**
+ * A debugger statement.
+ *
+ * interface DebuggerStatement <: Statement {
+ * type: "DebuggerStatement";
+ * }
+ */
+ DebuggerStatement: function STV_DebuggerStatement(aNode, aParent, aCallbacks) {
+ aNode._parent = aParent;
+
+ if (this.break) {
+ return;
+ }
+ if (aCallbacks.onNode) {
+ if (aCallbacks.onNode(aNode, aParent) === false) {
+ return;
+ }
+ }
+ if (aCallbacks.onDebuggerStatement) {
+ aCallbacks.onDebuggerStatement(aNode);
+ }
+ },
+
+ /**
+ * Any declaration node. Note that declarations are considered statements;
+ * this is because declarations can appear in any statement context in the
+ * language recognized by the SpiderMonkey parser.
+ *
+ * interface Declaration <: Statement { }
+ */
+ Declaration: function STV_Declaration(aNode, aParent, aCallbacks) {
+ aNode._parent = aParent;
+
+ if (this.break) {
+ return;
+ }
+ if (aCallbacks.onNode) {
+ if (aCallbacks.onNode(aNode, aParent) === false) {
+ return;
+ }
+ }
+ if (aCallbacks.onDeclaration) {
+ aCallbacks.onDeclaration(aNode);
+ }
+ },
+
+ /**
+ * A function declaration.
+ *
+ * interface FunctionDeclaration <: Function, Declaration {
+ * type: "FunctionDeclaration";
+ * id: Identifier;
+ * params: [ Pattern ];
+ * defaults: [ Expression ];
+ * rest: Identifier | null;
+ * body: BlockStatement | Expression;
+ * generator: boolean;
+ * expression: boolean;
+ * }
+ */
+ FunctionDeclaration: function STV_FunctionDeclaration(aNode, aParent, aCallbacks) {
+ aNode._parent = aParent;
+
+ if (this.break) {
+ return;
+ }
+ if (aCallbacks.onNode) {
+ if (aCallbacks.onNode(aNode, aParent) === false) {
+ return;
+ }
+ }
+ if (aCallbacks.onFunctionDeclaration) {
+ aCallbacks.onFunctionDeclaration(aNode);
+ }
+ this[aNode.id.type](aNode.id, aNode, aCallbacks);
+ for (let param of aNode.params) {
+ this[param.type](param, aNode, aCallbacks);
+ }
+ for (let _default of aNode.defaults) {
+ this[_default.type](_default, aNode, aCallbacks);
+ }
+ if (aNode.rest) {
+ this[aNode.rest.type](aNode.rest, aNode, aCallbacks);
+ }
+ this[aNode.body.type](aNode.body, aNode, aCallbacks);
+ },
+
+ /**
+ * A variable declaration, via one of var, let, or const.
+ *
+ * interface VariableDeclaration <: Declaration {
+ * type: "VariableDeclaration";
+ * declarations: [ VariableDeclarator ];
+ * kind: "var" | "let" | "const";
+ * }
+ */
+ VariableDeclaration: function STV_VariableDeclaration(aNode, aParent, aCallbacks) {
+ aNode._parent = aParent;
+
+ if (this.break) {
+ return;
+ }
+ if (aCallbacks.onNode) {
+ if (aCallbacks.onNode(aNode, aParent) === false) {
+ return;
+ }
+ }
+ if (aCallbacks.onVariableDeclaration) {
+ aCallbacks.onVariableDeclaration(aNode);
+ }
+ for (let declaration of aNode.declarations) {
+ this[declaration.type](declaration, aNode, aCallbacks);
+ }
+ },
+
+ /**
+ * A variable declarator.
+ *
+ * interface VariableDeclarator <: Node {
+ * type: "VariableDeclarator";
+ * id: Pattern;
+ * init: Expression | null;
+ * }
+ */
+ VariableDeclarator: function STV_VariableDeclarator(aNode, aParent, aCallbacks) {
+ aNode._parent = aParent;
+
+ if (this.break) {
+ return;
+ }
+ if (aCallbacks.onNode) {
+ if (aCallbacks.onNode(aNode, aParent) === false) {
+ return;
+ }
+ }
+ if (aCallbacks.onVariableDeclarator) {
+ aCallbacks.onVariableDeclarator(aNode);
+ }
+ this[aNode.id.type](aNode.id, aNode, aCallbacks);
+ if (aNode.init) {
+ this[aNode.init.type](aNode.init, aNode, aCallbacks);
+ }
+ },
+
+ /**
+ * Any expression node. Since the left-hand side of an assignment may be any
+ * expression in general, an expression can also be a pattern.
+ *
+ * interface Expression <: Node, Pattern { }
+ */
+ Expression: function STV_Expression(aNode, aParent, aCallbacks) {
+ aNode._parent = aParent;
+
+ if (this.break) {
+ return;
+ }
+ if (aCallbacks.onNode) {
+ if (aCallbacks.onNode(aNode, aParent) === false) {
+ return;
+ }
+ }
+ if (aCallbacks.onExpression) {
+ aCallbacks.onExpression(aNode);
+ }
+ },
+
+ /**
+ * A this expression.
+ *
+ * interface ThisExpression <: Expression {
+ * type: "ThisExpression";
+ * }
+ */
+ ThisExpression: function STV_ThisExpression(aNode, aParent, aCallbacks) {
+ aNode._parent = aParent;
+
+ if (this.break) {
+ return;
+ }
+ if (aCallbacks.onNode) {
+ if (aCallbacks.onNode(aNode, aParent) === false) {
+ return;
+ }
+ }
+ if (aCallbacks.onThisExpression) {
+ aCallbacks.onThisExpression(aNode);
+ }
+ },
+
+ /**
+ * An array expression.
+ *
+ * interface ArrayExpression <: Expression {
+ * type: "ArrayExpression";
+ * elements: [ Expression | null ];
+ * }
+ */
+ ArrayExpression: function STV_ArrayExpression(aNode, aParent, aCallbacks) {
+ aNode._parent = aParent;
+
+ if (this.break) {
+ return;
+ }
+ if (aCallbacks.onNode) {
+ if (aCallbacks.onNode(aNode, aParent) === false) {
+ return;
+ }
+ }
+ if (aCallbacks.onArrayExpression) {
+ aCallbacks.onArrayExpression(aNode);
+ }
+ for (let element of aNode.elements) {
+ if (element) {
+ this[element.type](element, aNode, aCallbacks);
+ }
+ }
+ },
+
+ /**
+ * An object expression. A literal property in an object expression can have
+ * either a string or number as its value. Ordinary property initializers
+ * have a kind value "init"; getters and setters have the kind values "get"
+ * and "set", respectively.
+ *
+ * interface ObjectExpression <: Expression {
+ * type: "ObjectExpression";
+ * properties: [ { key: Literal | Identifier,
+ * value: Expression,
+ * kind: "init" | "get" | "set" } ];
+ * }
+ */
+ ObjectExpression: function STV_ObjectExpression(aNode, aParent, aCallbacks) {
+ aNode._parent = aParent;
+
+ if (this.break) {
+ return;
+ }
+ if (aCallbacks.onNode) {
+ if (aCallbacks.onNode(aNode, aParent) === false) {
+ return;
+ }
+ }
+ if (aCallbacks.onObjectExpression) {
+ aCallbacks.onObjectExpression(aNode);
+ }
+ for (let { key, value } of aNode.properties) {
+ this[key.type](key, aNode, aCallbacks);
+ this[value.type](value, aNode, aCallbacks);
+ }
+ },
+
+ /**
+ * A function expression.
+ *
+ * interface FunctionExpression <: Function, Expression {
+ * type: "FunctionExpression";
+ * id: Identifier | null;
+ * params: [ Pattern ];
+ * defaults: [ Expression ];
+ * rest: Identifier | null;
+ * body: BlockStatement | Expression;
+ * generator: boolean;
+ * expression: boolean;
+ * }
+ */
+ FunctionExpression: function STV_FunctionExpression(aNode, aParent, aCallbacks) {
+ aNode._parent = aParent;
+
+ if (this.break) {
+ return;
+ }
+ if (aCallbacks.onNode) {
+ if (aCallbacks.onNode(aNode, aParent) === false) {
+ return;
+ }
+ }
+ if (aCallbacks.onFunctionExpression) {
+ aCallbacks.onFunctionExpression(aNode);
+ }
+ if (aNode.id) {
+ this[aNode.id.type](aNode.id, aNode, aCallbacks);
+ }
+ for (let param of aNode.params) {
+ this[param.type](param, aNode, aCallbacks);
+ }
+ for (let _default of aNode.defaults) {
+ this[_default.type](_default, aNode, aCallbacks);
+ }
+ if (aNode.rest) {
+ this[aNode.rest.type](aNode.rest, aNode, aCallbacks);
+ }
+ this[aNode.body.type](aNode.body, aNode, aCallbacks);
+ },
+
+ /**
+ * An arrow expression.
+ *
+ * interface ArrowExpression <: Function, Expression {
+ * type: "ArrowExpression";
+ * params: [ Pattern ];
+ * defaults: [ Expression ];
+ * rest: Identifier | null;
+ * body: BlockStatement | Expression;
+ * generator: boolean;
+ * expression: boolean;
+ * }
+ */
+ ArrowExpression: function STV_ArrowExpression(aNode, aParent, aCallbacks) {
+ aNode._parent = aParent;
+
+ if (this.break) {
+ return;
+ }
+ if (aCallbacks.onNode) {
+ if (aCallbacks.onNode(aNode, aParent) === false) {
+ return;
+ }
+ }
+ if (aCallbacks.onArrowExpression) {
+ aCallbacks.onArrowExpression(aNode);
+ }
+ for (let param of aNode.params) {
+ this[param.type](param, aNode, aCallbacks);
+ }
+ for (let _default of aNode.defaults) {
+ this[_default.type](_default, aNode, aCallbacks);
+ }
+ if (aNode.rest) {
+ this[aNode.rest.type](aNode.rest, aNode, aCallbacks);
+ }
+ this[aNode.body.type](aNode.body, aNode, aCallbacks);
+ },
+
+ /**
+ * A sequence expression, i.e., a comma-separated sequence of expressions.
+ *
+ * interface SequenceExpression <: Expression {
+ * type: "SequenceExpression";
+ * expressions: [ Expression ];
+ * }
+ */
+ SequenceExpression: function STV_SequenceExpression(aNode, aParent, aCallbacks) {
+ aNode._parent = aParent;
+
+ if (this.break) {
+ return;
+ }
+ if (aCallbacks.onNode) {
+ if (aCallbacks.onNode(aNode, aParent) === false) {
+ return;
+ }
+ }
+ if (aCallbacks.onSequenceExpression) {
+ aCallbacks.onSequenceExpression(aNode);
+ }
+ for (let expression of aNode.expressions) {
+ this[expression.type](expression, aNode, aCallbacks);
+ }
+ },
+
+ /**
+ * A unary operator expression.
+ *
+ * interface UnaryExpression <: Expression {
+ * type: "UnaryExpression";
+ * operator: UnaryOperator;
+ * prefix: boolean;
+ * argument: Expression;
+ * }
+ */
+ UnaryExpression: function STV_UnaryExpression(aNode, aParent, aCallbacks) {
+ aNode._parent = aParent;
+
+ if (this.break) {
+ return;
+ }
+ if (aCallbacks.onNode) {
+ if (aCallbacks.onNode(aNode, aParent) === false) {
+ return;
+ }
+ }
+ if (aCallbacks.onUnaryExpression) {
+ aCallbacks.onUnaryExpression(aNode);
+ }
+ this[aNode.argument.type](aNode.argument, aNode, aCallbacks);
+ },
+
+ /**
+ * A binary operator expression.
+ *
+ * interface BinaryExpression <: Expression {
+ * type: "BinaryExpression";
+ * operator: BinaryOperator;
+ * left: Expression;
+ * right: Expression;
+ * }
+ */
+ BinaryExpression: function STV_BinaryExpression(aNode, aParent, aCallbacks) {
+ aNode._parent = aParent;
+
+ if (this.break) {
+ return;
+ }
+ if (aCallbacks.onNode) {
+ if (aCallbacks.onNode(aNode, aParent) === false) {
+ return;
+ }
+ }
+ if (aCallbacks.onBinaryExpression) {
+ aCallbacks.onBinaryExpression(aNode);
+ }
+ this[aNode.left.type](aNode.left, aNode, aCallbacks);
+ this[aNode.right.type](aNode.right, aNode, aCallbacks);
+ },
+
+ /**
+ * An assignment operator expression.
+ *
+ * interface AssignmentExpression <: Expression {
+ * type: "AssignmentExpression";
+ * operator: AssignmentOperator;
+ * left: Expression;
+ * right: Expression;
+ * }
+ */
+ AssignmentExpression: function STV_AssignmentExpression(aNode, aParent, aCallbacks) {
+ aNode._parent = aParent;
+
+ if (this.break) {
+ return;
+ }
+ if (aCallbacks.onNode) {
+ if (aCallbacks.onNode(aNode, aParent) === false) {
+ return;
+ }
+ }
+ if (aCallbacks.onAssignmentExpression) {
+ aCallbacks.onAssignmentExpression(aNode);
+ }
+ this[aNode.left.type](aNode.left, aNode, aCallbacks);
+ this[aNode.right.type](aNode.right, aNode, aCallbacks);
+ },
+
+ /**
+ * An update (increment or decrement) operator expression.
+ *
+ * interface UpdateExpression <: Expression {
+ * type: "UpdateExpression";
+ * operator: UpdateOperator;
+ * argument: Expression;
+ * prefix: boolean;
+ * }
+ */
+ UpdateExpression: function STV_UpdateExpression(aNode, aParent, aCallbacks) {
+ aNode._parent = aParent;
+
+ if (this.break) {
+ return;
+ }
+ if (aCallbacks.onNode) {
+ if (aCallbacks.onNode(aNode, aParent) === false) {
+ return;
+ }
+ }
+ if (aCallbacks.onUpdateExpression) {
+ aCallbacks.onUpdateExpression(aNode);
+ }
+ this[aNode.argument.type](aNode.argument, aNode, aCallbacks);
+ },
+
+ /**
+ * A logical operator expression.
+ *
+ * interface LogicalExpression <: Expression {
+ * type: "LogicalExpression";
+ * operator: LogicalOperator;
+ * left: Expression;
+ * right: Expression;
+ * }
+ */
+ LogicalExpression: function STV_LogicalExpression(aNode, aParent, aCallbacks) {
+ aNode._parent = aParent;
+
+ if (this.break) {
+ return;
+ }
+ if (aCallbacks.onNode) {
+ if (aCallbacks.onNode(aNode, aParent) === false) {
+ return;
+ }
+ }
+ if (aCallbacks.onLogicalExpression) {
+ aCallbacks.onLogicalExpression(aNode);
+ }
+ this[aNode.left.type](aNode.left, aNode, aCallbacks);
+ this[aNode.right.type](aNode.right, aNode, aCallbacks);
+ },
+
+ /**
+ * A conditional expression, i.e., a ternary ?/: expression.
+ *
+ * interface ConditionalExpression <: Expression {
+ * type: "ConditionalExpression";
+ * test: Expression;
+ * alternate: Expression;
+ * consequent: Expression;
+ * }
+ */
+ ConditionalExpression: function STV_ConditionalExpression(aNode, aParent, aCallbacks) {
+ aNode._parent = aParent;
+
+ if (this.break) {
+ return;
+ }
+ if (aCallbacks.onNode) {
+ if (aCallbacks.onNode(aNode, aParent) === false) {
+ return;
+ }
+ }
+ if (aCallbacks.onConditionalExpression) {
+ aCallbacks.onConditionalExpression(aNode);
+ }
+ this[aNode.test.type](aNode.test, aNode, aCallbacks);
+ this[aNode.alternate.type](aNode.alternate, aNode, aCallbacks);
+ this[aNode.consequent.type](aNode.consequent, aNode, aCallbacks);
+ },
+
+ /**
+ * A new expression.
+ *
+ * interface NewExpression <: Expression {
+ * type: "NewExpression";
+ * callee: Expression;
+ * arguments: [ Expression | null ];
+ * }
+ */
+ NewExpression: function STV_NewExpression(aNode, aParent, aCallbacks) {
+ aNode._parent = aParent;
+
+ if (this.break) {
+ return;
+ }
+ if (aCallbacks.onNode) {
+ if (aCallbacks.onNode(aNode, aParent) === false) {
+ return;
+ }
+ }
+ if (aCallbacks.onNewExpression) {
+ aCallbacks.onNewExpression(aNode);
+ }
+ this[aNode.callee.type](aNode.callee, aNode, aCallbacks);
+ for (let argument of aNode.arguments) {
+ if (argument) {
+ this[argument.type](argument, aNode, aCallbacks);
+ }
+ }
+ },
+
+ /**
+ * A function or method call expression.
+ *
+ * interface CallExpression <: Expression {
+ * type: "CallExpression";
+ * callee: Expression;
+ * arguments: [ Expression | null ];
+ * }
+ */
+ CallExpression: function STV_CallExpression(aNode, aParent, aCallbacks) {
+ aNode._parent = aParent;
+
+ if (this.break) {
+ return;
+ }
+ if (aCallbacks.onNode) {
+ if (aCallbacks.onNode(aNode, aParent) === false) {
+ return;
+ }
+ }
+ if (aCallbacks.onCallExpression) {
+ aCallbacks.onCallExpression(aNode);
+ }
+ this[aNode.callee.type](aNode.callee, aNode, aCallbacks);
+ for (let argument of aNode.arguments) {
+ if (argument) {
+ this[argument.type](argument, aNode, aCallbacks);
+ }
+ }
+ },
+
+ /**
+ * A member expression. If computed is true, the node corresponds to a
+ * computed e1[e2] expression and property is an Expression. If computed is
+ * false, the node corresponds to a static e1.x expression and property is an
+ * Identifier.
+ *
+ * interface MemberExpression <: Expression {
+ * type: "MemberExpression";
+ * object: Expression;
+ * property: Identifier | Expression;
+ * computed: boolean;
+ * }
+ */
+ MemberExpression: function STV_MemberExpression(aNode, aParent, aCallbacks) {
+ aNode._parent = aParent;
+
+ if (this.break) {
+ return;
+ }
+ if (aCallbacks.onNode) {
+ if (aCallbacks.onNode(aNode, aParent) === false) {
+ return;
+ }
+ }
+ if (aCallbacks.onMemberExpression) {
+ aCallbacks.onMemberExpression(aNode);
+ }
+ this[aNode.object.type](aNode.object, aNode, aCallbacks);
+ this[aNode.property.type](aNode.property, aNode, aCallbacks);
+ },
+
+ /**
+ * A yield expression.
+ *
+ * interface YieldExpression <: Expression {
+ * argument: Expression | null;
+ * }
+ */
+ YieldExpression: function STV_YieldExpression(aNode, aParent, aCallbacks) {
+ aNode._parent = aParent;
+
+ if (this.break) {
+ return;
+ }
+ if (aCallbacks.onNode) {
+ if (aCallbacks.onNode(aNode, aParent) === false) {
+ return;
+ }
+ }
+ if (aCallbacks.onYieldExpression) {
+ aCallbacks.onYieldExpression(aNode);
+ }
+ if (aNode.argument) {
+ this[aNode.argument.type](aNode.argument, aNode, aCallbacks);
+ }
+ },
+
+ /**
+ * An array comprehension. The blocks array corresponds to the sequence of
+ * for and for each blocks. The optional filter expression corresponds to the
+ * final if clause, if present.
+ *
+ * interface ComprehensionExpression <: Expression {
+ * body: Expression;
+ * blocks: [ ComprehensionBlock ];
+ * filter: Expression | null;
+ * }
+ */
+ ComprehensionExpression: function STV_ComprehensionExpression(aNode, aParent, aCallbacks) {
+ aNode._parent = aParent;
+
+ if (this.break) {
+ return;
+ }
+ if (aCallbacks.onNode) {
+ if (aCallbacks.onNode(aNode, aParent) === false) {
+ return;
+ }
+ }
+ if (aCallbacks.onComprehensionExpression) {
+ aCallbacks.onComprehensionExpression(aNode);
+ }
+ this[aNode.body.type](aNode.body, aNode, aCallbacks);
+ for (let block of aNode.blocks) {
+ this[block.type](block, aNode, aCallbacks);
+ }
+ if (aNode.filter) {
+ this[aNode.filter.type](aNode.filter, aNode, aCallbacks);
+ }
+ },
+
+ /**
+ * A generator expression. As with array comprehensions, the blocks array
+ * corresponds to the sequence of for and for each blocks, and the optional
+ * filter expression corresponds to the final if clause, if present.
+ *
+ * interface GeneratorExpression <: Expression {
+ * body: Expression;
+ * blocks: [ ComprehensionBlock ];
+ * filter: Expression | null;
+ * }
+ */
+ GeneratorExpression: function STV_GeneratorExpression(aNode, aParent, aCallbacks) {
+ aNode._parent = aParent;
+
+ if (this.break) {
+ return;
+ }
+ if (aCallbacks.onNode) {
+ if (aCallbacks.onNode(aNode, aParent) === false) {
+ return;
+ }
+ }
+ if (aCallbacks.onGeneratorExpression) {
+ aCallbacks.onGeneratorExpression(aNode);
+ }
+ this[aNode.body.type](aNode.body, aNode, aCallbacks);
+ for (let block of aNode.blocks) {
+ this[block.type](block, aNode, aCallbacks);
+ }
+ if (aNode.filter) {
+ this[aNode.filter.type](aNode.filter, aNode, aCallbacks);
+ }
+ },
+
+ /**
+ * A graph expression, aka "sharp literal," such as #1={ self: #1# }.
+ *
+ * interface GraphExpression <: Expression {
+ * index: uint32;
+ * expression: Literal;
+ * }
+ */
+ GraphExpression: function STV_GraphExpression(aNode, aParent, aCallbacks) {
+ aNode._parent = aParent;
+
+ if (this.break) {
+ return;
+ }
+ if (aCallbacks.onNode) {
+ if (aCallbacks.onNode(aNode, aParent) === false) {
+ return;
+ }
+ }
+ if (aCallbacks.onGraphExpression) {
+ aCallbacks.onGraphExpression(aNode);
+ }
+ this[aNode.expression.type](aNode.expression, aNode, aCallbacks);
+ },
+
+ /**
+ * A graph index expression, aka "sharp variable," such as #1#.
+ *
+ * interface GraphIndexExpression <: Expression {
+ * index: uint32;
+ * }
+ */
+ GraphIndexExpression: function STV_GraphIndexExpression(aNode, aParent, aCallbacks) {
+ aNode._parent = aParent;
+
+ if (this.break) {
+ return;
+ }
+ if (aCallbacks.onNode) {
+ if (aCallbacks.onNode(aNode, aParent) === false) {
+ return;
+ }
+ }
+ if (aCallbacks.onGraphIndexExpression) {
+ aCallbacks.onGraphIndexExpression(aNode);
+ }
+ },
+
+ /**
+ * A let expression.
+ *
+ * interface LetExpression <: Expression {
+ * type: "LetExpression";
+ * head: [ { id: Pattern, init: Expression | null } ];
+ * body: Expression;
+ * }
+ */
+ LetExpression: function STV_LetExpression(aNode, aParent, aCallbacks) {
+ aNode._parent = aParent;
+
+ if (this.break) {
+ return;
+ }
+ if (aCallbacks.onNode) {
+ if (aCallbacks.onNode(aNode, aParent) === false) {
+ return;
+ }
+ }
+ if (aCallbacks.onLetExpression) {
+ aCallbacks.onLetExpression(aNode);
+ }
+ for (let { id, init } of aNode.head) {
+ this[id.type](id, aNode, aCallbacks);
+ if (init) {
+ this[init.type](init, aNode, aCallbacks);
+ }
+ }
+ this[aNode.body.type](aNode.body, aNode, aCallbacks);
+ },
+
+ /**
+ * Any pattern.
+ *
+ * interface Pattern <: Node { }
+ */
+ Pattern: function STV_Pattern(aNode, aParent, aCallbacks) {
+ aNode._parent = aParent;
+
+ if (this.break) {
+ return;
+ }
+ if (aCallbacks.onNode) {
+ if (aCallbacks.onNode(aNode, aParent) === false) {
+ return;
+ }
+ }
+ if (aCallbacks.onPattern) {
+ aCallbacks.onPattern(aNode);
+ }
+ },
+
+ /**
+ * An object-destructuring pattern. A literal property in an object pattern
+ * can have either a string or number as its value.
+ *
+ * interface ObjectPattern <: Pattern {
+ * type: "ObjectPattern";
+ * properties: [ { key: Literal | Identifier, value: Pattern } ];
+ * }
+ */
+ ObjectPattern: function STV_ObjectPattern(aNode, aParent, aCallbacks) {
+ aNode._parent = aParent;
+
+ if (this.break) {
+ return;
+ }
+ if (aCallbacks.onNode) {
+ if (aCallbacks.onNode(aNode, aParent) === false) {
+ return;
+ }
+ }
+ if (aCallbacks.onObjectPattern) {
+ aCallbacks.onObjectPattern(aNode);
+ }
+ for (let { key, value } of aNode.properties) {
+ this[key.type](key, aNode, aCallbacks);
+ this[value.type](value, aNode, aCallbacks);
+ }
+ },
+
+ /**
+ * An array-destructuring pattern.
+ *
+ * interface ArrayPattern <: Pattern {
+ * type: "ArrayPattern";
+ * elements: [ Pattern | null ];
+ * }
+ */
+ ArrayPattern: function STV_ArrayPattern(aNode, aParent, aCallbacks) {
+ aNode._parent = aParent;
+
+ if (this.break) {
+ return;
+ }
+ if (aCallbacks.onNode) {
+ if (aCallbacks.onNode(aNode, aParent) === false) {
+ return;
+ }
+ }
+ if (aCallbacks.onArrayPattern) {
+ aCallbacks.onArrayPattern(aNode);
+ }
+ for (let element of aNode.elements) {
+ if (element) {
+ this[element.type](element, aNode, aCallbacks);
+ }
+ }
+ },
+
+ /**
+ * A case (if test is an Expression) or default (if test is null) clause in
+ * the body of a switch statement.
+ *
+ * interface SwitchCase <: Node {
+ * type: "SwitchCase";
+ * test: Expression | null;
+ * consequent: [ Statement ];
+ * }
+ */
+ SwitchCase: function STV_SwitchCase(aNode, aParent, aCallbacks) {
+ aNode._parent = aParent;
+
+ if (this.break) {
+ return;
+ }
+ if (aCallbacks.onNode) {
+ if (aCallbacks.onNode(aNode, aParent) === false) {
+ return;
+ }
+ }
+ if (aCallbacks.onSwitchCase) {
+ aCallbacks.onSwitchCase(aNode);
+ }
+ if (aNode.test) {
+ this[aNode.test.type](aNode.test, aNode, aCallbacks);
+ }
+ for (let consequent of aNode.consequent) {
+ this[consequent.type](consequent, aNode, aCallbacks);
+ }
+ },
+
+ /**
+ * A catch clause following a try block. The optional guard property
+ * corresponds to the optional expression guard on the bound variable.
+ *
+ * interface CatchClause <: Node {
+ * type: "CatchClause";
+ * param: Pattern;
+ * guard: Expression | null;
+ * body: BlockStatement;
+ * }
+ */
+ CatchClause: function STV_CatchClause(aNode, aParent, aCallbacks) {
+ aNode._parent = aParent;
+
+ if (this.break) {
+ return;
+ }
+ if (aCallbacks.onNode) {
+ if (aCallbacks.onNode(aNode, aParent) === false) {
+ return;
+ }
+ }
+ if (aCallbacks.onCatchClause) {
+ aCallbacks.onCatchClause(aNode);
+ }
+ this[aNode.param.type](aNode.param, aNode, aCallbacks);
+ if (aNode.guard) {
+ this[aNode.guard.type](aNode.guard, aNode, aCallbacks);
+ }
+ this[aNode.body.type](aNode.body, aNode, aCallbacks);
+ },
+
+ /**
+ * A for or for each block in an array comprehension or generator expression.
+ *
+ * interface ComprehensionBlock <: Node {
+ * left: Pattern;
+ * right: Expression;
+ * each: boolean;
+ * }
+ */
+ ComprehensionBlock: function STV_ComprehensionBlock(aNode, aParent, aCallbacks) {
+ aNode._parent = aParent;
+
+ if (this.break) {
+ return;
+ }
+ if (aCallbacks.onNode) {
+ if (aCallbacks.onNode(aNode, aParent) === false) {
+ return;
+ }
+ }
+ if (aCallbacks.onComprehensionBlock) {
+ aCallbacks.onComprehensionBlock(aNode);
+ }
+ this[aNode.left.type](aNode.left, aNode, aCallbacks);
+ this[aNode.right.type](aNode.right, aNode, aCallbacks);
+ },
+
+ /**
+ * An identifier. Note that an identifier may be an expression or a
+ * destructuring pattern.
+ *
+ * interface Identifier <: Node, Expression, Pattern {
+ * type: "Identifier";
+ * name: string;
+ * }
+ */
+ Identifier: function STV_Identifier(aNode, aParent, aCallbacks) {
+ aNode._parent = aParent;
+
+ if (this.break) {
+ return;
+ }
+ if (aCallbacks.onNode) {
+ if (aCallbacks.onNode(aNode, aParent) === false) {
+ return;
+ }
+ }
+ if (aCallbacks.onIdentifier) {
+ aCallbacks.onIdentifier(aNode);
+ }
+ },
+
+ /**
+ * A literal token. Note that a literal can be an expression.
+ *
+ * interface Literal <: Node, Expression {
+ * type: "Literal";
+ * value: string | boolean | null | number | RegExp;
+ * }
+ */
+ Literal: function STV_Literal(aNode, aParent, aCallbacks) {
+ aNode._parent = aParent;
+
+ if (this.break) {
+ return;
+ }
+ if (aCallbacks.onNode) {
+ if (aCallbacks.onNode(aNode, aParent) === false) {
+ return;
+ }
+ }
+ if (aCallbacks.onLiteral) {
+ aCallbacks.onLiteral(aNode);
+ }
+ }
+};
+
+/**
+ * Logs a warning.
+ *
+ * @param string aStr
+ * The message to be displayed.
+ * @param Exception aEx
+ * The thrown exception.
+ */
+function log(aStr, aEx) {
+ let msg = "Warning: " + aStr + ", " + aEx + "\n" + aEx.stack;
+ Cu.reportError(msg);
+ dump(msg + "\n");
+};
+
+XPCOMUtils.defineLazyGetter(Parser, "reflectionAPI", function() Reflect);
diff --git a/browser/devtools/shared/SplitView.jsm b/browser/devtools/shared/SplitView.jsm
new file mode 100644
index 000000000..ec8376f45
--- /dev/null
+++ b/browser/devtools/shared/SplitView.jsm
@@ -0,0 +1,302 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* 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";
+
+this.EXPORTED_SYMBOLS = ["SplitView"];
+
+/* this must be kept in sync with CSS (ie. splitview.css) */
+const LANDSCAPE_MEDIA_QUERY = "(min-width: 551px)";
+
+let bindings = new WeakMap();
+
+/**
+ * SplitView constructor
+ *
+ * Initialize the split view UI on an existing DOM element.
+ *
+ * A split view contains items, each of those having one summary and one details
+ * elements.
+ * It is adaptive as it behaves similarly to a richlistbox when there the aspect
+ * ratio is narrow or as a pair listbox-box otherwise.
+ *
+ * @param DOMElement aRoot
+ * @see appendItem
+ */
+this.SplitView = function SplitView(aRoot)
+{
+ this._root = aRoot;
+ this._controller = aRoot.querySelector(".splitview-controller");
+ this._nav = aRoot.querySelector(".splitview-nav");
+ this._side = aRoot.querySelector(".splitview-side-details");
+ this._activeSummary = null
+
+ this._mql = aRoot.ownerDocument.defaultView.matchMedia(LANDSCAPE_MEDIA_QUERY);
+
+ // items list focus and search-on-type handling
+ this._nav.addEventListener("keydown", function onKeyCatchAll(aEvent) {
+ function getFocusedItemWithin(nav) {
+ let node = nav.ownerDocument.activeElement;
+ while (node && node.parentNode != nav) {
+ node = node.parentNode;
+ }
+ return node;
+ }
+
+ // do not steal focus from inside iframes or textboxes
+ if (aEvent.target.ownerDocument != this._nav.ownerDocument ||
+ aEvent.target.tagName == "input" ||
+ aEvent.target.tagName == "textbox" ||
+ aEvent.target.tagName == "textarea" ||
+ aEvent.target.classList.contains("textbox")) {
+ return false;
+ }
+
+ // handle keyboard navigation within the items list
+ let newFocusOrdinal;
+ if (aEvent.keyCode == aEvent.DOM_VK_PAGE_UP ||
+ aEvent.keyCode == aEvent.DOM_VK_HOME) {
+ newFocusOrdinal = 0;
+ } else if (aEvent.keyCode == aEvent.DOM_VK_PAGE_DOWN ||
+ aEvent.keyCode == aEvent.DOM_VK_END) {
+ newFocusOrdinal = this._nav.childNodes.length - 1;
+ } else if (aEvent.keyCode == aEvent.DOM_VK_UP) {
+ newFocusOrdinal = getFocusedItemWithin(this._nav).getAttribute("data-ordinal");
+ newFocusOrdinal--;
+ } else if (aEvent.keyCode == aEvent.DOM_VK_DOWN) {
+ newFocusOrdinal = getFocusedItemWithin(this._nav).getAttribute("data-ordinal");
+ newFocusOrdinal++;
+ }
+ if (newFocusOrdinal !== undefined) {
+ aEvent.stopPropagation();
+ let el = this.getSummaryElementByOrdinal(newFocusOrdinal);
+ if (el) {
+ el.focus();
+ }
+ return false;
+ }
+ }.bind(this), false);
+}
+
+SplitView.prototype = {
+ /**
+ * Retrieve whether the UI currently has a landscape orientation.
+ *
+ * @return boolean
+ */
+ get isLandscape() this._mql.matches,
+
+ /**
+ * Retrieve the root element.
+ *
+ * @return DOMElement
+ */
+ get rootElement() this._root,
+
+ /**
+ * Retrieve the active item's summary element or null if there is none.
+ *
+ * @return DOMElement
+ */
+ get activeSummary() this._activeSummary,
+
+ /**
+ * Set the active item's summary element.
+ *
+ * @param DOMElement aSummary
+ */
+ set activeSummary(aSummary)
+ {
+ if (aSummary == this._activeSummary) {
+ return;
+ }
+
+ if (this._activeSummary) {
+ let binding = bindings.get(this._activeSummary);
+
+ if (binding.onHide) {
+ binding.onHide(this._activeSummary, binding._details, binding.data);
+ }
+
+ this._activeSummary.classList.remove("splitview-active");
+ binding._details.classList.remove("splitview-active");
+ }
+
+ if (!aSummary) {
+ return;
+ }
+
+ let binding = bindings.get(aSummary);
+ aSummary.classList.add("splitview-active");
+ binding._details.classList.add("splitview-active");
+
+ this._activeSummary = aSummary;
+
+ if (binding.onShow) {
+ binding.onShow(aSummary, binding._details, binding.data);
+ }
+ },
+
+ /**
+ * Retrieve the active item's details element or null if there is none.
+ * @return DOMElement
+ */
+ get activeDetails()
+ {
+ let summary = this.activeSummary;
+ return summary ? bindings.get(summary)._details : null;
+ },
+
+ /**
+ * Retrieve the summary element for a given ordinal.
+ *
+ * @param number aOrdinal
+ * @return DOMElement
+ * Summary element with given ordinal or null if not found.
+ * @see appendItem
+ */
+ getSummaryElementByOrdinal: function SEC_getSummaryElementByOrdinal(aOrdinal)
+ {
+ return this._nav.querySelector("* > li[data-ordinal='" + aOrdinal + "']");
+ },
+
+ /**
+ * Append an item to the split view.
+ *
+ * @param DOMElement aSummary
+ * The summary element for the item.
+ * @param DOMElement aDetails
+ * The details element for the item.
+ * @param object aOptions
+ * Optional object that defines custom behavior and data for the item.
+ * All properties are optional :
+ * - function(DOMElement summary, DOMElement details, object data) onCreate
+ * Called when the item has been added.
+ * - function(summary, details, data) onShow
+ * Called when the item is shown/active.
+ * - function(summary, details, data) onHide
+ * Called when the item is hidden/inactive.
+ * - function(summary, details, data) onDestroy
+ * Called when the item has been removed.
+ * - object data
+ * Object to pass to the callbacks above.
+ * - number ordinal
+ * Items with a lower ordinal are displayed before those with a
+ * higher ordinal.
+ */
+ appendItem: function ASV_appendItem(aSummary, aDetails, aOptions)
+ {
+ let binding = aOptions || {};
+
+ binding._summary = aSummary;
+ binding._details = aDetails;
+ bindings.set(aSummary, binding);
+
+ this._nav.appendChild(aSummary);
+
+ aSummary.addEventListener("click", function onSummaryClick(aEvent) {
+ aEvent.stopPropagation();
+ this.activeSummary = aSummary;
+ }.bind(this), false);
+
+ this._side.appendChild(aDetails);
+
+ if (binding.onCreate) {
+ // queue onCreate handler
+ this._root.ownerDocument.defaultView.setTimeout(function () {
+ binding.onCreate(aSummary, aDetails, binding.data);
+ }, 0);
+ }
+ },
+
+ /**
+ * Append an item to the split view according to two template elements
+ * (one for the item's summary and the other for the item's details).
+ *
+ * @param string aName
+ * Name of the template elements to instantiate.
+ * Requires two (hidden) DOM elements with id "splitview-tpl-summary-"
+ * and "splitview-tpl-details-" suffixed with aName.
+ * @param object aOptions
+ * Optional object that defines custom behavior and data for the item.
+ * See appendItem for full description.
+ * @return object{summary:,details:}
+ * Object with the new DOM elements created for summary and details.
+ * @see appendItem
+ */
+ appendTemplatedItem: function ASV_appendTemplatedItem(aName, aOptions)
+ {
+ aOptions = aOptions || {};
+ let summary = this._root.querySelector("#splitview-tpl-summary-" + aName);
+ let details = this._root.querySelector("#splitview-tpl-details-" + aName);
+
+ summary = summary.cloneNode(true);
+ summary.id = "";
+ if (aOptions.ordinal !== undefined) { // can be zero
+ summary.style.MozBoxOrdinalGroup = aOptions.ordinal;
+ summary.setAttribute("data-ordinal", aOptions.ordinal);
+ }
+ details = details.cloneNode(true);
+ details.id = "";
+
+ this.appendItem(summary, details, aOptions);
+ return {summary: summary, details: details};
+ },
+
+ /**
+ * Remove an item from the split view.
+ *
+ * @param DOMElement aSummary
+ * Summary element of the item to remove.
+ */
+ removeItem: function ASV_removeItem(aSummary)
+ {
+ if (aSummary == this._activeSummary) {
+ this.activeSummary = null;
+ }
+
+ let binding = bindings.get(aSummary);
+ aSummary.parentNode.removeChild(aSummary);
+ binding._details.parentNode.removeChild(binding._details);
+
+ if (binding.onDestroy) {
+ binding.onDestroy(aSummary, binding._details, binding.data);
+ }
+ },
+
+ /**
+ * Remove all items from the split view.
+ */
+ removeAll: function ASV_removeAll()
+ {
+ while (this._nav.hasChildNodes()) {
+ this.removeItem(this._nav.firstChild);
+ }
+ },
+
+ /**
+ * Set the item's CSS class name.
+ * This sets the class on both the summary and details elements, retaining
+ * any SplitView-specific classes.
+ *
+ * @param DOMElement aSummary
+ * Summary element of the item to set.
+ * @param string aClassName
+ * One or more space-separated CSS classes.
+ */
+ setItemClassName: function ASV_setItemClassName(aSummary, aClassName)
+ {
+ let binding = bindings.get(aSummary);
+ let viewSpecific;
+
+ viewSpecific = aSummary.className.match(/(splitview\-[\w-]+)/g);
+ viewSpecific = viewSpecific ? viewSpecific.join(" ") : "";
+ aSummary.className = viewSpecific + " " + aClassName;
+
+ viewSpecific = binding._details.className.match(/(splitview\-[\w-]+)/g);
+ viewSpecific = viewSpecific ? viewSpecific.join(" ") : "";
+ binding._details.className = viewSpecific + " " + aClassName;
+ },
+};
diff --git a/browser/devtools/shared/event-emitter.js b/browser/devtools/shared/event-emitter.js
new file mode 100644
index 000000000..44f5fd5e3
--- /dev/null
+++ b/browser/devtools/shared/event-emitter.js
@@ -0,0 +1,118 @@
+/* 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/. */
+
+/**
+ * EventEmitter.
+ */
+
+this.EventEmitter = function EventEmitter() {};
+
+if (typeof(require) === "function") {
+ module.exports = EventEmitter;
+ var {Cu} = require("chrome");
+} else {
+ var EXPORTED_SYMBOLS = ["EventEmitter"];
+ var Cu = this["Components"].utils;
+}
+
+/**
+ * Decorate an object with event emitter functionality.
+ *
+ * @param Object aObjectToDecorate
+ * Bind all public methods of EventEmitter to
+ * the aObjectToDecorate object.
+ */
+EventEmitter.decorate = function EventEmitter_decorate (aObjectToDecorate) {
+ let emitter = new EventEmitter();
+ aObjectToDecorate.on = emitter.on.bind(emitter);
+ aObjectToDecorate.off = emitter.off.bind(emitter);
+ aObjectToDecorate.once = emitter.once.bind(emitter);
+ aObjectToDecorate.emit = emitter.emit.bind(emitter);
+};
+
+EventEmitter.prototype = {
+ /**
+ * Connect a listener.
+ *
+ * @param string aEvent
+ * The event name to which we're connecting.
+ * @param function aListener
+ * Called when the event is fired.
+ */
+ on: function EventEmitter_on(aEvent, aListener) {
+ if (!this._eventEmitterListeners)
+ this._eventEmitterListeners = new Map();
+ if (!this._eventEmitterListeners.has(aEvent)) {
+ this._eventEmitterListeners.set(aEvent, []);
+ }
+ this._eventEmitterListeners.get(aEvent).push(aListener);
+ },
+
+ /**
+ * Listen for the next time an event is fired.
+ *
+ * @param string aEvent
+ * The event name to which we're connecting.
+ * @param function aListener
+ * Called when the event is fired. Will be called at most one time.
+ */
+ once: function EventEmitter_once(aEvent, aListener) {
+ let handler = function() {
+ this.off(aEvent, handler);
+ aListener.apply(null, arguments);
+ }.bind(this);
+ this.on(aEvent, handler);
+ },
+
+ /**
+ * Remove a previously-registered event listener. Works for events
+ * registered with either on or once.
+ *
+ * @param string aEvent
+ * The event name whose listener we're disconnecting.
+ * @param function aListener
+ * The listener to remove.
+ */
+ off: function EventEmitter_off(aEvent, aListener) {
+ if (!this._eventEmitterListeners)
+ return;
+ let listeners = this._eventEmitterListeners.get(aEvent);
+ if (listeners) {
+ this._eventEmitterListeners.set(aEvent, listeners.filter(function(l) aListener != l));
+ }
+ },
+
+ /**
+ * Emit an event. All arguments to this method will
+ * be sent to listner functions.
+ */
+ emit: function EventEmitter_emit(aEvent) {
+ if (!this._eventEmitterListeners || !this._eventEmitterListeners.has(aEvent))
+ return;
+
+ let originalListeners = this._eventEmitterListeners.get(aEvent);
+ for (let listener of this._eventEmitterListeners.get(aEvent)) {
+ // If the object was destroyed during event emission, stop
+ // emitting.
+ if (!this._eventEmitterListeners) {
+ break;
+ }
+
+ // If listeners were removed during emission, make sure the
+ // event handler we're going to fire wasn't removed.
+ if (originalListeners === this._eventEmitterListeners.get(aEvent) ||
+ this._eventEmitterListeners.get(aEvent).some(function(l) l === listener)) {
+ try {
+ listener.apply(null, arguments);
+ }
+ catch (ex) {
+ // Prevent a bad listener from interfering with the others.
+ let msg = ex + ": " + ex.stack;
+ Cu.reportError(msg);
+ dump(msg + "\n");
+ }
+ }
+ }
+ }
+};
diff --git a/browser/devtools/shared/inplace-editor.js b/browser/devtools/shared/inplace-editor.js
new file mode 100644
index 000000000..cc5d28038
--- /dev/null
+++ b/browser/devtools/shared/inplace-editor.js
@@ -0,0 +1,851 @@
+/* -*- Mode: javascript; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+
+/**
+ * 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/.
+ *
+ * Basic use:
+ * let spanToEdit = document.getElementById("somespan");
+ *
+ * editableField({
+ * element: spanToEdit,
+ * done: function(value, commit) {
+ * if (commit) {
+ * spanToEdit.textContent = value;
+ * }
+ * },
+ * trigger: "dblclick"
+ * });
+ *
+ * See editableField() for more options.
+ */
+
+"use strict";
+
+const {Ci, Cu} = require("chrome");
+
+const HTML_NS = "http://www.w3.org/1999/xhtml";
+
+const FOCUS_FORWARD = Ci.nsIFocusManager.MOVEFOCUS_FORWARD;
+const FOCUS_BACKWARD = Ci.nsIFocusManager.MOVEFOCUS_BACKWARD;
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+/**
+ * Mark a span editable. |editableField| will listen for the span to
+ * be focused and create an InlineEditor to handle text input.
+ * Changes will be committed when the InlineEditor's input is blurred
+ * or dropped when the user presses escape.
+ *
+ * @param {object} aOptions
+ * Options for the editable field, including:
+ * {Element} element:
+ * (required) The span to be edited on focus.
+ * {function} canEdit:
+ * Will be called before creating the inplace editor. Editor
+ * won't be created if canEdit returns false.
+ * {function} start:
+ * Will be called when the inplace editor is initialized.
+ * {function} change:
+ * Will be called when the text input changes. Will be called
+ * with the current value of the text input.
+ * {function} done:
+ * Called when input is committed or blurred. Called with
+ * current value and a boolean telling the caller whether to
+ * commit the change. This function is called before the editor
+ * has been torn down.
+ * {function} destroy:
+ * Called when the editor is destroyed and has been torn down.
+ * {string} advanceChars:
+ * If any characters in advanceChars are typed, focus will advance
+ * to the next element.
+ * {boolean} stopOnReturn:
+ * If true, the return key will not advance the editor to the next
+ * focusable element.
+ * {string} trigger: The DOM event that should trigger editing,
+ * defaults to "click"
+ */
+function editableField(aOptions)
+{
+ return editableItem(aOptions, function(aElement, aEvent) {
+ new InplaceEditor(aOptions, aEvent);
+ });
+}
+
+exports.editableField = editableField;
+
+/**
+ * Handle events for an element that should respond to
+ * clicks and sit in the editing tab order, and call
+ * a callback when it is activated.
+ *
+ * @param {object} aOptions
+ * The options for this editor, including:
+ * {Element} element: The DOM element.
+ * {string} trigger: The DOM event that should trigger editing,
+ * defaults to "click"
+ * @param {function} aCallback
+ * Called when the editor is activated.
+ */
+function editableItem(aOptions, aCallback)
+{
+ let trigger = aOptions.trigger || "click"
+ let element = aOptions.element;
+ element.addEventListener(trigger, function(evt) {
+ let win = this.ownerDocument.defaultView;
+ let selection = win.getSelection();
+ if (trigger != "click" || selection.isCollapsed) {
+ aCallback(element, evt);
+ }
+ evt.stopPropagation();
+ }, false);
+
+ // If focused by means other than a click, start editing by
+ // pressing enter or space.
+ element.addEventListener("keypress", function(evt) {
+ if (evt.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_RETURN ||
+ evt.charCode === Ci.nsIDOMKeyEvent.DOM_VK_SPACE) {
+ aCallback(element);
+ }
+ }, true);
+
+ // Ugly workaround - the element is focused on mousedown but
+ // the editor is activated on click/mouseup. This leads
+ // to an ugly flash of the focus ring before showing the editor.
+ // So hide the focus ring while the mouse is down.
+ element.addEventListener("mousedown", function(evt) {
+ let cleanup = function() {
+ element.style.removeProperty("outline-style");
+ element.removeEventListener("mouseup", cleanup, false);
+ element.removeEventListener("mouseout", cleanup, false);
+ };
+ element.style.setProperty("outline-style", "none");
+ element.addEventListener("mouseup", cleanup, false);
+ element.addEventListener("mouseout", cleanup, false);
+ }, false);
+
+ // Mark the element editable field for tab
+ // navigation while editing.
+ element._editable = true;
+}
+
+exports.editableItem = this.editableItem;
+
+/*
+ * Various API consumers (especially tests) sometimes want to grab the
+ * inplaceEditor expando off span elements. However, when each global has its
+ * own compartment, those expandos live on Xray wrappers that are only visible
+ * within this JSM. So we provide a little workaround here.
+ */
+
+function getInplaceEditorForSpan(aSpan)
+{
+ return aSpan.inplaceEditor;
+};
+exports.getInplaceEditorForSpan = getInplaceEditorForSpan;
+
+function InplaceEditor(aOptions, aEvent)
+{
+ this.elt = aOptions.element;
+ let doc = this.elt.ownerDocument;
+ this.doc = doc;
+ this.elt.inplaceEditor = this;
+
+ this.change = aOptions.change;
+ this.done = aOptions.done;
+ this.destroy = aOptions.destroy;
+ this.initial = aOptions.initial ? aOptions.initial : this.elt.textContent;
+ this.multiline = aOptions.multiline || false;
+ this.stopOnReturn = !!aOptions.stopOnReturn;
+
+ this._onBlur = this._onBlur.bind(this);
+ this._onKeyPress = this._onKeyPress.bind(this);
+ this._onInput = this._onInput.bind(this);
+ this._onKeyup = this._onKeyup.bind(this);
+
+ this._createInput();
+ this._autosize();
+
+ // Pull out character codes for advanceChars, listing the
+ // characters that should trigger a blur.
+ this._advanceCharCodes = {};
+ let advanceChars = aOptions.advanceChars || '';
+ for (let i = 0; i < advanceChars.length; i++) {
+ this._advanceCharCodes[advanceChars.charCodeAt(i)] = true;
+ }
+
+ // Hide the provided element and add our editor.
+ this.originalDisplay = this.elt.style.display;
+ this.elt.style.display = "none";
+ this.elt.parentNode.insertBefore(this.input, this.elt);
+
+ if (typeof(aOptions.selectAll) == "undefined" || aOptions.selectAll) {
+ this.input.select();
+ }
+ this.input.focus();
+
+ this.input.addEventListener("blur", this._onBlur, false);
+ this.input.addEventListener("keypress", this._onKeyPress, false);
+ this.input.addEventListener("input", this._onInput, false);
+ this.input.addEventListener("mousedown", function(aEvt) {
+ aEvt.stopPropagation();
+ }, false);
+
+ this.warning = aOptions.warning;
+ this.validate = aOptions.validate;
+
+ if (this.warning && this.validate) {
+ this.input.addEventListener("keyup", this._onKeyup, false);
+ }
+
+ if (aOptions.start) {
+ aOptions.start(this, aEvent);
+ }
+}
+
+exports.InplaceEditor = InplaceEditor;
+
+InplaceEditor.prototype = {
+ _createInput: function InplaceEditor_createEditor()
+ {
+ this.input =
+ this.doc.createElementNS(HTML_NS, this.multiline ? "textarea" : "input");
+ this.input.inplaceEditor = this;
+ this.input.classList.add("styleinspector-propertyeditor");
+ this.input.value = this.initial;
+
+ copyTextStyles(this.elt, this.input);
+ },
+
+ /**
+ * Get rid of the editor.
+ */
+ _clear: function InplaceEditor_clear()
+ {
+ if (!this.input) {
+ // Already cleared.
+ return;
+ }
+
+ this.input.removeEventListener("blur", this._onBlur, false);
+ this.input.removeEventListener("keypress", this._onKeyPress, false);
+ this.input.removeEventListener("keyup", this._onKeyup, false);
+ this.input.removeEventListener("oninput", this._onInput, false);
+ this._stopAutosize();
+
+ this.elt.style.display = this.originalDisplay;
+ this.elt.focus();
+
+ if (this.destroy) {
+ this.destroy();
+ }
+
+ this.elt.parentNode.removeChild(this.input);
+ this.input = null;
+
+ delete this.elt.inplaceEditor;
+ delete this.elt;
+ },
+
+ /**
+ * Keeps the editor close to the size of its input string. This is pretty
+ * crappy, suggestions for improvement welcome.
+ */
+ _autosize: function InplaceEditor_autosize()
+ {
+ // Create a hidden, absolutely-positioned span to measure the text
+ // in the input. Boo.
+
+ // We can't just measure the original element because a) we don't
+ // change the underlying element's text ourselves (we leave that
+ // up to the client), and b) without tweaking the style of the
+ // original element, it might wrap differently or something.
+ this._measurement =
+ this.doc.createElementNS(HTML_NS, this.multiline ? "pre" : "span");
+ this._measurement.className = "autosizer";
+ this.elt.parentNode.appendChild(this._measurement);
+ let style = this._measurement.style;
+ style.visibility = "hidden";
+ style.position = "absolute";
+ style.top = "0";
+ style.left = "0";
+ copyTextStyles(this.input, this._measurement);
+ this._updateSize();
+ },
+
+ /**
+ * Clean up the mess created by _autosize().
+ */
+ _stopAutosize: function InplaceEditor_stopAutosize()
+ {
+ if (!this._measurement) {
+ return;
+ }
+ this._measurement.parentNode.removeChild(this._measurement);
+ delete this._measurement;
+ },
+
+ /**
+ * Size the editor to fit its current contents.
+ */
+ _updateSize: function InplaceEditor_updateSize()
+ {
+ // Replace spaces with non-breaking spaces. Otherwise setting
+ // the span's textContent will collapse spaces and the measurement
+ // will be wrong.
+ this._measurement.textContent = this.input.value.replace(/ /g, '\u00a0');
+
+ // We add a bit of padding to the end. Should be enough to fit
+ // any letter that could be typed, otherwise we'll scroll before
+ // we get a chance to resize. Yuck.
+ let width = this._measurement.offsetWidth + 10;
+
+ if (this.multiline) {
+ // Make sure there's some content in the current line. This is a hack to
+ // account for the fact that after adding a newline the <pre> doesn't grow
+ // unless there's text content on the line.
+ width += 15;
+ this._measurement.textContent += "M";
+ this.input.style.height = this._measurement.offsetHeight + "px";
+ }
+
+ this.input.style.width = width + "px";
+ },
+
+ /**
+ * Increment property values in rule view.
+ *
+ * @param {number} increment
+ * The amount to increase/decrease the property value.
+ * @return {bool} true if value has been incremented.
+ */
+ _incrementValue: function InplaceEditor_incrementValue(increment)
+ {
+ let value = this.input.value;
+ let selectionStart = this.input.selectionStart;
+ let selectionEnd = this.input.selectionEnd;
+
+ let newValue = this._incrementCSSValue(value, increment, selectionStart,
+ selectionEnd);
+
+ if (!newValue) {
+ return false;
+ }
+
+ this.input.value = newValue.value;
+ this.input.setSelectionRange(newValue.start, newValue.end);
+
+ return true;
+ },
+
+ /**
+ * Increment the property value based on the property type.
+ *
+ * @param {string} value
+ * Property value.
+ * @param {number} increment
+ * Amount to increase/decrease the property value.
+ * @param {number} selStart
+ * Starting index of the value.
+ * @param {number} selEnd
+ * Ending index of the value.
+ * @return {object} object with properties 'value', 'start', and 'end'.
+ */
+ _incrementCSSValue: function InplaceEditor_incrementCSSValue(value, increment,
+ selStart, selEnd)
+ {
+ let range = this._parseCSSValue(value, selStart);
+ let type = (range && range.type) || "";
+ let rawValue = (range ? value.substring(range.start, range.end) : "");
+ let incrementedValue = null, selection;
+
+ if (type === "num") {
+ let newValue = this._incrementRawValue(rawValue, increment);
+ if (newValue !== null) {
+ incrementedValue = newValue;
+ selection = [0, incrementedValue.length];
+ }
+ } else if (type === "hex") {
+ let exprOffset = selStart - range.start;
+ let exprOffsetEnd = selEnd - range.start;
+ let newValue = this._incHexColor(rawValue, increment, exprOffset,
+ exprOffsetEnd);
+ if (newValue) {
+ incrementedValue = newValue.value;
+ selection = newValue.selection;
+ }
+ } else {
+ let info;
+ if (type === "rgb" || type === "hsl") {
+ info = {};
+ let part = value.substring(range.start, selStart).split(",").length - 1;
+ if (part === 3) { // alpha
+ info.minValue = 0;
+ info.maxValue = 1;
+ } else if (type === "rgb") {
+ info.minValue = 0;
+ info.maxValue = 255;
+ } else if (part !== 0) { // hsl percentage
+ info.minValue = 0;
+ info.maxValue = 100;
+
+ // select the previous number if the selection is at the end of a
+ // percentage sign.
+ if (value.charAt(selStart - 1) === "%") {
+ --selStart;
+ }
+ }
+ }
+ return this._incrementGenericValue(value, increment, selStart, selEnd, info);
+ }
+
+ if (incrementedValue === null) {
+ return;
+ }
+
+ let preRawValue = value.substr(0, range.start);
+ let postRawValue = value.substr(range.end);
+
+ return {
+ value: preRawValue + incrementedValue + postRawValue,
+ start: range.start + selection[0],
+ end: range.start + selection[1]
+ };
+ },
+
+ /**
+ * Parses the property value and type.
+ *
+ * @param {string} value
+ * Property value.
+ * @param {number} offset
+ * Starting index of value.
+ * @return {object} object with properties 'value', 'start', 'end', and 'type'.
+ */
+ _parseCSSValue: function InplaceEditor_parseCSSValue(value, offset)
+ {
+ const reSplitCSS = /(url\("?[^"\)]+"?\)?)|(rgba?\([^)]*\)?)|(hsla?\([^)]*\)?)|(#[\dA-Fa-f]+)|(-?\d+(\.\d+)?(%|[a-z]{1,4})?)|"([^"]*)"?|'([^']*)'?|([^,\s\/!\(\)]+)|(!(.*)?)/;
+ let start = 0;
+ let m;
+
+ // retreive values from left to right until we find the one at our offset
+ while ((m = reSplitCSS.exec(value)) &&
+ (m.index + m[0].length < offset)) {
+ value = value.substr(m.index + m[0].length);
+ start += m.index + m[0].length;
+ offset -= m.index + m[0].length;
+ }
+
+ if (!m) {
+ return;
+ }
+
+ let type;
+ if (m[1]) {
+ type = "url";
+ } else if (m[2]) {
+ type = "rgb";
+ } else if (m[3]) {
+ type = "hsl";
+ } else if (m[4]) {
+ type = "hex";
+ } else if (m[5]) {
+ type = "num";
+ }
+
+ return {
+ value: m[0],
+ start: start + m.index,
+ end: start + m.index + m[0].length,
+ type: type
+ };
+ },
+
+ /**
+ * Increment the property value for types other than
+ * number or hex, such as rgb, hsl, and file names.
+ *
+ * @param {string} value
+ * Property value.
+ * @param {number} increment
+ * Amount to increment/decrement.
+ * @param {number} offset
+ * Starting index of the property value.
+ * @param {number} offsetEnd
+ * Ending index of the property value.
+ * @param {object} info
+ * Object with details about the property value.
+ * @return {object} object with properties 'value', 'start', and 'end'.
+ */
+ _incrementGenericValue:
+ function InplaceEditor_incrementGenericValue(value, increment, offset,
+ offsetEnd, info)
+ {
+ // Try to find a number around the cursor to increment.
+ let start, end;
+ // Check if we are incrementing in a non-number context (such as a URL)
+ if (/^-?[0-9.]/.test(value.substring(offset, offsetEnd)) &&
+ !(/\d/.test(value.charAt(offset - 1) + value.charAt(offsetEnd)))) {
+ // We have a number selected, possibly with a suffix, and we are not in
+ // the disallowed case of just part of a known number being selected.
+ // Use that number.
+ start = offset;
+ end = offsetEnd;
+ } else {
+ // Parse periods as belonging to the number only if we are in a known number
+ // context. (This makes incrementing the 1 in 'image1.gif' work.)
+ let pattern = "[" + (info ? "0-9." : "0-9") + "]*";
+ let before = new RegExp(pattern + "$").exec(value.substr(0, offset))[0].length;
+ let after = new RegExp("^" + pattern).exec(value.substr(offset))[0].length;
+
+ start = offset - before;
+ end = offset + after;
+
+ // Expand the number to contain an initial minus sign if it seems
+ // free-standing.
+ if (value.charAt(start - 1) === "-" &&
+ (start - 1 === 0 || /[ (:,='"]/.test(value.charAt(start - 2)))) {
+ --start;
+ }
+ }
+
+ if (start !== end)
+ {
+ // Include percentages as part of the incremented number (they are
+ // common enough).
+ if (value.charAt(end) === "%") {
+ ++end;
+ }
+
+ let first = value.substr(0, start);
+ let mid = value.substring(start, end);
+ let last = value.substr(end);
+
+ mid = this._incrementRawValue(mid, increment, info);
+
+ if (mid !== null) {
+ return {
+ value: first + mid + last,
+ start: start,
+ end: start + mid.length
+ };
+ }
+ }
+ },
+
+ /**
+ * Increment the property value for numbers.
+ *
+ * @param {string} rawValue
+ * Raw value to increment.
+ * @param {number} increment
+ * Amount to increase/decrease the raw value.
+ * @param {object} info
+ * Object with info about the property value.
+ * @return {string} the incremented value.
+ */
+ _incrementRawValue:
+ function InplaceEditor_incrementRawValue(rawValue, increment, info)
+ {
+ let num = parseFloat(rawValue);
+
+ if (isNaN(num)) {
+ return null;
+ }
+
+ let number = /\d+(\.\d+)?/.exec(rawValue);
+ let units = rawValue.substr(number.index + number[0].length);
+
+ // avoid rounding errors
+ let newValue = Math.round((num + increment) * 1000) / 1000;
+
+ if (info && "minValue" in info) {
+ newValue = Math.max(newValue, info.minValue);
+ }
+ if (info && "maxValue" in info) {
+ newValue = Math.min(newValue, info.maxValue);
+ }
+
+ newValue = newValue.toString();
+
+ return newValue + units;
+ },
+
+ /**
+ * Increment the property value for hex.
+ *
+ * @param {string} value
+ * Property value.
+ * @param {number} increment
+ * Amount to increase/decrease the property value.
+ * @param {number} offset
+ * Starting index of the property value.
+ * @param {number} offsetEnd
+ * Ending index of the property value.
+ * @return {object} object with properties 'value' and 'selection'.
+ */
+ _incHexColor:
+ function InplaceEditor_incHexColor(rawValue, increment, offset, offsetEnd)
+ {
+ // Return early if no part of the rawValue is selected.
+ if (offsetEnd > rawValue.length && offset >= rawValue.length) {
+ return;
+ }
+ if (offset < 1 && offsetEnd <= 1) {
+ return;
+ }
+ // Ignore the leading #.
+ rawValue = rawValue.substr(1);
+ --offset;
+ --offsetEnd;
+
+ // Clamp the selection to within the actual value.
+ offset = Math.max(offset, 0);
+ offsetEnd = Math.min(offsetEnd, rawValue.length);
+ offsetEnd = Math.max(offsetEnd, offset);
+
+ // Normalize #ABC -> #AABBCC.
+ if (rawValue.length === 3) {
+ rawValue = rawValue.charAt(0) + rawValue.charAt(0) +
+ rawValue.charAt(1) + rawValue.charAt(1) +
+ rawValue.charAt(2) + rawValue.charAt(2);
+ offset *= 2;
+ offsetEnd *= 2;
+ }
+
+ if (rawValue.length !== 6) {
+ return;
+ }
+
+ // If no selection, increment an adjacent color, preferably one to the left.
+ if (offset === offsetEnd) {
+ if (offset === 0) {
+ offsetEnd = 1;
+ } else {
+ offset = offsetEnd - 1;
+ }
+ }
+
+ // Make the selection cover entire parts.
+ offset -= offset % 2;
+ offsetEnd += offsetEnd % 2;
+
+ // Remap the increments from [0.1, 1, 10] to [1, 1, 16].
+ if (-1 < increment && increment < 1) {
+ increment = (increment < 0 ? -1 : 1);
+ }
+ if (Math.abs(increment) === 10) {
+ increment = (increment < 0 ? -16 : 16);
+ }
+
+ let isUpper = (rawValue.toUpperCase() === rawValue);
+
+ for (let pos = offset; pos < offsetEnd; pos += 2) {
+ // Increment the part in [pos, pos+2).
+ let mid = rawValue.substr(pos, 2);
+ let value = parseInt(mid, 16);
+
+ if (isNaN(value)) {
+ return;
+ }
+
+ mid = Math.min(Math.max(value + increment, 0), 255).toString(16);
+
+ while (mid.length < 2) {
+ mid = "0" + mid;
+ }
+ if (isUpper) {
+ mid = mid.toUpperCase();
+ }
+
+ rawValue = rawValue.substr(0, pos) + mid + rawValue.substr(pos + 2);
+ }
+
+ return {
+ value: "#" + rawValue,
+ selection: [offset + 1, offsetEnd + 1]
+ };
+ },
+
+ /**
+ * Call the client's done handler and clear out.
+ */
+ _apply: function InplaceEditor_apply(aEvent)
+ {
+ if (this._applied) {
+ return;
+ }
+
+ this._applied = true;
+
+ if (this.done) {
+ let val = this.input.value.trim();
+ return this.done(this.cancelled ? this.initial : val, !this.cancelled);
+ }
+
+ return null;
+ },
+
+ /**
+ * Handle loss of focus by calling done if it hasn't been called yet.
+ */
+ _onBlur: function InplaceEditor_onBlur(aEvent, aDoNotClear)
+ {
+ this._apply();
+ if (!aDoNotClear) {
+ this._clear();
+ }
+ },
+
+ /**
+ * Handle the input field's keypress event.
+ */
+ _onKeyPress: function InplaceEditor_onKeyPress(aEvent)
+ {
+ let prevent = false;
+
+ const largeIncrement = 100;
+ const mediumIncrement = 10;
+ const smallIncrement = 0.1;
+
+ let increment = 0;
+
+ if (aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_UP
+ || aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_PAGE_UP) {
+ increment = 1;
+ } else if (aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_DOWN
+ || aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_PAGE_DOWN) {
+ increment = -1;
+ }
+
+ if (aEvent.shiftKey && !aEvent.altKey) {
+ if (aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_PAGE_UP
+ || aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_PAGE_DOWN) {
+ increment *= largeIncrement;
+ } else {
+ increment *= mediumIncrement;
+ }
+ } else if (aEvent.altKey && !aEvent.shiftKey) {
+ increment *= smallIncrement;
+ }
+
+ if (increment && this._incrementValue(increment) ) {
+ this._updateSize();
+ prevent = true;
+ }
+
+ if (this.multiline &&
+ aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_RETURN &&
+ aEvent.shiftKey) {
+ prevent = false;
+ } else if (aEvent.charCode in this._advanceCharCodes
+ || aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_RETURN
+ || aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_TAB) {
+ prevent = true;
+
+ let direction = FOCUS_FORWARD;
+ if (aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_TAB &&
+ aEvent.shiftKey) {
+ this.cancelled = true;
+ direction = FOCUS_BACKWARD;
+ }
+ if (this.stopOnReturn && aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_RETURN) {
+ direction = null;
+ }
+
+ let input = this.input;
+
+ this._apply();
+
+ if (direction !== null && focusManager.focusedElement === input) {
+ // If the focused element wasn't changed by the done callback,
+ // move the focus as requested.
+ let next = moveFocus(this.doc.defaultView, direction);
+
+ // If the next node to be focused has been tagged as an editable
+ // node, send it a click event to trigger
+ if (next && next.ownerDocument === this.doc && next._editable) {
+ next.click();
+ }
+ }
+
+ this._clear();
+ } else if (aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_ESCAPE) {
+ // Cancel and blur ourselves.
+ prevent = true;
+ this.cancelled = true;
+ this._apply();
+ this._clear();
+ aEvent.stopPropagation();
+ } else if (aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_SPACE) {
+ // No need for leading spaces here. This is particularly
+ // noticable when adding a property: it's very natural to type
+ // <name>: (which advances to the next property) then spacebar.
+ prevent = !this.input.value;
+ }
+
+ if (prevent) {
+ aEvent.preventDefault();
+ }
+ },
+
+ /**
+ * Handle the input field's keyup event.
+ */
+ _onKeyup: function(aEvent) {
+ // Validate the entered value.
+ this.warning.hidden = this.validate(this.input.value);
+ this._applied = false;
+ this._onBlur(null, true);
+ },
+
+ /**
+ * Handle changes to the input text.
+ */
+ _onInput: function InplaceEditor_onInput(aEvent)
+ {
+ // Validate the entered value.
+ if (this.warning && this.validate) {
+ this.warning.hidden = this.validate(this.input.value);
+ }
+
+ // Update size if we're autosizing.
+ if (this._measurement) {
+ this._updateSize();
+ }
+
+ // Call the user's change handler if available.
+ if (this.change) {
+ this.change(this.input.value.trim());
+ }
+ }
+};
+
+/**
+ * Copy text-related styles from one element to another.
+ */
+function copyTextStyles(aFrom, aTo)
+{
+ let win = aFrom.ownerDocument.defaultView;
+ let style = win.getComputedStyle(aFrom);
+ aTo.style.fontFamily = style.getPropertyCSSValue("font-family").cssText;
+ aTo.style.fontSize = style.getPropertyCSSValue("font-size").cssText;
+ aTo.style.fontWeight = style.getPropertyCSSValue("font-weight").cssText;
+ aTo.style.fontStyle = style.getPropertyCSSValue("font-style").cssText;
+}
+
+/**
+ * Trigger a focus change similar to pressing tab/shift-tab.
+ */
+function moveFocus(aWin, aDirection)
+{
+ return focusManager.moveFocus(aWin, null, aDirection, 0);
+}
+
+
+XPCOMUtils.defineLazyGetter(this, "focusManager", function() {
+ return Services.focus;
+});
diff --git a/browser/devtools/shared/moz.build b/browser/devtools/shared/moz.build
new file mode 100644
index 000000000..86ec46748
--- /dev/null
+++ b/browser/devtools/shared/moz.build
@@ -0,0 +1,7 @@
+# -*- Mode: python; c-basic-offset: 4; 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/.
+
+TEST_DIRS += ['test']
diff --git a/browser/devtools/shared/splitview.css b/browser/devtools/shared/splitview.css
new file mode 100644
index 000000000..72d4d61f9
--- /dev/null
+++ b/browser/devtools/shared/splitview.css
@@ -0,0 +1,98 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* 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/. */
+
+box,
+.splitview-nav {
+ -moz-box-flex: 1;
+ -moz-box-orient: vertical;
+}
+
+.splitview-nav-container {
+ -moz-box-pack: center;
+}
+
+.loading .splitview-nav-container > .placeholder {
+ display: none !important;
+}
+
+.splitview-controller,
+.splitview-main {
+ -moz-box-flex: 0;
+}
+
+.splitview-controller {
+ min-height: 3em;
+ max-height: 14em;
+ max-width: 400px;
+}
+
+.splitview-nav {
+ display: -moz-box;
+ overflow-x: hidden;
+ overflow-y: auto;
+}
+
+/* only the active details pane is shown */
+.splitview-side-details > * {
+ display: none;
+}
+.splitview-side-details > .splitview-active {
+ display: -moz-box;
+}
+
+.splitview-landscape-resizer {
+ cursor: ew-resize;
+}
+
+/* this is to keep in sync with SplitView.jsm's LANDSCAPE_MEDIA_QUERY */
+@media (min-width: 551px) {
+ .splitview-root {
+ -moz-box-orient: horizontal;
+ }
+ .splitview-controller {
+ max-height: none;
+ }
+ .splitview-details {
+ display: none;
+ }
+ .splitview-details.splitview-active {
+ display: -moz-box;
+ }
+}
+
+/* filtered items are hidden */
+ol.splitview-nav > li.splitview-filtered {
+ display: none;
+}
+
+/* "empty list" and "all filtered" placeholders are hidden */
+.splitview-nav:empty,
+.splitview-nav.splitview-all-filtered,
+.splitview-nav + .splitview-nav.placeholder {
+ display: none;
+}
+.splitview-nav.splitview-all-filtered ~ .splitview-nav.placeholder.all-filtered,
+.splitview-nav:empty ~ .splitview-nav.placeholder.empty {
+ display: -moz-box;
+}
+
+.splitview-portrait-resizer {
+ display: none;
+}
+
+/* portrait mode */
+@media (max-width: 550px) {
+ .splitview-landscape-splitter {
+ display: none;
+ }
+
+ .splitview-portrait-resizer {
+ display: -moz-box;
+ }
+
+ .splitview-controller {
+ max-width: none;
+ }
+}
diff --git a/browser/devtools/shared/telemetry.js b/browser/devtools/shared/telemetry.js
new file mode 100644
index 000000000..ef0f957b0
--- /dev/null
+++ b/browser/devtools/shared/telemetry.js
@@ -0,0 +1,259 @@
+/* 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/. */
+
+/**
+ * Telemetry.
+ *
+ * To add metrics for a tool:
+ *
+ * 1. Create boolean, flag and exponential entries in
+ * toolkit/components/telemetry/Histograms.json. Each type is optional but it
+ * is best if all three can be included.
+ *
+ * 2. Add your chart entries to browser/devtools/shared/telemetry.js
+ * (Telemetry.prototype._histograms):
+ * mytoolname: {
+ * histogram: "DEVTOOLS_MYTOOLNAME_OPENED_BOOLEAN",
+ * userHistogram: "DEVTOOLS_MYTOOLNAME_OPENED_PER_USER_FLAG",
+ * timerHistogram: "DEVTOOLS_MYTOOLNAME_TIME_ACTIVE_SECONDS"
+ * },
+ *
+ * 3. Include this module at the top of your tool. Use:
+ * let Telemetry = require("devtools/shared/telemetry")
+ *
+ * 4. Create a telemetry instance in your tool's constructor:
+ * this._telemetry = new Telemetry();
+ *
+ * 5. When your tool is opened call:
+ * this._telemetry.toolOpened("mytoolname");
+ *
+ * 6. When your tool is closed call:
+ * this._telemetry.toolClosed("mytoolname");
+ *
+ * Note:
+ * You can view telemetry stats for your local Firefox instance via
+ * about:telemetry.
+ *
+ * You can view telemetry stats for large groups of Firefox users at
+ * metrics.mozilla.com.
+ */
+
+const TOOLS_OPENED_PREF = "devtools.telemetry.tools.opened.version";
+
+this.Telemetry = function() {
+ // Bind pretty much all functions so that callers do not need to.
+ this.toolOpened = this.toolOpened.bind(this);
+ this.toolClosed = this.toolClosed.bind(this);
+ this.log = this.log.bind(this);
+ this.logOncePerBrowserVersion = this.logOncePerBrowserVersion.bind(this);
+ this.destroy = this.destroy.bind(this);
+
+ this._timers = new Map();
+};
+
+module.exports = Telemetry;
+
+let {Cc, Ci, Cu} = require("chrome");
+let {Services} = Cu.import("resource://gre/modules/Services.jsm", {});
+let {XPCOMUtils} = Cu.import("resource://gre/modules/XPCOMUtils.jsm", {});
+
+Telemetry.prototype = {
+ _histograms: {
+ toolbox: {
+ timerHistogram: "DEVTOOLS_TOOLBOX_TIME_ACTIVE_SECONDS"
+ },
+ options: {
+ histogram: "DEVTOOLS_OPTIONS_OPENED_BOOLEAN",
+ userHistogram: "DEVTOOLS_OPTIONS_OPENED_PER_USER_FLAG",
+ timerHistogram: "DEVTOOLS_OPTIONS_TIME_ACTIVE_SECONDS"
+ },
+ webconsole: {
+ histogram: "DEVTOOLS_WEBCONSOLE_OPENED_BOOLEAN",
+ userHistogram: "DEVTOOLS_WEBCONSOLE_OPENED_PER_USER_FLAG",
+ timerHistogram: "DEVTOOLS_WEBCONSOLE_TIME_ACTIVE_SECONDS"
+ },
+ browserconsole: {
+ histogram: "DEVTOOLS_BROWSERCONSOLE_OPENED_BOOLEAN",
+ userHistogram: "DEVTOOLS_BROWSERCONSOLE_OPENED_PER_USER_FLAG",
+ timerHistogram: "DEVTOOLS_BROWSERCONSOLE_TIME_ACTIVE_SECONDS"
+ },
+ inspector: {
+ histogram: "DEVTOOLS_INSPECTOR_OPENED_BOOLEAN",
+ userHistogram: "DEVTOOLS_INSPECTOR_OPENED_PER_USER_FLAG",
+ timerHistogram: "DEVTOOLS_INSPECTOR_TIME_ACTIVE_SECONDS"
+ },
+ ruleview: {
+ histogram: "DEVTOOLS_RULEVIEW_OPENED_BOOLEAN",
+ userHistogram: "DEVTOOLS_RULEVIEW_OPENED_PER_USER_FLAG",
+ timerHistogram: "DEVTOOLS_RULEVIEW_TIME_ACTIVE_SECONDS"
+ },
+ computedview: {
+ histogram: "DEVTOOLS_COMPUTEDVIEW_OPENED_BOOLEAN",
+ userHistogram: "DEVTOOLS_COMPUTEDVIEW_OPENED_PER_USER_FLAG",
+ timerHistogram: "DEVTOOLS_COMPUTEDVIEW_TIME_ACTIVE_SECONDS"
+ },
+ layoutview: {
+ histogram: "DEVTOOLS_LAYOUTVIEW_OPENED_BOOLEAN",
+ userHistogram: "DEVTOOLS_LAYOUTVIEW_OPENED_PER_USER_FLAG",
+ timerHistogram: "DEVTOOLS_LAYOUTVIEW_TIME_ACTIVE_SECONDS"
+ },
+ fontinspector: {
+ histogram: "DEVTOOLS_FONTINSPECTOR_OPENED_BOOLEAN",
+ userHistogram: "DEVTOOLS_FONTINSPECTOR_OPENED_PER_USER_FLAG",
+ timerHistogram: "DEVTOOLS_FONTINSPECTOR_TIME_ACTIVE_SECONDS"
+ },
+ jsdebugger: {
+ histogram: "DEVTOOLS_JSDEBUGGER_OPENED_BOOLEAN",
+ userHistogram: "DEVTOOLS_JSDEBUGGER_OPENED_PER_USER_FLAG",
+ timerHistogram: "DEVTOOLS_JSDEBUGGER_TIME_ACTIVE_SECONDS"
+ },
+ jsbrowserdebugger: {
+ histogram: "DEVTOOLS_JSBROWSERDEBUGGER_OPENED_BOOLEAN",
+ userHistogram: "DEVTOOLS_JSBROWSERDEBUGGER_OPENED_PER_USER_FLAG",
+ timerHistogram: "DEVTOOLS_JSBROWSERDEBUGGER_TIME_ACTIVE_SECONDS"
+ },
+ styleeditor: {
+ histogram: "DEVTOOLS_STYLEEDITOR_OPENED_BOOLEAN",
+ userHistogram: "DEVTOOLS_STYLEEDITOR_OPENED_PER_USER_FLAG",
+ timerHistogram: "DEVTOOLS_STYLEEDITOR_TIME_ACTIVE_SECONDS"
+ },
+ jsprofiler: {
+ histogram: "DEVTOOLS_JSPROFILER_OPENED_BOOLEAN",
+ userHistogram: "DEVTOOLS_JSPROFILER_OPENED_PER_USER_FLAG",
+ timerHistogram: "DEVTOOLS_JSPROFILER_TIME_ACTIVE_SECONDS"
+ },
+ netmonitor: {
+ histogram: "DEVTOOLS_NETMONITOR_OPENED_BOOLEAN",
+ userHistogram: "DEVTOOLS_NETMONITOR_OPENED_PER_USER_FLAG",
+ timerHistogram: "DEVTOOLS_NETMONITOR_TIME_ACTIVE_SECONDS"
+ },
+ tilt: {
+ histogram: "DEVTOOLS_TILT_OPENED_BOOLEAN",
+ userHistogram: "DEVTOOLS_TILT_OPENED_PER_USER_FLAG",
+ timerHistogram: "DEVTOOLS_TILT_TIME_ACTIVE_SECONDS"
+ },
+ paintflashing: {
+ histogram: "DEVTOOLS_PAINTFLASHING_OPENED_BOOLEAN",
+ userHistogram: "DEVTOOLS_PAINTFLASHING_OPENED_PER_USER_FLAG",
+ timerHistogram: "DEVTOOLS_PAINTFLASHING_TIME_ACTIVE_SECONDS"
+ },
+ scratchpad: {
+ histogram: "DEVTOOLS_SCRATCHPAD_OPENED_BOOLEAN",
+ userHistogram: "DEVTOOLS_SCRATCHPAD_OPENED_PER_USER_FLAG",
+ timerHistogram: "DEVTOOLS_SCRATCHPAD_TIME_ACTIVE_SECONDS"
+ },
+ responsive: {
+ histogram: "DEVTOOLS_RESPONSIVE_OPENED_BOOLEAN",
+ userHistogram: "DEVTOOLS_RESPONSIVE_OPENED_PER_USER_FLAG",
+ timerHistogram: "DEVTOOLS_RESPONSIVE_TIME_ACTIVE_SECONDS"
+ },
+ developertoolbar: {
+ histogram: "DEVTOOLS_DEVELOPERTOOLBAR_OPENED_BOOLEAN",
+ userHistogram: "DEVTOOLS_DEVELOPERTOOLBAR_OPENED_PER_USER_FLAG",
+ timerHistogram: "DEVTOOLS_DEVELOPERTOOLBAR_TIME_ACTIVE_SECONDS"
+ }
+ },
+
+ /**
+ * Add an entry to a histogram.
+ *
+ * @param {String} id
+ * Used to look up the relevant histogram ID and log true to that
+ * histogram.
+ */
+ toolOpened: function(id) {
+ let charts = this._histograms[id];
+
+ if (!charts) {
+ dump('Warning: An attempt was made to open a tool with an id of "' + id +
+ '", which is not listed in Telemetry._histograms. ' +
+ "Location: telemetry.js/toolOpened()\n");
+ return;
+ }
+
+ if (charts.histogram) {
+ this.log(charts.histogram, true);
+ }
+ if (charts.userHistogram) {
+ this.logOncePerBrowserVersion(charts.userHistogram, true);
+ }
+ if (charts.timerHistogram) {
+ this._timers.set(charts.timerHistogram, new Date());
+ }
+ },
+
+ toolClosed: function(id) {
+ let charts = this._histograms[id];
+
+ if (!charts || !charts.timerHistogram) {
+ return;
+ }
+
+ let startTime = this._timers.get(charts.timerHistogram);
+
+ if (startTime) {
+ let time = (new Date() - startTime) / 1000;
+ this.log(charts.timerHistogram, time);
+ this._timers.delete(charts.timerHistogram);
+ }
+ },
+
+ /**
+ * Log a value to a histogram.
+ *
+ * @param {String} histogramId
+ * Histogram in which the data is to be stored.
+ * @param value
+ * Value to store.
+ */
+ log: function(histogramId, value) {
+ if (histogramId) {
+ let histogram;
+
+ try {
+ let histogram = Services.telemetry.getHistogramById(histogramId);
+ histogram.add(value);
+ } catch(e) {
+ dump("Warning: An attempt was made to write to the " + histogramId +
+ " histogram, which is not defined in Histograms.json\n");
+ }
+ }
+ },
+
+ /**
+ * Log info about usage once per browser version. This allows us to discover
+ * how many individual users are using our tools for each browser version.
+ *
+ * @param {String} perUserHistogram
+ * Histogram in which the data is to be stored.
+ */
+ logOncePerBrowserVersion: function(perUserHistogram, value) {
+ let currentVersion = appInfo.version;
+ let latest = Services.prefs.getCharPref(TOOLS_OPENED_PREF);
+ let latestObj = JSON.parse(latest);
+
+ let lastVersionHistogramUpdated = latestObj[perUserHistogram];
+
+ if (typeof lastVersionHistogramUpdated == "undefined" ||
+ lastVersionHistogramUpdated !== currentVersion) {
+ latestObj[perUserHistogram] = currentVersion;
+ latest = JSON.stringify(latestObj);
+ Services.prefs.setCharPref(TOOLS_OPENED_PREF, latest);
+ this.log(perUserHistogram, value);
+ }
+ },
+
+ destroy: function() {
+ for (let [histogram, time] of this._timers) {
+ time = (new Date() - time) / 1000;
+
+ this.log(histogram, time);
+ this._timers.delete(histogram);
+ }
+ }
+};
+
+XPCOMUtils.defineLazyGetter(this, "appInfo", function() {
+ return Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULAppInfo);
+});
diff --git a/browser/devtools/shared/test/Makefile.in b/browser/devtools/shared/test/Makefile.in
new file mode 100644
index 000000000..088cfa596
--- /dev/null
+++ b/browser/devtools/shared/test/Makefile.in
@@ -0,0 +1,42 @@
+#
+# 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/.
+
+DEPTH = @DEPTH@
+topsrcdir = @top_srcdir@
+srcdir = @srcdir@
+VPATH = @srcdir@
+relativesrcdir = @relativesrcdir@
+
+include $(DEPTH)/config/autoconf.mk
+
+MOCHITEST_BROWSER_FILES = \
+ browser_eventemitter_basic.js \
+ browser_layoutHelpers.js \
+ browser_require_basic.js \
+ browser_telemetry_buttonsandsidebar.js \
+ browser_telemetry_toolboxtabs_inspector.js \
+ browser_telemetry_toolboxtabs_jsdebugger.js \
+ browser_telemetry_toolboxtabs_jsprofiler.js \
+ browser_telemetry_toolboxtabs_netmonitor.js \
+ browser_telemetry_toolboxtabs_options.js \
+ browser_telemetry_toolboxtabs_styleeditor.js \
+ browser_telemetry_toolboxtabs_webconsole.js \
+ browser_templater_basic.js \
+ browser_toolbar_basic.js \
+ browser_toolbar_tooltip.js \
+ browser_toolbar_webconsole_errors_count.js \
+ head.js \
+ leakhunt.js \
+ $(NULL)
+
+MOCHITEST_BROWSER_FILES += \
+ browser_templater_basic.html \
+ browser_toolbar_basic.html \
+ browser_toolbar_webconsole_errors_count.html \
+ browser_layoutHelpers.html \
+ browser_layoutHelpers_iframe.html \
+ $(NULL)
+
+include $(topsrcdir)/config/rules.mk
diff --git a/browser/devtools/shared/test/browser_eventemitter_basic.js b/browser/devtools/shared/test/browser_eventemitter_basic.js
new file mode 100644
index 000000000..7e9cccae3
--- /dev/null
+++ b/browser/devtools/shared/test/browser_eventemitter_basic.js
@@ -0,0 +1,80 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function test() {
+ testEmitter();
+ testEmitter({});
+}
+
+
+function testEmitter(aObject) {
+ Cu.import("resource:///modules/devtools/shared/event-emitter.js", this);
+
+ let emitter;
+
+ if (aObject) {
+ emitter = aObject;
+ EventEmitter.decorate(emitter);
+ } else {
+ emitter = new EventEmitter();
+ }
+
+ ok(emitter, "We have an event emitter");
+
+ emitter.on("next", next);
+ emitter.emit("next", "abc", "def");
+
+ let beenHere1 = false;
+ function next(eventName, str1, str2) {
+ is(eventName, "next", "Got event");
+ is(str1, "abc", "Argument 1 is correct");
+ is(str2, "def", "Argument 2 is correct");
+
+ ok(!beenHere1, "first time in next callback");
+ beenHere1 = true;
+
+ emitter.off("next", next);
+
+ emitter.emit("next");
+
+ emitter.once("onlyonce", onlyOnce);
+
+ emitter.emit("onlyonce");
+ emitter.emit("onlyonce");
+ }
+
+ let beenHere2 = false;
+ function onlyOnce() {
+ ok(!beenHere2, "\"once\" listner has been called once");
+ beenHere2 = true;
+ emitter.emit("onlyonce");
+
+ killItWhileEmitting();
+ }
+
+ function killItWhileEmitting() {
+ function c1() {
+ ok(true, "c1 called");
+ }
+ function c2() {
+ ok(true, "c2 called");
+ emitter.off("tick", c3);
+ }
+ function c3() {
+ ok(false, "c3 should not be called");
+ }
+ function c4() {
+ ok(true, "c4 called");
+ }
+
+ emitter.on("tick", c1);
+ emitter.on("tick", c2);
+ emitter.on("tick", c3);
+ emitter.on("tick", c4);
+
+ emitter.emit("tick");
+
+ delete emitter;
+ finish();
+ }
+}
diff --git a/browser/devtools/shared/test/browser_layoutHelpers.html b/browser/devtools/shared/test/browser_layoutHelpers.html
new file mode 100644
index 000000000..3b9a285b4
--- /dev/null
+++ b/browser/devtools/shared/test/browser_layoutHelpers.html
@@ -0,0 +1,25 @@
+<!doctype html>
+<meta charset=utf-8>
+<title> Layout Helpers </title>
+
+<style>
+ html {
+ height: 300%;
+ width: 300%;
+ }
+ div#some {
+ position: absolute;
+ background: black;
+ width: 2px;
+ height: 2px;
+ }
+ iframe {
+ position: absolute;
+ width: 40px;
+ height: 40px;
+ border: 0;
+ }
+</style>
+
+<div id=some></div>
+<iframe id=frame src='./browser_layoutHelpers_iframe.html'></iframe>
diff --git a/browser/devtools/shared/test/browser_layoutHelpers.js b/browser/devtools/shared/test/browser_layoutHelpers.js
new file mode 100644
index 000000000..9747399a4
--- /dev/null
+++ b/browser/devtools/shared/test/browser_layoutHelpers.js
@@ -0,0 +1,99 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests that scrollIntoViewIfNeeded works properly.
+
+let imported = {};
+Components.utils.import("resource:///modules/devtools/LayoutHelpers.jsm",
+ imported);
+registerCleanupFunction(function () {
+ imported = {};
+});
+
+let LayoutHelpers = imported.LayoutHelpers;
+
+const TEST_URI = "http://example.com/browser/browser/devtools/shared/test/browser_layoutHelpers.html";
+
+function test() {
+ addTab(TEST_URI, function(browser, tab) {
+ info("Starting browser_layoutHelpers.js");
+ let doc = browser.contentDocument;
+ runTest(doc.defaultView, doc.getElementById('some'));
+ gBrowser.removeCurrentTab();
+ finish();
+ });
+}
+
+function runTest(win, some) {
+ some.style.top = win.innerHeight + 'px';
+ some.style.left = win.innerWidth + 'px';
+ // The tests start with a black 2x2 pixels square below bottom right.
+ // Do not resize the window during the tests.
+
+ win.scroll(win.innerWidth / 2, win.innerHeight + 2); // Above the viewport.
+ LayoutHelpers.scrollIntoViewIfNeeded(some);
+ is(win.scrollY, Math.floor(win.innerHeight / 2) + 1,
+ 'Element completely hidden above should appear centered.');
+
+ win.scroll(win.innerWidth / 2, win.innerHeight + 1); // On the top edge.
+ LayoutHelpers.scrollIntoViewIfNeeded(some);
+ is(win.scrollY, win.innerHeight,
+ 'Element partially visible above should appear above.');
+
+ win.scroll(win.innerWidth / 2, 0); // Just below the viewport.
+ LayoutHelpers.scrollIntoViewIfNeeded(some);
+ is(win.scrollY, Math.floor(win.innerHeight / 2) + 1,
+ 'Element completely hidden below should appear centered.');
+
+ win.scroll(win.innerWidth / 2, 1); // On the bottom edge.
+ LayoutHelpers.scrollIntoViewIfNeeded(some);
+ is(win.scrollY, 2,
+ 'Element partially visible below should appear below.');
+
+
+ win.scroll(win.innerWidth / 2, win.innerHeight + 2); // Above the viewport.
+ LayoutHelpers.scrollIntoViewIfNeeded(some, false);
+ is(win.scrollY, win.innerHeight,
+ 'Element completely hidden above should appear above ' +
+ 'if parameter is false.');
+
+ win.scroll(win.innerWidth / 2, win.innerHeight + 1); // On the top edge.
+ LayoutHelpers.scrollIntoViewIfNeeded(some, false);
+ is(win.scrollY, win.innerHeight,
+ 'Element partially visible above should appear above ' +
+ 'if parameter is false.');
+
+ win.scroll(win.innerWidth / 2, 0); // Below the viewport.
+ LayoutHelpers.scrollIntoViewIfNeeded(some, false);
+ is(win.scrollY, 2,
+ 'Element completely hidden below should appear below ' +
+ 'if parameter is false.');
+
+ win.scroll(win.innerWidth / 2, 1); // On the bottom edge.
+ LayoutHelpers.scrollIntoViewIfNeeded(some, false);
+ is(win.scrollY, 2,
+ 'Element partially visible below should appear below ' +
+ 'if parameter is false.');
+
+ // The case of iframes.
+ win.scroll(0, 0);
+
+ let frame = win.document.getElementById('frame');
+ let fwin = frame.contentWindow;
+
+ frame.style.top = win.innerHeight + 'px';
+ frame.style.left = win.innerWidth + 'px';
+
+ fwin.addEventListener('load', function frameLoad() {
+ let some = fwin.document.getElementById('some');
+ LayoutHelpers.scrollIntoViewIfNeeded(some);
+ is(win.scrollX, Math.floor(win.innerWidth / 2) + 20,
+ 'Scrolling from an iframe should center the iframe vertically.');
+ is(win.scrollY, Math.floor(win.innerHeight / 2) + 20,
+ 'Scrolling from an iframe should center the iframe horizontally.');
+ is(fwin.scrollX, Math.floor(fwin.innerWidth / 2) + 1,
+ 'Scrolling from an iframe should center the element vertically.');
+ is(fwin.scrollY, Math.floor(fwin.innerHeight / 2) + 1,
+ 'Scrolling from an iframe should center the element horizontally.');
+ }, false);
+}
diff --git a/browser/devtools/shared/test/browser_layoutHelpers_iframe.html b/browser/devtools/shared/test/browser_layoutHelpers_iframe.html
new file mode 100644
index 000000000..66ef5b293
--- /dev/null
+++ b/browser/devtools/shared/test/browser_layoutHelpers_iframe.html
@@ -0,0 +1,19 @@
+<!doctype html>
+<meta charset=utf-8>
+<title> Layout Helpers </title>
+
+<style>
+ html {
+ height: 300%;
+ width: 300%;
+ }
+ div#some {
+ position: absolute;
+ background: black;
+ width: 2px;
+ height: 2px;
+ }
+</style>
+
+<div id=some></div>
+
diff --git a/browser/devtools/shared/test/browser_require_basic.js b/browser/devtools/shared/test/browser_require_basic.js
new file mode 100644
index 000000000..f86974df4
--- /dev/null
+++ b/browser/devtools/shared/test/browser_require_basic.js
@@ -0,0 +1,140 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests that source URLs in the Web Console can be clicked to display the
+// standard View Source window.
+
+let [ define, require ] = (function() {
+ let tempScope = {};
+ Components.utils.import("resource://gre/modules/devtools/Require.jsm", tempScope);
+ return [ tempScope.define, tempScope.require ];
+})();
+
+function test() {
+ addTab("about:blank", function() {
+ info("Starting Require Tests");
+ setup();
+
+ testWorking();
+ testDomains();
+ testLeakage();
+ testMultiImport();
+ testRecursive();
+ testUncompilable();
+ testFirebug();
+
+ shutdown();
+ });
+}
+
+function setup() {
+ define('gclitest/requirable', [ 'require', 'exports', 'module' ], function(require, exports, module) {
+ exports.thing1 = 'thing1';
+ exports.thing2 = 2;
+
+ let status = 'initial';
+ exports.setStatus = function(aStatus) { status = aStatus; };
+ exports.getStatus = function() { return status; };
+ });
+
+ define('gclitest/unrequirable', [ 'require', 'exports', 'module' ], function(require, exports, module) {
+ null.throwNPE();
+ });
+
+ define('gclitest/recurse', [ 'require', 'exports', 'module', 'gclitest/recurse' ], function(require, exports, module) {
+ require('gclitest/recurse');
+ });
+
+ define('gclitest/firebug', [ 'gclitest/requirable' ], function(requirable) {
+ return { requirable: requirable, fb: true };
+ });
+}
+
+function shutdown() {
+ delete define.modules['gclitest/requirable'];
+ delete define.globalDomain.modules['gclitest/requirable'];
+ delete define.modules['gclitest/unrequirable'];
+ delete define.globalDomain.modules['gclitest/unrequirable'];
+ delete define.modules['gclitest/recurse'];
+ delete define.globalDomain.modules['gclitest/recurse'];
+ delete define.modules['gclitest/firebug'];
+ delete define.globalDomain.modules['gclitest/firebug'];
+
+ define = undefined;
+ require = undefined;
+
+ finish();
+}
+
+function testWorking() {
+ // There are lots of requirement tests that we could be doing here
+ // The fact that we can get anything at all working is a testament to
+ // require doing what it should - we don't need to test the
+ let requireable = require('gclitest/requirable');
+ is('thing1', requireable.thing1, 'thing1 was required');
+ is(2, requireable.thing2, 'thing2 was required');
+ is(requireable.thing3, undefined, 'thing3 was not required');
+}
+
+function testDomains() {
+ let requireable = require('gclitest/requirable');
+ is(requireable.status, undefined, 'requirable has no status');
+ requireable.setStatus(null);
+ is(null, requireable.getStatus(), 'requirable.getStatus changed to null');
+ is(requireable.status, undefined, 'requirable still has no status');
+ requireable.setStatus('42');
+ is('42', requireable.getStatus(), 'requirable.getStatus changed to 42');
+ is(requireable.status, undefined, 'requirable *still* has no status');
+
+ let domain = new define.Domain();
+ let requireable2 = domain.require('gclitest/requirable');
+ is(requireable2.status, undefined, 'requirable2 has no status');
+ is('initial', requireable2.getStatus(), 'requirable2.getStatus is initial');
+ requireable2.setStatus(999);
+ is(999, requireable2.getStatus(), 'requirable2.getStatus changed to 999');
+ is(requireable2.status, undefined, 'requirable2 still has no status');
+
+ is('42', requireable.getStatus(), 'status 42');
+ ok(requireable.status === undefined, 'requirable has no status (as expected)');
+
+ delete domain.modules['gclitest/requirable'];
+}
+
+function testLeakage() {
+ let requireable = require('gclitest/requirable');
+ is(requireable.setup, null, 'leakage of setup');
+ is(requireable.shutdown, null, 'leakage of shutdown');
+ is(requireable.testWorking, null, 'leakage of testWorking');
+}
+
+function testMultiImport() {
+ let r1 = require('gclitest/requirable');
+ let r2 = require('gclitest/requirable');
+ is(r1, r2, 'double require was strict equal');
+}
+
+function testUncompilable() {
+ // It's not totally clear how a module loader should perform with unusable
+ // modules, however at least it should go into a flat spin ...
+ // GCLI mini_require reports an error as it should
+ try {
+ let unrequireable = require('gclitest/unrequirable');
+ fail();
+ }
+ catch (ex) {
+ // an exception is expected
+ }
+}
+
+function testRecursive() {
+ // See Bug 658583
+ // require('gclitest/recurse');
+ // Also see the comments in the testRecursive() function
+}
+
+function testFirebug() {
+ let requirable = require('gclitest/requirable');
+ let firebug = require('gclitest/firebug');
+ ok(firebug.fb, 'firebug.fb is true');
+ is(requirable, firebug.requirable, 'requirable pass-through');
+}
diff --git a/browser/devtools/shared/test/browser_telemetry_buttonsandsidebar.js b/browser/devtools/shared/test/browser_telemetry_buttonsandsidebar.js
new file mode 100644
index 000000000..8553ed799
--- /dev/null
+++ b/browser/devtools/shared/test/browser_telemetry_buttonsandsidebar.js
@@ -0,0 +1,179 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const TEST_URI = "data:text/html;charset=utf-8,<p>browser_telemetry_buttonsandsidebar.js</p>";
+
+// Because we need to gather stats for the period of time that a tool has been
+// opened we make use of setTimeout() to create tool active times.
+const TOOL_DELAY = 200;
+
+let {Promise} = Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js", {});
+let {Services} = Cu.import("resource://gre/modules/Services.jsm", {});
+
+let require = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools.require;
+let Telemetry = require("devtools/shared/telemetry");
+
+function init() {
+ Telemetry.prototype.telemetryInfo = {};
+ Telemetry.prototype._oldlog = Telemetry.prototype.log;
+ Telemetry.prototype.log = function(histogramId, value) {
+ if (histogramId) {
+ if (!this.telemetryInfo[histogramId]) {
+ this.telemetryInfo[histogramId] = [];
+ }
+
+ this.telemetryInfo[histogramId].push(value);
+ }
+ }
+
+ testButtons();
+}
+
+function testButtons() {
+ info("Testing buttons");
+
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+
+ gDevTools.showToolbox(target, "inspector").then(function(toolbox) {
+ let container = toolbox.doc.getElementById("toolbox-buttons");
+ let buttons = container.getElementsByTagName("toolbarbutton");
+
+ // Copy HTMLCollection to array.
+ buttons = Array.prototype.slice.call(buttons);
+
+ (function testButton() {
+ let button = buttons.pop();
+
+ if (button) {
+ info("Clicking button " + button.id);
+ button.click();
+ delayedClicks(button, 3).then(function(button) {
+ if (buttons.length == 0) {
+ // Remove scratchpads
+ let wins = Services.wm.getEnumerator("devtools:scratchpad");
+ while (wins.hasMoreElements()) {
+ let win = wins.getNext();
+ info("Closing scratchpad window");
+ win.close();
+ }
+
+ testSidebar();
+ } else {
+ setTimeout(testButton, TOOL_DELAY);
+ }
+ });
+ }
+ })();
+ }).then(null, reportError);
+}
+
+function delayedClicks(node, clicks) {
+ let deferred = Promise.defer();
+ let clicked = 0;
+
+ setTimeout(function delayedClick() {
+ info("Clicking button " + node.id);
+ node.click();
+ clicked++;
+
+ if (clicked >= clicks) {
+ deferred.resolve(node);
+ } else {
+ setTimeout(delayedClick, TOOL_DELAY);
+ }
+ }, TOOL_DELAY);
+
+ return deferred.promise;
+}
+
+function testSidebar() {
+ info("Testing sidebar");
+
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+
+ gDevTools.showToolbox(target, "inspector").then(function(toolbox) {
+ let inspector = toolbox.getCurrentPanel();
+ let sidebarTools = ["ruleview", "computedview", "fontinspector", "layoutview"];
+
+ // Concatenate the array with itself so that we can open each tool twice.
+ sidebarTools.push.apply(sidebarTools, sidebarTools);
+
+ setTimeout(function selectSidebarTab() {
+ let tool = sidebarTools.pop();
+ if (tool) {
+ inspector.sidebar.select(tool);
+ setTimeout(function() {
+ setTimeout(selectSidebarTab, TOOL_DELAY);
+ }, TOOL_DELAY);
+ } else {
+ checkResults();
+ }
+ }, TOOL_DELAY);
+ });
+}
+
+function checkResults() {
+ let result = Telemetry.prototype.telemetryInfo;
+
+ for (let [histId, value] of Iterator(result)) {
+ if (histId.startsWith("DEVTOOLS_INSPECTOR_")) {
+ // Inspector stats are tested in browser_telemetry_toolboxtabs.js so we
+ // skip them here because we only open the inspector once for this test.
+ continue;
+ }
+
+ if (histId.endsWith("OPENED_PER_USER_FLAG")) {
+ ok(value.length === 1 && value[0] === true,
+ "Per user value " + histId + " has a single value of true");
+ } else if (histId.endsWith("OPENED_BOOLEAN")) {
+ ok(value.length > 1, histId + " has more than one entry");
+
+ let okay = value.every(function(element) {
+ return element === true;
+ });
+
+ ok(okay, "All " + histId + " entries are === true");
+ } else if (histId.endsWith("TIME_ACTIVE_SECONDS")) {
+ ok(value.length > 1, histId + " has more than one entry");
+
+ let okay = value.every(function(element) {
+ return element > 0;
+ });
+
+ ok(okay, "All " + histId + " entries have time > 0");
+ }
+ }
+
+ finishUp();
+}
+
+function reportError(error) {
+ let stack = " " + error.stack.replace(/\n?.*?@/g, "\n JS frame :: ");
+
+ ok(false, "ERROR: " + error + " at " + error.fileName + ":" +
+ error.lineNumber + "\n\nStack trace:" + stack);
+ finishUp();
+}
+
+function finishUp() {
+ gBrowser.removeCurrentTab();
+
+ Telemetry.prototype.log = Telemetry.prototype._oldlog;
+ delete Telemetry.prototype._oldlog;
+ delete Telemetry.prototype.telemetryInfo;
+
+ TargetFactory = Services = Promise = require = null;
+
+ finish();
+}
+
+function test() {
+ waitForExplicitFinish();
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.selectedBrowser.addEventListener("load", function() {
+ gBrowser.selectedBrowser.removeEventListener("load", arguments.callee, true);
+ waitForFocus(init, content);
+ }, true);
+
+ content.location = TEST_URI;
+}
diff --git a/browser/devtools/shared/test/browser_telemetry_toolboxtabs_inspector.js b/browser/devtools/shared/test/browser_telemetry_toolboxtabs_inspector.js
new file mode 100644
index 000000000..e53c1b829
--- /dev/null
+++ b/browser/devtools/shared/test/browser_telemetry_toolboxtabs_inspector.js
@@ -0,0 +1,110 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const TEST_URI = "data:text/html;charset=utf-8,<p>browser_telemetry_toolboxtabs_inspector.js</p>";
+
+// Because we need to gather stats for the period of time that a tool has been
+// opened we make use of setTimeout() to create tool active times.
+const TOOL_DELAY = 200;
+
+let {Promise} = Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js", {});
+let {Services} = Cu.import("resource://gre/modules/Services.jsm", {});
+
+let require = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools.require;
+let Telemetry = require("devtools/shared/telemetry");
+
+function init() {
+ Telemetry.prototype.telemetryInfo = {};
+ Telemetry.prototype._oldlog = Telemetry.prototype.log;
+ Telemetry.prototype.log = function(histogramId, value) {
+ if (histogramId) {
+ if (!this.telemetryInfo[histogramId]) {
+ this.telemetryInfo[histogramId] = [];
+ }
+
+ this.telemetryInfo[histogramId].push(value);
+ }
+ }
+
+ openToolboxTabTwice("inspector", false);
+}
+
+function openToolboxTabTwice(id, secondPass) {
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+
+ gDevTools.showToolbox(target, id).then(function(toolbox) {
+ info("Toolbox tab " + id + " opened");
+
+ toolbox.once("destroyed", function() {
+ if (secondPass) {
+ checkResults();
+ } else {
+ openToolboxTabTwice(id, true);
+ }
+ });
+ // We use a timeout to check the tools active time
+ setTimeout(function() {
+ gDevTools.closeToolbox(target);
+ }, TOOL_DELAY);
+ }).then(null, reportError);
+}
+
+function checkResults() {
+ let result = Telemetry.prototype.telemetryInfo;
+
+ for (let [histId, value] of Iterator(result)) {
+ if (histId.endsWith("OPENED_PER_USER_FLAG")) {
+ ok(value.length === 1 && value[0] === true,
+ "Per user value " + histId + " has a single value of true");
+ } else if (histId.endsWith("OPENED_BOOLEAN")) {
+ ok(value.length > 1, histId + " has more than one entry");
+
+ let okay = value.every(function(element) {
+ return element === true;
+ });
+
+ ok(okay, "All " + histId + " entries are === true");
+ } else if (histId.endsWith("TIME_ACTIVE_SECONDS")) {
+ ok(value.length > 1, histId + " has more than one entry");
+
+ let okay = value.every(function(element) {
+ return element > 0;
+ });
+
+ ok(okay, "All " + histId + " entries have time > 0");
+ }
+ }
+
+ finishUp();
+}
+
+function reportError(error) {
+ let stack = " " + error.stack.replace(/\n?.*?@/g, "\n JS frame :: ");
+
+ ok(false, "ERROR: " + error + " at " + error.fileName + ":" +
+ error.lineNumber + "\n\nStack trace:" + stack);
+ finishUp();
+}
+
+function finishUp() {
+ gBrowser.removeCurrentTab();
+
+ Telemetry.prototype.log = Telemetry.prototype._oldlog;
+ delete Telemetry.prototype._oldlog;
+ delete Telemetry.prototype.telemetryInfo;
+
+ TargetFactory = Services = Promise = require = null;
+
+ finish();
+}
+
+function test() {
+ waitForExplicitFinish();
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.selectedBrowser.addEventListener("load", function() {
+ gBrowser.selectedBrowser.removeEventListener("load", arguments.callee, true);
+ waitForFocus(init, content);
+ }, true);
+
+ content.location = TEST_URI;
+}
diff --git a/browser/devtools/shared/test/browser_telemetry_toolboxtabs_jsdebugger.js b/browser/devtools/shared/test/browser_telemetry_toolboxtabs_jsdebugger.js
new file mode 100644
index 000000000..1b1a6234b
--- /dev/null
+++ b/browser/devtools/shared/test/browser_telemetry_toolboxtabs_jsdebugger.js
@@ -0,0 +1,110 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const TEST_URI = "data:text/html;charset=utf-8,<p>browser_telemetry_toolboxtabs_jsdebugger.js</p>";
+
+// Because we need to gather stats for the period of time that a tool has been
+// opened we make use of setTimeout() to create tool active times.
+const TOOL_DELAY = 200;
+
+let {Promise} = Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js", {});
+let {Services} = Cu.import("resource://gre/modules/Services.jsm", {});
+
+let require = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools.require;
+let Telemetry = require("devtools/shared/telemetry");
+
+function init() {
+ Telemetry.prototype.telemetryInfo = {};
+ Telemetry.prototype._oldlog = Telemetry.prototype.log;
+ Telemetry.prototype.log = function(histogramId, value) {
+ if (histogramId) {
+ if (!this.telemetryInfo[histogramId]) {
+ this.telemetryInfo[histogramId] = [];
+ }
+
+ this.telemetryInfo[histogramId].push(value);
+ }
+ }
+
+ openToolboxTabTwice("jsdebugger", false);
+}
+
+function openToolboxTabTwice(id, secondPass) {
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+
+ gDevTools.showToolbox(target, id).then(function(toolbox) {
+ info("Toolbox tab " + id + " opened");
+
+ toolbox.once("destroyed", function() {
+ if (secondPass) {
+ checkResults();
+ } else {
+ openToolboxTabTwice(id, true);
+ }
+ });
+ // We use a timeout to check the tools active time
+ setTimeout(function() {
+ gDevTools.closeToolbox(target);
+ }, TOOL_DELAY);
+ }).then(null, reportError);
+}
+
+function checkResults() {
+ let result = Telemetry.prototype.telemetryInfo;
+
+ for (let [histId, value] of Iterator(result)) {
+ if (histId.endsWith("OPENED_PER_USER_FLAG")) {
+ ok(value.length === 1 && value[0] === true,
+ "Per user value " + histId + " has a single value of true");
+ } else if (histId.endsWith("OPENED_BOOLEAN")) {
+ ok(value.length > 1, histId + " has more than one entry");
+
+ let okay = value.every(function(element) {
+ return element === true;
+ });
+
+ ok(okay, "All " + histId + " entries are === true");
+ } else if (histId.endsWith("TIME_ACTIVE_SECONDS")) {
+ ok(value.length > 1, histId + " has more than one entry");
+
+ let okay = value.every(function(element) {
+ return element > 0;
+ });
+
+ ok(okay, "All " + histId + " entries have time > 0");
+ }
+ }
+
+ finishUp();
+}
+
+function reportError(error) {
+ let stack = " " + error.stack.replace(/\n?.*?@/g, "\n JS frame :: ");
+
+ ok(false, "ERROR: " + error + " at " + error.fileName + ":" +
+ error.lineNumber + "\n\nStack trace:" + stack);
+ finishUp();
+}
+
+function finishUp() {
+ gBrowser.removeCurrentTab();
+
+ Telemetry.prototype.log = Telemetry.prototype._oldlog;
+ delete Telemetry.prototype._oldlog;
+ delete Telemetry.prototype.telemetryInfo;
+
+ TargetFactory = Services = Promise = require = null;
+
+ finish();
+}
+
+function test() {
+ waitForExplicitFinish();
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.selectedBrowser.addEventListener("load", function() {
+ gBrowser.selectedBrowser.removeEventListener("load", arguments.callee, true);
+ waitForFocus(init, content);
+ }, true);
+
+ content.location = TEST_URI;
+}
diff --git a/browser/devtools/shared/test/browser_telemetry_toolboxtabs_jsprofiler.js b/browser/devtools/shared/test/browser_telemetry_toolboxtabs_jsprofiler.js
new file mode 100644
index 000000000..567219222
--- /dev/null
+++ b/browser/devtools/shared/test/browser_telemetry_toolboxtabs_jsprofiler.js
@@ -0,0 +1,110 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const TEST_URI = "data:text/html;charset=utf-8,<p>browser_telemetry_toolboxtabs_jsprofiler.js</p>";
+
+// Because we need to gather stats for the period of time that a tool has been
+// opened we make use of setTimeout() to create tool active times.
+const TOOL_DELAY = 200;
+
+let {Promise} = Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js", {});
+let {Services} = Cu.import("resource://gre/modules/Services.jsm", {});
+
+let require = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools.require;
+let Telemetry = require("devtools/shared/telemetry");
+
+function init() {
+ Telemetry.prototype.telemetryInfo = {};
+ Telemetry.prototype._oldlog = Telemetry.prototype.log;
+ Telemetry.prototype.log = function(histogramId, value) {
+ if (histogramId) {
+ if (!this.telemetryInfo[histogramId]) {
+ this.telemetryInfo[histogramId] = [];
+ }
+
+ this.telemetryInfo[histogramId].push(value);
+ }
+ }
+
+ openToolboxTabTwice("jsprofiler", false);
+}
+
+function openToolboxTabTwice(id, secondPass) {
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+
+ gDevTools.showToolbox(target, id).then(function(toolbox) {
+ info("Toolbox tab " + id + " opened");
+
+ toolbox.once("destroyed", function() {
+ if (secondPass) {
+ checkResults();
+ } else {
+ openToolboxTabTwice(id, true);
+ }
+ });
+ // We use a timeout to check the tools active time
+ setTimeout(function() {
+ gDevTools.closeToolbox(target);
+ }, TOOL_DELAY);
+ }).then(null, reportError);
+}
+
+function checkResults() {
+ let result = Telemetry.prototype.telemetryInfo;
+
+ for (let [histId, value] of Iterator(result)) {
+ if (histId.endsWith("OPENED_PER_USER_FLAG")) {
+ ok(value.length === 1 && value[0] === true,
+ "Per user value " + histId + " has a single value of true");
+ } else if (histId.endsWith("OPENED_BOOLEAN")) {
+ ok(value.length > 1, histId + " has more than one entry");
+
+ let okay = value.every(function(element) {
+ return element === true;
+ });
+
+ ok(okay, "All " + histId + " entries are === true");
+ } else if (histId.endsWith("TIME_ACTIVE_SECONDS")) {
+ ok(value.length > 1, histId + " has more than one entry");
+
+ let okay = value.every(function(element) {
+ return element > 0;
+ });
+
+ ok(okay, "All " + histId + " entries have time > 0");
+ }
+ }
+
+ finishUp();
+}
+
+function reportError(error) {
+ let stack = " " + error.stack.replace(/\n?.*?@/g, "\n JS frame :: ");
+
+ ok(false, "ERROR: " + error + " at " + error.fileName + ":" +
+ error.lineNumber + "\n\nStack trace:" + stack);
+ finishUp();
+}
+
+function finishUp() {
+ gBrowser.removeCurrentTab();
+
+ Telemetry.prototype.log = Telemetry.prototype._oldlog;
+ delete Telemetry.prototype._oldlog;
+ delete Telemetry.prototype.telemetryInfo;
+
+ TargetFactory = Services = Promise = require = null;
+
+ finish();
+}
+
+function test() {
+ waitForExplicitFinish();
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.selectedBrowser.addEventListener("load", function() {
+ gBrowser.selectedBrowser.removeEventListener("load", arguments.callee, true);
+ waitForFocus(init, content);
+ }, true);
+
+ content.location = TEST_URI;
+}
diff --git a/browser/devtools/shared/test/browser_telemetry_toolboxtabs_netmonitor.js b/browser/devtools/shared/test/browser_telemetry_toolboxtabs_netmonitor.js
new file mode 100644
index 000000000..bc139acfb
--- /dev/null
+++ b/browser/devtools/shared/test/browser_telemetry_toolboxtabs_netmonitor.js
@@ -0,0 +1,110 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const TEST_URI = "data:text/html;charset=utf-8,<p>browser_telemetry_toolboxtabs_netmonitor.js</p>";
+
+// Because we need to gather stats for the period of time that a tool has been
+// opened we make use of setTimeout() to create tool active times.
+const TOOL_DELAY = 200;
+
+let {Promise} = Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js", {});
+let {Services} = Cu.import("resource://gre/modules/Services.jsm", {});
+
+let require = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools.require;
+let Telemetry = require("devtools/shared/telemetry");
+
+function init() {
+ Telemetry.prototype.telemetryInfo = {};
+ Telemetry.prototype._oldlog = Telemetry.prototype.log;
+ Telemetry.prototype.log = function(histogramId, value) {
+ if (histogramId) {
+ if (!this.telemetryInfo[histogramId]) {
+ this.telemetryInfo[histogramId] = [];
+ }
+
+ this.telemetryInfo[histogramId].push(value);
+ }
+ }
+
+ openToolboxTabTwice("netmonitor", false);
+}
+
+function openToolboxTabTwice(id, secondPass) {
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+
+ gDevTools.showToolbox(target, id).then(function(toolbox) {
+ info("Toolbox tab " + id + " opened");
+
+ toolbox.once("destroyed", function() {
+ if (secondPass) {
+ checkResults();
+ } else {
+ openToolboxTabTwice(id, true);
+ }
+ });
+ // We use a timeout to check the tools active time
+ setTimeout(function() {
+ gDevTools.closeToolbox(target);
+ }, TOOL_DELAY);
+ }).then(null, reportError);
+}
+
+function checkResults() {
+ let result = Telemetry.prototype.telemetryInfo;
+
+ for (let [histId, value] of Iterator(result)) {
+ if (histId.endsWith("OPENED_PER_USER_FLAG")) {
+ ok(value.length === 1 && value[0] === true,
+ "Per user value " + histId + " has a single value of true");
+ } else if (histId.endsWith("OPENED_BOOLEAN")) {
+ ok(value.length > 1, histId + " has more than one entry");
+
+ let okay = value.every(function(element) {
+ return element === true;
+ });
+
+ ok(okay, "All " + histId + " entries are === true");
+ } else if (histId.endsWith("TIME_ACTIVE_SECONDS")) {
+ ok(value.length > 1, histId + " has more than one entry");
+
+ let okay = value.every(function(element) {
+ return element > 0;
+ });
+
+ ok(okay, "All " + histId + " entries have time > 0");
+ }
+ }
+
+ finishUp();
+}
+
+function reportError(error) {
+ let stack = " " + error.stack.replace(/\n?.*?@/g, "\n JS frame :: ");
+
+ ok(false, "ERROR: " + error + " at " + error.fileName + ":" +
+ error.lineNumber + "\n\nStack trace:" + stack);
+ finishUp();
+}
+
+function finishUp() {
+ gBrowser.removeCurrentTab();
+
+ Telemetry.prototype.log = Telemetry.prototype._oldlog;
+ delete Telemetry.prototype._oldlog;
+ delete Telemetry.prototype.telemetryInfo;
+
+ TargetFactory = Services = Promise = require = null;
+
+ finish();
+}
+
+function test() {
+ waitForExplicitFinish();
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.selectedBrowser.addEventListener("load", function() {
+ gBrowser.selectedBrowser.removeEventListener("load", arguments.callee, true);
+ waitForFocus(init, content);
+ }, true);
+
+ content.location = TEST_URI;
+}
diff --git a/browser/devtools/shared/test/browser_telemetry_toolboxtabs_options.js b/browser/devtools/shared/test/browser_telemetry_toolboxtabs_options.js
new file mode 100644
index 000000000..687a07c38
--- /dev/null
+++ b/browser/devtools/shared/test/browser_telemetry_toolboxtabs_options.js
@@ -0,0 +1,110 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const TEST_URI = "data:text/html;charset=utf-8,<p>browser_telemetry_toolboxtabs_options.js</p>";
+
+// Because we need to gather stats for the period of time that a tool has been
+// opened we make use of setTimeout() to create tool active times.
+const TOOL_DELAY = 200;
+
+let {Promise} = Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js", {});
+let {Services} = Cu.import("resource://gre/modules/Services.jsm", {});
+
+let require = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools.require;
+let Telemetry = require("devtools/shared/telemetry");
+
+function init() {
+ Telemetry.prototype.telemetryInfo = {};
+ Telemetry.prototype._oldlog = Telemetry.prototype.log;
+ Telemetry.prototype.log = function(histogramId, value) {
+ if (histogramId) {
+ if (!this.telemetryInfo[histogramId]) {
+ this.telemetryInfo[histogramId] = [];
+ }
+
+ this.telemetryInfo[histogramId].push(value);
+ }
+ }
+
+ openToolboxTabTwice("options", false);
+}
+
+function openToolboxTabTwice(id, secondPass) {
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+
+ gDevTools.showToolbox(target, id).then(function(toolbox) {
+ info("Toolbox tab " + id + " opened");
+
+ toolbox.once("destroyed", function() {
+ if (secondPass) {
+ checkResults();
+ } else {
+ openToolboxTabTwice(id, true);
+ }
+ });
+ // We use a timeout to check the tools active time
+ setTimeout(function() {
+ gDevTools.closeToolbox(target);
+ }, TOOL_DELAY);
+ }).then(null, reportError);
+}
+
+function checkResults() {
+ let result = Telemetry.prototype.telemetryInfo;
+
+ for (let [histId, value] of Iterator(result)) {
+ if (histId.endsWith("OPENED_PER_USER_FLAG")) {
+ ok(value.length === 1 && value[0] === true,
+ "Per user value " + histId + " has a single value of true");
+ } else if (histId.endsWith("OPENED_BOOLEAN")) {
+ ok(value.length > 1, histId + " has more than one entry");
+
+ let okay = value.every(function(element) {
+ return element === true;
+ });
+
+ ok(okay, "All " + histId + " entries are === true");
+ } else if (histId.endsWith("TIME_ACTIVE_SECONDS")) {
+ ok(value.length > 1, histId + " has more than one entry");
+
+ let okay = value.every(function(element) {
+ return element > 0;
+ });
+
+ ok(okay, "All " + histId + " entries have time > 0");
+ }
+ }
+
+ finishUp();
+}
+
+function reportError(error) {
+ let stack = " " + error.stack.replace(/\n?.*?@/g, "\n JS frame :: ");
+
+ ok(false, "ERROR: " + error + " at " + error.fileName + ":" +
+ error.lineNumber + "\n\nStack trace:" + stack);
+ finishUp();
+}
+
+function finishUp() {
+ gBrowser.removeCurrentTab();
+
+ Telemetry.prototype.log = Telemetry.prototype._oldlog;
+ delete Telemetry.prototype._oldlog;
+ delete Telemetry.prototype.telemetryInfo;
+
+ TargetFactory = Services = Promise = require = null;
+
+ finish();
+}
+
+function test() {
+ waitForExplicitFinish();
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.selectedBrowser.addEventListener("load", function() {
+ gBrowser.selectedBrowser.removeEventListener("load", arguments.callee, true);
+ waitForFocus(init, content);
+ }, true);
+
+ content.location = TEST_URI;
+}
diff --git a/browser/devtools/shared/test/browser_telemetry_toolboxtabs_styleeditor.js b/browser/devtools/shared/test/browser_telemetry_toolboxtabs_styleeditor.js
new file mode 100644
index 000000000..9e30c74fa
--- /dev/null
+++ b/browser/devtools/shared/test/browser_telemetry_toolboxtabs_styleeditor.js
@@ -0,0 +1,110 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const TEST_URI = "data:text/html;charset=utf-8,<p>browser_telemetry_toolboxtabs_styleeditor.js</p>";
+
+// Because we need to gather stats for the period of time that a tool has been
+// opened we make use of setTimeout() to create tool active times.
+const TOOL_DELAY = 200;
+
+let {Promise} = Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js", {});
+let {Services} = Cu.import("resource://gre/modules/Services.jsm", {});
+
+let require = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools.require;
+let Telemetry = require("devtools/shared/telemetry");
+
+function init() {
+ Telemetry.prototype.telemetryInfo = {};
+ Telemetry.prototype._oldlog = Telemetry.prototype.log;
+ Telemetry.prototype.log = function(histogramId, value) {
+ if (histogramId) {
+ if (!this.telemetryInfo[histogramId]) {
+ this.telemetryInfo[histogramId] = [];
+ }
+
+ this.telemetryInfo[histogramId].push(value);
+ }
+ }
+
+ openToolboxTabTwice("styleeditor", false);
+}
+
+function openToolboxTabTwice(id, secondPass) {
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+
+ gDevTools.showToolbox(target, id).then(function(toolbox) {
+ info("Toolbox tab " + id + " opened");
+
+ toolbox.once("destroyed", function() {
+ if (secondPass) {
+ checkResults();
+ } else {
+ openToolboxTabTwice(id, true);
+ }
+ });
+ // We use a timeout to check the tools active time
+ setTimeout(function() {
+ gDevTools.closeToolbox(target);
+ }, TOOL_DELAY);
+ }).then(null, reportError);
+}
+
+function checkResults() {
+ let result = Telemetry.prototype.telemetryInfo;
+
+ for (let [histId, value] of Iterator(result)) {
+ if (histId.endsWith("OPENED_PER_USER_FLAG")) {
+ ok(value.length === 1 && value[0] === true,
+ "Per user value " + histId + " has a single value of true");
+ } else if (histId.endsWith("OPENED_BOOLEAN")) {
+ ok(value.length > 1, histId + " has more than one entry");
+
+ let okay = value.every(function(element) {
+ return element === true;
+ });
+
+ ok(okay, "All " + histId + " entries are === true");
+ } else if (histId.endsWith("TIME_ACTIVE_SECONDS")) {
+ ok(value.length > 1, histId + " has more than one entry");
+
+ let okay = value.every(function(element) {
+ return element > 0;
+ });
+
+ ok(okay, "All " + histId + " entries have time > 0");
+ }
+ }
+
+ finishUp();
+}
+
+function reportError(error) {
+ let stack = " " + error.stack.replace(/\n?.*?@/g, "\n JS frame :: ");
+
+ ok(false, "ERROR: " + error + " at " + error.fileName + ":" +
+ error.lineNumber + "\n\nStack trace:" + stack);
+ finishUp();
+}
+
+function finishUp() {
+ gBrowser.removeCurrentTab();
+
+ Telemetry.prototype.log = Telemetry.prototype._oldlog;
+ delete Telemetry.prototype._oldlog;
+ delete Telemetry.prototype.telemetryInfo;
+
+ TargetFactory = Services = Promise = require = null;
+
+ finish();
+}
+
+function test() {
+ waitForExplicitFinish();
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.selectedBrowser.addEventListener("load", function() {
+ gBrowser.selectedBrowser.removeEventListener("load", arguments.callee, true);
+ waitForFocus(init, content);
+ }, true);
+
+ content.location = TEST_URI;
+}
diff --git a/browser/devtools/shared/test/browser_telemetry_toolboxtabs_webconsole.js b/browser/devtools/shared/test/browser_telemetry_toolboxtabs_webconsole.js
new file mode 100644
index 000000000..ba027e7fd
--- /dev/null
+++ b/browser/devtools/shared/test/browser_telemetry_toolboxtabs_webconsole.js
@@ -0,0 +1,110 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const TEST_URI = "data:text/html;charset=utf-8,<p>browser_telemetry_toolboxtabs_styleeditor_webconsole.js</p>";
+
+// Because we need to gather stats for the period of time that a tool has been
+// opened we make use of setTimeout() to create tool active times.
+const TOOL_DELAY = 200;
+
+let {Promise} = Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js", {});
+let {Services} = Cu.import("resource://gre/modules/Services.jsm", {});
+
+let require = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools.require;
+let Telemetry = require("devtools/shared/telemetry");
+
+function init() {
+ Telemetry.prototype.telemetryInfo = {};
+ Telemetry.prototype._oldlog = Telemetry.prototype.log;
+ Telemetry.prototype.log = function(histogramId, value) {
+ if (histogramId) {
+ if (!this.telemetryInfo[histogramId]) {
+ this.telemetryInfo[histogramId] = [];
+ }
+
+ this.telemetryInfo[histogramId].push(value);
+ }
+ }
+
+ openToolboxTabTwice("webconsole", false);
+}
+
+function openToolboxTabTwice(id, secondPass) {
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+
+ gDevTools.showToolbox(target, id).then(function(toolbox) {
+ info("Toolbox tab " + id + " opened");
+
+ toolbox.once("destroyed", function() {
+ if (secondPass) {
+ checkResults();
+ } else {
+ openToolboxTabTwice(id, true);
+ }
+ });
+ // We use a timeout to check the tools active time
+ setTimeout(function() {
+ gDevTools.closeToolbox(target);
+ }, TOOL_DELAY);
+ }).then(null, reportError);
+}
+
+function checkResults() {
+ let result = Telemetry.prototype.telemetryInfo;
+
+ for (let [histId, value] of Iterator(result)) {
+ if (histId.endsWith("OPENED_PER_USER_FLAG")) {
+ ok(value.length === 1 && value[0] === true,
+ "Per user value " + histId + " has a single value of true");
+ } else if (histId.endsWith("OPENED_BOOLEAN")) {
+ ok(value.length > 1, histId + " has more than one entry");
+
+ let okay = value.every(function(element) {
+ return element === true;
+ });
+
+ ok(okay, "All " + histId + " entries are === true");
+ } else if (histId.endsWith("TIME_ACTIVE_SECONDS")) {
+ ok(value.length > 1, histId + " has more than one entry");
+
+ let okay = value.every(function(element) {
+ return element > 0;
+ });
+
+ ok(okay, "All " + histId + " entries have time > 0");
+ }
+ }
+
+ finishUp();
+}
+
+function reportError(error) {
+ let stack = " " + error.stack.replace(/\n?.*?@/g, "\n JS frame :: ");
+
+ ok(false, "ERROR: " + error + " at " + error.fileName + ":" +
+ error.lineNumber + "\n\nStack trace:" + stack);
+ finishUp();
+}
+
+function finishUp() {
+ gBrowser.removeCurrentTab();
+
+ Telemetry.prototype.log = Telemetry.prototype._oldlog;
+ delete Telemetry.prototype._oldlog;
+ delete Telemetry.prototype.telemetryInfo;
+
+ TargetFactory = Services = Promise = require = null;
+
+ finish();
+}
+
+function test() {
+ waitForExplicitFinish();
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.selectedBrowser.addEventListener("load", function() {
+ gBrowser.selectedBrowser.removeEventListener("load", arguments.callee, true);
+ waitForFocus(init, content);
+ }, true);
+
+ content.location = TEST_URI;
+}
diff --git a/browser/devtools/shared/test/browser_templater_basic.html b/browser/devtools/shared/test/browser_templater_basic.html
new file mode 100644
index 000000000..473c731f3
--- /dev/null
+++ b/browser/devtools/shared/test/browser_templater_basic.html
@@ -0,0 +1,13 @@
+<!doctype html>
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+
+<html>
+<head>
+ <title>DOM Template Tests</title>
+</head>
+<body>
+
+</body>
+</html>
+
diff --git a/browser/devtools/shared/test/browser_templater_basic.js b/browser/devtools/shared/test/browser_templater_basic.js
new file mode 100644
index 000000000..ce8cb130e
--- /dev/null
+++ b/browser/devtools/shared/test/browser_templater_basic.js
@@ -0,0 +1,288 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests that the DOM Template engine works properly
+
+/*
+ * These tests run both in Mozilla/Mochitest and plain browsers (as does
+ * domtemplate)
+ * We should endevour to keep the source in sync.
+ */
+
+var Promise = Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js", {}).Promise;
+var template = Cu.import("resource://gre/modules/devtools/Templater.jsm", {}).template;
+
+const TEST_URI = "http://example.com/browser/browser/devtools/shared/test/browser_templater_basic.html";
+
+function test() {
+ addTab(TEST_URI, function() {
+ info("Starting DOM Templater Tests");
+ runTest(0);
+ });
+}
+
+function runTest(index) {
+ var options = tests[index] = tests[index]();
+ var holder = content.document.createElement('div');
+ holder.id = options.name;
+ var body = content.document.body;
+ body.appendChild(holder);
+ holder.innerHTML = options.template;
+
+ info('Running ' + options.name);
+ template(holder, options.data, options.options);
+
+ if (typeof options.result == 'string') {
+ is(holder.innerHTML, options.result, options.name);
+ }
+ else {
+ ok(holder.innerHTML.match(options.result) != null,
+ options.name + ' result=\'' + holder.innerHTML + '\'');
+ }
+
+ if (options.also) {
+ options.also(options);
+ }
+
+ function runNextTest() {
+ index++;
+ if (index < tests.length) {
+ runTest(index);
+ }
+ else {
+ finished();
+ }
+ }
+
+ if (options.later) {
+ var ais = is.bind(this);
+
+ function createTester(holder, options) {
+ return function() {
+ ais(holder.innerHTML, options.later, options.name + ' later');
+ runNextTest();
+ }.bind(this);
+ }
+
+ executeSoon(createTester(holder, options));
+ }
+ else {
+ runNextTest();
+ }
+}
+
+function finished() {
+ gBrowser.removeCurrentTab();
+ info("Finishing DOM Templater Tests");
+ tests = null;
+ finish();
+}
+
+/**
+ * Why have an array of functions that return data rather than just an array
+ * of the data itself? Some of these tests contain calls to delayReply() which
+ * sets up async processing using executeSoon(). Since the execution of these
+ * tests is asynchronous, the delayed reply will probably arrive before the
+ * test is executed, making the test be synchronous. So we wrap the data in a
+ * function so we only set it up just before we use it.
+ */
+var tests = [
+ function() { return {
+ name: 'simpleNesting',
+ template: '<div id="ex1">${nested.value}</div>',
+ data: { nested:{ value:'pass 1' } },
+ result: '<div id="ex1">pass 1</div>'
+ };},
+
+ function() { return {
+ name: 'returnDom',
+ template: '<div id="ex2">${__element.ownerDocument.createTextNode(\'pass 2\')}</div>',
+ options: { allowEval: true },
+ data: {},
+ result: '<div id="ex2">pass 2</div>'
+ };},
+
+ function() { return {
+ name: 'srcChange',
+ template: '<img _src="${fred}" id="ex3">',
+ data: { fred:'green.png' },
+ result: /<img( id="ex3")? src="green.png"( id="ex3")?>/
+ };},
+
+ function() { return {
+ name: 'ifTrue',
+ template: '<p if="${name !== \'jim\'}">hello ${name}</p>',
+ options: { allowEval: true },
+ data: { name: 'fred' },
+ result: '<p>hello fred</p>'
+ };},
+
+ function() { return {
+ name: 'ifFalse',
+ template: '<p if="${name !== \'jim\'}">hello ${name}</p>',
+ options: { allowEval: true },
+ data: { name: 'jim' },
+ result: ''
+ };},
+
+ function() { return {
+ name: 'simpleLoop',
+ template: '<p foreach="index in ${[ 1, 2, 3 ]}">${index}</p>',
+ options: { allowEval: true },
+ data: {},
+ result: '<p>1</p><p>2</p><p>3</p>'
+ };},
+
+ function() { return {
+ name: 'loopElement',
+ template: '<loop foreach="i in ${array}">${i}</loop>',
+ data: { array: [ 1, 2, 3 ] },
+ result: '123'
+ };},
+
+ // Bug 692028: DOMTemplate memory leak with asynchronous arrays
+ // Bug 692031: DOMTemplate async loops do not drop the loop element
+ function() { return {
+ name: 'asyncLoopElement',
+ template: '<loop foreach="i in ${array}">${i}</loop>',
+ data: { array: delayReply([1, 2, 3]) },
+ result: '<span></span>',
+ later: '123'
+ };},
+
+ function() { return {
+ name: 'saveElement',
+ template: '<p save="${element}">${name}</p>',
+ data: { name: 'pass 8' },
+ result: '<p>pass 8</p>',
+ also: function(options) {
+ ok(options.data.element.innerHTML, 'pass 9', 'saveElement saved');
+ delete options.data.element;
+ }
+ };},
+
+ function() { return {
+ name: 'useElement',
+ template: '<p id="pass9">${adjust(__element)}</p>',
+ options: { allowEval: true },
+ data: {
+ adjust: function(element) {
+ is('pass9', element.id, 'useElement adjust');
+ return 'pass 9b'
+ }
+ },
+ result: '<p id="pass9">pass 9b</p>'
+ };},
+
+ function() { return {
+ name: 'asyncInline',
+ template: '${delayed}',
+ data: { delayed: delayReply('inline') },
+ result: '<span></span>',
+ later: 'inline'
+ };},
+
+ // Bug 692028: DOMTemplate memory leak with asynchronous arrays
+ function() { return {
+ name: 'asyncArray',
+ template: '<p foreach="i in ${delayed}">${i}</p>',
+ data: { delayed: delayReply([1, 2, 3]) },
+ result: '<span></span>',
+ later: '<p>1</p><p>2</p><p>3</p>'
+ };},
+
+ function() { return {
+ name: 'asyncMember',
+ template: '<p foreach="i in ${delayed}">${i}</p>',
+ data: { delayed: [delayReply(4), delayReply(5), delayReply(6)] },
+ result: '<span></span><span></span><span></span>',
+ later: '<p>4</p><p>5</p><p>6</p>'
+ };},
+
+ // Bug 692028: DOMTemplate memory leak with asynchronous arrays
+ function() { return {
+ name: 'asyncBoth',
+ template: '<p foreach="i in ${delayed}">${i}</p>',
+ data: {
+ delayed: delayReply([
+ delayReply(4),
+ delayReply(5),
+ delayReply(6)
+ ])
+ },
+ result: '<span></span>',
+ later: '<p>4</p><p>5</p><p>6</p>'
+ };},
+
+ // Bug 701762: DOMTemplate fails when ${foo()} returns undefined
+ function() { return {
+ name: 'functionReturningUndefiend',
+ template: '<p>${foo()}</p>',
+ options: { allowEval: true },
+ data: {
+ foo: function() {}
+ },
+ result: '<p>undefined</p>'
+ };},
+
+ // Bug 702642: DOMTemplate is relatively slow when evaluating JS ${}
+ function() { return {
+ name: 'propertySimple',
+ template: '<p>${a.b.c}</p>',
+ data: { a: { b: { c: 'hello' } } },
+ result: '<p>hello</p>'
+ };},
+
+ function() { return {
+ name: 'propertyPass',
+ template: '<p>${Math.max(1, 2)}</p>',
+ options: { allowEval: true },
+ result: '<p>2</p>'
+ };},
+
+ function() { return {
+ name: 'propertyFail',
+ template: '<p>${Math.max(1, 2)}</p>',
+ result: '<p>${Math.max(1, 2)}</p>'
+ };},
+
+ // Bug 723431: DOMTemplate should allow customisation of display of
+ // null/undefined values
+ function() { return {
+ name: 'propertyUndefAttrFull',
+ template: '<p>${nullvar}|${undefinedvar1}|${undefinedvar2}</p>',
+ data: { nullvar: null, undefinedvar1: undefined },
+ result: '<p>null|undefined|undefined</p>'
+ };},
+
+ function() { return {
+ name: 'propertyUndefAttrBlank',
+ template: '<p>${nullvar}|${undefinedvar1}|${undefinedvar2}</p>',
+ data: { nullvar: null, undefinedvar1: undefined },
+ options: { blankNullUndefined: true },
+ result: '<p>||</p>'
+ };},
+
+ function() { return {
+ name: 'propertyUndefAttrFull',
+ template: '<div><p value="${nullvar}"></p><p value="${undefinedvar1}"></p><p value="${undefinedvar2}"></p></div>',
+ data: { nullvar: null, undefinedvar1: undefined },
+ result: '<div><p value="null"></p><p value="undefined"></p><p value="undefined"></p></div>'
+ };},
+
+ function() { return {
+ name: 'propertyUndefAttrBlank',
+ template: '<div><p value="${nullvar}"></p><p value="${undefinedvar1}"></p><p value="${undefinedvar2}"></p></div>',
+ data: { nullvar: null, undefinedvar1: undefined },
+ options: { blankNullUndefined: true },
+ result: '<div><p value=""></p><p value=""></p><p value=""></p></div>'
+ };}
+];
+
+function delayReply(data) {
+ var d = Promise.defer();
+ executeSoon(function() {
+ d.resolve(data);
+ });
+ return d.promise;
+}
diff --git a/browser/devtools/shared/test/browser_toolbar_basic.html b/browser/devtools/shared/test/browser_toolbar_basic.html
new file mode 100644
index 000000000..7ec012b0e
--- /dev/null
+++ b/browser/devtools/shared/test/browser_toolbar_basic.html
@@ -0,0 +1,35 @@
+<!doctype html>
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+
+<html>
+<head>
+ <meta charset="UTF-8">
+ <title>Developer Toolbar Tests</title>
+ <style type="text/css">
+ #single { color: red; }
+ </style>
+ <script type="text/javascript">var a=1;</script>
+</head>
+<body>
+
+<p id=single>
+1
+</p>
+
+<p class=twin>
+2a
+</p>
+
+<p class=twin>
+2b
+</p>
+
+<style>
+.twin { color: blue; }
+</style>
+<script>var b=2;</script>
+
+</body>
+</html>
+
diff --git a/browser/devtools/shared/test/browser_toolbar_basic.js b/browser/devtools/shared/test/browser_toolbar_basic.js
new file mode 100644
index 000000000..8819effc1
--- /dev/null
+++ b/browser/devtools/shared/test/browser_toolbar_basic.js
@@ -0,0 +1,74 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests that the developer toolbar works properly
+
+const TEST_URI = "http://example.com/browser/browser/devtools/shared/test/browser_toolbar_basic.html";
+
+function test() {
+ addTab(TEST_URI, function(browser, tab) {
+ info("Starting browser_toolbar_basic.js");
+ runTest();
+ });
+}
+
+function runTest() {
+ ok(!DeveloperToolbar.visible, "DeveloperToolbar is not visible in runTest");
+
+ oneTimeObserve(DeveloperToolbar.NOTIFICATIONS.SHOW, catchFail(checkOpen));
+ document.getElementById("Tools:DevToolbar").doCommand();
+}
+
+function isChecked(b) {
+ return b.getAttribute("checked") == "true";
+}
+
+function checkOpen() {
+ ok(DeveloperToolbar.visible, "DeveloperToolbar is visible in checkOpen");
+ let close = document.getElementById("developer-toolbar-closebutton");
+ ok(close, "Close button exists");
+
+ let toggleToolbox =
+ document.getElementById("devtoolsMenuBroadcaster_DevToolbox");
+ ok(!isChecked(toggleToolbox), "toggle toolbox button is not checked");
+
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+ gDevTools.showToolbox(target, "inspector").then(function(toolbox) {
+ ok(isChecked(toggleToolbox), "toggle toolbox button is checked");
+
+ addTab("about:blank", function(browser, tab) {
+ info("Opened a new tab");
+
+ ok(!isChecked(toggleToolbox), "toggle toolbox button is not checked");
+
+ gBrowser.removeCurrentTab();
+
+ oneTimeObserve(DeveloperToolbar.NOTIFICATIONS.HIDE, catchFail(checkClosed));
+ document.getElementById("Tools:DevToolbar").doCommand();
+ });
+ });
+}
+
+function checkClosed() {
+ ok(!DeveloperToolbar.visible, "DeveloperToolbar is not visible in checkClosed");
+
+ oneTimeObserve(DeveloperToolbar.NOTIFICATIONS.SHOW, catchFail(checkReOpen));
+ document.getElementById("Tools:DevToolbar").doCommand();
+}
+
+function checkReOpen() {
+ ok(DeveloperToolbar.visible, "DeveloperToolbar is visible in checkReOpen");
+
+ let toggleToolbox =
+ document.getElementById("devtoolsMenuBroadcaster_DevToolbox");
+ ok(isChecked(toggleToolbox), "toggle toolbox button is checked");
+
+ oneTimeObserve(DeveloperToolbar.NOTIFICATIONS.HIDE, catchFail(checkReClosed));
+ document.getElementById("developer-toolbar-closebutton").doCommand();
+}
+
+function checkReClosed() {
+ ok(!DeveloperToolbar.visible, "DeveloperToolbar is not visible in checkReClosed");
+
+ finish();
+}
diff --git a/browser/devtools/shared/test/browser_toolbar_tooltip.js b/browser/devtools/shared/test/browser_toolbar_tooltip.js
new file mode 100644
index 000000000..fa86b2fcb
--- /dev/null
+++ b/browser/devtools/shared/test/browser_toolbar_tooltip.js
@@ -0,0 +1,54 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests that the developer toolbar works properly
+
+const TEST_URI = "data:text/html;charset=utf-8,<p>Tooltip Tests</p>";
+
+function test() {
+ addTab(TEST_URI, function(browser, tab) {
+ info("Starting browser_toolbar_tooltip.js");
+ openTest();
+ });
+}
+
+function openTest() {
+ ok(!DeveloperToolbar.visible, "DeveloperToolbar is not visible in runTest");
+
+ oneTimeObserve(DeveloperToolbar.NOTIFICATIONS.SHOW, catchFail(runTest));
+ document.getElementById("Tools:DevToolbar").doCommand();
+}
+
+function runTest() {
+ let tooltipPanel = DeveloperToolbar.tooltipPanel;
+
+ DeveloperToolbar.display.focusManager.helpRequest();
+ DeveloperToolbar.display.inputter.setInput('help help');
+
+ DeveloperToolbar.display.inputter.setCursor({ start: 'help help'.length });
+ is(tooltipPanel._dimensions.start, 'help '.length,
+ 'search param start, when cursor at end');
+ ok(getLeftMargin() > 30, 'tooltip offset, when cursor at end')
+
+ DeveloperToolbar.display.inputter.setCursor({ start: 'help'.length });
+ is(tooltipPanel._dimensions.start, 0,
+ 'search param start, when cursor at end of command');
+ ok(getLeftMargin() > 9, 'tooltip offset, when cursor at end of command')
+
+ DeveloperToolbar.display.inputter.setCursor({ start: 'help help'.length - 1 });
+ is(tooltipPanel._dimensions.start, 'help '.length,
+ 'search param start, when cursor at penultimate position');
+ ok(getLeftMargin() > 30, 'tooltip offset, when cursor at penultimate position')
+
+ DeveloperToolbar.display.inputter.setCursor({ start: 0 });
+ is(tooltipPanel._dimensions.start, 0,
+ 'search param start, when cursor at start');
+ ok(getLeftMargin() > 9, 'tooltip offset, when cursor at start')
+
+ finish();
+}
+
+function getLeftMargin() {
+ let style = DeveloperToolbar.tooltipPanel._panel.style.marginLeft;
+ return parseInt(style.slice(0, -2), 10);
+}
diff --git a/browser/devtools/shared/test/browser_toolbar_webconsole_errors_count.html b/browser/devtools/shared/test/browser_toolbar_webconsole_errors_count.html
new file mode 100644
index 000000000..216cc0d49
--- /dev/null
+++ b/browser/devtools/shared/test/browser_toolbar_webconsole_errors_count.html
@@ -0,0 +1,32 @@
+<!DOCTYPE HTML>
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<html>
+<head>
+ <meta charset="UTF-8">
+ <title>Developer Toolbar Tests - errors count in the Web Console button</title>
+ <script type="text/javascript">
+ console.log("foobarBug762996consoleLog");
+ window.onload = function() {
+ window.foobarBug762996load();
+ };
+ window.foobarBug762996a();
+ </script>
+ <script type="text/javascript">
+ window.foobarBug762996b();
+ </script>
+</head>
+<body>
+ <p>Hello world! Test for errors count in the Web Console button (developer
+ toolbar).</p>
+ <p style="color: foobarBug762996css"><button>click me</button></p>
+ <script type="text/javascript;version=1.8">
+ "use strict";
+ let testObj = {};
+ document.querySelector("button").onclick = function() {
+ let test = testObj.fooBug788445 + "warning";
+ window.foobarBug762996click();
+ };
+ </script>
+</body>
+</html>
diff --git a/browser/devtools/shared/test/browser_toolbar_webconsole_errors_count.js b/browser/devtools/shared/test/browser_toolbar_webconsole_errors_count.js
new file mode 100644
index 000000000..aaf027451
--- /dev/null
+++ b/browser/devtools/shared/test/browser_toolbar_webconsole_errors_count.js
@@ -0,0 +1,245 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests that the developer toolbar errors count works properly.
+
+function test() {
+ const TEST_URI = "http://example.com/browser/browser/devtools/shared/test/" +
+ "browser_toolbar_webconsole_errors_count.html";
+
+ let HUDService = Cu.import("resource:///modules/HUDService.jsm",
+ {}).HUDService;
+ let gDevTools = Cu.import("resource:///modules/devtools/gDevTools.jsm",
+ {}).gDevTools;
+
+ let webconsole = document.getElementById("developer-toolbar-toolbox-button");
+ let tab1, tab2;
+
+ Services.prefs.setBoolPref("javascript.options.strict", true);
+
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("javascript.options.strict");
+ });
+
+ ignoreAllUncaughtExceptions();
+ addTab(TEST_URI, openToolbar);
+
+ function openToolbar(browser, tab) {
+ tab1 = tab;
+ ignoreAllUncaughtExceptions(false);
+
+ expectUncaughtException();
+
+ if (!DeveloperToolbar.visible) {
+ DeveloperToolbar.show(true, onOpenToolbar);
+ }
+ else {
+ onOpenToolbar();
+ }
+ }
+
+ function onOpenToolbar() {
+ ok(DeveloperToolbar.visible, "DeveloperToolbar is visible");
+
+ waitForButtonUpdate({
+ name: "web console button shows page errors",
+ errors: 3,
+ warnings: 0,
+ callback: addErrors,
+ });
+ }
+
+ function addErrors() {
+ expectUncaughtException();
+
+ waitForFocus(function() {
+ let button = content.document.querySelector("button");
+ executeSoon(function() {
+ EventUtils.synthesizeMouse(button, 3, 2, {}, content);
+ });
+ }, content);
+
+ waitForButtonUpdate({
+ name: "button shows one more error after click in page",
+ errors: 4,
+ warnings: 1,
+ callback: () => {
+ ignoreAllUncaughtExceptions();
+ addTab(TEST_URI, onOpenSecondTab);
+ },
+ });
+ }
+
+ function onOpenSecondTab(browser, tab) {
+ tab2 = tab;
+
+ ignoreAllUncaughtExceptions(false);
+ expectUncaughtException();
+
+ waitForButtonUpdate({
+ name: "button shows correct number of errors after new tab is open",
+ errors: 3,
+ warnings: 0,
+ callback: switchToTab1,
+ });
+ }
+
+ function switchToTab1() {
+ gBrowser.selectedTab = tab1;
+ waitForButtonUpdate({
+ name: "button shows the page errors from tab 1",
+ errors: 4,
+ warnings: 1,
+ callback: openWebConsole.bind(null, tab1, onWebConsoleOpen),
+ });
+ }
+
+ function onWebConsoleOpen(hud) {
+ waitForValue({
+ name: "web console shows the page errors",
+ validator: function() {
+ return hud.outputNode.querySelectorAll(".hud-exception").length;
+ },
+ value: 4,
+ success: checkConsoleOutput.bind(null, hud),
+ failure: finish,
+ });
+ }
+
+ function checkConsoleOutput(hud) {
+ let msgs = ["foobarBug762996a", "foobarBug762996b", "foobarBug762996load",
+ "foobarBug762996click", "foobarBug762996consoleLog",
+ "foobarBug762996css", "fooBug788445"];
+ msgs.forEach(function(msg) {
+ isnot(hud.outputNode.textContent.indexOf(msg), -1,
+ msg + " found in the Web Console output");
+ });
+
+ hud.jsterm.clearOutput();
+
+ is(hud.outputNode.textContent.indexOf("foobarBug762996color"), -1,
+ "clearOutput() worked");
+
+ expectUncaughtException();
+ let button = content.document.querySelector("button");
+ EventUtils.synthesizeMouse(button, 2, 2, {}, content);
+
+ waitForButtonUpdate({
+ name: "button shows one more error after another click in page",
+ errors: 5,
+ warnings: 1, // warnings are not repeated by the js engine
+ callback: () => waitForValue(waitForNewError),
+ });
+
+ let waitForNewError = {
+ name: "the Web Console displays the new error",
+ validator: function() {
+ return hud.outputNode.textContent.indexOf("foobarBug762996click") > -1;
+ },
+ success: doClearConsoleButton.bind(null, hud),
+ failure: finish,
+ };
+ }
+
+ function doClearConsoleButton(hud) {
+ let clearButton = hud.ui.rootElement
+ .querySelector(".webconsole-clear-console-button");
+ EventUtils.synthesizeMouse(clearButton, 2, 2, {}, hud.iframeWindow);
+
+ is(hud.outputNode.textContent.indexOf("foobarBug762996click"), -1,
+ "clear console button worked");
+ is(getErrorsCount(), 0, "page errors counter has been reset");
+ let tooltip = getTooltipValues();
+ is(tooltip[1], 0, "page warnings counter has been reset");
+
+ doPageReload(hud);
+ }
+
+ function doPageReload(hud) {
+ tab1.linkedBrowser.addEventListener("load", onReload, true);
+
+ ignoreAllUncaughtExceptions();
+ content.location.reload();
+
+ function onReload() {
+ tab1.linkedBrowser.removeEventListener("load", onReload, true);
+ ignoreAllUncaughtExceptions(false);
+ expectUncaughtException();
+
+ waitForButtonUpdate({
+ name: "the Web Console button count has been reset after page reload",
+ errors: 3,
+ warnings: 0,
+ callback: waitForValue.bind(null, waitForConsoleOutputAfterReload),
+ });
+ }
+
+ let waitForConsoleOutputAfterReload = {
+ name: "the Web Console displays the correct number of errors after reload",
+ validator: function() {
+ return hud.outputNode.querySelectorAll(".hud-exception").length;
+ },
+ value: 3,
+ success: function() {
+ isnot(hud.outputNode.textContent.indexOf("foobarBug762996load"), -1,
+ "foobarBug762996load found in console output after page reload");
+ testEnd();
+ },
+ failure: testEnd,
+ };
+ }
+
+ function testEnd() {
+ document.getElementById("developer-toolbar-closebutton").doCommand();
+ let target1 = TargetFactory.forTab(tab1);
+ gDevTools.closeToolbox(target1);
+ gBrowser.removeTab(tab1);
+ gBrowser.removeTab(tab2);
+ finish();
+ }
+
+ // Utility functions
+
+ function getErrorsCount() {
+ let count = webconsole.getAttribute("error-count");
+ return count ? count : "0";
+ }
+
+ function getTooltipValues() {
+ let matches = webconsole.getAttribute("tooltiptext")
+ .match(/(\d+) errors?, (\d+) warnings?/);
+ return matches ? [matches[1], matches[2]] : [0, 0];
+ }
+
+ function waitForButtonUpdate(options) {
+ function check() {
+ let errors = getErrorsCount();
+ let tooltip = getTooltipValues();
+ let result = errors == options.errors && tooltip[1] == options.warnings;
+ if (result) {
+ ok(true, options.name);
+ is(errors, tooltip[0], "button error-count is the same as in the tooltip");
+
+ // Get out of the toolbar event execution loop.
+ executeSoon(options.callback);
+ }
+ return result;
+ }
+
+ if (!check()) {
+ info("wait for: " + options.name);
+ DeveloperToolbar.on("errors-counter-updated", function onUpdate(event) {
+ if (check()) {
+ DeveloperToolbar.off(event, onUpdate);
+ }
+ });
+ }
+ }
+
+ function openWebConsole(tab, callback)
+ {
+ let target = TargetFactory.forTab(tab);
+ gDevTools.showToolbox(target, "webconsole").then((toolbox) =>
+ callback(toolbox.getCurrentPanel().hud));
+ }
+}
diff --git a/browser/devtools/shared/test/head.js b/browser/devtools/shared/test/head.js
new file mode 100644
index 000000000..d79c5c4c8
--- /dev/null
+++ b/browser/devtools/shared/test/head.js
@@ -0,0 +1,121 @@
+/* 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/. */
+
+let {devtools} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
+let TargetFactory = devtools.TargetFactory;
+
+/**
+ * Open a new tab at a URL and call a callback on load
+ */
+function addTab(aURL, aCallback)
+{
+ waitForExplicitFinish();
+
+ gBrowser.selectedTab = gBrowser.addTab();
+ content.location = aURL;
+
+ let tab = gBrowser.selectedTab;
+ let browser = gBrowser.getBrowserForTab(tab);
+
+ function onTabLoad() {
+ browser.removeEventListener("load", onTabLoad, true);
+ aCallback(browser, tab, browser.contentDocument);
+ }
+
+ browser.addEventListener("load", onTabLoad, true);
+}
+
+registerCleanupFunction(function tearDown() {
+ while (gBrowser.tabs.length > 1) {
+ gBrowser.removeCurrentTab();
+ }
+
+ console = undefined;
+});
+
+function catchFail(func) {
+ return function() {
+ try {
+ return func.apply(null, arguments);
+ }
+ catch (ex) {
+ ok(false, ex);
+ console.error(ex);
+ finish();
+ throw ex;
+ }
+ };
+}
+
+/**
+ * Polls a given function waiting for the given value.
+ *
+ * @param object aOptions
+ * Options object with the following properties:
+ * - validator
+ * A validator function that should return the expected value. This is
+ * called every few milliseconds to check if the result is the expected
+ * one. When the returned result is the expected one, then the |success|
+ * function is called and polling stops. If |validator| never returns
+ * the expected value, then polling timeouts after several tries and
+ * a failure is recorded - the given |failure| function is invoked.
+ * - success
+ * A function called when the validator function returns the expected
+ * value.
+ * - failure
+ * A function called if the validator function timeouts - fails to return
+ * the expected value in the given time.
+ * - name
+ * Name of test. This is used to generate the success and failure
+ * messages.
+ * - timeout
+ * Timeout for validator function, in milliseconds. Default is 5000 ms.
+ * - value
+ * The expected value. If this option is omitted then the |validator|
+ * function must return a trueish value.
+ * Each of the provided callback functions will receive two arguments:
+ * the |aOptions| object and the last value returned by |validator|.
+ */
+function waitForValue(aOptions)
+{
+ let start = Date.now();
+ let timeout = aOptions.timeout || 5000;
+ let lastValue;
+
+ function wait(validatorFn, successFn, failureFn)
+ {
+ if ((Date.now() - start) > timeout) {
+ // Log the failure.
+ ok(false, "Timed out while waiting for: " + aOptions.name);
+ let expected = "value" in aOptions ?
+ "'" + aOptions.value + "'" :
+ "a trueish value";
+ info("timeout info :: got '" + lastValue + "', expected " + expected);
+ failureFn(aOptions, lastValue);
+ return;
+ }
+
+ lastValue = validatorFn(aOptions, lastValue);
+ let successful = "value" in aOptions ?
+ lastValue == aOptions.value :
+ lastValue;
+ if (successful) {
+ ok(true, aOptions.name);
+ successFn(aOptions, lastValue);
+ }
+ else {
+ setTimeout(function() wait(validatorFn, successFn, failureFn), 100);
+ }
+ }
+
+ wait(aOptions.validator, aOptions.success, aOptions.failure);
+}
+
+function oneTimeObserve(name, callback) {
+ var func = function() {
+ Services.obs.removeObserver(func, name);
+ callback();
+ };
+ Services.obs.addObserver(func, name, false);
+}
diff --git a/browser/devtools/shared/test/leakhunt.js b/browser/devtools/shared/test/leakhunt.js
new file mode 100644
index 000000000..66d067a3c
--- /dev/null
+++ b/browser/devtools/shared/test/leakhunt.js
@@ -0,0 +1,170 @@
+/* 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/. */
+
+/**
+ * Memory leak hunter. Walks a tree of objects looking for DOM nodes.
+ * Usage:
+ * leakHunt({
+ * thing: thing,
+ * otherthing: otherthing
+ * });
+ */
+function leakHunt(root) {
+ var path = [];
+ var seen = [];
+
+ try {
+ var output = leakHunt.inner(root, path, seen);
+ output.forEach(function(line) {
+ dump(line + '\n');
+ });
+ }
+ catch (ex) {
+ dump(ex + '\n');
+ }
+}
+
+leakHunt.inner = function LH_inner(root, path, seen) {
+ var prefix = new Array(path.length).join(' ');
+
+ var reply = [];
+ function log(msg) {
+ reply.push(msg);
+ }
+
+ var direct
+ try {
+ direct = Object.keys(root);
+ }
+ catch (ex) {
+ log(prefix + ' Error enumerating: ' + ex);
+ return reply;
+ }
+
+ try {
+ var index = 0;
+ for (var data of root) {
+ var prop = '' + index;
+ leakHunt.digProperty(prop, data, path, seen, direct, log);
+ index++;
+ }
+ }
+ catch (ex) { /* Ignore things that are not enumerable */ }
+
+ for (var prop in root) {
+ var data;
+ try {
+ data = root[prop];
+ }
+ catch (ex) {
+ log(prefix + ' ' + prop + ' = Error: ' + ex.toString().substring(0, 30));
+ continue;
+ }
+
+ leakHunt.digProperty(prop, data, path, seen, direct, log);
+ }
+
+ return reply;
+}
+
+leakHunt.hide = [ /^string$/, /^number$/, /^boolean$/, /^null/, /^undefined/ ];
+
+leakHunt.noRecurse = [
+ /^string$/, /^number$/, /^boolean$/, /^null/, /^undefined/,
+ /^Window$/, /^Document$/,
+ /^XULDocument$/, /^XULElement$/,
+ /^DOMWindow$/, /^HTMLDocument$/, /^HTML.*Element$/, /^ChromeWindow$/
+];
+
+leakHunt.digProperty = function LH_digProperty(prop, data, path, seen, direct, log) {
+ var newPath = path.slice();
+ newPath.push(prop);
+ var prefix = new Array(newPath.length).join(' ');
+
+ var recurse = true;
+ var message = leakHunt.getType(data);
+
+ if (leakHunt.matchesAnyPattern(message, leakHunt.hide)) {
+ return;
+ }
+
+ if (message === 'function' && direct.indexOf(prop) == -1) {
+ return;
+ }
+
+ if (message === 'string') {
+ var extra = data.length > 10 ? data.substring(0, 9) + '_' : data;
+ message += ' "' + extra.replace(/\n/g, "|") + '"';
+ recurse = false;
+ }
+ else if (leakHunt.matchesAnyPattern(message, leakHunt.noRecurse)) {
+ message += ' (no recurse)'
+ recurse = false;
+ }
+ else if (seen.indexOf(data) !== -1) {
+ message += ' (already seen)';
+ recurse = false;
+ }
+
+ if (recurse) {
+ seen.push(data);
+ var lines = leakHunt.inner(data, newPath, seen);
+ if (lines.length == 0) {
+ if (message !== 'function') {
+ log(prefix + prop + ' = ' + message + ' { }');
+ }
+ }
+ else {
+ log(prefix + prop + ' = ' + message + ' {');
+ lines.forEach(function(line) {
+ log(line);
+ });
+ log(prefix + '}');
+ }
+ }
+ else {
+ log(prefix + prop + ' = ' + message);
+ }
+};
+
+leakHunt.matchesAnyPattern = function LH_matchesAnyPattern(str, patterns) {
+ var match = false;
+ patterns.forEach(function(pattern) {
+ if (str.match(pattern)) {
+ match = true;
+ }
+ });
+ return match;
+};
+
+leakHunt.getType = function LH_getType(data) {
+ if (data === null) {
+ return 'null';
+ }
+ if (data === undefined) {
+ return 'undefined';
+ }
+
+ var type = typeof data;
+ if (type === 'object' || type === 'Object') {
+ type = leakHunt.getCtorName(data);
+ }
+
+ return type;
+};
+
+leakHunt.getCtorName = function LH_getCtorName(aObj) {
+ try {
+ if (aObj.constructor && aObj.constructor.name) {
+ return aObj.constructor.name;
+ }
+ }
+ catch (ex) {
+ return 'UnknownObject';
+ }
+
+ // If that fails, use Objects toString which sometimes gives something
+ // better than 'Object', and at least defaults to Object if nothing better
+ return Object.prototype.toString.call(aObj).slice(8, -1);
+};
diff --git a/browser/devtools/shared/test/moz.build b/browser/devtools/shared/test/moz.build
new file mode 100644
index 000000000..191c90f0b
--- /dev/null
+++ b/browser/devtools/shared/test/moz.build
@@ -0,0 +1,7 @@
+# -*- Mode: python; c-basic-offset: 4; 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/.
+
+XPCSHELL_TESTS_MANIFESTS += ['unit/xpcshell.ini']
diff --git a/browser/devtools/shared/test/unit/test_undoStack.js b/browser/devtools/shared/test/unit/test_undoStack.js
new file mode 100644
index 000000000..f1a230693
--- /dev/null
+++ b/browser/devtools/shared/test/unit/test_undoStack.js
@@ -0,0 +1,98 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const Cu = Components.utils;
+let {Loader} = Cu.import("resource://gre/modules/commonjs/toolkit/loader.js", {});
+
+let loader = new Loader.Loader({
+ paths: {
+ "": "resource://gre/modules/commonjs/",
+ "devtools": "resource:///modules/devtools",
+ },
+ globals: {},
+});
+let require = Loader.Require(loader, { id: "undo-test" })
+
+let {UndoStack} = require("devtools/shared/undo");
+
+const MAX_SIZE = 5;
+
+function run_test()
+{
+ let str = "";
+ let stack = new UndoStack(MAX_SIZE);
+
+ function add(ch) {
+ stack.do(function() {
+ str += ch;
+ }, function() {
+ str = str.slice(0, -1);
+ });
+ }
+
+ do_check_false(stack.canUndo());
+ do_check_false(stack.canRedo());
+
+ // Check adding up to the limit of the size
+ add("a");
+ do_check_true(stack.canUndo());
+ do_check_false(stack.canRedo());
+
+ add("b");
+ add("c");
+ add("d");
+ add("e");
+
+ do_check_eq(str, "abcde");
+
+ // Check a simple undo+redo
+ stack.undo();
+
+ do_check_eq(str, "abcd");
+ do_check_true(stack.canRedo());
+
+ stack.redo();
+ do_check_eq(str, "abcde")
+ do_check_false(stack.canRedo());
+
+ // Check an undo followed by a new action
+ stack.undo();
+ do_check_eq(str, "abcd");
+
+ add("q");
+ do_check_eq(str, "abcdq");
+ do_check_false(stack.canRedo());
+
+ stack.undo();
+ do_check_eq(str, "abcd");
+ stack.redo();
+ do_check_eq(str, "abcdq");
+
+ // Revert back to the beginning of the queue...
+ while (stack.canUndo()) {
+ stack.undo();
+ }
+ do_check_eq(str, "");
+
+ // Now put it all back....
+ while (stack.canRedo()) {
+ stack.redo();
+ }
+ do_check_eq(str, "abcdq");
+
+ // Now go over the undo limit...
+ add("1");
+ add("2");
+ add("3");
+
+ do_check_eq(str, "abcdq123");
+
+ // And now undoing the whole stack should only undo 5 actions.
+ while (stack.canUndo()) {
+ stack.undo();
+ }
+
+ do_check_eq(str, "abc");
+}
diff --git a/browser/devtools/shared/test/unit/xpcshell.ini b/browser/devtools/shared/test/unit/xpcshell.ini
new file mode 100644
index 000000000..342274bed
--- /dev/null
+++ b/browser/devtools/shared/test/unit/xpcshell.ini
@@ -0,0 +1,6 @@
+[DEFAULT]
+head =
+tail =
+firefox-appdir = browser
+
+[test_undoStack.js]
diff --git a/browser/devtools/shared/theme-switching.js b/browser/devtools/shared/theme-switching.js
new file mode 100644
index 000000000..d36e51c87
--- /dev/null
+++ b/browser/devtools/shared/theme-switching.js
@@ -0,0 +1,74 @@
+/* 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/. */
+
+(function() {
+ const DEVTOOLS_SKIN_URL = "chrome://browser/skin/devtools/";
+
+ function forceStyle() {
+ let computedStyle = window.getComputedStyle(document.documentElement);
+ if (!computedStyle) {
+ // Null when documentElement is not ready. This method is anyways not
+ // required then as scrollbars would be in their state without flushing.
+ return;
+ }
+ let display = computedStyle.display; // Save display value
+ document.documentElement.style.display = "none";
+ window.getComputedStyle(document.documentElement).display; // Flush
+ document.documentElement.style.display = display; // Restore
+ }
+
+ function switchTheme(newTheme, oldTheme) {
+ let winUtils = window.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindowUtils);
+
+ if (oldTheme && newTheme != oldTheme) {
+ let oldThemeUrl = Services.io.newURI(
+ DEVTOOLS_SKIN_URL + oldTheme + "-theme.css", null, null);
+ try {
+ winUtils.removeSheet(oldThemeUrl, window.AUTHOR_SHEET);
+ } catch(ex) {}
+ }
+
+ let newThemeUrl = Services.io.newURI(
+ DEVTOOLS_SKIN_URL + newTheme + "-theme.css", null, null);
+ winUtils.loadSheet(newThemeUrl, window.AUTHOR_SHEET);
+
+ // Floating scrollbars à la osx
+ if (Services.appinfo.OS != "Darwin") {
+ let scrollbarsUrl = Services.io.newURI(
+ DEVTOOLS_SKIN_URL + "floating-scrollbars-light.css", null, null);
+
+ if (newTheme == "dark") {
+ winUtils.loadSheet(scrollbarsUrl, window.AGENT_SHEET);
+ } else if (oldTheme == "dark") {
+ try {
+ winUtils.removeSheet(scrollbarsUrl, window.AGENT_SHEET);
+ } catch(ex) {}
+ }
+ forceStyle();
+ }
+
+ document.documentElement.classList.remove("theme-" + oldTheme);
+ document.documentElement.classList.add("theme-" + newTheme);
+ }
+
+ function handlePrefChange(event, data) {
+ if (data.pref == "devtools.theme") {
+ switchTheme(data.newValue, data.oldValue);
+ }
+ }
+
+ const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+
+ Cu.import("resource://gre/modules/Services.jsm");
+ Cu.import("resource:///modules/devtools/gDevTools.jsm");
+
+ let theme = Services.prefs.getCharPref("devtools.theme");
+ switchTheme(theme);
+
+ gDevTools.on("pref-changed", handlePrefChange);
+ window.addEventListener("unload", function() {
+ gDevTools.off("pref-changed", handlePrefChange);
+ });
+})();
diff --git a/browser/devtools/shared/undo.js b/browser/devtools/shared/undo.js
new file mode 100644
index 000000000..ca825329a
--- /dev/null
+++ b/browser/devtools/shared/undo.js
@@ -0,0 +1,206 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* 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/. */
+
+/**
+ * A simple undo stack manager.
+ *
+ * Actions are added along with the necessary code to
+ * reverse the action.
+ *
+ * @param function aChange Called whenever the size or position
+ * of the undo stack changes, to use for updating undo-related
+ * UI.
+ * @param integer aMaxUndo Maximum number of undo steps.
+ * defaults to 50.
+ */
+function UndoStack(aMaxUndo)
+{
+ this.maxUndo = aMaxUndo || 50;
+ this._stack = [];
+}
+
+exports.UndoStack = UndoStack;
+
+UndoStack.prototype = {
+ // Current index into the undo stack. Is positioned after the last
+ // currently-applied change.
+ _index: 0,
+
+ // The current batch depth (see startBatch() for details)
+ _batchDepth: 0,
+
+ destroy: function Undo_destroy()
+ {
+ this.uninstallController();
+ delete this._stack;
+ },
+
+ /**
+ * Start a collection of related changes. Changes will be batched
+ * together into one undo/redo item until endBatch() is called.
+ *
+ * Batches can be nested, in which case the outer batch will contain
+ * all items from the inner batches. This allows larger user
+ * actions made up of a collection of smaller actions to be
+ * undone as a single action.
+ */
+ startBatch: function Undo_startBatch()
+ {
+ if (this._batchDepth++ === 0) {
+ this._batch = [];
+ }
+ },
+
+ /**
+ * End a batch of related changes, performing its action and adding
+ * it to the undo stack.
+ */
+ endBatch: function Undo_endBatch()
+ {
+ if (--this._batchDepth > 0) {
+ return;
+ }
+
+ // Cut off the end of the undo stack at the current index,
+ // and the beginning to prevent a stack larger than maxUndo.
+ let start = Math.max((this._index + 1) - this.maxUndo, 0);
+ this._stack = this._stack.slice(start, this._index);
+
+ let batch = this._batch;
+ delete this._batch;
+ let entry = {
+ do: function() {
+ for (let item of batch) {
+ item.do();
+ }
+ },
+ undo: function() {
+ for (let i = batch.length - 1; i >= 0; i--) {
+ batch[i].undo();
+ }
+ }
+ };
+ this._stack.push(entry);
+ this._index = this._stack.length;
+ entry.do();
+ this._change();
+ },
+
+ /**
+ * Perform an action, adding it to the undo stack.
+ *
+ * @param function aDo Called to perform the action.
+ * @param function aUndo Called to reverse the action.
+ */
+ do: function Undo_do(aDo, aUndo) {
+ this.startBatch();
+ this._batch.push({ do: aDo, undo: aUndo });
+ this.endBatch();
+ },
+
+ /*
+ * Returns true if undo() will do anything.
+ */
+ canUndo: function Undo_canUndo()
+ {
+ return this._index > 0;
+ },
+
+ /**
+ * Undo the top of the undo stack.
+ *
+ * @return true if an action was undone.
+ */
+ undo: function Undo_canUndo()
+ {
+ if (!this.canUndo()) {
+ return false;
+ }
+ this._stack[--this._index].undo();
+ this._change();
+ return true;
+ },
+
+ /**
+ * Returns true if redo() will do anything.
+ */
+ canRedo: function Undo_canRedo()
+ {
+ return this._stack.length > this._index;
+ },
+
+ /**
+ * Redo the most recently undone action.
+ *
+ * @return true if an action was redone.
+ */
+ redo: function Undo_canRedo()
+ {
+ if (!this.canRedo()) {
+ return false;
+ }
+ this._stack[this._index++].do();
+ this._change();
+ return true;
+ },
+
+ _change: function Undo__change()
+ {
+ if (this._controllerWindow) {
+ this._controllerWindow.goUpdateCommand("cmd_undo");
+ this._controllerWindow.goUpdateCommand("cmd_redo");
+ }
+ },
+
+ /**
+ * ViewController implementation for undo/redo.
+ */
+
+ /**
+ * Install this object as a command controller.
+ */
+ installController: function Undo_installController(aControllerWindow)
+ {
+ this._controllerWindow = aControllerWindow;
+ aControllerWindow.controllers.appendController(this);
+ },
+
+ /**
+ * Uninstall this object from the command controller.
+ */
+ uninstallController: function Undo_uninstallController()
+ {
+ if (!this._controllerWindow) {
+ return;
+ }
+ this._controllerWindow.controllers.removeController(this);
+ },
+
+ supportsCommand: function Undo_supportsCommand(aCommand)
+ {
+ return (aCommand == "cmd_undo" ||
+ aCommand == "cmd_redo");
+ },
+
+ isCommandEnabled: function Undo_isCommandEnabled(aCommand)
+ {
+ switch(aCommand) {
+ case "cmd_undo": return this.canUndo();
+ case "cmd_redo": return this.canRedo();
+ };
+ return false;
+ },
+
+ doCommand: function Undo_doCommand(aCommand)
+ {
+ switch(aCommand) {
+ case "cmd_undo": return this.undo();
+ case "cmd_redo": return this.redo();
+ }
+ },
+
+ onEvent: function Undo_onEvent(aEvent) {},
+}
diff --git a/browser/devtools/shared/widgets/BreadcrumbsWidget.jsm b/browser/devtools/shared/widgets/BreadcrumbsWidget.jsm
new file mode 100644
index 000000000..a6c50a01c
--- /dev/null
+++ b/browser/devtools/shared/widgets/BreadcrumbsWidget.jsm
@@ -0,0 +1,227 @@
+/* -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* 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 = Components.interfaces;
+const Cu = Components.utils;
+
+const ENSURE_SELECTION_VISIBLE_DELAY = 50; // ms
+
+Cu.import("resource:///modules/devtools/ViewHelpers.jsm");
+Cu.import("resource:///modules/devtools/shared/event-emitter.js");
+
+this.EXPORTED_SYMBOLS = ["BreadcrumbsWidget"];
+
+/**
+ * A breadcrumb-like list of items.
+ * This widget should be used in tandem with the WidgetMethods in ViewHelpers.jsm
+ *
+ * @param nsIDOMNode aNode
+ * The element associated with the widget.
+ */
+this.BreadcrumbsWidget = function BreadcrumbsWidget(aNode) {
+ this.document = aNode.ownerDocument;
+ this.window = this.document.defaultView;
+ this._parent = aNode;
+
+ // Create an internal arrowscrollbox container.
+ this._list = this.document.createElement("arrowscrollbox");
+ this._list.className = "breadcrumbs-widget-container";
+ this._list.setAttribute("flex", "1");
+ this._list.setAttribute("orient", "horizontal");
+ this._list.setAttribute("clicktoscroll", "true")
+ this._list.addEventListener("keypress", e => this.emit("keyPress", e), false);
+ this._list.addEventListener("mousedown", e => this.emit("mousePress", e), false);
+ this._parent.appendChild(this._list);
+
+ // By default, hide the arrows. We let the arrowscrollbox show them
+ // in case of overflow.
+ this._list._scrollButtonUp.collapsed = true;
+ this._list._scrollButtonDown.collapsed = true;
+ this._list.addEventListener("underflow", this._onUnderflow.bind(this), false);
+ this._list.addEventListener("overflow", this._onOverflow.bind(this), false);
+
+ // This widget emits events that can be handled in a MenuContainer.
+ EventEmitter.decorate(this);
+
+ // Delegate some of the associated node's methods to satisfy the interface
+ // required by MenuContainer instances.
+ ViewHelpers.delegateWidgetAttributeMethods(this, aNode);
+ ViewHelpers.delegateWidgetEventMethods(this, aNode);
+};
+
+BreadcrumbsWidget.prototype = {
+ /**
+ * Inserts an item in this container at the specified index.
+ *
+ * @param number aIndex
+ * The position in the container intended for this item.
+ * @param string | nsIDOMNode aContents
+ * The string or node displayed in the container.
+ * @return nsIDOMNode
+ * The element associated with the displayed item.
+ */
+ insertItemAt: function(aIndex, aContents) {
+ let list = this._list;
+ let breadcrumb = new Breadcrumb(this, aContents);
+ return list.insertBefore(breadcrumb._target, list.childNodes[aIndex]);
+ },
+
+ /**
+ * Returns the child node in this container situated at the specified index.
+ *
+ * @param number aIndex
+ * The position in the container intended for this item.
+ * @return nsIDOMNode
+ * The element associated with the displayed item.
+ */
+ getItemAtIndex: function(aIndex) {
+ return this._list.childNodes[aIndex];
+ },
+
+ /**
+ * Removes the specified child node from this container.
+ *
+ * @param nsIDOMNode aChild
+ * The element associated with the displayed item.
+ */
+ removeChild: function(aChild) {
+ this._list.removeChild(aChild);
+
+ if (this._selectedItem == aChild) {
+ this._selectedItem = null;
+ }
+ },
+
+ /**
+ * Removes all of the child nodes from this container.
+ */
+ removeAllItems: function() {
+ let list = this._list;
+
+ while (list.hasChildNodes()) {
+ list.firstChild.remove();
+ }
+
+ this._selectedItem = null;
+ },
+
+ /**
+ * Gets the currently selected child node in this container.
+ * @return nsIDOMNode
+ */
+ get selectedItem() this._selectedItem,
+
+ /**
+ * Sets the currently selected child node in this container.
+ * @param nsIDOMNode aChild
+ */
+ set selectedItem(aChild) {
+ let childNodes = this._list.childNodes;
+
+ if (!aChild) {
+ this._selectedItem = null;
+ }
+ for (let node of childNodes) {
+ if (node == aChild) {
+ node.setAttribute("checked", "");
+ this._selectedItem = node;
+ } else {
+ node.removeAttribute("checked");
+ }
+ }
+
+ // Repeated calls to ensureElementIsVisible would interfere with each other
+ // and may sometimes result in incorrect scroll positions.
+ this.window.clearTimeout(this._ensureVisibleTimeout);
+ this._ensureVisibleTimeout = this.window.setTimeout(() => {
+ if (this._selectedItem) {
+ this._list.ensureElementIsVisible(this._selectedItem);
+ }
+ }, ENSURE_SELECTION_VISIBLE_DELAY);
+ },
+
+ /**
+ * The underflow and overflow listener for the arrowscrollbox container.
+ */
+ _onUnderflow: function({ target }) {
+ if (target != this._list) {
+ return;
+ }
+ target._scrollButtonUp.collapsed = true;
+ target._scrollButtonDown.collapsed = true;
+ target.removeAttribute("overflows");
+ },
+
+ /**
+ * The underflow and overflow listener for the arrowscrollbox container.
+ */
+ _onOverflow: function({ target }) {
+ if (target != this._list) {
+ return;
+ }
+ target._scrollButtonUp.collapsed = false;
+ target._scrollButtonDown.collapsed = false;
+ target.setAttribute("overflows", "");
+ },
+
+ window: null,
+ document: null,
+ _parent: null,
+ _list: null,
+ _selectedItem: null,
+ _ensureVisibleTimeout: null
+};
+
+/**
+ * A Breadcrumb constructor for the BreadcrumbsWidget.
+ *
+ * @param BreadcrumbsWidget aWidget
+ * The widget to contain this breadcrumb.
+ * @param string | nsIDOMNode aContents
+ * The string or node displayed in the container.
+ */
+function Breadcrumb(aWidget, aContents) {
+ this.document = aWidget.document;
+ this.window = aWidget.window;
+ this.ownerView = aWidget;
+
+ this._target = this.document.createElement("hbox");
+ this._target.className = "breadcrumbs-widget-item";
+ this._target.setAttribute("align", "center");
+ this.contents = aContents;
+}
+
+Breadcrumb.prototype = {
+ /**
+ * Sets the contents displayed in this item's view.
+ *
+ * @param string | nsIDOMNode aContents
+ * The string or node displayed in the container.
+ */
+ set contents(aContents) {
+ // If this item's view contents are a string, then create a label to hold
+ // the text displayed in this breadcrumb.
+ if (typeof aContents == "string") {
+ let label = this.document.createElement("label");
+ label.setAttribute("value", aContents);
+ this.contents = label;
+ return;
+ }
+ // If there are already some contents displayed, replace them.
+ if (this._target.hasChildNodes()) {
+ this._target.replaceChild(aContents, this._target.firstChild);
+ return;
+ }
+ // These are the first contents ever displayed.
+ this._target.appendChild(aContents);
+ },
+
+ window: null,
+ document: null,
+ ownerView: null,
+ _target: null
+};
diff --git a/browser/devtools/shared/widgets/SideMenuWidget.jsm b/browser/devtools/shared/widgets/SideMenuWidget.jsm
new file mode 100644
index 000000000..d4ca8d9f0
--- /dev/null
+++ b/browser/devtools/shared/widgets/SideMenuWidget.jsm
@@ -0,0 +1,621 @@
+/* -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* 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 = Components.interfaces;
+const Cu = Components.utils;
+
+const ENSURE_SELECTION_VISIBLE_DELAY = 50; // ms
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource:///modules/devtools/ViewHelpers.jsm");
+Cu.import("resource:///modules/devtools/shared/event-emitter.js");
+
+XPCOMUtils.defineLazyModuleGetter(this, "NetworkHelper",
+ "resource://gre/modules/devtools/NetworkHelper.jsm");
+
+this.EXPORTED_SYMBOLS = ["SideMenuWidget"];
+
+/**
+ * A simple side menu, with the ability of grouping menu items.
+ * This widget should be used in tandem with the WidgetMethods in ViewHelpers.jsm
+ *
+ * @param nsIDOMNode aNode
+ * The element associated with the widget.
+ * @param boolean aShowArrows
+ * Specifies if items in this container should display horizontal arrows.
+ */
+this.SideMenuWidget = function SideMenuWidget(aNode, aShowArrows = true) {
+ this.document = aNode.ownerDocument;
+ this.window = this.document.defaultView;
+ this._parent = aNode;
+ this._showArrows = aShowArrows;
+
+ // Create an internal scrollbox container.
+ this._list = this.document.createElement("scrollbox");
+ this._list.className = "side-menu-widget-container";
+ this._list.setAttribute("flex", "1");
+ this._list.setAttribute("orient", "vertical");
+ this._list.setAttribute("with-arrow", aShowArrows);
+ this._list.setAttribute("tabindex", "0");
+ this._list.addEventListener("keypress", e => this.emit("keyPress", e), false);
+ this._list.addEventListener("mousedown", e => this.emit("mousePress", e), false);
+ this._parent.appendChild(this._list);
+ this._boxObject = this._list.boxObject.QueryInterface(Ci.nsIScrollBoxObject);
+
+ // Menu items can optionally be grouped.
+ this._groupsByName = new Map(); // Can't use a WeakMap because keys are strings.
+ this._orderedGroupElementsArray = [];
+ this._orderedMenuElementsArray = [];
+
+ // This widget emits events that can be handled in a MenuContainer.
+ EventEmitter.decorate(this);
+
+ // Delegate some of the associated node's methods to satisfy the interface
+ // required by MenuContainer instances.
+ ViewHelpers.delegateWidgetEventMethods(this, aNode);
+};
+
+SideMenuWidget.prototype = {
+ /**
+ * Specifies if groups in this container should be sorted alphabetically.
+ */
+ sortedGroups: true,
+
+ /**
+ * Specifies if this container should try to keep the selected item visible.
+ * (For example, when new items are added the selection is brought into view).
+ */
+ maintainSelectionVisible: true,
+
+ /**
+ * Specifies that the container viewport should be "stuck" to the
+ * bottom. That is, the container is automatically scrolled down to
+ * keep appended items visible, but only when the scroll position is
+ * already at the bottom.
+ */
+ autoscrollWithAppendedItems: false,
+
+ /**
+ * Inserts an item in this container at the specified index, optionally
+ * grouping by name.
+ *
+ * @param number aIndex
+ * The position in the container intended for this item.
+ * @param string | nsIDOMNode aContents
+ * The string or node displayed in the container.
+ * @param string aTooltip [optional]
+ * A tooltip attribute for the displayed item.
+ * @param string aGroup [optional]
+ * The group to place the displayed item into.
+ * @return nsIDOMNode
+ * The element associated with the displayed item.
+ */
+ insertItemAt: function(aIndex, aContents, aTooltip = "", aGroup = "") {
+ aTooltip = NetworkHelper.convertToUnicode(unescape(aTooltip));
+ aGroup = NetworkHelper.convertToUnicode(unescape(aGroup));
+
+ // Invalidate any notices set on this widget.
+ this.removeAttribute("notice");
+
+ // Maintaining scroll position at the bottom when a new item is inserted
+ // depends on several factors (the order of testing is important to avoid
+ // needlessly expensive operations that may cause reflows):
+ let maintainScrollAtBottom =
+ // 1. The behavior should be enabled,
+ this.autoscrollWithAppendedItems &&
+ // 2. There shouldn't currently be any selected item in the list.
+ !this._selectedItem &&
+ // 3. The new item should be appended at the end of the list.
+ (aIndex < 0 || aIndex >= this._orderedMenuElementsArray.length) &&
+ // 4. The list should already be scrolled at the bottom.
+ (this._list.scrollTop + this._list.clientHeight >= this._list.scrollHeight);
+
+ let group = this._getMenuGroupForName(aGroup);
+ let item = this._getMenuItemForGroup(group, aContents, aTooltip);
+ let element = item.insertSelfAt(aIndex);
+
+ if (this.maintainSelectionVisible) {
+ this.ensureSelectionIsVisible({ withGroup: true, delayed: true });
+ }
+ if (maintainScrollAtBottom) {
+ this._list.scrollTop = this._list.scrollHeight;
+ }
+
+ return element;
+ },
+
+ /**
+ * Returns the child node in this container situated at the specified index.
+ *
+ * @param number aIndex
+ * The position in the container intended for this item.
+ * @return nsIDOMNode
+ * The element associated with the displayed item.
+ */
+ getItemAtIndex: function(aIndex) {
+ return this._orderedMenuElementsArray[aIndex];
+ },
+
+ /**
+ * Removes the specified child node from this container.
+ *
+ * @param nsIDOMNode aChild
+ * The element associated with the displayed item.
+ */
+ removeChild: function(aChild) {
+ if (aChild.className == "side-menu-widget-item-contents") {
+ // Remove the item itself, not the contents.
+ aChild.parentNode.remove();
+ } else {
+ // Groups with no title don't have any special internal structure.
+ aChild.remove();
+ }
+
+ this._orderedMenuElementsArray.splice(
+ this._orderedMenuElementsArray.indexOf(aChild), 1);
+
+ if (this._selectedItem == aChild) {
+ this._selectedItem = null;
+ }
+ },
+
+ /**
+ * Removes all of the child nodes from this container.
+ */
+ removeAllItems: function() {
+ let parent = this._parent;
+ let list = this._list;
+
+ while (list.hasChildNodes()) {
+ list.firstChild.remove();
+ }
+
+ this._selectedItem = null;
+
+ this._groupsByName.clear();
+ this._orderedGroupElementsArray.length = 0;
+ this._orderedMenuElementsArray.length = 0;
+ },
+
+ /**
+ * Gets the currently selected child node in this container.
+ * @return nsIDOMNode
+ */
+ get selectedItem() this._selectedItem,
+
+ /**
+ * Sets the currently selected child node in this container.
+ * @param nsIDOMNode aChild
+ */
+ set selectedItem(aChild) {
+ let menuArray = this._orderedMenuElementsArray;
+
+ if (!aChild) {
+ this._selectedItem = null;
+ }
+ for (let node of menuArray) {
+ if (node == aChild) {
+ node.classList.add("selected");
+ node.parentNode.classList.add("selected");
+ this._selectedItem = node;
+ } else {
+ node.classList.remove("selected");
+ node.parentNode.classList.remove("selected");
+ }
+ }
+
+ // Repeated calls to ensureElementIsVisible would interfere with each other
+ // and may sometimes result in incorrect scroll positions.
+ this.ensureSelectionIsVisible({ delayed: true });
+ },
+
+ /**
+ * Ensures the selected element is visible.
+ * @see SideMenuWidget.prototype.ensureElementIsVisible.
+ */
+ ensureSelectionIsVisible: function(aFlags) {
+ this.ensureElementIsVisible(this.selectedItem, aFlags);
+ },
+
+ /**
+ * Ensures the specified element is visible.
+ *
+ * @param nsIDOMNode aElement
+ * The element to make visible.
+ * @param object aFlags [optional]
+ * An object containing some of the following flags:
+ * - withGroup: true if the group header should also be made visible, if possible
+ * - delayed: wait a few cycles before ensuring the selection is visible
+ */
+ ensureElementIsVisible: function(aElement, aFlags = {}) {
+ if (!aElement) {
+ return;
+ }
+
+ if (aFlags.delayed) {
+ delete aFlags.delayed;
+ this.window.clearTimeout(this._ensureVisibleTimeout);
+ this._ensureVisibleTimeout = this.window.setTimeout(() => {
+ this.ensureElementIsVisible(aElement, aFlags);
+ }, ENSURE_SELECTION_VISIBLE_DELAY);
+ return;
+ }
+
+ if (aFlags.withGroup) {
+ let groupList = aElement.parentNode;
+ let groupContainer = groupList.parentNode;
+ groupContainer.scrollIntoView(true); // Align with the top.
+ }
+
+ this._boxObject.ensureElementIsVisible(aElement);
+ },
+
+ /**
+ * Shows all the groups, even the ones with no visible children.
+ */
+ showEmptyGroups: function() {
+ for (let group of this._orderedGroupElementsArray) {
+ group.hidden = false;
+ }
+ },
+
+ /**
+ * Hides all the groups which have no visible children.
+ */
+ hideEmptyGroups: function() {
+ let visibleChildNodes = ".side-menu-widget-item-contents:not([hidden=true])";
+
+ for (let group of this._orderedGroupElementsArray) {
+ group.hidden = group.querySelectorAll(visibleChildNodes).length == 0;
+ }
+ for (let menuItem of this._orderedMenuElementsArray) {
+ menuItem.parentNode.hidden = menuItem.hidden;
+ }
+ },
+
+ /**
+ * Returns the value of the named attribute on this container.
+ *
+ * @param string aName
+ * The name of the attribute.
+ * @return string
+ * The current attribute value.
+ */
+ getAttribute: function(aName) {
+ return this._parent.getAttribute(aName);
+ },
+
+ /**
+ * Adds a new attribute or changes an existing attribute on this container.
+ *
+ * @param string aName
+ * The name of the attribute.
+ * @param string aValue
+ * The desired attribute value.
+ */
+ setAttribute: function(aName, aValue) {
+ this._parent.setAttribute(aName, aValue);
+
+ if (aName == "notice") {
+ this.notice = aValue;
+ }
+ },
+
+ /**
+ * Removes an attribute on this container.
+ *
+ * @param string aName
+ * The name of the attribute.
+ */
+ removeAttribute: function(aName) {
+ this._parent.removeAttribute(aName);
+
+ if (aName == "notice") {
+ this._removeNotice();
+ }
+ },
+
+ /**
+ * Sets the text displayed in this container as a notice.
+ * @param string aValue
+ */
+ set notice(aValue) {
+ if (this._noticeTextNode) {
+ this._noticeTextNode.setAttribute("value", aValue);
+ }
+ this._noticeTextValue = aValue;
+ this._appendNotice();
+ },
+
+ /**
+ * Creates and appends a label representing a notice in this container.
+ */
+ _appendNotice: function() {
+ if (this._noticeTextNode || !this._noticeTextValue) {
+ return;
+ }
+
+ let container = this.document.createElement("vbox");
+ container.className = "side-menu-widget-empty-notice-container";
+ container.setAttribute("align", "center");
+
+ let label = this.document.createElement("label");
+ label.className = "plain side-menu-widget-empty-notice";
+ label.setAttribute("value", this._noticeTextValue);
+ container.appendChild(label);
+
+ this._parent.insertBefore(container, this._list);
+ this._noticeTextContainer = container;
+ this._noticeTextNode = label;
+ },
+
+ /**
+ * Removes the label representing a notice in this container.
+ */
+ _removeNotice: function() {
+ if (!this._noticeTextNode) {
+ return;
+ }
+
+ this._parent.removeChild(this._noticeTextContainer);
+ this._noticeTextContainer = null;
+ this._noticeTextNode = null;
+ },
+
+ /**
+ * Gets a container representing a group for menu items. If the container
+ * is not available yet, it is immediately created.
+ *
+ * @param string aName
+ * The required group name.
+ * @return SideMenuGroup
+ * The newly created group.
+ */
+ _getMenuGroupForName: function(aName) {
+ let cachedGroup = this._groupsByName.get(aName);
+ if (cachedGroup) {
+ return cachedGroup;
+ }
+
+ let group = new SideMenuGroup(this, aName);
+ this._groupsByName.set(aName, group);
+ group.insertSelfAt(this.sortedGroups ? group.findExpectedIndexForSelf() : -1);
+ return group;
+ },
+
+ /**
+ * Gets a menu item to be displayed inside a group.
+ * @see SideMenuWidget.prototype._getMenuGroupForName
+ *
+ * @param SideMenuGroup aGroup
+ * The group to contain the menu item.
+ * @param string | nsIDOMNode aContents
+ * The string or node displayed in the container.
+ * @param string aTooltip [optional]
+ * A tooltip attribute for the displayed item.
+ */
+ _getMenuItemForGroup: function(aGroup, aContents, aTooltip) {
+ return new SideMenuItem(aGroup, aContents, aTooltip, this._showArrows);
+ },
+
+ window: null,
+ document: null,
+ _showArrows: false,
+ _parent: null,
+ _list: null,
+ _boxObject: null,
+ _selectedItem: null,
+ _groupsByName: null,
+ _orderedGroupElementsArray: null,
+ _orderedMenuElementsArray: null,
+ _ensureVisibleTimeout: null,
+ _noticeTextContainer: null,
+ _noticeTextNode: null,
+ _noticeTextValue: ""
+};
+
+/**
+ * A SideMenuGroup constructor for the BreadcrumbsWidget.
+ * Represents a group which should contain SideMenuItems.
+ *
+ * @param SideMenuWidget aWidget
+ * The widget to contain this menu item.
+ * @param string aName
+ * The string displayed in the container.
+ */
+function SideMenuGroup(aWidget, aName) {
+ this.document = aWidget.document;
+ this.window = aWidget.window;
+ this.ownerView = aWidget;
+ this.identifier = aName;
+
+ // Create an internal title and list container.
+ if (aName) {
+ let target = this._target = this.document.createElement("vbox");
+ target.className = "side-menu-widget-group";
+ target.setAttribute("name", aName);
+ target.setAttribute("tooltiptext", aName);
+
+ let list = this._list = this.document.createElement("vbox");
+ list.className = "side-menu-widget-group-list";
+
+ let title = this._title = this.document.createElement("hbox");
+ title.className = "side-menu-widget-group-title";
+
+ let name = this._name = this.document.createElement("label");
+ name.className = "plain name";
+ name.setAttribute("value", aName);
+ name.setAttribute("crop", "end");
+ name.setAttribute("flex", "1");
+
+ title.appendChild(name);
+ target.appendChild(title);
+ target.appendChild(list);
+ }
+ // Skip a few redundant nodes when no title is shown.
+ else {
+ let target = this._target = this._list = this.document.createElement("vbox");
+ target.className = "side-menu-widget-group side-menu-widget-group-list";
+ }
+}
+
+SideMenuGroup.prototype = {
+ get _orderedGroupElementsArray() this.ownerView._orderedGroupElementsArray,
+ get _orderedMenuElementsArray() this.ownerView._orderedMenuElementsArray,
+
+ /**
+ * Inserts this group in the parent container at the specified index.
+ *
+ * @param number aIndex
+ * The position in the container intended for this group.
+ */
+ insertSelfAt: function(aIndex) {
+ let ownerList = this.ownerView._list;
+ let groupsArray = this._orderedGroupElementsArray;
+
+ if (aIndex >= 0) {
+ ownerList.insertBefore(this._target, groupsArray[aIndex]);
+ groupsArray.splice(aIndex, 0, this._target);
+ } else {
+ ownerList.appendChild(this._target);
+ groupsArray.push(this._target);
+ }
+ },
+
+ /**
+ * Finds the expected index of this group based on its name.
+ *
+ * @return number
+ * The expected index.
+ */
+ findExpectedIndexForSelf: function() {
+ let identifier = this.identifier;
+ let groupsArray = this._orderedGroupElementsArray;
+
+ for (let group of groupsArray) {
+ let name = group.getAttribute("name");
+ if (name > identifier && // Insertion sort at its best :)
+ !name.contains(identifier)) { // Least significat group should be last.
+ return groupsArray.indexOf(group);
+ }
+ }
+ return -1;
+ },
+
+ window: null,
+ document: null,
+ ownerView: null,
+ identifier: "",
+ _target: null,
+ _title: null,
+ _name: null,
+ _list: null
+};
+
+/**
+ * A SideMenuItem constructor for the BreadcrumbsWidget.
+ *
+ * @param SideMenuGroup aGroup
+ * The group to contain this menu item.
+ * @param string aTooltip [optional]
+ * A tooltip attribute for the displayed item.
+ * @param string | nsIDOMNode aContents
+ * The string or node displayed in the container.
+ * @param boolean aArrowFlag
+ * True if a horizontal arrow should be shown.
+ */
+function SideMenuItem(aGroup, aContents, aTooltip, aArrowFlag) {
+ this.document = aGroup.document;
+ this.window = aGroup.window;
+ this.ownerView = aGroup;
+
+ // Show a horizontal arrow towards the content.
+ if (aArrowFlag) {
+ let container = this._container = this.document.createElement("hbox");
+ container.className = "side-menu-widget-item";
+ container.setAttribute("tooltiptext", aTooltip);
+
+ let target = this._target = this.document.createElement("vbox");
+ target.className = "side-menu-widget-item-contents";
+
+ let arrow = this._arrow = this.document.createElement("hbox");
+ arrow.className = "side-menu-widget-item-arrow";
+
+ container.appendChild(target);
+ container.appendChild(arrow);
+ }
+ // Skip a few redundant nodes when no horizontal arrow is shown.
+ else {
+ let target = this._target = this._container = this.document.createElement("hbox");
+ target.className = "side-menu-widget-item side-menu-widget-item-contents";
+ }
+
+ this._target.setAttribute("flex", "1");
+ this.contents = aContents;
+}
+
+SideMenuItem.prototype = {
+ get _orderedGroupElementsArray() this.ownerView._orderedGroupElementsArray,
+ get _orderedMenuElementsArray() this.ownerView._orderedMenuElementsArray,
+
+ /**
+ * Inserts this item in the parent group at the specified index.
+ *
+ * @param number aIndex
+ * The position in the container intended for this item.
+ * @return nsIDOMNode
+ * The element associated with the displayed item.
+ */
+ insertSelfAt: function(aIndex) {
+ let ownerList = this.ownerView._list;
+ let menuArray = this._orderedMenuElementsArray;
+
+ if (aIndex >= 0) {
+ ownerList.insertBefore(this._container, ownerList.childNodes[aIndex]);
+ menuArray.splice(aIndex, 0, this._target);
+ } else {
+ ownerList.appendChild(this._container);
+ menuArray.push(this._target);
+ }
+
+ return this._target;
+ },
+
+ /**
+ * Sets the contents displayed in this item's view.
+ *
+ * @param string | nsIDOMNode aContents
+ * The string or node displayed in the container.
+ */
+ set contents(aContents) {
+ // If this item's view contents are a string, then create a label to hold
+ // the text displayed in this breadcrumb.
+ if (typeof aContents == "string") {
+ let label = this.document.createElement("label");
+ label.className = "side-menu-widget-item-label";
+ label.setAttribute("value", aContents);
+ label.setAttribute("crop", "start");
+ label.setAttribute("flex", "1");
+ this.contents = label;
+ return;
+ }
+ // If there are already some contents displayed, replace them.
+ if (this._target.hasChildNodes()) {
+ this._target.replaceChild(aContents, this._target.firstChild);
+ return;
+ }
+ // These are the first contents ever displayed.
+ this._target.appendChild(aContents);
+ },
+
+ window: null,
+ document: null,
+ ownerView: null,
+ _target: null,
+ _container: null,
+ _arrow: null
+};
diff --git a/browser/devtools/shared/widgets/VariablesView.jsm b/browser/devtools/shared/widgets/VariablesView.jsm
new file mode 100644
index 000000000..7a4d5531b
--- /dev/null
+++ b/browser/devtools/shared/widgets/VariablesView.jsm
@@ -0,0 +1,3166 @@
+/* -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* 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 = Components.interfaces;
+const Cu = Components.utils;
+
+const DBG_STRINGS_URI = "chrome://browser/locale/devtools/debugger.properties";
+const LAZY_EMPTY_DELAY = 150; // ms
+const LAZY_EXPAND_DELAY = 50; // ms
+const LAZY_APPEND_DELAY = 100; // ms
+const LAZY_APPEND_BATCH = 100; // nodes
+const PAGE_SIZE_SCROLL_HEIGHT_RATIO = 100;
+const PAGE_SIZE_MAX_JUMPS = 30;
+const SEARCH_ACTION_MAX_DELAY = 300; // ms
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource:///modules/devtools/ViewHelpers.jsm");
+Cu.import("resource:///modules/devtools/shared/event-emitter.js");
+Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js");
+
+XPCOMUtils.defineLazyModuleGetter(this, "NetworkHelper",
+ "resource://gre/modules/devtools/NetworkHelper.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "WebConsoleUtils",
+ "resource://gre/modules/devtools/WebConsoleUtils.jsm");
+
+this.EXPORTED_SYMBOLS = ["VariablesView"];
+
+/**
+ * Debugger localization strings.
+ */
+const STR = Services.strings.createBundle(DBG_STRINGS_URI);
+
+/**
+ * A tree view for inspecting scopes, objects and properties.
+ * Iterable via "for (let [id, scope] in instance) { }".
+ * Requires the devtools common.css and debugger.css skin stylesheets.
+ *
+ * To allow replacing variable or property values in this view, provide an
+ * "eval" function property. To allow replacing variable or property names,
+ * provide a "switch" function. To handle deleting variables or properties,
+ * provide a "delete" function.
+ *
+ * @param nsIDOMNode aParentNode
+ * The parent node to hold this view.
+ * @param object aFlags [optional]
+ * An object contaning initialization options for this view.
+ * e.g. { lazyEmpty: true, searchEnabled: true ... }
+ */
+this.VariablesView = function VariablesView(aParentNode, aFlags = {}) {
+ this._store = []; // Can't use a Map because Scope names needn't be unique.
+ this._itemsByElement = new WeakMap();
+ this._prevHierarchy = new Map();
+ this._currHierarchy = new Map();
+
+ this._parent = aParentNode;
+ this._parent.classList.add("variables-view-container");
+ this._appendEmptyNotice();
+
+ this._onSearchboxInput = this._onSearchboxInput.bind(this);
+ this._onSearchboxKeyPress = this._onSearchboxKeyPress.bind(this);
+ this._onViewKeyPress = this._onViewKeyPress.bind(this);
+
+ // Create an internal scrollbox container.
+ this._list = this.document.createElement("scrollbox");
+ this._list.setAttribute("orient", "vertical");
+ this._list.addEventListener("keypress", this._onViewKeyPress, false);
+ this._parent.appendChild(this._list);
+ this._boxObject = this._list.boxObject.QueryInterface(Ci.nsIScrollBoxObject);
+
+ for (let name in aFlags) {
+ this[name] = aFlags[name];
+ }
+
+ EventEmitter.decorate(this);
+};
+
+VariablesView.prototype = {
+ /**
+ * Helper setter for populating this container with a raw object.
+ *
+ * @param object aObject
+ * The raw object to display. You can only provide this object
+ * if you want the variables view to work in sync mode.
+ */
+ set rawObject(aObject) {
+ this.empty();
+ this.addScope().addItem().populate(aObject, { sorted: true });
+ },
+
+ /**
+ * Adds a scope to contain any inspected variables.
+ *
+ * @param string aName
+ * The scope's name (e.g. "Local", "Global" etc.).
+ * @return Scope
+ * The newly created Scope instance.
+ */
+ addScope: function(aName = "") {
+ this._removeEmptyNotice();
+ this._toggleSearchVisibility(true);
+
+ let scope = new Scope(this, aName);
+ this._store.push(scope);
+ this._itemsByElement.set(scope._target, scope);
+ this._currHierarchy.set(aName, scope);
+ scope.header = !!aName;
+ return scope;
+ },
+
+ /**
+ * Removes all items from this container.
+ *
+ * @param number aTimeout [optional]
+ * The number of milliseconds to delay the operation if
+ * lazy emptying of this container is enabled.
+ */
+ empty: function(aTimeout = this.lazyEmptyDelay) {
+ // If there are no items in this container, emptying is useless.
+ if (!this._store.length) {
+ return;
+ }
+ // Check if this empty operation may be executed lazily.
+ if (this.lazyEmpty && aTimeout > 0) {
+ this._emptySoon(aTimeout);
+ return;
+ }
+
+ let list = this._list;
+
+ while (list.hasChildNodes()) {
+ list.firstChild.remove();
+ }
+
+ this._store.length = 0;
+ this._itemsByElement.clear();
+
+ this._appendEmptyNotice();
+ this._toggleSearchVisibility(false);
+ },
+
+ /**
+ * Emptying this container and rebuilding it immediately afterwards would
+ * result in a brief redraw flicker, because the previously expanded nodes
+ * may get asynchronously re-expanded, after fetching the prototype and
+ * properties from a server.
+ *
+ * To avoid such behaviour, a normal container list is rebuild, but not
+ * immediately attached to the parent container. The old container list
+ * is kept around for a short period of time, hopefully accounting for the
+ * data fetching delay. In the meantime, any operations can be executed
+ * normally.
+ *
+ * @see VariablesView.empty
+ * @see VariablesView.commitHierarchy
+ */
+ _emptySoon: function(aTimeout) {
+ let prevList = this._list;
+ let currList = this._list = this.document.createElement("scrollbox");
+
+ this._store.length = 0;
+ this._itemsByElement.clear();
+
+ this._emptyTimeout = this.window.setTimeout(() => {
+ this._emptyTimeout = null;
+
+ prevList.removeEventListener("keypress", this._onViewKeyPress, false);
+ currList.addEventListener("keypress", this._onViewKeyPress, false);
+ currList.setAttribute("orient", "vertical");
+
+ this._parent.removeChild(prevList);
+ this._parent.appendChild(currList);
+ this._boxObject = currList.boxObject.QueryInterface(Ci.nsIScrollBoxObject);
+
+ if (!this._store.length) {
+ this._appendEmptyNotice();
+ this._toggleSearchVisibility(false);
+ }
+ }, aTimeout);
+ },
+
+ /**
+ * The controller for this VariablesView, if it has one.
+ */
+ controller: null,
+
+ /**
+ * The amount of time (in milliseconds) it takes to empty this view lazily.
+ */
+ lazyEmptyDelay: LAZY_EMPTY_DELAY,
+
+ /**
+ * Specifies if this view may be emptied lazily.
+ * @see VariablesView.prototype.empty
+ */
+ lazyEmpty: false,
+
+ /**
+ * Specifies if nodes in this view may be added lazily.
+ * @see Scope.prototype._lazyAppend
+ */
+ lazyAppend: true,
+
+ /**
+ * Specifies if nodes in this view may be expanded lazily.
+ * @see Scope.prototype.expand
+ */
+ lazyExpand: true,
+
+ /**
+ * Function called each time a variable or property's value is changed via
+ * user interaction. If null, then value changes are disabled.
+ *
+ * This property is applied recursively onto each scope in this view and
+ * affects only the child nodes when they're created.
+ */
+ eval: null,
+
+ /**
+ * Function called each time a variable or property's name is changed via
+ * user interaction. If null, then name changes are disabled.
+ *
+ * This property is applied recursively onto each scope in this view and
+ * affects only the child nodes when they're created.
+ */
+ switch: null,
+
+ /**
+ * Function called each time a variable or property is deleted via
+ * user interaction. If null, then deletions are disabled.
+ *
+ * This property is applied recursively onto each scope in this view and
+ * affects only the child nodes when they're created.
+ */
+ delete: null,
+
+ /**
+ * Specifies if after an eval or switch operation, the variable or property
+ * which has been edited should be disabled.
+ */
+ preventDisableOnChage: false,
+
+ /**
+ * Specifies if, whenever a variable or property descriptor is available,
+ * configurable, enumerable, writable, frozen, sealed and extensible
+ * attributes should not affect presentation.
+ *
+ * This flag is applied recursively onto each scope in this view and
+ * affects only the child nodes when they're created.
+ */
+ preventDescriptorModifiers: false,
+
+ /**
+ * The tooltip text shown on a variable or property's value if an |eval|
+ * function is provided, in order to change the variable or property's value.
+ *
+ * This flag is applied recursively onto each scope in this view and
+ * affects only the child nodes when they're created.
+ */
+ editableValueTooltip: STR.GetStringFromName("variablesEditableValueTooltip"),
+
+ /**
+ * The tooltip text shown on a variable or property's name if a |switch|
+ * function is provided, in order to change the variable or property's name.
+ *
+ * This flag is applied recursively onto each scope in this view and
+ * affects only the child nodes when they're created.
+ */
+ editableNameTooltip: STR.GetStringFromName("variablesEditableNameTooltip"),
+
+ /**
+ * The tooltip text shown on a variable or property's edit button if an
+ * |eval| function is provided and a getter/setter descriptor is present,
+ * in order to change the variable or property to a plain value.
+ *
+ * This flag is applied recursively onto each scope in this view and
+ * affects only the child nodes when they're created.
+ */
+ editButtonTooltip: STR.GetStringFromName("variablesEditButtonTooltip"),
+
+ /**
+ * The tooltip text shown on a variable or property's delete button if a
+ * |delete| function is provided, in order to delete the variable or property.
+ *
+ * This flag is applied recursively onto each scope in this view and
+ * affects only the child nodes when they're created.
+ */
+ deleteButtonTooltip: STR.GetStringFromName("variablesCloseButtonTooltip"),
+
+ /**
+ * Specifies the context menu attribute set on variables and properties.
+ *
+ * This flag is applied recursively onto each scope in this view and
+ * affects only the child nodes when they're created.
+ */
+ contextMenuId: "",
+
+ /**
+ * The separator label between the variables or properties name and value.
+ *
+ * This flag is applied recursively onto each scope in this view and
+ * affects only the child nodes when they're created.
+ */
+ separatorStr: STR.GetStringFromName("variablesSeparatorLabel"),
+
+ /**
+ * Specifies if enumerable properties and variables should be displayed.
+ * These variables and properties are visible by default.
+ * @param boolean aFlag
+ */
+ set enumVisible(aFlag) {
+ this._enumVisible = aFlag;
+
+ for (let scope of this._store) {
+ scope._enumVisible = aFlag;
+ }
+ },
+
+ /**
+ * Specifies if non-enumerable properties and variables should be displayed.
+ * These variables and properties are visible by default.
+ * @param boolean aFlag
+ */
+ set nonEnumVisible(aFlag) {
+ this._nonEnumVisible = aFlag;
+
+ for (let scope of this._store) {
+ scope._nonEnumVisible = aFlag;
+ }
+ },
+
+ /**
+ * Specifies if only enumerable properties and variables should be displayed.
+ * Both types of these variables and properties are visible by default.
+ * @param boolean aFlag
+ */
+ set onlyEnumVisible(aFlag) {
+ if (aFlag) {
+ this.enumVisible = true;
+ this.nonEnumVisible = false;
+ } else {
+ this.enumVisible = true;
+ this.nonEnumVisible = true;
+ }
+ },
+
+ /**
+ * Sets if the variable and property searching is enabled.
+ * @param boolean aFlag
+ */
+ set searchEnabled(aFlag) aFlag ? this._enableSearch() : this._disableSearch(),
+
+ /**
+ * Gets if the variable and property searching is enabled.
+ * @return boolean
+ */
+ get searchEnabled() !!this._searchboxContainer,
+
+ /**
+ * Sets the text displayed for the searchbox in this container.
+ * @param string aValue
+ */
+ set searchPlaceholder(aValue) {
+ if (this._searchboxNode) {
+ this._searchboxNode.setAttribute("placeholder", aValue);
+ }
+ this._searchboxPlaceholder = aValue;
+ },
+
+ /**
+ * Gets the text displayed for the searchbox in this container.
+ * @return string
+ */
+ get searchPlaceholder() this._searchboxPlaceholder,
+
+ /**
+ * Enables variable and property searching in this view.
+ * Use the "searchEnabled" setter to enable searching.
+ */
+ _enableSearch: function() {
+ // If searching was already enabled, no need to re-enable it again.
+ if (this._searchboxContainer) {
+ return;
+ }
+ let document = this.document;
+ let ownerView = this._parent.parentNode;
+
+ let container = this._searchboxContainer = document.createElement("hbox");
+ container.className = "devtools-toolbar";
+
+ // Hide the variables searchbox container if there are no variables or
+ // properties to display.
+ container.hidden = !this._store.length;
+
+ let searchbox = this._searchboxNode = document.createElement("textbox");
+ searchbox.className = "variables-view-searchinput devtools-searchinput";
+ searchbox.setAttribute("placeholder", this._searchboxPlaceholder);
+ searchbox.setAttribute("type", "search");
+ searchbox.setAttribute("flex", "1");
+ searchbox.addEventListener("input", this._onSearchboxInput, false);
+ searchbox.addEventListener("keypress", this._onSearchboxKeyPress, false);
+
+ container.appendChild(searchbox);
+ ownerView.insertBefore(container, this._parent);
+ },
+
+ /**
+ * Disables variable and property searching in this view.
+ * Use the "searchEnabled" setter to disable searching.
+ */
+ _disableSearch: function() {
+ // If searching was already disabled, no need to re-disable it again.
+ if (!this._searchboxContainer) {
+ return;
+ }
+ this._searchboxContainer.remove();
+ this._searchboxNode.removeEventListener("input", this._onSearchboxInput, false);
+ this._searchboxNode.removeEventListener("keypress", this._onSearchboxKeyPress, false);
+
+ this._searchboxContainer = null;
+ this._searchboxNode = null;
+ },
+
+ /**
+ * Sets the variables searchbox container hidden or visible.
+ * It's hidden by default.
+ *
+ * @param boolean aVisibleFlag
+ * Specifies the intended visibility.
+ */
+ _toggleSearchVisibility: function(aVisibleFlag) {
+ // If searching was already disabled, there's no need to hide it.
+ if (!this._searchboxContainer) {
+ return;
+ }
+ this._searchboxContainer.hidden = !aVisibleFlag;
+ },
+
+ /**
+ * Listener handling the searchbox input event.
+ */
+ _onSearchboxInput: function() {
+ this.performSearch(this._searchboxNode.value);
+ },
+
+ /**
+ * Listener handling the searchbox key press event.
+ */
+ _onSearchboxKeyPress: function(e) {
+ switch(e.keyCode) {
+ case e.DOM_VK_RETURN:
+ case e.DOM_VK_ENTER:
+ this._onSearchboxInput();
+ return;
+ case e.DOM_VK_ESCAPE:
+ this._searchboxNode.value = "";
+ this._onSearchboxInput();
+ return;
+ }
+ },
+
+ /**
+ * Allows searches to be scheduled and delayed to avoid redundant calls.
+ */
+ delayedSearch: true,
+
+ /**
+ * Schedules searching for variables or properties matching the query.
+ *
+ * @param string aQuery
+ * The variable or property to search for.
+ */
+ scheduleSearch: function(aQuery) {
+ if (!this.delayedSearch) {
+ this.performSearch(aQuery);
+ return;
+ }
+ let delay = Math.max(SEARCH_ACTION_MAX_DELAY / aQuery.length, 0);
+
+ this.window.clearTimeout(this._searchTimeout);
+ this._searchFunction = this._startSearch.bind(this, aQuery);
+ this._searchTimeout = this.window.setTimeout(this._searchFunction, delay);
+ },
+
+ /**
+ * Immediately searches for variables or properties matching the query.
+ *
+ * @param string aQuery
+ * The variable or property to search for.
+ */
+ performSearch: function(aQuery) {
+ this.window.clearTimeout(this._searchTimeout);
+ this._searchFunction = null;
+ this._startSearch(aQuery);
+ },
+
+ /**
+ * Performs a case insensitive search for variables or properties matching
+ * the query, and hides non-matched items.
+ *
+ * If aQuery is empty string, then all the scopes are unhidden and expanded,
+ * while the available variables and properties inside those scopes are
+ * just unhidden.
+ *
+ * If aQuery is null or undefined, then all the scopes are just unhidden,
+ * and the available variables and properties inside those scopes are also
+ * just unhidden.
+ *
+ * @param string aQuery
+ * The variable or property to search for.
+ */
+ _startSearch: function(aQuery) {
+ for (let scope of this._store) {
+ switch (aQuery) {
+ case "":
+ scope.expand();
+ // fall through
+ case null:
+ case undefined:
+ scope._performSearch("");
+ break;
+ default:
+ scope._performSearch(aQuery.toLowerCase());
+ break;
+ }
+ }
+ },
+
+ /**
+ * Expands the first search results in this container.
+ */
+ expandFirstSearchResults: function() {
+ for (let scope of this._store) {
+ let match = scope._firstMatch;
+ if (match) {
+ match.expand();
+ }
+ }
+ },
+
+ /**
+ * Find the first item in the tree of visible items in this container that
+ * matches the predicate. Searches in visual order (the order seen by the
+ * user). Descends into each scope to check the scope and its children.
+ *
+ * @param function aPredicate
+ * A function that returns true when a match is found.
+ * @return Scope | Variable | Property
+ * The first visible scope, variable or property, or null if nothing
+ * is found.
+ */
+ _findInVisibleItems: function(aPredicate) {
+ for (let scope of this._store) {
+ let result = scope._findInVisibleItems(aPredicate);
+ if (result) {
+ return result;
+ }
+ }
+ return null;
+ },
+
+ /**
+ * Find the last item in the tree of visible items in this container that
+ * matches the predicate. Searches in reverse visual order (opposite of the
+ * order seen by the user). Descends into each scope to check the scope and
+ * its children.
+ *
+ * @param function aPredicate
+ * A function that returns true when a match is found.
+ * @return Scope | Variable | Property
+ * The last visible scope, variable or property, or null if nothing
+ * is found.
+ */
+ _findInVisibleItemsReverse: function(aPredicate) {
+ for (let i = this._store.length - 1; i >= 0; i--) {
+ let scope = this._store[i];
+ let result = scope._findInVisibleItemsReverse(aPredicate);
+ if (result) {
+ return result;
+ }
+ }
+ return null;
+ },
+
+ /**
+ * Searches for the scope in this container displayed by the specified node.
+ *
+ * @param nsIDOMNode aNode
+ * The node to search for.
+ * @return Scope
+ * The matched scope, or null if nothing is found.
+ */
+ getScopeForNode: function(aNode) {
+ let item = this._itemsByElement.get(aNode);
+ // Match only Scopes, not Variables or Properties.
+ if (item && !(item instanceof Variable)) {
+ return item;
+ }
+ return null;
+ },
+
+ /**
+ * Recursively searches this container for the scope, variable or property
+ * displayed by the specified node.
+ *
+ * @param nsIDOMNode aNode
+ * The node to search for.
+ * @return Scope | Variable | Property
+ * The matched scope, variable or property, or null if nothing is found.
+ */
+ getItemForNode: function(aNode) {
+ return this._itemsByElement.get(aNode);
+ },
+
+ /**
+ * Gets the currently focused scope, variable or property in this view.
+ *
+ * @return Scope | Variable | Property
+ * The focused scope, variable or property, or null if nothing is found.
+ */
+ getFocusedItem: function() {
+ let focused = this.document.commandDispatcher.focusedElement;
+ return this.getItemForNode(focused);
+ },
+
+ /**
+ * Focuses the first visible scope, variable, or property in this container.
+ */
+ focusFirstVisibleItem: function() {
+ let focusableItem = this._findInVisibleItems(item => item.focusable);
+ if (focusableItem) {
+ this._focusItem(focusableItem);
+ }
+ this._parent.scrollTop = 0;
+ this._parent.scrollLeft = 0;
+ },
+
+ /**
+ * Focuses the last visible scope, variable, or property in this container.
+ */
+ focusLastVisibleItem: function() {
+ let focusableItem = this._findInVisibleItemsReverse(item => item.focusable);
+ if (focusableItem) {
+ this._focusItem(focusableItem);
+ }
+ this._parent.scrollTop = this._parent.scrollHeight;
+ this._parent.scrollLeft = 0;
+ },
+
+ /**
+ * Focuses the next scope, variable or property in this view.
+ */
+ focusNextItem: function() {
+ this.focusItemAtDelta(+1);
+ },
+
+ /**
+ * Focuses the previous scope, variable or property in this view.
+ */
+ focusPrevItem: function() {
+ this.focusItemAtDelta(-1);
+ },
+
+ /**
+ * Focuses another scope, variable or property in this view, based on
+ * the index distance from the currently focused item.
+ *
+ * @param number aDelta
+ * A scalar specifying by how many items should the selection change.
+ */
+ focusItemAtDelta: function(aDelta) {
+ let direction = aDelta > 0 ? "advanceFocus" : "rewindFocus";
+ let distance = Math.abs(Math[aDelta > 0 ? "ceil" : "floor"](aDelta));
+ while (distance--) {
+ if (!this._focusChange(direction)) {
+ break; // Out of bounds.
+ }
+ }
+ },
+
+ /**
+ * Focuses the next or previous scope, variable or property in this view.
+ *
+ * @param string aDirection
+ * Either "advanceFocus" or "rewindFocus".
+ * @return boolean
+ * False if the focus went out of bounds and the first or last element
+ * in this view was focused instead.
+ */
+ _focusChange: function(aDirection) {
+ let commandDispatcher = this.document.commandDispatcher;
+ let prevFocusedElement = commandDispatcher.focusedElement;
+ let currFocusedItem = null;
+
+ do {
+ commandDispatcher.suppressFocusScroll = true;
+ commandDispatcher[aDirection]();
+
+ // Make sure the newly focused item is a part of this view.
+ // If the focus goes out of bounds, revert the previously focused item.
+ if (!(currFocusedItem = this.getFocusedItem())) {
+ prevFocusedElement.focus();
+ return false;
+ }
+ } while (!currFocusedItem.focusable);
+
+ // Focus remained within bounds.
+ return true;
+ },
+
+ /**
+ * Focuses a scope, variable or property and makes sure it's visible.
+ *
+ * @param aItem Scope | Variable | Property
+ * The item to focus.
+ * @param boolean aCollapseFlag
+ * True if the focused item should also be collapsed.
+ * @return boolean
+ * True if the item was successfully focused.
+ */
+ _focusItem: function(aItem, aCollapseFlag) {
+ if (!aItem.focusable) {
+ return false;
+ }
+ if (aCollapseFlag) {
+ aItem.collapse();
+ }
+ aItem._target.focus();
+ this._boxObject.ensureElementIsVisible(aItem._arrow);
+ return true;
+ },
+
+ /**
+ * Listener handling a key press event on the view.
+ */
+ _onViewKeyPress: function(e) {
+ let item = this.getFocusedItem();
+
+ // Prevent scrolling when pressing navigation keys.
+ ViewHelpers.preventScrolling(e);
+
+ switch (e.keyCode) {
+ case e.DOM_VK_UP:
+ // Always rewind focus.
+ this.focusPrevItem(true);
+ return;
+
+ case e.DOM_VK_DOWN:
+ // Always advance focus.
+ this.focusNextItem(true);
+ return;
+
+ case e.DOM_VK_LEFT:
+ // Collapse scopes, variables and properties before rewinding focus.
+ if (item._isExpanded && item._isArrowVisible) {
+ item.collapse();
+ } else {
+ this._focusItem(item.ownerView);
+ }
+ return;
+
+ case e.DOM_VK_RIGHT:
+ // Nothing to do here if this item never expands.
+ if (!item._isArrowVisible) {
+ return;
+ }
+ // Expand scopes, variables and properties before advancing focus.
+ if (!item._isExpanded) {
+ item.expand();
+ } else {
+ this.focusNextItem(true);
+ }
+ return;
+
+ case e.DOM_VK_PAGE_UP:
+ // Rewind a certain number of elements based on the container height.
+ this.focusItemAtDelta(-(this.pageSize || Math.min(Math.floor(this._list.scrollHeight /
+ PAGE_SIZE_SCROLL_HEIGHT_RATIO),
+ PAGE_SIZE_MAX_JUMPS)));
+ return;
+
+ case e.DOM_VK_PAGE_DOWN:
+ // Advance a certain number of elements based on the container height.
+ this.focusItemAtDelta(+(this.pageSize || Math.min(Math.floor(this._list.scrollHeight /
+ PAGE_SIZE_SCROLL_HEIGHT_RATIO),
+ PAGE_SIZE_MAX_JUMPS)));
+ return;
+
+ case e.DOM_VK_HOME:
+ this.focusFirstVisibleItem();
+ return;
+
+ case e.DOM_VK_END:
+ this.focusLastVisibleItem();
+ return;
+
+ case e.DOM_VK_RETURN:
+ case e.DOM_VK_ENTER:
+ // Start editing the value or name of the Variable or Property.
+ if (item instanceof Variable) {
+ if (e.metaKey || e.altKey || e.shiftKey) {
+ item._activateNameInput();
+ } else {
+ item._activateValueInput();
+ }
+ }
+ return;
+
+ case e.DOM_VK_DELETE:
+ case e.DOM_VK_BACK_SPACE:
+ // Delete the Variable or Property if allowed.
+ if (item instanceof Variable) {
+ item._onDelete(e);
+ }
+ return;
+ }
+ },
+
+ /**
+ * The number of elements in this container to jump when Page Up or Page Down
+ * keys are pressed. If falsy, then the page size will be based on the
+ * container height.
+ */
+ pageSize: 0,
+
+ /**
+ * Sets the text displayed in this container when there are no available items.
+ * @param string aValue
+ */
+ set emptyText(aValue) {
+ if (this._emptyTextNode) {
+ this._emptyTextNode.setAttribute("value", aValue);
+ }
+ this._emptyTextValue = aValue;
+ this._appendEmptyNotice();
+ },
+
+ /**
+ * Creates and appends a label signaling that this container is empty.
+ */
+ _appendEmptyNotice: function() {
+ if (this._emptyTextNode || !this._emptyTextValue) {
+ return;
+ }
+
+ let label = this.document.createElement("label");
+ label.className = "variables-view-empty-notice";
+ label.setAttribute("value", this._emptyTextValue);
+
+ this._parent.appendChild(label);
+ this._emptyTextNode = label;
+ },
+
+ /**
+ * Removes the label signaling that this container is empty.
+ */
+ _removeEmptyNotice: function() {
+ if (!this._emptyTextNode) {
+ return;
+ }
+
+ this._parent.removeChild(this._emptyTextNode);
+ this._emptyTextNode = null;
+ },
+
+ /**
+ * Gets the parent node holding this view.
+ * @return nsIDOMNode
+ */
+ get parentNode() this._parent,
+
+ /**
+ * Gets the owner document holding this view.
+ * @return nsIHTMLDocument
+ */
+ get document() this._document || (this._document = this._parent.ownerDocument),
+
+ /**
+ * Gets the default window holding this view.
+ * @return nsIDOMWindow
+ */
+ get window() this._window || (this._window = this.document.defaultView),
+
+ _document: null,
+ _window: null,
+
+ _store: null,
+ _prevHierarchy: null,
+ _currHierarchy: null,
+ _enumVisible: true,
+ _nonEnumVisible: true,
+ _emptyTimeout: null,
+ _searchTimeout: null,
+ _searchFunction: null,
+ _parent: null,
+ _list: null,
+ _boxObject: null,
+ _searchboxNode: null,
+ _searchboxContainer: null,
+ _searchboxPlaceholder: "",
+ _emptyTextNode: null,
+ _emptyTextValue: ""
+};
+
+VariablesView.NON_SORTABLE_CLASSES = [
+ "Array",
+ "Int8Array",
+ "Uint8Array",
+ "Uint8ClampedArray",
+ "Int16Array",
+ "Uint16Array",
+ "Int32Array",
+ "Uint32Array",
+ "Float32Array",
+ "Float64Array"
+];
+
+/**
+ * Determine whether an object's properties should be sorted based on its class.
+ *
+ * @param string aClassName
+ * The class of the object.
+ */
+VariablesView.isSortable = function(aClassName) {
+ return VariablesView.NON_SORTABLE_CLASSES.indexOf(aClassName) == -1;
+};
+
+/**
+ * Generates the string evaluated when performing simple value changes.
+ *
+ * @param Variable | Property aItem
+ * The current variable or property.
+ * @param string aCurrentString
+ * The trimmed user inputted string.
+ * @param string aPrefix [optional]
+ * Prefix for the symbolic name.
+ * @return string
+ * The string to be evaluated.
+ */
+VariablesView.simpleValueEvalMacro = function(aItem, aCurrentString, aPrefix = "") {
+ return aPrefix + aItem._symbolicName + "=" + aCurrentString;
+};
+
+/**
+ * Generates the string evaluated when overriding getters and setters with
+ * plain values.
+ *
+ * @param Property aItem
+ * The current getter or setter property.
+ * @param string aCurrentString
+ * The trimmed user inputted string.
+ * @param string aPrefix [optional]
+ * Prefix for the symbolic name.
+ * @return string
+ * The string to be evaluated.
+ */
+VariablesView.overrideValueEvalMacro = function(aItem, aCurrentString, aPrefix = "") {
+ let property = "\"" + aItem._nameString + "\"";
+ let parent = aPrefix + aItem.ownerView._symbolicName || "this";
+
+ return "Object.defineProperty(" + parent + "," + property + "," +
+ "{ value: " + aCurrentString +
+ ", enumerable: " + parent + ".propertyIsEnumerable(" + property + ")" +
+ ", configurable: true" +
+ ", writable: true" +
+ "})";
+};
+
+/**
+ * Generates the string evaluated when performing getters and setters changes.
+ *
+ * @param Property aItem
+ * The current getter or setter property.
+ * @param string aCurrentString
+ * The trimmed user inputted string.
+ * @param string aPrefix [optional]
+ * Prefix for the symbolic name.
+ * @return string
+ * The string to be evaluated.
+ */
+VariablesView.getterOrSetterEvalMacro = function(aItem, aCurrentString, aPrefix = "") {
+ let type = aItem._nameString;
+ let propertyObject = aItem.ownerView;
+ let parentObject = propertyObject.ownerView;
+ let property = "\"" + propertyObject._nameString + "\"";
+ let parent = aPrefix + parentObject._symbolicName || "this";
+
+ switch (aCurrentString) {
+ case "":
+ case "null":
+ case "undefined":
+ let mirrorType = type == "get" ? "set" : "get";
+ let mirrorLookup = type == "get" ? "__lookupSetter__" : "__lookupGetter__";
+
+ // If the parent object will end up without any getter or setter,
+ // morph it into a plain value.
+ if ((type == "set" && propertyObject.getter.type == "undefined") ||
+ (type == "get" && propertyObject.setter.type == "undefined")) {
+ // Make sure the right getter/setter to value override macro is applied to the target object.
+ return propertyObject.evaluationMacro(propertyObject, "undefined", aPrefix);
+ }
+
+ // Construct and return the getter/setter removal evaluation string.
+ // e.g: Object.defineProperty(foo, "bar", {
+ // get: foo.__lookupGetter__("bar"),
+ // set: undefined,
+ // enumerable: true,
+ // configurable: true
+ // })
+ return "Object.defineProperty(" + parent + "," + property + "," +
+ "{" + mirrorType + ":" + parent + "." + mirrorLookup + "(" + property + ")" +
+ "," + type + ":" + undefined +
+ ", enumerable: " + parent + ".propertyIsEnumerable(" + property + ")" +
+ ", configurable: true" +
+ "})";
+
+ default:
+ // Wrap statements inside a function declaration if not already wrapped.
+ if (!aCurrentString.startsWith("function")) {
+ let header = "function(" + (type == "set" ? "value" : "") + ")";
+ let body = "";
+ // If there's a return statement explicitly written, always use the
+ // standard function definition syntax
+ if (aCurrentString.contains("return ")) {
+ body = "{" + aCurrentString + "}";
+ }
+ // If block syntax is used, use the whole string as the function body.
+ else if (aCurrentString.startsWith("{")) {
+ body = aCurrentString;
+ }
+ // Prefer an expression closure.
+ else {
+ body = "(" + aCurrentString + ")";
+ }
+ aCurrentString = header + body;
+ }
+
+ // Determine if a new getter or setter should be defined.
+ let defineType = type == "get" ? "__defineGetter__" : "__defineSetter__";
+
+ // Make sure all quotes are escaped in the expression's syntax,
+ let defineFunc = "eval(\"(" + aCurrentString.replace(/"/g, "\\$&") + ")\")";
+
+ // Construct and return the getter/setter evaluation string.
+ // e.g: foo.__defineGetter__("bar", eval("(function() { return 42; })"))
+ return parent + "." + defineType + "(" + property + "," + defineFunc + ")";
+ }
+};
+
+/**
+ * Function invoked when a getter or setter is deleted.
+ *
+ * @param Property aItem
+ * The current getter or setter property.
+ */
+VariablesView.getterOrSetterDeleteCallback = function(aItem) {
+ aItem._disable();
+
+ // Make sure the right getter/setter to value override macro is applied
+ // to the target object.
+ aItem.ownerView.eval(aItem.evaluationMacro(aItem, ""));
+
+ return true; // Don't hide the element.
+};
+
+
+/**
+ * A Scope is an object holding Variable instances.
+ * Iterable via "for (let [name, variable] in instance) { }".
+ *
+ * @param VariablesView aView
+ * The view to contain this scope.
+ * @param string aName
+ * The scope's name.
+ * @param object aFlags [optional]
+ * Additional options or flags for this scope.
+ */
+function Scope(aView, aName, aFlags = {}) {
+ this.ownerView = aView;
+
+ this._onClick = this._onClick.bind(this);
+ this._openEnum = this._openEnum.bind(this);
+ this._openNonEnum = this._openNonEnum.bind(this);
+ this._batchAppend = this._batchAppend.bind(this);
+
+ // Inherit properties and flags from the parent view. You can override
+ // each of these directly onto any scope, variable or property instance.
+ this.eval = aView.eval;
+ this.switch = aView.switch;
+ this.delete = aView.delete;
+ this.editableValueTooltip = aView.editableValueTooltip;
+ this.editableNameTooltip = aView.editableNameTooltip;
+ this.editButtonTooltip = aView.editButtonTooltip;
+ this.deleteButtonTooltip = aView.deleteButtonTooltip;
+ this.preventDescriptorModifiers = aView.preventDescriptorModifiers;
+ this.contextMenuId = aView.contextMenuId;
+ this.separatorStr = aView.separatorStr;
+
+ // Creating maps and arrays thousands of times for variables or properties
+ // with a large number of children fills up a lot of memory. Make sure
+ // these are instantiated only if needed.
+ XPCOMUtils.defineLazyGetter(this, "_store", () => new Map());
+ XPCOMUtils.defineLazyGetter(this, "_enumItems", () => []);
+ XPCOMUtils.defineLazyGetter(this, "_nonEnumItems", () => []);
+ XPCOMUtils.defineLazyGetter(this, "_batchItems", () => []);
+
+ this._init(aName.trim(), aFlags);
+}
+
+Scope.prototype = {
+ /**
+ * Whether this Scope should be prefetched when it is remoted.
+ */
+ shouldPrefetch: true,
+
+ /**
+ * Create a new Variable that is a child of this Scope.
+ *
+ * @param string aName
+ * The name of the new Property.
+ * @param object aDescriptor
+ * The variable's descriptor.
+ * @return Variable
+ * The newly created child Variable.
+ */
+ _createChild: function(aName, aDescriptor) {
+ return new Variable(this, aName, aDescriptor);
+ },
+
+ /**
+ * Adds a child to contain any inspected properties.
+ *
+ * @param string aName
+ * The child's name.
+ * @param object aDescriptor
+ * Specifies the value and/or type & class of the child,
+ * or 'get' & 'set' accessor properties. If the type is implicit,
+ * it will be inferred from the value.
+ * e.g. - { value: 42 }
+ * - { value: true }
+ * - { value: "nasu" }
+ * - { value: { type: "undefined" } }
+ * - { value: { type: "null" } }
+ * - { value: { type: "object", class: "Object" } }
+ * - { get: { type: "object", class: "Function" },
+ * set: { type: "undefined" } }
+ * @param boolean aRelaxed
+ * True if name duplicates should be allowed.
+ * @return Variable
+ * The newly created Variable instance, null if it already exists.
+ */
+ addItem: function(aName = "", aDescriptor = {}, aRelaxed = false) {
+ if (this._store.has(aName) && !aRelaxed) {
+ return null;
+ }
+
+ let child = this._createChild(aName, aDescriptor);
+ this._store.set(aName, child);
+ this._variablesView._itemsByElement.set(child._target, child);
+ this._variablesView._currHierarchy.set(child._absoluteName, child);
+ child.header = !!aName;
+ return child;
+ },
+
+ /**
+ * Adds items for this variable.
+ *
+ * @param object aItems
+ * An object containing some { name: descriptor } data properties,
+ * specifying the value and/or type & class of the variable,
+ * or 'get' & 'set' accessor properties. If the type is implicit,
+ * it will be inferred from the value.
+ * e.g. - { someProp0: { value: 42 },
+ * someProp1: { value: true },
+ * someProp2: { value: "nasu" },
+ * someProp3: { value: { type: "undefined" } },
+ * someProp4: { value: { type: "null" } },
+ * someProp5: { value: { type: "object", class: "Object" } },
+ * someProp6: { get: { type: "object", class: "Function" },
+ * set: { type: "undefined" } } }
+ * @param object aOptions [optional]
+ * Additional options for adding the properties. Supported options:
+ * - sorted: true to sort all the properties before adding them
+ * - callback: function invoked after each item is added
+ */
+ addItems: function(aItems, aOptions = {}) {
+ let names = Object.keys(aItems);
+
+ // Sort all of the properties before adding them, if preferred.
+ if (aOptions.sorted) {
+ names.sort();
+ }
+ // Add the properties to the current scope.
+ for (let name of names) {
+ let descriptor = aItems[name];
+ let item = this.addItem(name, descriptor);
+
+ if (aOptions.callback) {
+ aOptions.callback(item, descriptor.value);
+ }
+ }
+ },
+
+ /**
+ * Gets the variable in this container having the specified name.
+ *
+ * @param string aName
+ * The name of the variable to get.
+ * @return Variable
+ * The matched variable, or null if nothing is found.
+ */
+ get: function(aName) {
+ return this._store.get(aName);
+ },
+
+ /**
+ * Recursively searches for the variable or property in this container
+ * displayed by the specified node.
+ *
+ * @param nsIDOMNode aNode
+ * The node to search for.
+ * @return Variable | Property
+ * The matched variable or property, or null if nothing is found.
+ */
+ find: function(aNode) {
+ for (let [, variable] of this._store) {
+ let match;
+ if (variable._target == aNode) {
+ match = variable;
+ } else {
+ match = variable.find(aNode);
+ }
+ if (match) {
+ return match;
+ }
+ }
+ return null;
+ },
+
+ /**
+ * Determines if this scope is a direct child of a parent variables view,
+ * scope, variable or property.
+ *
+ * @param VariablesView | Scope | Variable | Property
+ * The parent to check.
+ * @return boolean
+ * True if the specified item is a direct child, false otherwise.
+ */
+ isChildOf: function(aParent) {
+ return this.ownerView == aParent;
+ },
+
+ /**
+ * Determines if this scope is a descendant of a parent variables view,
+ * scope, variable or property.
+ *
+ * @param VariablesView | Scope | Variable | Property
+ * The parent to check.
+ * @return boolean
+ * True if the specified item is a descendant, false otherwise.
+ */
+ isDescendantOf: function(aParent) {
+ if (this.isChildOf(aParent)) {
+ return true;
+ }
+
+ // Recurse to parent if it is a Scope, Variable, or Property.
+ if (this.ownerView instanceof Scope) {
+ return this.ownerView.isDescendantOf(aParent);
+ }
+
+ return false;
+ },
+
+ /**
+ * Shows the scope.
+ */
+ show: function() {
+ this._target.hidden = false;
+ this._isContentVisible = true;
+
+ if (this.onshow) {
+ this.onshow(this);
+ }
+ },
+
+ /**
+ * Hides the scope.
+ */
+ hide: function() {
+ this._target.hidden = true;
+ this._isContentVisible = false;
+
+ if (this.onhide) {
+ this.onhide(this);
+ }
+ },
+
+ /**
+ * Expands the scope, showing all the added details.
+ */
+ expand: function() {
+ if (this._isExpanded || this._locked) {
+ return;
+ }
+ // If there's a large number of enumerable or non-enumerable items
+ // contained in this scope, painting them may take several seconds,
+ // even if they were already displayed before. In this case, show a throbber
+ // to suggest that this scope is expanding.
+ if (!this._isExpanding &&
+ this._variablesView.lazyExpand &&
+ this._store.size > LAZY_APPEND_BATCH) {
+ this._isExpanding = true;
+
+ // Start spinning a throbber in this scope's title and allow a few
+ // milliseconds for it to be painted.
+ this._startThrobber();
+ this.window.setTimeout(this.expand.bind(this), LAZY_EXPAND_DELAY);
+ return;
+ }
+
+ if (this._variablesView._enumVisible) {
+ this._openEnum();
+ }
+ if (this._variablesView._nonEnumVisible) {
+ Services.tm.currentThread.dispatch({ run: this._openNonEnum }, 0);
+ }
+ this._isExpanding = false;
+ this._isExpanded = true;
+
+ if (this.onexpand) {
+ this.onexpand(this);
+ }
+ },
+
+ /**
+ * Collapses the scope, hiding all the added details.
+ */
+ collapse: function() {
+ if (!this._isExpanded || this._locked) {
+ return;
+ }
+ this._arrow.removeAttribute("open");
+ this._enum.removeAttribute("open");
+ this._nonenum.removeAttribute("open");
+ this._isExpanded = false;
+
+ if (this.oncollapse) {
+ this.oncollapse(this);
+ }
+ },
+
+ /**
+ * Toggles between the scope's collapsed and expanded state.
+ */
+ toggle: function(e) {
+ if (e && e.button != 0) {
+ // Only allow left-click to trigger this event.
+ return;
+ }
+ this._wasToggled = true;
+ this.expanded ^= 1;
+
+ // Make sure the scope and its contents are visibile.
+ for (let [, variable] of this._store) {
+ variable.header = true;
+ variable._matched = true;
+ }
+ if (this.ontoggle) {
+ this.ontoggle(this);
+ }
+ },
+
+ /**
+ * Shows the scope's title header.
+ */
+ showHeader: function() {
+ if (this._isHeaderVisible || !this._nameString) {
+ return;
+ }
+ this._target.removeAttribute("non-header");
+ this._isHeaderVisible = true;
+ },
+
+ /**
+ * Hides the scope's title header.
+ * This action will automatically expand the scope.
+ */
+ hideHeader: function() {
+ if (!this._isHeaderVisible) {
+ return;
+ }
+ this.expand();
+ this._target.setAttribute("non-header", "");
+ this._isHeaderVisible = false;
+ },
+
+ /**
+ * Shows the scope's expand/collapse arrow.
+ */
+ showArrow: function() {
+ if (this._isArrowVisible) {
+ return;
+ }
+ this._arrow.removeAttribute("invisible");
+ this._isArrowVisible = true;
+ },
+
+ /**
+ * Hides the scope's expand/collapse arrow.
+ */
+ hideArrow: function() {
+ if (!this._isArrowVisible) {
+ return;
+ }
+ this._arrow.setAttribute("invisible", "");
+ this._isArrowVisible = false;
+ },
+
+ /**
+ * Gets the visibility state.
+ * @return boolean
+ */
+ get visible() this._isContentVisible,
+
+ /**
+ * Gets the expanded state.
+ * @return boolean
+ */
+ get expanded() this._isExpanded,
+
+ /**
+ * Gets the header visibility state.
+ * @return boolean
+ */
+ get header() this._isHeaderVisible,
+
+ /**
+ * Gets the twisty visibility state.
+ * @return boolean
+ */
+ get twisty() this._isArrowVisible,
+
+ /**
+ * Gets the expand lock state.
+ * @return boolean
+ */
+ get locked() this._locked,
+
+ /**
+ * Sets the visibility state.
+ * @param boolean aFlag
+ */
+ set visible(aFlag) aFlag ? this.show() : this.hide(),
+
+ /**
+ * Sets the expanded state.
+ * @param boolean aFlag
+ */
+ set expanded(aFlag) aFlag ? this.expand() : this.collapse(),
+
+ /**
+ * Sets the header visibility state.
+ * @param boolean aFlag
+ */
+ set header(aFlag) aFlag ? this.showHeader() : this.hideHeader(),
+
+ /**
+ * Sets the twisty visibility state.
+ * @param boolean aFlag
+ */
+ set twisty(aFlag) aFlag ? this.showArrow() : this.hideArrow(),
+
+ /**
+ * Sets the expand lock state.
+ * @param boolean aFlag
+ */
+ set locked(aFlag) this._locked = aFlag,
+
+ /**
+ * Specifies if this target node may be focused.
+ * @return boolean
+ */
+ get focusable() {
+ // Check if this target node is actually visibile.
+ if (!this._nameString ||
+ !this._isContentVisible ||
+ !this._isHeaderVisible ||
+ !this._isMatch) {
+ return false;
+ }
+ // Check if all parent objects are expanded.
+ let item = this;
+
+ // Recurse while parent is a Scope, Variable, or Property
+ while ((item = item.ownerView) && item instanceof Scope) {
+ if (!item._isExpanded) {
+ return false;
+ }
+ }
+ return true;
+ },
+
+ /**
+ * Focus this scope.
+ */
+ focus: function() {
+ this._variablesView._focusItem(this);
+ },
+
+ /**
+ * Adds an event listener for a certain event on this scope's title.
+ * @param string aName
+ * @param function aCallback
+ * @param boolean aCapture
+ */
+ addEventListener: function(aName, aCallback, aCapture) {
+ this._title.addEventListener(aName, aCallback, aCapture);
+ },
+
+ /**
+ * Removes an event listener for a certain event on this scope's title.
+ * @param string aName
+ * @param function aCallback
+ * @param boolean aCapture
+ */
+ removeEventListener: function(aName, aCallback, aCapture) {
+ this._title.removeEventListener(aName, aCallback, aCapture);
+ },
+
+ /**
+ * Gets the id associated with this item.
+ * @return string
+ */
+ get id() this._idString,
+
+ /**
+ * Gets the name associated with this item.
+ * @return string
+ */
+ get name() this._nameString,
+
+ /**
+ * Gets the displayed value for this item.
+ * @return string
+ */
+ get displayValue() this._valueString,
+
+ /**
+ * Gets the class names used for the displayed value.
+ * @return string
+ */
+ get displayValueClassName() this._valueClassName,
+
+ /**
+ * Gets the element associated with this item.
+ * @return nsIDOMNode
+ */
+ get target() this._target,
+
+ /**
+ * Initializes this scope's id, view and binds event listeners.
+ *
+ * @param string aName
+ * The scope's name.
+ * @param object aFlags [optional]
+ * Additional options or flags for this scope.
+ */
+ _init: function(aName, aFlags) {
+ this._idString = generateId(this._nameString = aName);
+ this._displayScope(aName, "variables-view-scope", "devtools-toolbar");
+ this._addEventListeners();
+ this.parentNode.appendChild(this._target);
+ },
+
+ /**
+ * Creates the necessary nodes for this scope.
+ *
+ * @param string aName
+ * The scope's name.
+ * @param string aClassName
+ * A custom class name for this scope.
+ * @param string aTitleClassName [optional]
+ * A custom class name for this scope's title.
+ */
+ _displayScope: function(aName, aClassName, aTitleClassName) {
+ let document = this.document;
+
+ let element = this._target = document.createElement("vbox");
+ element.id = this._idString;
+ element.className = aClassName;
+
+ let arrow = this._arrow = document.createElement("hbox");
+ arrow.className = "arrow";
+
+ let name = this._name = document.createElement("label");
+ name.className = "plain name";
+ name.setAttribute("value", aName);
+
+ let title = this._title = document.createElement("hbox");
+ title.className = "title " + (aTitleClassName || "");
+ title.setAttribute("align", "center");
+
+ let enumerable = this._enum = document.createElement("vbox");
+ let nonenum = this._nonenum = document.createElement("vbox");
+ enumerable.className = "variables-view-element-details enum";
+ nonenum.className = "variables-view-element-details nonenum";
+
+ title.appendChild(arrow);
+ title.appendChild(name);
+
+ element.appendChild(title);
+ element.appendChild(enumerable);
+ element.appendChild(nonenum);
+ },
+
+ /**
+ * Adds the necessary event listeners for this scope.
+ */
+ _addEventListeners: function() {
+ this._title.addEventListener("mousedown", this._onClick, false);
+ },
+
+ /**
+ * The click listener for this scope's title.
+ */
+ _onClick: function(e) {
+ if (e.target == this._inputNode ||
+ e.target == this._editNode ||
+ e.target == this._deleteNode) {
+ return;
+ }
+ this.toggle();
+ this.focus();
+ },
+
+ /**
+ * Lazily appends a node to this scope's enumerable or non-enumerable
+ * container. Once a certain number of nodes have been batched, they
+ * will be appended.
+ *
+ * @param boolean aImmediateFlag
+ * Set to false if append calls should be dispatched synchronously
+ * on the current thread, to allow for a paint flush.
+ * @param boolean aEnumerableFlag
+ * Specifies if the node to append is enumerable or non-enumerable.
+ * @param nsIDOMNode aChild
+ * The child node to append.
+ */
+ _lazyAppend: function(aImmediateFlag, aEnumerableFlag, aChild) {
+ // Append immediately, don't stage items and don't allow for a paint flush.
+ if (aImmediateFlag || !this._variablesView.lazyAppend) {
+ if (aEnumerableFlag) {
+ this._enum.appendChild(aChild);
+ } else {
+ this._nonenum.appendChild(aChild);
+ }
+ return;
+ }
+
+ let window = this.window;
+ let batchItems = this._batchItems;
+
+ window.clearTimeout(this._batchTimeout);
+ batchItems.push({ enumerableFlag: aEnumerableFlag, child: aChild });
+
+ // If a certain number of nodes have been batched, append all the
+ // staged items now.
+ if (batchItems.length > LAZY_APPEND_BATCH) {
+ // Allow for a paint flush.
+ Services.tm.currentThread.dispatch({ run: this._batchAppend }, 1);
+ return;
+ }
+ // Postpone appending the staged items for later, to allow batching
+ // more nodes.
+ this._batchTimeout = window.setTimeout(this._batchAppend, LAZY_APPEND_DELAY);
+ },
+
+ /**
+ * Appends all the batched nodes to this scope's enumerable and non-enumerable
+ * containers.
+ */
+ _batchAppend: function() {
+ let document = this.document;
+ let batchItems = this._batchItems;
+
+ // Create two document fragments, one for enumerable nodes, and one
+ // for non-enumerable nodes.
+ let frags = [document.createDocumentFragment(), document.createDocumentFragment()];
+
+ for (let item of batchItems) {
+ frags[~~item.enumerableFlag].appendChild(item.child);
+ }
+ batchItems.length = 0;
+ this._enum.appendChild(frags[1]);
+ this._nonenum.appendChild(frags[0]);
+ },
+
+ /**
+ * Starts spinning a throbber in this scope's title.
+ */
+ _startThrobber: function() {
+ if (this._throbber) {
+ this._throbber.hidden = false;
+ return;
+ }
+ let throbber = this._throbber = this.document.createElement("hbox");
+ throbber.className = "variables-view-throbber";
+ this._title.appendChild(throbber);
+ },
+
+ /**
+ * Stops spinning the throbber in this scope's title.
+ */
+ _stopThrobber: function() {
+ if (!this._throbber) {
+ return;
+ }
+ this._throbber.hidden = true;
+ },
+
+ /**
+ * Opens the enumerable items container.
+ */
+ _openEnum: function() {
+ this._arrow.setAttribute("open", "");
+ this._enum.setAttribute("open", "");
+ this._stopThrobber();
+ },
+
+ /**
+ * Opens the non-enumerable items container.
+ */
+ _openNonEnum: function() {
+ this._nonenum.setAttribute("open", "");
+ this._stopThrobber();
+ },
+
+ /**
+ * Specifies if enumerable properties and variables should be displayed.
+ * @param boolean aFlag
+ */
+ set _enumVisible(aFlag) {
+ for (let [, variable] of this._store) {
+ variable._enumVisible = aFlag;
+
+ if (!this._isExpanded) {
+ continue;
+ }
+ if (aFlag) {
+ this._enum.setAttribute("open", "");
+ } else {
+ this._enum.removeAttribute("open");
+ }
+ }
+ },
+
+ /**
+ * Specifies if non-enumerable properties and variables should be displayed.
+ * @param boolean aFlag
+ */
+ set _nonEnumVisible(aFlag) {
+ for (let [, variable] of this._store) {
+ variable._nonEnumVisible = aFlag;
+
+ if (!this._isExpanded) {
+ continue;
+ }
+ if (aFlag) {
+ this._nonenum.setAttribute("open", "");
+ } else {
+ this._nonenum.removeAttribute("open");
+ }
+ }
+ },
+
+ /**
+ * Performs a case insensitive search for variables or properties matching
+ * the query, and hides non-matched items.
+ *
+ * @param string aLowerCaseQuery
+ * The lowercased name of the variable or property to search for.
+ */
+ _performSearch: function(aLowerCaseQuery) {
+ for (let [, variable] of this._store) {
+ let currentObject = variable;
+ let lowerCaseName = variable._nameString.toLowerCase();
+ let lowerCaseValue = variable._valueString.toLowerCase();
+
+ // Non-matched variables or properties require a corresponding attribute.
+ if (!lowerCaseName.contains(aLowerCaseQuery) &&
+ !lowerCaseValue.contains(aLowerCaseQuery)) {
+ variable._matched = false;
+ }
+ // Variable or property is matched.
+ else {
+ variable._matched = true;
+
+ // If the variable was ever expanded, there's a possibility it may
+ // contain some matched properties, so make sure they're visible
+ // ("expand downwards").
+
+ if (variable._wasToggled && aLowerCaseQuery) {
+ variable.expand();
+ }
+ if (variable._isExpanded && !aLowerCaseQuery) {
+ variable._wasToggled = true;
+ }
+
+ // If the variable is contained in another Scope, Variable, or Property,
+ // the parent may not be a match, thus hidden. It should be visible
+ // ("expand upwards").
+ while ((variable = variable.ownerView) && /* Parent object exists. */
+ variable instanceof Scope) {
+
+ // Show and expand the parent, as it is certainly accessible.
+ variable._matched = true;
+ aLowerCaseQuery && variable.expand();
+ }
+ }
+
+ // Proceed with the search recursively inside this variable or property.
+ if (currentObject._wasToggled ||
+ currentObject.getter ||
+ currentObject.setter) {
+ currentObject._performSearch(aLowerCaseQuery);
+ }
+ }
+ },
+
+ /**
+ * Sets if this object instance is a matched or non-matched item.
+ * @param boolean aStatus
+ */
+ set _matched(aStatus) {
+ if (this._isMatch == aStatus) {
+ return;
+ }
+ if (aStatus) {
+ this._isMatch = true;
+ this.target.removeAttribute("non-match");
+ } else {
+ this._isMatch = false;
+ this.target.setAttribute("non-match", "");
+ }
+ },
+
+ /**
+ * Gets the first search results match in this scope.
+ * @return Variable | Property
+ */
+ get _firstMatch() {
+ for (let [, variable] of this._store) {
+ let match;
+ if (variable._isMatch) {
+ match = variable;
+ } else {
+ match = variable._firstMatch;
+ }
+ if (match) {
+ return match;
+ }
+ }
+ return null;
+ },
+
+ /**
+ * Find the first item in the tree of visible items in this item that matches
+ * the predicate. Searches in visual order (the order seen by the user).
+ * Tests itself, then descends into first the enumerable children and then
+ * the non-enumerable children (since they are presented in separate groups).
+ *
+ * @param function aPredicate
+ * A function that returns true when a match is found.
+ * @return Scope | Variable | Property
+ * The first visible scope, variable or property, or null if nothing
+ * is found.
+ */
+ _findInVisibleItems: function(aPredicate) {
+ if (aPredicate(this)) {
+ return this;
+ }
+
+ if (this._isExpanded) {
+ if (this._variablesView._enumVisible) {
+ for (let item of this._enumItems) {
+ let result = item._findInVisibleItems(aPredicate);
+ if (result) {
+ return result;
+ }
+ }
+ }
+
+ if (this._variablesView._nonEnumVisible) {
+ for (let item of this._nonEnumItems) {
+ let result = item._findInVisibleItems(aPredicate);
+ if (result) {
+ return result;
+ }
+ }
+ }
+ }
+
+ return null;
+ },
+
+ /**
+ * Find the last item in the tree of visible items in this item that matches
+ * the predicate. Searches in reverse visual order (opposite of the order
+ * seen by the user). Descends into first the non-enumerable children, then
+ * the enumerable children (since they are presented in separate groups), and
+ * finally tests itself.
+ *
+ * @param function aPredicate
+ * A function that returns true when a match is found.
+ * @return Scope | Variable | Property
+ * The last visible scope, variable or property, or null if nothing
+ * is found.
+ */
+ _findInVisibleItemsReverse: function(aPredicate) {
+ if (this._isExpanded) {
+ if (this._variablesView._nonEnumVisible) {
+ for (let i = this._nonEnumItems.length - 1; i >= 0; i--) {
+ let item = this._nonEnumItems[i];
+ let result = item._findInVisibleItemsReverse(aPredicate);
+ if (result) {
+ return result;
+ }
+ }
+ }
+
+ if (this._variablesView._enumVisible) {
+ for (let i = this._enumItems.length - 1; i >= 0; i--) {
+ let item = this._enumItems[i];
+ let result = item._findInVisibleItemsReverse(aPredicate);
+ if (result) {
+ return result;
+ }
+ }
+ }
+ }
+
+ if (aPredicate(this)) {
+ return this;
+ }
+
+ return null;
+ },
+
+ /**
+ * Gets top level variables view instance.
+ * @return VariablesView
+ */
+ get _variablesView() this._topView || (this._topView = (function(self) {
+ let parentView = self.ownerView;
+ let topView;
+
+ while (topView = parentView.ownerView) {
+ parentView = topView;
+ }
+ return parentView;
+ })(this)),
+
+ /**
+ * Gets the parent node holding this scope.
+ * @return nsIDOMNode
+ */
+ get parentNode() this.ownerView._list,
+
+ /**
+ * Gets the owner document holding this scope.
+ * @return nsIHTMLDocument
+ */
+ get document() this._document || (this._document = this.ownerView.document),
+
+ /**
+ * Gets the default window holding this scope.
+ * @return nsIDOMWindow
+ */
+ get window() this._window || (this._window = this.ownerView.window),
+
+ _topView: null,
+ _document: null,
+ _window: null,
+
+ ownerView: null,
+ eval: null,
+ switch: null,
+ delete: null,
+ editableValueTooltip: "",
+ editableNameTooltip: "",
+ editButtonTooltip: "",
+ deleteButtonTooltip: "",
+ preventDescriptorModifiers: false,
+ contextMenuId: "",
+ separatorStr: "",
+
+ _store: null,
+ _enumItems: null,
+ _nonEnumItems: null,
+ _fetched: false,
+ _retrieved: false,
+ _committed: false,
+ _batchItems: null,
+ _batchTimeout: null,
+ _locked: false,
+ _isExpanding: false,
+ _isExpanded: false,
+ _wasToggled: false,
+ _isContentVisible: true,
+ _isHeaderVisible: true,
+ _isArrowVisible: true,
+ _isMatch: true,
+ _idString: "",
+ _nameString: "",
+ _target: null,
+ _arrow: null,
+ _name: null,
+ _title: null,
+ _enum: null,
+ _nonenum: null,
+ _throbber: null
+};
+
+/**
+ * A Variable is a Scope holding Property instances.
+ * Iterable via "for (let [name, property] in instance) { }".
+ *
+ * @param Scope aScope
+ * The scope to contain this variable.
+ * @param string aName
+ * The variable's name.
+ * @param object aDescriptor
+ * The variable's descriptor.
+ */
+function Variable(aScope, aName, aDescriptor) {
+ this._setTooltips = this._setTooltips.bind(this);
+ this._activateNameInput = this._activateNameInput.bind(this);
+ this._activateValueInput = this._activateValueInput.bind(this);
+
+ // Treat safe getter descriptors as descriptors with a value.
+ if ("getterValue" in aDescriptor) {
+ aDescriptor.value = aDescriptor.getterValue;
+ delete aDescriptor.get;
+ delete aDescriptor.set;
+ }
+
+ Scope.call(this, aScope, aName, this._initialDescriptor = aDescriptor);
+ this.setGrip(aDescriptor.value);
+ this._symbolicName = aName;
+ this._absoluteName = aScope.name + "[\"" + aName + "\"]";
+}
+
+Variable.prototype = Heritage.extend(Scope.prototype, {
+ /**
+ * Whether this Scope should be prefetched when it is remoted.
+ */
+ get shouldPrefetch(){
+ return this.name == "window" || this.name == "this";
+ },
+
+ /**
+ * Create a new Property that is a child of Variable.
+ *
+ * @param string aName
+ * The name of the new Property.
+ * @param object aDescriptor
+ * The property's descriptor.
+ * @return Property
+ * The newly created child Property.
+ */
+ _createChild: function(aName, aDescriptor) {
+ return new Property(this, aName, aDescriptor);
+ },
+
+ /**
+ * Populates this variable to contain all the properties of an object.
+ *
+ * @param object aObject
+ * The raw object you want to display.
+ * @param object aOptions [optional]
+ * Additional options for adding the properties. Supported options:
+ * - sorted: true to sort all the properties before adding them
+ * - expanded: true to expand all the properties after adding them
+ */
+ populate: function(aObject, aOptions = {}) {
+ // Retrieve the properties only once.
+ if (this._fetched) {
+ return;
+ }
+ this._fetched = true;
+
+ let propertyNames = Object.getOwnPropertyNames(aObject);
+ let prototype = Object.getPrototypeOf(aObject);
+
+ // Sort all of the properties before adding them, if preferred.
+ if (aOptions.sorted) {
+ propertyNames.sort();
+ }
+ // Add all the variable properties.
+ for (let name of propertyNames) {
+ let descriptor = Object.getOwnPropertyDescriptor(aObject, name);
+ if (descriptor.get || descriptor.set) {
+ let prop = this._addRawNonValueProperty(name, descriptor);
+ if (aOptions.expanded) {
+ prop.expanded = true;
+ }
+ } else {
+ let prop = this._addRawValueProperty(name, descriptor, aObject[name]);
+ if (aOptions.expanded) {
+ prop.expanded = true;
+ }
+ }
+ }
+ // Add the variable's __proto__.
+ if (prototype) {
+ this._addRawValueProperty("__proto__", {}, prototype);
+ }
+ },
+
+ /**
+ * Populates a specific variable or property instance to contain all the
+ * properties of an object
+ *
+ * @param Variable | Property aVar
+ * The target variable to populate.
+ * @param object aObject [optional]
+ * The raw object you want to display. If unspecified, the object is
+ * assumed to be defined in a _sourceValue property on the target.
+ */
+ _populateTarget: function(aVar, aObject = aVar._sourceValue) {
+ aVar.populate(aObject);
+ },
+
+ /**
+ * Adds a property for this variable based on a raw value descriptor.
+ *
+ * @param string aName
+ * The property's name.
+ * @param object aDescriptor
+ * Specifies the exact property descriptor as returned by a call to
+ * Object.getOwnPropertyDescriptor.
+ * @param object aValue
+ * The raw property value you want to display.
+ * @return Property
+ * The newly added property instance.
+ */
+ _addRawValueProperty: function(aName, aDescriptor, aValue) {
+ let descriptor = Object.create(aDescriptor);
+ descriptor.value = VariablesView.getGrip(aValue);
+
+ let propertyItem = this.addItem(aName, descriptor);
+ propertyItem._sourceValue = aValue;
+
+ // Add an 'onexpand' callback for the property, lazily handling
+ // the addition of new child properties.
+ if (!VariablesView.isPrimitive(descriptor)) {
+ propertyItem.onexpand = this._populateTarget;
+ }
+ return propertyItem;
+ },
+
+ /**
+ * Adds a property for this variable based on a getter/setter descriptor.
+ *
+ * @param string aName
+ * The property's name.
+ * @param object aDescriptor
+ * Specifies the exact property descriptor as returned by a call to
+ * Object.getOwnPropertyDescriptor.
+ * @return Property
+ * The newly added property instance.
+ */
+ _addRawNonValueProperty: function(aName, aDescriptor) {
+ let descriptor = Object.create(aDescriptor);
+ descriptor.get = VariablesView.getGrip(aDescriptor.get);
+ descriptor.set = VariablesView.getGrip(aDescriptor.set);
+
+ return this.addItem(aName, descriptor);
+ },
+
+ /**
+ * Gets this variable's path to the topmost scope.
+ * For example, a symbolic name may look like "arguments['0']['foo']['bar']".
+ * @return string
+ */
+ get symbolicName() this._symbolicName,
+
+ /**
+ * Returns this variable's value from the descriptor if available.
+ * @return any
+ */
+ get value() this._initialDescriptor.value,
+
+ /**
+ * Returns this variable's getter from the descriptor if available.
+ * @return object
+ */
+ get getter() this._initialDescriptor.get,
+
+ /**
+ * Returns this variable's getter from the descriptor if available.
+ * @return object
+ */
+ get setter() this._initialDescriptor.set,
+
+ /**
+ * Sets the specific grip for this variable (applies the text content and
+ * class name to the value label).
+ *
+ * The grip should contain the value or the type & class, as defined in the
+ * remote debugger protocol. For convenience, undefined and null are
+ * both considered types.
+ *
+ * @param any aGrip
+ * Specifies the value and/or type & class of the variable.
+ * e.g. - 42
+ * - true
+ * - "nasu"
+ * - { type: "undefined" }
+ * - { type: "null" }
+ * - { type: "object", class: "Object" }
+ */
+ setGrip: function(aGrip) {
+ // Don't allow displaying grip information if there's no name available.
+ if (!this._nameString || aGrip === undefined || aGrip === null) {
+ return;
+ }
+ // Getters and setters should display grip information in sub-properties.
+ if (!this._isUndefined && (this.getter || this.setter)) {
+ this._valueLabel.setAttribute("value", "");
+ return;
+ }
+
+ // Make sure the value is escaped unicode if it's a string.
+ if (typeof aGrip == "string") {
+ aGrip = NetworkHelper.convertToUnicode(unescape(aGrip));
+ }
+
+ let prevGrip = this._valueGrip;
+ if (prevGrip) {
+ this._valueLabel.classList.remove(VariablesView.getClass(prevGrip));
+ }
+ this._valueGrip = aGrip;
+ this._valueString = VariablesView.getString(aGrip);
+ this._valueClassName = VariablesView.getClass(aGrip);
+
+ this._valueLabel.classList.add(this._valueClassName);
+ this._valueLabel.setAttribute("value", this._valueString);
+ },
+
+ /**
+ * Initializes this variable's id, view and binds event listeners.
+ *
+ * @param string aName
+ * The variable's name.
+ * @param object aDescriptor
+ * The variable's descriptor.
+ */
+ _init: function(aName, aDescriptor) {
+ this._idString = generateId(this._nameString = aName);
+ this._displayScope(aName, "variables-view-variable variable-or-property");
+
+ // Don't allow displaying variable information there's no name available.
+ if (this._nameString) {
+ this._displayVariable();
+ this._customizeVariable();
+ this._prepareTooltips();
+ this._setAttributes();
+ this._addEventListeners();
+ }
+
+ this._onInit(this.ownerView._store.size < LAZY_APPEND_BATCH);
+ },
+
+ /**
+ * Called when this variable has finished initializing, and is ready to
+ * be attached to the owner view.
+ *
+ * @param boolean aImmediateFlag
+ * @see Scope.prototype._lazyAppend
+ */
+ _onInit: function(aImmediateFlag) {
+ if (this._initialDescriptor.enumerable ||
+ this._nameString == "this" ||
+ this._nameString == "<return>" ||
+ this._nameString == "<exception>") {
+ this.ownerView._lazyAppend(aImmediateFlag, true, this._target);
+ this.ownerView._enumItems.push(this);
+ } else {
+ this.ownerView._lazyAppend(aImmediateFlag, false, this._target);
+ this.ownerView._nonEnumItems.push(this);
+ }
+ },
+
+ /**
+ * Creates the necessary nodes for this variable.
+ */
+ _displayVariable: function() {
+ let document = this.document;
+ let descriptor = this._initialDescriptor;
+
+ let separatorLabel = this._separatorLabel = document.createElement("label");
+ separatorLabel.className = "plain separator";
+ separatorLabel.setAttribute("value", this.ownerView.separatorStr);
+
+ let valueLabel = this._valueLabel = document.createElement("label");
+ valueLabel.className = "plain value";
+ valueLabel.setAttribute("crop", "center");
+ valueLabel.setAttribute('flex', "1");
+
+ this._title.appendChild(separatorLabel);
+ this._title.appendChild(valueLabel);
+
+ let isPrimitive = this._isPrimitive = VariablesView.isPrimitive(descriptor);
+ let isUndefined = this._isUndefined = VariablesView.isUndefined(descriptor);
+
+ if (isPrimitive || isUndefined) {
+ this.hideArrow();
+ }
+ if (!isUndefined && (descriptor.get || descriptor.set)) {
+ separatorLabel.hidden = true;
+ valueLabel.hidden = true;
+
+ // Changing getter/setter names is never allowed.
+ this.switch = null;
+
+ // Getter/setter properties require special handling when it comes to
+ // evaluation and deletion.
+ if (this.ownerView.eval) {
+ this.delete = VariablesView.getterOrSetterDeleteCallback;
+ this.evaluationMacro = VariablesView.overrideValueEvalMacro;
+ }
+ // Deleting getters and setters individually is not allowed if no
+ // evaluation method is provided.
+ else {
+ this.delete = null;
+ this.evaluationMacro = null;
+ }
+
+ let getter = this.addItem("get", { value: descriptor.get });
+ let setter = this.addItem("set", { value: descriptor.set });
+ getter.evaluationMacro = VariablesView.getterOrSetterEvalMacro;
+ setter.evaluationMacro = VariablesView.getterOrSetterEvalMacro;
+
+ getter.hideArrow();
+ setter.hideArrow();
+ this.expand();
+ }
+ },
+
+ /**
+ * Adds specific nodes for this variable based on custom flags.
+ */
+ _customizeVariable: function() {
+ let ownerView = this.ownerView;
+ let descriptor = this._initialDescriptor;
+
+ if (ownerView.eval) {
+ if (!this._isUndefined && (this.getter || this.setter)) {
+ let editNode = this._editNode = this.document.createElement("toolbarbutton");
+ editNode.className = "plain variables-view-edit";
+ editNode.addEventListener("mousedown", this._onEdit.bind(this), false);
+ this._title.appendChild(editNode);
+ }
+ }
+ if (ownerView.delete) {
+ if (!this._isUndefined || !(ownerView.getter && ownerView.setter)) {
+ let deleteNode = this._deleteNode = this.document.createElement("toolbarbutton");
+ deleteNode.className = "plain variables-view-delete";
+ deleteNode.setAttribute("ordinal", 2);
+ deleteNode.addEventListener("click", this._onDelete.bind(this), false);
+ this._title.appendChild(deleteNode);
+ }
+ }
+ if (ownerView.contextMenuId) {
+ this._title.setAttribute("context", ownerView.contextMenuId);
+ }
+
+ if (ownerView.preventDescriptorModifiers) {
+ return;
+ }
+
+ if (!descriptor.writable && !ownerView.getter && !ownerView.setter) {
+ let nonWritableIcon = this.document.createElement("hbox");
+ nonWritableIcon.className = "variable-or-property-non-writable-icon";
+ this._title.appendChild(nonWritableIcon);
+ }
+ if (descriptor.value && typeof descriptor.value == "object") {
+ if (descriptor.value.frozen) {
+ let frozenLabel = this.document.createElement("label");
+ frozenLabel.className = "plain variable-or-property-frozen-label";
+ frozenLabel.setAttribute("value", "F");
+ this._title.appendChild(frozenLabel);
+ }
+ if (descriptor.value.sealed) {
+ let sealedLabel = this.document.createElement("label");
+ sealedLabel.className = "plain variable-or-property-sealed-label";
+ sealedLabel.setAttribute("value", "S");
+ this._title.appendChild(sealedLabel);
+ }
+ if (!descriptor.value.extensible) {
+ let nonExtensibleLabel = this.document.createElement("label");
+ nonExtensibleLabel.className = "plain variable-or-property-non-extensible-label";
+ nonExtensibleLabel.setAttribute("value", "N");
+ this._title.appendChild(nonExtensibleLabel);
+ }
+ }
+ },
+
+ /**
+ * Prepares all tooltips for this variable.
+ */
+ _prepareTooltips: function() {
+ this._target.addEventListener("mouseover", this._setTooltips, false);
+ },
+
+ /**
+ * Sets all tooltips for this variable.
+ */
+ _setTooltips: function() {
+ this._target.removeEventListener("mouseover", this._setTooltips, false);
+
+ let ownerView = this.ownerView;
+ if (ownerView.preventDescriptorModifiers) {
+ return;
+ }
+
+ let tooltip = this.document.createElement("tooltip");
+ tooltip.id = "tooltip-" + this._idString;
+ tooltip.setAttribute("orient", "horizontal");
+
+ let labels = [
+ "configurable", "enumerable", "writable",
+ "frozen", "sealed", "extensible", "WebIDL"];
+
+ for (let label of labels) {
+ let labelElement = this.document.createElement("label");
+ labelElement.setAttribute("value", label);
+ tooltip.appendChild(labelElement);
+ }
+
+ this._target.appendChild(tooltip);
+ this._target.setAttribute("tooltip", tooltip.id);
+
+ if (this._editNode && ownerView.eval) {
+ this._editNode.setAttribute("tooltiptext", ownerView.editButtonTooltip);
+ }
+ if (this._valueLabel && ownerView.eval) {
+ this._valueLabel.setAttribute("tooltiptext", ownerView.editableValueTooltip);
+ }
+ if (this._name && ownerView.switch) {
+ this._name.setAttribute("tooltiptext", ownerView.editableNameTooltip);
+ }
+ if (this._deleteNode && ownerView.delete) {
+ this._deleteNode.setAttribute("tooltiptext", ownerView.deleteButtonTooltip);
+ }
+ },
+
+ /**
+ * Sets a variable's configurable, enumerable and writable attributes,
+ * and specifies if it's a 'this', '<exception>' or '__proto__' reference.
+ */
+ _setAttributes: function() {
+ let ownerView = this.ownerView;
+ if (ownerView.preventDescriptorModifiers) {
+ return;
+ }
+
+ let descriptor = this._initialDescriptor;
+ let target = this._target;
+ let name = this._nameString;
+
+ if (ownerView.eval) {
+ target.setAttribute("editable", "");
+ }
+
+ if (!descriptor.configurable) {
+ target.setAttribute("non-configurable", "");
+ }
+ if (!descriptor.enumerable) {
+ target.setAttribute("non-enumerable", "");
+ }
+ if (!descriptor.writable && !ownerView.getter && !ownerView.setter) {
+ target.setAttribute("non-writable", "");
+ }
+
+ if (descriptor.value && typeof descriptor.value == "object") {
+ if (descriptor.value.frozen) {
+ target.setAttribute("frozen", "");
+ }
+ if (descriptor.value.sealed) {
+ target.setAttribute("sealed", "");
+ }
+ if (!descriptor.value.extensible) {
+ target.setAttribute("non-extensible", "");
+ }
+ }
+
+ if (descriptor && "getterValue" in descriptor) {
+ target.setAttribute("safe-getter", "");
+ }
+ if (name == "this") {
+ target.setAttribute("self", "");
+ }
+
+ else if (name == "<exception>") {
+ target.setAttribute("exception", "");
+ }
+ else if (name == "<return>") {
+ target.setAttribute("return", "");
+ }
+ else if (name == "__proto__") {
+ target.setAttribute("proto", "");
+ }
+ },
+
+ /**
+ * Adds the necessary event listeners for this variable.
+ */
+ _addEventListeners: function() {
+ this._name.addEventListener("dblclick", this._activateNameInput, false);
+ this._valueLabel.addEventListener("mousedown", this._activateValueInput, false);
+ this._title.addEventListener("mousedown", this._onClick, false);
+ },
+
+ /**
+ * Creates a textbox node in place of a label.
+ *
+ * @param nsIDOMNode aLabel
+ * The label to be replaced with a textbox.
+ * @param string aClassName
+ * The class to be applied to the textbox.
+ * @param object aCallbacks
+ * An object containing the onKeypress and onBlur callbacks.
+ */
+ _activateInput: function(aLabel, aClassName, aCallbacks) {
+ let initialString = aLabel.getAttribute("value");
+
+ // Create a texbox input element which will be shown in the current
+ // element's specified label location.
+ let input = this.document.createElement("textbox");
+ input.className = "plain " + aClassName;
+ input.setAttribute("value", initialString);
+ input.setAttribute("flex", "1");
+
+ // Replace the specified label with a textbox input element.
+ aLabel.parentNode.replaceChild(input, aLabel);
+ this._variablesView._boxObject.ensureElementIsVisible(input);
+ input.select();
+
+ // When the value is a string (displayed as "value"), then we probably want
+ // to change it to another string in the textbox, so to avoid typing the ""
+ // again, tackle with the selection bounds just a bit.
+ if (aLabel.getAttribute("value").match(/^".+"$/)) {
+ input.selectionEnd--;
+ input.selectionStart++;
+ }
+
+ input.addEventListener("keypress", aCallbacks.onKeypress, false);
+ input.addEventListener("blur", aCallbacks.onBlur, false);
+
+ this._prevExpandable = this.twisty;
+ this._prevExpanded = this.expanded;
+ this.collapse();
+ this.hideArrow();
+ this._locked = true;
+
+ this._inputNode = input;
+ this._stopThrobber();
+ },
+
+ /**
+ * Removes the textbox node in place of a label.
+ *
+ * @param nsIDOMNode aLabel
+ * The label which was replaced with a textbox.
+ * @param object aCallbacks
+ * An object containing the onKeypress and onBlur callbacks.
+ */
+ _deactivateInput: function(aLabel, aInput, aCallbacks) {
+ aInput.parentNode.replaceChild(aLabel, aInput);
+ this._variablesView._boxObject.scrollBy(-this._target.clientWidth, 0);
+
+ aInput.removeEventListener("keypress", aCallbacks.onKeypress, false);
+ aInput.removeEventListener("blur", aCallbacks.onBlur, false);
+
+ this._locked = false;
+ this.twisty = this._prevExpandable;
+ this.expanded = this._prevExpanded;
+
+ this._inputNode = null;
+ this._stopThrobber();
+ },
+
+ /**
+ * Makes this variable's name editable.
+ */
+ _activateNameInput: function(e) {
+ if (e && e.button != 0) {
+ // Only allow left-click to trigger this event.
+ return;
+ }
+ if (!this.ownerView.switch) {
+ return;
+ }
+ if (e) {
+ e.preventDefault();
+ e.stopPropagation();
+ }
+
+ this._onNameInputKeyPress = this._onNameInputKeyPress.bind(this);
+ this._deactivateNameInput = this._deactivateNameInput.bind(this);
+
+ this._activateInput(this._name, "element-name-input", {
+ onKeypress: this._onNameInputKeyPress,
+ onBlur: this._deactivateNameInput
+ });
+ this._separatorLabel.hidden = true;
+ this._valueLabel.hidden = true;
+ },
+
+ /**
+ * Deactivates this variable's editable name mode.
+ */
+ _deactivateNameInput: function(e) {
+ this._deactivateInput(this._name, e.target, {
+ onKeypress: this._onNameInputKeyPress,
+ onBlur: this._deactivateNameInput
+ });
+ this._separatorLabel.hidden = false;
+ this._valueLabel.hidden = false;
+ },
+
+ /**
+ * Makes this variable's value editable.
+ */
+ _activateValueInput: function(e) {
+ if (e && e.button != 0) {
+ // Only allow left-click to trigger this event.
+ return;
+ }
+ if (!this.ownerView.eval) {
+ return;
+ }
+ if (e) {
+ e.preventDefault();
+ e.stopPropagation();
+ }
+
+ this._onValueInputKeyPress = this._onValueInputKeyPress.bind(this);
+ this._deactivateValueInput = this._deactivateValueInput.bind(this);
+
+ this._activateInput(this._valueLabel, "element-value-input", {
+ onKeypress: this._onValueInputKeyPress,
+ onBlur: this._deactivateValueInput
+ });
+ },
+
+ /**
+ * Deactivates this variable's editable value mode.
+ */
+ _deactivateValueInput: function(e) {
+ this._deactivateInput(this._valueLabel, e.target, {
+ onKeypress: this._onValueInputKeyPress,
+ onBlur: this._deactivateValueInput
+ });
+ },
+
+ /**
+ * Disables this variable prior to a new name switch or value evaluation.
+ */
+ _disable: function() {
+ this.hideArrow();
+ this._separatorLabel.hidden = true;
+ this._valueLabel.hidden = true;
+ this._enum.hidden = true;
+ this._nonenum.hidden = true;
+
+ if (this._editNode) {
+ this._editNode.hidden = true;
+ }
+ if (this._deleteNode) {
+ this._deleteNode.hidden = true;
+ }
+ },
+
+ /**
+ * Deactivates this variable's editable mode and callbacks the new name.
+ */
+ _saveNameInput: function(e) {
+ let input = e.target;
+ let initialString = this._name.getAttribute("value");
+ let currentString = input.value.trim();
+ this._deactivateNameInput(e);
+
+ if (initialString != currentString) {
+ if (!this._variablesView.preventDisableOnChage) {
+ this._disable();
+ this._name.value = currentString;
+ }
+ this.ownerView.switch(this, currentString);
+ }
+ },
+
+ /**
+ * Deactivates this variable's editable mode and evaluates the new value.
+ */
+ _saveValueInput: function(e) {
+ let input = e.target;
+ let initialString = this._valueLabel.getAttribute("value");
+ let currentString = input.value.trim();
+ this._deactivateValueInput(e);
+
+ if (initialString != currentString) {
+ if (!this._variablesView.preventDisableOnChage) {
+ this._disable();
+ }
+ this.ownerView.eval(this.evaluationMacro(this, currentString.trim()));
+ }
+ },
+
+ /**
+ * The current macro used to generate the string evaluated when performing
+ * a variable or property value change.
+ */
+ evaluationMacro: VariablesView.simpleValueEvalMacro,
+
+ /**
+ * The key press listener for this variable's editable name textbox.
+ */
+ _onNameInputKeyPress: function(e) {
+ e.stopPropagation();
+
+ switch(e.keyCode) {
+ case e.DOM_VK_RETURN:
+ case e.DOM_VK_ENTER:
+ this._saveNameInput(e);
+ this.focus();
+ return;
+ case e.DOM_VK_ESCAPE:
+ this._deactivateNameInput(e);
+ this.focus();
+ return;
+ }
+ },
+
+ /**
+ * The key press listener for this variable's editable value textbox.
+ */
+ _onValueInputKeyPress: function(e) {
+ e.stopPropagation();
+
+ switch(e.keyCode) {
+ case e.DOM_VK_RETURN:
+ case e.DOM_VK_ENTER:
+ this._saveValueInput(e);
+ this.focus();
+ return;
+ case e.DOM_VK_ESCAPE:
+ this._deactivateValueInput(e);
+ this.focus();
+ return;
+ }
+ },
+
+ /**
+ * The click listener for the edit button.
+ */
+ _onEdit: function(e) {
+ e.preventDefault();
+ e.stopPropagation();
+ this._activateValueInput();
+ },
+
+ /**
+ * The click listener for the delete button.
+ */
+ _onDelete: function(e) {
+ e.preventDefault();
+ e.stopPropagation();
+
+ if (this.ownerView.delete) {
+ if (!this.ownerView.delete(this)) {
+ this.hide();
+ }
+ }
+ },
+
+ _symbolicName: "",
+ _absoluteName: "",
+ _initialDescriptor: null,
+ _isPrimitive: false,
+ _isUndefined: false,
+ _separatorLabel: null,
+ _valueLabel: null,
+ _inputNode: null,
+ _editNode: null,
+ _deleteNode: null,
+ _tooltip: null,
+ _valueGrip: null,
+ _valueString: "",
+ _valueClassName: "",
+ _prevExpandable: false,
+ _prevExpanded: false
+});
+
+/**
+ * A Property is a Variable holding additional child Property instances.
+ * Iterable via "for (let [name, property] in instance) { }".
+ *
+ * @param Variable aVar
+ * The variable to contain this property.
+ * @param string aName
+ * The property's name.
+ * @param object aDescriptor
+ * The property's descriptor.
+ */
+function Property(aVar, aName, aDescriptor) {
+ Variable.call(this, aVar, aName, aDescriptor);
+ this._symbolicName = aVar._symbolicName + "[\"" + aName + "\"]";
+ this._absoluteName = aVar._absoluteName + "[\"" + aName + "\"]";
+}
+
+Property.prototype = Heritage.extend(Variable.prototype, {
+ /**
+ * Initializes this property's id, view and binds event listeners.
+ *
+ * @param string aName
+ * The property's name.
+ * @param object aDescriptor
+ * The property's descriptor.
+ */
+ _init: function(aName, aDescriptor) {
+ this._idString = generateId(this._nameString = aName);
+ this._displayScope(aName, "variables-view-property variable-or-property");
+
+ // Don't allow displaying property information there's no name available.
+ if (this._nameString) {
+ this._displayVariable();
+ this._customizeVariable();
+ this._prepareTooltips();
+ this._setAttributes();
+ this._addEventListeners();
+ }
+
+ this._onInit(this.ownerView._store.size < LAZY_APPEND_BATCH);
+ },
+
+ /**
+ * Called when this property has finished initializing, and is ready to
+ * be attached to the owner view.
+ *
+ * @param boolean aImmediateFlag
+ * @see Scope.prototype._lazyAppend
+ */
+ _onInit: function(aImmediateFlag) {
+ if (this._initialDescriptor.enumerable) {
+ this.ownerView._lazyAppend(aImmediateFlag, true, this._target);
+ this.ownerView._enumItems.push(this);
+ } else {
+ this.ownerView._lazyAppend(aImmediateFlag, false, this._target);
+ this.ownerView._nonEnumItems.push(this);
+ }
+ }
+});
+
+/**
+ * A generator-iterator over the VariablesView, Scopes, Variables and Properties.
+ */
+VariablesView.prototype.__iterator__ =
+Scope.prototype.__iterator__ =
+Variable.prototype.__iterator__ =
+Property.prototype.__iterator__ = function() {
+ for (let item of this._store) {
+ yield item;
+ }
+};
+
+/**
+ * Forget everything recorded about added scopes, variables or properties.
+ * @see VariablesView.createHierarchy
+ */
+VariablesView.prototype.clearHierarchy = function() {
+ this._prevHierarchy.clear();
+ this._currHierarchy.clear();
+};
+
+/**
+ * Start recording a hierarchy of any added scopes, variables or properties.
+ * @see VariablesView.commitHierarchy
+ */
+VariablesView.prototype.createHierarchy = function() {
+ this._prevHierarchy = this._currHierarchy;
+ this._currHierarchy = new Map(); // Don't clear, this is just simple swapping.
+};
+
+/**
+ * Briefly flash the variables that changed between the previous and current
+ * scope/variable/property hierarchies and reopen previously expanded nodes.
+ */
+VariablesView.prototype.commitHierarchy = function() {
+ let prevHierarchy = this._prevHierarchy;
+ let currHierarchy = this._currHierarchy;
+
+ for (let [absoluteName, currVariable] of currHierarchy) {
+ // Ignore variables which were already commmitted.
+ if (currVariable._committed) {
+ continue;
+ }
+ // Avoid performing expensive operations.
+ if (this.commitHierarchyIgnoredItems[currVariable._nameString]) {
+ continue;
+ }
+
+ // Try to get the previous instance of the inspected variable to
+ // determine the difference in state.
+ let prevVariable = prevHierarchy.get(absoluteName);
+ let expanded = false;
+ let changed = false;
+
+ // If the inspected variable existed in a previous hierarchy, check if
+ // the displayed value (a representation of the grip) has changed and if
+ // it was previously expanded.
+ if (prevVariable) {
+ expanded = prevVariable._isExpanded;
+
+ // Only analyze Variables and Properties for displayed value changes.
+ if (currVariable instanceof Variable) {
+ changed = prevVariable._valueString != currVariable._valueString;
+ }
+ }
+
+ // Make sure this variable is not handled in ulteror commits for the
+ // same hierarchy.
+ currVariable._committed = true;
+
+ // Re-expand the variable if not previously collapsed.
+ if (expanded) {
+ currVariable._wasToggled = prevVariable._wasToggled;
+ currVariable.expand();
+ }
+ // This variable was either not changed or removed, no need to continue.
+ if (!changed) {
+ continue;
+ }
+
+ // Apply an attribute determining the flash type and duration.
+ // Dispatch this action after all the nodes have been drawn, so that
+ // the transition efects can take place.
+ this.window.setTimeout(function(aTarget) {
+ aTarget.addEventListener("transitionend", function onEvent() {
+ aTarget.removeEventListener("transitionend", onEvent, false);
+ aTarget.removeAttribute("changed");
+ }, false);
+ aTarget.setAttribute("changed", "");
+ }.bind(this, currVariable.target), this.lazyEmptyDelay + 1);
+ }
+};
+
+// Some variables are likely to contain a very large number of properties.
+// It would be a bad idea to re-expand them or perform expensive operations.
+VariablesView.prototype.commitHierarchyIgnoredItems = Object.create(null, {
+ "window": { value: true }
+});
+
+/**
+ * Returns true if the descriptor represents an undefined, null or
+ * primitive value.
+ *
+ * @param object aDescriptor
+ * The variable's descriptor.
+ */
+VariablesView.isPrimitive = function(aDescriptor) {
+ // For accessor property descriptors, the getter and setter need to be
+ // contained in 'get' and 'set' properties.
+ let getter = aDescriptor.get;
+ let setter = aDescriptor.set;
+ if (getter || setter) {
+ return false;
+ }
+
+ // As described in the remote debugger protocol, the value grip
+ // must be contained in a 'value' property.
+ let grip = aDescriptor.value;
+ if (typeof grip != "object") {
+ return true;
+ }
+
+ // For convenience, undefined, null and long strings are considered types.
+ let type = grip.type;
+ if (type == "undefined" || type == "null" || type == "longString") {
+ return true;
+ }
+
+ return false;
+};
+
+/**
+ * Returns true if the descriptor represents an undefined value.
+ *
+ * @param object aDescriptor
+ * The variable's descriptor.
+ */
+VariablesView.isUndefined = function(aDescriptor) {
+ // For accessor property descriptors, the getter and setter need to be
+ // contained in 'get' and 'set' properties.
+ let getter = aDescriptor.get;
+ let setter = aDescriptor.set;
+ if (typeof getter == "object" && getter.type == "undefined" &&
+ typeof setter == "object" && setter.type == "undefined") {
+ return true;
+ }
+
+ // As described in the remote debugger protocol, the value grip
+ // must be contained in a 'value' property.
+ let grip = aDescriptor.value;
+ if (typeof grip == "object" && grip.type == "undefined") {
+ return true;
+ }
+
+ return false;
+};
+
+/**
+ * Returns true if the descriptor represents a falsy value.
+ *
+ * @param object aDescriptor
+ * The variable's descriptor.
+ */
+VariablesView.isFalsy = function(aDescriptor) {
+ // As described in the remote debugger protocol, the value grip
+ // must be contained in a 'value' property.
+ let grip = aDescriptor.value;
+ if (typeof grip != "object") {
+ return !grip;
+ }
+
+ // For convenience, undefined and null are both considered types.
+ let type = grip.type;
+ if (type == "undefined" || type == "null") {
+ return true;
+ }
+
+ return false;
+};
+
+/**
+ * Returns true if the value is an instance of Variable or Property.
+ *
+ * @param any aValue
+ * The value to test.
+ */
+VariablesView.isVariable = function(aValue) {
+ return aValue instanceof Variable;
+};
+
+/**
+ * Returns a standard grip for a value.
+ *
+ * @param any aValue
+ * The raw value to get a grip for.
+ * @return any
+ * The value's grip.
+ */
+VariablesView.getGrip = function(aValue) {
+ if (aValue === undefined) {
+ return { type: "undefined" };
+ }
+ if (aValue === null) {
+ return { type: "null" };
+ }
+ if (typeof aValue == "object" || typeof aValue == "function") {
+ return { type: "object", class: WebConsoleUtils.getObjectClassName(aValue) };
+ }
+ return aValue;
+};
+
+/**
+ * Returns a custom formatted property string for a grip.
+ *
+ * @param any aGrip
+ * @see Variable.setGrip
+ * @param boolean aConciseFlag
+ * Return a concisely formatted property string.
+ * @return string
+ * The formatted property string.
+ */
+VariablesView.getString = function(aGrip, aConciseFlag) {
+ if (aGrip && typeof aGrip == "object") {
+ switch (aGrip.type) {
+ case "undefined":
+ return "undefined";
+ case "null":
+ return "null";
+ case "longString":
+ return "\"" + aGrip.initial + "\"";
+ default:
+ if (!aConciseFlag) {
+ return "[" + aGrip.type + " " + aGrip.class + "]";
+ } else {
+ return aGrip.class;
+ }
+ }
+ } else {
+ switch (typeof aGrip) {
+ case "string":
+ return "\"" + aGrip + "\"";
+ case "boolean":
+ return aGrip ? "true" : "false";
+ }
+ }
+ return aGrip + "";
+};
+
+/**
+ * Returns a custom class style for a grip.
+ *
+ * @param any aGrip
+ * @see Variable.setGrip
+ * @return string
+ * The custom class style.
+ */
+VariablesView.getClass = function(aGrip) {
+ if (aGrip && typeof aGrip == "object") {
+ switch (aGrip.type) {
+ case "undefined":
+ return "token-undefined";
+ case "null":
+ return "token-null";
+ case "longString":
+ return "token-string";
+ }
+ } else {
+ switch (typeof aGrip) {
+ case "string":
+ return "token-string";
+ case "boolean":
+ return "token-boolean";
+ case "number":
+ return "token-number";
+ }
+ }
+ return "token-other";
+};
+
+/**
+ * A monotonically-increasing counter, that guarantees the uniqueness of scope,
+ * variables and properties ids.
+ *
+ * @param string aName
+ * An optional string to prefix the id with.
+ * @return number
+ * A unique id.
+ */
+let generateId = (function() {
+ let count = 0;
+ return function(aName = "") {
+ return aName.toLowerCase().trim().replace(/\s+/g, "-") + (++count);
+ };
+})();
diff --git a/browser/devtools/shared/widgets/VariablesView.xul b/browser/devtools/shared/widgets/VariablesView.xul
new file mode 100644
index 000000000..2269e65c9
--- /dev/null
+++ b/browser/devtools/shared/widgets/VariablesView.xul
@@ -0,0 +1,16 @@
+<?xml version="1.0"?>
+<!-- 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/. -->
+<?xml-stylesheet href="chrome://global/skin/global.css"?>
+<?xml-stylesheet href="chrome://browser/content/devtools/widgets.css" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/skin/devtools/common.css" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/skin/devtools/widgets.css" type="text/css"?>
+<!DOCTYPE window [
+ <!ENTITY % viewDTD SYSTEM "chrome://browser/locale/devtools/VariablesView.dtd">
+ %viewDTD;
+]>
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="&PropertiesViewWindowTitle;">
+ <vbox id="variables" flex="1"/>
+</window>
diff --git a/browser/devtools/shared/widgets/VariablesViewController.jsm b/browser/devtools/shared/widgets/VariablesViewController.jsm
new file mode 100644
index 000000000..56704b2d2
--- /dev/null
+++ b/browser/devtools/shared/widgets/VariablesViewController.jsm
@@ -0,0 +1,350 @@
+/* -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* 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 { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js");
+Cu.import("resource:///modules/devtools/VariablesView.jsm");
+Cu.import("resource:///modules/devtools/ViewHelpers.jsm");
+Cu.import("resource://gre/modules/devtools/WebConsoleUtils.jsm");
+
+XPCOMUtils.defineLazyGetter(this, "VARIABLES_SORTING_ENABLED", () =>
+ Services.prefs.getBoolPref("devtools.debugger.ui.variables-sorting-enabled")
+);
+
+const MAX_LONG_STRING_LENGTH = 200000;
+
+this.EXPORTED_SYMBOLS = ["VariablesViewController"];
+
+
+/**
+ * Controller for a VariablesView that handles interfacing with the debugger
+ * protocol. Is able to populate scopes and variables via the protocol as well
+ * as manage actor lifespans.
+ *
+ * @param VariablesView aView
+ * The view to attach to.
+ * @param object aOptions
+ * Options for configuring the controller. Supported options:
+ * - getGripClient: callback for creating an object grip client
+ * - getLongStringClient: callback for creating a long string grip client
+ * - releaseActor: callback for releasing an actor when it's no longer needed
+ * - overrideValueEvalMacro: callback for creating an overriding eval macro
+ * - getterOrSetterEvalMacro: callback for creating a getter/setter eval macro
+ * - simpleValueEvalMacro: callback for creating a simple value eval macro
+ */
+function VariablesViewController(aView, aOptions) {
+ this.addExpander = this.addExpander.bind(this);
+
+ this._getGripClient = aOptions.getGripClient;
+ this._getLongStringClient = aOptions.getLongStringClient;
+ this._releaseActor = aOptions.releaseActor;
+
+ if (aOptions.overrideValueEvalMacro) {
+ this._overrideValueEvalMacro = aOptions.overrideValueEvalMacro;
+ }
+ if (aOptions.getterOrSetterEvalMacro) {
+ this._getterOrSetterEvalMacro = aOptions.getterOrSetterEvalMacro;
+ }
+ if (aOptions.simpleValueEvalMacro) {
+ this._simpleValueEvalMacro = aOptions.simpleValueEvalMacro;
+ }
+
+ this._actors = new Set();
+ this.view = aView;
+ this.view.controller = this;
+}
+
+VariablesViewController.prototype = {
+ /**
+ * The default getter/setter evaluation macro.
+ */
+ _getterOrSetterEvalMacro: VariablesView.getterOrSetterEvalMacro,
+
+ /**
+ * The default override value evaluation macro.
+ */
+ _overrideValueEvalMacro: VariablesView.overrideValueEvalMacro,
+
+ /**
+ * The default simple value evaluation macro.
+ */
+ _simpleValueEvalMacro: VariablesView.simpleValueEvalMacro,
+
+ /**
+ * Populate a long string into a target using a grip.
+ *
+ * @param Variable aTarget
+ * The target Variable/Property to put the retrieved string into.
+ * @param LongStringActor aGrip
+ * The long string grip that use to retrieve the full string.
+ * @return Promise
+ * The promise that will be resolved when the string is retrieved.
+ */
+ _populateFromLongString: function(aTarget, aGrip){
+ let deferred = Promise.defer();
+
+ let from = aGrip.initial.length;
+ let to = Math.min(aGrip.length, MAX_LONG_STRING_LENGTH);
+
+ this._getLongStringClient(aGrip).substring(from, to, aResponse => {
+ // Stop tracking the actor because it's no longer needed.
+ this.releaseActor(aGrip);
+
+ // Replace the preview with the full string and make it non-expandable.
+ aTarget.onexpand = null;
+ aTarget.setGrip(aGrip.initial + aResponse.substring);
+ aTarget.hideArrow();
+
+ // Mark the string as having retrieved.
+ aTarget._retrieved = true;
+ deferred.resolve();
+ });
+
+ return deferred.promise;
+ },
+
+ /**
+ * Adds properties to a Scope, Variable, or Property in the view. Triggered
+ * when a scope is expanded or certain variables are hovered.
+ *
+ * @param Scope aTarget
+ * The Scope where the properties will be placed into.
+ * @param object aGrip
+ * The grip to use to populate the target.
+ */
+ _populateFromObject: function(aTarget, aGrip) {
+ let deferred = Promise.defer();
+
+ this._getGripClient(aGrip).getPrototypeAndProperties(aResponse => {
+ let { ownProperties, prototype, safeGetterValues } = aResponse;
+ let sortable = VariablesView.isSortable(aGrip.class);
+
+ // Merge the safe getter values into one object such that we can use it
+ // in VariablesView.
+ for (let name of Object.keys(safeGetterValues)) {
+ if (name in ownProperties) {
+ ownProperties[name].getterValue = safeGetterValues[name].getterValue;
+ ownProperties[name].getterPrototypeLevel = safeGetterValues[name]
+ .getterPrototypeLevel;
+ } else {
+ ownProperties[name] = safeGetterValues[name];
+ }
+ }
+
+ // Add all the variable properties.
+ if (ownProperties) {
+ aTarget.addItems(ownProperties, {
+ // Not all variables need to force sorted properties.
+ sorted: sortable,
+ // Expansion handlers must be set after the properties are added.
+ callback: this.addExpander
+ });
+ }
+
+ // Add the variable's __proto__.
+ if (prototype && prototype.type != "null") {
+ let proto = aTarget.addItem("__proto__", { value: prototype });
+ // Expansion handlers must be set after the properties are added.
+ this.addExpander(proto, prototype);
+ }
+
+ // Mark the variable as having retrieved all its properties.
+ aTarget._retrieved = true;
+ this.view.commitHierarchy();
+ deferred.resolve();
+ });
+
+ return deferred.promise;
+ },
+
+ /**
+ * Adds an 'onexpand' callback for a variable, lazily handling
+ * the addition of new properties.
+ *
+ * @param Variable aVar
+ * The variable where the properties will be placed into.
+ * @param any aSource
+ * The source to use to populate the target.
+ */
+ addExpander: function(aTarget, aSource) {
+ // Attach evaluation macros as necessary.
+ if (aTarget.getter || aTarget.setter) {
+ aTarget.evaluationMacro = this._overrideValueEvalMacro;
+
+ let getter = aTarget.get("get");
+ if (getter) {
+ getter.evaluationMacro = this._getterOrSetterEvalMacro;
+ }
+
+ let setter = aTarget.get("set");
+ if (setter) {
+ setter.evaluationMacro = this._getterOrSetterEvalMacro;
+ }
+ } else {
+ aTarget.evaluationMacro = this._simpleValueEvalMacro;
+ }
+
+ // If the source is primitive then an expander is not needed.
+ if (VariablesView.isPrimitive({ value: aSource })) {
+ return;
+ }
+
+ // If the source is a long string then show the arrow.
+ if (WebConsoleUtils.isActorGrip(aSource) && aSource.type == "longString") {
+ aTarget.showArrow();
+ }
+
+ // Make sure that properties are always available on expansion.
+ aTarget.onexpand = () => this.expand(aTarget, aSource);
+
+ // Some variables are likely to contain a very large number of properties.
+ // It's a good idea to be prepared in case of an expansion.
+ if (aTarget.shouldPrefetch) {
+ aTarget.addEventListener("mouseover", aTarget.onexpand, false);
+ }
+
+ // Register all the actors that this controller now depends on.
+ for (let grip of [aTarget.value, aTarget.getter, aTarget.setter]) {
+ if (WebConsoleUtils.isActorGrip(grip)) {
+ this._actors.add(grip.actor);
+ }
+ }
+ },
+
+ /**
+ * Adds properties to a Scope, Variable, or Property in the view. Triggered
+ * when a scope is expanded or certain variables are hovered.
+ *
+ * @param Scope aTarget
+ * The Scope to be expanded.
+ * @param object aSource
+ * The source to use to populate the target.
+ * @return Promise
+ * The promise that is resolved once the target has been expanded.
+ */
+ expand: function(aTarget, aSource) {
+ // Fetch the variables only once.
+ if (aTarget._fetched) {
+ return aTarget._fetched;
+ }
+
+ let deferred = Promise.defer();
+ aTarget._fetched = deferred.promise;
+
+ if (!aSource) {
+ throw new Error("No actor grip was given for the variable.");
+ }
+
+ // If the target a Variable or Property then we're fetching properties
+ if (VariablesView.isVariable(aTarget)) {
+ this._populateFromObject(aTarget, aSource).then(() => {
+ deferred.resolve();
+ // Signal that properties have been fetched.
+ this.view.emit("fetched", "properties", aTarget);
+ });
+ return deferred.promise;
+ }
+
+ switch (aSource.type) {
+ case "longString":
+ this._populateFromLongString(aTarget, aSource).then(() => {
+ deferred.resolve();
+ // Signal that a long string has been fetched.
+ this.view.emit("fetched", "longString", aTarget);
+ });
+ break;
+ case "with":
+ case "object":
+ this._populateFromObject(aTarget, aSource.object).then(() => {
+ deferred.resolve();
+ // Signal that variables have been fetched.
+ this.view.emit("fetched", "variables", aTarget);
+ });
+ break;
+ case "block":
+ case "function":
+ // Add nodes for every argument and every other variable in scope.
+ let args = aSource.bindings.arguments;
+ if (args) {
+ for (let arg of args) {
+ let name = Object.getOwnPropertyNames(arg)[0];
+ let ref = aTarget.addItem(name, arg[name]);
+ let val = arg[name].value;
+ this.addExpander(ref, val);
+ }
+ }
+
+ aTarget.addItems(aSource.bindings.variables, {
+ // Not all variables need to force sorted properties.
+ sorted: VARIABLES_SORTING_ENABLED,
+ // Expansion handlers must be set after the properties are added.
+ callback: this.addExpander
+ });
+
+ // No need to signal that variables have been fetched, since
+ // the scope arguments and variables are already attached to the
+ // environment bindings, so pausing the active thread is unnecessary.
+
+ deferred.resolve();
+ break;
+ default:
+ let error = "Unknown Debugger.Environment type: " + aSource.type;
+ Cu.reportError(error);
+ deferred.reject(error);
+ }
+
+ return deferred.promise;
+ },
+
+ /**
+ * Release an actor from the controller.
+ *
+ * @param object aActor
+ * The actor to release.
+ */
+ releaseActor: function(aActor){
+ if (this._releaseActor) {
+ this._releaseActor(aActor);
+ }
+ this._actors.delete(aActor);
+ },
+
+ /**
+ * Release all the actors referenced by the controller, optionally filtered.
+ *
+ * @param function aFilter [optional]
+ * Callback to filter which actors are released.
+ */
+ releaseActors: function(aFilter) {
+ for (let actor of this._actors) {
+ if (!aFilter || aFilter(actor)) {
+ this.releaseActor(actor);
+ }
+ }
+ },
+};
+
+
+/**
+ * Attaches a VariablesViewController to a VariablesView if it doesn't already
+ * have one.
+ *
+ * @param VariablesView aView
+ * The view to attach to.
+ * @param object aOptions
+ * The options to use in creating the controller.
+ * @return VariablesViewController
+ */
+VariablesViewController.attach = function(aView, aOptions) {
+ if (aView.controller) {
+ return aView.controller;
+ }
+ return new VariablesViewController(aView, aOptions);
+};
diff --git a/browser/devtools/shared/widgets/ViewHelpers.jsm b/browser/devtools/shared/widgets/ViewHelpers.jsm
new file mode 100644
index 000000000..848fabd89
--- /dev/null
+++ b/browser/devtools/shared/widgets/ViewHelpers.jsm
@@ -0,0 +1,1606 @@
+/* -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* 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 = Components.classes;
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+
+const PANE_APPEARANCE_DELAY = 50;
+const PAGE_SIZE_ITEM_COUNT_RATIO = 5;
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+this.EXPORTED_SYMBOLS = ["Heritage", "ViewHelpers", "WidgetMethods"];
+
+/**
+ * Inheritance helpers from the addon SDK's core/heritage.
+ * Remove these when all devtools are loadered.
+ */
+this.Heritage = {
+ /**
+ * @see extend in sdk/core/heritage.
+ */
+ extend: function(aPrototype, aProperties = {}) {
+ return Object.create(aPrototype, this.getOwnPropertyDescriptors(aProperties));
+ },
+
+ /**
+ * @see getOwnPropertyDescriptors in sdk/core/heritage.
+ */
+ getOwnPropertyDescriptors: function(aObject) {
+ return Object.getOwnPropertyNames(aObject).reduce((aDescriptor, aName) => {
+ aDescriptor[aName] = Object.getOwnPropertyDescriptor(aObject, aName);
+ return aDescriptor;
+ }, {});
+ }
+};
+
+/**
+ * Helpers for creating and messaging between UI components.
+ */
+this.ViewHelpers = {
+ /**
+ * Convenience method, dispatching a custom event.
+ *
+ * @param nsIDOMNode aTarget
+ * A custom target element to dispatch the event from.
+ * @param string aType
+ * The name of the event.
+ * @param any aDetail
+ * The data passed when initializing the event.
+ * @return boolean
+ * True if the event was cancelled or a registered handler
+ * called preventDefault.
+ */
+ dispatchEvent: function(aTarget, aType, aDetail) {
+ if (!(aTarget instanceof Ci.nsIDOMNode)) {
+ return true; // Event cancelled.
+ }
+ let document = aTarget.ownerDocument || aTarget;
+ let dispatcher = aTarget.ownerDocument ? aTarget : document.documentElement;
+
+ let event = document.createEvent("CustomEvent");
+ event.initCustomEvent(aType, true, true, aDetail);
+ return dispatcher.dispatchEvent(event);
+ },
+
+ /**
+ * Helper delegating some of the DOM attribute methods of a node to a widget.
+ *
+ * @param object aWidget
+ * The widget to assign the methods to.
+ * @param nsIDOMNode aNode
+ * A node to delegate the methods to.
+ */
+ delegateWidgetAttributeMethods: function(aWidget, aNode) {
+ aWidget.getAttribute = aNode.getAttribute.bind(aNode);
+ aWidget.setAttribute = aNode.setAttribute.bind(aNode);
+ aWidget.removeAttribute = aNode.removeAttribute.bind(aNode);
+ },
+
+ /**
+ * Helper delegating some of the DOM event methods of a node to a widget.
+ *
+ * @param object aWidget
+ * The widget to assign the methods to.
+ * @param nsIDOMNode aNode
+ * A node to delegate the methods to.
+ */
+ delegateWidgetEventMethods: function(aWidget, aNode) {
+ aWidget.addEventListener = aNode.addEventListener.bind(aNode);
+ aWidget.removeEventListener = aNode.removeEventListener.bind(aNode);
+ },
+
+ /**
+ * Checks if the specified object looks like it's been decorated by an
+ * event emitter.
+ *
+ * @return boolean
+ * True if it looks, walks and quacks like an event emitter.
+ */
+ isEventEmitter: function(aObject) {
+ return aObject && aObject.on && aObject.off && aObject.once && aObject.emit;
+ },
+
+ /**
+ * Checks if the specified object is an instance of a DOM node.
+ *
+ * @return boolean
+ * True if it's a node, false otherwise.
+ */
+ isNode: function(aObject) {
+ return aObject instanceof Ci.nsIDOMNode ||
+ aObject instanceof Ci.nsIDOMElement ||
+ aObject instanceof Ci.nsIDOMDocumentFragment;
+ },
+
+ /**
+ * Prevents event propagation when navigation keys are pressed.
+ *
+ * @param Event e
+ * The event to be prevented.
+ */
+ preventScrolling: function(e) {
+ switch (e.keyCode) {
+ case e.DOM_VK_UP:
+ case e.DOM_VK_DOWN:
+ case e.DOM_VK_LEFT:
+ case e.DOM_VK_RIGHT:
+ case e.DOM_VK_PAGE_UP:
+ case e.DOM_VK_PAGE_DOWN:
+ case e.DOM_VK_HOME:
+ case e.DOM_VK_END:
+ e.preventDefault();
+ e.stopPropagation();
+ }
+ },
+
+ /**
+ * Sets a side pane hidden or visible.
+ *
+ * @param object aFlags
+ * An object containing some of the following properties:
+ * - visible: true if the pane should be shown, false to hide
+ * - animated: true to display an animation on toggle
+ * - delayed: true to wait a few cycles before toggle
+ * - callback: a function to invoke when the toggle finishes
+ * @param nsIDOMNode aPane
+ * The element representing the pane to toggle.
+ */
+ togglePane: function(aFlags, aPane) {
+ // Hiding is always handled via margins, not the hidden attribute.
+ aPane.removeAttribute("hidden");
+
+ // Add a class to the pane to handle min-widths, margins and animations.
+ if (!aPane.classList.contains("generic-toggled-side-pane")) {
+ aPane.classList.add("generic-toggled-side-pane");
+ }
+
+ // Avoid useless toggles.
+ if (aFlags.visible == !aPane.hasAttribute("pane-collapsed")) {
+ if (aFlags.callback) aFlags.callback();
+ return;
+ }
+
+ // Computes and sets the pane margins in order to hide or show it.
+ function set() {
+ if (aFlags.visible) {
+ aPane.style.marginLeft = "0";
+ aPane.style.marginRight = "0";
+ aPane.removeAttribute("pane-collapsed");
+ } else {
+ let margin = ~~(aPane.getAttribute("width")) + 1;
+ aPane.style.marginLeft = -margin + "px";
+ aPane.style.marginRight = -margin + "px";
+ aPane.setAttribute("pane-collapsed", "");
+ }
+
+ // Invoke the callback when the transition ended.
+ if (aFlags.animated) {
+ aPane.addEventListener("transitionend", function onEvent() {
+ aPane.removeEventListener("transitionend", onEvent, false);
+ if (aFlags.callback) aFlags.callback();
+ }, false);
+ }
+ // Invoke the callback immediately since there's no transition.
+ else {
+ if (aFlags.callback) aFlags.callback();
+ }
+ }
+
+ // The "animated" attributes enables animated toggles (slide in-out).
+ if (aFlags.animated) {
+ aPane.setAttribute("animated", "");
+ } else {
+ aPane.removeAttribute("animated");
+ }
+
+ // Sometimes it's useful delaying the toggle a few ticks to ensure
+ // a smoother slide in-out animation.
+ if (aFlags.delayed) {
+ aPane.ownerDocument.defaultView.setTimeout(set.bind(this), PANE_APPEARANCE_DELAY);
+ } else {
+ set.call(this);
+ }
+ }
+};
+
+/**
+ * Localization convenience methods.
+ *
+ * @param string aStringBundleName
+ * The desired string bundle's name.
+ */
+ViewHelpers.L10N = function(aStringBundleName) {
+ XPCOMUtils.defineLazyGetter(this, "stringBundle", () =>
+ Services.strings.createBundle(aStringBundleName));
+
+ XPCOMUtils.defineLazyGetter(this, "ellipsis", () =>
+ Services.prefs.getComplexValue("intl.ellipsis", Ci.nsIPrefLocalizedString).data);
+};
+
+ViewHelpers.L10N.prototype = {
+ stringBundle: null,
+
+ /**
+ * L10N shortcut function.
+ *
+ * @param string aName
+ * @return string
+ */
+ getStr: function(aName) {
+ return this.stringBundle.GetStringFromName(aName);
+ },
+
+ /**
+ * L10N shortcut function.
+ *
+ * @param string aName
+ * @param array aArgs
+ * @return string
+ */
+ getFormatStr: function(aName, ...aArgs) {
+ return this.stringBundle.formatStringFromName(aName, aArgs, aArgs.length);
+ },
+
+ /**
+ * L10N shortcut function for numeric arguments that need to be formatted.
+ * All numeric arguments will be fixed to 2 decimals and given a localized
+ * decimal separator. Other arguments will be left alone.
+ *
+ * @param string aName
+ * @param array aArgs
+ * @return string
+ */
+ getFormatStrWithNumbers: function(aName, ...aArgs) {
+ let newArgs = aArgs.map(x => typeof x == "number" ? this.numberWithDecimals(x, 2) : x);
+ return this.stringBundle.formatStringFromName(aName, newArgs, newArgs.length);
+ },
+
+ /**
+ * Converts a number to a locale-aware string format and keeps a certain
+ * number of decimals.
+ *
+ * @param number aNumber
+ * The number to convert.
+ * @param number aDecimals [optional]
+ * Total decimals to keep.
+ * @return string
+ * The localized number as a string.
+ */
+ numberWithDecimals: function(aNumber, aDecimals = 0) {
+ // If this is an integer, don't do anything special.
+ if (aNumber == (aNumber | 0)) {
+ return aNumber;
+ }
+ // Remove {n} trailing decimals. Can't use toFixed(n) because
+ // toLocaleString converts the number to a string. Also can't use
+ // toLocaleString(, { maximumFractionDigits: n }) because it's not
+ // implemented on OS X (bug 368838). Gross.
+ let localized = aNumber.toLocaleString(); // localize
+ let padded = localized + new Array(aDecimals).join("0"); // pad with zeros
+ let match = padded.match("([^]*?\\d{" + aDecimals + "})\\d*$");
+ return match.pop();
+ }
+};
+
+/**
+ * Shortcuts for lazily accessing and setting various preferences.
+ * Usage:
+ * let prefs = new ViewHelpers.Prefs("root.path.to.branch", {
+ * myIntPref: ["Int", "leaf.path.to.my-int-pref"],
+ * myCharPref: ["Char", "leaf.path.to.my-char-pref"],
+ * ...
+ * });
+ *
+ * prefs.myCharPref = "foo";
+ * let aux = prefs.myCharPref;
+ *
+ * @param string aPrefsRoot
+ * The root path to the required preferences branch.
+ * @param object aPrefsObject
+ * An object containing { accessorName: [prefType, prefName] } keys.
+ */
+ViewHelpers.Prefs = function(aPrefsRoot = "", aPrefsObject = {}) {
+ this.root = aPrefsRoot;
+
+ for (let accessorName in aPrefsObject) {
+ let [prefType, prefName] = aPrefsObject[accessorName];
+ this.map(accessorName, prefType, prefName);
+ }
+};
+
+ViewHelpers.Prefs.prototype = {
+ /**
+ * Helper method for getting a pref value.
+ *
+ * @param string aType
+ * @param string aPrefName
+ * @return any
+ */
+ _get: function(aType, aPrefName) {
+ if (this[aPrefName] === undefined) {
+ this[aPrefName] = Services.prefs["get" + aType + "Pref"](aPrefName);
+ }
+ return this[aPrefName];
+ },
+
+ /**
+ * Helper method for setting a pref value.
+ *
+ * @param string aType
+ * @param string aPrefName
+ * @param any aValue
+ */
+ _set: function(aType, aPrefName, aValue) {
+ Services.prefs["set" + aType + "Pref"](aPrefName, aValue);
+ this[aPrefName] = aValue;
+ },
+
+ /**
+ * Maps a property name to a pref, defining lazy getters and setters.
+ *
+ * @param string aAccessorName
+ * @param string aType
+ * @param string aPrefName
+ */
+ map: function(aAccessorName, aType, aPrefName) {
+ Object.defineProperty(this, aAccessorName, {
+ get: () => this._get(aType, [this.root, aPrefName].join(".")),
+ set: (aValue) => this._set(aType, [this.root, aPrefName].join("."), aValue)
+ });
+ }
+};
+
+/**
+ * A generic Item is used to describe children present in a Widget.
+ * The label, value and description properties are necessarily strings.
+ * Iterable via "for (let childItem in parentItem) { }".
+ *
+ * @param object aOwnerView
+ * The owner view creating this item.
+ * @param any aAttachment
+ * Some attached primitive/object.
+ * @param nsIDOMNode | nsIDOMDocumentFragment | array aContents [optional]
+ * A prebuilt node, or an array containing the following properties:
+ * - aLabel: the label displayed in the widget
+ * - aValue: the actual internal value of the item
+ * - aDescription: an optional description of the item
+ */
+function Item(aOwnerView, aAttachment, aContents = []) {
+ this.ownerView = aOwnerView;
+ this.attachment = aAttachment;
+
+ let [aLabel, aValue, aDescription] = aContents;
+ this._label = aLabel + "";
+ this._value = aValue + "";
+ this._description = (aDescription || "") + "";
+
+ // Allow the insertion of prebuilt nodes, otherwise delegate the item view
+ // creation to a widget.
+ if (ViewHelpers.isNode(aLabel)) {
+ this._prebuiltTarget = aLabel;
+ }
+
+ XPCOMUtils.defineLazyGetter(this, "_itemsByElement", () => new Map());
+};
+
+Item.prototype = {
+ get label() this._label,
+ get value() this._value,
+ get description() this._description,
+ get target() this._target,
+
+ /**
+ * Immediately appends a child item to this item.
+ *
+ * @param nsIDOMNode aElement
+ * An nsIDOMNode representing the child element to append.
+ * @param object aOptions [optional]
+ * Additional options or flags supported by this operation:
+ * - attachment: some attached primitive/object for the item
+ * - attributes: a batch of attributes set to the displayed element
+ * - finalize: function invoked when the child item is removed
+ * @return Item
+ * The item associated with the displayed element.
+ */
+ append: function(aElement, aOptions = {}) {
+ let item = new Item(this, aOptions.attachment);
+
+ // Entangle the item with the newly inserted child node.
+ this._entangleItem(item, this._target.appendChild(aElement));
+
+ // Handle any additional options after entangling the item.
+ if (aOptions.attributes) {
+ aOptions.attributes.forEach(e => item._target.setAttribute(e[0], e[1]));
+ }
+ if (aOptions.finalize) {
+ item.finalize = aOptions.finalize;
+ }
+
+ // Return the item associated with the displayed element.
+ return item;
+ },
+
+ /**
+ * Immediately removes the specified child item from this item.
+ *
+ * @param Item aItem
+ * The item associated with the element to remove.
+ */
+ remove: function(aItem) {
+ if (!aItem) {
+ return;
+ }
+ this._target.removeChild(aItem._target);
+ this._untangleItem(aItem);
+ },
+
+ /**
+ * Entangles an item (model) with a displayed node element (view).
+ *
+ * @param Item aItem
+ * The item describing a target element.
+ * @param nsIDOMNode aElement
+ * The element displaying the item.
+ */
+ _entangleItem: function(aItem, aElement) {
+ this._itemsByElement.set(aElement, aItem);
+ aItem._target = aElement;
+ },
+
+ /**
+ * Untangles an item (model) from a displayed node element (view).
+ *
+ * @param Item aItem
+ * The item describing a target element.
+ */
+ _untangleItem: function(aItem) {
+ if (aItem.finalize) {
+ aItem.finalize(aItem);
+ }
+ for (let childItem in aItem) {
+ aItem.remove(childItem);
+ }
+
+ this._unlinkItem(aItem);
+ aItem._prebuiltTarget = null;
+ aItem._target = null;
+ },
+
+ /**
+ * Deletes an item from the its parent's storage maps.
+ *
+ * @param Item aItem
+ * The item describing a target element.
+ */
+ _unlinkItem: function(aItem) {
+ this._itemsByElement.delete(aItem._target);
+ },
+
+ /**
+ * Returns a string representing the object.
+ * @return string
+ */
+ toString: function() {
+ if (this._label && this._value) {
+ return this._label + " -> " + this._value;
+ }
+ if (this.attachment) {
+ return this.attachment.toString();
+ }
+ return "(null)";
+ },
+
+ _label: "",
+ _value: "",
+ _description: "",
+ _prebuiltTarget: null,
+ _target: null,
+ finalize: null,
+ attachment: null
+};
+
+/**
+ * Some generic Widget methods handling Item instances.
+ * Iterable via "for (let childItem in wrappedView) { }".
+ *
+ * Usage:
+ * function MyView() {
+ * this.widget = new MyWidget(document.querySelector(".my-node"));
+ * }
+ *
+ * MyView.prototype = Heritage.extend(WidgetMethods, {
+ * myMethod: function() {},
+ * ...
+ * });
+ *
+ * See https://gist.github.com/victorporof/5749386 for more details.
+ *
+ * Language:
+ * - An "item" is an instance of an Item.
+ * - An "element" or "node" is a nsIDOMNode.
+ *
+ * The supplied element node or widget can either be a <xul:menulist>, or any
+ * other object interfacing the following methods:
+ * - function:nsIDOMNode insertItemAt(aIndex:number, aLabel:string, aValue:string)
+ * - function:nsIDOMNode getItemAtIndex(aIndex:number)
+ * - function removeChild(aChild:nsIDOMNode)
+ * - function removeAllItems()
+ * - get:nsIDOMNode selectedItem()
+ * - set selectedItem(aChild:nsIDOMNode)
+ * - function getAttribute(aName:string)
+ * - function setAttribute(aName:string, aValue:string)
+ * - function removeAttribute(aName:string)
+ * - function addEventListener(aName:string, aCallback:function, aBubbleFlag:boolean)
+ * - function removeEventListener(aName:string, aCallback:function, aBubbleFlag:boolean)
+ *
+ * For automagical keyboard and mouse accessibility, the element node or widget
+ * should be an event emitter with the following events:
+ * - "keyPress" -> (aName:string, aEvent:KeyboardEvent)
+ * - "mousePress" -> (aName:string, aEvent:MouseEvent)
+ */
+this.WidgetMethods = {
+ /**
+ * Sets the element node or widget associated with this container.
+ * @param nsIDOMNode | object aWidget
+ */
+ set widget(aWidget) {
+ this._widget = aWidget;
+
+ // Can't use WeakMaps for itemsByLabel or itemsByValue because
+ // keys are strings, and itemsByElement needs to be iterable.
+ XPCOMUtils.defineLazyGetter(this, "_itemsByLabel", () => new Map());
+ XPCOMUtils.defineLazyGetter(this, "_itemsByValue", () => new Map());
+ XPCOMUtils.defineLazyGetter(this, "_itemsByElement", () => new Map());
+ XPCOMUtils.defineLazyGetter(this, "_stagedItems", () => []);
+
+ // Handle internal events emitted by the widget if necessary.
+ if (ViewHelpers.isEventEmitter(aWidget)) {
+ aWidget.on("keyPress", this._onWidgetKeyPress.bind(this));
+ aWidget.on("mousePress", this._onWidgetMousePress.bind(this));
+ }
+ },
+
+ /**
+ * Gets the element node or widget associated with this container.
+ * @return nsIDOMNode | object
+ */
+ get widget() this._widget,
+
+ /**
+ * Prepares an item to be added to this container. This allows, for example,
+ * for a large number of items to be batched up before being sorted & added.
+ *
+ * If the "staged" flag is *not* set to true, the item will be immediately
+ * inserted at the correct position in this container, so that all the items
+ * still remain sorted. This can (possibly) be much slower than batching up
+ * multiple items.
+ *
+ * By default, this container assumes that all the items should be displayed
+ * sorted by their label. This can be overridden with the "index" flag,
+ * specifying on which position should an item be appended. The "staged" and
+ * "index" flags are mutually exclusive, meaning that all staged items
+ * will always be appended.
+ *
+ * Furthermore, this container makes sure that all the items are unique
+ * (two items with the same label or value are not allowed) and non-degenerate
+ * (items with "undefined" or "null" labels/values). This can, as well, be
+ * overridden via the "relaxed" flag.
+ *
+ * @param nsIDOMNode | nsIDOMDocumentFragment array aContents
+ * A prebuilt node, or an array containing the following properties:
+ * - label: the label displayed in the container
+ * - value: the actual internal value of the item
+ * - description: an optional description of the item
+ * @param object aOptions [optional]
+ * Additional options or flags supported by this operation:
+ * - staged: true to stage the item to be appended later
+ * - index: specifies on which position should the item be appended
+ * - relaxed: true if this container should allow dupes & degenerates
+ * - attachment: some attached primitive/object for the item
+ * - attributes: a batch of attributes set to the displayed element
+ * - finalize: function invoked when the item is removed
+ * @return Item
+ * The item associated with the displayed element if an unstaged push,
+ * undefined if the item was staged for a later commit.
+ */
+ push: function(aContents, aOptions = {}) {
+ let item = new Item(this, aOptions.attachment, aContents);
+
+ // Batch the item to be added later.
+ if (aOptions.staged) {
+ // An ulterior commit operation will ignore any specified index.
+ delete aOptions.index;
+ return void this._stagedItems.push({ item: item, options: aOptions });
+ }
+ // Find the target position in this container and insert the item there.
+ if (!("index" in aOptions)) {
+ return this._insertItemAt(this._findExpectedIndexFor(item), item, aOptions);
+ }
+ // Insert the item at the specified index. If negative or out of bounds,
+ // the item will be simply appended.
+ return this._insertItemAt(aOptions.index, item, aOptions);
+ },
+
+ /**
+ * Flushes all the prepared items into this container.
+ * Any specified index on the items will be ignored. Everything is appended.
+ *
+ * @param object aOptions [optional]
+ * Additional options or flags supported by this operation:
+ * - sorted: true to sort all the items before adding them
+ */
+ commit: function(aOptions = {}) {
+ let stagedItems = this._stagedItems;
+
+ // Sort the items before adding them to this container, if preferred.
+ if (aOptions.sorted) {
+ stagedItems.sort((a, b) => this._currentSortPredicate(a.item, b.item));
+ }
+ // Append the prepared items to this container.
+ for (let { item, options } of stagedItems) {
+ this._insertItemAt(-1, item, options);
+ }
+ // Recreate the temporary items list for ulterior pushes.
+ this._stagedItems.length = 0;
+ },
+
+ /**
+ * Updates this container to reflect the information provided by the
+ * currently selected item.
+ *
+ * @return boolean
+ * True if a selected item was available, false otherwise.
+ */
+ refresh: function() {
+ let selectedItem = this.selectedItem;
+ if (!selectedItem) {
+ return false;
+ }
+ this._widget.removeAttribute("notice");
+ this._widget.setAttribute("label", selectedItem._label);
+ this._widget.setAttribute("tooltiptext", selectedItem._value);
+ return true;
+ },
+
+ /**
+ * Immediately removes the specified item from this container.
+ *
+ * @param Item aItem
+ * The item associated with the element to remove.
+ */
+ remove: function(aItem) {
+ if (!aItem) {
+ return;
+ }
+ this._widget.removeChild(aItem._target);
+ this._untangleItem(aItem);
+ },
+
+ /**
+ * Removes the item at the specified index from this container.
+ *
+ * @param number aIndex
+ * The index of the item to remove.
+ */
+ removeAt: function(aIndex) {
+ this.remove(this.getItemAtIndex(aIndex));
+ },
+
+ /**
+ * Removes all items from this container.
+ */
+ empty: function() {
+ this._preferredValue = this.selectedValue;
+ this._widget.selectedItem = null;
+ this._widget.removeAllItems();
+ this._widget.setAttribute("notice", this.emptyText);
+ this._widget.setAttribute("label", this.emptyText);
+ this._widget.removeAttribute("tooltiptext");
+
+ for (let [, item] of this._itemsByElement) {
+ this._untangleItem(item);
+ }
+
+ this._itemsByLabel.clear();
+ this._itemsByValue.clear();
+ this._itemsByElement.clear();
+ this._stagedItems.length = 0;
+ },
+
+ /**
+ * Does not remove any item in this container. Instead, it overrides the
+ * current label to signal that it is unavailable and removes the tooltip.
+ */
+ setUnavailable: function() {
+ this._widget.setAttribute("notice", this.unavailableText);
+ this._widget.setAttribute("label", this.unavailableText);
+ this._widget.removeAttribute("tooltiptext");
+ },
+
+ /**
+ * The label string automatically added to this container when there are
+ * no child nodes present.
+ */
+ emptyText: "",
+
+ /**
+ * The label string added to this container when it is marked as unavailable.
+ */
+ unavailableText: "",
+
+ /**
+ * Toggles all the items in this container hidden or visible.
+ *
+ * This does not change the default filtering predicate, so newly inserted
+ * items will always be visible. Use WidgetMethods.filterContents if you care.
+ *
+ * @param boolean aVisibleFlag
+ * Specifies the intended visibility.
+ */
+ toggleContents: function(aVisibleFlag) {
+ for (let [element, item] of this._itemsByElement) {
+ element.hidden = !aVisibleFlag;
+ }
+ },
+
+ /**
+ * Toggles all items in this container hidden or visible based on a predicate.
+ *
+ * @param function aPredicate [optional]
+ * Items are toggled according to the return value of this function,
+ * which will become the new default filtering predicate in this container.
+ * If unspecified, all items will be toggled visible.
+ */
+ filterContents: function(aPredicate = this._currentFilterPredicate) {
+ this._currentFilterPredicate = aPredicate;
+
+ for (let [element, item] of this._itemsByElement) {
+ element.hidden = !aPredicate(item);
+ }
+ },
+
+ /**
+ * Sorts all the items in this container based on a predicate.
+ *
+ * @param function aPredicate [optional]
+ * Items are sorted according to the return value of the function,
+ * which will become the new default sorting predicate in this container.
+ * If unspecified, all items will be sorted by their label.
+ */
+ sortContents: function(aPredicate = this._currentSortPredicate) {
+ let sortedItems = this.orderedItems.sort(this._currentSortPredicate = aPredicate);
+
+ for (let i = 0, len = sortedItems.length; i < len; i++) {
+ this.swapItems(this.getItemAtIndex(i), sortedItems[i]);
+ }
+ },
+
+ /**
+ * Visually swaps two items in this container.
+ *
+ * @param Item aFirst
+ * The first item to be swapped.
+ * @param Item aSecond
+ * The second item to be swapped.
+ */
+ swapItems: function(aFirst, aSecond) {
+ if (aFirst == aSecond) { // We're just dandy, thank you.
+ return;
+ }
+ let { _prebuiltTarget: firstPrebuiltTarget, target: firstTarget } = aFirst;
+ let { _prebuiltTarget: secondPrebuiltTarget, target: secondTarget } = aSecond;
+
+ // If the two items were constructed with prebuilt nodes as DocumentFragments,
+ // then those DocumentFragments are now empty and need to be reassembled.
+ if (firstPrebuiltTarget instanceof Ci.nsIDOMDocumentFragment) {
+ for (let node of firstTarget.childNodes) {
+ firstPrebuiltTarget.appendChild(node.cloneNode(true));
+ }
+ }
+ if (secondPrebuiltTarget instanceof Ci.nsIDOMDocumentFragment) {
+ for (let node of secondTarget.childNodes) {
+ secondPrebuiltTarget.appendChild(node.cloneNode(true));
+ }
+ }
+
+ // 1. Get the indices of the two items to swap.
+ let i = this._indexOfElement(firstTarget);
+ let j = this._indexOfElement(secondTarget);
+
+ // 2. Remeber the selection index, to reselect an item, if necessary.
+ let selectedTarget = this._widget.selectedItem;
+ let selectedIndex = -1;
+ if (selectedTarget == firstTarget) {
+ selectedIndex = i;
+ } else if (selectedTarget == secondTarget) {
+ selectedIndex = j;
+ }
+
+ // 3. Silently nuke both items, nobody needs to know about this.
+ this._widget.removeChild(firstTarget);
+ this._widget.removeChild(secondTarget);
+ this._unlinkItem(aFirst);
+ this._unlinkItem(aSecond);
+
+ // 4. Add the items again, but reversing their indices.
+ this._insertItemAt.apply(this, i < j ? [i, aSecond] : [j, aFirst]);
+ this._insertItemAt.apply(this, i < j ? [j, aFirst] : [i, aSecond]);
+
+ // 5. Restore the previous selection, if necessary.
+ if (selectedIndex == i) {
+ this._widget.selectedItem = aFirst._target;
+ } else if (selectedIndex == j) {
+ this._widget.selectedItem = aSecond._target;
+ }
+ },
+
+ /**
+ * Visually swaps two items in this container at specific indices.
+ *
+ * @param number aFirst
+ * The index of the first item to be swapped.
+ * @param number aSecond
+ * The index of the second item to be swapped.
+ */
+ swapItemsAtIndices: function(aFirst, aSecond) {
+ this.swapItems(this.getItemAtIndex(aFirst), this.getItemAtIndex(aSecond));
+ },
+
+ /**
+ * Checks whether an item with the specified label is among the elements
+ * shown in this container.
+ *
+ * @param string aLabel
+ * The item's label.
+ * @return boolean
+ * True if the label is known, false otherwise.
+ */
+ containsLabel: function(aLabel) {
+ return this._itemsByLabel.has(aLabel) ||
+ this._stagedItems.some(({ item }) => item._label == aLabel);
+ },
+
+ /**
+ * Checks whether an item with the specified value is among the elements
+ * shown in this container.
+ *
+ * @param string aValue
+ * The item's value.
+ * @return boolean
+ * True if the value is known, false otherwise.
+ */
+ containsValue: function(aValue) {
+ return this._itemsByValue.has(aValue) ||
+ this._stagedItems.some(({ item }) => item._value == aValue);
+ },
+
+ /**
+ * Gets the "preferred value". This is the latest selected item's value,
+ * remembered just before emptying this container.
+ * @return string
+ */
+ get preferredValue() this._preferredValue,
+
+ /**
+ * Retrieves the item associated with the selected element.
+ * @return Item
+ */
+ get selectedItem() {
+ let selectedElement = this._widget.selectedItem;
+ if (selectedElement) {
+ return this._itemsByElement.get(selectedElement);
+ }
+ return null;
+ },
+
+ /**
+ * Retrieves the selected element's index in this container.
+ * @return number
+ */
+ get selectedIndex() {
+ let selectedElement = this._widget.selectedItem;
+ if (selectedElement) {
+ return this._indexOfElement(selectedElement);
+ }
+ return -1;
+ },
+
+ /**
+ * Retrieves the label of the selected element.
+ * @return string
+ */
+ get selectedLabel() {
+ let selectedElement = this._widget.selectedItem;
+ if (selectedElement) {
+ return this._itemsByElement.get(selectedElement)._label;
+ }
+ return "";
+ },
+
+ /**
+ * Retrieves the value of the selected element.
+ * @return string
+ */
+ get selectedValue() {
+ let selectedElement = this._widget.selectedItem;
+ if (selectedElement) {
+ return this._itemsByElement.get(selectedElement)._value;
+ }
+ return "";
+ },
+
+ /**
+ * Selects the element with the entangled item in this container.
+ * @param Item | function aItem
+ */
+ set selectedItem(aItem) {
+ // A predicate is allowed to select a specific item.
+ // If no item is matched, then the current selection is removed.
+ if (typeof aItem == "function") {
+ aItem = this.getItemForPredicate(aItem);
+ }
+
+ // A falsy item is allowed to invalidate the current selection.
+ let targetElement = aItem ? aItem._target : null;
+ let prevElement = this._widget.selectedItem;
+
+ // Make sure the currently selected item's target element is also focused.
+ if (this.autoFocusOnSelection && targetElement) {
+ targetElement.focus();
+ }
+
+ // Prevent selecting the same item again and avoid dispatching
+ // a redundant selection event, so return early.
+ if (targetElement == prevElement) {
+ return;
+ }
+ this._widget.selectedItem = targetElement;
+ ViewHelpers.dispatchEvent(targetElement || prevElement, "select", aItem);
+
+ // Updates this container to reflect the information provided by the
+ // currently selected item.
+ this.refresh();
+ },
+
+ /**
+ * Selects the element at the specified index in this container.
+ * @param number aIndex
+ */
+ set selectedIndex(aIndex) {
+ let targetElement = this._widget.getItemAtIndex(aIndex);
+ if (targetElement) {
+ this.selectedItem = this._itemsByElement.get(targetElement);
+ return;
+ }
+ this.selectedItem = null;
+ },
+
+ /**
+ * Selects the element with the specified label in this container.
+ * @param string aLabel
+ */
+ set selectedLabel(aLabel)
+ this.selectedItem = this._itemsByLabel.get(aLabel),
+
+ /**
+ * Selects the element with the specified value in this container.
+ * @param string aValue
+ */
+ set selectedValue(aValue)
+ this.selectedItem = this._itemsByValue.get(aValue),
+
+ /**
+ * Focus this container the first time an element is inserted?
+ *
+ * If this flag is set to true, then when the first item is inserted in
+ * this container (and thus it's the only item available), its corresponding
+ * target element is focused as well.
+ */
+ autoFocusOnFirstItem: true,
+
+ /**
+ * Focus on selection?
+ *
+ * If this flag is set to true, then whenever an item is selected in
+ * this container (e.g. via the selectedIndex or selectedItem setters),
+ * its corresponding target element is focused as well.
+ *
+ * You can disable this flag, for example, to maintain a certain node
+ * focused but visually indicate a different selection in this container.
+ */
+ autoFocusOnSelection: true,
+
+ /**
+ * Focus on input (e.g. mouse click)?
+ *
+ * If this flag is set to true, then whenever an item receives user input in
+ * this container, its corresponding target element is focused as well.
+ */
+ autoFocusOnInput: true,
+
+ /**
+ * The number of elements in this container to jump when Page Up or Page Down
+ * keys are pressed. If falsy, then the page size will be based on the
+ * number of visible items in the container.
+ */
+ pageSize: 0,
+
+ /**
+ * Focuses the first visible item in this container.
+ */
+ focusFirstVisibleItem: function() {
+ this.focusItemAtDelta(-this.itemCount);
+ },
+
+ /**
+ * Focuses the last visible item in this container.
+ */
+ focusLastVisibleItem: function() {
+ this.focusItemAtDelta(+this.itemCount);
+ },
+
+ /**
+ * Focuses the next item in this container.
+ */
+ focusNextItem: function() {
+ this.focusItemAtDelta(+1);
+ },
+
+ /**
+ * Focuses the previous item in this container.
+ */
+ focusPrevItem: function() {
+ this.focusItemAtDelta(-1);
+ },
+
+ /**
+ * Focuses another item in this container based on the index distance
+ * from the currently focused item.
+ *
+ * @param number aDelta
+ * A scalar specifying by how many items should the selection change.
+ */
+ focusItemAtDelta: function(aDelta) {
+ // Make sure the currently selected item is also focused, so that the
+ // command dispatcher mechanism has a relative node to work with.
+ // If there's no selection, just select an item at a corresponding index
+ // (e.g. the first item in this container if aDelta <= 1).
+ let selectedElement = this._widget.selectedItem;
+ if (selectedElement) {
+ selectedElement.focus();
+ } else {
+ this.selectedIndex = Math.max(0, aDelta - 1);
+ return;
+ }
+
+ let direction = aDelta > 0 ? "advanceFocus" : "rewindFocus";
+ let distance = Math.abs(Math[aDelta > 0 ? "ceil" : "floor"](aDelta));
+ while (distance--) {
+ if (!this._focusChange(direction)) {
+ break; // Out of bounds.
+ }
+ }
+
+ // Synchronize the selected item as being the currently focused element.
+ this.selectedItem = this.getItemForElement(this._focusedElement);
+ },
+
+ /**
+ * Focuses the next or previous item in this container.
+ *
+ * @param string aDirection
+ * Either "advanceFocus" or "rewindFocus".
+ * @return boolean
+ * False if the focus went out of bounds and the first or last item
+ * in this container was focused instead.
+ */
+ _focusChange: function(aDirection) {
+ let commandDispatcher = this._commandDispatcher;
+ let prevFocusedElement = commandDispatcher.focusedElement;
+
+ commandDispatcher.suppressFocusScroll = true;
+ commandDispatcher[aDirection]();
+
+ // Make sure the newly focused item is a part of this container.
+ // If the focus goes out of bounds, revert the previously focused item.
+ if (!this.getItemForElement(commandDispatcher.focusedElement)) {
+ prevFocusedElement.focus();
+ return false;
+ }
+ // Focus remained within bounds.
+ return true;
+ },
+
+ /**
+ * Gets the command dispatcher instance associated with this container's DOM.
+ * If there are no items displayed in this container, null is returned.
+ * @return nsIDOMXULCommandDispatcher | null
+ */
+ get _commandDispatcher() {
+ if (this._cachedCommandDispatcher) {
+ return this._cachedCommandDispatcher;
+ }
+ let someElement = this._widget.getItemAtIndex(0);
+ if (someElement) {
+ let commandDispatcher = someElement.ownerDocument.commandDispatcher;
+ return this._cachedCommandDispatcher = commandDispatcher;
+ }
+ return null;
+ },
+
+ /**
+ * Gets the currently focused element in this container.
+ *
+ * @return nsIDOMNode
+ * The focused element, or null if nothing is found.
+ */
+ get _focusedElement() {
+ let commandDispatcher = this._commandDispatcher;
+ if (commandDispatcher) {
+ return commandDispatcher.focusedElement;
+ }
+ return null;
+ },
+
+ /**
+ * Gets the item in the container having the specified index.
+ *
+ * @param number aIndex
+ * The index used to identify the element.
+ * @return Item
+ * The matched item, or null if nothing is found.
+ */
+ getItemAtIndex: function(aIndex) {
+ return this.getItemForElement(this._widget.getItemAtIndex(aIndex));
+ },
+
+ /**
+ * Gets the item in the container having the specified label.
+ *
+ * @param string aLabel
+ * The label used to identify the element.
+ * @return Item
+ * The matched item, or null if nothing is found.
+ */
+ getItemByLabel: function(aLabel) {
+ return this._itemsByLabel.get(aLabel);
+ },
+
+ /**
+ * Gets the item in the container having the specified value.
+ *
+ * @param string aValue
+ * The value used to identify the element.
+ * @return Item
+ * The matched item, or null if nothing is found.
+ */
+ getItemByValue: function(aValue) {
+ return this._itemsByValue.get(aValue);
+ },
+
+ /**
+ * Gets the item in the container associated with the specified element.
+ *
+ * @param nsIDOMNode aElement
+ * The element used to identify the item.
+ * @return Item
+ * The matched item, or null if nothing is found.
+ */
+ getItemForElement: function(aElement) {
+ while (aElement) {
+ let item = this._itemsByElement.get(aElement);
+ if (item) {
+ return item;
+ }
+ aElement = aElement.parentNode;
+ }
+ return null;
+ },
+
+ /**
+ * Gets a visible item in this container validating a specified predicate.
+ *
+ * @param function aPredicate
+ * The first item which validates this predicate is returned
+ * @return Item
+ * The matched item, or null if nothing is found.
+ */
+ getItemForPredicate: function(aPredicate, aOwner = this) {
+ for (let [element, item] of aOwner._itemsByElement) {
+ let match;
+ if (aPredicate(item) && !element.hidden) {
+ match = item;
+ } else {
+ match = this.getItemForPredicate(aPredicate, item);
+ }
+ if (match) {
+ return match;
+ }
+ }
+ return null;
+ },
+
+ /**
+ * Finds the index of an item in the container.
+ *
+ * @param Item aItem
+ * The item get the index for.
+ * @return number
+ * The index of the matched item, or -1 if nothing is found.
+ */
+ indexOfItem: function(aItem) {
+ return this._indexOfElement(aItem._target);
+ },
+
+ /**
+ * Finds the index of an element in the container.
+ *
+ * @param nsIDOMNode aElement
+ * The element get the index for.
+ * @return number
+ * The index of the matched element, or -1 if nothing is found.
+ */
+ _indexOfElement: function(aElement) {
+ for (let i = 0; i < this._itemsByElement.size; i++) {
+ if (this._widget.getItemAtIndex(i) == aElement) {
+ return i;
+ }
+ }
+ return -1;
+ },
+
+ /**
+ * Gets the total number of items in this container.
+ * @return number
+ */
+ get itemCount() this._itemsByElement.size,
+
+ /**
+ * Returns a list of items in this container, in no particular order.
+ * @return array
+ */
+ get items() {
+ let items = [];
+ for (let [, item] of this._itemsByElement) {
+ items.push(item);
+ }
+ return items;
+ },
+
+ /**
+ * Returns a list of labels in this container, in no particular order.
+ * @return array
+ */
+ get labels() {
+ let labels = [];
+ for (let [label] of this._itemsByLabel) {
+ labels.push(label);
+ }
+ return labels;
+ },
+
+ /**
+ * Returns a list of values in this container, in no particular order.
+ * @return array
+ */
+ get values() {
+ let values = [];
+ for (let [value] of this._itemsByValue) {
+ values.push(value);
+ }
+ return values;
+ },
+
+ /**
+ * Returns a list of all the visible (non-hidden) items in this container,
+ * in no particular order.
+ * @return array
+ */
+ get visibleItems() {
+ let items = [];
+ for (let [element, item] of this._itemsByElement) {
+ if (!element.hidden) {
+ items.push(item);
+ }
+ }
+ return items;
+ },
+
+ /**
+ * Returns a list of all items in this container, in the displayed order.
+ * @return array
+ */
+ get orderedItems() {
+ let items = [];
+ let itemCount = this.itemCount;
+ for (let i = 0; i < itemCount; i++) {
+ items.push(this.getItemAtIndex(i));
+ }
+ return items;
+ },
+
+ /**
+ * Returns a list of all the visible (non-hidden) items in this container,
+ * in the displayed order
+ * @return array
+ */
+ get orderedVisibleItems() {
+ let items = [];
+ let itemCount = this.itemCount;
+ for (let i = 0; i < itemCount; i++) {
+ let item = this.getItemAtIndex(i);
+ if (!item._target.hidden) {
+ items.push(item);
+ }
+ }
+ return items;
+ },
+
+ /**
+ * Specifies the required conditions for an item to be considered unique.
+ * Possible values:
+ * - 1: label AND value are different from all other items
+ * - 2: label OR value are different from all other items
+ * - 3: only label is required to be different
+ * - 4: only value is required to be different
+ */
+ uniquenessQualifier: 1,
+
+ /**
+ * Checks if an item is unique in this container.
+ *
+ * @param Item aItem
+ * The item for which to verify uniqueness.
+ * @return boolean
+ * True if the item is unique, false otherwise.
+ */
+ isUnique: function(aItem) {
+ switch (this.uniquenessQualifier) {
+ case 1:
+ return !this._itemsByLabel.has(aItem._label) &&
+ !this._itemsByValue.has(aItem._value);
+ case 2:
+ return !this._itemsByLabel.has(aItem._label) ||
+ !this._itemsByValue.has(aItem._value);
+ case 3:
+ return !this._itemsByLabel.has(aItem._label);
+ case 4:
+ return !this._itemsByValue.has(aItem._value);
+ }
+ return false;
+ },
+
+ /**
+ * Checks if an item is eligible for this container.
+ *
+ * @param Item aItem
+ * The item for which to verify eligibility.
+ * @return boolean
+ * True if the item is eligible, false otherwise.
+ */
+ isEligible: function(aItem) {
+ let isUnique = this.isUnique(aItem);
+ let isPrebuilt = !!aItem._prebuiltTarget;
+ let isDegenerate = aItem._label == "undefined" || aItem._label == "null" ||
+ aItem._value == "undefined" || aItem._value == "null";
+
+ return isPrebuilt || (isUnique && !isDegenerate);
+ },
+
+ /**
+ * Finds the expected item index in this container based on the default
+ * sort predicate.
+ *
+ * @param Item aItem
+ * The item for which to get the expected index.
+ * @return number
+ * The expected item index.
+ */
+ _findExpectedIndexFor: function(aItem) {
+ let itemCount = this.itemCount;
+
+ for (let i = 0; i < itemCount; i++) {
+ if (this._currentSortPredicate(this.getItemAtIndex(i), aItem) > 0) {
+ return i;
+ }
+ }
+ return itemCount;
+ },
+
+ /**
+ * Immediately inserts an item in this container at the specified index.
+ *
+ * @param number aIndex
+ * The position in the container intended for this item.
+ * @param Item aItem
+ * An object containing a label and a value property (at least).
+ * @param object aOptions [optional]
+ * Additional options or flags supported by this operation:
+ * - node: allows the insertion of prebuilt nodes instead of labels
+ * - relaxed: true if this container should allow dupes & degenerates
+ * - attributes: a batch of attributes set to the displayed element
+ * - finalize: function when the item is untangled (removed)
+ * @return Item
+ * The item associated with the displayed element, null if rejected.
+ */
+ _insertItemAt: function(aIndex, aItem, aOptions = {}) {
+ // Relaxed nodes may be appended without verifying their eligibility.
+ if (!aOptions.relaxed && !this.isEligible(aItem)) {
+ return null;
+ }
+
+ // Entangle the item with the newly inserted node.
+ this._entangleItem(aItem, this._widget.insertItemAt(aIndex,
+ aItem._prebuiltTarget || aItem._label, // Allow the insertion of prebuilt nodes.
+ aItem._value,
+ aItem._description,
+ aItem.attachment));
+
+ // Handle any additional options after entangling the item.
+ if (!this._currentFilterPredicate(aItem)) {
+ aItem._target.hidden = true;
+ }
+ if (this.autoFocusOnFirstItem && this._itemsByElement.size == 1) {
+ aItem._target.focus();
+ }
+ if (aOptions.attributes) {
+ aOptions.attributes.forEach(e => aItem._target.setAttribute(e[0], e[1]));
+ }
+ if (aOptions.finalize) {
+ aItem.finalize = aOptions.finalize;
+ }
+
+ // Return the item associated with the displayed element.
+ return aItem;
+ },
+
+ /**
+ * Entangles an item (model) with a displayed node element (view).
+ *
+ * @param Item aItem
+ * The item describing a target element.
+ * @param nsIDOMNode aElement
+ * The element displaying the item.
+ */
+ _entangleItem: function(aItem, aElement) {
+ this._itemsByLabel.set(aItem._label, aItem);
+ this._itemsByValue.set(aItem._value, aItem);
+ this._itemsByElement.set(aElement, aItem);
+ aItem._target = aElement;
+ },
+
+ /**
+ * Untangles an item (model) from a displayed node element (view).
+ *
+ * @param Item aItem
+ * The item describing a target element.
+ */
+ _untangleItem: function(aItem) {
+ if (aItem.finalize) {
+ aItem.finalize(aItem);
+ }
+ for (let childItem in aItem) {
+ aItem.remove(childItem);
+ }
+
+ this._unlinkItem(aItem);
+ aItem._prebuiltTarget = null;
+ aItem._target = null;
+ },
+
+ /**
+ * Deletes an item from the its parent's storage maps.
+ *
+ * @param Item aItem
+ * The item describing a target element.
+ */
+ _unlinkItem: function(aItem) {
+ this._itemsByLabel.delete(aItem._label);
+ this._itemsByValue.delete(aItem._value);
+ this._itemsByElement.delete(aItem._target);
+ },
+
+ /**
+ * The keyPress event listener for this container.
+ * @param string aName
+ * @param KeyboardEvent aEvent
+ */
+ _onWidgetKeyPress: function(aName, aEvent) {
+ // Prevent scrolling when pressing navigation keys.
+ ViewHelpers.preventScrolling(aEvent);
+
+ switch (aEvent.keyCode) {
+ case aEvent.DOM_VK_UP:
+ case aEvent.DOM_VK_LEFT:
+ this.focusPrevItem();
+ return;
+ case aEvent.DOM_VK_DOWN:
+ case aEvent.DOM_VK_RIGHT:
+ this.focusNextItem();
+ return;
+ case aEvent.DOM_VK_PAGE_UP:
+ this.focusItemAtDelta(-(this.pageSize || (this.itemCount / PAGE_SIZE_ITEM_COUNT_RATIO)));
+ return;
+ case aEvent.DOM_VK_PAGE_DOWN:
+ this.focusItemAtDelta(+(this.pageSize || (this.itemCount / PAGE_SIZE_ITEM_COUNT_RATIO)));
+ return;
+ case aEvent.DOM_VK_HOME:
+ this.focusFirstVisibleItem();
+ return;
+ case aEvent.DOM_VK_END:
+ this.focusLastVisibleItem();
+ return;
+ }
+ },
+
+ /**
+ * The keyPress event listener for this container.
+ * @param string aName
+ * @param MouseEvent aEvent
+ */
+ _onWidgetMousePress: function(aName, aEvent) {
+ if (aEvent.button != 0) {
+ // Only allow left-click to trigger this event.
+ return;
+ }
+
+ let item = this.getItemForElement(aEvent.target);
+ if (item) {
+ // The container is not empty and we clicked on an actual item.
+ this.selectedItem = item;
+ // Make sure the current event's target element is also focused.
+ this.autoFocusOnInput && item._target.focus();
+ }
+ },
+
+ /**
+ * The predicate used when filtering items. By default, all items in this
+ * view are visible.
+ *
+ * @param Item aItem
+ * The item passing through the filter.
+ * @return boolean
+ * True if the item should be visible, false otherwise.
+ */
+ _currentFilterPredicate: function(aItem) {
+ return true;
+ },
+
+ /**
+ * The predicate used when sorting items. By default, items in this view
+ * are sorted by their label.
+ *
+ * @param Item aFirst
+ * The first item used in the comparison.
+ * @param Item aSecond
+ * The second item used in the comparison.
+ * @return number
+ * -1 to sort aFirst to a lower index than aSecond
+ * 0 to leave aFirst and aSecond unchanged with respect to each other
+ * 1 to sort aSecond to a lower index than aFirst
+ */
+ _currentSortPredicate: function(aFirst, aSecond) {
+ return +(aFirst._label.toLowerCase() > aSecond._label.toLowerCase());
+ },
+
+ _widget: null,
+ _preferredValue: null,
+ _cachedCommandDispatcher: null
+};
+
+/**
+ * A generator-iterator over all the items in this container.
+ */
+Item.prototype.__iterator__ =
+WidgetMethods.__iterator__ = function() {
+ for (let [, item] of this._itemsByElement) {
+ yield item;
+ }
+};
diff --git a/browser/devtools/shared/widgets/widgets.css b/browser/devtools/shared/widgets/widgets.css
new file mode 100644
index 000000000..5ee65ea91
--- /dev/null
+++ b/browser/devtools/shared/widgets/widgets.css
@@ -0,0 +1,59 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* 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/. */
+
+/* BreacrumbsWidget */
+
+.breadcrumbs-widget-item {
+ direction: ltr;
+}
+
+.breadcrumbs-widget-item {
+ -moz-user-focus: normal;
+}
+
+/* SideMenuWidget */
+
+.side-menu-widget-container {
+ overflow-x: hidden;
+ overflow-y: auto;
+}
+
+.side-menu-widget-item-contents {
+ -moz-user-focus: normal;
+}
+
+/* VariablesView */
+
+.variables-view-container {
+ overflow-x: hidden;
+ overflow-y: auto;
+}
+
+.variables-view-element-details:not([open]) {
+ display: none;
+}
+
+.variables-view-scope,
+.variable-or-property {
+ -moz-user-focus: normal;
+}
+
+.variables-view-scope > .title,
+.variable-or-property > .title {
+ overflow: hidden;
+}
+
+.variables-view-scope[non-header] > .title,
+.variable-or-property[non-header] > .title,
+.variable-or-property[non-match] > .title {
+ display: none;
+}
+
+.variable-or-property:not([safe-getter]) > tooltip > label[value=WebIDL],
+.variable-or-property:not([non-extensible]) > tooltip > label[value=extensible],
+.variable-or-property:not([frozen]) > tooltip > label[value=frozen],
+.variable-or-property:not([sealed]) > tooltip > label[value=sealed] {
+ display: none;
+}