/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ "use strict"; // Maps known URLs to friendly source group names and put them at the // bottom of source list. const KNOWN_SOURCE_GROUPS = { "Add-on SDK": "resource://gre/modules/commonjs/", }; KNOWN_SOURCE_GROUPS[L10N.getStr("anonymousSourcesLabel")] = "anonymous"; /** * Functions handling the sources UI. */ function SourcesView(DebuggerController, DebuggerView) { dumpn("SourcesView was instantiated"); this.Breakpoints = DebuggerController.Breakpoints; this.SourceScripts = DebuggerController.SourceScripts; this.DebuggerView = DebuggerView; this.togglePrettyPrint = this.togglePrettyPrint.bind(this); this.toggleBlackBoxing = this.toggleBlackBoxing.bind(this); this.toggleBreakpoints = this.toggleBreakpoints.bind(this); this._onEditorLoad = this._onEditorLoad.bind(this); this._onEditorUnload = this._onEditorUnload.bind(this); this._onEditorCursorActivity = this._onEditorCursorActivity.bind(this); this._onSourceSelect = this._onSourceSelect.bind(this); this._onStopBlackBoxing = this._onStopBlackBoxing.bind(this); this._onBreakpointRemoved = this._onBreakpointRemoved.bind(this); this._onBreakpointClick = this._onBreakpointClick.bind(this); this._onBreakpointCheckboxClick = this._onBreakpointCheckboxClick.bind(this); this._onConditionalPopupShowing = this._onConditionalPopupShowing.bind(this); this._onConditionalPopupShown = this._onConditionalPopupShown.bind(this); this._onConditionalPopupHiding = this._onConditionalPopupHiding.bind(this); this._onConditionalTextboxKeyPress = this._onConditionalTextboxKeyPress.bind(this); this._onCopyUrlCommand = this._onCopyUrlCommand.bind(this); this._onNewTabCommand = this._onNewTabCommand.bind(this); } SourcesView.prototype = Heritage.extend(WidgetMethods, { /** * Initialization function, called when the debugger is started. */ initialize: function() { dumpn("Initializing the SourcesView"); this.widget = new SideMenuWidget(document.getElementById("sources"), { contextMenu: document.getElementById("debuggerSourcesContextMenu"), showArrows: true }); this._unnamedSourceIndex = 0; this.emptyText = L10N.getStr("noSourcesText"); this._blackBoxCheckboxTooltip = L10N.getStr("blackBoxCheckboxTooltip"); this._commandset = document.getElementById("debuggerCommands"); this._popupset = document.getElementById("debuggerPopupset"); this._cmPopup = document.getElementById("sourceEditorContextMenu"); this._cbPanel = document.getElementById("conditional-breakpoint-panel"); this._cbTextbox = document.getElementById("conditional-breakpoint-panel-textbox"); this._blackBoxButton = document.getElementById("black-box"); this._stopBlackBoxButton = document.getElementById("black-boxed-message-button"); this._prettyPrintButton = document.getElementById("pretty-print"); this._toggleBreakpointsButton = document.getElementById("toggle-breakpoints"); this._newTabMenuItem = document.getElementById("debugger-sources-context-newtab"); this._copyUrlMenuItem = document.getElementById("debugger-sources-context-copyurl"); if (Prefs.prettyPrintEnabled) { this._prettyPrintButton.removeAttribute("hidden"); } window.on(EVENTS.EDITOR_LOADED, this._onEditorLoad, false); window.on(EVENTS.EDITOR_UNLOADED, this._onEditorUnload, false); this.widget.addEventListener("select", this._onSourceSelect, false); this._stopBlackBoxButton.addEventListener("click", this._onStopBlackBoxing, false); this._cbPanel.addEventListener("popupshowing", this._onConditionalPopupShowing, false); this._cbPanel.addEventListener("popupshown", this._onConditionalPopupShown, false); this._cbPanel.addEventListener("popuphiding", this._onConditionalPopupHiding, false); this._cbTextbox.addEventListener("keypress", this._onConditionalTextboxKeyPress, false); this._copyUrlMenuItem.addEventListener("command", this._onCopyUrlCommand, false); this._newTabMenuItem.addEventListener("command", this._onNewTabCommand, false); this.allowFocusOnRightClick = true; this.autoFocusOnSelection = false; // Sort the contents by the displayed label. this.sortContents((aFirst, aSecond) => { return +(aFirst.attachment.label.toLowerCase() > aSecond.attachment.label.toLowerCase()); }); // Sort known source groups towards the end of the list this.widget.groupSortPredicate = function(a, b) { if ((a in KNOWN_SOURCE_GROUPS) == (b in KNOWN_SOURCE_GROUPS)) { return a.localeCompare(b); } return (a in KNOWN_SOURCE_GROUPS) ? 1 : -1; }; this._addCommands(); }, /** * Destruction function, called when the debugger is closed. */ destroy: function() { dumpn("Destroying the SourcesView"); window.off(EVENTS.EDITOR_LOADED, this._onEditorLoad, false); window.off(EVENTS.EDITOR_UNLOADED, this._onEditorUnload, false); this.widget.removeEventListener("select", this._onSourceSelect, false); this._stopBlackBoxButton.removeEventListener("click", this._onStopBlackBoxing, false); this._cbPanel.removeEventListener("popupshowing", this._onConditionalPopupShowing, false); this._cbPanel.removeEventListener("popupshowing", this._onConditionalPopupShown, false); this._cbPanel.removeEventListener("popuphiding", this._onConditionalPopupHiding, false); this._cbTextbox.removeEventListener("keypress", this._onConditionalTextboxKeyPress, false); this._copyUrlMenuItem.removeEventListener("command", this._onCopyUrlCommand, false); this._newTabMenuItem.removeEventListener("command", this._onNewTabCommand, false); }, empty: function() { WidgetMethods.empty.call(this); this._unnamedSourceIndex = 0; }, /** * Add commands that XUL can fire. */ _addCommands: function() { XULUtils.addCommands(this._commandset, { addBreakpointCommand: e => this._onCmdAddBreakpoint(e), addConditionalBreakpointCommand: e => this._onCmdAddConditionalBreakpoint(e), blackBoxCommand: () => this.toggleBlackBoxing(), unBlackBoxButton: () => this._onStopBlackBoxing(), prettyPrintCommand: () => this.togglePrettyPrint(), toggleBreakpointsCommand: () =>this.toggleBreakpoints(), togglePromiseDebuggerCommand: () => this.togglePromiseDebugger(), nextSourceCommand: () => this.selectNextItem(), prevSourceCommand: () => this.selectPrevItem() }); }, /** * Sets the preferred location to be selected in this sources container. * @param string aUrl */ set preferredSource(aUrl) { this._preferredValue = aUrl; // Selects the element with the specified value in this sources container, // if already inserted. if (this.containsValue(aUrl)) { this.selectedValue = aUrl; } }, /** * Adds a source to this sources container. * * @param object aSource * The source object coming from the active thread. * @param object aOptions [optional] * Additional options for adding the source. Supported options: * - staged: true to stage the item to be appended later */ addSource: function(aSource, aOptions = {}) { if (!aSource.url && !aOptions.force) { // We don't show any unnamed eval scripts yet (see bug 1124106) return; } let { label, group, unicodeUrl } = this._parseUrl(aSource); let contents = document.createElement("label"); contents.className = "plain dbg-source-item"; contents.setAttribute("value", label); contents.setAttribute("crop", "start"); contents.setAttribute("flex", "1"); contents.setAttribute("tooltiptext", unicodeUrl); // If the source is blackboxed, apply the appropriate style. if (gThreadClient.source(aSource).isBlackBoxed) { contents.classList.add("black-boxed"); } // Append a source item to this container. this.push([contents, aSource.actor], { staged: aOptions.staged, /* stage the item to be appended later? */ attachment: { label: label, group: group, checkboxState: !aSource.isBlackBoxed, checkboxTooltip: this._blackBoxCheckboxTooltip, source: aSource } }); }, _parseUrl: function(aSource) { let fullUrl = aSource.url; let url, unicodeUrl, label, group; if(!fullUrl) { unicodeUrl = 'SCRIPT' + this._unnamedSourceIndex++; label = unicodeUrl; group = L10N.getStr("anonymousSourcesLabel"); } else { let url = fullUrl.split(" -> ").pop(); label = aSource.addonPath ? aSource.addonPath : SourceUtils.getSourceLabel(url); group = aSource.addonID ? aSource.addonID : SourceUtils.getSourceGroup(url); unicodeUrl = NetworkHelper.convertToUnicode(unescape(fullUrl)); } return { label: label, group: group, unicodeUrl: unicodeUrl }; }, /** * Adds a breakpoint to this sources container. * * @param object aBreakpointClient * See Breakpoints.prototype._showBreakpoint * @param object aOptions [optional] * @see DebuggerController.Breakpoints.addBreakpoint */ addBreakpoint: function(aBreakpointClient, aOptions = {}) { let { location, disabled } = aBreakpointClient; // Make sure we're not duplicating anything. If a breakpoint at the // specified source url and line already exists, just toggle it. if (this.getBreakpoint(location)) { this[disabled ? "disableBreakpoint" : "enableBreakpoint"](location); return; } // Get the source item to which the breakpoint should be attached. let sourceItem = this.getItemByValue(this.getActorForLocation(location)); // Create the element node and menu popup for the breakpoint item. let breakpointArgs = Heritage.extend(aBreakpointClient, aOptions); let breakpointView = this._createBreakpointView.call(this, breakpointArgs); let contextMenu = this._createContextMenu.call(this, breakpointArgs); // Append a breakpoint child item to the corresponding source item. sourceItem.append(breakpointView.container, { attachment: Heritage.extend(breakpointArgs, { actor: location.actor, line: location.line, view: breakpointView, popup: contextMenu }), attributes: [ ["contextmenu", contextMenu.menupopupId] ], // Make sure that when the breakpoint item is removed, the corresponding // menupopup and commandset are also destroyed. finalize: this._onBreakpointRemoved }); // Highlight the newly appended breakpoint child item if // necessary. if (aOptions.openPopup || !aOptions.noEditorUpdate) { this.highlightBreakpoint(location, aOptions); } window.emit(EVENTS.BREAKPOINT_SHOWN_IN_PANE); }, /** * Removes a breakpoint from this sources container. * It does not also remove the breakpoint from the controller. Be careful. * * @param object aLocation * @see DebuggerController.Breakpoints.addBreakpoint */ removeBreakpoint: function(aLocation) { // When a parent source item is removed, all the child breakpoint items are // also automagically removed. let sourceItem = this.getItemByValue(aLocation.actor); if (!sourceItem) { return; } let breakpointItem = this.getBreakpoint(aLocation); if (!breakpointItem) { return; } // Clear the breakpoint view. sourceItem.remove(breakpointItem); window.emit(EVENTS.BREAKPOINT_HIDDEN_IN_PANE); }, /** * Returns the breakpoint at the specified source url and line. * * @param object aLocation * @see DebuggerController.Breakpoints.addBreakpoint * @return object * The corresponding breakpoint item if found, null otherwise. */ getBreakpoint: function(aLocation) { return this.getItemForPredicate(aItem => aItem.attachment.actor == aLocation.actor && aItem.attachment.line == aLocation.line); }, /** * Returns all breakpoints for all sources. * * @return array * The breakpoints for all sources if any, an empty array otherwise. */ getAllBreakpoints: function(aStore = []) { return this.getOtherBreakpoints(undefined, aStore); }, /** * Returns all breakpoints which are not at the specified source url and line. * * @param object aLocation [optional] * @see DebuggerController.Breakpoints.addBreakpoint * @param array aStore [optional] * A list in which to store the corresponding breakpoints. * @return array * The corresponding breakpoints if found, an empty array otherwise. */ getOtherBreakpoints: function(aLocation = {}, aStore = []) { for (let source of this) { for (let breakpointItem of source) { let { actor, line } = breakpointItem.attachment; if (actor != aLocation.actor || line != aLocation.line) { aStore.push(breakpointItem); } } } return aStore; }, /** * Enables a breakpoint. * * @param object aLocation * @see DebuggerController.Breakpoints.addBreakpoint * @param object aOptions [optional] * Additional options or flags supported by this operation: * - silent: pass true to not update the checkbox checked state; * this is usually necessary when the checked state will * be updated automatically (e.g: on a checkbox click). * @return object * A promise that is resolved after the breakpoint is enabled, or * rejected if no breakpoint was found at the specified location. */ enableBreakpoint: function(aLocation, aOptions = {}) { let breakpointItem = this.getBreakpoint(aLocation); if (!breakpointItem) { return promise.reject(new Error("No breakpoint found.")); } // Breakpoint will now be enabled. let attachment = breakpointItem.attachment; attachment.disabled = false; // Update the corresponding menu items to reflect the enabled state. let prefix = "bp-cMenu-"; // "breakpoints context menu" let identifier = this.Breakpoints.getIdentifier(attachment); let enableSelfId = prefix + "enableSelf-" + identifier + "-menuitem"; let disableSelfId = prefix + "disableSelf-" + identifier + "-menuitem"; document.getElementById(enableSelfId).setAttribute("hidden", "true"); document.getElementById(disableSelfId).removeAttribute("hidden"); // Update the breakpoint toggle button checked state. this._toggleBreakpointsButton.removeAttribute("checked"); // Update the checkbox state if necessary. if (!aOptions.silent) { attachment.view.checkbox.setAttribute("checked", "true"); } return this.Breakpoints.addBreakpoint(aLocation, { // No need to update the pane, since this method is invoked because // a breakpoint's view was interacted with. noPaneUpdate: true }); }, /** * Disables a breakpoint. * * @param object aLocation * @see DebuggerController.Breakpoints.addBreakpoint * @param object aOptions [optional] * Additional options or flags supported by this operation: * - silent: pass true to not update the checkbox checked state; * this is usually necessary when the checked state will * be updated automatically (e.g: on a checkbox click). * @return object * A promise that is resolved after the breakpoint is disabled, or * rejected if no breakpoint was found at the specified location. */ disableBreakpoint: function(aLocation, aOptions = {}) { let breakpointItem = this.getBreakpoint(aLocation); if (!breakpointItem) { return promise.reject(new Error("No breakpoint found.")); } // Breakpoint will now be disabled. let attachment = breakpointItem.attachment; attachment.disabled = true; // Update the corresponding menu items to reflect the disabled state. let prefix = "bp-cMenu-"; // "breakpoints context menu" let identifier = this.Breakpoints.getIdentifier(attachment); let enableSelfId = prefix + "enableSelf-" + identifier + "-menuitem"; let disableSelfId = prefix + "disableSelf-" + identifier + "-menuitem"; document.getElementById(enableSelfId).removeAttribute("hidden"); document.getElementById(disableSelfId).setAttribute("hidden", "true"); // Update the checkbox state if necessary. if (!aOptions.silent) { attachment.view.checkbox.removeAttribute("checked"); } return this.Breakpoints.removeBreakpoint(aLocation, { // No need to update this pane, since this method is invoked because // a breakpoint's view was interacted with. noPaneUpdate: true, // Mark this breakpoint as being "disabled", not completely removed. // This makes sure it will not be forgotten across target navigations. rememberDisabled: true }); }, /** * Highlights a breakpoint in this sources container. * * @param object aLocation * @see DebuggerController.Breakpoints.addBreakpoint * @param object aOptions [optional] * An object containing some of the following boolean properties: * - openPopup: tells if the expression popup should be shown. * - noEditorUpdate: tells if you want to skip editor updates. */ highlightBreakpoint: function(aLocation, aOptions = {}) { let breakpointItem = this.getBreakpoint(aLocation); if (!breakpointItem) { return; } // Breakpoint will now be selected. this._selectBreakpoint(breakpointItem); // Update the editor location if necessary. if (!aOptions.noEditorUpdate) { this.DebuggerView.setEditorLocation(aLocation.actor, aLocation.line, { noDebug: true }); } // If the breakpoint requires a new conditional expression, display // the panel to input the corresponding expression. if (aOptions.openPopup) { this._openConditionalPopup(); } else { this._hideConditionalPopup(); } }, /** * Highlight the breakpoint on the current currently focused line/column * if it exists. */ highlightBreakpointAtCursor: function() { let actor = this.selectedValue; let line = this.DebuggerView.editor.getCursor().line + 1; let location = { actor: actor, line: line }; this.highlightBreakpoint(location, { noEditorUpdate: true }); }, /** * Unhighlights the current breakpoint in this sources container. */ unhighlightBreakpoint: function() { this._hideConditionalPopup(); this._unselectBreakpoint(); }, /** * Display the message thrown on breakpoint condition */ showBreakpointConditionThrownMessage: function(aLocation, aMessage = "") { let breakpointItem = this.getBreakpoint(aLocation); if (!breakpointItem) { return; } let attachment = breakpointItem.attachment; attachment.view.container.classList.add("dbg-breakpoint-condition-thrown"); attachment.view.message.setAttribute("value", aMessage); }, /** * Update the checked/unchecked and enabled/disabled states of the buttons in * the sources toolbar based on the currently selected source's state. */ updateToolbarButtonsState: function() { const { source } = this.selectedItem.attachment; const sourceClient = gThreadClient.source(source); if (sourceClient.isBlackBoxed) { this._prettyPrintButton.setAttribute("disabled", true); this._blackBoxButton.setAttribute("checked", true); } else { this._prettyPrintButton.removeAttribute("disabled"); this._blackBoxButton.removeAttribute("checked"); } if (sourceClient.isPrettyPrinted) { this._prettyPrintButton.setAttribute("checked", true); } else { this._prettyPrintButton.removeAttribute("checked"); } }, /** * Toggle the pretty printing of the selected source. */ togglePrettyPrint: Task.async(function*() { if (this._prettyPrintButton.hasAttribute("disabled")) { return; } const resetEditor = ([{ actor }]) => { // Only set the text when the source is still selected. if (actor == this.selectedValue) { this.DebuggerView.setEditorLocation(actor, 0, { force: true }); } }; const printError = ([{ url }, error]) => { DevToolsUtils.reportException("togglePrettyPrint", error); }; this.DebuggerView.showProgressBar(); const { source } = this.selectedItem.attachment; const sourceClient = gThreadClient.source(source); const shouldPrettyPrint = !sourceClient.isPrettyPrinted; if (shouldPrettyPrint) { this._prettyPrintButton.setAttribute("checked", true); } else { this._prettyPrintButton.removeAttribute("checked"); } try { let resolution = yield this.SourceScripts.togglePrettyPrint(source); resetEditor(resolution); } catch (rejection) { printError(rejection); } this.DebuggerView.showEditor(); this.updateToolbarButtonsState(); }), /** * Toggle the black boxed state of the selected source. */ toggleBlackBoxing: Task.async(function*() { const { source } = this.selectedItem.attachment; const sourceClient = gThreadClient.source(source); const shouldBlackBox = !sourceClient.isBlackBoxed; // Be optimistic that the (un-)black boxing will succeed, so enable/disable // the pretty print button and check/uncheck the black box button immediately. // Then, once we actually get the results from the server, make sure that // it is in the correct state again by calling `updateToolbarButtonsState`. if (shouldBlackBox) { this._prettyPrintButton.setAttribute("disabled", true); this._blackBoxButton.setAttribute("checked", true); } else { this._prettyPrintButton.removeAttribute("disabled"); this._blackBoxButton.removeAttribute("checked"); } try { yield this.SourceScripts.setBlackBoxing(source, shouldBlackBox); } catch (e) { // Continue execution in this task even if blackboxing failed. } this.updateToolbarButtonsState(); }), /** * Toggles all breakpoints enabled/disabled. */ toggleBreakpoints: function() { let breakpoints = this.getAllBreakpoints(); let hasBreakpoints = breakpoints.length > 0; let hasEnabledBreakpoints = breakpoints.some(e => !e.attachment.disabled); if (hasBreakpoints && hasEnabledBreakpoints) { this._toggleBreakpointsButton.setAttribute("checked", true); this._onDisableAll(); } else { this._toggleBreakpointsButton.removeAttribute("checked"); this._onEnableAll(); } }, togglePromiseDebugger: function() { if (Prefs.promiseDebuggerEnabled) { let promisePane = this.DebuggerView._promisePane; promisePane.hidden = !promisePane.hidden; if (!this.DebuggerView._promiseDebuggerIframe) { this.DebuggerView._initializePromiseDebugger(); } } }, hidePrettyPrinting: function() { this._prettyPrintButton.style.display = 'none'; if (this._blackBoxButton.style.display === 'none') { let sep = document.querySelector('#sources-toolbar .devtools-separator'); sep.style.display = 'none'; } }, hideBlackBoxing: function() { this._blackBoxButton.style.display = 'none'; if (this._prettyPrintButton.style.display === 'none') { let sep = document.querySelector('#sources-toolbar .devtools-separator'); sep.style.display = 'none'; } }, /** * Look up a source actor id for a location. This is necessary for * backwards compatibility; otherwise we could just use the `actor` * property. Older servers don't use the same actor ids for sources * across reloads, so we resolve a url to the current actor if a url * exists. * * @param object aLocation * An object with the following properties: * - actor: the source actor id * - url: a url (might be null) */ getActorForLocation: function(aLocation) { if (aLocation.url) { for (var item of this) { let source = item.attachment.source; if (aLocation.url === source.url) { return source.actor; } } } return aLocation.actor; }, getDisplayURL: function(source) { if(!source.url) { return this.getItemByValue(source.actor).attachment.label; } return NetworkHelper.convertToUnicode(unescape(source.url)) }, /** * Marks a breakpoint as selected in this sources container. * * @param object aItem * The breakpoint item to select. */ _selectBreakpoint: function(aItem) { if (this._selectedBreakpointItem == aItem) { return; } this._unselectBreakpoint(); this._selectedBreakpointItem = aItem; this._selectedBreakpointItem.target.classList.add("selected"); // Ensure the currently selected breakpoint is visible. this.widget.ensureElementIsVisible(aItem.target); }, /** * Marks the current breakpoint as unselected in this sources container. */ _unselectBreakpoint: function() { if (!this._selectedBreakpointItem) { return; } this._selectedBreakpointItem.target.classList.remove("selected"); this._selectedBreakpointItem = null; }, /** * Opens a conditional breakpoint's expression input popup. */ _openConditionalPopup: function() { let breakpointItem = this._selectedBreakpointItem; let attachment = breakpointItem.attachment; // Check if this is an enabled conditional breakpoint, and if so, // retrieve the current conditional epression. let breakpointPromise = this.Breakpoints._getAdded(attachment); if (breakpointPromise) { breakpointPromise.then(aBreakpointClient => { let isConditionalBreakpoint = aBreakpointClient.hasCondition(); let condition = aBreakpointClient.getCondition(); doOpen.call(this, isConditionalBreakpoint ? condition : "") }); } else { doOpen.call(this, "") } function doOpen(aConditionalExpression) { // Update the conditional expression textbox. If no expression was // previously set, revert to using an empty string by default. this._cbTextbox.value = aConditionalExpression; // Show the conditional expression panel. The popup arrow should be pointing // at the line number node in the breakpoint item view. this._cbPanel.hidden = false; this._cbPanel.openPopup(breakpointItem.attachment.view.lineNumber, BREAKPOINT_CONDITIONAL_POPUP_POSITION, BREAKPOINT_CONDITIONAL_POPUP_OFFSET_X, BREAKPOINT_CONDITIONAL_POPUP_OFFSET_Y); } }, /** * Hides a conditional breakpoint's expression input popup. */ _hideConditionalPopup: function() { this._cbPanel.hidden = true; // Sometimes this._cbPanel doesn't have hidePopup method which doesn't // break anything but simply outputs an exception to the console. if (this._cbPanel.hidePopup) { this._cbPanel.hidePopup(); } }, /** * Customization function for creating a breakpoint item's UI. * * @param object aOptions * A couple of options or flags supported by this operation: * - location: the breakpoint's source location and line number * - disabled: the breakpoint's disabled state, boolean * - text: the breakpoint's line text to be displayed * - message: thrown string when the breakpoint condition throws * @return object * An object containing the breakpoint container, checkbox, * line number and line text nodes. */ _createBreakpointView: function(aOptions) { let { location, disabled, text, message } = aOptions; let identifier = this.Breakpoints.getIdentifier(location); let checkbox = document.createElement("checkbox"); checkbox.setAttribute("checked", !disabled); checkbox.className = "dbg-breakpoint-checkbox"; let lineNumberNode = document.createElement("label"); lineNumberNode.className = "plain dbg-breakpoint-line"; lineNumberNode.setAttribute("value", location.line); let lineTextNode = document.createElement("label"); lineTextNode.className = "plain dbg-breakpoint-text"; lineTextNode.setAttribute("value", text); lineTextNode.setAttribute("crop", "end"); lineTextNode.setAttribute("flex", "1"); let tooltip = text ? text.substr(0, BREAKPOINT_LINE_TOOLTIP_MAX_LENGTH) : ""; lineTextNode.setAttribute("tooltiptext", tooltip); let thrownNode = document.createElement("label"); thrownNode.className = "plain dbg-breakpoint-condition-thrown-message dbg-breakpoint-text"; thrownNode.setAttribute("value", message); thrownNode.setAttribute("crop", "end"); thrownNode.setAttribute("flex", "1"); let bpLineContainer = document.createElement("hbox"); bpLineContainer.className = "plain dbg-breakpoint-line-container"; bpLineContainer.setAttribute("flex", "1"); bpLineContainer.appendChild(lineNumberNode); bpLineContainer.appendChild(lineTextNode); let bpDetailContainer = document.createElement("vbox"); bpDetailContainer.className = "plain dbg-breakpoint-detail-container"; bpDetailContainer.setAttribute("flex", "1"); bpDetailContainer.appendChild(bpLineContainer); bpDetailContainer.appendChild(thrownNode); let container = document.createElement("hbox"); container.id = "breakpoint-" + identifier; container.className = "dbg-breakpoint side-menu-widget-item-other"; container.classList.add("devtools-monospace"); container.setAttribute("align", "center"); container.setAttribute("flex", "1"); container.addEventListener("click", this._onBreakpointClick, false); checkbox.addEventListener("click", this._onBreakpointCheckboxClick, false); container.appendChild(checkbox); container.appendChild(bpDetailContainer); return { container: container, checkbox: checkbox, lineNumber: lineNumberNode, lineText: lineTextNode, message: thrownNode }; }, /** * Creates a context menu for a breakpoint element. * * @param object aOptions * A couple of options or flags supported by this operation: * - location: the breakpoint's source location and line number * - disabled: the breakpoint's disabled state, boolean * @return object * An object containing the breakpoint commandset and menu popup ids. */ _createContextMenu: function(aOptions) { let { location, disabled } = aOptions; let identifier = this.Breakpoints.getIdentifier(location); let commandset = document.createElement("commandset"); let menupopup = document.createElement("menupopup"); commandset.id = "bp-cSet-" + identifier; menupopup.id = "bp-mPop-" + identifier; createMenuItem.call(this, "enableSelf", !disabled); createMenuItem.call(this, "disableSelf", disabled); createMenuItem.call(this, "deleteSelf"); createMenuSeparator(); createMenuItem.call(this, "setConditional"); createMenuSeparator(); createMenuItem.call(this, "enableOthers"); createMenuItem.call(this, "disableOthers"); createMenuItem.call(this, "deleteOthers"); createMenuSeparator(); createMenuItem.call(this, "enableAll"); createMenuItem.call(this, "disableAll"); createMenuSeparator(); createMenuItem.call(this, "deleteAll"); this._popupset.appendChild(menupopup); this._commandset.appendChild(commandset); return { commandsetId: commandset.id, menupopupId: menupopup.id }; /** * Creates a menu item specified by a name with the appropriate attributes * (label and handler). * * @param string aName * A global identifier for the menu item. * @param boolean aHiddenFlag * True if this menuitem should be hidden. */ function createMenuItem(aName, aHiddenFlag) { let menuitem = document.createElement("menuitem"); let command = document.createElement("command"); let prefix = "bp-cMenu-"; // "breakpoints context menu" let commandId = prefix + aName + "-" + identifier + "-command"; let menuitemId = prefix + aName + "-" + identifier + "-menuitem"; let label = L10N.getStr("breakpointMenuItem." + aName); let func = "_on" + aName.charAt(0).toUpperCase() + aName.slice(1); command.id = commandId; command.setAttribute("label", label); command.addEventListener("command", () => this[func](location), false); menuitem.id = menuitemId; menuitem.setAttribute("command", commandId); aHiddenFlag && menuitem.setAttribute("hidden", "true"); commandset.appendChild(command); menupopup.appendChild(menuitem); } /** * Creates a simple menu separator element and appends it to the current * menupopup hierarchy. */ function createMenuSeparator() { let menuseparator = document.createElement("menuseparator"); menupopup.appendChild(menuseparator); } }, /** * Copy the source url from the currently selected item. */ _onCopyUrlCommand: function() { let selected = this.selectedItem && this.selectedItem.attachment; if (!selected) { return; } clipboardHelper.copyString(selected.source.url); }, /** * Opens selected item source in a new tab. */ _onNewTabCommand: function() { let win = Services.wm.getMostRecentWindow("navigator:browser"); let selected = this.selectedItem.attachment; win.openUILinkIn(selected.source.url, "tab", { relatedToCurrent: true }); }, /** * Function called each time a breakpoint item is removed. * * @param object aItem * The corresponding item. */ _onBreakpointRemoved: function(aItem) { dumpn("Finalizing breakpoint item: " + aItem.stringify()); // Destroy the context menu for the breakpoint. let contextMenu = aItem.attachment.popup; document.getElementById(contextMenu.commandsetId).remove(); document.getElementById(contextMenu.menupopupId).remove(); // Clear the breakpoint selection. if (this._selectedBreakpointItem == aItem) { this._selectedBreakpointItem = null; } }, /** * The load listener for the source editor. */ _onEditorLoad: function(aName, aEditor) { aEditor.on("cursorActivity", this._onEditorCursorActivity); }, /** * The unload listener for the source editor. */ _onEditorUnload: function(aName, aEditor) { aEditor.off("cursorActivity", this._onEditorCursorActivity); }, /** * The selection listener for the source editor. */ _onEditorCursorActivity: function(e) { let editor = this.DebuggerView.editor; let start = editor.getCursor("start").line + 1; let end = editor.getCursor().line + 1; let actor = this.selectedValue; let location = { actor: actor, line: start }; if (this.getBreakpoint(location) && start == end) { this.highlightBreakpoint(location, { noEditorUpdate: true }); } else { this.unhighlightBreakpoint(); } }, /** * The select listener for the sources container. */ _onSourceSelect: Task.async(function*({ detail: sourceItem }) { if (!sourceItem) { return; } const { source } = sourceItem.attachment; const sourceClient = gThreadClient.source(source); // The container is not empty and an actual item was selected. this.DebuggerView.setEditorLocation(sourceItem.value); // Attempt to automatically pretty print minified source code. if (Prefs.autoPrettyPrint && !sourceClient.isPrettyPrinted) { let isMinified = yield SourceUtils.isMinified(sourceClient); if (isMinified) { this.togglePrettyPrint(); } } // Set window title. No need to split the url by " -> " here, because it was // already sanitized when the source was added. document.title = L10N.getFormatStr("DebuggerWindowScriptTitle", sourceItem.attachment.source.url); this.DebuggerView.maybeShowBlackBoxMessage(); this.updateToolbarButtonsState(); }), /** * The click listener for the "stop black boxing" button. */ _onStopBlackBoxing: Task.async(function*() { const { source } = this.selectedItem.attachment; try { yield this.SourceScripts.setBlackBoxing(source, false); } catch (e) { // Continue execution in this task even if blackboxing failed. } this.updateToolbarButtonsState(); }), /** * The click listener for a breakpoint container. */ _onBreakpointClick: function(e) { let sourceItem = this.getItemForElement(e.target); let breakpointItem = this.getItemForElement.call(sourceItem, e.target); let attachment = breakpointItem.attachment; // Check if this is an enabled conditional breakpoint. let breakpointPromise = this.Breakpoints._getAdded(attachment); if (breakpointPromise) { breakpointPromise.then(aBreakpointClient => { doHighlight.call(this, aBreakpointClient.hasCondition()); }); } else { doHighlight.call(this, false); } function doHighlight(aConditionalBreakpointFlag) { // Highlight the breakpoint in this pane and in the editor. this.highlightBreakpoint(attachment, { // Don't show the conditional expression popup if this is not a // conditional breakpoint, or the right mouse button was pressed (to // avoid clashing the popup with the context menu). openPopup: aConditionalBreakpointFlag && e.button == 0 }); } }, /** * The click listener for a breakpoint checkbox. */ _onBreakpointCheckboxClick: function(e) { let sourceItem = this.getItemForElement(e.target); let breakpointItem = this.getItemForElement.call(sourceItem, e.target); let attachment = breakpointItem.attachment; // Toggle the breakpoint enabled or disabled. this[attachment.disabled ? "enableBreakpoint" : "disableBreakpoint"](attachment, { // Do this silently (don't update the checkbox checked state), since // this listener is triggered because a checkbox was already clicked. silent: true }); // Don't update the editor location (avoid propagating into _onBreakpointClick). e.preventDefault(); e.stopPropagation(); }, /** * The popup showing listener for the breakpoints conditional expression panel. */ _onConditionalPopupShowing: function() { this._conditionalPopupVisible = true; // Used in tests. window.emit(EVENTS.CONDITIONAL_BREAKPOINT_POPUP_SHOWING); }, /** * The popup shown listener for the breakpoints conditional expression panel. */ _onConditionalPopupShown: function() { this._cbTextbox.focus(); this._cbTextbox.select(); }, /** * The popup hiding listener for the breakpoints conditional expression panel. */ _onConditionalPopupHiding: Task.async(function*() { this._conditionalPopupVisible = false; // Used in tests. let breakpointItem = this._selectedBreakpointItem; let attachment = breakpointItem.attachment; // Check if this is an enabled conditional breakpoint, and if so, // save the current conditional expression. let breakpointPromise = this.Breakpoints._getAdded(attachment); if (breakpointPromise) { let { location } = yield breakpointPromise; let condition = this._cbTextbox.value; yield this.Breakpoints.updateCondition(location, condition); } window.emit(EVENTS.CONDITIONAL_BREAKPOINT_POPUP_HIDING); }), /** * The keypress listener for the breakpoints conditional expression textbox. */ _onConditionalTextboxKeyPress: function(e) { if (e.keyCode == e.DOM_VK_RETURN) { this._hideConditionalPopup(); } }, /** * Called when the add breakpoint key sequence was pressed. */ _onCmdAddBreakpoint: function(e) { let actor = this.selectedValue; let line = (e && e.sourceEvent.target.tagName == 'menuitem' ? this.DebuggerView.clickedLine + 1 : this.DebuggerView.editor.getCursor().line + 1); let location = { actor, line }; let breakpointItem = this.getBreakpoint(location); // If a breakpoint already existed, remove it now. if (breakpointItem) { this.Breakpoints.removeBreakpoint(location); } // No breakpoint existed at the required location, add one now. else { this.Breakpoints.addBreakpoint(location); } }, /** * Called when the add conditional breakpoint key sequence was pressed. */ _onCmdAddConditionalBreakpoint: function(e) { let actor = this.selectedValue; let line = (e && e.sourceEvent.target.tagName == 'menuitem' ? this.DebuggerView.clickedLine + 1 : this.DebuggerView.editor.getCursor().line + 1); let location = { actor, line }; let breakpointItem = this.getBreakpoint(location); // If a breakpoint already existed or wasn't a conditional, morph it now. if (breakpointItem) { this.highlightBreakpoint(location, { openPopup: true }); } // No breakpoint existed at the required location, add one now. else { this.Breakpoints.addBreakpoint(location, { openPopup: true }); } }, /** * Function invoked on the "setConditional" menuitem command. * * @param object aLocation * @see DebuggerController.Breakpoints.addBreakpoint */ _onSetConditional: function(aLocation) { // Highlight the breakpoint and show a conditional expression popup. this.highlightBreakpoint(aLocation, { openPopup: true }); }, /** * Function invoked on the "enableSelf" menuitem command. * * @param object aLocation * @see DebuggerController.Breakpoints.addBreakpoint */ _onEnableSelf: function(aLocation) { // Enable the breakpoint, in this container and the controller store. this.enableBreakpoint(aLocation); }, /** * Function invoked on the "disableSelf" menuitem command. * * @param object aLocation * @see DebuggerController.Breakpoints.addBreakpoint */ _onDisableSelf: function(aLocation) { // Disable the breakpoint, in this container and the controller store. this.disableBreakpoint(aLocation); }, /** * Function invoked on the "deleteSelf" menuitem command. * * @param object aLocation * @see DebuggerController.Breakpoints.addBreakpoint */ _onDeleteSelf: function(aLocation) { // Remove the breakpoint, from this container and the controller store. this.removeBreakpoint(aLocation); this.Breakpoints.removeBreakpoint(aLocation); }, /** * Function invoked on the "enableOthers" menuitem command. * * @param object aLocation * @see DebuggerController.Breakpoints.addBreakpoint */ _onEnableOthers: function(aLocation) { let enableOthers = aCallback => { let other = this.getOtherBreakpoints(aLocation); let outstanding = other.map(e => this.enableBreakpoint(e.attachment)); promise.all(outstanding).then(aCallback); } // Breakpoints can only be set while the debuggee is paused. To avoid // an avalanche of pause/resume interrupts of the main thread, simply // pause it beforehand if it's not already. if (gThreadClient.state != "paused") { gThreadClient.interrupt(() => enableOthers(() => gThreadClient.resume())); } else { enableOthers(); } }, /** * Function invoked on the "disableOthers" menuitem command. * * @param object aLocation * @see DebuggerController.Breakpoints.addBreakpoint */ _onDisableOthers: function(aLocation) { let other = this.getOtherBreakpoints(aLocation); other.forEach(e => this._onDisableSelf(e.attachment)); }, /** * Function invoked on the "deleteOthers" menuitem command. * * @param object aLocation * @see DebuggerController.Breakpoints.addBreakpoint */ _onDeleteOthers: function(aLocation) { let other = this.getOtherBreakpoints(aLocation); other.forEach(e => this._onDeleteSelf(e.attachment)); }, /** * Function invoked on the "enableAll" menuitem command. */ _onEnableAll: function() { this._onEnableOthers(undefined); }, /** * Function invoked on the "disableAll" menuitem command. */ _onDisableAll: function() { this._onDisableOthers(undefined); }, /** * Function invoked on the "deleteAll" menuitem command. */ _onDeleteAll: function() { this._onDeleteOthers(undefined); }, _commandset: null, _popupset: null, _cmPopup: null, _cbPanel: null, _cbTextbox: null, _selectedBreakpointItem: null, _conditionalPopupVisible: false }); DebuggerView.Sources = new SourcesView(DebuggerController, DebuggerView);