зеркало из https://github.com/mozilla/gecko-dev.git
Bug 1145262 - Modularize the pane views in the debugger. r=fitzgen
This commit is contained in:
Родитель
cc0aeb3f6e
Коммит
ca2d6d3dcb
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -77,7 +77,7 @@ ToolbarView.prototype = {
|
|||
* Add commands that XUL can fire.
|
||||
*/
|
||||
_addCommands: function() {
|
||||
utils.addCommands(document.getElementById('debuggerCommands'), {
|
||||
XULUtils.addCommands(document.getElementById('debuggerCommands'), {
|
||||
resumeCommand: () => this._onResumePressed(),
|
||||
stepOverCommand: () => this._onStepOverPressed(),
|
||||
stepInCommand: () => this._onStepInPressed(),
|
||||
|
@ -254,7 +254,7 @@ OptionsView.prototype = {
|
|||
* Add commands that XUL can fire.
|
||||
*/
|
||||
_addCommands: function() {
|
||||
utils.addCommands(document.getElementById('debuggerCommands'), {
|
||||
XULUtils.addCommands(document.getElementById('debuggerCommands'), {
|
||||
toggleAutoPrettyPrint: () => this._toggleAutoPrettyPrint(),
|
||||
togglePauseOnExceptions: () => this._togglePauseOnExceptions(),
|
||||
toggleIgnoreCaughtExceptions: () => this._toggleIgnoreCaughtExceptions(),
|
||||
|
@ -835,7 +835,7 @@ FilterView.prototype = {
|
|||
* Add commands that XUL can fire.
|
||||
*/
|
||||
_addCommands: function() {
|
||||
utils.addCommands(document.getElementById('debuggerCommands'), {
|
||||
XULUtils.addCommands(document.getElementById('debuggerCommands'), {
|
||||
fileSearchCommand: () => this._doFileSearch(),
|
||||
globalSearchCommand: () => this._doGlobalSearch(),
|
||||
functionSearchCommand: () => this._doFunctionSearch(),
|
||||
|
|
|
@ -25,12 +25,17 @@
|
|||
<script type="application/javascript;version=1.8"
|
||||
src="chrome://browser/content/devtools/theme-switching.js"/>
|
||||
<script type="text/javascript" src="chrome://global/content/globalOverlay.js"/>
|
||||
<script type="text/javascript" src="debugger/utils.js"/>
|
||||
<script type="text/javascript" src="debugger-controller.js"/>
|
||||
<script type="text/javascript" src="debugger-view.js"/>
|
||||
<script type="text/javascript" src="debugger-toolbar.js"/>
|
||||
<script type="text/javascript" src="debugger-panes.js"/>
|
||||
|
||||
<script type="text/javascript" src="debugger/utils.js"/>
|
||||
<script type="text/javascript" src="debugger/sources-view.js"/>
|
||||
<script type="text/javascript" src="debugger/variable-bubble-view.js"/>
|
||||
<script type="text/javascript" src="debugger/tracer-view.js"/>
|
||||
<script type="text/javascript" src="debugger/watch-expressions-view.js"/>
|
||||
<script type="text/javascript" src="debugger/event-listeners-view.js"/>
|
||||
<script type="text/javascript" src="debugger/global-search-view.js"/>
|
||||
|
||||
<commandset id="editMenuCommands"/>
|
||||
|
||||
<commandset id="debuggerCommands"></commandset>
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
"use strict";
|
||||
|
||||
const utils = {
|
||||
const XULUtils = {
|
||||
/**
|
||||
* Create <command> elements within `commandset` with event handlers
|
||||
* bound to the `command` event
|
||||
|
@ -26,3 +26,296 @@ const utils = {
|
|||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Used to detect minification for automatic pretty printing
|
||||
const SAMPLE_SIZE = 50; // no of lines
|
||||
const INDENT_COUNT_THRESHOLD = 5; // percentage
|
||||
const CHARACTER_LIMIT = 250; // line character limit
|
||||
|
||||
/**
|
||||
* Utility functions for handling sources.
|
||||
*/
|
||||
const SourceUtils = {
|
||||
_labelsCache: new Map(), // Can't use WeakMaps because keys are strings.
|
||||
_groupsCache: new Map(),
|
||||
_minifiedCache: new WeakMap(),
|
||||
|
||||
/**
|
||||
* Returns true if the specified url and/or content type are specific to
|
||||
* javascript files.
|
||||
*
|
||||
* @return boolean
|
||||
* True if the source is likely javascript.
|
||||
*/
|
||||
isJavaScript: function(aUrl, aContentType = "") {
|
||||
return (aUrl && /\.jsm?$/.test(this.trimUrlQuery(aUrl))) ||
|
||||
aContentType.contains("javascript");
|
||||
},
|
||||
|
||||
/**
|
||||
* Determines if the source text is minified by using
|
||||
* the percentage indented of a subset of lines
|
||||
*
|
||||
* @return object
|
||||
* A promise that resolves to true if source text is minified.
|
||||
*/
|
||||
isMinified: Task.async(function*(sourceClient) {
|
||||
if (this._minifiedCache.has(sourceClient)) {
|
||||
return this._minifiedCache.get(sourceClient);
|
||||
}
|
||||
|
||||
let [, text] = yield DebuggerController.SourceScripts.getText(sourceClient);
|
||||
let isMinified;
|
||||
let lineEndIndex = 0;
|
||||
let lineStartIndex = 0;
|
||||
let lines = 0;
|
||||
let indentCount = 0;
|
||||
let overCharLimit = false;
|
||||
|
||||
// Strip comments.
|
||||
text = text.replace(/\/\*[\S\s]*?\*\/|\/\/(.+|\n)/g, "");
|
||||
|
||||
while (lines++ < SAMPLE_SIZE) {
|
||||
lineEndIndex = text.indexOf("\n", lineStartIndex);
|
||||
if (lineEndIndex == -1) {
|
||||
break;
|
||||
}
|
||||
if (/^\s+/.test(text.slice(lineStartIndex, lineEndIndex))) {
|
||||
indentCount++;
|
||||
}
|
||||
// For files with no indents but are not minified.
|
||||
if ((lineEndIndex - lineStartIndex) > CHARACTER_LIMIT) {
|
||||
overCharLimit = true;
|
||||
break;
|
||||
}
|
||||
lineStartIndex = lineEndIndex + 1;
|
||||
}
|
||||
|
||||
isMinified =
|
||||
((indentCount / lines) * 100) < INDENT_COUNT_THRESHOLD || overCharLimit;
|
||||
|
||||
this._minifiedCache.set(sourceClient, isMinified);
|
||||
return isMinified;
|
||||
}),
|
||||
|
||||
/**
|
||||
* Clears the labels, groups and minify cache, populated by methods like
|
||||
* SourceUtils.getSourceLabel or Source Utils.getSourceGroup.
|
||||
* This should be done every time the content location changes.
|
||||
*/
|
||||
clearCache: function() {
|
||||
this._labelsCache.clear();
|
||||
this._groupsCache.clear();
|
||||
this._minifiedCache.clear();
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets a unique, simplified label from a source url.
|
||||
*
|
||||
* @param string aUrl
|
||||
* The source url.
|
||||
* @return string
|
||||
* The simplified label.
|
||||
*/
|
||||
getSourceLabel: function(aUrl) {
|
||||
let cachedLabel = this._labelsCache.get(aUrl);
|
||||
if (cachedLabel) {
|
||||
return cachedLabel;
|
||||
}
|
||||
|
||||
let sourceLabel = null;
|
||||
|
||||
for (let name of Object.keys(KNOWN_SOURCE_GROUPS)) {
|
||||
if (aUrl.startsWith(KNOWN_SOURCE_GROUPS[name])) {
|
||||
sourceLabel = aUrl.substring(KNOWN_SOURCE_GROUPS[name].length);
|
||||
}
|
||||
}
|
||||
|
||||
if (!sourceLabel) {
|
||||
sourceLabel = this.trimUrl(aUrl);
|
||||
}
|
||||
|
||||
let unicodeLabel = NetworkHelper.convertToUnicode(unescape(sourceLabel));
|
||||
this._labelsCache.set(aUrl, unicodeLabel);
|
||||
return unicodeLabel;
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets as much information as possible about the hostname and directory paths
|
||||
* of an url to create a short url group identifier.
|
||||
*
|
||||
* @param string aUrl
|
||||
* The source url.
|
||||
* @return string
|
||||
* The simplified group.
|
||||
*/
|
||||
getSourceGroup: function(aUrl) {
|
||||
let cachedGroup = this._groupsCache.get(aUrl);
|
||||
if (cachedGroup) {
|
||||
return cachedGroup;
|
||||
}
|
||||
|
||||
try {
|
||||
// Use an nsIURL to parse all the url path parts.
|
||||
var uri = Services.io.newURI(aUrl, null, null).QueryInterface(Ci.nsIURL);
|
||||
} catch (e) {
|
||||
// This doesn't look like a url, or nsIURL can't handle it.
|
||||
return "";
|
||||
}
|
||||
|
||||
let groupLabel = uri.prePath;
|
||||
|
||||
for (let name of Object.keys(KNOWN_SOURCE_GROUPS)) {
|
||||
if (aUrl.startsWith(KNOWN_SOURCE_GROUPS[name])) {
|
||||
groupLabel = name;
|
||||
}
|
||||
}
|
||||
|
||||
let unicodeLabel = NetworkHelper.convertToUnicode(unescape(groupLabel));
|
||||
this._groupsCache.set(aUrl, unicodeLabel)
|
||||
return unicodeLabel;
|
||||
},
|
||||
|
||||
/**
|
||||
* Trims the url by shortening it if it exceeds a certain length, adding an
|
||||
* ellipsis at the end.
|
||||
*
|
||||
* @param string aUrl
|
||||
* The source url.
|
||||
* @param number aLength [optional]
|
||||
* The expected source url length.
|
||||
* @param number aSection [optional]
|
||||
* The section to trim. Supported values: "start", "center", "end"
|
||||
* @return string
|
||||
* The shortened url.
|
||||
*/
|
||||
trimUrlLength: function(aUrl, aLength, aSection) {
|
||||
aLength = aLength || SOURCE_URL_DEFAULT_MAX_LENGTH;
|
||||
aSection = aSection || "end";
|
||||
|
||||
if (aUrl.length > aLength) {
|
||||
switch (aSection) {
|
||||
case "start":
|
||||
return L10N.ellipsis + aUrl.slice(-aLength);
|
||||
break;
|
||||
case "center":
|
||||
return aUrl.substr(0, aLength / 2 - 1) + L10N.ellipsis + aUrl.slice(-aLength / 2 + 1);
|
||||
break;
|
||||
case "end":
|
||||
return aUrl.substr(0, aLength) + L10N.ellipsis;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return aUrl;
|
||||
},
|
||||
|
||||
/**
|
||||
* Trims the query part or reference identifier of a url string, if necessary.
|
||||
*
|
||||
* @param string aUrl
|
||||
* The source url.
|
||||
* @return string
|
||||
* The shortened url.
|
||||
*/
|
||||
trimUrlQuery: function(aUrl) {
|
||||
let length = aUrl.length;
|
||||
let q1 = aUrl.indexOf('?');
|
||||
let q2 = aUrl.indexOf('&');
|
||||
let q3 = aUrl.indexOf('#');
|
||||
let q = Math.min(q1 != -1 ? q1 : length,
|
||||
q2 != -1 ? q2 : length,
|
||||
q3 != -1 ? q3 : length);
|
||||
|
||||
return aUrl.slice(0, q);
|
||||
},
|
||||
|
||||
/**
|
||||
* Trims as much as possible from a url, while keeping the label unique
|
||||
* in the sources container.
|
||||
*
|
||||
* @param string | nsIURL aUrl
|
||||
* The source url.
|
||||
* @param string aLabel [optional]
|
||||
* The resulting label at each step.
|
||||
* @param number aSeq [optional]
|
||||
* The current iteration step.
|
||||
* @return string
|
||||
* The resulting label at the final step.
|
||||
*/
|
||||
trimUrl: function(aUrl, aLabel, aSeq) {
|
||||
if (!(aUrl instanceof Ci.nsIURL)) {
|
||||
try {
|
||||
// Use an nsIURL to parse all the url path parts.
|
||||
aUrl = Services.io.newURI(aUrl, null, null).QueryInterface(Ci.nsIURL);
|
||||
} catch (e) {
|
||||
// This doesn't look like a url, or nsIURL can't handle it.
|
||||
return aUrl;
|
||||
}
|
||||
}
|
||||
if (!aSeq) {
|
||||
let name = aUrl.fileName;
|
||||
if (name) {
|
||||
// This is a regular file url, get only the file name (contains the
|
||||
// base name and extension if available).
|
||||
|
||||
// If this url contains an invalid query, unfortunately nsIURL thinks
|
||||
// it's part of the file extension. It must be removed.
|
||||
aLabel = aUrl.fileName.replace(/\&.*/, "");
|
||||
} else {
|
||||
// This is not a file url, hence there is no base name, nor extension.
|
||||
// Proceed using other available information.
|
||||
aLabel = "";
|
||||
}
|
||||
aSeq = 1;
|
||||
}
|
||||
|
||||
// If we have a label and it doesn't only contain a query...
|
||||
if (aLabel && aLabel.indexOf("?") != 0) {
|
||||
// A page may contain multiple requests to the same url but with different
|
||||
// queries. It is *not* redundant to show each one.
|
||||
if (!DebuggerView.Sources.getItemForAttachment(e => e.label == aLabel)) {
|
||||
return aLabel;
|
||||
}
|
||||
}
|
||||
|
||||
// Append the url query.
|
||||
if (aSeq == 1) {
|
||||
let query = aUrl.query;
|
||||
if (query) {
|
||||
return this.trimUrl(aUrl, aLabel + "?" + query, aSeq + 1);
|
||||
}
|
||||
aSeq++;
|
||||
}
|
||||
// Append the url reference.
|
||||
if (aSeq == 2) {
|
||||
let ref = aUrl.ref;
|
||||
if (ref) {
|
||||
return this.trimUrl(aUrl, aLabel + "#" + aUrl.ref, aSeq + 1);
|
||||
}
|
||||
aSeq++;
|
||||
}
|
||||
// Prepend the url directory.
|
||||
if (aSeq == 3) {
|
||||
let dir = aUrl.directory;
|
||||
if (dir) {
|
||||
return this.trimUrl(aUrl, dir.replace(/^\//, "") + aLabel, aSeq + 1);
|
||||
}
|
||||
aSeq++;
|
||||
}
|
||||
// Prepend the hostname and port number.
|
||||
if (aSeq == 4) {
|
||||
let host = aUrl.hostPort;
|
||||
if (host) {
|
||||
return this.trimUrl(aUrl, host + "/" + aLabel, aSeq + 1);
|
||||
}
|
||||
aSeq++;
|
||||
}
|
||||
// Use the whole url spec but ignoring the reference.
|
||||
if (aSeq == 5) {
|
||||
return this.trimUrl(aUrl, aUrl.specIgnoringRef, aSeq + 1);
|
||||
}
|
||||
// Give up.
|
||||
return aUrl.spec;
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -0,0 +1,274 @@
|
|||
/* 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";
|
||||
|
||||
/**
|
||||
* Functions handling the event listeners UI.
|
||||
*/
|
||||
function EventListenersView(DebuggerController) {
|
||||
dumpn("EventListenersView was instantiated");
|
||||
|
||||
this.Breakpoints = DebuggerController.Breakpoints;
|
||||
|
||||
this._onCheck = this._onCheck.bind(this);
|
||||
this._onClick = this._onClick.bind(this);
|
||||
}
|
||||
|
||||
EventListenersView.prototype = Heritage.extend(WidgetMethods, {
|
||||
/**
|
||||
* Initialization function, called when the debugger is started.
|
||||
*/
|
||||
initialize: function() {
|
||||
dumpn("Initializing the EventListenersView");
|
||||
|
||||
this.widget = new SideMenuWidget(document.getElementById("event-listeners"), {
|
||||
showItemCheckboxes: true,
|
||||
showGroupCheckboxes: true
|
||||
});
|
||||
|
||||
this.emptyText = L10N.getStr("noEventListenersText");
|
||||
this._eventCheckboxTooltip = L10N.getStr("eventCheckboxTooltip");
|
||||
this._onSelectorString = " " + L10N.getStr("eventOnSelector") + " ";
|
||||
this._inSourceString = " " + L10N.getStr("eventInSource") + " ";
|
||||
this._inNativeCodeString = L10N.getStr("eventNative");
|
||||
|
||||
this.widget.addEventListener("check", this._onCheck, false);
|
||||
this.widget.addEventListener("click", this._onClick, false);
|
||||
},
|
||||
|
||||
/**
|
||||
* Destruction function, called when the debugger is closed.
|
||||
*/
|
||||
destroy: function() {
|
||||
dumpn("Destroying the EventListenersView");
|
||||
|
||||
this.widget.removeEventListener("check", this._onCheck, false);
|
||||
this.widget.removeEventListener("click", this._onClick, false);
|
||||
},
|
||||
|
||||
/**
|
||||
* Adds an event to this event listeners container.
|
||||
*
|
||||
* @param object aListener
|
||||
* The listener object coming from the active thread.
|
||||
* @param object aOptions [optional]
|
||||
* Additional options for adding the source. Supported options:
|
||||
* - staged: true to stage the item to be appended later
|
||||
*/
|
||||
addListener: function(aListener, aOptions = {}) {
|
||||
let { node: { selector }, function: { url }, type } = aListener;
|
||||
if (!type) return;
|
||||
|
||||
// Some listener objects may be added from plugins, thus getting
|
||||
// translated to native code.
|
||||
if (!url) {
|
||||
url = this._inNativeCodeString;
|
||||
}
|
||||
|
||||
// If an event item for this listener's url and type was already added,
|
||||
// avoid polluting the view and simply increase the "targets" count.
|
||||
let eventItem = this.getItemForPredicate(aItem =>
|
||||
aItem.attachment.url == url &&
|
||||
aItem.attachment.type == type);
|
||||
|
||||
if (eventItem) {
|
||||
let { selectors, view: { targets } } = eventItem.attachment;
|
||||
if (selectors.indexOf(selector) == -1) {
|
||||
selectors.push(selector);
|
||||
targets.setAttribute("value", L10N.getFormatStr("eventNodes", selectors.length));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// There's no easy way of grouping event types into higher-level groups,
|
||||
// so we need to do this by hand.
|
||||
let is = (...args) => args.indexOf(type) != -1;
|
||||
let has = str => type.contains(str);
|
||||
let starts = str => type.startsWith(str);
|
||||
let group;
|
||||
|
||||
if (starts("animation")) {
|
||||
group = L10N.getStr("animationEvents");
|
||||
} else if (starts("audio")) {
|
||||
group = L10N.getStr("audioEvents");
|
||||
} else if (is("levelchange")) {
|
||||
group = L10N.getStr("batteryEvents");
|
||||
} else if (is("cut", "copy", "paste")) {
|
||||
group = L10N.getStr("clipboardEvents");
|
||||
} else if (starts("composition")) {
|
||||
group = L10N.getStr("compositionEvents");
|
||||
} else if (starts("device")) {
|
||||
group = L10N.getStr("deviceEvents");
|
||||
} else if (is("fullscreenchange", "fullscreenerror", "orientationchange",
|
||||
"overflow", "resize", "scroll", "underflow", "zoom")) {
|
||||
group = L10N.getStr("displayEvents");
|
||||
} else if (starts("drag") || starts("drop")) {
|
||||
group = L10N.getStr("dragAndDropEvents");
|
||||
} else if (starts("gamepad")) {
|
||||
group = L10N.getStr("gamepadEvents");
|
||||
} else if (is("canplay", "canplaythrough", "durationchange", "emptied",
|
||||
"ended", "loadeddata", "loadedmetadata", "pause", "play", "playing",
|
||||
"ratechange", "seeked", "seeking", "stalled", "suspend", "timeupdate",
|
||||
"volumechange", "waiting")) {
|
||||
group = L10N.getStr("mediaEvents");
|
||||
} else if (is("blocked", "complete", "success", "upgradeneeded", "versionchange")) {
|
||||
group = L10N.getStr("indexedDBEvents");
|
||||
} else if (is("blur", "change", "focus", "focusin", "focusout", "invalid",
|
||||
"reset", "select", "submit")) {
|
||||
group = L10N.getStr("interactionEvents");
|
||||
} else if (starts("key") || is("input")) {
|
||||
group = L10N.getStr("keyboardEvents");
|
||||
} else if (starts("mouse") || has("click") || is("contextmenu", "show", "wheel")) {
|
||||
group = L10N.getStr("mouseEvents");
|
||||
} else if (starts("DOM")) {
|
||||
group = L10N.getStr("mutationEvents");
|
||||
} else if (is("abort", "error", "hashchange", "load", "loadend", "loadstart",
|
||||
"pagehide", "pageshow", "progress", "timeout", "unload", "uploadprogress",
|
||||
"visibilitychange")) {
|
||||
group = L10N.getStr("navigationEvents");
|
||||
} else if (is("pointerlockchange", "pointerlockerror")) {
|
||||
group = L10N.getStr("pointerLockEvents");
|
||||
} else if (is("compassneedscalibration", "userproximity")) {
|
||||
group = L10N.getStr("sensorEvents");
|
||||
} else if (starts("storage")) {
|
||||
group = L10N.getStr("storageEvents");
|
||||
} else if (is("beginEvent", "endEvent", "repeatEvent")) {
|
||||
group = L10N.getStr("timeEvents");
|
||||
} else if (starts("touch")) {
|
||||
group = L10N.getStr("touchEvents");
|
||||
} else {
|
||||
group = L10N.getStr("otherEvents");
|
||||
}
|
||||
|
||||
// Create the element node for the event listener item.
|
||||
let itemView = this._createItemView(type, selector, url);
|
||||
|
||||
// Event breakpoints survive target navigations. Make sure the newly
|
||||
// inserted event item is correctly checked.
|
||||
let checkboxState =
|
||||
this.Breakpoints.DOM.activeEventNames.indexOf(type) != -1;
|
||||
|
||||
// Append an event listener item to this container.
|
||||
this.push([itemView.container], {
|
||||
staged: aOptions.staged, /* stage the item to be appended later? */
|
||||
attachment: {
|
||||
url: url,
|
||||
type: type,
|
||||
view: itemView,
|
||||
selectors: [selector],
|
||||
group: group,
|
||||
checkboxState: checkboxState,
|
||||
checkboxTooltip: this._eventCheckboxTooltip
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets all the event types known to this container.
|
||||
*
|
||||
* @return array
|
||||
* List of event types, for example ["load", "click"...]
|
||||
*/
|
||||
getAllEvents: function() {
|
||||
return this.attachments.map(e => e.type);
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets the checked event types in this container.
|
||||
*
|
||||
* @return array
|
||||
* List of event types, for example ["load", "click"...]
|
||||
*/
|
||||
getCheckedEvents: function() {
|
||||
return this.attachments.filter(e => e.checkboxState).map(e => e.type);
|
||||
},
|
||||
|
||||
/**
|
||||
* Customization function for creating an item's UI.
|
||||
*
|
||||
* @param string aType
|
||||
* The event type, for example "click".
|
||||
* @param string aSelector
|
||||
* The target element's selector.
|
||||
* @param string url
|
||||
* The source url in which the event listener is located.
|
||||
* @return object
|
||||
* An object containing the event listener view nodes.
|
||||
*/
|
||||
_createItemView: function(aType, aSelector, aUrl) {
|
||||
let container = document.createElement("hbox");
|
||||
container.className = "dbg-event-listener";
|
||||
|
||||
let eventType = document.createElement("label");
|
||||
eventType.className = "plain dbg-event-listener-type";
|
||||
eventType.setAttribute("value", aType);
|
||||
container.appendChild(eventType);
|
||||
|
||||
let typeSeparator = document.createElement("label");
|
||||
typeSeparator.className = "plain dbg-event-listener-separator";
|
||||
typeSeparator.setAttribute("value", this._onSelectorString);
|
||||
container.appendChild(typeSeparator);
|
||||
|
||||
let eventTargets = document.createElement("label");
|
||||
eventTargets.className = "plain dbg-event-listener-targets";
|
||||
eventTargets.setAttribute("value", aSelector);
|
||||
container.appendChild(eventTargets);
|
||||
|
||||
let selectorSeparator = document.createElement("label");
|
||||
selectorSeparator.className = "plain dbg-event-listener-separator";
|
||||
selectorSeparator.setAttribute("value", this._inSourceString);
|
||||
container.appendChild(selectorSeparator);
|
||||
|
||||
let eventLocation = document.createElement("label");
|
||||
eventLocation.className = "plain dbg-event-listener-location";
|
||||
eventLocation.setAttribute("value", SourceUtils.getSourceLabel(aUrl));
|
||||
eventLocation.setAttribute("flex", "1");
|
||||
eventLocation.setAttribute("crop", "center");
|
||||
container.appendChild(eventLocation);
|
||||
|
||||
return {
|
||||
container: container,
|
||||
type: eventType,
|
||||
targets: eventTargets,
|
||||
location: eventLocation
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* The check listener for the event listeners container.
|
||||
*/
|
||||
_onCheck: function({ detail: { description, checked }, target }) {
|
||||
if (description == "item") {
|
||||
this.getItemForElement(target).attachment.checkboxState = checked;
|
||||
this.Breakpoints.DOM.scheduleEventBreakpointsUpdate();
|
||||
return;
|
||||
}
|
||||
|
||||
// Check all the event items in this group.
|
||||
this.items
|
||||
.filter(e => e.attachment.group == description)
|
||||
.forEach(e => this.callMethod("checkItem", e.target, checked));
|
||||
},
|
||||
|
||||
/**
|
||||
* The select listener for the event listeners container.
|
||||
*/
|
||||
_onClick: function({ target }) {
|
||||
// Changing the checkbox state is handled by the _onCheck event. Avoid
|
||||
// handling that again in this click event, so pass in "noSiblings"
|
||||
// when retrieving the target's item, to ignore the checkbox.
|
||||
let eventItem = this.getItemForElement(target, { noSiblings: true });
|
||||
if (eventItem) {
|
||||
let newState = eventItem.attachment.checkboxState ^= 1;
|
||||
this.callMethod("checkItem", eventItem.target, newState);
|
||||
}
|
||||
},
|
||||
|
||||
_eventCheckboxTooltip: "",
|
||||
_onSelectorString: "",
|
||||
_inSourceString: "",
|
||||
_inNativeCodeString: ""
|
||||
});
|
||||
|
||||
DebuggerView.EventListeners = new EventListenersView(DebuggerController);
|
|
@ -0,0 +1,736 @@
|
|||
/* 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";
|
||||
|
||||
/**
|
||||
* Functions handling the global search UI.
|
||||
*/
|
||||
function GlobalSearchView(DebuggerController, DebuggerView) {
|
||||
dumpn("GlobalSearchView was instantiated");
|
||||
|
||||
this.SourceScripts = DebuggerController.SourceScripts;
|
||||
this.DebuggerView = DebuggerView;
|
||||
|
||||
this._onHeaderClick = this._onHeaderClick.bind(this);
|
||||
this._onLineClick = this._onLineClick.bind(this);
|
||||
this._onMatchClick = this._onMatchClick.bind(this);
|
||||
}
|
||||
|
||||
GlobalSearchView.prototype = Heritage.extend(WidgetMethods, {
|
||||
/**
|
||||
* Initialization function, called when the debugger is started.
|
||||
*/
|
||||
initialize: function() {
|
||||
dumpn("Initializing the GlobalSearchView");
|
||||
|
||||
this.widget = new SimpleListWidget(document.getElementById("globalsearch"));
|
||||
this._splitter = document.querySelector("#globalsearch + .devtools-horizontal-splitter");
|
||||
|
||||
this.emptyText = L10N.getStr("noMatchingStringsText");
|
||||
},
|
||||
|
||||
/**
|
||||
* Destruction function, called when the debugger is closed.
|
||||
*/
|
||||
destroy: function() {
|
||||
dumpn("Destroying the GlobalSearchView");
|
||||
},
|
||||
|
||||
/**
|
||||
* Sets the results container hidden or visible. It's hidden by default.
|
||||
* @param boolean aFlag
|
||||
*/
|
||||
set hidden(aFlag) {
|
||||
this.widget.setAttribute("hidden", aFlag);
|
||||
this._splitter.setAttribute("hidden", aFlag);
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets the visibility state of the global search container.
|
||||
* @return boolean
|
||||
*/
|
||||
get hidden()
|
||||
this.widget.getAttribute("hidden") == "true" ||
|
||||
this._splitter.getAttribute("hidden") == "true",
|
||||
|
||||
/**
|
||||
* Hides and removes all items from this search container.
|
||||
*/
|
||||
clearView: function() {
|
||||
this.hidden = true;
|
||||
this.empty();
|
||||
},
|
||||
|
||||
/**
|
||||
* Selects the next found item in this container.
|
||||
* Does not change the currently focused node.
|
||||
*/
|
||||
selectNext: function() {
|
||||
let totalLineResults = LineResults.size();
|
||||
if (!totalLineResults) {
|
||||
return;
|
||||
}
|
||||
if (++this._currentlyFocusedMatch >= totalLineResults) {
|
||||
this._currentlyFocusedMatch = 0;
|
||||
}
|
||||
this._onMatchClick({
|
||||
target: LineResults.getElementAtIndex(this._currentlyFocusedMatch)
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Selects the previously found item in this container.
|
||||
* Does not change the currently focused node.
|
||||
*/
|
||||
selectPrev: function() {
|
||||
let totalLineResults = LineResults.size();
|
||||
if (!totalLineResults) {
|
||||
return;
|
||||
}
|
||||
if (--this._currentlyFocusedMatch < 0) {
|
||||
this._currentlyFocusedMatch = totalLineResults - 1;
|
||||
}
|
||||
this._onMatchClick({
|
||||
target: LineResults.getElementAtIndex(this._currentlyFocusedMatch)
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Schedules searching for a string in all of the sources.
|
||||
*
|
||||
* @param string aToken
|
||||
* The string to search for.
|
||||
* @param number aWait
|
||||
* The amount of milliseconds to wait until draining.
|
||||
*/
|
||||
scheduleSearch: function(aToken, aWait) {
|
||||
// The amount of time to wait for the requests to settle.
|
||||
let maxDelay = GLOBAL_SEARCH_ACTION_MAX_DELAY;
|
||||
let delay = aWait === undefined ? maxDelay / aToken.length : aWait;
|
||||
|
||||
// Allow requests to settle down first.
|
||||
setNamedTimeout("global-search", delay, () => {
|
||||
// Start fetching as many sources as possible, then perform the search.
|
||||
let actors = this.DebuggerView.Sources.values;
|
||||
let sourcesFetched = this.SourceScripts.getTextForSources(actors);
|
||||
sourcesFetched.then(aSources => this._doSearch(aToken, aSources));
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Finds string matches in all the sources stored in the controller's cache,
|
||||
* and groups them by url and line number.
|
||||
*
|
||||
* @param string aToken
|
||||
* The string to search for.
|
||||
* @param array aSources
|
||||
* An array of [url, text] tuples for each source.
|
||||
*/
|
||||
_doSearch: function(aToken, aSources) {
|
||||
// Don't continue filtering if the searched token is an empty string.
|
||||
if (!aToken) {
|
||||
this.clearView();
|
||||
return;
|
||||
}
|
||||
|
||||
// Search is not case sensitive, prepare the actual searched token.
|
||||
let lowerCaseToken = aToken.toLowerCase();
|
||||
let tokenLength = aToken.length;
|
||||
|
||||
// Create a Map containing search details for each source.
|
||||
let globalResults = new GlobalResults();
|
||||
|
||||
// Search for the specified token in each source's text.
|
||||
for (let [actor, text] of aSources) {
|
||||
let item = this.DebuggerView.Sources.getItemByValue(actor);
|
||||
let url = item.attachment.source.url;
|
||||
if (!url) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Verify that the search token is found anywhere in the source.
|
||||
if (!text.toLowerCase().contains(lowerCaseToken)) {
|
||||
continue;
|
||||
}
|
||||
// ...and if so, create a Map containing search details for each line.
|
||||
let sourceResults = new SourceResults(actor,
|
||||
globalResults,
|
||||
this.DebuggerView.Sources);
|
||||
|
||||
// Search for the specified token in each line's text.
|
||||
text.split("\n").forEach((aString, aLine) => {
|
||||
// Search is not case sensitive, prepare the actual searched line.
|
||||
let lowerCaseLine = aString.toLowerCase();
|
||||
|
||||
// Verify that the search token is found anywhere in this line.
|
||||
if (!lowerCaseLine.contains(lowerCaseToken)) {
|
||||
return;
|
||||
}
|
||||
// ...and if so, create a Map containing search details for each word.
|
||||
let lineResults = new LineResults(aLine, sourceResults);
|
||||
|
||||
// Search for the specified token this line's text.
|
||||
lowerCaseLine.split(lowerCaseToken).reduce((aPrev, aCurr, aIndex, aArray) => {
|
||||
let prevLength = aPrev.length;
|
||||
let currLength = aCurr.length;
|
||||
|
||||
// Everything before the token is unmatched.
|
||||
let unmatched = aString.substr(prevLength, currLength);
|
||||
lineResults.add(unmatched);
|
||||
|
||||
// The lowered-case line was split by the lowered-case token. So,
|
||||
// get the actual matched text from the original line's text.
|
||||
if (aIndex != aArray.length - 1) {
|
||||
let matched = aString.substr(prevLength + currLength, tokenLength);
|
||||
let range = { start: prevLength + currLength, length: matched.length };
|
||||
lineResults.add(matched, range, true);
|
||||
}
|
||||
|
||||
// Continue with the next sub-region in this line's text.
|
||||
return aPrev + aToken + aCurr;
|
||||
}, "");
|
||||
|
||||
if (lineResults.matchCount) {
|
||||
sourceResults.add(lineResults);
|
||||
}
|
||||
});
|
||||
|
||||
if (sourceResults.matchCount) {
|
||||
globalResults.add(sourceResults);
|
||||
}
|
||||
}
|
||||
|
||||
// Rebuild the results, then signal if there are any matches.
|
||||
if (globalResults.matchCount) {
|
||||
this.hidden = false;
|
||||
this._currentlyFocusedMatch = -1;
|
||||
this._createGlobalResultsUI(globalResults);
|
||||
window.emit(EVENTS.GLOBAL_SEARCH_MATCH_FOUND);
|
||||
} else {
|
||||
window.emit(EVENTS.GLOBAL_SEARCH_MATCH_NOT_FOUND);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Creates global search results entries and adds them to this container.
|
||||
*
|
||||
* @param GlobalResults aGlobalResults
|
||||
* An object containing all source results, grouped by source location.
|
||||
*/
|
||||
_createGlobalResultsUI: function(aGlobalResults) {
|
||||
let i = 0;
|
||||
|
||||
for (let sourceResults of aGlobalResults) {
|
||||
if (i++ == 0) {
|
||||
this._createSourceResultsUI(sourceResults);
|
||||
} else {
|
||||
// Dispatch subsequent document manipulation operations, to avoid
|
||||
// blocking the main thread when a large number of search results
|
||||
// is found, thus giving the impression of faster searching.
|
||||
Services.tm.currentThread.dispatch({ run:
|
||||
this._createSourceResultsUI.bind(this, sourceResults)
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Creates source search results entries and adds them to this container.
|
||||
*
|
||||
* @param SourceResults aSourceResults
|
||||
* An object containing all the matched lines for a specific source.
|
||||
*/
|
||||
_createSourceResultsUI: function(aSourceResults) {
|
||||
// Create the element node for the source results item.
|
||||
let container = document.createElement("hbox");
|
||||
aSourceResults.createView(container, {
|
||||
onHeaderClick: this._onHeaderClick,
|
||||
onLineClick: this._onLineClick,
|
||||
onMatchClick: this._onMatchClick
|
||||
});
|
||||
|
||||
// Append a source results item to this container.
|
||||
let item = this.push([container], {
|
||||
index: -1, /* specifies on which position should the item be appended */
|
||||
attachment: {
|
||||
sourceResults: aSourceResults
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* The click listener for a results header.
|
||||
*/
|
||||
_onHeaderClick: function(e) {
|
||||
let sourceResultsItem = SourceResults.getItemForElement(e.target);
|
||||
sourceResultsItem.instance.toggle(e);
|
||||
},
|
||||
|
||||
/**
|
||||
* The click listener for a results line.
|
||||
*/
|
||||
_onLineClick: function(e) {
|
||||
let lineResultsItem = LineResults.getItemForElement(e.target);
|
||||
this._onMatchClick({ target: lineResultsItem.firstMatch });
|
||||
},
|
||||
|
||||
/**
|
||||
* The click listener for a result match.
|
||||
*/
|
||||
_onMatchClick: function(e) {
|
||||
if (e instanceof Event) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
let target = e.target;
|
||||
let sourceResultsItem = SourceResults.getItemForElement(target);
|
||||
let lineResultsItem = LineResults.getItemForElement(target);
|
||||
|
||||
sourceResultsItem.instance.expand();
|
||||
this._currentlyFocusedMatch = LineResults.indexOfElement(target);
|
||||
this._scrollMatchIntoViewIfNeeded(target);
|
||||
this._bounceMatch(target);
|
||||
|
||||
let actor = sourceResultsItem.instance.actor;
|
||||
let line = lineResultsItem.instance.line;
|
||||
|
||||
this.DebuggerView.setEditorLocation(actor, line + 1, { noDebug: true });
|
||||
|
||||
let range = lineResultsItem.lineData.range;
|
||||
let cursor = this.DebuggerView.editor.getOffset({ line: line, ch: 0 });
|
||||
let [ anchor, head ] = this.DebuggerView.editor.getPosition(
|
||||
cursor + range.start,
|
||||
cursor + range.start + range.length
|
||||
);
|
||||
|
||||
this.DebuggerView.editor.setSelection(anchor, head);
|
||||
},
|
||||
|
||||
/**
|
||||
* Scrolls a match into view if not already visible.
|
||||
*
|
||||
* @param nsIDOMNode aMatch
|
||||
* The match to scroll into view.
|
||||
*/
|
||||
_scrollMatchIntoViewIfNeeded: function(aMatch) {
|
||||
this.widget.ensureElementIsVisible(aMatch);
|
||||
},
|
||||
|
||||
/**
|
||||
* Starts a bounce animation for a match.
|
||||
*
|
||||
* @param nsIDOMNode aMatch
|
||||
* The match to start a bounce animation for.
|
||||
*/
|
||||
_bounceMatch: function(aMatch) {
|
||||
Services.tm.currentThread.dispatch({ run: () => {
|
||||
aMatch.addEventListener("transitionend", function onEvent() {
|
||||
aMatch.removeEventListener("transitionend", onEvent);
|
||||
aMatch.removeAttribute("focused");
|
||||
});
|
||||
aMatch.setAttribute("focused", "");
|
||||
}}, 0);
|
||||
aMatch.setAttribute("focusing", "");
|
||||
},
|
||||
|
||||
_splitter: null,
|
||||
_currentlyFocusedMatch: -1,
|
||||
_forceExpandResults: false
|
||||
});
|
||||
|
||||
DebuggerView.GlobalSearch = new GlobalSearchView(DebuggerController, DebuggerView);
|
||||
|
||||
/**
|
||||
* An object containing all source results, grouped by source location.
|
||||
* Iterable via "for (let [location, sourceResults] of globalResults) { }".
|
||||
*/
|
||||
function GlobalResults() {
|
||||
this._store = [];
|
||||
SourceResults._itemsByElement = new Map();
|
||||
LineResults._itemsByElement = new Map();
|
||||
}
|
||||
|
||||
GlobalResults.prototype = {
|
||||
/**
|
||||
* Adds source results to this store.
|
||||
*
|
||||
* @param SourceResults aSourceResults
|
||||
* An object containing search results for a specific source.
|
||||
*/
|
||||
add: function(aSourceResults) {
|
||||
this._store.push(aSourceResults);
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets the number of source results in this store.
|
||||
*/
|
||||
get matchCount() this._store.length
|
||||
};
|
||||
|
||||
/**
|
||||
* An object containing all the matched lines for a specific source.
|
||||
* Iterable via "for (let [lineNumber, lineResults] of sourceResults) { }".
|
||||
*
|
||||
* @param string aActor
|
||||
* The target source actor id.
|
||||
* @param GlobalResults aGlobalResults
|
||||
* An object containing all source results, grouped by source location.
|
||||
*/
|
||||
function SourceResults(aActor, aGlobalResults, sourcesView) {
|
||||
let item = sourcesView.getItemByValue(aActor);
|
||||
this.actor = aActor;
|
||||
this.label = item.attachment.source.url;
|
||||
this._globalResults = aGlobalResults;
|
||||
this._store = [];
|
||||
}
|
||||
|
||||
SourceResults.prototype = {
|
||||
/**
|
||||
* Adds line results to this store.
|
||||
*
|
||||
* @param LineResults aLineResults
|
||||
* An object containing search results for a specific line.
|
||||
*/
|
||||
add: function(aLineResults) {
|
||||
this._store.push(aLineResults);
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets the number of line results in this store.
|
||||
*/
|
||||
get matchCount() this._store.length,
|
||||
|
||||
/**
|
||||
* Expands the element, showing all the added details.
|
||||
*/
|
||||
expand: function() {
|
||||
this._resultsContainer.removeAttribute("hidden");
|
||||
this._arrow.setAttribute("open", "");
|
||||
},
|
||||
|
||||
/**
|
||||
* Collapses the element, hiding all the added details.
|
||||
*/
|
||||
collapse: function() {
|
||||
this._resultsContainer.setAttribute("hidden", "true");
|
||||
this._arrow.removeAttribute("open");
|
||||
},
|
||||
|
||||
/**
|
||||
* Toggles between the element collapse/expand state.
|
||||
*/
|
||||
toggle: function(e) {
|
||||
this.expanded ^= 1;
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets this element's expanded state.
|
||||
* @return boolean
|
||||
*/
|
||||
get expanded()
|
||||
this._resultsContainer.getAttribute("hidden") != "true" &&
|
||||
this._arrow.hasAttribute("open"),
|
||||
|
||||
/**
|
||||
* Sets this element's expanded state.
|
||||
* @param boolean aFlag
|
||||
*/
|
||||
set expanded(aFlag) this[aFlag ? "expand" : "collapse"](),
|
||||
|
||||
/**
|
||||
* Gets the element associated with this item.
|
||||
* @return nsIDOMNode
|
||||
*/
|
||||
get target() this._target,
|
||||
|
||||
/**
|
||||
* Customization function for creating this item's UI.
|
||||
*
|
||||
* @param nsIDOMNode aElementNode
|
||||
* The element associated with the displayed item.
|
||||
* @param object aCallbacks
|
||||
* An object containing all the necessary callback functions:
|
||||
* - onHeaderClick
|
||||
* - onMatchClick
|
||||
*/
|
||||
createView: function(aElementNode, aCallbacks) {
|
||||
this._target = aElementNode;
|
||||
|
||||
let arrow = this._arrow = document.createElement("box");
|
||||
arrow.className = "arrow";
|
||||
|
||||
let locationNode = document.createElement("label");
|
||||
locationNode.className = "plain dbg-results-header-location";
|
||||
locationNode.setAttribute("value", this.label);
|
||||
|
||||
let matchCountNode = document.createElement("label");
|
||||
matchCountNode.className = "plain dbg-results-header-match-count";
|
||||
matchCountNode.setAttribute("value", "(" + this.matchCount + ")");
|
||||
|
||||
let resultsHeader = this._resultsHeader = document.createElement("hbox");
|
||||
resultsHeader.className = "dbg-results-header";
|
||||
resultsHeader.setAttribute("align", "center")
|
||||
resultsHeader.appendChild(arrow);
|
||||
resultsHeader.appendChild(locationNode);
|
||||
resultsHeader.appendChild(matchCountNode);
|
||||
resultsHeader.addEventListener("click", aCallbacks.onHeaderClick, false);
|
||||
|
||||
let resultsContainer = this._resultsContainer = document.createElement("vbox");
|
||||
resultsContainer.className = "dbg-results-container";
|
||||
resultsContainer.setAttribute("hidden", "true");
|
||||
|
||||
// Create lines search results entries and add them to this container.
|
||||
// Afterwards, if the number of matches is reasonable, expand this
|
||||
// container automatically.
|
||||
for (let lineResults of this._store) {
|
||||
lineResults.createView(resultsContainer, aCallbacks);
|
||||
}
|
||||
if (this.matchCount < GLOBAL_SEARCH_EXPAND_MAX_RESULTS) {
|
||||
this.expand();
|
||||
}
|
||||
|
||||
let resultsBox = document.createElement("vbox");
|
||||
resultsBox.setAttribute("flex", "1");
|
||||
resultsBox.appendChild(resultsHeader);
|
||||
resultsBox.appendChild(resultsContainer);
|
||||
|
||||
aElementNode.id = "source-results-" + this.actor;
|
||||
aElementNode.className = "dbg-source-results";
|
||||
aElementNode.appendChild(resultsBox);
|
||||
|
||||
SourceResults._itemsByElement.set(aElementNode, { instance: this });
|
||||
},
|
||||
|
||||
actor: "",
|
||||
_globalResults: null,
|
||||
_store: null,
|
||||
_target: null,
|
||||
_arrow: null,
|
||||
_resultsHeader: null,
|
||||
_resultsContainer: null
|
||||
};
|
||||
|
||||
/**
|
||||
* An object containing all the matches for a specific line.
|
||||
* Iterable via "for (let chunk of lineResults) { }".
|
||||
*
|
||||
* @param number aLine
|
||||
* The target line in the source.
|
||||
* @param SourceResults aSourceResults
|
||||
* An object containing all the matched lines for a specific source.
|
||||
*/
|
||||
function LineResults(aLine, aSourceResults) {
|
||||
this.line = aLine;
|
||||
this._sourceResults = aSourceResults;
|
||||
this._store = [];
|
||||
this._matchCount = 0;
|
||||
}
|
||||
|
||||
LineResults.prototype = {
|
||||
/**
|
||||
* Adds string details to this store.
|
||||
*
|
||||
* @param string aString
|
||||
* The text contents chunk in the line.
|
||||
* @param object aRange
|
||||
* An object containing the { start, length } of the chunk.
|
||||
* @param boolean aMatchFlag
|
||||
* True if the chunk is a matched string, false if just text content.
|
||||
*/
|
||||
add: function(aString, aRange, aMatchFlag) {
|
||||
this._store.push({ string: aString, range: aRange, match: !!aMatchFlag });
|
||||
this._matchCount += aMatchFlag ? 1 : 0;
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets the number of word results in this store.
|
||||
*/
|
||||
get matchCount() this._matchCount,
|
||||
|
||||
/**
|
||||
* Gets the element associated with this item.
|
||||
* @return nsIDOMNode
|
||||
*/
|
||||
get target() this._target,
|
||||
|
||||
/**
|
||||
* Customization function for creating this item's UI.
|
||||
*
|
||||
* @param nsIDOMNode aElementNode
|
||||
* The element associated with the displayed item.
|
||||
* @param object aCallbacks
|
||||
* An object containing all the necessary callback functions:
|
||||
* - onMatchClick
|
||||
* - onLineClick
|
||||
*/
|
||||
createView: function(aElementNode, aCallbacks) {
|
||||
this._target = aElementNode;
|
||||
|
||||
let lineNumberNode = document.createElement("label");
|
||||
lineNumberNode.className = "plain dbg-results-line-number";
|
||||
lineNumberNode.classList.add("devtools-monospace");
|
||||
lineNumberNode.setAttribute("value", this.line + 1);
|
||||
|
||||
let lineContentsNode = document.createElement("hbox");
|
||||
lineContentsNode.className = "dbg-results-line-contents";
|
||||
lineContentsNode.classList.add("devtools-monospace");
|
||||
lineContentsNode.setAttribute("flex", "1");
|
||||
|
||||
let lineString = "";
|
||||
let lineLength = 0;
|
||||
let firstMatch = null;
|
||||
|
||||
for (let lineChunk of this._store) {
|
||||
let { string, range, match } = lineChunk;
|
||||
lineString = string.substr(0, GLOBAL_SEARCH_LINE_MAX_LENGTH - lineLength);
|
||||
lineLength += string.length;
|
||||
|
||||
let lineChunkNode = document.createElement("label");
|
||||
lineChunkNode.className = "plain dbg-results-line-contents-string";
|
||||
lineChunkNode.setAttribute("value", lineString);
|
||||
lineChunkNode.setAttribute("match", match);
|
||||
lineContentsNode.appendChild(lineChunkNode);
|
||||
|
||||
if (match) {
|
||||
this._entangleMatch(lineChunkNode, lineChunk);
|
||||
lineChunkNode.addEventListener("click", aCallbacks.onMatchClick, false);
|
||||
firstMatch = firstMatch || lineChunkNode;
|
||||
}
|
||||
if (lineLength >= GLOBAL_SEARCH_LINE_MAX_LENGTH) {
|
||||
lineContentsNode.appendChild(this._ellipsis.cloneNode(true));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
this._entangleLine(lineContentsNode, firstMatch);
|
||||
lineContentsNode.addEventListener("click", aCallbacks.onLineClick, false);
|
||||
|
||||
let searchResult = document.createElement("hbox");
|
||||
searchResult.className = "dbg-search-result";
|
||||
searchResult.appendChild(lineNumberNode);
|
||||
searchResult.appendChild(lineContentsNode);
|
||||
|
||||
aElementNode.appendChild(searchResult);
|
||||
},
|
||||
|
||||
/**
|
||||
* Handles a match while creating the view.
|
||||
* @param nsIDOMNode aNode
|
||||
* @param object aMatchChunk
|
||||
*/
|
||||
_entangleMatch: function(aNode, aMatchChunk) {
|
||||
LineResults._itemsByElement.set(aNode, {
|
||||
instance: this,
|
||||
lineData: aMatchChunk
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Handles a line while creating the view.
|
||||
* @param nsIDOMNode aNode
|
||||
* @param nsIDOMNode aFirstMatch
|
||||
*/
|
||||
_entangleLine: function(aNode, aFirstMatch) {
|
||||
LineResults._itemsByElement.set(aNode, {
|
||||
instance: this,
|
||||
firstMatch: aFirstMatch,
|
||||
ignored: true
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* An nsIDOMNode label with an ellipsis value.
|
||||
*/
|
||||
_ellipsis: (function() {
|
||||
let label = document.createElement("label");
|
||||
label.className = "plain dbg-results-line-contents-string";
|
||||
label.setAttribute("value", L10N.ellipsis);
|
||||
return label;
|
||||
})(),
|
||||
|
||||
line: 0,
|
||||
_sourceResults: null,
|
||||
_store: null,
|
||||
_target: null
|
||||
};
|
||||
|
||||
/**
|
||||
* A generator-iterator over the global, source or line results.
|
||||
*/
|
||||
GlobalResults.prototype[Symbol.iterator] =
|
||||
SourceResults.prototype[Symbol.iterator] =
|
||||
LineResults.prototype[Symbol.iterator] = function*() {
|
||||
yield* this._store;
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the item associated with the specified element.
|
||||
*
|
||||
* @param nsIDOMNode aElement
|
||||
* The element used to identify the item.
|
||||
* @return object
|
||||
* The matched item, or null if nothing is found.
|
||||
*/
|
||||
SourceResults.getItemForElement =
|
||||
LineResults.getItemForElement = function(aElement) {
|
||||
return WidgetMethods.getItemForElement.call(this, aElement, { noSiblings: true });
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the element associated with a particular item at a specified index.
|
||||
*
|
||||
* @param number aIndex
|
||||
* The index used to identify the item.
|
||||
* @return nsIDOMNode
|
||||
* The matched element, or null if nothing is found.
|
||||
*/
|
||||
SourceResults.getElementAtIndex =
|
||||
LineResults.getElementAtIndex = function(aIndex) {
|
||||
for (let [element, item] of this._itemsByElement) {
|
||||
if (!item.ignored && !aIndex--) {
|
||||
return element;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the index of an item associated with the specified element.
|
||||
*
|
||||
* @param nsIDOMNode aElement
|
||||
* The element to get the index for.
|
||||
* @return number
|
||||
* The index of the matched element, or -1 if nothing is found.
|
||||
*/
|
||||
SourceResults.indexOfElement =
|
||||
LineResults.indexOfElement = function(aElement) {
|
||||
let count = 0;
|
||||
for (let [element, item] of this._itemsByElement) {
|
||||
if (element == aElement) {
|
||||
return count;
|
||||
}
|
||||
if (!item.ignored) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the number of cached items associated with a specified element.
|
||||
*
|
||||
* @return number
|
||||
* The number of key/value pairs in the corresponding map.
|
||||
*/
|
||||
SourceResults.size =
|
||||
LineResults.size = function() {
|
||||
let count = 0;
|
||||
for (let [, item] of this._itemsByElement) {
|
||||
if (!item.ignored) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
};
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -0,0 +1,416 @@
|
|||
/* 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";
|
||||
|
||||
/**
|
||||
* Functions handling the traces UI.
|
||||
*/
|
||||
function TracerView(DebuggerController, DebuggerView) {
|
||||
this._selectedItem = null;
|
||||
this._matchingItems = null;
|
||||
this.widget = null;
|
||||
|
||||
this.Tracer = DebuggerController.Tracer;
|
||||
this.DebuggerView = DebuggerView;
|
||||
|
||||
this._highlightItem = this._highlightItem.bind(this);
|
||||
this._isNotSelectedItem = this._isNotSelectedItem.bind(this);
|
||||
|
||||
this._unhighlightMatchingItems =
|
||||
DevToolsUtils.makeInfallible(this._unhighlightMatchingItems.bind(this));
|
||||
this._onToggleTracing =
|
||||
DevToolsUtils.makeInfallible(this._onToggleTracing.bind(this));
|
||||
this._onStartTracing =
|
||||
DevToolsUtils.makeInfallible(this._onStartTracing.bind(this));
|
||||
this._onClear =
|
||||
DevToolsUtils.makeInfallible(this._onClear.bind(this));
|
||||
this._onSelect =
|
||||
DevToolsUtils.makeInfallible(this._onSelect.bind(this));
|
||||
this._onMouseOver =
|
||||
DevToolsUtils.makeInfallible(this._onMouseOver.bind(this));
|
||||
this._onSearch =
|
||||
DevToolsUtils.makeInfallible(this._onSearch.bind(this));
|
||||
}
|
||||
|
||||
TracerView.MAX_TRACES = 200;
|
||||
|
||||
TracerView.prototype = Heritage.extend(WidgetMethods, {
|
||||
/**
|
||||
* Initialization function, called when the debugger is started.
|
||||
*/
|
||||
initialize: function() {
|
||||
dumpn("Initializing the TracerView");
|
||||
|
||||
this._traceButton = document.getElementById("trace");
|
||||
this._tracerTab = document.getElementById("tracer-tab");
|
||||
|
||||
// Remove tracer related elements from the dom and tear everything down if
|
||||
// the tracer isn't enabled.
|
||||
if (!Prefs.tracerEnabled) {
|
||||
this._traceButton.remove();
|
||||
this._traceButton = null;
|
||||
this._tracerTab.remove();
|
||||
this._tracerTab = null;
|
||||
return;
|
||||
}
|
||||
|
||||
this.widget = new FastListWidget(document.getElementById("tracer-traces"));
|
||||
this._traceButton.removeAttribute("hidden");
|
||||
this._tracerTab.removeAttribute("hidden");
|
||||
|
||||
this._search = document.getElementById("tracer-search");
|
||||
this._template = document.getElementsByClassName("trace-item-template")[0];
|
||||
this._templateItem = this._template.getElementsByClassName("trace-item")[0];
|
||||
this._templateTypeIcon = this._template.getElementsByClassName("trace-type")[0];
|
||||
this._templateNameNode = this._template.getElementsByClassName("trace-name")[0];
|
||||
|
||||
this.widget.addEventListener("select", this._onSelect, false);
|
||||
this.widget.addEventListener("mouseover", this._onMouseOver, false);
|
||||
this.widget.addEventListener("mouseout", this._unhighlightMatchingItems, false);
|
||||
this._search.addEventListener("input", this._onSearch, false);
|
||||
|
||||
this._startTooltip = L10N.getStr("startTracingTooltip");
|
||||
this._stopTooltip = L10N.getStr("stopTracingTooltip");
|
||||
this._tracingNotStartedString = L10N.getStr("tracingNotStartedText");
|
||||
this._noFunctionCallsString = L10N.getStr("noFunctionCallsText");
|
||||
|
||||
this._traceButton.setAttribute("tooltiptext", this._startTooltip);
|
||||
this.emptyText = this._tracingNotStartedString;
|
||||
|
||||
this._addCommands();
|
||||
},
|
||||
|
||||
/**
|
||||
* Destruction function, called when the debugger is closed.
|
||||
*/
|
||||
destroy: function() {
|
||||
dumpn("Destroying the TracerView");
|
||||
|
||||
if (!this.widget) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.widget.removeEventListener("select", this._onSelect, false);
|
||||
this.widget.removeEventListener("mouseover", this._onMouseOver, false);
|
||||
this.widget.removeEventListener("mouseout", this._unhighlightMatchingItems, false);
|
||||
this._search.removeEventListener("input", this._onSearch, false);
|
||||
},
|
||||
|
||||
/**
|
||||
* Add commands that XUL can fire.
|
||||
*/
|
||||
_addCommands: function() {
|
||||
XULUtils.addCommands(document.getElementById('debuggerCommands'), {
|
||||
toggleTracing: () => this._onToggleTracing(),
|
||||
startTracing: () => this._onStartTracing(),
|
||||
clearTraces: () => this._onClear()
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Function invoked by the "toggleTracing" command to switch the tracer state.
|
||||
*/
|
||||
_onToggleTracing: function() {
|
||||
if (this.Tracer.tracing) {
|
||||
this._onStopTracing();
|
||||
} else {
|
||||
this._onStartTracing();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Function invoked either by the "startTracing" command or by
|
||||
* _onToggleTracing to start execution tracing in the backend.
|
||||
*
|
||||
* @return object
|
||||
* A promise resolved once the tracing has successfully started.
|
||||
*/
|
||||
_onStartTracing: function() {
|
||||
this._traceButton.setAttribute("checked", true);
|
||||
this._traceButton.setAttribute("tooltiptext", this._stopTooltip);
|
||||
|
||||
this.empty();
|
||||
this.emptyText = this._noFunctionCallsString;
|
||||
|
||||
let deferred = promise.defer();
|
||||
this.Tracer.startTracing(deferred.resolve);
|
||||
return deferred.promise;
|
||||
},
|
||||
|
||||
/**
|
||||
* Function invoked by _onToggleTracing to stop execution tracing in the
|
||||
* backend.
|
||||
*
|
||||
* @return object
|
||||
* A promise resolved once the tracing has successfully stopped.
|
||||
*/
|
||||
_onStopTracing: function() {
|
||||
this._traceButton.removeAttribute("checked");
|
||||
this._traceButton.setAttribute("tooltiptext", this._startTooltip);
|
||||
|
||||
this.emptyText = this._tracingNotStartedString;
|
||||
|
||||
let deferred = promise.defer();
|
||||
this.Tracer.stopTracing(deferred.resolve);
|
||||
return deferred.promise;
|
||||
},
|
||||
|
||||
/**
|
||||
* Function invoked by the "clearTraces" command to empty the traces pane.
|
||||
*/
|
||||
_onClear: function() {
|
||||
this.empty();
|
||||
},
|
||||
|
||||
/**
|
||||
* Populate the given parent scope with the variable with the provided name
|
||||
* and value.
|
||||
*
|
||||
* @param String aName
|
||||
* The name of the variable.
|
||||
* @param Object aParent
|
||||
* The parent scope.
|
||||
* @param Object aValue
|
||||
* The value of the variable.
|
||||
*/
|
||||
_populateVariable: function(aName, aParent, aValue) {
|
||||
let item = aParent.addItem(aName, { value: aValue });
|
||||
|
||||
if (aValue) {
|
||||
let wrappedValue = new this.Tracer.WrappedObject(aValue);
|
||||
this.DebuggerView.Variables.controller.populate(item, wrappedValue);
|
||||
item.expand();
|
||||
item.twisty = false;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Handler for the widget's "select" event. Displays parameters, exception, or
|
||||
* return value depending on whether the selected trace is a call, throw, or
|
||||
* return respectively.
|
||||
*
|
||||
* @param Object traceItem
|
||||
* The selected trace item.
|
||||
*/
|
||||
_onSelect: function _onSelect({ detail: traceItem }) {
|
||||
if (!traceItem) {
|
||||
return;
|
||||
}
|
||||
|
||||
const data = traceItem.attachment.trace;
|
||||
const { location: { url, line } } = data;
|
||||
this.DebuggerView.setEditorLocation(
|
||||
this.DebuggerView.Sources.getActorForLocation({ url }),
|
||||
line,
|
||||
{ noDebug: true }
|
||||
);
|
||||
|
||||
this.DebuggerView.Variables.empty();
|
||||
const scope = this.DebuggerView.Variables.addScope();
|
||||
|
||||
if (data.type == "call") {
|
||||
const params = DevToolsUtils.zip(data.parameterNames, data.arguments);
|
||||
for (let [name, val] of params) {
|
||||
if (val === undefined) {
|
||||
scope.addItem(name, { value: "<value not available>" });
|
||||
} else {
|
||||
this._populateVariable(name, scope, val);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const varName = "<" + (data.type == "throw" ? "exception" : data.type) + ">";
|
||||
this._populateVariable(varName, scope, data.returnVal);
|
||||
}
|
||||
|
||||
scope.expand();
|
||||
this.DebuggerView.showInstrumentsPane();
|
||||
},
|
||||
|
||||
/**
|
||||
* Add the hover frame enter/exit highlighting to a given item.
|
||||
*/
|
||||
_highlightItem: function(aItem) {
|
||||
if (!aItem || !aItem.target) {
|
||||
return;
|
||||
}
|
||||
const trace = aItem.target.querySelector(".trace-item");
|
||||
trace.classList.add("selected-matching");
|
||||
},
|
||||
|
||||
/**
|
||||
* Remove the hover frame enter/exit highlighting to a given item.
|
||||
*/
|
||||
_unhighlightItem: function(aItem) {
|
||||
if (!aItem || !aItem.target) {
|
||||
return;
|
||||
}
|
||||
const match = aItem.target.querySelector(".selected-matching");
|
||||
if (match) {
|
||||
match.classList.remove("selected-matching");
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Remove the frame enter/exit pair highlighting we do when hovering.
|
||||
*/
|
||||
_unhighlightMatchingItems: function() {
|
||||
if (this._matchingItems) {
|
||||
this._matchingItems.forEach(this._unhighlightItem);
|
||||
this._matchingItems = null;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns true if the given item is not the selected item.
|
||||
*/
|
||||
_isNotSelectedItem: function(aItem) {
|
||||
return aItem !== this.selectedItem;
|
||||
},
|
||||
|
||||
/**
|
||||
* Highlight the frame enter/exit pair of items for the given item.
|
||||
*/
|
||||
_highlightMatchingItems: function(aItem) {
|
||||
const frameId = aItem.attachment.trace.frameId;
|
||||
const predicate = e => e.attachment.trace.frameId == frameId;
|
||||
|
||||
this._unhighlightMatchingItems();
|
||||
this._matchingItems = this.items.filter(predicate);
|
||||
this._matchingItems
|
||||
.filter(this._isNotSelectedItem)
|
||||
.forEach(this._highlightItem);
|
||||
},
|
||||
|
||||
/**
|
||||
* Listener for the mouseover event.
|
||||
*/
|
||||
_onMouseOver: function({ target }) {
|
||||
const traceItem = this.getItemForElement(target);
|
||||
if (traceItem) {
|
||||
this._highlightMatchingItems(traceItem);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Listener for typing in the search box.
|
||||
*/
|
||||
_onSearch: function() {
|
||||
const query = this._search.value.trim().toLowerCase();
|
||||
const predicate = name => name.toLowerCase().contains(query);
|
||||
this.filterContents(item => predicate(item.attachment.trace.name));
|
||||
},
|
||||
|
||||
/**
|
||||
* Select the traces tab in the sidebar.
|
||||
*/
|
||||
selectTab: function() {
|
||||
const tabs = this._tracerTab.parentElement;
|
||||
tabs.selectedIndex = Array.indexOf(tabs.children, this._tracerTab);
|
||||
},
|
||||
|
||||
/**
|
||||
* Commit all staged items to the widget. Overridden so that we can call
|
||||
* |FastListWidget.prototype.flush|.
|
||||
*/
|
||||
commit: function() {
|
||||
WidgetMethods.commit.call(this);
|
||||
// TODO: Accessing non-standard widget properties. Figure out what's the
|
||||
// best way to expose such things. Bug 895514.
|
||||
this.widget.flush();
|
||||
},
|
||||
|
||||
/**
|
||||
* Adds the trace record provided as an argument to the view.
|
||||
*
|
||||
* @param object aTrace
|
||||
* The trace record coming from the tracer actor.
|
||||
*/
|
||||
addTrace: function(aTrace) {
|
||||
// Create the element node for the trace item.
|
||||
let view = this._createView(aTrace);
|
||||
|
||||
// Append a source item to this container.
|
||||
this.push([view], {
|
||||
staged: true,
|
||||
attachment: {
|
||||
trace: aTrace
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Customization function for creating an item's UI.
|
||||
*
|
||||
* @return nsIDOMNode
|
||||
* The network request view.
|
||||
*/
|
||||
_createView: function(aTrace) {
|
||||
let { type, name, location, blackBoxed, depth, frameId } = aTrace;
|
||||
let { parameterNames, returnVal, arguments: args } = aTrace;
|
||||
let fragment = document.createDocumentFragment();
|
||||
|
||||
this._templateItem.classList.toggle("black-boxed", blackBoxed);
|
||||
this._templateItem.setAttribute("tooltiptext", SourceUtils.trimUrl(location.url));
|
||||
this._templateItem.style.MozPaddingStart = depth + "em";
|
||||
|
||||
const TYPES = ["call", "yield", "return", "throw"];
|
||||
for (let t of TYPES) {
|
||||
this._templateTypeIcon.classList.toggle("trace-" + t, t == type);
|
||||
}
|
||||
this._templateTypeIcon.setAttribute("value", {
|
||||
call: "\u2192",
|
||||
yield: "Y",
|
||||
return: "\u2190",
|
||||
throw: "E",
|
||||
terminated: "TERMINATED"
|
||||
}[type]);
|
||||
|
||||
this._templateNameNode.setAttribute("value", name);
|
||||
|
||||
// All extra syntax and parameter nodes added.
|
||||
const addedNodes = [];
|
||||
|
||||
if (parameterNames) {
|
||||
const syntax = (p) => {
|
||||
const el = document.createElement("label");
|
||||
el.setAttribute("value", p);
|
||||
el.classList.add("trace-syntax");
|
||||
el.classList.add("plain");
|
||||
addedNodes.push(el);
|
||||
return el;
|
||||
};
|
||||
|
||||
this._templateItem.appendChild(syntax("("));
|
||||
|
||||
for (let i = 0, n = parameterNames.length; i < n; i++) {
|
||||
let param = document.createElement("label");
|
||||
param.setAttribute("value", parameterNames[i]);
|
||||
param.classList.add("trace-param");
|
||||
param.classList.add("plain");
|
||||
addedNodes.push(param);
|
||||
this._templateItem.appendChild(param);
|
||||
|
||||
if (i + 1 !== n) {
|
||||
this._templateItem.appendChild(syntax(", "));
|
||||
}
|
||||
}
|
||||
|
||||
this._templateItem.appendChild(syntax(")"));
|
||||
}
|
||||
|
||||
// Flatten the DOM by removing one redundant box (the template container).
|
||||
for (let node of this._template.childNodes) {
|
||||
fragment.appendChild(node.cloneNode(true));
|
||||
}
|
||||
|
||||
// Remove any added nodes from the template.
|
||||
for (let node of addedNodes) {
|
||||
this._templateItem.removeChild(node);
|
||||
}
|
||||
|
||||
return fragment;
|
||||
}
|
||||
});
|
||||
|
||||
DebuggerView.Tracer = new TracerView(DebuggerController, DebuggerView);
|
|
@ -0,0 +1,300 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* Functions handling the variables bubble UI.
|
||||
*/
|
||||
function VariableBubbleView(DebuggerController, DebuggerView) {
|
||||
dumpn("VariableBubbleView was instantiated");
|
||||
|
||||
this.StackFrames = DebuggerController.StackFrames;
|
||||
this.Parser = DebuggerController.Parser;
|
||||
this.DebuggerView = DebuggerView;
|
||||
|
||||
this._onMouseMove = this._onMouseMove.bind(this);
|
||||
this._onMouseOut = this._onMouseOut.bind(this);
|
||||
this._onPopupHiding = this._onPopupHiding.bind(this);
|
||||
}
|
||||
|
||||
VariableBubbleView.prototype = {
|
||||
/**
|
||||
* Initialization function, called when the debugger is started.
|
||||
*/
|
||||
initialize: function() {
|
||||
dumpn("Initializing the VariableBubbleView");
|
||||
|
||||
this._toolbox = DebuggerController._toolbox;
|
||||
this._editorContainer = document.getElementById("editor");
|
||||
this._editorContainer.addEventListener("mousemove", this._onMouseMove, false);
|
||||
this._editorContainer.addEventListener("mouseout", this._onMouseOut, false);
|
||||
|
||||
this._tooltip = new Tooltip(document, {
|
||||
closeOnEvents: [{
|
||||
emitter: this._toolbox,
|
||||
event: "select"
|
||||
}, {
|
||||
emitter: this._editorContainer,
|
||||
event: "scroll",
|
||||
useCapture: true
|
||||
}]
|
||||
});
|
||||
this._tooltip.defaultPosition = EDITOR_VARIABLE_POPUP_POSITION;
|
||||
this._tooltip.defaultShowDelay = EDITOR_VARIABLE_HOVER_DELAY;
|
||||
this._tooltip.panel.addEventListener("popuphiding", this._onPopupHiding);
|
||||
},
|
||||
|
||||
/**
|
||||
* Destruction function, called when the debugger is closed.
|
||||
*/
|
||||
destroy: function() {
|
||||
dumpn("Destroying the VariableBubbleView");
|
||||
|
||||
this._tooltip.panel.removeEventListener("popuphiding", this._onPopupHiding);
|
||||
this._editorContainer.removeEventListener("mousemove", this._onMouseMove, false);
|
||||
this._editorContainer.removeEventListener("mouseout", this._onMouseOut, false);
|
||||
},
|
||||
|
||||
/**
|
||||
* Specifies whether literals can be (redundantly) inspected in a popup.
|
||||
* This behavior is deprecated, but still tested in a few places.
|
||||
*/
|
||||
_ignoreLiterals: true,
|
||||
|
||||
/**
|
||||
* Searches for an identifier underneath the specified position in the
|
||||
* source editor, and if found, opens a VariablesView inspection popup.
|
||||
*
|
||||
* @param number x, y
|
||||
* The left/top coordinates where to look for an identifier.
|
||||
*/
|
||||
_findIdentifier: function(x, y) {
|
||||
let editor = this.DebuggerView.editor;
|
||||
|
||||
// Calculate the editor's line and column at the current x and y coords.
|
||||
let hoveredPos = editor.getPositionFromCoords({ left: x, top: y });
|
||||
let hoveredOffset = editor.getOffset(hoveredPos);
|
||||
let hoveredLine = hoveredPos.line;
|
||||
let hoveredColumn = hoveredPos.ch;
|
||||
|
||||
// A source contains multiple scripts. Find the start index of the script
|
||||
// containing the specified offset relative to its parent source.
|
||||
let contents = editor.getText();
|
||||
let location = this.DebuggerView.Sources.selectedValue;
|
||||
let parsedSource = this.Parser.get(contents, location);
|
||||
let scriptInfo = parsedSource.getScriptInfo(hoveredOffset);
|
||||
|
||||
// If the script length is negative, we're not hovering JS source code.
|
||||
if (scriptInfo.length == -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Using the script offset, determine the actual line and column inside the
|
||||
// script, to use when finding identifiers.
|
||||
let scriptStart = editor.getPosition(scriptInfo.start);
|
||||
let scriptLineOffset = scriptStart.line;
|
||||
let scriptColumnOffset = (hoveredLine == scriptStart.line ? scriptStart.ch : 0);
|
||||
|
||||
let scriptLine = hoveredLine - scriptLineOffset;
|
||||
let scriptColumn = hoveredColumn - scriptColumnOffset;
|
||||
let identifierInfo = parsedSource.getIdentifierAt({
|
||||
line: scriptLine + 1,
|
||||
column: scriptColumn,
|
||||
scriptIndex: scriptInfo.index,
|
||||
ignoreLiterals: this._ignoreLiterals
|
||||
});
|
||||
|
||||
// If the info is null, we're not hovering any identifier.
|
||||
if (!identifierInfo) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Transform the line and column relative to the parsed script back
|
||||
// to the context of the parent source.
|
||||
let { start: identifierStart, end: identifierEnd } = identifierInfo.location;
|
||||
let identifierCoords = {
|
||||
line: identifierStart.line + scriptLineOffset,
|
||||
column: identifierStart.column + scriptColumnOffset,
|
||||
length: identifierEnd.column - identifierStart.column
|
||||
};
|
||||
|
||||
// Evaluate the identifier in the current stack frame and show the
|
||||
// results in a VariablesView inspection popup.
|
||||
this.StackFrames.evaluate(identifierInfo.evalString)
|
||||
.then(frameFinished => {
|
||||
if ("return" in frameFinished) {
|
||||
this.showContents({
|
||||
coords: identifierCoords,
|
||||
evalPrefix: identifierInfo.evalString,
|
||||
objectActor: frameFinished.return
|
||||
});
|
||||
} else {
|
||||
let msg = "Evaluation has thrown for: " + identifierInfo.evalString;
|
||||
console.warn(msg);
|
||||
dumpn(msg);
|
||||
}
|
||||
})
|
||||
.then(null, err => {
|
||||
let msg = "Couldn't evaluate: " + err.message;
|
||||
console.error(msg);
|
||||
dumpn(msg);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Shows an inspection popup for a specified object actor grip.
|
||||
*
|
||||
* @param string object
|
||||
* An object containing the following properties:
|
||||
* - coords: the inspected identifier coordinates in the editor,
|
||||
* containing the { line, column, length } properties.
|
||||
* - evalPrefix: a prefix for the variables view evaluation macros.
|
||||
* - objectActor: the value grip for the object actor.
|
||||
*/
|
||||
showContents: function({ coords, evalPrefix, objectActor }) {
|
||||
let editor = this.DebuggerView.editor;
|
||||
let { line, column, length } = coords;
|
||||
|
||||
// Highlight the function found at the mouse position.
|
||||
this._markedText = editor.markText(
|
||||
{ line: line - 1, ch: column },
|
||||
{ line: line - 1, ch: column + length });
|
||||
|
||||
// If the grip represents a primitive value, use a more lightweight
|
||||
// machinery to display it.
|
||||
if (VariablesView.isPrimitive({ value: objectActor })) {
|
||||
let className = VariablesView.getClass(objectActor);
|
||||
let textContent = VariablesView.getString(objectActor);
|
||||
this._tooltip.setTextContent({
|
||||
messages: [textContent],
|
||||
messagesClass: className,
|
||||
containerClass: "plain"
|
||||
}, [{
|
||||
label: L10N.getStr('addWatchExpressionButton'),
|
||||
className: "dbg-expression-button",
|
||||
command: () => {
|
||||
this.DebuggerView.VariableBubble.hideContents();
|
||||
this.DebuggerView.WatchExpressions.addExpression(evalPrefix, true);
|
||||
}
|
||||
}]);
|
||||
} else {
|
||||
this._tooltip.setVariableContent(objectActor, {
|
||||
searchPlaceholder: L10N.getStr("emptyPropertiesFilterText"),
|
||||
searchEnabled: Prefs.variablesSearchboxVisible,
|
||||
eval: (variable, value) => {
|
||||
let string = variable.evaluationMacro(variable, value);
|
||||
this.StackFrames.evaluate(string);
|
||||
this.DebuggerView.VariableBubble.hideContents();
|
||||
}
|
||||
}, {
|
||||
getEnvironmentClient: aObject => gThreadClient.environment(aObject),
|
||||
getObjectClient: aObject => gThreadClient.pauseGrip(aObject),
|
||||
simpleValueEvalMacro: this._getSimpleValueEvalMacro(evalPrefix),
|
||||
getterOrSetterEvalMacro: this._getGetterOrSetterEvalMacro(evalPrefix),
|
||||
overrideValueEvalMacro: this._getOverrideValueEvalMacro(evalPrefix)
|
||||
}, {
|
||||
fetched: (aEvent, aType) => {
|
||||
if (aType == "properties") {
|
||||
window.emit(EVENTS.FETCHED_BUBBLE_PROPERTIES);
|
||||
}
|
||||
}
|
||||
}, [{
|
||||
label: L10N.getStr("addWatchExpressionButton"),
|
||||
className: "dbg-expression-button",
|
||||
command: () => {
|
||||
this.DebuggerView.VariableBubble.hideContents();
|
||||
this.DebuggerView.WatchExpressions.addExpression(evalPrefix, true);
|
||||
}
|
||||
}], this._toolbox);
|
||||
}
|
||||
|
||||
this._tooltip.show(this._markedText.anchor);
|
||||
},
|
||||
|
||||
/**
|
||||
* Hides the inspection popup.
|
||||
*/
|
||||
hideContents: function() {
|
||||
clearNamedTimeout("editor-mouse-move");
|
||||
this._tooltip.hide();
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks whether the inspection popup is shown.
|
||||
*
|
||||
* @return boolean
|
||||
* True if the panel is shown or showing, false otherwise.
|
||||
*/
|
||||
contentsShown: function() {
|
||||
return this._tooltip.isShown();
|
||||
},
|
||||
|
||||
/**
|
||||
* Functions for getting customized variables view evaluation macros.
|
||||
*
|
||||
* @param string aPrefix
|
||||
* See the corresponding VariablesView.* functions.
|
||||
*/
|
||||
_getSimpleValueEvalMacro: function(aPrefix) {
|
||||
return (item, string) =>
|
||||
VariablesView.simpleValueEvalMacro(item, string, aPrefix);
|
||||
},
|
||||
_getGetterOrSetterEvalMacro: function(aPrefix) {
|
||||
return (item, string) =>
|
||||
VariablesView.getterOrSetterEvalMacro(item, string, aPrefix);
|
||||
},
|
||||
_getOverrideValueEvalMacro: function(aPrefix) {
|
||||
return (item, string) =>
|
||||
VariablesView.overrideValueEvalMacro(item, string, aPrefix);
|
||||
},
|
||||
|
||||
/**
|
||||
* The mousemove listener for the source editor.
|
||||
*/
|
||||
_onMouseMove: function(e) {
|
||||
// Prevent the variable inspection popup from showing when the thread client
|
||||
// is not paused, or while a popup is already visible, or when the user tries
|
||||
// to select text in the editor.
|
||||
let isResumed = gThreadClient && gThreadClient.state != "paused";
|
||||
let isSelecting = this.DebuggerView.editor.somethingSelected() && e.buttons > 0;
|
||||
let isPopupVisible = !this._tooltip.isHidden();
|
||||
if (isResumed || isSelecting || isPopupVisible) {
|
||||
clearNamedTimeout("editor-mouse-move");
|
||||
return;
|
||||
}
|
||||
// Allow events to settle down first. If the mouse hovers over
|
||||
// a certain point in the editor long enough, try showing a variable bubble.
|
||||
setNamedTimeout("editor-mouse-move",
|
||||
EDITOR_VARIABLE_HOVER_DELAY, () => this._findIdentifier(e.clientX, e.clientY));
|
||||
},
|
||||
|
||||
/**
|
||||
* The mouseout listener for the source editor container node.
|
||||
*/
|
||||
_onMouseOut: function() {
|
||||
clearNamedTimeout("editor-mouse-move");
|
||||
},
|
||||
|
||||
/**
|
||||
* Listener handling the popup hiding event.
|
||||
*/
|
||||
_onPopupHiding: function({ target }) {
|
||||
if (this._tooltip.panel != target) {
|
||||
return;
|
||||
}
|
||||
if (this._markedText) {
|
||||
this._markedText.clear();
|
||||
this._markedText = null;
|
||||
}
|
||||
if (!this._tooltip.isEmpty()) {
|
||||
this._tooltip.empty();
|
||||
}
|
||||
},
|
||||
|
||||
_editorContainer: null,
|
||||
_markedText: null,
|
||||
_tooltip: null
|
||||
};
|
||||
|
||||
DebuggerView.VariableBubble = new VariableBubbleView(DebuggerController, DebuggerView);
|
|
@ -0,0 +1,295 @@
|
|||
/* 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";
|
||||
|
||||
/**
|
||||
* Functions handling the watch expressions UI.
|
||||
*/
|
||||
function WatchExpressionsView(DebuggerController, DebuggerView) {
|
||||
dumpn("WatchExpressionsView was instantiated");
|
||||
|
||||
this.StackFrames = DebuggerController.StackFrames;
|
||||
this.DebuggerView = DebuggerView;
|
||||
|
||||
this.switchExpression = this.switchExpression.bind(this);
|
||||
this.deleteExpression = this.deleteExpression.bind(this);
|
||||
this._createItemView = this._createItemView.bind(this);
|
||||
this._onClick = this._onClick.bind(this);
|
||||
this._onClose = this._onClose.bind(this);
|
||||
this._onBlur = this._onBlur.bind(this);
|
||||
this._onKeyPress = this._onKeyPress.bind(this);
|
||||
}
|
||||
|
||||
WatchExpressionsView.prototype = Heritage.extend(WidgetMethods, {
|
||||
/**
|
||||
* Initialization function, called when the debugger is started.
|
||||
*/
|
||||
initialize: function() {
|
||||
dumpn("Initializing the WatchExpressionsView");
|
||||
|
||||
this.widget = new SimpleListWidget(document.getElementById("expressions"));
|
||||
this.widget.setAttribute("context", "debuggerWatchExpressionsContextMenu");
|
||||
this.widget.addEventListener("click", this._onClick, false);
|
||||
|
||||
this.headerText = L10N.getStr("addWatchExpressionText");
|
||||
this._addCommands();
|
||||
},
|
||||
|
||||
/**
|
||||
* Destruction function, called when the debugger is closed.
|
||||
*/
|
||||
destroy: function() {
|
||||
dumpn("Destroying the WatchExpressionsView");
|
||||
|
||||
this.widget.removeEventListener("click", this._onClick, false);
|
||||
},
|
||||
|
||||
/**
|
||||
* Add commands that XUL can fire.
|
||||
*/
|
||||
_addCommands: function() {
|
||||
XULUtils.addCommands(document.getElementById('debuggerCommands'), {
|
||||
addWatchExpressionCommand: () => this._onCmdAddExpression(),
|
||||
removeAllWatchExpressionsCommand: () => this._onCmdRemoveAllExpressions()
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Adds a watch expression in this container.
|
||||
*
|
||||
* @param string aExpression [optional]
|
||||
* An optional initial watch expression text.
|
||||
* @param boolean aSkipUserInput [optional]
|
||||
* Pass true to avoid waiting for additional user input
|
||||
* on the watch expression.
|
||||
*/
|
||||
addExpression: function(aExpression = "", aSkipUserInput = false) {
|
||||
// Watch expressions are UI elements which benefit from visible panes.
|
||||
this.DebuggerView.showInstrumentsPane();
|
||||
|
||||
// Create the element node for the watch expression item.
|
||||
let itemView = this._createItemView(aExpression);
|
||||
|
||||
// Append a watch expression item to this container.
|
||||
let expressionItem = this.push([itemView.container], {
|
||||
index: 0, /* specifies on which position should the item be appended */
|
||||
attachment: {
|
||||
view: itemView,
|
||||
initialExpression: aExpression,
|
||||
currentExpression: "",
|
||||
}
|
||||
});
|
||||
|
||||
// Automatically focus the new watch expression input
|
||||
// if additional user input is desired.
|
||||
if (!aSkipUserInput) {
|
||||
expressionItem.attachment.view.inputNode.select();
|
||||
expressionItem.attachment.view.inputNode.focus();
|
||||
this.DebuggerView.Variables.parentNode.scrollTop = 0;
|
||||
}
|
||||
// Otherwise, add and evaluate the new watch expression immediately.
|
||||
else {
|
||||
this.toggleContents(false);
|
||||
this._onBlur({ target: expressionItem.attachment.view.inputNode });
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Changes the watch expression corresponding to the specified variable item.
|
||||
* This function is called whenever a watch expression's code is edited in
|
||||
* the variables view container.
|
||||
*
|
||||
* @param Variable aVar
|
||||
* The variable representing the watch expression evaluation.
|
||||
* @param string aExpression
|
||||
* The new watch expression text.
|
||||
*/
|
||||
switchExpression: function(aVar, aExpression) {
|
||||
let expressionItem =
|
||||
[i for (i of this) if (i.attachment.currentExpression == aVar.name)][0];
|
||||
|
||||
// Remove the watch expression if it's going to be empty or a duplicate.
|
||||
if (!aExpression || this.getAllStrings().indexOf(aExpression) != -1) {
|
||||
this.deleteExpression(aVar);
|
||||
return;
|
||||
}
|
||||
|
||||
// Save the watch expression code string.
|
||||
expressionItem.attachment.currentExpression = aExpression;
|
||||
expressionItem.attachment.view.inputNode.value = aExpression;
|
||||
|
||||
// Synchronize with the controller's watch expressions store.
|
||||
this.StackFrames.syncWatchExpressions();
|
||||
},
|
||||
|
||||
/**
|
||||
* Removes the watch expression corresponding to the specified variable item.
|
||||
* This function is called whenever a watch expression's value is edited in
|
||||
* the variables view container.
|
||||
*
|
||||
* @param Variable aVar
|
||||
* The variable representing the watch expression evaluation.
|
||||
*/
|
||||
deleteExpression: function(aVar) {
|
||||
let expressionItem =
|
||||
[i for (i of this) if (i.attachment.currentExpression == aVar.name)][0];
|
||||
|
||||
// Remove the watch expression.
|
||||
this.remove(expressionItem);
|
||||
|
||||
// Synchronize with the controller's watch expressions store.
|
||||
this.StackFrames.syncWatchExpressions();
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets the watch expression code string for an item in this container.
|
||||
*
|
||||
* @param number aIndex
|
||||
* The index used to identify the watch expression.
|
||||
* @return string
|
||||
* The watch expression code string.
|
||||
*/
|
||||
getString: function(aIndex) {
|
||||
return this.getItemAtIndex(aIndex).attachment.currentExpression;
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets the watch expressions code strings for all items in this container.
|
||||
*
|
||||
* @return array
|
||||
* The watch expressions code strings.
|
||||
*/
|
||||
getAllStrings: function() {
|
||||
return this.items.map(e => e.attachment.currentExpression);
|
||||
},
|
||||
|
||||
/**
|
||||
* Customization function for creating an item's UI.
|
||||
*
|
||||
* @param string aExpression
|
||||
* The watch expression string.
|
||||
*/
|
||||
_createItemView: function(aExpression) {
|
||||
let container = document.createElement("hbox");
|
||||
container.className = "list-widget-item dbg-expression";
|
||||
container.setAttribute("align", "center");
|
||||
|
||||
let arrowNode = document.createElement("hbox");
|
||||
arrowNode.className = "dbg-expression-arrow";
|
||||
|
||||
let inputNode = document.createElement("textbox");
|
||||
inputNode.className = "plain dbg-expression-input devtools-monospace";
|
||||
inputNode.setAttribute("value", aExpression);
|
||||
inputNode.setAttribute("flex", "1");
|
||||
|
||||
let closeNode = document.createElement("toolbarbutton");
|
||||
closeNode.className = "plain variables-view-delete";
|
||||
|
||||
closeNode.addEventListener("click", this._onClose, false);
|
||||
inputNode.addEventListener("blur", this._onBlur, false);
|
||||
inputNode.addEventListener("keypress", this._onKeyPress, false);
|
||||
|
||||
container.appendChild(arrowNode);
|
||||
container.appendChild(inputNode);
|
||||
container.appendChild(closeNode);
|
||||
|
||||
return {
|
||||
container: container,
|
||||
arrowNode: arrowNode,
|
||||
inputNode: inputNode,
|
||||
closeNode: closeNode
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Called when the add watch expression key sequence was pressed.
|
||||
*/
|
||||
_onCmdAddExpression: function(aText) {
|
||||
// Only add a new expression if there's no pending input.
|
||||
if (this.getAllStrings().indexOf("") == -1) {
|
||||
this.addExpression(aText || this.DebuggerView.editor.getSelection());
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Called when the remove all watch expressions key sequence was pressed.
|
||||
*/
|
||||
_onCmdRemoveAllExpressions: function() {
|
||||
// Empty the view of all the watch expressions and clear the cache.
|
||||
this.empty();
|
||||
|
||||
// Synchronize with the controller's watch expressions store.
|
||||
this.StackFrames.syncWatchExpressions();
|
||||
},
|
||||
|
||||
/**
|
||||
* The click listener for this container.
|
||||
*/
|
||||
_onClick: function(e) {
|
||||
if (e.button != 0) {
|
||||
// Only allow left-click to trigger this event.
|
||||
return;
|
||||
}
|
||||
let expressionItem = this.getItemForElement(e.target);
|
||||
if (!expressionItem) {
|
||||
// The container is empty or we didn't click on an actual item.
|
||||
this.addExpression();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* The click listener for a watch expression's close button.
|
||||
*/
|
||||
_onClose: function(e) {
|
||||
// Remove the watch expression.
|
||||
this.remove(this.getItemForElement(e.target));
|
||||
|
||||
// Synchronize with the controller's watch expressions store.
|
||||
this.StackFrames.syncWatchExpressions();
|
||||
|
||||
// Prevent clicking the expression element itself.
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
},
|
||||
|
||||
/**
|
||||
* The blur listener for a watch expression's textbox.
|
||||
*/
|
||||
_onBlur: function({ target: textbox }) {
|
||||
let expressionItem = this.getItemForElement(textbox);
|
||||
let oldExpression = expressionItem.attachment.currentExpression;
|
||||
let newExpression = textbox.value.trim();
|
||||
|
||||
// Remove the watch expression if it's empty.
|
||||
if (!newExpression) {
|
||||
this.remove(expressionItem);
|
||||
}
|
||||
// Remove the watch expression if it's a duplicate.
|
||||
else if (!oldExpression && this.getAllStrings().indexOf(newExpression) != -1) {
|
||||
this.remove(expressionItem);
|
||||
}
|
||||
// Expression is eligible.
|
||||
else {
|
||||
expressionItem.attachment.currentExpression = newExpression;
|
||||
}
|
||||
|
||||
// Synchronize with the controller's watch expressions store.
|
||||
this.StackFrames.syncWatchExpressions();
|
||||
},
|
||||
|
||||
/**
|
||||
* The keypress listener for a watch expression's textbox.
|
||||
*/
|
||||
_onKeyPress: function(e) {
|
||||
switch (e.keyCode) {
|
||||
case e.DOM_VK_RETURN:
|
||||
case e.DOM_VK_ESCAPE:
|
||||
e.stopPropagation();
|
||||
this.DebuggerView.editor.focus();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
DebuggerView.WatchExpressions = new WatchExpressionsView(DebuggerController,
|
||||
DebuggerView);
|
|
@ -71,7 +71,12 @@ browser.jar:
|
|||
content/browser/devtools/debugger-controller.js (debugger/debugger-controller.js)
|
||||
content/browser/devtools/debugger-view.js (debugger/debugger-view.js)
|
||||
content/browser/devtools/debugger-toolbar.js (debugger/debugger-toolbar.js)
|
||||
content/browser/devtools/debugger-panes.js (debugger/debugger-panes.js)
|
||||
content/browser/devtools/debugger/sources-view.js (debugger/views/sources-view.js)
|
||||
content/browser/devtools/debugger/variable-bubble-view.js (debugger/views/variable-bubble-view.js)
|
||||
content/browser/devtools/debugger/tracer-view.js (debugger/views/tracer-view.js)
|
||||
content/browser/devtools/debugger/watch-expressions-view.js (debugger/views/watch-expressions-view.js)
|
||||
content/browser/devtools/debugger/event-listeners-view.js (debugger/views/event-listeners-view.js)
|
||||
content/browser/devtools/debugger/global-search-view.js (debugger/views/global-search-view.js)
|
||||
content/browser/devtools/debugger/utils.js (debugger/utils.js)
|
||||
content/browser/devtools/shadereditor.xul (shadereditor/shadereditor.xul)
|
||||
content/browser/devtools/shadereditor.js (shadereditor/shadereditor.js)
|
||||
|
|
Загрузка…
Ссылка в новой задаче