diff -r 000000000000 -r 6474c204b198 browser/devtools/debugger/debugger-panes.js --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/browser/devtools/debugger/debugger-panes.js Wed Dec 31 06:09:35 2014 +0100 @@ -0,0 +1,3315 @@ +/* -*- 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"; + +XPCOMUtils.defineLazyModuleGetter(this, "Task", + "resource://gre/modules/Task.jsm"); + +// 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 + +// Maps known URLs to friendly source group names +const KNOWN_SOURCE_GROUPS = { + "Add-on SDK": "resource://gre/modules/commonjs/", +}; + +/** + * Functions handling the sources UI. + */ +function SourcesView() { + dumpn("SourcesView was instantiated"); + + this.togglePrettyPrint = this.togglePrettyPrint.bind(this); + this.toggleBlackBoxing = this.toggleBlackBoxing.bind(this); + this.toggleBreakpoints = this.toggleBreakpoints.bind(this); + + this._onEditorLoad = this._onEditorLoad.bind(this); + this._onEditorUnload = this._onEditorUnload.bind(this); + this._onEditorCursorActivity = this._onEditorCursorActivity.bind(this); + this._onSourceSelect = this._onSourceSelect.bind(this); + this._onStopBlackBoxing = this._onStopBlackBoxing.bind(this); + this._onBreakpointRemoved = this._onBreakpointRemoved.bind(this); + this._onBreakpointClick = this._onBreakpointClick.bind(this); + this._onBreakpointCheckboxClick = this._onBreakpointCheckboxClick.bind(this); + this._onConditionalPopupShowing = this._onConditionalPopupShowing.bind(this); + this._onConditionalPopupShown = this._onConditionalPopupShown.bind(this); + this._onConditionalPopupHiding = this._onConditionalPopupHiding.bind(this); + this._onConditionalTextboxKeyPress = this._onConditionalTextboxKeyPress.bind(this); + + this.updateToolbarButtonsState = this.updateToolbarButtonsState.bind(this); +} + +SourcesView.prototype = Heritage.extend(WidgetMethods, { + /** + * Initialization function, called when the debugger is started. + */ + initialize: function() { + dumpn("Initializing the SourcesView"); + + this.widget = new SideMenuWidget(document.getElementById("sources"), { + showArrows: true + }); + + // Sort known source groups towards the end of the list + this.widget.groupSortPredicate = function(a, b) { + if ((a in KNOWN_SOURCE_GROUPS) == (b in KNOWN_SOURCE_GROUPS)) { + return a.localeCompare(b); + } + + return (a in KNOWN_SOURCE_GROUPS) ? 1 : -1; + }; + + this.emptyText = L10N.getStr("noSourcesText"); + this._blackBoxCheckboxTooltip = L10N.getStr("blackBoxCheckboxTooltip"); + + this._commandset = document.getElementById("debuggerCommands"); + this._popupset = document.getElementById("debuggerPopupset"); + this._cmPopup = document.getElementById("sourceEditorContextMenu"); + this._cbPanel = document.getElementById("conditional-breakpoint-panel"); + this._cbTextbox = document.getElementById("conditional-breakpoint-panel-textbox"); + this._blackBoxButton = document.getElementById("black-box"); + this._stopBlackBoxButton = document.getElementById("black-boxed-message-button"); + this._prettyPrintButton = document.getElementById("pretty-print"); + this._toggleBreakpointsButton = document.getElementById("toggle-breakpoints"); + + if (Prefs.prettyPrintEnabled) { + this._prettyPrintButton.removeAttribute("hidden"); + } + + window.on(EVENTS.EDITOR_LOADED, this._onEditorLoad, false); + window.on(EVENTS.EDITOR_UNLOADED, this._onEditorUnload, false); + this.widget.addEventListener("select", this._onSourceSelect, false); + this._stopBlackBoxButton.addEventListener("click", this._onStopBlackBoxing, false); + this._cbPanel.addEventListener("popupshowing", this._onConditionalPopupShowing, false); + this._cbPanel.addEventListener("popupshown", this._onConditionalPopupShown, false); + this._cbPanel.addEventListener("popuphiding", this._onConditionalPopupHiding, false); + this._cbTextbox.addEventListener("keypress", this._onConditionalTextboxKeyPress, false); + + this.autoFocusOnSelection = false; + + // Sort the contents by the displayed label. + this.sortContents((aFirst, aSecond) => { + return +(aFirst.attachment.label.toLowerCase() > + aSecond.attachment.label.toLowerCase()); + }); + }, + + /** + * Destruction function, called when the debugger is closed. + */ + destroy: function() { + dumpn("Destroying the SourcesView"); + + window.off(EVENTS.EDITOR_LOADED, this._onEditorLoad, false); + window.off(EVENTS.EDITOR_UNLOADED, this._onEditorUnload, false); + this.widget.removeEventListener("select", this._onSourceSelect, false); + this._stopBlackBoxButton.removeEventListener("click", this._onStopBlackBoxing, false); + this._cbPanel.removeEventListener("popupshowing", this._onConditionalPopupShowing, false); + this._cbPanel.removeEventListener("popupshowing", this._onConditionalPopupShown, false); + this._cbPanel.removeEventListener("popuphiding", this._onConditionalPopupHiding, false); + this._cbTextbox.removeEventListener("keypress", this._onConditionalTextboxKeyPress, false); + }, + + /** + * Sets the preferred location to be selected in this sources container. + * @param string aUrl + */ + set preferredSource(aUrl) { + this._preferredValue = aUrl; + + // Selects the element with the specified value in this sources container, + // if already inserted. + if (this.containsValue(aUrl)) { + this.selectedValue = aUrl; + } + }, + + /** + * Adds a source to this sources container. + * + * @param object aSource + * The source 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 + */ + addSource: function(aSource, aOptions = {}) { + let fullUrl = aSource.url; + let url = fullUrl.split(" -> ").pop(); + let label = aSource.addonPath ? aSource.addonPath : SourceUtils.getSourceLabel(url); + let group = aSource.addonID ? aSource.addonID : SourceUtils.getSourceGroup(url); + let unicodeUrl = NetworkHelper.convertToUnicode(unescape(fullUrl)); + + let contents = document.createElement("label"); + contents.className = "plain dbg-source-item"; + contents.setAttribute("value", label); + contents.setAttribute("crop", "start"); + contents.setAttribute("flex", "1"); + contents.setAttribute("tooltiptext", unicodeUrl); + + // Append a source item to this container. + this.push([contents, fullUrl], { + staged: aOptions.staged, /* stage the item to be appended later? */ + attachment: { + label: label, + group: group, + checkboxState: !aSource.isBlackBoxed, + checkboxTooltip: this._blackBoxCheckboxTooltip, + source: aSource + } + }); + }, + + /** + * Adds a breakpoint to this sources container. + * + * @param object aBreakpointData + * Information about the breakpoint to be shown. + * This object must have the following properties: + * - location: the breakpoint's source location and line number + * - disabled: the breakpoint's disabled state, boolean + * - text: the breakpoint's line text to be displayed + * @param object aOptions [optional] + * @see DebuggerController.Breakpoints.addBreakpoint + */ + addBreakpoint: function(aBreakpointData, aOptions = {}) { + let { location, disabled } = aBreakpointData; + + // Make sure we're not duplicating anything. If a breakpoint at the + // specified source url and line already exists, just toggle it. + if (this.getBreakpoint(location)) { + this[disabled ? "disableBreakpoint" : "enableBreakpoint"](location); + return; + } + + // Get the source item to which the breakpoint should be attached. + let sourceItem = this.getItemByValue(location.url); + + // Create the element node and menu popup for the breakpoint item. + let breakpointArgs = Heritage.extend(aBreakpointData, aOptions); + let breakpointView = this._createBreakpointView.call(this, breakpointArgs); + let contextMenu = this._createContextMenu.call(this, breakpointArgs); + + // Append a breakpoint child item to the corresponding source item. + sourceItem.append(breakpointView.container, { + attachment: Heritage.extend(breakpointArgs, { + url: location.url, + line: location.line, + view: breakpointView, + popup: contextMenu + }), + attributes: [ + ["contextmenu", contextMenu.menupopupId] + ], + // Make sure that when the breakpoint item is removed, the corresponding + // menupopup and commandset are also destroyed. + finalize: this._onBreakpointRemoved + }); + + // Highlight the newly appended breakpoint child item if necessary. + if (aOptions.openPopup || !aOptions.noEditorUpdate) { + this.highlightBreakpoint(location, aOptions); + } + }, + + /** + * Removes a breakpoint from this sources container. + * It does not also remove the breakpoint from the controller. Be careful. + * + * @param object aLocation + * @see DebuggerController.Breakpoints.addBreakpoint + */ + removeBreakpoint: function(aLocation) { + // When a parent source item is removed, all the child breakpoint items are + // also automagically removed. + let sourceItem = this.getItemByValue(aLocation.url); + if (!sourceItem) { + return; + } + let breakpointItem = this.getBreakpoint(aLocation); + if (!breakpointItem) { + return; + } + + // Clear the breakpoint view. + sourceItem.remove(breakpointItem); + }, + + /** + * Returns the breakpoint at the specified source url and line. + * + * @param object aLocation + * @see DebuggerController.Breakpoints.addBreakpoint + * @return object + * The corresponding breakpoint item if found, null otherwise. + */ + getBreakpoint: function(aLocation) { + return this.getItemForPredicate(aItem => + aItem.attachment.url == aLocation.url && + aItem.attachment.line == aLocation.line); + }, + + /** + * Returns all breakpoints for all sources. + * + * @return array + * The breakpoints for all sources if any, an empty array otherwise. + */ + getAllBreakpoints: function(aStore = []) { + return this.getOtherBreakpoints(undefined, aStore); + }, + + /** + * Returns all breakpoints which are not at the specified source url and line. + * + * @param object aLocation [optional] + * @see DebuggerController.Breakpoints.addBreakpoint + * @param array aStore [optional] + * A list in which to store the corresponding breakpoints. + * @return array + * The corresponding breakpoints if found, an empty array otherwise. + */ + getOtherBreakpoints: function(aLocation = {}, aStore = []) { + for (let source of this) { + for (let breakpointItem of source) { + let { url, line } = breakpointItem.attachment; + if (url != aLocation.url || line != aLocation.line) { + aStore.push(breakpointItem); + } + } + } + return aStore; + }, + + /** + * Enables a breakpoint. + * + * @param object aLocation + * @see DebuggerController.Breakpoints.addBreakpoint + * @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). + * @return object + * A promise that is resolved after the breakpoint is enabled, or + * rejected if no breakpoint was found at the specified location. + */ + enableBreakpoint: function(aLocation, aOptions = {}) { + let breakpointItem = this.getBreakpoint(aLocation); + if (!breakpointItem) { + return promise.reject(new Error("No breakpoint found.")); + } + + // Breakpoint will now be enabled. + let attachment = breakpointItem.attachment; + attachment.disabled = false; + + // Update the corresponding menu items to reflect the enabled state. + let prefix = "bp-cMenu-"; // "breakpoints context menu" + let identifier = DebuggerController.Breakpoints.getIdentifier(attachment); + let enableSelfId = prefix + "enableSelf-" + identifier + "-menuitem"; + let disableSelfId = prefix + "disableSelf-" + identifier + "-menuitem"; + document.getElementById(enableSelfId).setAttribute("hidden", "true"); + document.getElementById(disableSelfId).removeAttribute("hidden"); + + // Update the breakpoint toggle button checked state. + this._toggleBreakpointsButton.removeAttribute("checked"); + + // Update the checkbox state if necessary. + if (!aOptions.silent) { + attachment.view.checkbox.setAttribute("checked", "true"); + } + + return DebuggerController.Breakpoints.addBreakpoint(aLocation, { + // No need to update the pane, since this method is invoked because + // a breakpoint's view was interacted with. + noPaneUpdate: true + }); + }, + + /** + * Disables a breakpoint. + * + * @param object aLocation + * @see DebuggerController.Breakpoints.addBreakpoint + * @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). + * @return object + * A promise that is resolved after the breakpoint is disabled, or + * rejected if no breakpoint was found at the specified location. + */ + disableBreakpoint: function(aLocation, aOptions = {}) { + let breakpointItem = this.getBreakpoint(aLocation); + if (!breakpointItem) { + return promise.reject(new Error("No breakpoint found.")); + } + + // Breakpoint will now be disabled. + let attachment = breakpointItem.attachment; + attachment.disabled = true; + + // Update the corresponding menu items to reflect the disabled state. + let prefix = "bp-cMenu-"; // "breakpoints context menu" + let identifier = DebuggerController.Breakpoints.getIdentifier(attachment); + let enableSelfId = prefix + "enableSelf-" + identifier + "-menuitem"; + let disableSelfId = prefix + "disableSelf-" + identifier + "-menuitem"; + document.getElementById(enableSelfId).removeAttribute("hidden"); + document.getElementById(disableSelfId).setAttribute("hidden", "true"); + + // Update the checkbox state if necessary. + if (!aOptions.silent) { + attachment.view.checkbox.removeAttribute("checked"); + } + + return DebuggerController.Breakpoints.removeBreakpoint(aLocation, { + // No need to update this pane, since this method is invoked because + // a breakpoint's view was interacted with. + noPaneUpdate: true, + // Mark this breakpoint as being "disabled", not completely removed. + // This makes sure it will not be forgotten across target navigations. + rememberDisabled: true + }); + }, + + /** + * Highlights a breakpoint in this sources container. + * + * @param object aLocation + * @see DebuggerController.Breakpoints.addBreakpoint + * @param object aOptions [optional] + * An object containing some of the following boolean properties: + * - openPopup: tells if the expression popup should be shown. + * - noEditorUpdate: tells if you want to skip editor updates. + */ + highlightBreakpoint: function(aLocation, aOptions = {}) { + let breakpointItem = this.getBreakpoint(aLocation); + if (!breakpointItem) { + return; + } + + // Breakpoint will now be selected. + this._selectBreakpoint(breakpointItem); + + // Update the editor location if necessary. + if (!aOptions.noEditorUpdate) { + DebuggerView.setEditorLocation(aLocation.url, aLocation.line, { noDebug: true }); + } + + // If the breakpoint requires a new conditional expression, display + // the panel to input the corresponding expression. + if (aOptions.openPopup) { + this._openConditionalPopup(); + } else { + this._hideConditionalPopup(); + } + }, + + /** + * Unhighlights the current breakpoint in this sources container. + */ + unhighlightBreakpoint: function() { + this._unselectBreakpoint(); + this._hideConditionalPopup(); + }, + + /** + * Update the checked/unchecked and enabled/disabled states of the buttons in + * the sources toolbar based on the currently selected source's state. + */ + updateToolbarButtonsState: function() { + const { source } = this.selectedItem.attachment; + const sourceClient = gThreadClient.source(source); + + if (sourceClient.isBlackBoxed) { + this._prettyPrintButton.setAttribute("disabled", true); + this._blackBoxButton.setAttribute("checked", true); + } else { + this._prettyPrintButton.removeAttribute("disabled"); + this._blackBoxButton.removeAttribute("checked"); + } + + if (sourceClient.isPrettyPrinted) { + this._prettyPrintButton.setAttribute("checked", true); + } else { + this._prettyPrintButton.removeAttribute("checked"); + } + }, + + /** + * Toggle the pretty printing of the selected source. + */ + togglePrettyPrint: function() { + if (this._prettyPrintButton.hasAttribute("disabled")) { + return; + } + + const resetEditor = ([{ url }]) => { + // Only set the text when the source is still selected. + if (url == this.selectedValue) { + DebuggerView.setEditorLocation(url, 0, { force: true }); + } + }; + + const printError = ([{ url }, error]) => { + DevToolsUtils.reportException("togglePrettyPrint", error); + }; + + DebuggerView.showProgressBar(); + const { source } = this.selectedItem.attachment; + const sourceClient = gThreadClient.source(source); + const shouldPrettyPrint = !sourceClient.isPrettyPrinted; + + if (shouldPrettyPrint) { + this._prettyPrintButton.setAttribute("checked", true); + } else { + this._prettyPrintButton.removeAttribute("checked"); + } + + DebuggerController.SourceScripts.togglePrettyPrint(source) + .then(resetEditor, printError) + .then(DebuggerView.showEditor) + .then(this.updateToolbarButtonsState); + }, + + /** + * Toggle the black boxed state of the selected source. + */ + toggleBlackBoxing: function() { + const { source } = this.selectedItem.attachment; + const sourceClient = gThreadClient.source(source); + const shouldBlackBox = !sourceClient.isBlackBoxed; + + // Be optimistic that the (un-)black boxing will succeed, so enable/disable + // the pretty print button and check/uncheck the black box button + // immediately. Then, once we actually get the results from the server, make + // sure that it is in the correct state again by calling + // `updateToolbarButtonsState`. + + if (shouldBlackBox) { + this._prettyPrintButton.setAttribute("disabled", true); + this._blackBoxButton.setAttribute("checked", true); + } else { + this._prettyPrintButton.removeAttribute("disabled"); + this._blackBoxButton.removeAttribute("checked"); + } + + DebuggerController.SourceScripts.setBlackBoxing(source, shouldBlackBox) + .then(this.updateToolbarButtonsState, + this.updateToolbarButtonsState); + }, + + /** + * Toggles all breakpoints enabled/disabled. + */ + toggleBreakpoints: function() { + let breakpoints = this.getAllBreakpoints(); + let hasBreakpoints = breakpoints.length > 0; + let hasEnabledBreakpoints = breakpoints.some(e => !e.attachment.disabled); + + if (hasBreakpoints && hasEnabledBreakpoints) { + this._toggleBreakpointsButton.setAttribute("checked", true); + this._onDisableAll(); + } else { + this._toggleBreakpointsButton.removeAttribute("checked"); + this._onEnableAll(); + } + }, + + /** + * Marks a breakpoint as selected in this sources container. + * + * @param object aItem + * The breakpoint item to select. + */ + _selectBreakpoint: function(aItem) { + if (this._selectedBreakpointItem == aItem) { + return; + } + this._unselectBreakpoint(); + this._selectedBreakpointItem = aItem; + this._selectedBreakpointItem.target.classList.add("selected"); + + // Ensure the currently selected breakpoint is visible. + this.widget.ensureElementIsVisible(aItem.target); + }, + + /** + * Marks the current breakpoint as unselected in this sources container. + */ + _unselectBreakpoint: function() { + if (!this._selectedBreakpointItem) { + return; + } + this._selectedBreakpointItem.target.classList.remove("selected"); + this._selectedBreakpointItem = null; + }, + + /** + * Opens a conditional breakpoint's expression input popup. + */ + _openConditionalPopup: function() { + let breakpointItem = this._selectedBreakpointItem; + let attachment = breakpointItem.attachment; + // Check if this is an enabled conditional breakpoint, and if so, + // retrieve the current conditional epression. + let breakpointPromise = DebuggerController.Breakpoints._getAdded(attachment); + if (breakpointPromise) { + breakpointPromise.then(aBreakpointClient => { + let isConditionalBreakpoint = aBreakpointClient.hasCondition(); + let condition = aBreakpointClient.getCondition(); + doOpen.call(this, isConditionalBreakpoint ? condition : "") + }); + } else { + doOpen.call(this, "") + } + + function doOpen(aConditionalExpression) { + // Update the conditional expression textbox. If no expression was + // previously set, revert to using an empty string by default. + this._cbTextbox.value = aConditionalExpression; + + // Show the conditional expression panel. The popup arrow should be pointing + // at the line number node in the breakpoint item view. + this._cbPanel.hidden = false; + this._cbPanel.openPopup(breakpointItem.attachment.view.lineNumber, + BREAKPOINT_CONDITIONAL_POPUP_POSITION, + BREAKPOINT_CONDITIONAL_POPUP_OFFSET_X, + BREAKPOINT_CONDITIONAL_POPUP_OFFSET_Y); + } + }, + + /** + * Hides a conditional breakpoint's expression input popup. + */ + _hideConditionalPopup: function() { + this._cbPanel.hidden = true; + + // Sometimes this._cbPanel doesn't have hidePopup method which doesn't + // break anything but simply outputs an exception to the console. + if (this._cbPanel.hidePopup) { + this._cbPanel.hidePopup(); + } + }, + + /** + * Customization function for creating a breakpoint item's UI. + * + * @param object aOptions + * A couple of options or flags supported by this operation: + * - location: the breakpoint's source location and line number + * - disabled: the breakpoint's disabled state, boolean + * - text: the breakpoint's line text to be displayed + * @return object + * An object containing the breakpoint container, checkbox, + * line number and line text nodes. + */ + _createBreakpointView: function(aOptions) { + let { location, disabled, text } = aOptions; + let identifier = DebuggerController.Breakpoints.getIdentifier(location); + + let checkbox = document.createElement("checkbox"); + checkbox.setAttribute("checked", !disabled); + checkbox.className = "dbg-breakpoint-checkbox"; + + let lineNumberNode = document.createElement("label"); + lineNumberNode.className = "plain dbg-breakpoint-line"; + lineNumberNode.setAttribute("value", location.line); + + let lineTextNode = document.createElement("label"); + lineTextNode.className = "plain dbg-breakpoint-text"; + lineTextNode.setAttribute("value", text); + lineTextNode.setAttribute("crop", "end"); + lineTextNode.setAttribute("flex", "1"); + + let tooltip = text.substr(0, BREAKPOINT_LINE_TOOLTIP_MAX_LENGTH); + lineTextNode.setAttribute("tooltiptext", tooltip); + + let container = document.createElement("hbox"); + container.id = "breakpoint-" + identifier; + container.className = "dbg-breakpoint side-menu-widget-item-other"; + container.classList.add("devtools-monospace"); + container.setAttribute("align", "center"); + container.setAttribute("flex", "1"); + + container.addEventListener("click", this._onBreakpointClick, false); + checkbox.addEventListener("click", this._onBreakpointCheckboxClick, false); + + container.appendChild(checkbox); + container.appendChild(lineNumberNode); + container.appendChild(lineTextNode); + + return { + container: container, + checkbox: checkbox, + lineNumber: lineNumberNode, + lineText: lineTextNode + }; + }, + + /** + * Creates a context menu for a breakpoint element. + * + * @param object aOptions + * A couple of options or flags supported by this operation: + * - location: the breakpoint's source location and line number + * - disabled: the breakpoint's disabled state, boolean + * @return object + * An object containing the breakpoint commandset and menu popup ids. + */ + _createContextMenu: function(aOptions) { + let { location, disabled } = aOptions; + let identifier = DebuggerController.Breakpoints.getIdentifier(location); + + let commandset = document.createElement("commandset"); + let menupopup = document.createElement("menupopup"); + commandset.id = "bp-cSet-" + identifier; + menupopup.id = "bp-mPop-" + identifier; + + createMenuItem.call(this, "enableSelf", !disabled); + createMenuItem.call(this, "disableSelf", disabled); + createMenuItem.call(this, "deleteSelf"); + createMenuSeparator(); + createMenuItem.call(this, "setConditional"); + createMenuSeparator(); + createMenuItem.call(this, "enableOthers"); + createMenuItem.call(this, "disableOthers"); + createMenuItem.call(this, "deleteOthers"); + createMenuSeparator(); + createMenuItem.call(this, "enableAll"); + createMenuItem.call(this, "disableAll"); + createMenuSeparator(); + createMenuItem.call(this, "deleteAll"); + + this._popupset.appendChild(menupopup); + this._commandset.appendChild(commandset); + + return { + commandsetId: commandset.id, + menupopupId: menupopup.id + }; + + /** + * 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-"; // "breakpoints context menu" + let commandId = prefix + aName + "-" + identifier + "-command"; + let menuitemId = prefix + aName + "-" + identifier + "-menuitem"; + + let label = L10N.getStr("breakpointMenuItem." + aName); + let func = "_on" + aName.charAt(0).toUpperCase() + aName.slice(1); + + command.id = commandId; + command.setAttribute("label", label); + command.addEventListener("command", () => this[func](location), false); + + menuitem.id = menuitemId; + menuitem.setAttribute("command", commandId); + aHiddenFlag && menuitem.setAttribute("hidden", "true"); + + commandset.appendChild(command); + menupopup.appendChild(menuitem); + } + + /** + * Creates a simple menu separator element and appends it to the current + * menupopup hierarchy. + */ + function createMenuSeparator() { + let menuseparator = document.createElement("menuseparator"); + menupopup.appendChild(menuseparator); + } + }, + + /** + * Function called each time a breakpoint item is removed. + * + * @param object aItem + * The corresponding item. + */ + _onBreakpointRemoved: function(aItem) { + dumpn("Finalizing breakpoint item: " + aItem); + + // Destroy the context menu for the breakpoint. + let contextMenu = aItem.attachment.popup; + document.getElementById(contextMenu.commandsetId).remove(); + document.getElementById(contextMenu.menupopupId).remove(); + + // Clear the breakpoint selection. + if (this._selectedBreakpointItem == aItem) { + this._selectedBreakpointItem = null; + } + }, + + /** + * The load listener for the source editor. + */ + _onEditorLoad: function(aName, aEditor) { + aEditor.on("cursorActivity", this._onEditorCursorActivity); + }, + + /** + * The unload listener for the source editor. + */ + _onEditorUnload: function(aName, aEditor) { + aEditor.off("cursorActivity", this._onEditorCursorActivity); + }, + + /** + * The selection listener for the source editor. + */ + _onEditorCursorActivity: function(e) { + let editor = DebuggerView.editor; + let start = editor.getCursor("start").line + 1; + let end = editor.getCursor().line + 1; + let url = this.selectedValue; + + let location = { url: url, line: start }; + + if (this.getBreakpoint(location) && start == end) { + this.highlightBreakpoint(location, { noEditorUpdate: true }); + } else { + this.unhighlightBreakpoint(); + } + }, + + /** + * The select listener for the sources container. + */ + _onSourceSelect: function({ detail: sourceItem }) { + if (!sourceItem) { + return; + } + const { source } = sourceItem.attachment; + const sourceClient = gThreadClient.source(source); + + // The container is not empty and an actual item was selected. + DebuggerView.setEditorLocation(sourceItem.value); + + if (Prefs.autoPrettyPrint && !sourceClient.isPrettyPrinted) { + DebuggerController.SourceScripts.getText(source).then(([, aText]) => { + if (SourceUtils.isMinified(sourceClient, aText)) { + this.togglePrettyPrint(); + } + }).then(null, e => DevToolsUtils.reportException("_onSourceSelect", e)); + } + + // Set window title. No need to split the url by " -> " here, because it was + // already sanitized when the source was added. + document.title = L10N.getFormatStr("DebuggerWindowScriptTitle", sourceItem.value); + + DebuggerView.maybeShowBlackBoxMessage(); + this.updateToolbarButtonsState(); + }, + + /** + * The click listener for the "stop black boxing" button. + */ + _onStopBlackBoxing: function() { + const { source } = this.selectedItem.attachment; + + DebuggerController.SourceScripts.setBlackBoxing(source, false) + .then(this.updateToolbarButtonsState, + this.updateToolbarButtonsState); + }, + + /** + * The click listener for a breakpoint container. + */ + _onBreakpointClick: function(e) { + let sourceItem = this.getItemForElement(e.target); + let breakpointItem = this.getItemForElement.call(sourceItem, e.target); + let attachment = breakpointItem.attachment; + + // Check if this is an enabled conditional breakpoint. + let breakpointPromise = DebuggerController.Breakpoints._getAdded(attachment); + if (breakpointPromise) { + breakpointPromise.then(aBreakpointClient => { + doHighlight.call(this, aBreakpointClient.hasCondition()); + }); + } else { + doHighlight.call(this, false); + } + + function doHighlight(aConditionalBreakpointFlag) { + // Highlight the breakpoint in this pane and in the editor. + this.highlightBreakpoint(attachment, { + // Don't show the conditional expression popup if this is not a + // conditional breakpoint, or the right mouse button was pressed (to + // avoid clashing the popup with the context menu). + openPopup: aConditionalBreakpointFlag && e.button == 0 + }); + } + }, + + /** + * The click listener for a breakpoint checkbox. + */ + _onBreakpointCheckboxClick: function(e) { + let sourceItem = this.getItemForElement(e.target); + let breakpointItem = this.getItemForElement.call(sourceItem, e.target); + let attachment = breakpointItem.attachment; + + // Toggle the breakpoint enabled or disabled. + this[attachment.disabled ? "enableBreakpoint" : "disableBreakpoint"](attachment, { + // Do this silently (don't update the checkbox checked state), since + // this listener is triggered because a checkbox was already clicked. + silent: true + }); + + // Don't update the editor location (avoid propagating into _onBreakpointClick). + e.preventDefault(); + e.stopPropagation(); + }, + + /** + * The popup showing listener for the breakpoints conditional expression panel. + */ + _onConditionalPopupShowing: function() { + this._conditionalPopupVisible = true; // Used in tests. + window.emit(EVENTS.CONDITIONAL_BREAKPOINT_POPUP_SHOWING); + }, + + /** + * The popup shown listener for the breakpoints conditional expression panel. + */ + _onConditionalPopupShown: function() { + this._cbTextbox.focus(); + this._cbTextbox.select(); + }, + + /** + * The popup hiding listener for the breakpoints conditional expression panel. + */ + _onConditionalPopupHiding: Task.async(function*() { + this._conditionalPopupVisible = false; // Used in tests. + let breakpointItem = this._selectedBreakpointItem; + let attachment = breakpointItem.attachment; + + // Check if this is an enabled conditional breakpoint, and if so, + // save the current conditional epression. + let breakpointPromise = DebuggerController.Breakpoints._getAdded(attachment); + if (breakpointPromise) { + let breakpointClient = yield breakpointPromise; + yield DebuggerController.Breakpoints.updateCondition( + breakpointClient.location, + this._cbTextbox.value + ); + } + + window.emit(EVENTS.CONDITIONAL_BREAKPOINT_POPUP_HIDING); + }), + + /** + * The keypress listener for the breakpoints conditional expression textbox. + */ + _onConditionalTextboxKeyPress: function(e) { + if (e.keyCode == e.DOM_VK_RETURN) { + this._hideConditionalPopup(); + } + }, + + /** + * Called when the add breakpoint key sequence was pressed. + */ + _onCmdAddBreakpoint: function(e) { + let url = DebuggerView.Sources.selectedValue; + let line = DebuggerView.editor.getCursor().line + 1; + let location = { url: url, line: line }; + let breakpointItem = this.getBreakpoint(location); + + // If a breakpoint already existed, remove it now. + if (breakpointItem) { + DebuggerController.Breakpoints.removeBreakpoint(location); + } + // No breakpoint existed at the required location, add one now. + else { + DebuggerController.Breakpoints.addBreakpoint(location); + } + }, + + /** + * Called when the add conditional breakpoint key sequence was pressed. + */ + _onCmdAddConditionalBreakpoint: function() { + let url = DebuggerView.Sources.selectedValue; + let line = DebuggerView.editor.getCursor().line + 1; + let location = { url: url, line: line }; + let breakpointItem = this.getBreakpoint(location); + + // If a breakpoint already existed or wasn't a conditional, morph it now. + if (breakpointItem) { + this.highlightBreakpoint(location, { openPopup: true }); + } + // No breakpoint existed at the required location, add one now. + else { + DebuggerController.Breakpoints.addBreakpoint(location, { openPopup: true }); + } + }, + + /** + * Function invoked on the "setConditional" menuitem command. + * + * @param object aLocation + * @see DebuggerController.Breakpoints.addBreakpoint + */ + _onSetConditional: function(aLocation) { + // Highlight the breakpoint and show a conditional expression popup. + this.highlightBreakpoint(aLocation, { openPopup: true }); + }, + + /** + * Function invoked on the "enableSelf" menuitem command. + * + * @param object aLocation + * @see DebuggerController.Breakpoints.addBreakpoint + */ + _onEnableSelf: function(aLocation) { + // Enable the breakpoint, in this container and the controller store. + this.enableBreakpoint(aLocation); + }, + + /** + * Function invoked on the "disableSelf" menuitem command. + * + * @param object aLocation + * @see DebuggerController.Breakpoints.addBreakpoint + */ + _onDisableSelf: function(aLocation) { + // Disable the breakpoint, in this container and the controller store. + this.disableBreakpoint(aLocation); + }, + + /** + * Function invoked on the "deleteSelf" menuitem command. + * + * @param object aLocation + * @see DebuggerController.Breakpoints.addBreakpoint + */ + _onDeleteSelf: function(aLocation) { + // Remove the breakpoint, from this container and the controller store. + this.removeBreakpoint(aLocation); + DebuggerController.Breakpoints.removeBreakpoint(aLocation); + }, + + /** + * Function invoked on the "enableOthers" menuitem command. + * + * @param object aLocation + * @see DebuggerController.Breakpoints.addBreakpoint + */ + _onEnableOthers: function(aLocation) { + let enableOthers = aCallback => { + let other = this.getOtherBreakpoints(aLocation); + let outstanding = other.map(e => this.enableBreakpoint(e.attachment)); + promise.all(outstanding).then(aCallback); + } + + // Breakpoints can only be set while the debuggee is paused. To avoid + // an avalanche of pause/resume interrupts of the main thread, simply + // pause it beforehand if it's not already. + if (gThreadClient.state != "paused") { + gThreadClient.interrupt(() => enableOthers(() => gThreadClient.resume())); + } else { + enableOthers(); + } + }, + + /** + * Function invoked on the "disableOthers" menuitem command. + * + * @param object aLocation + * @see DebuggerController.Breakpoints.addBreakpoint + */ + _onDisableOthers: function(aLocation) { + let other = this.getOtherBreakpoints(aLocation); + other.forEach(e => this._onDisableSelf(e.attachment)); + }, + + /** + * Function invoked on the "deleteOthers" menuitem command. + * + * @param object aLocation + * @see DebuggerController.Breakpoints.addBreakpoint + */ + _onDeleteOthers: function(aLocation) { + let other = this.getOtherBreakpoints(aLocation); + other.forEach(e => this._onDeleteSelf(e.attachment)); + }, + + /** + * Function invoked on the "enableAll" menuitem command. + */ + _onEnableAll: function() { + this._onEnableOthers(undefined); + }, + + /** + * Function invoked on the "disableAll" menuitem command. + */ + _onDisableAll: function() { + this._onDisableOthers(undefined); + }, + + /** + * Function invoked on the "deleteAll" menuitem command. + */ + _onDeleteAll: function() { + this._onDeleteOthers(undefined); + }, + + _commandset: null, + _popupset: null, + _cmPopup: null, + _cbPanel: null, + _cbTextbox: null, + _selectedBreakpointItem: null, + _conditionalPopupVisible: false +}); + +/** + * Functions handling the traces UI. + */ +function TracerView() { + this._selectedItem = null; + this._matchingItems = null; + this.widget = null; + + 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; + }, + + /** + * 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); + }, + + /** + * Function invoked by the "toggleTracing" command to switch the tracer state. + */ + _onToggleTracing: function() { + if (DebuggerController.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(); + DebuggerController.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(); + DebuggerController.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 DebuggerController.Tracer.WrappedObject(aValue); + 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; + DebuggerView.setEditorLocation(url, line, { noDebug: true }); + + DebuggerView.Variables.empty(); + const scope = 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: "" }); + } else { + this._populateVariable(name, scope, val); + } + } + } else { + const varName = "<" + (data.type == "throw" ? "exception" : data.type) + ">"; + this._populateVariable(varName, scope, data.returnVal); + } + + scope.expand(); + 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, depth, frameId } = aTrace; + let { parameterNames, returnVal, arguments: args } = aTrace; + let fragment = document.createDocumentFragment(); + + 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; + } +}); + +/** + * Utility functions for handling sources. + */ +let 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 /\.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 + * + * @param string aText + * The source text. + * @return boolean + * True if source text is minified. + */ + isMinified: function(sourceClient, aText){ + if (this._minifiedCache.has(sourceClient)) { + return this._minifiedCache.get(sourceClient); + } + + let isMinified; + let lineEndIndex = 0; + let lineStartIndex = 0; + let lines = 0; + let indentCount = 0; + let overCharLimit = false; + + // Strip comments. + aText = aText.replace(/\/\*[\S\s]*?\*\/|\/\/(.+|\n)/g, ""); + + while (lines++ < SAMPLE_SIZE) { + lineEndIndex = aText.indexOf("\n", lineStartIndex); + if (lineEndIndex == -1) { + break; + } + if (/^\s+/.test(aText.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; + } +}; + +/** + * Functions handling the variables bubble UI. + */ +function VariableBubbleView() { + dumpn("VariableBubbleView was instantiated"); + + this._onMouseMove = this._onMouseMove.bind(this); + this._onMouseLeave = this._onMouseLeave.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._editorContainer = document.getElementById("editor"); + this._editorContainer.addEventListener("mousemove", this._onMouseMove, false); + this._editorContainer.addEventListener("mouseleave", this._onMouseLeave, false); + + this._tooltip = new Tooltip(document, { + closeOnEvents: [{ + emitter: DebuggerController._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("mouseleave", this._onMouseLeave, 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 = 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 = DebuggerView.Sources.selectedValue; + let parsedSource = DebuggerController.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. + DebuggerController.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 = 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: () => { + DebuggerView.VariableBubble.hideContents(); + 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); + DebuggerController.StackFrames.evaluate(string); + 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: () => { + DebuggerView.VariableBubble.hideContents(); + DebuggerView.WatchExpressions.addExpression(evalPrefix, true); + } + }], DebuggerController._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({ clientX: x, clientY: y, buttons: btns }) { + // 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. + if (gThreadClient && gThreadClient.state != "paused" + || !this._tooltip.isHidden() + || (DebuggerView.editor.somethingSelected() + && btns > 0)) { + 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(x, y)); + }, + + /** + * The mouseleave listener for the source editor container node. + */ + _onMouseLeave: 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 +}; + +/** + * Functions handling the watch expressions UI. + */ +function WatchExpressionsView() { + dumpn("WatchExpressionsView was instantiated"); + + 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"); + }, + + /** + * Destruction function, called when the debugger is closed. + */ + destroy: function() { + dumpn("Destroying the WatchExpressionsView"); + + this.widget.removeEventListener("click", this._onClick, false); + }, + + /** + * 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. + 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(); + 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. + DebuggerController.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. + DebuggerController.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"; + + 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 || 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. + DebuggerController.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. + DebuggerController.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. + DebuggerController.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(); + DebuggerView.editor.focus(); + return; + } + } +}); + +/** + * Functions handling the event listeners UI. + */ +function EventListenersView() { + dumpn("EventListenersView was instantiated"); + + 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("Drag and dropEvents"); + } 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("Pointer lockEvents"); + } 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 = + DebuggerController.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; + DebuggerController.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: "" +}); + +/** + * Functions handling the global search UI. + */ +function GlobalSearchView() { + dumpn("GlobalSearchView was instantiated"); + + 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 urls = DebuggerView.Sources.values; + let sourcesFetched = DebuggerController.SourceScripts.getTextForSources(urls); + 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 [url, text] of aSources) { + // 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(url, globalResults); + + // 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 url = sourceResultsItem.instance.url; + let line = lineResultsItem.instance.line; + + DebuggerView.setEditorLocation(url, line + 1, { noDebug: true }); + + let range = lineResultsItem.lineData.range; + let cursor = DebuggerView.editor.getOffset({ line: line, ch: 0 }); + let [ anchor, head ] = DebuggerView.editor.getPosition( + cursor + range.start, + cursor + range.start + range.length + ); + + 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 +}); + +/** + * 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 aUrl + * The target source url. + * @param GlobalResults aGlobalResults + * An object containing all source results, grouped by source location. + */ +function SourceResults(aUrl, aGlobalResults) { + this.url = aUrl; + 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.url); + + 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.url; + aElementNode.className = "dbg-source-results"; + aElementNode.appendChild(resultsBox); + + SourceResults._itemsByElement.set(aElementNode, { instance: this }); + }, + + url: "", + _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["@@iterator"] = +SourceResults.prototype["@@iterator"] = +LineResults.prototype["@@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; +}; + +/** + * Preliminary setup for the DebuggerView object. + */ +DebuggerView.Sources = new SourcesView(); +DebuggerView.VariableBubble = new VariableBubbleView(); +DebuggerView.Tracer = new TracerView(); +DebuggerView.WatchExpressions = new WatchExpressionsView(); +DebuggerView.EventListeners = new EventListenersView(); +DebuggerView.GlobalSearch = new GlobalSearchView();