gecko/browser/devtools/debugger/debugger-panes.js

1545 lines
47 KiB
JavaScript
Raw Normal View History

/* -*- 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";
/**
* Functions handling the stackframes UI.
*/
function StackFramesView() {
dumpn("StackFramesView was instantiated");
MenuContainer.call(this);
this._onClick = this._onClick.bind(this);
this._onScroll = this._onScroll.bind(this);
}
create({ constructor: StackFramesView, proto: MenuContainer.prototype }, {
/**
* Initialization function, called when the debugger is started.
*/
initialize: function DVSF_initialize() {
dumpn("Initializing the StackFramesView");
this._container = new StackList(document.getElementById("stackframes"));
this._container.emptyText = L10N.getStr("emptyStackText");
this._container.addEventListener("click", this._onClick, false);
this._container.addEventListener("scroll", this._onScroll, true);
window.addEventListener("resize", this._onScroll, true);
this._cache = new Map();
},
/**
* Destruction function, called when the debugger is closed.
*/
destroy: function DVSF_destroy() {
dumpn("Destroying the StackFramesView");
this._container.removeEventListener("click", this._onClick, true);
this._container.removeEventListener("scroll", this._onScroll, true);
window.removeEventListener("resize", this._onScroll, true);
},
/**
* Adds a frame in this stackframes container.
*
* @param string aFrameName
* Name to be displayed in the list.
* @param string aFrameDetails
* Details to be displayed in the list.
* @param number aDepth
* The frame depth specified by the debugger.
* @param object aOptions [optional]
* Additional options or flags supported by this operation:
* - attachment: any kind of primitive/object to attach
*/
addFrame:
function DVSF_addFrame(aFrameName, aFrameDetails, aDepth, aOptions = {}) {
// Stackframes are UI elements which benefit from visible panes.
DebuggerView.showPanesSoon();
// Append a stackframe item to this container.
let stackframeItem = this.push(aFrameName, aFrameDetails, {
forced: true,
unsorted: true,
relaxed: true,
attachment: aOptions.attachment
});
// Check if stackframe was already appended.
if (!stackframeItem) {
return;
}
let element = stackframeItem.target;
element.id = "stackframe-" + aDepth;
element.className = "dbg-stackframe list-item";
element.labelNode.className = "dbg-stackframe-name plain";
element.valueNode.className = "dbg-stackframe-details plain";
this._cache.set(aDepth, stackframeItem);
},
/**
* Highlights a frame in this stackframes container.
*
* @param number aDepth
* The frame depth specified by the debugger controller.
*/
highlightFrame: function DVSF_highlightFrame(aDepth) {
this._container.selectedItem = this._cache.get(aDepth).target;
},
/**
* Specifies if the active thread has more frames that need to be loaded.
*/
dirty: false,
/**
* The click listener for the stackframes container.
*/
_onClick: function DVSF__onClick(e) {
let item = this.getItemForElement(e.target);
if (item) {
// The container is not empty and we clicked on an actual item.
DebuggerController.StackFrames.selectFrame(item.attachment.depth);
}
},
/**
* The scroll listener for the stackframes container.
*/
_onScroll: function DVSF__onScroll() {
// Update the stackframes container only if we have to.
if (this.dirty) {
let list = this._container._list;
// If the stackframes container was scrolled past 95% of the height,
// load more content.
if (list.scrollTop >= (list.scrollHeight - list.clientHeight) * 0.95) {
DebuggerController.StackFrames.addMoreFrames();
this.dirty = false;
}
}
},
_cache: null
});
/**
* Utility functions for handling stackframes.
*/
let StackFrameUtils = {
/**
* Create a textual representation for the specified stack frame
* to display in the stack frame container.
*
* @param object aFrame
* The stack frame to label.
*/
getFrameTitle: function SFU_getFrameTitle(aFrame) {
if (aFrame.type == "call") {
return aFrame.calleeName || "(anonymous)";
}
return "(" + aFrame.type + ")";
}
};
/**
* Functions handling the breakpoints UI.
*/
function BreakpointsView() {
dumpn("BreakpointsView was instantiated");
MenuContainer.call(this);
this._createItemView = this._createItemView.bind(this);
this._onBreakpointRemoved = this._onBreakpointRemoved.bind(this);
this._onClick = this._onClick.bind(this);
this._onCheckboxClick = this._onCheckboxClick.bind(this);
}
create({ constructor: BreakpointsView, proto: MenuContainer.prototype }, {
/**
* Initialization function, called when the debugger is started.
*/
initialize: function DVB_initialize() {
dumpn("Initializing the BreakpointsView");
this._container = new StackList(document.getElementById("breakpoints"));
this._popupset = document.getElementById("debuggerPopupset");
this._container.emptyText = L10N.getStr("emptyBreakpointsText");
this._container.itemFactory = this._createItemView;
this._container.uniquenessQualifier = 2;
this._container.addEventListener("click", this._onClick, false);
this._cache = new Map();
},
/**
* Destruction function, called when the debugger is closed.
*/
destroy: function DVB_destroy() {
dumpn("Destroying the BreakpointsView");
this._container.removeEventListener("click", this._onClick, false);
},
/**
* Adds a breakpoint in this breakpoints container.
*
* @param string aLineInfo
* Line information (parent source etc.) to be displayed in the list.
* @param string aLineText
* Line text to be displayed in the list.
* @param string aSourceLocation
* The breakpoint source location specified by the debugger controller.
* @param number aLineNumber
* The breakpoint line number specified by the debugger controller.
* @parm string aId
* A breakpoint identifier specified by the debugger controller.
*/
addBreakpoint:
function DVB_addBreakpoint(aLineInfo, aLineText, aSourceLocation, aLineNumber, aId) {
// Append a breakpoint item to this container.
let breakpointItem = this.push(aLineInfo.trim(), aLineText.trim(), {
forced: true,
attachment: {
enabled: true,
sourceLocation: aSourceLocation,
lineNumber: aLineNumber
}
});
// Check if breakpoint was already appended.
if (!breakpointItem) {
this.enableBreakpoint(aSourceLocation, aLineNumber, { id: aId });
return;
}
let element = breakpointItem.target;
element.id = "breakpoint-" + aId;
element.className = "dbg-breakpoint list-item";
element.infoNode.className = "dbg-breakpoint-info plain";
element.textNode.className = "dbg-breakpoint-text plain";
element.setAttribute("contextmenu", this._createContextMenu(element));
breakpointItem.finalize = this._onBreakpointRemoved;
this._cache.set(this._key(aSourceLocation, aLineNumber), breakpointItem);
},
/**
* Removes a breakpoint from this breakpoints container.
*
* @param string aSourceLocation
* The breakpoint source location.
* @param number aLineNumber
* The breakpoint line number.
*/
removeBreakpoint: function DVB_removeBreakpoint(aSourceLocation, aLineNumber) {
let breakpointItem = this.getBreakpoint(aSourceLocation, aLineNumber);
if (breakpointItem) {
this.remove(breakpointItem);
this._cache.delete(this._key(aSourceLocation, aLineNumber));
}
},
/**
* Enables a breakpoint.
*
* @param string aSourceLocation
* The breakpoint source location.
* @param number aLineNumber
* The breakpoint line number.
* @param object aOptions [optional]
* Additional options or flags supported by this operation:
* - silent: pass true to not update the checkbox checked state;
* this is usually necessary when the checked state will
* be updated automatically (e.g: on a checkbox click).
* - callback: function to invoke once the breakpoint is disabled
* - id: a new id to be applied to the corresponding element node
* @return boolean
* True if breakpoint existed and was enabled, false otherwise.
*/
enableBreakpoint:
function DVB_enableBreakpoint(aSourceLocation, aLineNumber, aOptions = {}) {
let breakpointItem = this.getBreakpoint(aSourceLocation, aLineNumber);
if (breakpointItem) {
// Set a new id to the corresponding breakpoint element if required.
if (aOptions.id) {
breakpointItem.target.id = "breakpoint-" + aOptions.id;
}
// Update the checkbox state if necessary.
if (!aOptions.silent) {
breakpointItem.target.checkbox.setAttribute("checked", "true");
}
let { sourceLocation: url, lineNumber: line } = breakpointItem.attachment;
let breakpointLocation = { url: url, line: line };
DebuggerController.Breakpoints.addBreakpoint(breakpointLocation, aOptions.callback, {
noPaneUpdate: true
});
// Breakpoint is now enabled.
breakpointItem.attachment.enabled = true;
return true;
}
return false;
},
/**
* Disables a breakpoint.
*
* @param string aSourceLocation
* The breakpoint source location.
* @param number aLineNumber
* The breakpoint line number.
* @param object aOptions [optional]
* Additional options or flags supported by this operation:
* - silent: pass true to not update the checkbox checked state;
* this is usually necessary when the checked state will
* be updated automatically (e.g: on a checkbox click).
* - callback: function to invoke once the breakpoint is disabled
* @return boolean
* True if breakpoint existed and was disabled, false otherwise.
*/
disableBreakpoint:
function DVB_disableBreakpoint(aSourceLocation, aLineNumber, aOptions = {}) {
let breakpointItem = this.getBreakpoint(aSourceLocation, aLineNumber);
if (breakpointItem) {
// Update the checkbox state if necessary.
if (!aOptions.silent) {
breakpointItem.target.checkbox.removeAttribute("checked");
}
let { sourceLocation: url, lineNumber: line } = breakpointItem.attachment;
let breakpointClient = DebuggerController.Breakpoints.getBreakpoint(url, line);
DebuggerController.Breakpoints.removeBreakpoint(breakpointClient, aOptions.callback, {
noPaneUpdate: true
});
// Breakpoint is now disabled.
breakpointItem.attachment.enabled = false;
return true;
}
return false;
},
/**
* Highlights a breakpoint in this breakpoints container.
*
* @param string aSourceLocation
* The breakpoint source location.
* @param number aLineNumber
* The breakpoint line number.
*/
highlightBreakpoint: function DVB_highlightBreakpoint(aSourceLocation, aLineNumber) {
let breakpointItem = this.getBreakpoint(aSourceLocation, aLineNumber);
if (breakpointItem) {
this._container.selectedItem = breakpointItem.target;
} else {
this._container.selectedIndex = -1;
}
},
/**
* Unhighlights the current breakpoint in this breakpoints container.
*/
unhighlightBreakpoint: function DVB_highlightBreakpoint() {
this.highlightBreakpoint(null);
},
/**
* Checks whether a breakpoint with the specified source location and
* line number exists in this container, and returns the corresponding item
* if true, null otherwise.
*
* @param string aSourceLocation
* The breakpoint source location.
* @param number aLineNumber
* The breakpoint line number.
* @return object
* The corresponding item.
*/
getBreakpoint: function DVB_getBreakpoint(aSourceLocation, aLineNumber) {
return this._cache.get(this._key(aSourceLocation, aLineNumber));
},
/**
* Customization function for creating an item's UI.
*
* @param nsIDOMNode aElementNode
* The element associated with the displayed item.
* @param string aInfo
* The breakpoint's line info.
* @param string aText
* The breakpoint's line text.
* @param any aAttachment [optional]
* Some attached primitive/object.
*/
_createItemView: function DVB__createItemView(aElementNode, aInfo, aText, aAttachment) {
let checkbox = document.createElement("checkbox");
checkbox.setAttribute("checked", "true");
checkbox.addEventListener("click", this._onCheckboxClick, false);
let lineInfo = document.createElement("label");
lineInfo.setAttribute("value", aInfo);
lineInfo.setAttribute("crop", "end");
let lineText = document.createElement("label");
lineText.setAttribute("value", aText);
lineText.setAttribute("crop", "end");
lineText.setAttribute("tooltiptext",
aText.substr(0, BREAKPOINT_LINE_TOOLTIP_MAX_LENGTH));
let state = document.createElement("vbox");
state.className = "state";
state.appendChild(checkbox);
let content = document.createElement("vbox");
content.className = "content";
content.setAttribute("flex", "1");
content.appendChild(lineInfo);
content.appendChild(lineText);
aElementNode.appendChild(state);
aElementNode.appendChild(content);
aElementNode.checkbox = checkbox;
aElementNode.infoNode = lineInfo;
aElementNode.textNode = lineText;
},
/**
* Creates a context menu for a breakpoint element.
*
* @param nsIDOMNode aElementNode
* The element associated with the displayed breakpoint item.
* @return string
* The newly created menupopup id.
*/
_createContextMenu: function DVB__createContextMenu(aElementNode) {
let breakpointId = aElementNode.id;
let commandsetId = "breakpointCommandset-" + breakpointId;
let menupopupId = "breakpointMenupopup-" + breakpointId;
let commandset = document.createElement("commandset");
let menupopup = document.createElement("menupopup");
commandset.setAttribute("id", commandsetId);
menupopup.setAttribute("id", menupopupId);
createMenuItem.call(this, "deleteAll");
createMenuSeparator();
createMenuItem.call(this, "enableAll");
createMenuItem.call(this, "disableAll");
createMenuSeparator();
createMenuItem.call(this, "enableOthers");
createMenuItem.call(this, "disableOthers");
createMenuItem.call(this, "deleteOthers");
createMenuSeparator();
createMenuItem.call(this, "enableSelf", true);
createMenuItem.call(this, "disableSelf");
createMenuItem.call(this, "deleteSelf");
this._popupset.appendChild(menupopup);
document.documentElement.appendChild(commandset);
aElementNode.commandset = commandset;
aElementNode.menupopup = menupopup;
return menupopupId;
/**
* Creates a menu item specified by a name with the appropriate attributes
* (label and handler).
*
* @param string aName
* A global identifier for the menu item.
* @param boolean aHiddenFlag
* True if this menuitem should be hidden.
*/
function createMenuItem(aName, aHiddenFlag) {
let menuitem = document.createElement("menuitem");
let command = document.createElement("command");
let prefix = "bp-cMenu-";
let commandId = prefix + aName + "-" + breakpointId + "-command";
let menuitemId = prefix + aName + "-" + breakpointId + "-menuitem";
let label = L10N.getStr("breakpointMenuItem." + aName);
let func = this["_on" + aName.charAt(0).toUpperCase() + aName.slice(1)];
command.setAttribute("id", commandId);
command.setAttribute("label", label);
command.addEventListener("command", func.bind(this, aElementNode), false);
menuitem.setAttribute("id", menuitemId);
menuitem.setAttribute("command", commandId);
menuitem.setAttribute("hidden", aHiddenFlag);
commandset.appendChild(command);
menupopup.appendChild(menuitem);
aElementNode[aName] = { menuitem: menuitem, command: command };
}
/**
* Creates a simple menu separator element and appends it to the current
* menupopup hierarchy.
*/
function createMenuSeparator() {
let menuseparator = document.createElement("menuseparator");
menupopup.appendChild(menuseparator);
}
},
/**
* Destroys a context menu for a breakpoint.
*
* @param nsIDOMNode aElementNode
* The element associated with the displayed breakpoint item.
*/
_destroyContextMenu: function DVB__destroyContextMenu(aElementNode) {
let commandset = aElementNode.commandset;
let menupopup = aElementNode.menupopup;
commandset.parentNode.removeChild(commandset);
menupopup.parentNode.removeChild(menupopup);
},
/**
* Function called each time a breakpoint item is removed.
*/
_onBreakpointRemoved: function DVB__onBreakpointRemoved(aItem) {
this._destroyContextMenu(aItem.target);
},
/**
* The click listener for the breakpoints container.
*/
_onClick: function DVB__onClick(e) {
let breakpointItem = this.getItemForElement(e.target);
if (!breakpointItem) {
// The container is empty or we didn't click on an actual item.
return;
}
let { sourceLocation: url, lineNumber: line } = breakpointItem.attachment;
DebuggerView.updateEditor(url, line, { noDebug: true });
this.highlightBreakpoint(url, line);
},
/**
* The click listener for a breakpoint checkbox.
*/
_onCheckboxClick: function DVB__onCheckboxClick(e) {
let breakpointItem = this.getItemForElement(e.target);
if (!breakpointItem) {
// The container is empty or we didn't click on an actual item.
return;
}
let { sourceLocation: url, lineNumber: line, enabled } = breakpointItem.attachment;
// Don't update the editor location.
e.preventDefault();
e.stopPropagation();
this[enabled
? "disableBreakpoint"
: "enableBreakpoint"](url, line, { silent: true });
},
/**
* Listener handling the "enableSelf" menuitem command.
*
* @param object aTarget
* The corresponding breakpoint element node.
*/
_onEnableSelf: function DVB__onEnableSelf(aTarget) {
if (!aTarget) {
return;
}
let breakpointItem = this.getItemForElement(aTarget);
let { sourceLocation: url, lineNumber: line } = breakpointItem.attachment;
if (this.enableBreakpoint(url, line)) {
aTarget.enableSelf.menuitem.setAttribute("hidden", "true");
aTarget.disableSelf.menuitem.removeAttribute("hidden");
}
},
/**
* Listener handling the "disableSelf" menuitem command.
*
* @param object aTarget
* The corresponding breakpoint element node.
*/
_onDisableSelf: function DVB__onDisableSelf(aTarget) {
if (!aTarget) {
return;
}
let breakpointItem = this.getItemForElement(aTarget);
let { sourceLocation: url, lineNumber: line } = breakpointItem.attachment;
if (this.disableBreakpoint(url, line)) {
aTarget.enableSelf.menuitem.removeAttribute("hidden");
aTarget.disableSelf.menuitem.setAttribute("hidden", "true");
}
},
/**
* Listener handling the "deleteSelf" menuitem command.
*
* @param object aTarget
* The corresponding breakpoint element node.
*/
_onDeleteSelf: function DVB__onDeleteSelf(aTarget) {
if (!aTarget) {
return;
}
let breakpointItem = this.getItemForElement(aTarget);
let { sourceLocation: url, lineNumber: line } = breakpointItem.attachment;
let breakpointClient = DebuggerController.Breakpoints.getBreakpoint(url, line);
this.removeBreakpoint(url, line);
DebuggerController.Breakpoints.removeBreakpoint(breakpointClient);
},
/**
* Listener handling the "enableOthers" menuitem command.
*
* @param object aTarget
* The corresponding breakpoint element node.
*/
_onEnableOthers: function DVB__onEnableOthers(aTarget) {
for (let item in this) {
if (item.target != aTarget) {
this._onEnableSelf(item.target);
}
}
},
/**
* Listener handling the "disableOthers" menuitem command.
*
* @param object aTarget
* The corresponding breakpoint element node.
*/
_onDisableOthers: function DVB__onDisableOthers(aTarget) {
for (let item in this) {
if (item.target != aTarget) {
this._onDisableSelf(item.target);
}
}
},
/**
* Listener handling the "deleteOthers" menuitem command.
*
* @param object aTarget
* The corresponding breakpoint element node.
*/
_onDeleteOthers: function DVB__onDeleteOthers(aTarget) {
for (let item in this) {
if (item.target != aTarget) {
this._onDeleteSelf(item.target);
}
}
},
/**
* Listener handling the "enableAll" menuitem command.
*
* @param object aTarget
* The corresponding breakpoint element node.
*/
_onEnableAll: function DVB__onEnableAll(aTarget) {
this._onEnableOthers(aTarget);
this._onEnableSelf(aTarget);
},
/**
* Listener handling the "disableAll" menuitem command.
*
* @param object aTarget
* The corresponding breakpoint element node.
*/
_onDisableAll: function DVB__onDisableAll(aTarget) {
this._onDisableOthers(aTarget);
this._onDisableSelf(aTarget);
},
/**
* Listener handling the "deleteAll" menuitem command.
*
* @param object aTarget
* The corresponding breakpoint element node.
*/
_onDeleteAll: function DVB__onDeleteAll(aTarget) {
this._onDeleteOthers(aTarget);
this._onDeleteSelf(aTarget);
},
/**
* Gets an identifier for a breakpoint item for the current cache.
*/
_key: function DVB__key(aSourceLocation, aLineNumber) {
return aSourceLocation + aLineNumber;
},
_popupset: null,
_cache: null
});
/**
* Functions handling the global search UI.
*/
function GlobalSearchView() {
dumpn("GlobalSearchView was instantiated");
MenuContainer.call(this);
this._startSearch = this._startSearch.bind(this);
this._onFetchSourceFinished = this._onFetchSourceFinished.bind(this);
this._onFetchSourcesFinished = this._onFetchSourcesFinished.bind(this);
this._createItemView = this._createItemView.bind(this);
this._onScroll = this._onScroll.bind(this);
this._onHeaderClick = this._onHeaderClick.bind(this);
this._onLineClick = this._onLineClick.bind(this);
this._onMatchClick = this._onMatchClick.bind(this);
}
create({ constructor: GlobalSearchView, proto: MenuContainer.prototype }, {
/**
* Initialization function, called when the debugger is started.
*/
initialize: function DVGS_initialize() {
dumpn("Initializing the GlobalSearchView");
this._container = new StackList(document.getElementById("globalsearch"));
this._splitter = document.getElementById("globalsearch-splitter");
this._container.emptyText = L10N.getStr("noMatchingStringsText");
this._container.itemFactory = this._createItemView;
this._container.addEventListener("scroll", this._onScroll, false);
this._cache = new Map();
},
/**
* Destruction function, called when the debugger is closed.
*/
destroy: function DVGS_destroy() {
dumpn("Destroying the GlobalSearchView");
this._container.removeEventListener("scroll", this._onScroll, false);
},
/**
* Gets the visibility state of the global search container.
* @return boolean
*/
get hidden() this._container.getAttribute("hidden") == "true",
/**
* Sets the results container hidden or visible. It's hidden by default.
* @param boolean aFlag
*/
set hidden(aFlag) {
this._container.setAttribute("hidden", aFlag);
this._splitter.setAttribute("hidden", aFlag);
},
/**
* Removes all items from this container and hides it.
*/
clearView: function DVGS_clearView() {
this.hidden = true;
this.empty();
window.dispatchEvent("Debugger:GlobalSearch:ViewCleared");
},
/**
* Clears all the fetched sources from cache.
*/
clearCache: function DVGS_clearCache() {
this._cache = new Map();
window.dispatchEvent("Debugger:GlobalSearch:CacheCleared");
},
/**
* Focuses the next found match in the source editor.
*/
focusNextMatch: function DVGS_focusNextMatch() {
let totalLineResults = LineResults.size();
if (!totalLineResults) {
return;
}
if (++this._currentlyFocusedMatch >= totalLineResults) {
this._currentlyFocusedMatch = 0;
}
this._onMatchClick({
target: LineResults.getElementAtIndex(this._currentlyFocusedMatch)
});
},
/**
* Focuses the previously found match in the source editor.
*/
focusPrevMatch: function DVGS_focusPrevMatch() {
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.
*/
scheduleSearch: function DVGS_scheduleSearch() {
window.clearTimeout(this._searchTimeout);
this._searchTimeout = window.setTimeout(this._startSearch, GLOBAL_SEARCH_ACTION_DELAY);
},
/**
* Starts searching for a string in all of the sources.
*/
_startSearch: function DVGS__startSearch() {
let locations = DebuggerView.Sources.values;
this._sourcesCount = locations.length;
this._fetchSources(
this._onFetchSourceFinished,
this._onFetchSourcesFinished, locations);
},
/**
* Starts fetching all the sources, silently.
*
* @param function aFetchCallback
* Called after each source is fetched.
* @param function aFetchedCallback
* Called if all the sources were already fetched.
* @param array aLocations
* The locations for the sources to fetch.
*/
_fetchSources: function DVGS__fetchSources(aFetchCallback, aFetchedCallback, aLocations) {
// If all the sources were already fetched, then don't do anything.
if (this._cache.size == aLocations.length) {
aFetchedCallback();
return;
}
// Fetch each new source.
for (let location of aLocations) {
if (this._cache.has(location)) {
continue;
}
let sourceItem = DebuggerView.Sources.getItemByValue(location);
DebuggerController.SourceScripts.getText(sourceItem.attachment, aFetchCallback);
}
},
/**
* Called when a source has been fetched.
*
* @param string aLocation
* The location of the source.
* @param string aContents
* The text contents of the source.
*/
_onFetchSourceFinished: function DVGS__onFetchSourceFinished(aLocation, aContents) {
// Remember the source in a cache so we don't have to fetch it again.
this._cache.set(aLocation, aContents);
// Check if all sources were fetched and stored in the cache.
if (this._cache.size == this._sourcesCount) {
this._onFetchSourcesFinished();
}
},
/**
* Called when all the sources have been fetched.
*/
_onFetchSourcesFinished: function DVGS__onFetchSourcesFinished() {
// All sources are fetched and stored in the cache, we can start searching.
this._performGlobalSearch();
},
/**
* Finds string matches in all the sources stored in the cache, and groups
* them by location and line number.
*/
_performGlobalSearch: function DVGS__performGlobalSearch() {
// Get the currently searched token from the filtering input.
let token = DebuggerView.Filtering.searchedToken;
// Make sure we're actually searching for something.
if (!token) {
this.clearView();
window.dispatchEvent("Debugger:GlobalSearch:TokenEmpty");
return;
}
// Search is not case sensitive, prepare the actual searched token.
let lowerCaseToken = token.toLowerCase();
let tokenLength = token.length;
// Prepare the results map, containing search details for each line.
let globalResults = new GlobalResults();
for (let [location, contents] of this._cache) {
// Verify that the search token is found anywhere in the source.
if (!contents.toLowerCase().contains(lowerCaseToken)) {
continue;
}
let lines = contents.split("\n");
let sourceResults = new SourceResults();
for (let i = 0, len = lines.length; i < len; i++) {
let line = lines[i];
let lowerCaseLine = line.toLowerCase();
// Search is not case sensitive, and is tied to each line in the source.
if (!lowerCaseLine.contains(lowerCaseToken)) {
continue;
}
let lineNumber = i;
let lineResults = new LineResults();
lowerCaseLine.split(lowerCaseToken).reduce(function(prev, curr, index, {length}) {
let prevLength = prev.length;
let currLength = curr.length;
let unmatched = line.substr(prevLength, currLength);
lineResults.add(unmatched);
if (index != length - 1) {
let matched = line.substr(prevLength + currLength, tokenLength);
let range = {
start: prevLength + currLength,
length: matched.length
};
lineResults.add(matched, range, true);
sourceResults.matchCount++;
}
return prev + token + curr;
}, "");
if (sourceResults.matchCount) {
sourceResults.add(lineNumber, lineResults);
}
}
if (sourceResults.matchCount) {
globalResults.add(location, sourceResults);
}
}
// Empty this container to rebuild the search results.
this.empty();
// Signal if there are any matches, and the rebuild the results.
if (globalResults.itemCount) {
this.hidden = false;
this._currentlyFocusedMatch = -1;
this._createGlobalResultsUI(globalResults);
window.dispatchEvent("Debugger:GlobalSearch:MatchFound");
} else {
window.dispatchEvent("Debugger:GlobalSearch:MatchNotFound");
}
},
/**
* 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 DVGS__createGlobalResultsUI(aGlobalResults) {
let i = 0;
for (let [location, sourceResults] in aGlobalResults) {
if (i++ == 0) {
this._createSourceResultsUI(location, sourceResults, true);
} 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, location, sourceResults) }, 0);
}
}
},
/**
* Creates source search results entries and adds them to this container.
*
* @param string aLocation
* The location of the source.
* @param SourceResults aSourceResults
* An object containing all the matched lines for a specific source.
* @param boolean aExpandFlag
* True to expand the source results.
*/
_createSourceResultsUI:
function DVGS__createSourceResultsUI(aLocation, aSourceResults, aExpandFlag) {
// Append a source results item to this container.
let sourceResultsItem = this.push(aLocation, aSourceResults.matchCount, {
forced: true,
unsorted: true,
relaxed: true,
attachment: {
sourceResults: aSourceResults,
expandFlag: aExpandFlag
}
});
},
/**
* Customization function for creating an item's UI.
*
* @param nsIDOMNode aElementNode
* The element associated with the displayed item.
* @param string aLocation
* The source result's location.
* @param string aMatchCount
* The source result's match count.
* @param any aAttachment [optional]
* Some attached primitive/object.
*/
_createItemView:
function DVGS__createItemView(aElementNode, aLocation, aMatchCount, aAttachment) {
let { sourceResults, expandFlag } = aAttachment;
sourceResults.createView(aElementNode, aLocation, aMatchCount, expandFlag, {
onHeaderClick: this._onHeaderClick,
onLineClick: this._onLineClick,
onMatchClick: this._onMatchClick
});
},
/**
* The click listener for a results header.
*/
_onHeaderClick: function DVGS__onHeaderClick(e) {
let sourceResultsItem = SourceResults.getItemForElement(e.target);
sourceResultsItem.instance.toggle(e);
},
/**
* The click listener for a results line.
*/
_onLineClick: function DVGLS__onLineClick(e) {
let lineResultsItem = LineResults.getItemForElement(e.target);
this._onMatchClick({ target: lineResultsItem.firstMatch });
},
/**
* The click listener for a result match.
*/
_onMatchClick: function DVGLS__onMatchClick(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 location = sourceResultsItem.location;
let lineNumber = lineResultsItem.lineNumber;
DebuggerView.updateEditor(location, lineNumber + 1, { noDebug: true });
let editor = DebuggerView.editor;
let offset = editor.getCaretOffset();
let { start, length } = lineResultsItem.lineData.range;
editor.setSelection(offset + start, offset + start + length);
},
/**
* The scroll listener for the global search container.
*/
_onScroll: function DVGS__onScroll(e) {
for (let item in this) {
this._expandResultsIfNeeded(item.target);
}
},
/**
* Expands the source results it they are currently visible.
*
* @param nsIDOMNode aTarget
* The element associated with the displayed item.
*/
_expandResultsIfNeeded: function DVGS__expandResultsIfNeeded(aTarget) {
let sourceResultsItem = SourceResults.getItemForElement(aTarget);
if (sourceResultsItem.instance.toggled ||
sourceResultsItem.instance.expanded) {
return;
}
let { top, height } = aTarget.getBoundingClientRect();
let { clientHeight } = this._container._parent;
if (top - height <= clientHeight || this._forceExpandResults) {
sourceResultsItem.instance.expand();
}
},
/**
* Scrolls a match into view.
*
* @param nsIDOMNode aMatch
* The match to scroll into view.
*/
_scrollMatchIntoViewIfNeeded: function DVGS__scrollMatchIntoViewIfNeeded(aMatch) {
let { top, height } = aMatch.getBoundingClientRect();
let { clientHeight } = this._container._parent;
let style = window.getComputedStyle(aMatch);
let topBorderSize = window.parseInt(style.getPropertyValue("border-top-width"));
let bottomBorderSize = window.parseInt(style.getPropertyValue("border-bottom-width"));
let marginY = top - (height + topBorderSize + bottomBorderSize) * 2;
if (marginY <= 0) {
this._container._parent.scrollTop += marginY;
}
if (marginY + height > clientHeight) {
this._container._parent.scrollTop += height - (clientHeight - marginY);
}
},
/**
* Starts a bounce animation for a match.
*
* @param nsIDOMNode aMatch
* The match to start a bounce animation for.
*/
_bounceMatch: function DVGS__bounceMatch(aMatch) {
Services.tm.currentThread.dispatch({ run: function() {
aMatch.setAttribute("focused", "");
aMatch.addEventListener("transitionend", function onEvent() {
aMatch.removeEventListener("transitionend", onEvent);
aMatch.removeAttribute("focused");
});
}}, 0);
},
_splitter: null,
_currentlyFocusedMatch: -1,
_forceExpandResults: false,
_searchTimeout: null,
_sourcesCount: -1,
_cache: null
});
/**
* An object containing all source results, grouped by source location.
* Iterable via "for (let [location, sourceResults] in globalResults) { }".
*/
function GlobalResults() {
this._store = new Map();
SourceResults._itemsByElement = new Map();
LineResults._itemsByElement = new Map();
}
GlobalResults.prototype = {
/**
* Adds source results to this store.
*
* @param string aLocation
* The location of the source.
* @param SourceResults aSourceResults
* An object containing all the matched lines for a specific source.
*/
add: function GR_add(aLocation, aSourceResults) {
this._store.set(aLocation, aSourceResults);
},
/**
* Gets the number of source results in this store.
*/
get itemCount() this._store.size,
_store: null
};
/**
* An object containing all the matched lines for a specific source.
* Iterable via "for (let [lineNumber, lineResults] in sourceResults) { }".
*/
function SourceResults() {
this._store = new Map();
this.matchCount = 0;
}
SourceResults.prototype = {
/**
* Adds line results to this store.
*
* @param number aLineNumber
* The line location in the source.
* @param LineResults aLineResults
* An object containing all the matches for a specific line.
*/
add: function SR_add(aLineNumber, aLineResults) {
this._store.set(aLineNumber, aLineResults);
},
/**
* The number of matches in this store. One line may have multiple matches.
*/
matchCount: -1,
/**
* Expands the element, showing all the added details.
*
* @param boolean aSkipAnimationFlag
* Pass true to not show an opening animation.
*/
expand: function SR_expand(aSkipAnimationFlag) {
this._target.resultsContainer.setAttribute("open", "");
this._target.arrow.setAttribute("open", "");
if (!aSkipAnimationFlag) {
this._target.resultsContainer.setAttribute("animated", "");
}
},
/**
* Collapses the element, hiding all the added details.
*/
collapse: function SR_collapse() {
this._target.resultsContainer.removeAttribute("animated");
this._target.resultsContainer.removeAttribute("open");
this._target.arrow.removeAttribute("open");
},
/**
* Toggles between the element collapse/expand state.
*/
toggle: function SR_toggle(e) {
if (e instanceof Event) {
this._userToggled = true;
}
this.expanded ^= 1;
},
/**
* Gets this element's expanded state.
* @return boolean
*/
get expanded() this._target.resultsContainer.hasAttribute("open"),
/**
* Sets this element's expanded state.
* @param boolean aFlag
*/
set expanded(aFlag) this[aFlag ? "expand" : "collapse"](),
/**
* Returns if this element was ever toggled via user interaction.
* @return boolean
*/
get toggled() this._userToggled,
/**
* 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 string aLocation
* The source result's location.
* @param string aMatchCount
* The source result's match count.
* @param boolean aExpandFlag
* True to expand the source results.
* @param object aCallbacks
* An object containing all the necessary callback functions:
* - onHeaderClick
* - onMatchClick
*/
createView:
function SR_createView(aElementNode, aLocation, aMatchCount, aExpandFlag, aCallbacks) {
this._target = aElementNode;
let arrow = document.createElement("box");
arrow.className = "arrow";
let locationNode = document.createElement("label");
locationNode.className = "plain location";
locationNode.setAttribute("value", SourceUtils.trimUrlLength(aLocation));
let matchCountNode = document.createElement("label");
matchCountNode.className = "plain match-count";
matchCountNode.setAttribute("value", "(" + aMatchCount + ")");
let 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 = document.createElement("vbox");
resultsContainer.className = "dbg-results-container";
for (let [lineNumber, lineResults] of this._store) {
lineResults.createView(resultsContainer, lineNumber, aCallbacks)
}
aElementNode.arrow = arrow;
aElementNode.resultsHeader = resultsHeader;
aElementNode.resultsContainer = resultsContainer;
if (aExpandFlag && aMatchCount < 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-" + aLocation;
aElementNode.className = "dbg-source-results";
aElementNode.appendChild(resultsBox);
SourceResults._itemsByElement.set(aElementNode, {
location: aLocation,
matchCount: aMatchCount,
autoExpand: aExpandFlag,
instance: this
});
},
_store: null,
_target: null,
_userToggled: false
};
/**
* An object containing all the matches for a specific line.
* Iterable via "for (let chunk in lineResults) { }".
*/
function LineResults() {
this._store = [];
}
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 LC_add(aString, aRange, aMatchFlag) {
this._store.push({
string: aString,
range: aRange,
match: !!aMatchFlag
});
},
/**
* Gets the element associated with this item.
* @return nsIDOMNode
*/
get target() this._target,
/**
* Customization function for creating this item's UI.
*
* @param nsIDOMNode aContainer
* The element associated with the displayed item.
* @param number aLineNumber
* The line location in the source.
* @param object aCallbacks
* An object containing all the necessary callback functions:
* - onMatchClick
* - onLineClick
*/
createView: function LR_createView(aContainer, aLineNumber, aCallbacks) {
this._target = aContainer;
let lineNumberNode = document.createElement("label");
let lineContentsNode = document.createElement("hbox");
let lineString = "";
let lineLength = 0;
let firstMatch = null;
lineNumberNode.className = "plain line-number";
lineNumberNode.setAttribute("value", aLineNumber + 1);
lineContentsNode.className = "line-contents";
lineContentsNode.setAttribute("flex", "1");
for (let chunk of this._store) {
let { string, range, match } = chunk;
lineString = string.substr(0, GLOBAL_SEARCH_LINE_MAX_LENGTH - lineLength);
lineLength += string.length;
let label = document.createElement("label");
label.className = "plain string";
label.setAttribute("value", lineString);
label.setAttribute("match", match);
lineContentsNode.appendChild(label);
if (match) {
this._entangleMatch(aLineNumber, label, chunk);
label.addEventListener("click", aCallbacks.onMatchClick, false);
firstMatch = firstMatch || label;
}
if (lineLength >= GLOBAL_SEARCH_LINE_MAX_LENGTH) {
lineContentsNode.appendChild(this._ellipsis.cloneNode());
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);
aContainer.appendChild(searchResult);
},
/**
* Handles a match while creating the view.
* @param number aLineNumber
* @param nsIDOMNode aNode
* @param object aMatchChunk
*/
_entangleMatch: function LR__entangleMatch(aLineNumber, aNode, aMatchChunk) {
LineResults._itemsByElement.set(aNode, {
lineNumber: aLineNumber,
lineData: aMatchChunk
});
},
/**
* Handles a line while creating the view.
* @param nsIDOMNode aNode
* @param nsIDOMNode aFirstMatch
*/
_entangleLine: function LR__entangleLine(aNode, aFirstMatch) {
LineResults._itemsByElement.set(aNode, {
firstMatch: aFirstMatch,
nonenumerable: true
});
},
/**
* An nsIDOMNode label with an ellipsis value.
*/
_ellipsis: (function() {
let label = document.createElement("label");
label.className = "plain string";
label.setAttribute("value", L10N.ellipsis);
return label;
})(),
_store: null,
_target: null
};
/**
* A generator-iterator over the global, source or line results.
*/
GlobalResults.prototype.__iterator__ =
SourceResults.prototype.__iterator__ =
LineResults.prototype.__iterator__ = function DVGS_iterator() {
for (let item of this._store) {
yield item;
}
};
/**
* 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 DVGS_getItemForElement(aElement) {
return MenuContainer.prototype.getItemForElement.call(this, aElement);
};
/**
* 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 DVGS_getElementAtIndex(aIndex) {
for (let [element, item] of this._itemsByElement) {
if (!item.nonenumerable && !aIndex--) {
return element;
}
}
};
/**
* Gets the index of an item associated with the specified element.
*
* @return number
* The index of the matched item, or -1 if nothing is found.
*/
SourceResults.indexOfElement =
LineResults.indexOfElement = function DVGS_indexOFElement(aElement) {
let count = 0;
for (let [element, item] of this._itemsByElement) {
if (element == aElement) {
return count;
}
if (!item.nonenumerable) {
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 DVGS_size() {
let count = 0;
for (let [, item] of this._itemsByElement) {
if (!item.nonenumerable) {
count++;
}
}
return count;
};
/**
* Preliminary setup for the DebuggerView object.
*/
DebuggerView.StackFrames = new StackFramesView();
DebuggerView.Breakpoints = new BreakpointsView();
DebuggerView.GlobalSearch = new GlobalSearchView();