michael@0: /* -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ michael@0: /* vim: set ft=javascript ts=2 et sw=2 tw=80: */ michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: "use strict"; michael@0: michael@0: XPCOMUtils.defineLazyModuleGetter(this, "Task", michael@0: "resource://gre/modules/Task.jsm"); michael@0: michael@0: // Used to detect minification for automatic pretty printing michael@0: const SAMPLE_SIZE = 50; // no of lines michael@0: const INDENT_COUNT_THRESHOLD = 5; // percentage michael@0: const CHARACTER_LIMIT = 250; // line character limit michael@0: michael@0: // Maps known URLs to friendly source group names michael@0: const KNOWN_SOURCE_GROUPS = { michael@0: "Add-on SDK": "resource://gre/modules/commonjs/", michael@0: }; michael@0: michael@0: /** michael@0: * Functions handling the sources UI. michael@0: */ michael@0: function SourcesView() { michael@0: dumpn("SourcesView was instantiated"); michael@0: michael@0: this.togglePrettyPrint = this.togglePrettyPrint.bind(this); michael@0: this.toggleBlackBoxing = this.toggleBlackBoxing.bind(this); michael@0: this.toggleBreakpoints = this.toggleBreakpoints.bind(this); michael@0: michael@0: this._onEditorLoad = this._onEditorLoad.bind(this); michael@0: this._onEditorUnload = this._onEditorUnload.bind(this); michael@0: this._onEditorCursorActivity = this._onEditorCursorActivity.bind(this); michael@0: this._onSourceSelect = this._onSourceSelect.bind(this); michael@0: this._onStopBlackBoxing = this._onStopBlackBoxing.bind(this); michael@0: this._onBreakpointRemoved = this._onBreakpointRemoved.bind(this); michael@0: this._onBreakpointClick = this._onBreakpointClick.bind(this); michael@0: this._onBreakpointCheckboxClick = this._onBreakpointCheckboxClick.bind(this); michael@0: this._onConditionalPopupShowing = this._onConditionalPopupShowing.bind(this); michael@0: this._onConditionalPopupShown = this._onConditionalPopupShown.bind(this); michael@0: this._onConditionalPopupHiding = this._onConditionalPopupHiding.bind(this); michael@0: this._onConditionalTextboxKeyPress = this._onConditionalTextboxKeyPress.bind(this); michael@0: michael@0: this.updateToolbarButtonsState = this.updateToolbarButtonsState.bind(this); michael@0: } michael@0: michael@0: SourcesView.prototype = Heritage.extend(WidgetMethods, { michael@0: /** michael@0: * Initialization function, called when the debugger is started. michael@0: */ michael@0: initialize: function() { michael@0: dumpn("Initializing the SourcesView"); michael@0: michael@0: this.widget = new SideMenuWidget(document.getElementById("sources"), { michael@0: showArrows: true michael@0: }); michael@0: michael@0: // Sort known source groups towards the end of the list michael@0: this.widget.groupSortPredicate = function(a, b) { michael@0: if ((a in KNOWN_SOURCE_GROUPS) == (b in KNOWN_SOURCE_GROUPS)) { michael@0: return a.localeCompare(b); michael@0: } michael@0: michael@0: return (a in KNOWN_SOURCE_GROUPS) ? 1 : -1; michael@0: }; michael@0: michael@0: this.emptyText = L10N.getStr("noSourcesText"); michael@0: this._blackBoxCheckboxTooltip = L10N.getStr("blackBoxCheckboxTooltip"); michael@0: michael@0: this._commandset = document.getElementById("debuggerCommands"); michael@0: this._popupset = document.getElementById("debuggerPopupset"); michael@0: this._cmPopup = document.getElementById("sourceEditorContextMenu"); michael@0: this._cbPanel = document.getElementById("conditional-breakpoint-panel"); michael@0: this._cbTextbox = document.getElementById("conditional-breakpoint-panel-textbox"); michael@0: this._blackBoxButton = document.getElementById("black-box"); michael@0: this._stopBlackBoxButton = document.getElementById("black-boxed-message-button"); michael@0: this._prettyPrintButton = document.getElementById("pretty-print"); michael@0: this._toggleBreakpointsButton = document.getElementById("toggle-breakpoints"); michael@0: michael@0: if (Prefs.prettyPrintEnabled) { michael@0: this._prettyPrintButton.removeAttribute("hidden"); michael@0: } michael@0: michael@0: window.on(EVENTS.EDITOR_LOADED, this._onEditorLoad, false); michael@0: window.on(EVENTS.EDITOR_UNLOADED, this._onEditorUnload, false); michael@0: this.widget.addEventListener("select", this._onSourceSelect, false); michael@0: this._stopBlackBoxButton.addEventListener("click", this._onStopBlackBoxing, false); michael@0: this._cbPanel.addEventListener("popupshowing", this._onConditionalPopupShowing, false); michael@0: this._cbPanel.addEventListener("popupshown", this._onConditionalPopupShown, false); michael@0: this._cbPanel.addEventListener("popuphiding", this._onConditionalPopupHiding, false); michael@0: this._cbTextbox.addEventListener("keypress", this._onConditionalTextboxKeyPress, false); michael@0: michael@0: this.autoFocusOnSelection = false; michael@0: michael@0: // Sort the contents by the displayed label. michael@0: this.sortContents((aFirst, aSecond) => { michael@0: return +(aFirst.attachment.label.toLowerCase() > michael@0: aSecond.attachment.label.toLowerCase()); michael@0: }); michael@0: }, michael@0: michael@0: /** michael@0: * Destruction function, called when the debugger is closed. michael@0: */ michael@0: destroy: function() { michael@0: dumpn("Destroying the SourcesView"); michael@0: michael@0: window.off(EVENTS.EDITOR_LOADED, this._onEditorLoad, false); michael@0: window.off(EVENTS.EDITOR_UNLOADED, this._onEditorUnload, false); michael@0: this.widget.removeEventListener("select", this._onSourceSelect, false); michael@0: this._stopBlackBoxButton.removeEventListener("click", this._onStopBlackBoxing, false); michael@0: this._cbPanel.removeEventListener("popupshowing", this._onConditionalPopupShowing, false); michael@0: this._cbPanel.removeEventListener("popupshowing", this._onConditionalPopupShown, false); michael@0: this._cbPanel.removeEventListener("popuphiding", this._onConditionalPopupHiding, false); michael@0: this._cbTextbox.removeEventListener("keypress", this._onConditionalTextboxKeyPress, false); michael@0: }, michael@0: michael@0: /** michael@0: * Sets the preferred location to be selected in this sources container. michael@0: * @param string aUrl michael@0: */ michael@0: set preferredSource(aUrl) { michael@0: this._preferredValue = aUrl; michael@0: michael@0: // Selects the element with the specified value in this sources container, michael@0: // if already inserted. michael@0: if (this.containsValue(aUrl)) { michael@0: this.selectedValue = aUrl; michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Adds a source to this sources container. michael@0: * michael@0: * @param object aSource michael@0: * The source object coming from the active thread. michael@0: * @param object aOptions [optional] michael@0: * Additional options for adding the source. Supported options: michael@0: * - staged: true to stage the item to be appended later michael@0: */ michael@0: addSource: function(aSource, aOptions = {}) { michael@0: let fullUrl = aSource.url; michael@0: let url = fullUrl.split(" -> ").pop(); michael@0: let label = aSource.addonPath ? aSource.addonPath : SourceUtils.getSourceLabel(url); michael@0: let group = aSource.addonID ? aSource.addonID : SourceUtils.getSourceGroup(url); michael@0: let unicodeUrl = NetworkHelper.convertToUnicode(unescape(fullUrl)); michael@0: michael@0: let contents = document.createElement("label"); michael@0: contents.className = "plain dbg-source-item"; michael@0: contents.setAttribute("value", label); michael@0: contents.setAttribute("crop", "start"); michael@0: contents.setAttribute("flex", "1"); michael@0: contents.setAttribute("tooltiptext", unicodeUrl); michael@0: michael@0: // Append a source item to this container. michael@0: this.push([contents, fullUrl], { michael@0: staged: aOptions.staged, /* stage the item to be appended later? */ michael@0: attachment: { michael@0: label: label, michael@0: group: group, michael@0: checkboxState: !aSource.isBlackBoxed, michael@0: checkboxTooltip: this._blackBoxCheckboxTooltip, michael@0: source: aSource michael@0: } michael@0: }); michael@0: }, michael@0: michael@0: /** michael@0: * Adds a breakpoint to this sources container. michael@0: * michael@0: * @param object aBreakpointData michael@0: * Information about the breakpoint to be shown. michael@0: * This object must have the following properties: michael@0: * - location: the breakpoint's source location and line number michael@0: * - disabled: the breakpoint's disabled state, boolean michael@0: * - text: the breakpoint's line text to be displayed michael@0: * @param object aOptions [optional] michael@0: * @see DebuggerController.Breakpoints.addBreakpoint michael@0: */ michael@0: addBreakpoint: function(aBreakpointData, aOptions = {}) { michael@0: let { location, disabled } = aBreakpointData; michael@0: michael@0: // Make sure we're not duplicating anything. If a breakpoint at the michael@0: // specified source url and line already exists, just toggle it. michael@0: if (this.getBreakpoint(location)) { michael@0: this[disabled ? "disableBreakpoint" : "enableBreakpoint"](location); michael@0: return; michael@0: } michael@0: michael@0: // Get the source item to which the breakpoint should be attached. michael@0: let sourceItem = this.getItemByValue(location.url); michael@0: michael@0: // Create the element node and menu popup for the breakpoint item. michael@0: let breakpointArgs = Heritage.extend(aBreakpointData, aOptions); michael@0: let breakpointView = this._createBreakpointView.call(this, breakpointArgs); michael@0: let contextMenu = this._createContextMenu.call(this, breakpointArgs); michael@0: michael@0: // Append a breakpoint child item to the corresponding source item. michael@0: sourceItem.append(breakpointView.container, { michael@0: attachment: Heritage.extend(breakpointArgs, { michael@0: url: location.url, michael@0: line: location.line, michael@0: view: breakpointView, michael@0: popup: contextMenu michael@0: }), michael@0: attributes: [ michael@0: ["contextmenu", contextMenu.menupopupId] michael@0: ], michael@0: // Make sure that when the breakpoint item is removed, the corresponding michael@0: // menupopup and commandset are also destroyed. michael@0: finalize: this._onBreakpointRemoved michael@0: }); michael@0: michael@0: // Highlight the newly appended breakpoint child item if necessary. michael@0: if (aOptions.openPopup || !aOptions.noEditorUpdate) { michael@0: this.highlightBreakpoint(location, aOptions); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Removes a breakpoint from this sources container. michael@0: * It does not also remove the breakpoint from the controller. Be careful. michael@0: * michael@0: * @param object aLocation michael@0: * @see DebuggerController.Breakpoints.addBreakpoint michael@0: */ michael@0: removeBreakpoint: function(aLocation) { michael@0: // When a parent source item is removed, all the child breakpoint items are michael@0: // also automagically removed. michael@0: let sourceItem = this.getItemByValue(aLocation.url); michael@0: if (!sourceItem) { michael@0: return; michael@0: } michael@0: let breakpointItem = this.getBreakpoint(aLocation); michael@0: if (!breakpointItem) { michael@0: return; michael@0: } michael@0: michael@0: // Clear the breakpoint view. michael@0: sourceItem.remove(breakpointItem); michael@0: }, michael@0: michael@0: /** michael@0: * Returns the breakpoint at the specified source url and line. michael@0: * michael@0: * @param object aLocation michael@0: * @see DebuggerController.Breakpoints.addBreakpoint michael@0: * @return object michael@0: * The corresponding breakpoint item if found, null otherwise. michael@0: */ michael@0: getBreakpoint: function(aLocation) { michael@0: return this.getItemForPredicate(aItem => michael@0: aItem.attachment.url == aLocation.url && michael@0: aItem.attachment.line == aLocation.line); michael@0: }, michael@0: michael@0: /** michael@0: * Returns all breakpoints for all sources. michael@0: * michael@0: * @return array michael@0: * The breakpoints for all sources if any, an empty array otherwise. michael@0: */ michael@0: getAllBreakpoints: function(aStore = []) { michael@0: return this.getOtherBreakpoints(undefined, aStore); michael@0: }, michael@0: michael@0: /** michael@0: * Returns all breakpoints which are not at the specified source url and line. michael@0: * michael@0: * @param object aLocation [optional] michael@0: * @see DebuggerController.Breakpoints.addBreakpoint michael@0: * @param array aStore [optional] michael@0: * A list in which to store the corresponding breakpoints. michael@0: * @return array michael@0: * The corresponding breakpoints if found, an empty array otherwise. michael@0: */ michael@0: getOtherBreakpoints: function(aLocation = {}, aStore = []) { michael@0: for (let source of this) { michael@0: for (let breakpointItem of source) { michael@0: let { url, line } = breakpointItem.attachment; michael@0: if (url != aLocation.url || line != aLocation.line) { michael@0: aStore.push(breakpointItem); michael@0: } michael@0: } michael@0: } michael@0: return aStore; michael@0: }, michael@0: michael@0: /** michael@0: * Enables a breakpoint. michael@0: * michael@0: * @param object aLocation michael@0: * @see DebuggerController.Breakpoints.addBreakpoint michael@0: * @param object aOptions [optional] michael@0: * Additional options or flags supported by this operation: michael@0: * - silent: pass true to not update the checkbox checked state; michael@0: * this is usually necessary when the checked state will michael@0: * be updated automatically (e.g: on a checkbox click). michael@0: * @return object michael@0: * A promise that is resolved after the breakpoint is enabled, or michael@0: * rejected if no breakpoint was found at the specified location. michael@0: */ michael@0: enableBreakpoint: function(aLocation, aOptions = {}) { michael@0: let breakpointItem = this.getBreakpoint(aLocation); michael@0: if (!breakpointItem) { michael@0: return promise.reject(new Error("No breakpoint found.")); michael@0: } michael@0: michael@0: // Breakpoint will now be enabled. michael@0: let attachment = breakpointItem.attachment; michael@0: attachment.disabled = false; michael@0: michael@0: // Update the corresponding menu items to reflect the enabled state. michael@0: let prefix = "bp-cMenu-"; // "breakpoints context menu" michael@0: let identifier = DebuggerController.Breakpoints.getIdentifier(attachment); michael@0: let enableSelfId = prefix + "enableSelf-" + identifier + "-menuitem"; michael@0: let disableSelfId = prefix + "disableSelf-" + identifier + "-menuitem"; michael@0: document.getElementById(enableSelfId).setAttribute("hidden", "true"); michael@0: document.getElementById(disableSelfId).removeAttribute("hidden"); michael@0: michael@0: // Update the breakpoint toggle button checked state. michael@0: this._toggleBreakpointsButton.removeAttribute("checked"); michael@0: michael@0: // Update the checkbox state if necessary. michael@0: if (!aOptions.silent) { michael@0: attachment.view.checkbox.setAttribute("checked", "true"); michael@0: } michael@0: michael@0: return DebuggerController.Breakpoints.addBreakpoint(aLocation, { michael@0: // No need to update the pane, since this method is invoked because michael@0: // a breakpoint's view was interacted with. michael@0: noPaneUpdate: true michael@0: }); michael@0: }, michael@0: michael@0: /** michael@0: * Disables a breakpoint. michael@0: * michael@0: * @param object aLocation michael@0: * @see DebuggerController.Breakpoints.addBreakpoint michael@0: * @param object aOptions [optional] michael@0: * Additional options or flags supported by this operation: michael@0: * - silent: pass true to not update the checkbox checked state; michael@0: * this is usually necessary when the checked state will michael@0: * be updated automatically (e.g: on a checkbox click). michael@0: * @return object michael@0: * A promise that is resolved after the breakpoint is disabled, or michael@0: * rejected if no breakpoint was found at the specified location. michael@0: */ michael@0: disableBreakpoint: function(aLocation, aOptions = {}) { michael@0: let breakpointItem = this.getBreakpoint(aLocation); michael@0: if (!breakpointItem) { michael@0: return promise.reject(new Error("No breakpoint found.")); michael@0: } michael@0: michael@0: // Breakpoint will now be disabled. michael@0: let attachment = breakpointItem.attachment; michael@0: attachment.disabled = true; michael@0: michael@0: // Update the corresponding menu items to reflect the disabled state. michael@0: let prefix = "bp-cMenu-"; // "breakpoints context menu" michael@0: let identifier = DebuggerController.Breakpoints.getIdentifier(attachment); michael@0: let enableSelfId = prefix + "enableSelf-" + identifier + "-menuitem"; michael@0: let disableSelfId = prefix + "disableSelf-" + identifier + "-menuitem"; michael@0: document.getElementById(enableSelfId).removeAttribute("hidden"); michael@0: document.getElementById(disableSelfId).setAttribute("hidden", "true"); michael@0: michael@0: // Update the checkbox state if necessary. michael@0: if (!aOptions.silent) { michael@0: attachment.view.checkbox.removeAttribute("checked"); michael@0: } michael@0: michael@0: return DebuggerController.Breakpoints.removeBreakpoint(aLocation, { michael@0: // No need to update this pane, since this method is invoked because michael@0: // a breakpoint's view was interacted with. michael@0: noPaneUpdate: true, michael@0: // Mark this breakpoint as being "disabled", not completely removed. michael@0: // This makes sure it will not be forgotten across target navigations. michael@0: rememberDisabled: true michael@0: }); michael@0: }, michael@0: michael@0: /** michael@0: * Highlights a breakpoint in this sources container. michael@0: * michael@0: * @param object aLocation michael@0: * @see DebuggerController.Breakpoints.addBreakpoint michael@0: * @param object aOptions [optional] michael@0: * An object containing some of the following boolean properties: michael@0: * - openPopup: tells if the expression popup should be shown. michael@0: * - noEditorUpdate: tells if you want to skip editor updates. michael@0: */ michael@0: highlightBreakpoint: function(aLocation, aOptions = {}) { michael@0: let breakpointItem = this.getBreakpoint(aLocation); michael@0: if (!breakpointItem) { michael@0: return; michael@0: } michael@0: michael@0: // Breakpoint will now be selected. michael@0: this._selectBreakpoint(breakpointItem); michael@0: michael@0: // Update the editor location if necessary. michael@0: if (!aOptions.noEditorUpdate) { michael@0: DebuggerView.setEditorLocation(aLocation.url, aLocation.line, { noDebug: true }); michael@0: } michael@0: michael@0: // If the breakpoint requires a new conditional expression, display michael@0: // the panel to input the corresponding expression. michael@0: if (aOptions.openPopup) { michael@0: this._openConditionalPopup(); michael@0: } else { michael@0: this._hideConditionalPopup(); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Unhighlights the current breakpoint in this sources container. michael@0: */ michael@0: unhighlightBreakpoint: function() { michael@0: this._unselectBreakpoint(); michael@0: this._hideConditionalPopup(); michael@0: }, michael@0: michael@0: /** michael@0: * Update the checked/unchecked and enabled/disabled states of the buttons in michael@0: * the sources toolbar based on the currently selected source's state. michael@0: */ michael@0: updateToolbarButtonsState: function() { michael@0: const { source } = this.selectedItem.attachment; michael@0: const sourceClient = gThreadClient.source(source); michael@0: michael@0: if (sourceClient.isBlackBoxed) { michael@0: this._prettyPrintButton.setAttribute("disabled", true); michael@0: this._blackBoxButton.setAttribute("checked", true); michael@0: } else { michael@0: this._prettyPrintButton.removeAttribute("disabled"); michael@0: this._blackBoxButton.removeAttribute("checked"); michael@0: } michael@0: michael@0: if (sourceClient.isPrettyPrinted) { michael@0: this._prettyPrintButton.setAttribute("checked", true); michael@0: } else { michael@0: this._prettyPrintButton.removeAttribute("checked"); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Toggle the pretty printing of the selected source. michael@0: */ michael@0: togglePrettyPrint: function() { michael@0: if (this._prettyPrintButton.hasAttribute("disabled")) { michael@0: return; michael@0: } michael@0: michael@0: const resetEditor = ([{ url }]) => { michael@0: // Only set the text when the source is still selected. michael@0: if (url == this.selectedValue) { michael@0: DebuggerView.setEditorLocation(url, 0, { force: true }); michael@0: } michael@0: }; michael@0: michael@0: const printError = ([{ url }, error]) => { michael@0: DevToolsUtils.reportException("togglePrettyPrint", error); michael@0: }; michael@0: michael@0: DebuggerView.showProgressBar(); michael@0: const { source } = this.selectedItem.attachment; michael@0: const sourceClient = gThreadClient.source(source); michael@0: const shouldPrettyPrint = !sourceClient.isPrettyPrinted; michael@0: michael@0: if (shouldPrettyPrint) { michael@0: this._prettyPrintButton.setAttribute("checked", true); michael@0: } else { michael@0: this._prettyPrintButton.removeAttribute("checked"); michael@0: } michael@0: michael@0: DebuggerController.SourceScripts.togglePrettyPrint(source) michael@0: .then(resetEditor, printError) michael@0: .then(DebuggerView.showEditor) michael@0: .then(this.updateToolbarButtonsState); michael@0: }, michael@0: michael@0: /** michael@0: * Toggle the black boxed state of the selected source. michael@0: */ michael@0: toggleBlackBoxing: function() { michael@0: const { source } = this.selectedItem.attachment; michael@0: const sourceClient = gThreadClient.source(source); michael@0: const shouldBlackBox = !sourceClient.isBlackBoxed; michael@0: michael@0: // Be optimistic that the (un-)black boxing will succeed, so enable/disable michael@0: // the pretty print button and check/uncheck the black box button michael@0: // immediately. Then, once we actually get the results from the server, make michael@0: // sure that it is in the correct state again by calling michael@0: // `updateToolbarButtonsState`. michael@0: michael@0: if (shouldBlackBox) { michael@0: this._prettyPrintButton.setAttribute("disabled", true); michael@0: this._blackBoxButton.setAttribute("checked", true); michael@0: } else { michael@0: this._prettyPrintButton.removeAttribute("disabled"); michael@0: this._blackBoxButton.removeAttribute("checked"); michael@0: } michael@0: michael@0: DebuggerController.SourceScripts.setBlackBoxing(source, shouldBlackBox) michael@0: .then(this.updateToolbarButtonsState, michael@0: this.updateToolbarButtonsState); michael@0: }, michael@0: michael@0: /** michael@0: * Toggles all breakpoints enabled/disabled. michael@0: */ michael@0: toggleBreakpoints: function() { michael@0: let breakpoints = this.getAllBreakpoints(); michael@0: let hasBreakpoints = breakpoints.length > 0; michael@0: let hasEnabledBreakpoints = breakpoints.some(e => !e.attachment.disabled); michael@0: michael@0: if (hasBreakpoints && hasEnabledBreakpoints) { michael@0: this._toggleBreakpointsButton.setAttribute("checked", true); michael@0: this._onDisableAll(); michael@0: } else { michael@0: this._toggleBreakpointsButton.removeAttribute("checked"); michael@0: this._onEnableAll(); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Marks a breakpoint as selected in this sources container. michael@0: * michael@0: * @param object aItem michael@0: * The breakpoint item to select. michael@0: */ michael@0: _selectBreakpoint: function(aItem) { michael@0: if (this._selectedBreakpointItem == aItem) { michael@0: return; michael@0: } michael@0: this._unselectBreakpoint(); michael@0: this._selectedBreakpointItem = aItem; michael@0: this._selectedBreakpointItem.target.classList.add("selected"); michael@0: michael@0: // Ensure the currently selected breakpoint is visible. michael@0: this.widget.ensureElementIsVisible(aItem.target); michael@0: }, michael@0: michael@0: /** michael@0: * Marks the current breakpoint as unselected in this sources container. michael@0: */ michael@0: _unselectBreakpoint: function() { michael@0: if (!this._selectedBreakpointItem) { michael@0: return; michael@0: } michael@0: this._selectedBreakpointItem.target.classList.remove("selected"); michael@0: this._selectedBreakpointItem = null; michael@0: }, michael@0: michael@0: /** michael@0: * Opens a conditional breakpoint's expression input popup. michael@0: */ michael@0: _openConditionalPopup: function() { michael@0: let breakpointItem = this._selectedBreakpointItem; michael@0: let attachment = breakpointItem.attachment; michael@0: // Check if this is an enabled conditional breakpoint, and if so, michael@0: // retrieve the current conditional epression. michael@0: let breakpointPromise = DebuggerController.Breakpoints._getAdded(attachment); michael@0: if (breakpointPromise) { michael@0: breakpointPromise.then(aBreakpointClient => { michael@0: let isConditionalBreakpoint = aBreakpointClient.hasCondition(); michael@0: let condition = aBreakpointClient.getCondition(); michael@0: doOpen.call(this, isConditionalBreakpoint ? condition : "") michael@0: }); michael@0: } else { michael@0: doOpen.call(this, "") michael@0: } michael@0: michael@0: function doOpen(aConditionalExpression) { michael@0: // Update the conditional expression textbox. If no expression was michael@0: // previously set, revert to using an empty string by default. michael@0: this._cbTextbox.value = aConditionalExpression; michael@0: michael@0: // Show the conditional expression panel. The popup arrow should be pointing michael@0: // at the line number node in the breakpoint item view. michael@0: this._cbPanel.hidden = false; michael@0: this._cbPanel.openPopup(breakpointItem.attachment.view.lineNumber, michael@0: BREAKPOINT_CONDITIONAL_POPUP_POSITION, michael@0: BREAKPOINT_CONDITIONAL_POPUP_OFFSET_X, michael@0: BREAKPOINT_CONDITIONAL_POPUP_OFFSET_Y); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Hides a conditional breakpoint's expression input popup. michael@0: */ michael@0: _hideConditionalPopup: function() { michael@0: this._cbPanel.hidden = true; michael@0: michael@0: // Sometimes this._cbPanel doesn't have hidePopup method which doesn't michael@0: // break anything but simply outputs an exception to the console. michael@0: if (this._cbPanel.hidePopup) { michael@0: this._cbPanel.hidePopup(); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Customization function for creating a breakpoint item's UI. michael@0: * michael@0: * @param object aOptions michael@0: * A couple of options or flags supported by this operation: michael@0: * - location: the breakpoint's source location and line number michael@0: * - disabled: the breakpoint's disabled state, boolean michael@0: * - text: the breakpoint's line text to be displayed michael@0: * @return object michael@0: * An object containing the breakpoint container, checkbox, michael@0: * line number and line text nodes. michael@0: */ michael@0: _createBreakpointView: function(aOptions) { michael@0: let { location, disabled, text } = aOptions; michael@0: let identifier = DebuggerController.Breakpoints.getIdentifier(location); michael@0: michael@0: let checkbox = document.createElement("checkbox"); michael@0: checkbox.setAttribute("checked", !disabled); michael@0: checkbox.className = "dbg-breakpoint-checkbox"; michael@0: michael@0: let lineNumberNode = document.createElement("label"); michael@0: lineNumberNode.className = "plain dbg-breakpoint-line"; michael@0: lineNumberNode.setAttribute("value", location.line); michael@0: michael@0: let lineTextNode = document.createElement("label"); michael@0: lineTextNode.className = "plain dbg-breakpoint-text"; michael@0: lineTextNode.setAttribute("value", text); michael@0: lineTextNode.setAttribute("crop", "end"); michael@0: lineTextNode.setAttribute("flex", "1"); michael@0: michael@0: let tooltip = text.substr(0, BREAKPOINT_LINE_TOOLTIP_MAX_LENGTH); michael@0: lineTextNode.setAttribute("tooltiptext", tooltip); michael@0: michael@0: let container = document.createElement("hbox"); michael@0: container.id = "breakpoint-" + identifier; michael@0: container.className = "dbg-breakpoint side-menu-widget-item-other"; michael@0: container.classList.add("devtools-monospace"); michael@0: container.setAttribute("align", "center"); michael@0: container.setAttribute("flex", "1"); michael@0: michael@0: container.addEventListener("click", this._onBreakpointClick, false); michael@0: checkbox.addEventListener("click", this._onBreakpointCheckboxClick, false); michael@0: michael@0: container.appendChild(checkbox); michael@0: container.appendChild(lineNumberNode); michael@0: container.appendChild(lineTextNode); michael@0: michael@0: return { michael@0: container: container, michael@0: checkbox: checkbox, michael@0: lineNumber: lineNumberNode, michael@0: lineText: lineTextNode michael@0: }; michael@0: }, michael@0: michael@0: /** michael@0: * Creates a context menu for a breakpoint element. michael@0: * michael@0: * @param object aOptions michael@0: * A couple of options or flags supported by this operation: michael@0: * - location: the breakpoint's source location and line number michael@0: * - disabled: the breakpoint's disabled state, boolean michael@0: * @return object michael@0: * An object containing the breakpoint commandset and menu popup ids. michael@0: */ michael@0: _createContextMenu: function(aOptions) { michael@0: let { location, disabled } = aOptions; michael@0: let identifier = DebuggerController.Breakpoints.getIdentifier(location); michael@0: michael@0: let commandset = document.createElement("commandset"); michael@0: let menupopup = document.createElement("menupopup"); michael@0: commandset.id = "bp-cSet-" + identifier; michael@0: menupopup.id = "bp-mPop-" + identifier; michael@0: michael@0: createMenuItem.call(this, "enableSelf", !disabled); michael@0: createMenuItem.call(this, "disableSelf", disabled); michael@0: createMenuItem.call(this, "deleteSelf"); michael@0: createMenuSeparator(); michael@0: createMenuItem.call(this, "setConditional"); michael@0: createMenuSeparator(); michael@0: createMenuItem.call(this, "enableOthers"); michael@0: createMenuItem.call(this, "disableOthers"); michael@0: createMenuItem.call(this, "deleteOthers"); michael@0: createMenuSeparator(); michael@0: createMenuItem.call(this, "enableAll"); michael@0: createMenuItem.call(this, "disableAll"); michael@0: createMenuSeparator(); michael@0: createMenuItem.call(this, "deleteAll"); michael@0: michael@0: this._popupset.appendChild(menupopup); michael@0: this._commandset.appendChild(commandset); michael@0: michael@0: return { michael@0: commandsetId: commandset.id, michael@0: menupopupId: menupopup.id michael@0: }; michael@0: michael@0: /** michael@0: * Creates a menu item specified by a name with the appropriate attributes michael@0: * (label and handler). michael@0: * michael@0: * @param string aName michael@0: * A global identifier for the menu item. michael@0: * @param boolean aHiddenFlag michael@0: * True if this menuitem should be hidden. michael@0: */ michael@0: function createMenuItem(aName, aHiddenFlag) { michael@0: let menuitem = document.createElement("menuitem"); michael@0: let command = document.createElement("command"); michael@0: michael@0: let prefix = "bp-cMenu-"; // "breakpoints context menu" michael@0: let commandId = prefix + aName + "-" + identifier + "-command"; michael@0: let menuitemId = prefix + aName + "-" + identifier + "-menuitem"; michael@0: michael@0: let label = L10N.getStr("breakpointMenuItem." + aName); michael@0: let func = "_on" + aName.charAt(0).toUpperCase() + aName.slice(1); michael@0: michael@0: command.id = commandId; michael@0: command.setAttribute("label", label); michael@0: command.addEventListener("command", () => this[func](location), false); michael@0: michael@0: menuitem.id = menuitemId; michael@0: menuitem.setAttribute("command", commandId); michael@0: aHiddenFlag && menuitem.setAttribute("hidden", "true"); michael@0: michael@0: commandset.appendChild(command); michael@0: menupopup.appendChild(menuitem); michael@0: } michael@0: michael@0: /** michael@0: * Creates a simple menu separator element and appends it to the current michael@0: * menupopup hierarchy. michael@0: */ michael@0: function createMenuSeparator() { michael@0: let menuseparator = document.createElement("menuseparator"); michael@0: menupopup.appendChild(menuseparator); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Function called each time a breakpoint item is removed. michael@0: * michael@0: * @param object aItem michael@0: * The corresponding item. michael@0: */ michael@0: _onBreakpointRemoved: function(aItem) { michael@0: dumpn("Finalizing breakpoint item: " + aItem); michael@0: michael@0: // Destroy the context menu for the breakpoint. michael@0: let contextMenu = aItem.attachment.popup; michael@0: document.getElementById(contextMenu.commandsetId).remove(); michael@0: document.getElementById(contextMenu.menupopupId).remove(); michael@0: michael@0: // Clear the breakpoint selection. michael@0: if (this._selectedBreakpointItem == aItem) { michael@0: this._selectedBreakpointItem = null; michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * The load listener for the source editor. michael@0: */ michael@0: _onEditorLoad: function(aName, aEditor) { michael@0: aEditor.on("cursorActivity", this._onEditorCursorActivity); michael@0: }, michael@0: michael@0: /** michael@0: * The unload listener for the source editor. michael@0: */ michael@0: _onEditorUnload: function(aName, aEditor) { michael@0: aEditor.off("cursorActivity", this._onEditorCursorActivity); michael@0: }, michael@0: michael@0: /** michael@0: * The selection listener for the source editor. michael@0: */ michael@0: _onEditorCursorActivity: function(e) { michael@0: let editor = DebuggerView.editor; michael@0: let start = editor.getCursor("start").line + 1; michael@0: let end = editor.getCursor().line + 1; michael@0: let url = this.selectedValue; michael@0: michael@0: let location = { url: url, line: start }; michael@0: michael@0: if (this.getBreakpoint(location) && start == end) { michael@0: this.highlightBreakpoint(location, { noEditorUpdate: true }); michael@0: } else { michael@0: this.unhighlightBreakpoint(); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * The select listener for the sources container. michael@0: */ michael@0: _onSourceSelect: function({ detail: sourceItem }) { michael@0: if (!sourceItem) { michael@0: return; michael@0: } michael@0: const { source } = sourceItem.attachment; michael@0: const sourceClient = gThreadClient.source(source); michael@0: michael@0: // The container is not empty and an actual item was selected. michael@0: DebuggerView.setEditorLocation(sourceItem.value); michael@0: michael@0: if (Prefs.autoPrettyPrint && !sourceClient.isPrettyPrinted) { michael@0: DebuggerController.SourceScripts.getText(source).then(([, aText]) => { michael@0: if (SourceUtils.isMinified(sourceClient, aText)) { michael@0: this.togglePrettyPrint(); michael@0: } michael@0: }).then(null, e => DevToolsUtils.reportException("_onSourceSelect", e)); michael@0: } michael@0: michael@0: // Set window title. No need to split the url by " -> " here, because it was michael@0: // already sanitized when the source was added. michael@0: document.title = L10N.getFormatStr("DebuggerWindowScriptTitle", sourceItem.value); michael@0: michael@0: DebuggerView.maybeShowBlackBoxMessage(); michael@0: this.updateToolbarButtonsState(); michael@0: }, michael@0: michael@0: /** michael@0: * The click listener for the "stop black boxing" button. michael@0: */ michael@0: _onStopBlackBoxing: function() { michael@0: const { source } = this.selectedItem.attachment; michael@0: michael@0: DebuggerController.SourceScripts.setBlackBoxing(source, false) michael@0: .then(this.updateToolbarButtonsState, michael@0: this.updateToolbarButtonsState); michael@0: }, michael@0: michael@0: /** michael@0: * The click listener for a breakpoint container. michael@0: */ michael@0: _onBreakpointClick: function(e) { michael@0: let sourceItem = this.getItemForElement(e.target); michael@0: let breakpointItem = this.getItemForElement.call(sourceItem, e.target); michael@0: let attachment = breakpointItem.attachment; michael@0: michael@0: // Check if this is an enabled conditional breakpoint. michael@0: let breakpointPromise = DebuggerController.Breakpoints._getAdded(attachment); michael@0: if (breakpointPromise) { michael@0: breakpointPromise.then(aBreakpointClient => { michael@0: doHighlight.call(this, aBreakpointClient.hasCondition()); michael@0: }); michael@0: } else { michael@0: doHighlight.call(this, false); michael@0: } michael@0: michael@0: function doHighlight(aConditionalBreakpointFlag) { michael@0: // Highlight the breakpoint in this pane and in the editor. michael@0: this.highlightBreakpoint(attachment, { michael@0: // Don't show the conditional expression popup if this is not a michael@0: // conditional breakpoint, or the right mouse button was pressed (to michael@0: // avoid clashing the popup with the context menu). michael@0: openPopup: aConditionalBreakpointFlag && e.button == 0 michael@0: }); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * The click listener for a breakpoint checkbox. michael@0: */ michael@0: _onBreakpointCheckboxClick: function(e) { michael@0: let sourceItem = this.getItemForElement(e.target); michael@0: let breakpointItem = this.getItemForElement.call(sourceItem, e.target); michael@0: let attachment = breakpointItem.attachment; michael@0: michael@0: // Toggle the breakpoint enabled or disabled. michael@0: this[attachment.disabled ? "enableBreakpoint" : "disableBreakpoint"](attachment, { michael@0: // Do this silently (don't update the checkbox checked state), since michael@0: // this listener is triggered because a checkbox was already clicked. michael@0: silent: true michael@0: }); michael@0: michael@0: // Don't update the editor location (avoid propagating into _onBreakpointClick). michael@0: e.preventDefault(); michael@0: e.stopPropagation(); michael@0: }, michael@0: michael@0: /** michael@0: * The popup showing listener for the breakpoints conditional expression panel. michael@0: */ michael@0: _onConditionalPopupShowing: function() { michael@0: this._conditionalPopupVisible = true; // Used in tests. michael@0: window.emit(EVENTS.CONDITIONAL_BREAKPOINT_POPUP_SHOWING); michael@0: }, michael@0: michael@0: /** michael@0: * The popup shown listener for the breakpoints conditional expression panel. michael@0: */ michael@0: _onConditionalPopupShown: function() { michael@0: this._cbTextbox.focus(); michael@0: this._cbTextbox.select(); michael@0: }, michael@0: michael@0: /** michael@0: * The popup hiding listener for the breakpoints conditional expression panel. michael@0: */ michael@0: _onConditionalPopupHiding: Task.async(function*() { michael@0: this._conditionalPopupVisible = false; // Used in tests. michael@0: let breakpointItem = this._selectedBreakpointItem; michael@0: let attachment = breakpointItem.attachment; michael@0: michael@0: // Check if this is an enabled conditional breakpoint, and if so, michael@0: // save the current conditional epression. michael@0: let breakpointPromise = DebuggerController.Breakpoints._getAdded(attachment); michael@0: if (breakpointPromise) { michael@0: let breakpointClient = yield breakpointPromise; michael@0: yield DebuggerController.Breakpoints.updateCondition( michael@0: breakpointClient.location, michael@0: this._cbTextbox.value michael@0: ); michael@0: } michael@0: michael@0: window.emit(EVENTS.CONDITIONAL_BREAKPOINT_POPUP_HIDING); michael@0: }), michael@0: michael@0: /** michael@0: * The keypress listener for the breakpoints conditional expression textbox. michael@0: */ michael@0: _onConditionalTextboxKeyPress: function(e) { michael@0: if (e.keyCode == e.DOM_VK_RETURN) { michael@0: this._hideConditionalPopup(); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Called when the add breakpoint key sequence was pressed. michael@0: */ michael@0: _onCmdAddBreakpoint: function(e) { michael@0: let url = DebuggerView.Sources.selectedValue; michael@0: let line = DebuggerView.editor.getCursor().line + 1; michael@0: let location = { url: url, line: line }; michael@0: let breakpointItem = this.getBreakpoint(location); michael@0: michael@0: // If a breakpoint already existed, remove it now. michael@0: if (breakpointItem) { michael@0: DebuggerController.Breakpoints.removeBreakpoint(location); michael@0: } michael@0: // No breakpoint existed at the required location, add one now. michael@0: else { michael@0: DebuggerController.Breakpoints.addBreakpoint(location); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Called when the add conditional breakpoint key sequence was pressed. michael@0: */ michael@0: _onCmdAddConditionalBreakpoint: function() { michael@0: let url = DebuggerView.Sources.selectedValue; michael@0: let line = DebuggerView.editor.getCursor().line + 1; michael@0: let location = { url: url, line: line }; michael@0: let breakpointItem = this.getBreakpoint(location); michael@0: michael@0: // If a breakpoint already existed or wasn't a conditional, morph it now. michael@0: if (breakpointItem) { michael@0: this.highlightBreakpoint(location, { openPopup: true }); michael@0: } michael@0: // No breakpoint existed at the required location, add one now. michael@0: else { michael@0: DebuggerController.Breakpoints.addBreakpoint(location, { openPopup: true }); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Function invoked on the "setConditional" menuitem command. michael@0: * michael@0: * @param object aLocation michael@0: * @see DebuggerController.Breakpoints.addBreakpoint michael@0: */ michael@0: _onSetConditional: function(aLocation) { michael@0: // Highlight the breakpoint and show a conditional expression popup. michael@0: this.highlightBreakpoint(aLocation, { openPopup: true }); michael@0: }, michael@0: michael@0: /** michael@0: * Function invoked on the "enableSelf" menuitem command. michael@0: * michael@0: * @param object aLocation michael@0: * @see DebuggerController.Breakpoints.addBreakpoint michael@0: */ michael@0: _onEnableSelf: function(aLocation) { michael@0: // Enable the breakpoint, in this container and the controller store. michael@0: this.enableBreakpoint(aLocation); michael@0: }, michael@0: michael@0: /** michael@0: * Function invoked on the "disableSelf" menuitem command. michael@0: * michael@0: * @param object aLocation michael@0: * @see DebuggerController.Breakpoints.addBreakpoint michael@0: */ michael@0: _onDisableSelf: function(aLocation) { michael@0: // Disable the breakpoint, in this container and the controller store. michael@0: this.disableBreakpoint(aLocation); michael@0: }, michael@0: michael@0: /** michael@0: * Function invoked on the "deleteSelf" menuitem command. michael@0: * michael@0: * @param object aLocation michael@0: * @see DebuggerController.Breakpoints.addBreakpoint michael@0: */ michael@0: _onDeleteSelf: function(aLocation) { michael@0: // Remove the breakpoint, from this container and the controller store. michael@0: this.removeBreakpoint(aLocation); michael@0: DebuggerController.Breakpoints.removeBreakpoint(aLocation); michael@0: }, michael@0: michael@0: /** michael@0: * Function invoked on the "enableOthers" menuitem command. michael@0: * michael@0: * @param object aLocation michael@0: * @see DebuggerController.Breakpoints.addBreakpoint michael@0: */ michael@0: _onEnableOthers: function(aLocation) { michael@0: let enableOthers = aCallback => { michael@0: let other = this.getOtherBreakpoints(aLocation); michael@0: let outstanding = other.map(e => this.enableBreakpoint(e.attachment)); michael@0: promise.all(outstanding).then(aCallback); michael@0: } michael@0: michael@0: // Breakpoints can only be set while the debuggee is paused. To avoid michael@0: // an avalanche of pause/resume interrupts of the main thread, simply michael@0: // pause it beforehand if it's not already. michael@0: if (gThreadClient.state != "paused") { michael@0: gThreadClient.interrupt(() => enableOthers(() => gThreadClient.resume())); michael@0: } else { michael@0: enableOthers(); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Function invoked on the "disableOthers" menuitem command. michael@0: * michael@0: * @param object aLocation michael@0: * @see DebuggerController.Breakpoints.addBreakpoint michael@0: */ michael@0: _onDisableOthers: function(aLocation) { michael@0: let other = this.getOtherBreakpoints(aLocation); michael@0: other.forEach(e => this._onDisableSelf(e.attachment)); michael@0: }, michael@0: michael@0: /** michael@0: * Function invoked on the "deleteOthers" menuitem command. michael@0: * michael@0: * @param object aLocation michael@0: * @see DebuggerController.Breakpoints.addBreakpoint michael@0: */ michael@0: _onDeleteOthers: function(aLocation) { michael@0: let other = this.getOtherBreakpoints(aLocation); michael@0: other.forEach(e => this._onDeleteSelf(e.attachment)); michael@0: }, michael@0: michael@0: /** michael@0: * Function invoked on the "enableAll" menuitem command. michael@0: */ michael@0: _onEnableAll: function() { michael@0: this._onEnableOthers(undefined); michael@0: }, michael@0: michael@0: /** michael@0: * Function invoked on the "disableAll" menuitem command. michael@0: */ michael@0: _onDisableAll: function() { michael@0: this._onDisableOthers(undefined); michael@0: }, michael@0: michael@0: /** michael@0: * Function invoked on the "deleteAll" menuitem command. michael@0: */ michael@0: _onDeleteAll: function() { michael@0: this._onDeleteOthers(undefined); michael@0: }, michael@0: michael@0: _commandset: null, michael@0: _popupset: null, michael@0: _cmPopup: null, michael@0: _cbPanel: null, michael@0: _cbTextbox: null, michael@0: _selectedBreakpointItem: null, michael@0: _conditionalPopupVisible: false michael@0: }); michael@0: michael@0: /** michael@0: * Functions handling the traces UI. michael@0: */ michael@0: function TracerView() { michael@0: this._selectedItem = null; michael@0: this._matchingItems = null; michael@0: this.widget = null; michael@0: michael@0: this._highlightItem = this._highlightItem.bind(this); michael@0: this._isNotSelectedItem = this._isNotSelectedItem.bind(this); michael@0: michael@0: this._unhighlightMatchingItems = michael@0: DevToolsUtils.makeInfallible(this._unhighlightMatchingItems.bind(this)); michael@0: this._onToggleTracing = michael@0: DevToolsUtils.makeInfallible(this._onToggleTracing.bind(this)); michael@0: this._onStartTracing = michael@0: DevToolsUtils.makeInfallible(this._onStartTracing.bind(this)); michael@0: this._onClear = michael@0: DevToolsUtils.makeInfallible(this._onClear.bind(this)); michael@0: this._onSelect = michael@0: DevToolsUtils.makeInfallible(this._onSelect.bind(this)); michael@0: this._onMouseOver = michael@0: DevToolsUtils.makeInfallible(this._onMouseOver.bind(this)); michael@0: this._onSearch = DevToolsUtils.makeInfallible(this._onSearch.bind(this)); michael@0: } michael@0: michael@0: TracerView.MAX_TRACES = 200; michael@0: michael@0: TracerView.prototype = Heritage.extend(WidgetMethods, { michael@0: /** michael@0: * Initialization function, called when the debugger is started. michael@0: */ michael@0: initialize: function() { michael@0: dumpn("Initializing the TracerView"); michael@0: michael@0: this._traceButton = document.getElementById("trace"); michael@0: this._tracerTab = document.getElementById("tracer-tab"); michael@0: michael@0: // Remove tracer related elements from the dom and tear everything down if michael@0: // the tracer isn't enabled. michael@0: if (!Prefs.tracerEnabled) { michael@0: this._traceButton.remove(); michael@0: this._traceButton = null; michael@0: this._tracerTab.remove(); michael@0: this._tracerTab = null; michael@0: return; michael@0: } michael@0: michael@0: this.widget = new FastListWidget(document.getElementById("tracer-traces")); michael@0: this._traceButton.removeAttribute("hidden"); michael@0: this._tracerTab.removeAttribute("hidden"); michael@0: michael@0: this._search = document.getElementById("tracer-search"); michael@0: this._template = document.getElementsByClassName("trace-item-template")[0]; michael@0: this._templateItem = this._template.getElementsByClassName("trace-item")[0]; michael@0: this._templateTypeIcon = this._template.getElementsByClassName("trace-type")[0]; michael@0: this._templateNameNode = this._template.getElementsByClassName("trace-name")[0]; michael@0: michael@0: this.widget.addEventListener("select", this._onSelect, false); michael@0: this.widget.addEventListener("mouseover", this._onMouseOver, false); michael@0: this.widget.addEventListener("mouseout", this._unhighlightMatchingItems, false); michael@0: this._search.addEventListener("input", this._onSearch, false); michael@0: michael@0: this._startTooltip = L10N.getStr("startTracingTooltip"); michael@0: this._stopTooltip = L10N.getStr("stopTracingTooltip"); michael@0: this._tracingNotStartedString = L10N.getStr("tracingNotStartedText"); michael@0: this._noFunctionCallsString = L10N.getStr("noFunctionCallsText"); michael@0: michael@0: this._traceButton.setAttribute("tooltiptext", this._startTooltip); michael@0: this.emptyText = this._tracingNotStartedString; michael@0: }, michael@0: michael@0: /** michael@0: * Destruction function, called when the debugger is closed. michael@0: */ michael@0: destroy: function() { michael@0: dumpn("Destroying the TracerView"); michael@0: michael@0: if (!this.widget) { michael@0: return; michael@0: } michael@0: michael@0: this.widget.removeEventListener("select", this._onSelect, false); michael@0: this.widget.removeEventListener("mouseover", this._onMouseOver, false); michael@0: this.widget.removeEventListener("mouseout", this._unhighlightMatchingItems, false); michael@0: this._search.removeEventListener("input", this._onSearch, false); michael@0: }, michael@0: michael@0: /** michael@0: * Function invoked by the "toggleTracing" command to switch the tracer state. michael@0: */ michael@0: _onToggleTracing: function() { michael@0: if (DebuggerController.Tracer.tracing) { michael@0: this._onStopTracing(); michael@0: } else { michael@0: this._onStartTracing(); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Function invoked either by the "startTracing" command or by michael@0: * _onToggleTracing to start execution tracing in the backend. michael@0: * michael@0: * @return object michael@0: * A promise resolved once the tracing has successfully started. michael@0: */ michael@0: _onStartTracing: function() { michael@0: this._traceButton.setAttribute("checked", true); michael@0: this._traceButton.setAttribute("tooltiptext", this._stopTooltip); michael@0: michael@0: this.empty(); michael@0: this.emptyText = this._noFunctionCallsString; michael@0: michael@0: let deferred = promise.defer(); michael@0: DebuggerController.Tracer.startTracing(deferred.resolve); michael@0: return deferred.promise; michael@0: }, michael@0: michael@0: /** michael@0: * Function invoked by _onToggleTracing to stop execution tracing in the michael@0: * backend. michael@0: * michael@0: * @return object michael@0: * A promise resolved once the tracing has successfully stopped. michael@0: */ michael@0: _onStopTracing: function() { michael@0: this._traceButton.removeAttribute("checked"); michael@0: this._traceButton.setAttribute("tooltiptext", this._startTooltip); michael@0: michael@0: this.emptyText = this._tracingNotStartedString; michael@0: michael@0: let deferred = promise.defer(); michael@0: DebuggerController.Tracer.stopTracing(deferred.resolve); michael@0: return deferred.promise; michael@0: }, michael@0: michael@0: /** michael@0: * Function invoked by the "clearTraces" command to empty the traces pane. michael@0: */ michael@0: _onClear: function() { michael@0: this.empty(); michael@0: }, michael@0: michael@0: /** michael@0: * Populate the given parent scope with the variable with the provided name michael@0: * and value. michael@0: * michael@0: * @param String aName michael@0: * The name of the variable. michael@0: * @param Object aParent michael@0: * The parent scope. michael@0: * @param Object aValue michael@0: * The value of the variable. michael@0: */ michael@0: _populateVariable: function(aName, aParent, aValue) { michael@0: let item = aParent.addItem(aName, { value: aValue }); michael@0: if (aValue) { michael@0: let wrappedValue = new DebuggerController.Tracer.WrappedObject(aValue); michael@0: DebuggerView.Variables.controller.populate(item, wrappedValue); michael@0: item.expand(); michael@0: item.twisty = false; michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Handler for the widget's "select" event. Displays parameters, exception, or michael@0: * return value depending on whether the selected trace is a call, throw, or michael@0: * return respectively. michael@0: * michael@0: * @param Object traceItem michael@0: * The selected trace item. michael@0: */ michael@0: _onSelect: function _onSelect({ detail: traceItem }) { michael@0: if (!traceItem) { michael@0: return; michael@0: } michael@0: michael@0: const data = traceItem.attachment.trace; michael@0: const { location: { url, line } } = data; michael@0: DebuggerView.setEditorLocation(url, line, { noDebug: true }); michael@0: michael@0: DebuggerView.Variables.empty(); michael@0: const scope = DebuggerView.Variables.addScope(); michael@0: michael@0: if (data.type == "call") { michael@0: const params = DevToolsUtils.zip(data.parameterNames, data.arguments); michael@0: for (let [name, val] of params) { michael@0: if (val === undefined) { michael@0: scope.addItem(name, { value: "" }); michael@0: } else { michael@0: this._populateVariable(name, scope, val); michael@0: } michael@0: } michael@0: } else { michael@0: const varName = "<" + (data.type == "throw" ? "exception" : data.type) + ">"; michael@0: this._populateVariable(varName, scope, data.returnVal); michael@0: } michael@0: michael@0: scope.expand(); michael@0: DebuggerView.showInstrumentsPane(); michael@0: }, michael@0: michael@0: /** michael@0: * Add the hover frame enter/exit highlighting to a given item. michael@0: */ michael@0: _highlightItem: function(aItem) { michael@0: if (!aItem || !aItem.target) { michael@0: return; michael@0: } michael@0: const trace = aItem.target.querySelector(".trace-item"); michael@0: trace.classList.add("selected-matching"); michael@0: }, michael@0: michael@0: /** michael@0: * Remove the hover frame enter/exit highlighting to a given item. michael@0: */ michael@0: _unhighlightItem: function(aItem) { michael@0: if (!aItem || !aItem.target) { michael@0: return; michael@0: } michael@0: const match = aItem.target.querySelector(".selected-matching"); michael@0: if (match) { michael@0: match.classList.remove("selected-matching"); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Remove the frame enter/exit pair highlighting we do when hovering. michael@0: */ michael@0: _unhighlightMatchingItems: function() { michael@0: if (this._matchingItems) { michael@0: this._matchingItems.forEach(this._unhighlightItem); michael@0: this._matchingItems = null; michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Returns true if the given item is not the selected item. michael@0: */ michael@0: _isNotSelectedItem: function(aItem) { michael@0: return aItem !== this.selectedItem; michael@0: }, michael@0: michael@0: /** michael@0: * Highlight the frame enter/exit pair of items for the given item. michael@0: */ michael@0: _highlightMatchingItems: function(aItem) { michael@0: const frameId = aItem.attachment.trace.frameId; michael@0: const predicate = e => e.attachment.trace.frameId == frameId; michael@0: michael@0: this._unhighlightMatchingItems(); michael@0: this._matchingItems = this.items.filter(predicate); michael@0: this._matchingItems michael@0: .filter(this._isNotSelectedItem) michael@0: .forEach(this._highlightItem); michael@0: }, michael@0: michael@0: /** michael@0: * Listener for the mouseover event. michael@0: */ michael@0: _onMouseOver: function({ target }) { michael@0: const traceItem = this.getItemForElement(target); michael@0: if (traceItem) { michael@0: this._highlightMatchingItems(traceItem); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Listener for typing in the search box. michael@0: */ michael@0: _onSearch: function() { michael@0: const query = this._search.value.trim().toLowerCase(); michael@0: const predicate = name => name.toLowerCase().contains(query); michael@0: this.filterContents(item => predicate(item.attachment.trace.name)); michael@0: }, michael@0: michael@0: /** michael@0: * Select the traces tab in the sidebar. michael@0: */ michael@0: selectTab: function() { michael@0: const tabs = this._tracerTab.parentElement; michael@0: tabs.selectedIndex = Array.indexOf(tabs.children, this._tracerTab); michael@0: }, michael@0: michael@0: /** michael@0: * Commit all staged items to the widget. Overridden so that we can call michael@0: * |FastListWidget.prototype.flush|. michael@0: */ michael@0: commit: function() { michael@0: WidgetMethods.commit.call(this); michael@0: // TODO: Accessing non-standard widget properties. Figure out what's the michael@0: // best way to expose such things. Bug 895514. michael@0: this.widget.flush(); michael@0: }, michael@0: michael@0: /** michael@0: * Adds the trace record provided as an argument to the view. michael@0: * michael@0: * @param object aTrace michael@0: * The trace record coming from the tracer actor. michael@0: */ michael@0: addTrace: function(aTrace) { michael@0: // Create the element node for the trace item. michael@0: let view = this._createView(aTrace); michael@0: michael@0: // Append a source item to this container. michael@0: this.push([view], { michael@0: staged: true, michael@0: attachment: { michael@0: trace: aTrace michael@0: } michael@0: }); michael@0: }, michael@0: michael@0: /** michael@0: * Customization function for creating an item's UI. michael@0: * michael@0: * @return nsIDOMNode michael@0: * The network request view. michael@0: */ michael@0: _createView: function(aTrace) { michael@0: let { type, name, location, depth, frameId } = aTrace; michael@0: let { parameterNames, returnVal, arguments: args } = aTrace; michael@0: let fragment = document.createDocumentFragment(); michael@0: michael@0: this._templateItem.setAttribute("tooltiptext", SourceUtils.trimUrl(location.url)); michael@0: this._templateItem.style.MozPaddingStart = depth + "em"; michael@0: michael@0: const TYPES = ["call", "yield", "return", "throw"]; michael@0: for (let t of TYPES) { michael@0: this._templateTypeIcon.classList.toggle("trace-" + t, t == type); michael@0: } michael@0: this._templateTypeIcon.setAttribute("value", { michael@0: call: "\u2192", michael@0: yield: "Y", michael@0: return: "\u2190", michael@0: throw: "E", michael@0: terminated: "TERMINATED" michael@0: }[type]); michael@0: michael@0: this._templateNameNode.setAttribute("value", name); michael@0: michael@0: // All extra syntax and parameter nodes added. michael@0: const addedNodes = []; michael@0: michael@0: if (parameterNames) { michael@0: const syntax = (p) => { michael@0: const el = document.createElement("label"); michael@0: el.setAttribute("value", p); michael@0: el.classList.add("trace-syntax"); michael@0: el.classList.add("plain"); michael@0: addedNodes.push(el); michael@0: return el; michael@0: }; michael@0: michael@0: this._templateItem.appendChild(syntax("(")); michael@0: michael@0: for (let i = 0, n = parameterNames.length; i < n; i++) { michael@0: let param = document.createElement("label"); michael@0: param.setAttribute("value", parameterNames[i]); michael@0: param.classList.add("trace-param"); michael@0: param.classList.add("plain"); michael@0: addedNodes.push(param); michael@0: this._templateItem.appendChild(param); michael@0: michael@0: if (i + 1 !== n) { michael@0: this._templateItem.appendChild(syntax(", ")); michael@0: } michael@0: } michael@0: michael@0: this._templateItem.appendChild(syntax(")")); michael@0: } michael@0: michael@0: // Flatten the DOM by removing one redundant box (the template container). michael@0: for (let node of this._template.childNodes) { michael@0: fragment.appendChild(node.cloneNode(true)); michael@0: } michael@0: michael@0: // Remove any added nodes from the template. michael@0: for (let node of addedNodes) { michael@0: this._templateItem.removeChild(node); michael@0: } michael@0: michael@0: return fragment; michael@0: } michael@0: }); michael@0: michael@0: /** michael@0: * Utility functions for handling sources. michael@0: */ michael@0: let SourceUtils = { michael@0: _labelsCache: new Map(), // Can't use WeakMaps because keys are strings. michael@0: _groupsCache: new Map(), michael@0: _minifiedCache: new WeakMap(), michael@0: michael@0: /** michael@0: * Returns true if the specified url and/or content type are specific to michael@0: * javascript files. michael@0: * michael@0: * @return boolean michael@0: * True if the source is likely javascript. michael@0: */ michael@0: isJavaScript: function(aUrl, aContentType = "") { michael@0: return /\.jsm?$/.test(this.trimUrlQuery(aUrl)) || michael@0: aContentType.contains("javascript"); michael@0: }, michael@0: michael@0: /** michael@0: * Determines if the source text is minified by using michael@0: * the percentage indented of a subset of lines michael@0: * michael@0: * @param string aText michael@0: * The source text. michael@0: * @return boolean michael@0: * True if source text is minified. michael@0: */ michael@0: isMinified: function(sourceClient, aText){ michael@0: if (this._minifiedCache.has(sourceClient)) { michael@0: return this._minifiedCache.get(sourceClient); michael@0: } michael@0: michael@0: let isMinified; michael@0: let lineEndIndex = 0; michael@0: let lineStartIndex = 0; michael@0: let lines = 0; michael@0: let indentCount = 0; michael@0: let overCharLimit = false; michael@0: michael@0: // Strip comments. michael@0: aText = aText.replace(/\/\*[\S\s]*?\*\/|\/\/(.+|\n)/g, ""); michael@0: michael@0: while (lines++ < SAMPLE_SIZE) { michael@0: lineEndIndex = aText.indexOf("\n", lineStartIndex); michael@0: if (lineEndIndex == -1) { michael@0: break; michael@0: } michael@0: if (/^\s+/.test(aText.slice(lineStartIndex, lineEndIndex))) { michael@0: indentCount++; michael@0: } michael@0: // For files with no indents but are not minified. michael@0: if ((lineEndIndex - lineStartIndex) > CHARACTER_LIMIT) { michael@0: overCharLimit = true; michael@0: break; michael@0: } michael@0: lineStartIndex = lineEndIndex + 1; michael@0: } michael@0: isMinified = ((indentCount / lines ) * 100) < INDENT_COUNT_THRESHOLD || michael@0: overCharLimit; michael@0: michael@0: this._minifiedCache.set(sourceClient, isMinified); michael@0: return isMinified; michael@0: }, michael@0: michael@0: /** michael@0: * Clears the labels, groups and minify cache, populated by methods like michael@0: * SourceUtils.getSourceLabel or Source Utils.getSourceGroup. michael@0: * This should be done every time the content location changes. michael@0: */ michael@0: clearCache: function() { michael@0: this._labelsCache.clear(); michael@0: this._groupsCache.clear(); michael@0: this._minifiedCache.clear(); michael@0: }, michael@0: michael@0: /** michael@0: * Gets a unique, simplified label from a source url. michael@0: * michael@0: * @param string aUrl michael@0: * The source url. michael@0: * @return string michael@0: * The simplified label. michael@0: */ michael@0: getSourceLabel: function(aUrl) { michael@0: let cachedLabel = this._labelsCache.get(aUrl); michael@0: if (cachedLabel) { michael@0: return cachedLabel; michael@0: } michael@0: michael@0: let sourceLabel = null; michael@0: michael@0: for (let name of Object.keys(KNOWN_SOURCE_GROUPS)) { michael@0: if (aUrl.startsWith(KNOWN_SOURCE_GROUPS[name])) { michael@0: sourceLabel = aUrl.substring(KNOWN_SOURCE_GROUPS[name].length); michael@0: } michael@0: } michael@0: michael@0: if (!sourceLabel) { michael@0: sourceLabel = this.trimUrl(aUrl); michael@0: } michael@0: let unicodeLabel = NetworkHelper.convertToUnicode(unescape(sourceLabel)); michael@0: this._labelsCache.set(aUrl, unicodeLabel); michael@0: return unicodeLabel; michael@0: }, michael@0: michael@0: /** michael@0: * Gets as much information as possible about the hostname and directory paths michael@0: * of an url to create a short url group identifier. michael@0: * michael@0: * @param string aUrl michael@0: * The source url. michael@0: * @return string michael@0: * The simplified group. michael@0: */ michael@0: getSourceGroup: function(aUrl) { michael@0: let cachedGroup = this._groupsCache.get(aUrl); michael@0: if (cachedGroup) { michael@0: return cachedGroup; michael@0: } michael@0: michael@0: try { michael@0: // Use an nsIURL to parse all the url path parts. michael@0: var uri = Services.io.newURI(aUrl, null, null).QueryInterface(Ci.nsIURL); michael@0: } catch (e) { michael@0: // This doesn't look like a url, or nsIURL can't handle it. michael@0: return ""; michael@0: } michael@0: michael@0: let groupLabel = uri.prePath; michael@0: michael@0: for (let name of Object.keys(KNOWN_SOURCE_GROUPS)) { michael@0: if (aUrl.startsWith(KNOWN_SOURCE_GROUPS[name])) { michael@0: groupLabel = name; michael@0: } michael@0: } michael@0: michael@0: let unicodeLabel = NetworkHelper.convertToUnicode(unescape(groupLabel)); michael@0: this._groupsCache.set(aUrl, unicodeLabel) michael@0: return unicodeLabel; michael@0: }, michael@0: michael@0: /** michael@0: * Trims the url by shortening it if it exceeds a certain length, adding an michael@0: * ellipsis at the end. michael@0: * michael@0: * @param string aUrl michael@0: * The source url. michael@0: * @param number aLength [optional] michael@0: * The expected source url length. michael@0: * @param number aSection [optional] michael@0: * The section to trim. Supported values: "start", "center", "end" michael@0: * @return string michael@0: * The shortened url. michael@0: */ michael@0: trimUrlLength: function(aUrl, aLength, aSection) { michael@0: aLength = aLength || SOURCE_URL_DEFAULT_MAX_LENGTH; michael@0: aSection = aSection || "end"; michael@0: michael@0: if (aUrl.length > aLength) { michael@0: switch (aSection) { michael@0: case "start": michael@0: return L10N.ellipsis + aUrl.slice(-aLength); michael@0: break; michael@0: case "center": michael@0: return aUrl.substr(0, aLength / 2 - 1) + L10N.ellipsis + aUrl.slice(-aLength / 2 + 1); michael@0: break; michael@0: case "end": michael@0: return aUrl.substr(0, aLength) + L10N.ellipsis; michael@0: break; michael@0: } michael@0: } michael@0: return aUrl; michael@0: }, michael@0: michael@0: /** michael@0: * Trims the query part or reference identifier of a url string, if necessary. michael@0: * michael@0: * @param string aUrl michael@0: * The source url. michael@0: * @return string michael@0: * The shortened url. michael@0: */ michael@0: trimUrlQuery: function(aUrl) { michael@0: let length = aUrl.length; michael@0: let q1 = aUrl.indexOf('?'); michael@0: let q2 = aUrl.indexOf('&'); michael@0: let q3 = aUrl.indexOf('#'); michael@0: let q = Math.min(q1 != -1 ? q1 : length, michael@0: q2 != -1 ? q2 : length, michael@0: q3 != -1 ? q3 : length); michael@0: michael@0: return aUrl.slice(0, q); michael@0: }, michael@0: michael@0: /** michael@0: * Trims as much as possible from a url, while keeping the label unique michael@0: * in the sources container. michael@0: * michael@0: * @param string | nsIURL aUrl michael@0: * The source url. michael@0: * @param string aLabel [optional] michael@0: * The resulting label at each step. michael@0: * @param number aSeq [optional] michael@0: * The current iteration step. michael@0: * @return string michael@0: * The resulting label at the final step. michael@0: */ michael@0: trimUrl: function(aUrl, aLabel, aSeq) { michael@0: if (!(aUrl instanceof Ci.nsIURL)) { michael@0: try { michael@0: // Use an nsIURL to parse all the url path parts. michael@0: aUrl = Services.io.newURI(aUrl, null, null).QueryInterface(Ci.nsIURL); michael@0: } catch (e) { michael@0: // This doesn't look like a url, or nsIURL can't handle it. michael@0: return aUrl; michael@0: } michael@0: } michael@0: if (!aSeq) { michael@0: let name = aUrl.fileName; michael@0: if (name) { michael@0: // This is a regular file url, get only the file name (contains the michael@0: // base name and extension if available). michael@0: michael@0: // If this url contains an invalid query, unfortunately nsIURL thinks michael@0: // it's part of the file extension. It must be removed. michael@0: aLabel = aUrl.fileName.replace(/\&.*/, ""); michael@0: } else { michael@0: // This is not a file url, hence there is no base name, nor extension. michael@0: // Proceed using other available information. michael@0: aLabel = ""; michael@0: } michael@0: aSeq = 1; michael@0: } michael@0: michael@0: // If we have a label and it doesn't only contain a query... michael@0: if (aLabel && aLabel.indexOf("?") != 0) { michael@0: // A page may contain multiple requests to the same url but with different michael@0: // queries. It is *not* redundant to show each one. michael@0: if (!DebuggerView.Sources.getItemForAttachment(e => e.label == aLabel)) { michael@0: return aLabel; michael@0: } michael@0: } michael@0: michael@0: // Append the url query. michael@0: if (aSeq == 1) { michael@0: let query = aUrl.query; michael@0: if (query) { michael@0: return this.trimUrl(aUrl, aLabel + "?" + query, aSeq + 1); michael@0: } michael@0: aSeq++; michael@0: } michael@0: // Append the url reference. michael@0: if (aSeq == 2) { michael@0: let ref = aUrl.ref; michael@0: if (ref) { michael@0: return this.trimUrl(aUrl, aLabel + "#" + aUrl.ref, aSeq + 1); michael@0: } michael@0: aSeq++; michael@0: } michael@0: // Prepend the url directory. michael@0: if (aSeq == 3) { michael@0: let dir = aUrl.directory; michael@0: if (dir) { michael@0: return this.trimUrl(aUrl, dir.replace(/^\//, "") + aLabel, aSeq + 1); michael@0: } michael@0: aSeq++; michael@0: } michael@0: // Prepend the hostname and port number. michael@0: if (aSeq == 4) { michael@0: let host = aUrl.hostPort; michael@0: if (host) { michael@0: return this.trimUrl(aUrl, host + "/" + aLabel, aSeq + 1); michael@0: } michael@0: aSeq++; michael@0: } michael@0: // Use the whole url spec but ignoring the reference. michael@0: if (aSeq == 5) { michael@0: return this.trimUrl(aUrl, aUrl.specIgnoringRef, aSeq + 1); michael@0: } michael@0: // Give up. michael@0: return aUrl.spec; michael@0: } michael@0: }; michael@0: michael@0: /** michael@0: * Functions handling the variables bubble UI. michael@0: */ michael@0: function VariableBubbleView() { michael@0: dumpn("VariableBubbleView was instantiated"); michael@0: michael@0: this._onMouseMove = this._onMouseMove.bind(this); michael@0: this._onMouseLeave = this._onMouseLeave.bind(this); michael@0: this._onPopupHiding = this._onPopupHiding.bind(this); michael@0: } michael@0: michael@0: VariableBubbleView.prototype = { michael@0: /** michael@0: * Initialization function, called when the debugger is started. michael@0: */ michael@0: initialize: function() { michael@0: dumpn("Initializing the VariableBubbleView"); michael@0: michael@0: this._editorContainer = document.getElementById("editor"); michael@0: this._editorContainer.addEventListener("mousemove", this._onMouseMove, false); michael@0: this._editorContainer.addEventListener("mouseleave", this._onMouseLeave, false); michael@0: michael@0: this._tooltip = new Tooltip(document, { michael@0: closeOnEvents: [{ michael@0: emitter: DebuggerController._toolbox, michael@0: event: "select" michael@0: }, { michael@0: emitter: this._editorContainer, michael@0: event: "scroll", michael@0: useCapture: true michael@0: }] michael@0: }); michael@0: this._tooltip.defaultPosition = EDITOR_VARIABLE_POPUP_POSITION; michael@0: this._tooltip.defaultShowDelay = EDITOR_VARIABLE_HOVER_DELAY; michael@0: this._tooltip.panel.addEventListener("popuphiding", this._onPopupHiding); michael@0: }, michael@0: michael@0: /** michael@0: * Destruction function, called when the debugger is closed. michael@0: */ michael@0: destroy: function() { michael@0: dumpn("Destroying the VariableBubbleView"); michael@0: michael@0: this._tooltip.panel.removeEventListener("popuphiding", this._onPopupHiding); michael@0: this._editorContainer.removeEventListener("mousemove", this._onMouseMove, false); michael@0: this._editorContainer.removeEventListener("mouseleave", this._onMouseLeave, false); michael@0: }, michael@0: michael@0: /** michael@0: * Specifies whether literals can be (redundantly) inspected in a popup. michael@0: * This behavior is deprecated, but still tested in a few places. michael@0: */ michael@0: _ignoreLiterals: true, michael@0: michael@0: /** michael@0: * Searches for an identifier underneath the specified position in the michael@0: * source editor, and if found, opens a VariablesView inspection popup. michael@0: * michael@0: * @param number x, y michael@0: * The left/top coordinates where to look for an identifier. michael@0: */ michael@0: _findIdentifier: function(x, y) { michael@0: let editor = DebuggerView.editor; michael@0: michael@0: // Calculate the editor's line and column at the current x and y coords. michael@0: let hoveredPos = editor.getPositionFromCoords({ left: x, top: y }); michael@0: let hoveredOffset = editor.getOffset(hoveredPos); michael@0: let hoveredLine = hoveredPos.line; michael@0: let hoveredColumn = hoveredPos.ch; michael@0: michael@0: // A source contains multiple scripts. Find the start index of the script michael@0: // containing the specified offset relative to its parent source. michael@0: let contents = editor.getText(); michael@0: let location = DebuggerView.Sources.selectedValue; michael@0: let parsedSource = DebuggerController.Parser.get(contents, location); michael@0: let scriptInfo = parsedSource.getScriptInfo(hoveredOffset); michael@0: michael@0: // If the script length is negative, we're not hovering JS source code. michael@0: if (scriptInfo.length == -1) { michael@0: return; michael@0: } michael@0: michael@0: // Using the script offset, determine the actual line and column inside the michael@0: // script, to use when finding identifiers. michael@0: let scriptStart = editor.getPosition(scriptInfo.start); michael@0: let scriptLineOffset = scriptStart.line; michael@0: let scriptColumnOffset = (hoveredLine == scriptStart.line ? scriptStart.ch : 0); michael@0: michael@0: let scriptLine = hoveredLine - scriptLineOffset; michael@0: let scriptColumn = hoveredColumn - scriptColumnOffset; michael@0: let identifierInfo = parsedSource.getIdentifierAt({ michael@0: line: scriptLine + 1, michael@0: column: scriptColumn, michael@0: scriptIndex: scriptInfo.index, michael@0: ignoreLiterals: this._ignoreLiterals michael@0: }); michael@0: michael@0: // If the info is null, we're not hovering any identifier. michael@0: if (!identifierInfo) { michael@0: return; michael@0: } michael@0: michael@0: // Transform the line and column relative to the parsed script back michael@0: // to the context of the parent source. michael@0: let { start: identifierStart, end: identifierEnd } = identifierInfo.location; michael@0: let identifierCoords = { michael@0: line: identifierStart.line + scriptLineOffset, michael@0: column: identifierStart.column + scriptColumnOffset, michael@0: length: identifierEnd.column - identifierStart.column michael@0: }; michael@0: michael@0: // Evaluate the identifier in the current stack frame and show the michael@0: // results in a VariablesView inspection popup. michael@0: DebuggerController.StackFrames.evaluate(identifierInfo.evalString) michael@0: .then(frameFinished => { michael@0: if ("return" in frameFinished) { michael@0: this.showContents({ michael@0: coords: identifierCoords, michael@0: evalPrefix: identifierInfo.evalString, michael@0: objectActor: frameFinished.return michael@0: }); michael@0: } else { michael@0: let msg = "Evaluation has thrown for: " + identifierInfo.evalString; michael@0: console.warn(msg); michael@0: dumpn(msg); michael@0: } michael@0: }) michael@0: .then(null, err => { michael@0: let msg = "Couldn't evaluate: " + err.message; michael@0: console.error(msg); michael@0: dumpn(msg); michael@0: }); michael@0: }, michael@0: michael@0: /** michael@0: * Shows an inspection popup for a specified object actor grip. michael@0: * michael@0: * @param string object michael@0: * An object containing the following properties: michael@0: * - coords: the inspected identifier coordinates in the editor, michael@0: * containing the { line, column, length } properties. michael@0: * - evalPrefix: a prefix for the variables view evaluation macros. michael@0: * - objectActor: the value grip for the object actor. michael@0: */ michael@0: showContents: function({ coords, evalPrefix, objectActor }) { michael@0: let editor = DebuggerView.editor; michael@0: let { line, column, length } = coords; michael@0: michael@0: // Highlight the function found at the mouse position. michael@0: this._markedText = editor.markText( michael@0: { line: line - 1, ch: column }, michael@0: { line: line - 1, ch: column + length }); michael@0: michael@0: // If the grip represents a primitive value, use a more lightweight michael@0: // machinery to display it. michael@0: if (VariablesView.isPrimitive({ value: objectActor })) { michael@0: let className = VariablesView.getClass(objectActor); michael@0: let textContent = VariablesView.getString(objectActor); michael@0: this._tooltip.setTextContent({ michael@0: messages: [textContent], michael@0: messagesClass: className, michael@0: containerClass: "plain" michael@0: }, [{ michael@0: label: L10N.getStr('addWatchExpressionButton'), michael@0: className: "dbg-expression-button", michael@0: command: () => { michael@0: DebuggerView.VariableBubble.hideContents(); michael@0: DebuggerView.WatchExpressions.addExpression(evalPrefix, true); michael@0: } michael@0: }]); michael@0: } else { michael@0: this._tooltip.setVariableContent(objectActor, { michael@0: searchPlaceholder: L10N.getStr("emptyPropertiesFilterText"), michael@0: searchEnabled: Prefs.variablesSearchboxVisible, michael@0: eval: (variable, value) => { michael@0: let string = variable.evaluationMacro(variable, value); michael@0: DebuggerController.StackFrames.evaluate(string); michael@0: DebuggerView.VariableBubble.hideContents(); michael@0: } michael@0: }, { michael@0: getEnvironmentClient: aObject => gThreadClient.environment(aObject), michael@0: getObjectClient: aObject => gThreadClient.pauseGrip(aObject), michael@0: simpleValueEvalMacro: this._getSimpleValueEvalMacro(evalPrefix), michael@0: getterOrSetterEvalMacro: this._getGetterOrSetterEvalMacro(evalPrefix), michael@0: overrideValueEvalMacro: this._getOverrideValueEvalMacro(evalPrefix) michael@0: }, { michael@0: fetched: (aEvent, aType) => { michael@0: if (aType == "properties") { michael@0: window.emit(EVENTS.FETCHED_BUBBLE_PROPERTIES); michael@0: } michael@0: } michael@0: }, [{ michael@0: label: L10N.getStr("addWatchExpressionButton"), michael@0: className: "dbg-expression-button", michael@0: command: () => { michael@0: DebuggerView.VariableBubble.hideContents(); michael@0: DebuggerView.WatchExpressions.addExpression(evalPrefix, true); michael@0: } michael@0: }], DebuggerController._toolbox); michael@0: } michael@0: michael@0: this._tooltip.show(this._markedText.anchor); michael@0: }, michael@0: michael@0: /** michael@0: * Hides the inspection popup. michael@0: */ michael@0: hideContents: function() { michael@0: clearNamedTimeout("editor-mouse-move"); michael@0: this._tooltip.hide(); michael@0: }, michael@0: michael@0: /** michael@0: * Checks whether the inspection popup is shown. michael@0: * michael@0: * @return boolean michael@0: * True if the panel is shown or showing, false otherwise. michael@0: */ michael@0: contentsShown: function() { michael@0: return this._tooltip.isShown(); michael@0: }, michael@0: michael@0: /** michael@0: * Functions for getting customized variables view evaluation macros. michael@0: * michael@0: * @param string aPrefix michael@0: * See the corresponding VariablesView.* functions. michael@0: */ michael@0: _getSimpleValueEvalMacro: function(aPrefix) { michael@0: return (item, string) => michael@0: VariablesView.simpleValueEvalMacro(item, string, aPrefix); michael@0: }, michael@0: _getGetterOrSetterEvalMacro: function(aPrefix) { michael@0: return (item, string) => michael@0: VariablesView.getterOrSetterEvalMacro(item, string, aPrefix); michael@0: }, michael@0: _getOverrideValueEvalMacro: function(aPrefix) { michael@0: return (item, string) => michael@0: VariablesView.overrideValueEvalMacro(item, string, aPrefix); michael@0: }, michael@0: michael@0: /** michael@0: * The mousemove listener for the source editor. michael@0: */ michael@0: _onMouseMove: function({ clientX: x, clientY: y, buttons: btns }) { michael@0: // Prevent the variable inspection popup from showing when the thread client michael@0: // is not paused, or while a popup is already visible, or when the user tries michael@0: // to select text in the editor. michael@0: if (gThreadClient && gThreadClient.state != "paused" michael@0: || !this._tooltip.isHidden() michael@0: || (DebuggerView.editor.somethingSelected() michael@0: && btns > 0)) { michael@0: clearNamedTimeout("editor-mouse-move"); michael@0: return; michael@0: } michael@0: // Allow events to settle down first. If the mouse hovers over michael@0: // a certain point in the editor long enough, try showing a variable bubble. michael@0: setNamedTimeout("editor-mouse-move", michael@0: EDITOR_VARIABLE_HOVER_DELAY, () => this._findIdentifier(x, y)); michael@0: }, michael@0: michael@0: /** michael@0: * The mouseleave listener for the source editor container node. michael@0: */ michael@0: _onMouseLeave: function() { michael@0: clearNamedTimeout("editor-mouse-move"); michael@0: }, michael@0: michael@0: /** michael@0: * Listener handling the popup hiding event. michael@0: */ michael@0: _onPopupHiding: function({ target }) { michael@0: if (this._tooltip.panel != target) { michael@0: return; michael@0: } michael@0: if (this._markedText) { michael@0: this._markedText.clear(); michael@0: this._markedText = null; michael@0: } michael@0: if (!this._tooltip.isEmpty()) { michael@0: this._tooltip.empty(); michael@0: } michael@0: }, michael@0: michael@0: _editorContainer: null, michael@0: _markedText: null, michael@0: _tooltip: null michael@0: }; michael@0: michael@0: /** michael@0: * Functions handling the watch expressions UI. michael@0: */ michael@0: function WatchExpressionsView() { michael@0: dumpn("WatchExpressionsView was instantiated"); michael@0: michael@0: this.switchExpression = this.switchExpression.bind(this); michael@0: this.deleteExpression = this.deleteExpression.bind(this); michael@0: this._createItemView = this._createItemView.bind(this); michael@0: this._onClick = this._onClick.bind(this); michael@0: this._onClose = this._onClose.bind(this); michael@0: this._onBlur = this._onBlur.bind(this); michael@0: this._onKeyPress = this._onKeyPress.bind(this); michael@0: } michael@0: michael@0: WatchExpressionsView.prototype = Heritage.extend(WidgetMethods, { michael@0: /** michael@0: * Initialization function, called when the debugger is started. michael@0: */ michael@0: initialize: function() { michael@0: dumpn("Initializing the WatchExpressionsView"); michael@0: michael@0: this.widget = new SimpleListWidget(document.getElementById("expressions")); michael@0: this.widget.setAttribute("context", "debuggerWatchExpressionsContextMenu"); michael@0: this.widget.addEventListener("click", this._onClick, false); michael@0: michael@0: this.headerText = L10N.getStr("addWatchExpressionText"); michael@0: }, michael@0: michael@0: /** michael@0: * Destruction function, called when the debugger is closed. michael@0: */ michael@0: destroy: function() { michael@0: dumpn("Destroying the WatchExpressionsView"); michael@0: michael@0: this.widget.removeEventListener("click", this._onClick, false); michael@0: }, michael@0: michael@0: /** michael@0: * Adds a watch expression in this container. michael@0: * michael@0: * @param string aExpression [optional] michael@0: * An optional initial watch expression text. michael@0: * @param boolean aSkipUserInput [optional] michael@0: * Pass true to avoid waiting for additional user input michael@0: * on the watch expression. michael@0: */ michael@0: addExpression: function(aExpression = "", aSkipUserInput = false) { michael@0: // Watch expressions are UI elements which benefit from visible panes. michael@0: DebuggerView.showInstrumentsPane(); michael@0: michael@0: // Create the element node for the watch expression item. michael@0: let itemView = this._createItemView(aExpression); michael@0: michael@0: // Append a watch expression item to this container. michael@0: let expressionItem = this.push([itemView.container], { michael@0: index: 0, /* specifies on which position should the item be appended */ michael@0: attachment: { michael@0: view: itemView, michael@0: initialExpression: aExpression, michael@0: currentExpression: "", michael@0: } michael@0: }); michael@0: michael@0: // Automatically focus the new watch expression input michael@0: // if additional user input is desired. michael@0: if (!aSkipUserInput) { michael@0: expressionItem.attachment.view.inputNode.select(); michael@0: expressionItem.attachment.view.inputNode.focus(); michael@0: DebuggerView.Variables.parentNode.scrollTop = 0; michael@0: } michael@0: // Otherwise, add and evaluate the new watch expression immediately. michael@0: else { michael@0: this.toggleContents(false); michael@0: this._onBlur({ target: expressionItem.attachment.view.inputNode }); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Changes the watch expression corresponding to the specified variable item. michael@0: * This function is called whenever a watch expression's code is edited in michael@0: * the variables view container. michael@0: * michael@0: * @param Variable aVar michael@0: * The variable representing the watch expression evaluation. michael@0: * @param string aExpression michael@0: * The new watch expression text. michael@0: */ michael@0: switchExpression: function(aVar, aExpression) { michael@0: let expressionItem = michael@0: [i for (i of this) if (i.attachment.currentExpression == aVar.name)][0]; michael@0: michael@0: // Remove the watch expression if it's going to be empty or a duplicate. michael@0: if (!aExpression || this.getAllStrings().indexOf(aExpression) != -1) { michael@0: this.deleteExpression(aVar); michael@0: return; michael@0: } michael@0: michael@0: // Save the watch expression code string. michael@0: expressionItem.attachment.currentExpression = aExpression; michael@0: expressionItem.attachment.view.inputNode.value = aExpression; michael@0: michael@0: // Synchronize with the controller's watch expressions store. michael@0: DebuggerController.StackFrames.syncWatchExpressions(); michael@0: }, michael@0: michael@0: /** michael@0: * Removes the watch expression corresponding to the specified variable item. michael@0: * This function is called whenever a watch expression's value is edited in michael@0: * the variables view container. michael@0: * michael@0: * @param Variable aVar michael@0: * The variable representing the watch expression evaluation. michael@0: */ michael@0: deleteExpression: function(aVar) { michael@0: let expressionItem = michael@0: [i for (i of this) if (i.attachment.currentExpression == aVar.name)][0]; michael@0: michael@0: // Remove the watch expression. michael@0: this.remove(expressionItem); michael@0: michael@0: // Synchronize with the controller's watch expressions store. michael@0: DebuggerController.StackFrames.syncWatchExpressions(); michael@0: }, michael@0: michael@0: /** michael@0: * Gets the watch expression code string for an item in this container. michael@0: * michael@0: * @param number aIndex michael@0: * The index used to identify the watch expression. michael@0: * @return string michael@0: * The watch expression code string. michael@0: */ michael@0: getString: function(aIndex) { michael@0: return this.getItemAtIndex(aIndex).attachment.currentExpression; michael@0: }, michael@0: michael@0: /** michael@0: * Gets the watch expressions code strings for all items in this container. michael@0: * michael@0: * @return array michael@0: * The watch expressions code strings. michael@0: */ michael@0: getAllStrings: function() { michael@0: return this.items.map(e => e.attachment.currentExpression); michael@0: }, michael@0: michael@0: /** michael@0: * Customization function for creating an item's UI. michael@0: * michael@0: * @param string aExpression michael@0: * The watch expression string. michael@0: */ michael@0: _createItemView: function(aExpression) { michael@0: let container = document.createElement("hbox"); michael@0: container.className = "list-widget-item dbg-expression"; michael@0: michael@0: let arrowNode = document.createElement("hbox"); michael@0: arrowNode.className = "dbg-expression-arrow"; michael@0: michael@0: let inputNode = document.createElement("textbox"); michael@0: inputNode.className = "plain dbg-expression-input devtools-monospace"; michael@0: inputNode.setAttribute("value", aExpression); michael@0: inputNode.setAttribute("flex", "1"); michael@0: michael@0: let closeNode = document.createElement("toolbarbutton"); michael@0: closeNode.className = "plain variables-view-delete"; michael@0: michael@0: closeNode.addEventListener("click", this._onClose, false); michael@0: inputNode.addEventListener("blur", this._onBlur, false); michael@0: inputNode.addEventListener("keypress", this._onKeyPress, false); michael@0: michael@0: container.appendChild(arrowNode); michael@0: container.appendChild(inputNode); michael@0: container.appendChild(closeNode); michael@0: michael@0: return { michael@0: container: container, michael@0: arrowNode: arrowNode, michael@0: inputNode: inputNode, michael@0: closeNode: closeNode michael@0: }; michael@0: }, michael@0: michael@0: /** michael@0: * Called when the add watch expression key sequence was pressed. michael@0: */ michael@0: _onCmdAddExpression: function(aText) { michael@0: // Only add a new expression if there's no pending input. michael@0: if (this.getAllStrings().indexOf("") == -1) { michael@0: this.addExpression(aText || DebuggerView.editor.getSelection()); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Called when the remove all watch expressions key sequence was pressed. michael@0: */ michael@0: _onCmdRemoveAllExpressions: function() { michael@0: // Empty the view of all the watch expressions and clear the cache. michael@0: this.empty(); michael@0: michael@0: // Synchronize with the controller's watch expressions store. michael@0: DebuggerController.StackFrames.syncWatchExpressions(); michael@0: }, michael@0: michael@0: /** michael@0: * The click listener for this container. michael@0: */ michael@0: _onClick: function(e) { michael@0: if (e.button != 0) { michael@0: // Only allow left-click to trigger this event. michael@0: return; michael@0: } michael@0: let expressionItem = this.getItemForElement(e.target); michael@0: if (!expressionItem) { michael@0: // The container is empty or we didn't click on an actual item. michael@0: this.addExpression(); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * The click listener for a watch expression's close button. michael@0: */ michael@0: _onClose: function(e) { michael@0: // Remove the watch expression. michael@0: this.remove(this.getItemForElement(e.target)); michael@0: michael@0: // Synchronize with the controller's watch expressions store. michael@0: DebuggerController.StackFrames.syncWatchExpressions(); michael@0: michael@0: // Prevent clicking the expression element itself. michael@0: e.preventDefault(); michael@0: e.stopPropagation(); michael@0: }, michael@0: michael@0: /** michael@0: * The blur listener for a watch expression's textbox. michael@0: */ michael@0: _onBlur: function({ target: textbox }) { michael@0: let expressionItem = this.getItemForElement(textbox); michael@0: let oldExpression = expressionItem.attachment.currentExpression; michael@0: let newExpression = textbox.value.trim(); michael@0: michael@0: // Remove the watch expression if it's empty. michael@0: if (!newExpression) { michael@0: this.remove(expressionItem); michael@0: } michael@0: // Remove the watch expression if it's a duplicate. michael@0: else if (!oldExpression && this.getAllStrings().indexOf(newExpression) != -1) { michael@0: this.remove(expressionItem); michael@0: } michael@0: // Expression is eligible. michael@0: else { michael@0: expressionItem.attachment.currentExpression = newExpression; michael@0: } michael@0: michael@0: // Synchronize with the controller's watch expressions store. michael@0: DebuggerController.StackFrames.syncWatchExpressions(); michael@0: }, michael@0: michael@0: /** michael@0: * The keypress listener for a watch expression's textbox. michael@0: */ michael@0: _onKeyPress: function(e) { michael@0: switch(e.keyCode) { michael@0: case e.DOM_VK_RETURN: michael@0: case e.DOM_VK_ESCAPE: michael@0: e.stopPropagation(); michael@0: DebuggerView.editor.focus(); michael@0: return; michael@0: } michael@0: } michael@0: }); michael@0: michael@0: /** michael@0: * Functions handling the event listeners UI. michael@0: */ michael@0: function EventListenersView() { michael@0: dumpn("EventListenersView was instantiated"); michael@0: michael@0: this._onCheck = this._onCheck.bind(this); michael@0: this._onClick = this._onClick.bind(this); michael@0: } michael@0: michael@0: EventListenersView.prototype = Heritage.extend(WidgetMethods, { michael@0: /** michael@0: * Initialization function, called when the debugger is started. michael@0: */ michael@0: initialize: function() { michael@0: dumpn("Initializing the EventListenersView"); michael@0: michael@0: this.widget = new SideMenuWidget(document.getElementById("event-listeners"), { michael@0: showItemCheckboxes: true, michael@0: showGroupCheckboxes: true michael@0: }); michael@0: michael@0: this.emptyText = L10N.getStr("noEventListenersText"); michael@0: this._eventCheckboxTooltip = L10N.getStr("eventCheckboxTooltip"); michael@0: this._onSelectorString = " " + L10N.getStr("eventOnSelector") + " "; michael@0: this._inSourceString = " " + L10N.getStr("eventInSource") + " "; michael@0: this._inNativeCodeString = L10N.getStr("eventNative"); michael@0: michael@0: this.widget.addEventListener("check", this._onCheck, false); michael@0: this.widget.addEventListener("click", this._onClick, false); michael@0: }, michael@0: michael@0: /** michael@0: * Destruction function, called when the debugger is closed. michael@0: */ michael@0: destroy: function() { michael@0: dumpn("Destroying the EventListenersView"); michael@0: michael@0: this.widget.removeEventListener("check", this._onCheck, false); michael@0: this.widget.removeEventListener("click", this._onClick, false); michael@0: }, michael@0: michael@0: /** michael@0: * Adds an event to this event listeners container. michael@0: * michael@0: * @param object aListener michael@0: * The listener object coming from the active thread. michael@0: * @param object aOptions [optional] michael@0: * Additional options for adding the source. Supported options: michael@0: * - staged: true to stage the item to be appended later michael@0: */ michael@0: addListener: function(aListener, aOptions = {}) { michael@0: let { node: { selector }, function: { url }, type } = aListener; michael@0: if (!type) return; michael@0: michael@0: // Some listener objects may be added from plugins, thus getting michael@0: // translated to native code. michael@0: if (!url) { michael@0: url = this._inNativeCodeString; michael@0: } michael@0: michael@0: // If an event item for this listener's url and type was already added, michael@0: // avoid polluting the view and simply increase the "targets" count. michael@0: let eventItem = this.getItemForPredicate(aItem => michael@0: aItem.attachment.url == url && michael@0: aItem.attachment.type == type); michael@0: if (eventItem) { michael@0: let { selectors, view: { targets } } = eventItem.attachment; michael@0: if (selectors.indexOf(selector) == -1) { michael@0: selectors.push(selector); michael@0: targets.setAttribute("value", L10N.getFormatStr("eventNodes", selectors.length)); michael@0: } michael@0: return; michael@0: } michael@0: michael@0: // There's no easy way of grouping event types into higher-level groups, michael@0: // so we need to do this by hand. michael@0: let is = (...args) => args.indexOf(type) != -1; michael@0: let has = str => type.contains(str); michael@0: let starts = str => type.startsWith(str); michael@0: let group; michael@0: michael@0: if (starts("animation")) { michael@0: group = L10N.getStr("animationEvents"); michael@0: } else if (starts("audio")) { michael@0: group = L10N.getStr("audioEvents"); michael@0: } else if (is("levelchange")) { michael@0: group = L10N.getStr("batteryEvents"); michael@0: } else if (is("cut", "copy", "paste")) { michael@0: group = L10N.getStr("clipboardEvents"); michael@0: } else if (starts("composition")) { michael@0: group = L10N.getStr("compositionEvents"); michael@0: } else if (starts("device")) { michael@0: group = L10N.getStr("deviceEvents"); michael@0: } else if (is("fullscreenchange", "fullscreenerror", "orientationchange", michael@0: "overflow", "resize", "scroll", "underflow", "zoom")) { michael@0: group = L10N.getStr("displayEvents"); michael@0: } else if (starts("drag") || starts("drop")) { michael@0: group = L10N.getStr("Drag and dropEvents"); michael@0: } else if (starts("gamepad")) { michael@0: group = L10N.getStr("gamepadEvents"); michael@0: } else if (is("canplay", "canplaythrough", "durationchange", "emptied", michael@0: "ended", "loadeddata", "loadedmetadata", "pause", "play", "playing", michael@0: "ratechange", "seeked", "seeking", "stalled", "suspend", "timeupdate", michael@0: "volumechange", "waiting")) { michael@0: group = L10N.getStr("mediaEvents"); michael@0: } else if (is("blocked", "complete", "success", "upgradeneeded", "versionchange")) { michael@0: group = L10N.getStr("indexedDBEvents"); michael@0: } else if (is("blur", "change", "focus", "focusin", "focusout", "invalid", michael@0: "reset", "select", "submit")) { michael@0: group = L10N.getStr("interactionEvents"); michael@0: } else if (starts("key") || is("input")) { michael@0: group = L10N.getStr("keyboardEvents"); michael@0: } else if (starts("mouse") || has("click") || is("contextmenu", "show", "wheel")) { michael@0: group = L10N.getStr("mouseEvents"); michael@0: } else if (starts("DOM")) { michael@0: group = L10N.getStr("mutationEvents"); michael@0: } else if (is("abort", "error", "hashchange", "load", "loadend", "loadstart", michael@0: "pagehide", "pageshow", "progress", "timeout", "unload", "uploadprogress", michael@0: "visibilitychange")) { michael@0: group = L10N.getStr("navigationEvents"); michael@0: } else if (is("pointerlockchange", "pointerlockerror")) { michael@0: group = L10N.getStr("Pointer lockEvents"); michael@0: } else if (is("compassneedscalibration", "userproximity")) { michael@0: group = L10N.getStr("sensorEvents"); michael@0: } else if (starts("storage")) { michael@0: group = L10N.getStr("storageEvents"); michael@0: } else if (is("beginEvent", "endEvent", "repeatEvent")) { michael@0: group = L10N.getStr("timeEvents"); michael@0: } else if (starts("touch")) { michael@0: group = L10N.getStr("touchEvents"); michael@0: } else { michael@0: group = L10N.getStr("otherEvents"); michael@0: } michael@0: michael@0: // Create the element node for the event listener item. michael@0: let itemView = this._createItemView(type, selector, url); michael@0: michael@0: // Event breakpoints survive target navigations. Make sure the newly michael@0: // inserted event item is correctly checked. michael@0: let checkboxState = michael@0: DebuggerController.Breakpoints.DOM.activeEventNames.indexOf(type) != -1; michael@0: michael@0: // Append an event listener item to this container. michael@0: this.push([itemView.container], { michael@0: staged: aOptions.staged, /* stage the item to be appended later? */ michael@0: attachment: { michael@0: url: url, michael@0: type: type, michael@0: view: itemView, michael@0: selectors: [selector], michael@0: group: group, michael@0: checkboxState: checkboxState, michael@0: checkboxTooltip: this._eventCheckboxTooltip michael@0: } michael@0: }); michael@0: }, michael@0: michael@0: /** michael@0: * Gets all the event types known to this container. michael@0: * michael@0: * @return array michael@0: * List of event types, for example ["load", "click"...] michael@0: */ michael@0: getAllEvents: function() { michael@0: return this.attachments.map(e => e.type); michael@0: }, michael@0: michael@0: /** michael@0: * Gets the checked event types in this container. michael@0: * michael@0: * @return array michael@0: * List of event types, for example ["load", "click"...] michael@0: */ michael@0: getCheckedEvents: function() { michael@0: return this.attachments.filter(e => e.checkboxState).map(e => e.type); michael@0: }, michael@0: michael@0: /** michael@0: * Customization function for creating an item's UI. michael@0: * michael@0: * @param string aType michael@0: * The event type, for example "click". michael@0: * @param string aSelector michael@0: * The target element's selector. michael@0: * @param string url michael@0: * The source url in which the event listener is located. michael@0: * @return object michael@0: * An object containing the event listener view nodes. michael@0: */ michael@0: _createItemView: function(aType, aSelector, aUrl) { michael@0: let container = document.createElement("hbox"); michael@0: container.className = "dbg-event-listener"; michael@0: michael@0: let eventType = document.createElement("label"); michael@0: eventType.className = "plain dbg-event-listener-type"; michael@0: eventType.setAttribute("value", aType); michael@0: container.appendChild(eventType); michael@0: michael@0: let typeSeparator = document.createElement("label"); michael@0: typeSeparator.className = "plain dbg-event-listener-separator"; michael@0: typeSeparator.setAttribute("value", this._onSelectorString); michael@0: container.appendChild(typeSeparator); michael@0: michael@0: let eventTargets = document.createElement("label"); michael@0: eventTargets.className = "plain dbg-event-listener-targets"; michael@0: eventTargets.setAttribute("value", aSelector); michael@0: container.appendChild(eventTargets); michael@0: michael@0: let selectorSeparator = document.createElement("label"); michael@0: selectorSeparator.className = "plain dbg-event-listener-separator"; michael@0: selectorSeparator.setAttribute("value", this._inSourceString); michael@0: container.appendChild(selectorSeparator); michael@0: michael@0: let eventLocation = document.createElement("label"); michael@0: eventLocation.className = "plain dbg-event-listener-location"; michael@0: eventLocation.setAttribute("value", SourceUtils.getSourceLabel(aUrl)); michael@0: eventLocation.setAttribute("flex", "1"); michael@0: eventLocation.setAttribute("crop", "center"); michael@0: container.appendChild(eventLocation); michael@0: michael@0: return { michael@0: container: container, michael@0: type: eventType, michael@0: targets: eventTargets, michael@0: location: eventLocation michael@0: }; michael@0: }, michael@0: michael@0: /** michael@0: * The check listener for the event listeners container. michael@0: */ michael@0: _onCheck: function({ detail: { description, checked }, target }) { michael@0: if (description == "item") { michael@0: this.getItemForElement(target).attachment.checkboxState = checked; michael@0: DebuggerController.Breakpoints.DOM.scheduleEventBreakpointsUpdate(); michael@0: return; michael@0: } michael@0: michael@0: // Check all the event items in this group. michael@0: this.items michael@0: .filter(e => e.attachment.group == description) michael@0: .forEach(e => this.callMethod("checkItem", e.target, checked)); michael@0: }, michael@0: michael@0: /** michael@0: * The select listener for the event listeners container. michael@0: */ michael@0: _onClick: function({ target }) { michael@0: // Changing the checkbox state is handled by the _onCheck event. Avoid michael@0: // handling that again in this click event, so pass in "noSiblings" michael@0: // when retrieving the target's item, to ignore the checkbox. michael@0: let eventItem = this.getItemForElement(target, { noSiblings: true }); michael@0: if (eventItem) { michael@0: let newState = eventItem.attachment.checkboxState ^= 1; michael@0: this.callMethod("checkItem", eventItem.target, newState); michael@0: } michael@0: }, michael@0: michael@0: _eventCheckboxTooltip: "", michael@0: _onSelectorString: "", michael@0: _inSourceString: "", michael@0: _inNativeCodeString: "" michael@0: }); michael@0: michael@0: /** michael@0: * Functions handling the global search UI. michael@0: */ michael@0: function GlobalSearchView() { michael@0: dumpn("GlobalSearchView was instantiated"); michael@0: michael@0: this._onHeaderClick = this._onHeaderClick.bind(this); michael@0: this._onLineClick = this._onLineClick.bind(this); michael@0: this._onMatchClick = this._onMatchClick.bind(this); michael@0: } michael@0: michael@0: GlobalSearchView.prototype = Heritage.extend(WidgetMethods, { michael@0: /** michael@0: * Initialization function, called when the debugger is started. michael@0: */ michael@0: initialize: function() { michael@0: dumpn("Initializing the GlobalSearchView"); michael@0: michael@0: this.widget = new SimpleListWidget(document.getElementById("globalsearch")); michael@0: this._splitter = document.querySelector("#globalsearch + .devtools-horizontal-splitter"); michael@0: michael@0: this.emptyText = L10N.getStr("noMatchingStringsText"); michael@0: }, michael@0: michael@0: /** michael@0: * Destruction function, called when the debugger is closed. michael@0: */ michael@0: destroy: function() { michael@0: dumpn("Destroying the GlobalSearchView"); michael@0: }, michael@0: michael@0: /** michael@0: * Sets the results container hidden or visible. It's hidden by default. michael@0: * @param boolean aFlag michael@0: */ michael@0: set hidden(aFlag) { michael@0: this.widget.setAttribute("hidden", aFlag); michael@0: this._splitter.setAttribute("hidden", aFlag); michael@0: }, michael@0: michael@0: /** michael@0: * Gets the visibility state of the global search container. michael@0: * @return boolean michael@0: */ michael@0: get hidden() michael@0: this.widget.getAttribute("hidden") == "true" || michael@0: this._splitter.getAttribute("hidden") == "true", michael@0: michael@0: /** michael@0: * Hides and removes all items from this search container. michael@0: */ michael@0: clearView: function() { michael@0: this.hidden = true; michael@0: this.empty(); michael@0: }, michael@0: michael@0: /** michael@0: * Selects the next found item in this container. michael@0: * Does not change the currently focused node. michael@0: */ michael@0: selectNext: function() { michael@0: let totalLineResults = LineResults.size(); michael@0: if (!totalLineResults) { michael@0: return; michael@0: } michael@0: if (++this._currentlyFocusedMatch >= totalLineResults) { michael@0: this._currentlyFocusedMatch = 0; michael@0: } michael@0: this._onMatchClick({ michael@0: target: LineResults.getElementAtIndex(this._currentlyFocusedMatch) michael@0: }); michael@0: }, michael@0: michael@0: /** michael@0: * Selects the previously found item in this container. michael@0: * Does not change the currently focused node. michael@0: */ michael@0: selectPrev: function() { michael@0: let totalLineResults = LineResults.size(); michael@0: if (!totalLineResults) { michael@0: return; michael@0: } michael@0: if (--this._currentlyFocusedMatch < 0) { michael@0: this._currentlyFocusedMatch = totalLineResults - 1; michael@0: } michael@0: this._onMatchClick({ michael@0: target: LineResults.getElementAtIndex(this._currentlyFocusedMatch) michael@0: }); michael@0: }, michael@0: michael@0: /** michael@0: * Schedules searching for a string in all of the sources. michael@0: * michael@0: * @param string aToken michael@0: * The string to search for. michael@0: * @param number aWait michael@0: * The amount of milliseconds to wait until draining. michael@0: */ michael@0: scheduleSearch: function(aToken, aWait) { michael@0: // The amount of time to wait for the requests to settle. michael@0: let maxDelay = GLOBAL_SEARCH_ACTION_MAX_DELAY; michael@0: let delay = aWait === undefined ? maxDelay / aToken.length : aWait; michael@0: michael@0: // Allow requests to settle down first. michael@0: setNamedTimeout("global-search", delay, () => { michael@0: // Start fetching as many sources as possible, then perform the search. michael@0: let urls = DebuggerView.Sources.values; michael@0: let sourcesFetched = DebuggerController.SourceScripts.getTextForSources(urls); michael@0: sourcesFetched.then(aSources => this._doSearch(aToken, aSources)); michael@0: }); michael@0: }, michael@0: michael@0: /** michael@0: * Finds string matches in all the sources stored in the controller's cache, michael@0: * and groups them by url and line number. michael@0: * michael@0: * @param string aToken michael@0: * The string to search for. michael@0: * @param array aSources michael@0: * An array of [url, text] tuples for each source. michael@0: */ michael@0: _doSearch: function(aToken, aSources) { michael@0: // Don't continue filtering if the searched token is an empty string. michael@0: if (!aToken) { michael@0: this.clearView(); michael@0: return; michael@0: } michael@0: michael@0: // Search is not case sensitive, prepare the actual searched token. michael@0: let lowerCaseToken = aToken.toLowerCase(); michael@0: let tokenLength = aToken.length; michael@0: michael@0: // Create a Map containing search details for each source. michael@0: let globalResults = new GlobalResults(); michael@0: michael@0: // Search for the specified token in each source's text. michael@0: for (let [url, text] of aSources) { michael@0: // Verify that the search token is found anywhere in the source. michael@0: if (!text.toLowerCase().contains(lowerCaseToken)) { michael@0: continue; michael@0: } michael@0: // ...and if so, create a Map containing search details for each line. michael@0: let sourceResults = new SourceResults(url, globalResults); michael@0: michael@0: // Search for the specified token in each line's text. michael@0: text.split("\n").forEach((aString, aLine) => { michael@0: // Search is not case sensitive, prepare the actual searched line. michael@0: let lowerCaseLine = aString.toLowerCase(); michael@0: michael@0: // Verify that the search token is found anywhere in this line. michael@0: if (!lowerCaseLine.contains(lowerCaseToken)) { michael@0: return; michael@0: } michael@0: // ...and if so, create a Map containing search details for each word. michael@0: let lineResults = new LineResults(aLine, sourceResults); michael@0: michael@0: // Search for the specified token this line's text. michael@0: lowerCaseLine.split(lowerCaseToken).reduce((aPrev, aCurr, aIndex, aArray) => { michael@0: let prevLength = aPrev.length; michael@0: let currLength = aCurr.length; michael@0: michael@0: // Everything before the token is unmatched. michael@0: let unmatched = aString.substr(prevLength, currLength); michael@0: lineResults.add(unmatched); michael@0: michael@0: // The lowered-case line was split by the lowered-case token. So, michael@0: // get the actual matched text from the original line's text. michael@0: if (aIndex != aArray.length - 1) { michael@0: let matched = aString.substr(prevLength + currLength, tokenLength); michael@0: let range = { start: prevLength + currLength, length: matched.length }; michael@0: lineResults.add(matched, range, true); michael@0: } michael@0: michael@0: // Continue with the next sub-region in this line's text. michael@0: return aPrev + aToken + aCurr; michael@0: }, ""); michael@0: michael@0: if (lineResults.matchCount) { michael@0: sourceResults.add(lineResults); michael@0: } michael@0: }); michael@0: michael@0: if (sourceResults.matchCount) { michael@0: globalResults.add(sourceResults); michael@0: } michael@0: } michael@0: michael@0: // Rebuild the results, then signal if there are any matches. michael@0: if (globalResults.matchCount) { michael@0: this.hidden = false; michael@0: this._currentlyFocusedMatch = -1; michael@0: this._createGlobalResultsUI(globalResults); michael@0: window.emit(EVENTS.GLOBAL_SEARCH_MATCH_FOUND); michael@0: } else { michael@0: window.emit(EVENTS.GLOBAL_SEARCH_MATCH_NOT_FOUND); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Creates global search results entries and adds them to this container. michael@0: * michael@0: * @param GlobalResults aGlobalResults michael@0: * An object containing all source results, grouped by source location. michael@0: */ michael@0: _createGlobalResultsUI: function(aGlobalResults) { michael@0: let i = 0; michael@0: michael@0: for (let sourceResults of aGlobalResults) { michael@0: if (i++ == 0) { michael@0: this._createSourceResultsUI(sourceResults); michael@0: } else { michael@0: // Dispatch subsequent document manipulation operations, to avoid michael@0: // blocking the main thread when a large number of search results michael@0: // is found, thus giving the impression of faster searching. michael@0: Services.tm.currentThread.dispatch({ run: michael@0: this._createSourceResultsUI.bind(this, sourceResults) michael@0: }, 0); michael@0: } michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Creates source search results entries and adds them to this container. michael@0: * michael@0: * @param SourceResults aSourceResults michael@0: * An object containing all the matched lines for a specific source. michael@0: */ michael@0: _createSourceResultsUI: function(aSourceResults) { michael@0: // Create the element node for the source results item. michael@0: let container = document.createElement("hbox"); michael@0: aSourceResults.createView(container, { michael@0: onHeaderClick: this._onHeaderClick, michael@0: onLineClick: this._onLineClick, michael@0: onMatchClick: this._onMatchClick michael@0: }); michael@0: michael@0: // Append a source results item to this container. michael@0: let item = this.push([container], { michael@0: index: -1, /* specifies on which position should the item be appended */ michael@0: attachment: { michael@0: sourceResults: aSourceResults michael@0: } michael@0: }); michael@0: }, michael@0: michael@0: /** michael@0: * The click listener for a results header. michael@0: */ michael@0: _onHeaderClick: function(e) { michael@0: let sourceResultsItem = SourceResults.getItemForElement(e.target); michael@0: sourceResultsItem.instance.toggle(e); michael@0: }, michael@0: michael@0: /** michael@0: * The click listener for a results line. michael@0: */ michael@0: _onLineClick: function(e) { michael@0: let lineResultsItem = LineResults.getItemForElement(e.target); michael@0: this._onMatchClick({ target: lineResultsItem.firstMatch }); michael@0: }, michael@0: michael@0: /** michael@0: * The click listener for a result match. michael@0: */ michael@0: _onMatchClick: function(e) { michael@0: if (e instanceof Event) { michael@0: e.preventDefault(); michael@0: e.stopPropagation(); michael@0: } michael@0: michael@0: let target = e.target; michael@0: let sourceResultsItem = SourceResults.getItemForElement(target); michael@0: let lineResultsItem = LineResults.getItemForElement(target); michael@0: michael@0: sourceResultsItem.instance.expand(); michael@0: this._currentlyFocusedMatch = LineResults.indexOfElement(target); michael@0: this._scrollMatchIntoViewIfNeeded(target); michael@0: this._bounceMatch(target); michael@0: michael@0: let url = sourceResultsItem.instance.url; michael@0: let line = lineResultsItem.instance.line; michael@0: michael@0: DebuggerView.setEditorLocation(url, line + 1, { noDebug: true }); michael@0: michael@0: let range = lineResultsItem.lineData.range; michael@0: let cursor = DebuggerView.editor.getOffset({ line: line, ch: 0 }); michael@0: let [ anchor, head ] = DebuggerView.editor.getPosition( michael@0: cursor + range.start, michael@0: cursor + range.start + range.length michael@0: ); michael@0: michael@0: DebuggerView.editor.setSelection(anchor, head); michael@0: }, michael@0: michael@0: /** michael@0: * Scrolls a match into view if not already visible. michael@0: * michael@0: * @param nsIDOMNode aMatch michael@0: * The match to scroll into view. michael@0: */ michael@0: _scrollMatchIntoViewIfNeeded: function(aMatch) { michael@0: this.widget.ensureElementIsVisible(aMatch); michael@0: }, michael@0: michael@0: /** michael@0: * Starts a bounce animation for a match. michael@0: * michael@0: * @param nsIDOMNode aMatch michael@0: * The match to start a bounce animation for. michael@0: */ michael@0: _bounceMatch: function(aMatch) { michael@0: Services.tm.currentThread.dispatch({ run: () => { michael@0: aMatch.addEventListener("transitionend", function onEvent() { michael@0: aMatch.removeEventListener("transitionend", onEvent); michael@0: aMatch.removeAttribute("focused"); michael@0: }); michael@0: aMatch.setAttribute("focused", ""); michael@0: }}, 0); michael@0: aMatch.setAttribute("focusing", ""); michael@0: }, michael@0: michael@0: _splitter: null, michael@0: _currentlyFocusedMatch: -1, michael@0: _forceExpandResults: false michael@0: }); michael@0: michael@0: /** michael@0: * An object containing all source results, grouped by source location. michael@0: * Iterable via "for (let [location, sourceResults] of globalResults) { }". michael@0: */ michael@0: function GlobalResults() { michael@0: this._store = []; michael@0: SourceResults._itemsByElement = new Map(); michael@0: LineResults._itemsByElement = new Map(); michael@0: } michael@0: michael@0: GlobalResults.prototype = { michael@0: /** michael@0: * Adds source results to this store. michael@0: * michael@0: * @param SourceResults aSourceResults michael@0: * An object containing search results for a specific source. michael@0: */ michael@0: add: function(aSourceResults) { michael@0: this._store.push(aSourceResults); michael@0: }, michael@0: michael@0: /** michael@0: * Gets the number of source results in this store. michael@0: */ michael@0: get matchCount() this._store.length michael@0: }; michael@0: michael@0: /** michael@0: * An object containing all the matched lines for a specific source. michael@0: * Iterable via "for (let [lineNumber, lineResults] of sourceResults) { }". michael@0: * michael@0: * @param string aUrl michael@0: * The target source url. michael@0: * @param GlobalResults aGlobalResults michael@0: * An object containing all source results, grouped by source location. michael@0: */ michael@0: function SourceResults(aUrl, aGlobalResults) { michael@0: this.url = aUrl; michael@0: this._globalResults = aGlobalResults; michael@0: this._store = []; michael@0: } michael@0: michael@0: SourceResults.prototype = { michael@0: /** michael@0: * Adds line results to this store. michael@0: * michael@0: * @param LineResults aLineResults michael@0: * An object containing search results for a specific line. michael@0: */ michael@0: add: function(aLineResults) { michael@0: this._store.push(aLineResults); michael@0: }, michael@0: michael@0: /** michael@0: * Gets the number of line results in this store. michael@0: */ michael@0: get matchCount() this._store.length, michael@0: michael@0: /** michael@0: * Expands the element, showing all the added details. michael@0: */ michael@0: expand: function() { michael@0: this._resultsContainer.removeAttribute("hidden"); michael@0: this._arrow.setAttribute("open", ""); michael@0: }, michael@0: michael@0: /** michael@0: * Collapses the element, hiding all the added details. michael@0: */ michael@0: collapse: function() { michael@0: this._resultsContainer.setAttribute("hidden", "true"); michael@0: this._arrow.removeAttribute("open"); michael@0: }, michael@0: michael@0: /** michael@0: * Toggles between the element collapse/expand state. michael@0: */ michael@0: toggle: function(e) { michael@0: this.expanded ^= 1; michael@0: }, michael@0: michael@0: /** michael@0: * Gets this element's expanded state. michael@0: * @return boolean michael@0: */ michael@0: get expanded() michael@0: this._resultsContainer.getAttribute("hidden") != "true" && michael@0: this._arrow.hasAttribute("open"), michael@0: michael@0: /** michael@0: * Sets this element's expanded state. michael@0: * @param boolean aFlag michael@0: */ michael@0: set expanded(aFlag) this[aFlag ? "expand" : "collapse"](), michael@0: michael@0: /** michael@0: * Gets the element associated with this item. michael@0: * @return nsIDOMNode michael@0: */ michael@0: get target() this._target, michael@0: michael@0: /** michael@0: * Customization function for creating this item's UI. michael@0: * michael@0: * @param nsIDOMNode aElementNode michael@0: * The element associated with the displayed item. michael@0: * @param object aCallbacks michael@0: * An object containing all the necessary callback functions: michael@0: * - onHeaderClick michael@0: * - onMatchClick michael@0: */ michael@0: createView: function(aElementNode, aCallbacks) { michael@0: this._target = aElementNode; michael@0: michael@0: let arrow = this._arrow = document.createElement("box"); michael@0: arrow.className = "arrow"; michael@0: michael@0: let locationNode = document.createElement("label"); michael@0: locationNode.className = "plain dbg-results-header-location"; michael@0: locationNode.setAttribute("value", this.url); michael@0: michael@0: let matchCountNode = document.createElement("label"); michael@0: matchCountNode.className = "plain dbg-results-header-match-count"; michael@0: matchCountNode.setAttribute("value", "(" + this.matchCount + ")"); michael@0: michael@0: let resultsHeader = this._resultsHeader = document.createElement("hbox"); michael@0: resultsHeader.className = "dbg-results-header"; michael@0: resultsHeader.setAttribute("align", "center") michael@0: resultsHeader.appendChild(arrow); michael@0: resultsHeader.appendChild(locationNode); michael@0: resultsHeader.appendChild(matchCountNode); michael@0: resultsHeader.addEventListener("click", aCallbacks.onHeaderClick, false); michael@0: michael@0: let resultsContainer = this._resultsContainer = document.createElement("vbox"); michael@0: resultsContainer.className = "dbg-results-container"; michael@0: resultsContainer.setAttribute("hidden", "true"); michael@0: michael@0: // Create lines search results entries and add them to this container. michael@0: // Afterwards, if the number of matches is reasonable, expand this michael@0: // container automatically. michael@0: for (let lineResults of this._store) { michael@0: lineResults.createView(resultsContainer, aCallbacks); michael@0: } michael@0: if (this.matchCount < GLOBAL_SEARCH_EXPAND_MAX_RESULTS) { michael@0: this.expand(); michael@0: } michael@0: michael@0: let resultsBox = document.createElement("vbox"); michael@0: resultsBox.setAttribute("flex", "1"); michael@0: resultsBox.appendChild(resultsHeader); michael@0: resultsBox.appendChild(resultsContainer); michael@0: michael@0: aElementNode.id = "source-results-" + this.url; michael@0: aElementNode.className = "dbg-source-results"; michael@0: aElementNode.appendChild(resultsBox); michael@0: michael@0: SourceResults._itemsByElement.set(aElementNode, { instance: this }); michael@0: }, michael@0: michael@0: url: "", michael@0: _globalResults: null, michael@0: _store: null, michael@0: _target: null, michael@0: _arrow: null, michael@0: _resultsHeader: null, michael@0: _resultsContainer: null michael@0: }; michael@0: michael@0: /** michael@0: * An object containing all the matches for a specific line. michael@0: * Iterable via "for (let chunk of lineResults) { }". michael@0: * michael@0: * @param number aLine michael@0: * The target line in the source. michael@0: * @param SourceResults aSourceResults michael@0: * An object containing all the matched lines for a specific source. michael@0: */ michael@0: function LineResults(aLine, aSourceResults) { michael@0: this.line = aLine; michael@0: this._sourceResults = aSourceResults; michael@0: this._store = []; michael@0: this._matchCount = 0; michael@0: } michael@0: michael@0: LineResults.prototype = { michael@0: /** michael@0: * Adds string details to this store. michael@0: * michael@0: * @param string aString michael@0: * The text contents chunk in the line. michael@0: * @param object aRange michael@0: * An object containing the { start, length } of the chunk. michael@0: * @param boolean aMatchFlag michael@0: * True if the chunk is a matched string, false if just text content. michael@0: */ michael@0: add: function(aString, aRange, aMatchFlag) { michael@0: this._store.push({ string: aString, range: aRange, match: !!aMatchFlag }); michael@0: this._matchCount += aMatchFlag ? 1 : 0; michael@0: }, michael@0: michael@0: /** michael@0: * Gets the number of word results in this store. michael@0: */ michael@0: get matchCount() this._matchCount, michael@0: michael@0: /** michael@0: * Gets the element associated with this item. michael@0: * @return nsIDOMNode michael@0: */ michael@0: get target() this._target, michael@0: michael@0: /** michael@0: * Customization function for creating this item's UI. michael@0: * michael@0: * @param nsIDOMNode aElementNode michael@0: * The element associated with the displayed item. michael@0: * @param object aCallbacks michael@0: * An object containing all the necessary callback functions: michael@0: * - onMatchClick michael@0: * - onLineClick michael@0: */ michael@0: createView: function(aElementNode, aCallbacks) { michael@0: this._target = aElementNode; michael@0: michael@0: let lineNumberNode = document.createElement("label"); michael@0: lineNumberNode.className = "plain dbg-results-line-number"; michael@0: lineNumberNode.classList.add("devtools-monospace"); michael@0: lineNumberNode.setAttribute("value", this.line + 1); michael@0: michael@0: let lineContentsNode = document.createElement("hbox"); michael@0: lineContentsNode.className = "dbg-results-line-contents"; michael@0: lineContentsNode.classList.add("devtools-monospace"); michael@0: lineContentsNode.setAttribute("flex", "1"); michael@0: michael@0: let lineString = ""; michael@0: let lineLength = 0; michael@0: let firstMatch = null; michael@0: michael@0: for (let lineChunk of this._store) { michael@0: let { string, range, match } = lineChunk; michael@0: lineString = string.substr(0, GLOBAL_SEARCH_LINE_MAX_LENGTH - lineLength); michael@0: lineLength += string.length; michael@0: michael@0: let lineChunkNode = document.createElement("label"); michael@0: lineChunkNode.className = "plain dbg-results-line-contents-string"; michael@0: lineChunkNode.setAttribute("value", lineString); michael@0: lineChunkNode.setAttribute("match", match); michael@0: lineContentsNode.appendChild(lineChunkNode); michael@0: michael@0: if (match) { michael@0: this._entangleMatch(lineChunkNode, lineChunk); michael@0: lineChunkNode.addEventListener("click", aCallbacks.onMatchClick, false); michael@0: firstMatch = firstMatch || lineChunkNode; michael@0: } michael@0: if (lineLength >= GLOBAL_SEARCH_LINE_MAX_LENGTH) { michael@0: lineContentsNode.appendChild(this._ellipsis.cloneNode(true)); michael@0: break; michael@0: } michael@0: } michael@0: michael@0: this._entangleLine(lineContentsNode, firstMatch); michael@0: lineContentsNode.addEventListener("click", aCallbacks.onLineClick, false); michael@0: michael@0: let searchResult = document.createElement("hbox"); michael@0: searchResult.className = "dbg-search-result"; michael@0: searchResult.appendChild(lineNumberNode); michael@0: searchResult.appendChild(lineContentsNode); michael@0: michael@0: aElementNode.appendChild(searchResult); michael@0: }, michael@0: michael@0: /** michael@0: * Handles a match while creating the view. michael@0: * @param nsIDOMNode aNode michael@0: * @param object aMatchChunk michael@0: */ michael@0: _entangleMatch: function(aNode, aMatchChunk) { michael@0: LineResults._itemsByElement.set(aNode, { michael@0: instance: this, michael@0: lineData: aMatchChunk michael@0: }); michael@0: }, michael@0: michael@0: /** michael@0: * Handles a line while creating the view. michael@0: * @param nsIDOMNode aNode michael@0: * @param nsIDOMNode aFirstMatch michael@0: */ michael@0: _entangleLine: function(aNode, aFirstMatch) { michael@0: LineResults._itemsByElement.set(aNode, { michael@0: instance: this, michael@0: firstMatch: aFirstMatch, michael@0: ignored: true michael@0: }); michael@0: }, michael@0: michael@0: /** michael@0: * An nsIDOMNode label with an ellipsis value. michael@0: */ michael@0: _ellipsis: (function() { michael@0: let label = document.createElement("label"); michael@0: label.className = "plain dbg-results-line-contents-string"; michael@0: label.setAttribute("value", L10N.ellipsis); michael@0: return label; michael@0: })(), michael@0: michael@0: line: 0, michael@0: _sourceResults: null, michael@0: _store: null, michael@0: _target: null michael@0: }; michael@0: michael@0: /** michael@0: * A generator-iterator over the global, source or line results. michael@0: */ michael@0: GlobalResults.prototype["@@iterator"] = michael@0: SourceResults.prototype["@@iterator"] = michael@0: LineResults.prototype["@@iterator"] = function*() { michael@0: yield* this._store; michael@0: }; michael@0: michael@0: /** michael@0: * Gets the item associated with the specified element. michael@0: * michael@0: * @param nsIDOMNode aElement michael@0: * The element used to identify the item. michael@0: * @return object michael@0: * The matched item, or null if nothing is found. michael@0: */ michael@0: SourceResults.getItemForElement = michael@0: LineResults.getItemForElement = function(aElement) { michael@0: return WidgetMethods.getItemForElement.call(this, aElement, { noSiblings: true }); michael@0: }; michael@0: michael@0: /** michael@0: * Gets the element associated with a particular item at a specified index. michael@0: * michael@0: * @param number aIndex michael@0: * The index used to identify the item. michael@0: * @return nsIDOMNode michael@0: * The matched element, or null if nothing is found. michael@0: */ michael@0: SourceResults.getElementAtIndex = michael@0: LineResults.getElementAtIndex = function(aIndex) { michael@0: for (let [element, item] of this._itemsByElement) { michael@0: if (!item.ignored && !aIndex--) { michael@0: return element; michael@0: } michael@0: } michael@0: return null; michael@0: }; michael@0: michael@0: /** michael@0: * Gets the index of an item associated with the specified element. michael@0: * michael@0: * @param nsIDOMNode aElement michael@0: * The element to get the index for. michael@0: * @return number michael@0: * The index of the matched element, or -1 if nothing is found. michael@0: */ michael@0: SourceResults.indexOfElement = michael@0: LineResults.indexOfElement = function(aElement) { michael@0: let count = 0; michael@0: for (let [element, item] of this._itemsByElement) { michael@0: if (element == aElement) { michael@0: return count; michael@0: } michael@0: if (!item.ignored) { michael@0: count++; michael@0: } michael@0: } michael@0: return -1; michael@0: }; michael@0: michael@0: /** michael@0: * Gets the number of cached items associated with a specified element. michael@0: * michael@0: * @return number michael@0: * The number of key/value pairs in the corresponding map. michael@0: */ michael@0: SourceResults.size = michael@0: LineResults.size = function() { michael@0: let count = 0; michael@0: for (let [, item] of this._itemsByElement) { michael@0: if (!item.ignored) { michael@0: count++; michael@0: } michael@0: } michael@0: return count; michael@0: }; michael@0: michael@0: /** michael@0: * Preliminary setup for the DebuggerView object. michael@0: */ michael@0: DebuggerView.Sources = new SourcesView(); michael@0: DebuggerView.VariableBubble = new VariableBubbleView(); michael@0: DebuggerView.Tracer = new TracerView(); michael@0: DebuggerView.WatchExpressions = new WatchExpressionsView(); michael@0: DebuggerView.EventListeners = new EventListenersView(); michael@0: DebuggerView.GlobalSearch = new GlobalSearchView();