1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/browser/devtools/debugger/debugger-panes.js Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,3315 @@ 1.4 +/* -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ 1.5 +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ 1.6 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.7 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.8 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.9 + 1.10 +"use strict"; 1.11 + 1.12 +XPCOMUtils.defineLazyModuleGetter(this, "Task", 1.13 + "resource://gre/modules/Task.jsm"); 1.14 + 1.15 +// Used to detect minification for automatic pretty printing 1.16 +const SAMPLE_SIZE = 50; // no of lines 1.17 +const INDENT_COUNT_THRESHOLD = 5; // percentage 1.18 +const CHARACTER_LIMIT = 250; // line character limit 1.19 + 1.20 +// Maps known URLs to friendly source group names 1.21 +const KNOWN_SOURCE_GROUPS = { 1.22 + "Add-on SDK": "resource://gre/modules/commonjs/", 1.23 +}; 1.24 + 1.25 +/** 1.26 + * Functions handling the sources UI. 1.27 + */ 1.28 +function SourcesView() { 1.29 + dumpn("SourcesView was instantiated"); 1.30 + 1.31 + this.togglePrettyPrint = this.togglePrettyPrint.bind(this); 1.32 + this.toggleBlackBoxing = this.toggleBlackBoxing.bind(this); 1.33 + this.toggleBreakpoints = this.toggleBreakpoints.bind(this); 1.34 + 1.35 + this._onEditorLoad = this._onEditorLoad.bind(this); 1.36 + this._onEditorUnload = this._onEditorUnload.bind(this); 1.37 + this._onEditorCursorActivity = this._onEditorCursorActivity.bind(this); 1.38 + this._onSourceSelect = this._onSourceSelect.bind(this); 1.39 + this._onStopBlackBoxing = this._onStopBlackBoxing.bind(this); 1.40 + this._onBreakpointRemoved = this._onBreakpointRemoved.bind(this); 1.41 + this._onBreakpointClick = this._onBreakpointClick.bind(this); 1.42 + this._onBreakpointCheckboxClick = this._onBreakpointCheckboxClick.bind(this); 1.43 + this._onConditionalPopupShowing = this._onConditionalPopupShowing.bind(this); 1.44 + this._onConditionalPopupShown = this._onConditionalPopupShown.bind(this); 1.45 + this._onConditionalPopupHiding = this._onConditionalPopupHiding.bind(this); 1.46 + this._onConditionalTextboxKeyPress = this._onConditionalTextboxKeyPress.bind(this); 1.47 + 1.48 + this.updateToolbarButtonsState = this.updateToolbarButtonsState.bind(this); 1.49 +} 1.50 + 1.51 +SourcesView.prototype = Heritage.extend(WidgetMethods, { 1.52 + /** 1.53 + * Initialization function, called when the debugger is started. 1.54 + */ 1.55 + initialize: function() { 1.56 + dumpn("Initializing the SourcesView"); 1.57 + 1.58 + this.widget = new SideMenuWidget(document.getElementById("sources"), { 1.59 + showArrows: true 1.60 + }); 1.61 + 1.62 + // Sort known source groups towards the end of the list 1.63 + this.widget.groupSortPredicate = function(a, b) { 1.64 + if ((a in KNOWN_SOURCE_GROUPS) == (b in KNOWN_SOURCE_GROUPS)) { 1.65 + return a.localeCompare(b); 1.66 + } 1.67 + 1.68 + return (a in KNOWN_SOURCE_GROUPS) ? 1 : -1; 1.69 + }; 1.70 + 1.71 + this.emptyText = L10N.getStr("noSourcesText"); 1.72 + this._blackBoxCheckboxTooltip = L10N.getStr("blackBoxCheckboxTooltip"); 1.73 + 1.74 + this._commandset = document.getElementById("debuggerCommands"); 1.75 + this._popupset = document.getElementById("debuggerPopupset"); 1.76 + this._cmPopup = document.getElementById("sourceEditorContextMenu"); 1.77 + this._cbPanel = document.getElementById("conditional-breakpoint-panel"); 1.78 + this._cbTextbox = document.getElementById("conditional-breakpoint-panel-textbox"); 1.79 + this._blackBoxButton = document.getElementById("black-box"); 1.80 + this._stopBlackBoxButton = document.getElementById("black-boxed-message-button"); 1.81 + this._prettyPrintButton = document.getElementById("pretty-print"); 1.82 + this._toggleBreakpointsButton = document.getElementById("toggle-breakpoints"); 1.83 + 1.84 + if (Prefs.prettyPrintEnabled) { 1.85 + this._prettyPrintButton.removeAttribute("hidden"); 1.86 + } 1.87 + 1.88 + window.on(EVENTS.EDITOR_LOADED, this._onEditorLoad, false); 1.89 + window.on(EVENTS.EDITOR_UNLOADED, this._onEditorUnload, false); 1.90 + this.widget.addEventListener("select", this._onSourceSelect, false); 1.91 + this._stopBlackBoxButton.addEventListener("click", this._onStopBlackBoxing, false); 1.92 + this._cbPanel.addEventListener("popupshowing", this._onConditionalPopupShowing, false); 1.93 + this._cbPanel.addEventListener("popupshown", this._onConditionalPopupShown, false); 1.94 + this._cbPanel.addEventListener("popuphiding", this._onConditionalPopupHiding, false); 1.95 + this._cbTextbox.addEventListener("keypress", this._onConditionalTextboxKeyPress, false); 1.96 + 1.97 + this.autoFocusOnSelection = false; 1.98 + 1.99 + // Sort the contents by the displayed label. 1.100 + this.sortContents((aFirst, aSecond) => { 1.101 + return +(aFirst.attachment.label.toLowerCase() > 1.102 + aSecond.attachment.label.toLowerCase()); 1.103 + }); 1.104 + }, 1.105 + 1.106 + /** 1.107 + * Destruction function, called when the debugger is closed. 1.108 + */ 1.109 + destroy: function() { 1.110 + dumpn("Destroying the SourcesView"); 1.111 + 1.112 + window.off(EVENTS.EDITOR_LOADED, this._onEditorLoad, false); 1.113 + window.off(EVENTS.EDITOR_UNLOADED, this._onEditorUnload, false); 1.114 + this.widget.removeEventListener("select", this._onSourceSelect, false); 1.115 + this._stopBlackBoxButton.removeEventListener("click", this._onStopBlackBoxing, false); 1.116 + this._cbPanel.removeEventListener("popupshowing", this._onConditionalPopupShowing, false); 1.117 + this._cbPanel.removeEventListener("popupshowing", this._onConditionalPopupShown, false); 1.118 + this._cbPanel.removeEventListener("popuphiding", this._onConditionalPopupHiding, false); 1.119 + this._cbTextbox.removeEventListener("keypress", this._onConditionalTextboxKeyPress, false); 1.120 + }, 1.121 + 1.122 + /** 1.123 + * Sets the preferred location to be selected in this sources container. 1.124 + * @param string aUrl 1.125 + */ 1.126 + set preferredSource(aUrl) { 1.127 + this._preferredValue = aUrl; 1.128 + 1.129 + // Selects the element with the specified value in this sources container, 1.130 + // if already inserted. 1.131 + if (this.containsValue(aUrl)) { 1.132 + this.selectedValue = aUrl; 1.133 + } 1.134 + }, 1.135 + 1.136 + /** 1.137 + * Adds a source to this sources container. 1.138 + * 1.139 + * @param object aSource 1.140 + * The source object coming from the active thread. 1.141 + * @param object aOptions [optional] 1.142 + * Additional options for adding the source. Supported options: 1.143 + * - staged: true to stage the item to be appended later 1.144 + */ 1.145 + addSource: function(aSource, aOptions = {}) { 1.146 + let fullUrl = aSource.url; 1.147 + let url = fullUrl.split(" -> ").pop(); 1.148 + let label = aSource.addonPath ? aSource.addonPath : SourceUtils.getSourceLabel(url); 1.149 + let group = aSource.addonID ? aSource.addonID : SourceUtils.getSourceGroup(url); 1.150 + let unicodeUrl = NetworkHelper.convertToUnicode(unescape(fullUrl)); 1.151 + 1.152 + let contents = document.createElement("label"); 1.153 + contents.className = "plain dbg-source-item"; 1.154 + contents.setAttribute("value", label); 1.155 + contents.setAttribute("crop", "start"); 1.156 + contents.setAttribute("flex", "1"); 1.157 + contents.setAttribute("tooltiptext", unicodeUrl); 1.158 + 1.159 + // Append a source item to this container. 1.160 + this.push([contents, fullUrl], { 1.161 + staged: aOptions.staged, /* stage the item to be appended later? */ 1.162 + attachment: { 1.163 + label: label, 1.164 + group: group, 1.165 + checkboxState: !aSource.isBlackBoxed, 1.166 + checkboxTooltip: this._blackBoxCheckboxTooltip, 1.167 + source: aSource 1.168 + } 1.169 + }); 1.170 + }, 1.171 + 1.172 + /** 1.173 + * Adds a breakpoint to this sources container. 1.174 + * 1.175 + * @param object aBreakpointData 1.176 + * Information about the breakpoint to be shown. 1.177 + * This object must have the following properties: 1.178 + * - location: the breakpoint's source location and line number 1.179 + * - disabled: the breakpoint's disabled state, boolean 1.180 + * - text: the breakpoint's line text to be displayed 1.181 + * @param object aOptions [optional] 1.182 + * @see DebuggerController.Breakpoints.addBreakpoint 1.183 + */ 1.184 + addBreakpoint: function(aBreakpointData, aOptions = {}) { 1.185 + let { location, disabled } = aBreakpointData; 1.186 + 1.187 + // Make sure we're not duplicating anything. If a breakpoint at the 1.188 + // specified source url and line already exists, just toggle it. 1.189 + if (this.getBreakpoint(location)) { 1.190 + this[disabled ? "disableBreakpoint" : "enableBreakpoint"](location); 1.191 + return; 1.192 + } 1.193 + 1.194 + // Get the source item to which the breakpoint should be attached. 1.195 + let sourceItem = this.getItemByValue(location.url); 1.196 + 1.197 + // Create the element node and menu popup for the breakpoint item. 1.198 + let breakpointArgs = Heritage.extend(aBreakpointData, aOptions); 1.199 + let breakpointView = this._createBreakpointView.call(this, breakpointArgs); 1.200 + let contextMenu = this._createContextMenu.call(this, breakpointArgs); 1.201 + 1.202 + // Append a breakpoint child item to the corresponding source item. 1.203 + sourceItem.append(breakpointView.container, { 1.204 + attachment: Heritage.extend(breakpointArgs, { 1.205 + url: location.url, 1.206 + line: location.line, 1.207 + view: breakpointView, 1.208 + popup: contextMenu 1.209 + }), 1.210 + attributes: [ 1.211 + ["contextmenu", contextMenu.menupopupId] 1.212 + ], 1.213 + // Make sure that when the breakpoint item is removed, the corresponding 1.214 + // menupopup and commandset are also destroyed. 1.215 + finalize: this._onBreakpointRemoved 1.216 + }); 1.217 + 1.218 + // Highlight the newly appended breakpoint child item if necessary. 1.219 + if (aOptions.openPopup || !aOptions.noEditorUpdate) { 1.220 + this.highlightBreakpoint(location, aOptions); 1.221 + } 1.222 + }, 1.223 + 1.224 + /** 1.225 + * Removes a breakpoint from this sources container. 1.226 + * It does not also remove the breakpoint from the controller. Be careful. 1.227 + * 1.228 + * @param object aLocation 1.229 + * @see DebuggerController.Breakpoints.addBreakpoint 1.230 + */ 1.231 + removeBreakpoint: function(aLocation) { 1.232 + // When a parent source item is removed, all the child breakpoint items are 1.233 + // also automagically removed. 1.234 + let sourceItem = this.getItemByValue(aLocation.url); 1.235 + if (!sourceItem) { 1.236 + return; 1.237 + } 1.238 + let breakpointItem = this.getBreakpoint(aLocation); 1.239 + if (!breakpointItem) { 1.240 + return; 1.241 + } 1.242 + 1.243 + // Clear the breakpoint view. 1.244 + sourceItem.remove(breakpointItem); 1.245 + }, 1.246 + 1.247 + /** 1.248 + * Returns the breakpoint at the specified source url and line. 1.249 + * 1.250 + * @param object aLocation 1.251 + * @see DebuggerController.Breakpoints.addBreakpoint 1.252 + * @return object 1.253 + * The corresponding breakpoint item if found, null otherwise. 1.254 + */ 1.255 + getBreakpoint: function(aLocation) { 1.256 + return this.getItemForPredicate(aItem => 1.257 + aItem.attachment.url == aLocation.url && 1.258 + aItem.attachment.line == aLocation.line); 1.259 + }, 1.260 + 1.261 + /** 1.262 + * Returns all breakpoints for all sources. 1.263 + * 1.264 + * @return array 1.265 + * The breakpoints for all sources if any, an empty array otherwise. 1.266 + */ 1.267 + getAllBreakpoints: function(aStore = []) { 1.268 + return this.getOtherBreakpoints(undefined, aStore); 1.269 + }, 1.270 + 1.271 + /** 1.272 + * Returns all breakpoints which are not at the specified source url and line. 1.273 + * 1.274 + * @param object aLocation [optional] 1.275 + * @see DebuggerController.Breakpoints.addBreakpoint 1.276 + * @param array aStore [optional] 1.277 + * A list in which to store the corresponding breakpoints. 1.278 + * @return array 1.279 + * The corresponding breakpoints if found, an empty array otherwise. 1.280 + */ 1.281 + getOtherBreakpoints: function(aLocation = {}, aStore = []) { 1.282 + for (let source of this) { 1.283 + for (let breakpointItem of source) { 1.284 + let { url, line } = breakpointItem.attachment; 1.285 + if (url != aLocation.url || line != aLocation.line) { 1.286 + aStore.push(breakpointItem); 1.287 + } 1.288 + } 1.289 + } 1.290 + return aStore; 1.291 + }, 1.292 + 1.293 + /** 1.294 + * Enables a breakpoint. 1.295 + * 1.296 + * @param object aLocation 1.297 + * @see DebuggerController.Breakpoints.addBreakpoint 1.298 + * @param object aOptions [optional] 1.299 + * Additional options or flags supported by this operation: 1.300 + * - silent: pass true to not update the checkbox checked state; 1.301 + * this is usually necessary when the checked state will 1.302 + * be updated automatically (e.g: on a checkbox click). 1.303 + * @return object 1.304 + * A promise that is resolved after the breakpoint is enabled, or 1.305 + * rejected if no breakpoint was found at the specified location. 1.306 + */ 1.307 + enableBreakpoint: function(aLocation, aOptions = {}) { 1.308 + let breakpointItem = this.getBreakpoint(aLocation); 1.309 + if (!breakpointItem) { 1.310 + return promise.reject(new Error("No breakpoint found.")); 1.311 + } 1.312 + 1.313 + // Breakpoint will now be enabled. 1.314 + let attachment = breakpointItem.attachment; 1.315 + attachment.disabled = false; 1.316 + 1.317 + // Update the corresponding menu items to reflect the enabled state. 1.318 + let prefix = "bp-cMenu-"; // "breakpoints context menu" 1.319 + let identifier = DebuggerController.Breakpoints.getIdentifier(attachment); 1.320 + let enableSelfId = prefix + "enableSelf-" + identifier + "-menuitem"; 1.321 + let disableSelfId = prefix + "disableSelf-" + identifier + "-menuitem"; 1.322 + document.getElementById(enableSelfId).setAttribute("hidden", "true"); 1.323 + document.getElementById(disableSelfId).removeAttribute("hidden"); 1.324 + 1.325 + // Update the breakpoint toggle button checked state. 1.326 + this._toggleBreakpointsButton.removeAttribute("checked"); 1.327 + 1.328 + // Update the checkbox state if necessary. 1.329 + if (!aOptions.silent) { 1.330 + attachment.view.checkbox.setAttribute("checked", "true"); 1.331 + } 1.332 + 1.333 + return DebuggerController.Breakpoints.addBreakpoint(aLocation, { 1.334 + // No need to update the pane, since this method is invoked because 1.335 + // a breakpoint's view was interacted with. 1.336 + noPaneUpdate: true 1.337 + }); 1.338 + }, 1.339 + 1.340 + /** 1.341 + * Disables a breakpoint. 1.342 + * 1.343 + * @param object aLocation 1.344 + * @see DebuggerController.Breakpoints.addBreakpoint 1.345 + * @param object aOptions [optional] 1.346 + * Additional options or flags supported by this operation: 1.347 + * - silent: pass true to not update the checkbox checked state; 1.348 + * this is usually necessary when the checked state will 1.349 + * be updated automatically (e.g: on a checkbox click). 1.350 + * @return object 1.351 + * A promise that is resolved after the breakpoint is disabled, or 1.352 + * rejected if no breakpoint was found at the specified location. 1.353 + */ 1.354 + disableBreakpoint: function(aLocation, aOptions = {}) { 1.355 + let breakpointItem = this.getBreakpoint(aLocation); 1.356 + if (!breakpointItem) { 1.357 + return promise.reject(new Error("No breakpoint found.")); 1.358 + } 1.359 + 1.360 + // Breakpoint will now be disabled. 1.361 + let attachment = breakpointItem.attachment; 1.362 + attachment.disabled = true; 1.363 + 1.364 + // Update the corresponding menu items to reflect the disabled state. 1.365 + let prefix = "bp-cMenu-"; // "breakpoints context menu" 1.366 + let identifier = DebuggerController.Breakpoints.getIdentifier(attachment); 1.367 + let enableSelfId = prefix + "enableSelf-" + identifier + "-menuitem"; 1.368 + let disableSelfId = prefix + "disableSelf-" + identifier + "-menuitem"; 1.369 + document.getElementById(enableSelfId).removeAttribute("hidden"); 1.370 + document.getElementById(disableSelfId).setAttribute("hidden", "true"); 1.371 + 1.372 + // Update the checkbox state if necessary. 1.373 + if (!aOptions.silent) { 1.374 + attachment.view.checkbox.removeAttribute("checked"); 1.375 + } 1.376 + 1.377 + return DebuggerController.Breakpoints.removeBreakpoint(aLocation, { 1.378 + // No need to update this pane, since this method is invoked because 1.379 + // a breakpoint's view was interacted with. 1.380 + noPaneUpdate: true, 1.381 + // Mark this breakpoint as being "disabled", not completely removed. 1.382 + // This makes sure it will not be forgotten across target navigations. 1.383 + rememberDisabled: true 1.384 + }); 1.385 + }, 1.386 + 1.387 + /** 1.388 + * Highlights a breakpoint in this sources container. 1.389 + * 1.390 + * @param object aLocation 1.391 + * @see DebuggerController.Breakpoints.addBreakpoint 1.392 + * @param object aOptions [optional] 1.393 + * An object containing some of the following boolean properties: 1.394 + * - openPopup: tells if the expression popup should be shown. 1.395 + * - noEditorUpdate: tells if you want to skip editor updates. 1.396 + */ 1.397 + highlightBreakpoint: function(aLocation, aOptions = {}) { 1.398 + let breakpointItem = this.getBreakpoint(aLocation); 1.399 + if (!breakpointItem) { 1.400 + return; 1.401 + } 1.402 + 1.403 + // Breakpoint will now be selected. 1.404 + this._selectBreakpoint(breakpointItem); 1.405 + 1.406 + // Update the editor location if necessary. 1.407 + if (!aOptions.noEditorUpdate) { 1.408 + DebuggerView.setEditorLocation(aLocation.url, aLocation.line, { noDebug: true }); 1.409 + } 1.410 + 1.411 + // If the breakpoint requires a new conditional expression, display 1.412 + // the panel to input the corresponding expression. 1.413 + if (aOptions.openPopup) { 1.414 + this._openConditionalPopup(); 1.415 + } else { 1.416 + this._hideConditionalPopup(); 1.417 + } 1.418 + }, 1.419 + 1.420 + /** 1.421 + * Unhighlights the current breakpoint in this sources container. 1.422 + */ 1.423 + unhighlightBreakpoint: function() { 1.424 + this._unselectBreakpoint(); 1.425 + this._hideConditionalPopup(); 1.426 + }, 1.427 + 1.428 + /** 1.429 + * Update the checked/unchecked and enabled/disabled states of the buttons in 1.430 + * the sources toolbar based on the currently selected source's state. 1.431 + */ 1.432 + updateToolbarButtonsState: function() { 1.433 + const { source } = this.selectedItem.attachment; 1.434 + const sourceClient = gThreadClient.source(source); 1.435 + 1.436 + if (sourceClient.isBlackBoxed) { 1.437 + this._prettyPrintButton.setAttribute("disabled", true); 1.438 + this._blackBoxButton.setAttribute("checked", true); 1.439 + } else { 1.440 + this._prettyPrintButton.removeAttribute("disabled"); 1.441 + this._blackBoxButton.removeAttribute("checked"); 1.442 + } 1.443 + 1.444 + if (sourceClient.isPrettyPrinted) { 1.445 + this._prettyPrintButton.setAttribute("checked", true); 1.446 + } else { 1.447 + this._prettyPrintButton.removeAttribute("checked"); 1.448 + } 1.449 + }, 1.450 + 1.451 + /** 1.452 + * Toggle the pretty printing of the selected source. 1.453 + */ 1.454 + togglePrettyPrint: function() { 1.455 + if (this._prettyPrintButton.hasAttribute("disabled")) { 1.456 + return; 1.457 + } 1.458 + 1.459 + const resetEditor = ([{ url }]) => { 1.460 + // Only set the text when the source is still selected. 1.461 + if (url == this.selectedValue) { 1.462 + DebuggerView.setEditorLocation(url, 0, { force: true }); 1.463 + } 1.464 + }; 1.465 + 1.466 + const printError = ([{ url }, error]) => { 1.467 + DevToolsUtils.reportException("togglePrettyPrint", error); 1.468 + }; 1.469 + 1.470 + DebuggerView.showProgressBar(); 1.471 + const { source } = this.selectedItem.attachment; 1.472 + const sourceClient = gThreadClient.source(source); 1.473 + const shouldPrettyPrint = !sourceClient.isPrettyPrinted; 1.474 + 1.475 + if (shouldPrettyPrint) { 1.476 + this._prettyPrintButton.setAttribute("checked", true); 1.477 + } else { 1.478 + this._prettyPrintButton.removeAttribute("checked"); 1.479 + } 1.480 + 1.481 + DebuggerController.SourceScripts.togglePrettyPrint(source) 1.482 + .then(resetEditor, printError) 1.483 + .then(DebuggerView.showEditor) 1.484 + .then(this.updateToolbarButtonsState); 1.485 + }, 1.486 + 1.487 + /** 1.488 + * Toggle the black boxed state of the selected source. 1.489 + */ 1.490 + toggleBlackBoxing: function() { 1.491 + const { source } = this.selectedItem.attachment; 1.492 + const sourceClient = gThreadClient.source(source); 1.493 + const shouldBlackBox = !sourceClient.isBlackBoxed; 1.494 + 1.495 + // Be optimistic that the (un-)black boxing will succeed, so enable/disable 1.496 + // the pretty print button and check/uncheck the black box button 1.497 + // immediately. Then, once we actually get the results from the server, make 1.498 + // sure that it is in the correct state again by calling 1.499 + // `updateToolbarButtonsState`. 1.500 + 1.501 + if (shouldBlackBox) { 1.502 + this._prettyPrintButton.setAttribute("disabled", true); 1.503 + this._blackBoxButton.setAttribute("checked", true); 1.504 + } else { 1.505 + this._prettyPrintButton.removeAttribute("disabled"); 1.506 + this._blackBoxButton.removeAttribute("checked"); 1.507 + } 1.508 + 1.509 + DebuggerController.SourceScripts.setBlackBoxing(source, shouldBlackBox) 1.510 + .then(this.updateToolbarButtonsState, 1.511 + this.updateToolbarButtonsState); 1.512 + }, 1.513 + 1.514 + /** 1.515 + * Toggles all breakpoints enabled/disabled. 1.516 + */ 1.517 + toggleBreakpoints: function() { 1.518 + let breakpoints = this.getAllBreakpoints(); 1.519 + let hasBreakpoints = breakpoints.length > 0; 1.520 + let hasEnabledBreakpoints = breakpoints.some(e => !e.attachment.disabled); 1.521 + 1.522 + if (hasBreakpoints && hasEnabledBreakpoints) { 1.523 + this._toggleBreakpointsButton.setAttribute("checked", true); 1.524 + this._onDisableAll(); 1.525 + } else { 1.526 + this._toggleBreakpointsButton.removeAttribute("checked"); 1.527 + this._onEnableAll(); 1.528 + } 1.529 + }, 1.530 + 1.531 + /** 1.532 + * Marks a breakpoint as selected in this sources container. 1.533 + * 1.534 + * @param object aItem 1.535 + * The breakpoint item to select. 1.536 + */ 1.537 + _selectBreakpoint: function(aItem) { 1.538 + if (this._selectedBreakpointItem == aItem) { 1.539 + return; 1.540 + } 1.541 + this._unselectBreakpoint(); 1.542 + this._selectedBreakpointItem = aItem; 1.543 + this._selectedBreakpointItem.target.classList.add("selected"); 1.544 + 1.545 + // Ensure the currently selected breakpoint is visible. 1.546 + this.widget.ensureElementIsVisible(aItem.target); 1.547 + }, 1.548 + 1.549 + /** 1.550 + * Marks the current breakpoint as unselected in this sources container. 1.551 + */ 1.552 + _unselectBreakpoint: function() { 1.553 + if (!this._selectedBreakpointItem) { 1.554 + return; 1.555 + } 1.556 + this._selectedBreakpointItem.target.classList.remove("selected"); 1.557 + this._selectedBreakpointItem = null; 1.558 + }, 1.559 + 1.560 + /** 1.561 + * Opens a conditional breakpoint's expression input popup. 1.562 + */ 1.563 + _openConditionalPopup: function() { 1.564 + let breakpointItem = this._selectedBreakpointItem; 1.565 + let attachment = breakpointItem.attachment; 1.566 + // Check if this is an enabled conditional breakpoint, and if so, 1.567 + // retrieve the current conditional epression. 1.568 + let breakpointPromise = DebuggerController.Breakpoints._getAdded(attachment); 1.569 + if (breakpointPromise) { 1.570 + breakpointPromise.then(aBreakpointClient => { 1.571 + let isConditionalBreakpoint = aBreakpointClient.hasCondition(); 1.572 + let condition = aBreakpointClient.getCondition(); 1.573 + doOpen.call(this, isConditionalBreakpoint ? condition : "") 1.574 + }); 1.575 + } else { 1.576 + doOpen.call(this, "") 1.577 + } 1.578 + 1.579 + function doOpen(aConditionalExpression) { 1.580 + // Update the conditional expression textbox. If no expression was 1.581 + // previously set, revert to using an empty string by default. 1.582 + this._cbTextbox.value = aConditionalExpression; 1.583 + 1.584 + // Show the conditional expression panel. The popup arrow should be pointing 1.585 + // at the line number node in the breakpoint item view. 1.586 + this._cbPanel.hidden = false; 1.587 + this._cbPanel.openPopup(breakpointItem.attachment.view.lineNumber, 1.588 + BREAKPOINT_CONDITIONAL_POPUP_POSITION, 1.589 + BREAKPOINT_CONDITIONAL_POPUP_OFFSET_X, 1.590 + BREAKPOINT_CONDITIONAL_POPUP_OFFSET_Y); 1.591 + } 1.592 + }, 1.593 + 1.594 + /** 1.595 + * Hides a conditional breakpoint's expression input popup. 1.596 + */ 1.597 + _hideConditionalPopup: function() { 1.598 + this._cbPanel.hidden = true; 1.599 + 1.600 + // Sometimes this._cbPanel doesn't have hidePopup method which doesn't 1.601 + // break anything but simply outputs an exception to the console. 1.602 + if (this._cbPanel.hidePopup) { 1.603 + this._cbPanel.hidePopup(); 1.604 + } 1.605 + }, 1.606 + 1.607 + /** 1.608 + * Customization function for creating a breakpoint item's UI. 1.609 + * 1.610 + * @param object aOptions 1.611 + * A couple of options or flags supported by this operation: 1.612 + * - location: the breakpoint's source location and line number 1.613 + * - disabled: the breakpoint's disabled state, boolean 1.614 + * - text: the breakpoint's line text to be displayed 1.615 + * @return object 1.616 + * An object containing the breakpoint container, checkbox, 1.617 + * line number and line text nodes. 1.618 + */ 1.619 + _createBreakpointView: function(aOptions) { 1.620 + let { location, disabled, text } = aOptions; 1.621 + let identifier = DebuggerController.Breakpoints.getIdentifier(location); 1.622 + 1.623 + let checkbox = document.createElement("checkbox"); 1.624 + checkbox.setAttribute("checked", !disabled); 1.625 + checkbox.className = "dbg-breakpoint-checkbox"; 1.626 + 1.627 + let lineNumberNode = document.createElement("label"); 1.628 + lineNumberNode.className = "plain dbg-breakpoint-line"; 1.629 + lineNumberNode.setAttribute("value", location.line); 1.630 + 1.631 + let lineTextNode = document.createElement("label"); 1.632 + lineTextNode.className = "plain dbg-breakpoint-text"; 1.633 + lineTextNode.setAttribute("value", text); 1.634 + lineTextNode.setAttribute("crop", "end"); 1.635 + lineTextNode.setAttribute("flex", "1"); 1.636 + 1.637 + let tooltip = text.substr(0, BREAKPOINT_LINE_TOOLTIP_MAX_LENGTH); 1.638 + lineTextNode.setAttribute("tooltiptext", tooltip); 1.639 + 1.640 + let container = document.createElement("hbox"); 1.641 + container.id = "breakpoint-" + identifier; 1.642 + container.className = "dbg-breakpoint side-menu-widget-item-other"; 1.643 + container.classList.add("devtools-monospace"); 1.644 + container.setAttribute("align", "center"); 1.645 + container.setAttribute("flex", "1"); 1.646 + 1.647 + container.addEventListener("click", this._onBreakpointClick, false); 1.648 + checkbox.addEventListener("click", this._onBreakpointCheckboxClick, false); 1.649 + 1.650 + container.appendChild(checkbox); 1.651 + container.appendChild(lineNumberNode); 1.652 + container.appendChild(lineTextNode); 1.653 + 1.654 + return { 1.655 + container: container, 1.656 + checkbox: checkbox, 1.657 + lineNumber: lineNumberNode, 1.658 + lineText: lineTextNode 1.659 + }; 1.660 + }, 1.661 + 1.662 + /** 1.663 + * Creates a context menu for a breakpoint element. 1.664 + * 1.665 + * @param object aOptions 1.666 + * A couple of options or flags supported by this operation: 1.667 + * - location: the breakpoint's source location and line number 1.668 + * - disabled: the breakpoint's disabled state, boolean 1.669 + * @return object 1.670 + * An object containing the breakpoint commandset and menu popup ids. 1.671 + */ 1.672 + _createContextMenu: function(aOptions) { 1.673 + let { location, disabled } = aOptions; 1.674 + let identifier = DebuggerController.Breakpoints.getIdentifier(location); 1.675 + 1.676 + let commandset = document.createElement("commandset"); 1.677 + let menupopup = document.createElement("menupopup"); 1.678 + commandset.id = "bp-cSet-" + identifier; 1.679 + menupopup.id = "bp-mPop-" + identifier; 1.680 + 1.681 + createMenuItem.call(this, "enableSelf", !disabled); 1.682 + createMenuItem.call(this, "disableSelf", disabled); 1.683 + createMenuItem.call(this, "deleteSelf"); 1.684 + createMenuSeparator(); 1.685 + createMenuItem.call(this, "setConditional"); 1.686 + createMenuSeparator(); 1.687 + createMenuItem.call(this, "enableOthers"); 1.688 + createMenuItem.call(this, "disableOthers"); 1.689 + createMenuItem.call(this, "deleteOthers"); 1.690 + createMenuSeparator(); 1.691 + createMenuItem.call(this, "enableAll"); 1.692 + createMenuItem.call(this, "disableAll"); 1.693 + createMenuSeparator(); 1.694 + createMenuItem.call(this, "deleteAll"); 1.695 + 1.696 + this._popupset.appendChild(menupopup); 1.697 + this._commandset.appendChild(commandset); 1.698 + 1.699 + return { 1.700 + commandsetId: commandset.id, 1.701 + menupopupId: menupopup.id 1.702 + }; 1.703 + 1.704 + /** 1.705 + * Creates a menu item specified by a name with the appropriate attributes 1.706 + * (label and handler). 1.707 + * 1.708 + * @param string aName 1.709 + * A global identifier for the menu item. 1.710 + * @param boolean aHiddenFlag 1.711 + * True if this menuitem should be hidden. 1.712 + */ 1.713 + function createMenuItem(aName, aHiddenFlag) { 1.714 + let menuitem = document.createElement("menuitem"); 1.715 + let command = document.createElement("command"); 1.716 + 1.717 + let prefix = "bp-cMenu-"; // "breakpoints context menu" 1.718 + let commandId = prefix + aName + "-" + identifier + "-command"; 1.719 + let menuitemId = prefix + aName + "-" + identifier + "-menuitem"; 1.720 + 1.721 + let label = L10N.getStr("breakpointMenuItem." + aName); 1.722 + let func = "_on" + aName.charAt(0).toUpperCase() + aName.slice(1); 1.723 + 1.724 + command.id = commandId; 1.725 + command.setAttribute("label", label); 1.726 + command.addEventListener("command", () => this[func](location), false); 1.727 + 1.728 + menuitem.id = menuitemId; 1.729 + menuitem.setAttribute("command", commandId); 1.730 + aHiddenFlag && menuitem.setAttribute("hidden", "true"); 1.731 + 1.732 + commandset.appendChild(command); 1.733 + menupopup.appendChild(menuitem); 1.734 + } 1.735 + 1.736 + /** 1.737 + * Creates a simple menu separator element and appends it to the current 1.738 + * menupopup hierarchy. 1.739 + */ 1.740 + function createMenuSeparator() { 1.741 + let menuseparator = document.createElement("menuseparator"); 1.742 + menupopup.appendChild(menuseparator); 1.743 + } 1.744 + }, 1.745 + 1.746 + /** 1.747 + * Function called each time a breakpoint item is removed. 1.748 + * 1.749 + * @param object aItem 1.750 + * The corresponding item. 1.751 + */ 1.752 + _onBreakpointRemoved: function(aItem) { 1.753 + dumpn("Finalizing breakpoint item: " + aItem); 1.754 + 1.755 + // Destroy the context menu for the breakpoint. 1.756 + let contextMenu = aItem.attachment.popup; 1.757 + document.getElementById(contextMenu.commandsetId).remove(); 1.758 + document.getElementById(contextMenu.menupopupId).remove(); 1.759 + 1.760 + // Clear the breakpoint selection. 1.761 + if (this._selectedBreakpointItem == aItem) { 1.762 + this._selectedBreakpointItem = null; 1.763 + } 1.764 + }, 1.765 + 1.766 + /** 1.767 + * The load listener for the source editor. 1.768 + */ 1.769 + _onEditorLoad: function(aName, aEditor) { 1.770 + aEditor.on("cursorActivity", this._onEditorCursorActivity); 1.771 + }, 1.772 + 1.773 + /** 1.774 + * The unload listener for the source editor. 1.775 + */ 1.776 + _onEditorUnload: function(aName, aEditor) { 1.777 + aEditor.off("cursorActivity", this._onEditorCursorActivity); 1.778 + }, 1.779 + 1.780 + /** 1.781 + * The selection listener for the source editor. 1.782 + */ 1.783 + _onEditorCursorActivity: function(e) { 1.784 + let editor = DebuggerView.editor; 1.785 + let start = editor.getCursor("start").line + 1; 1.786 + let end = editor.getCursor().line + 1; 1.787 + let url = this.selectedValue; 1.788 + 1.789 + let location = { url: url, line: start }; 1.790 + 1.791 + if (this.getBreakpoint(location) && start == end) { 1.792 + this.highlightBreakpoint(location, { noEditorUpdate: true }); 1.793 + } else { 1.794 + this.unhighlightBreakpoint(); 1.795 + } 1.796 + }, 1.797 + 1.798 + /** 1.799 + * The select listener for the sources container. 1.800 + */ 1.801 + _onSourceSelect: function({ detail: sourceItem }) { 1.802 + if (!sourceItem) { 1.803 + return; 1.804 + } 1.805 + const { source } = sourceItem.attachment; 1.806 + const sourceClient = gThreadClient.source(source); 1.807 + 1.808 + // The container is not empty and an actual item was selected. 1.809 + DebuggerView.setEditorLocation(sourceItem.value); 1.810 + 1.811 + if (Prefs.autoPrettyPrint && !sourceClient.isPrettyPrinted) { 1.812 + DebuggerController.SourceScripts.getText(source).then(([, aText]) => { 1.813 + if (SourceUtils.isMinified(sourceClient, aText)) { 1.814 + this.togglePrettyPrint(); 1.815 + } 1.816 + }).then(null, e => DevToolsUtils.reportException("_onSourceSelect", e)); 1.817 + } 1.818 + 1.819 + // Set window title. No need to split the url by " -> " here, because it was 1.820 + // already sanitized when the source was added. 1.821 + document.title = L10N.getFormatStr("DebuggerWindowScriptTitle", sourceItem.value); 1.822 + 1.823 + DebuggerView.maybeShowBlackBoxMessage(); 1.824 + this.updateToolbarButtonsState(); 1.825 + }, 1.826 + 1.827 + /** 1.828 + * The click listener for the "stop black boxing" button. 1.829 + */ 1.830 + _onStopBlackBoxing: function() { 1.831 + const { source } = this.selectedItem.attachment; 1.832 + 1.833 + DebuggerController.SourceScripts.setBlackBoxing(source, false) 1.834 + .then(this.updateToolbarButtonsState, 1.835 + this.updateToolbarButtonsState); 1.836 + }, 1.837 + 1.838 + /** 1.839 + * The click listener for a breakpoint container. 1.840 + */ 1.841 + _onBreakpointClick: function(e) { 1.842 + let sourceItem = this.getItemForElement(e.target); 1.843 + let breakpointItem = this.getItemForElement.call(sourceItem, e.target); 1.844 + let attachment = breakpointItem.attachment; 1.845 + 1.846 + // Check if this is an enabled conditional breakpoint. 1.847 + let breakpointPromise = DebuggerController.Breakpoints._getAdded(attachment); 1.848 + if (breakpointPromise) { 1.849 + breakpointPromise.then(aBreakpointClient => { 1.850 + doHighlight.call(this, aBreakpointClient.hasCondition()); 1.851 + }); 1.852 + } else { 1.853 + doHighlight.call(this, false); 1.854 + } 1.855 + 1.856 + function doHighlight(aConditionalBreakpointFlag) { 1.857 + // Highlight the breakpoint in this pane and in the editor. 1.858 + this.highlightBreakpoint(attachment, { 1.859 + // Don't show the conditional expression popup if this is not a 1.860 + // conditional breakpoint, or the right mouse button was pressed (to 1.861 + // avoid clashing the popup with the context menu). 1.862 + openPopup: aConditionalBreakpointFlag && e.button == 0 1.863 + }); 1.864 + } 1.865 + }, 1.866 + 1.867 + /** 1.868 + * The click listener for a breakpoint checkbox. 1.869 + */ 1.870 + _onBreakpointCheckboxClick: function(e) { 1.871 + let sourceItem = this.getItemForElement(e.target); 1.872 + let breakpointItem = this.getItemForElement.call(sourceItem, e.target); 1.873 + let attachment = breakpointItem.attachment; 1.874 + 1.875 + // Toggle the breakpoint enabled or disabled. 1.876 + this[attachment.disabled ? "enableBreakpoint" : "disableBreakpoint"](attachment, { 1.877 + // Do this silently (don't update the checkbox checked state), since 1.878 + // this listener is triggered because a checkbox was already clicked. 1.879 + silent: true 1.880 + }); 1.881 + 1.882 + // Don't update the editor location (avoid propagating into _onBreakpointClick). 1.883 + e.preventDefault(); 1.884 + e.stopPropagation(); 1.885 + }, 1.886 + 1.887 + /** 1.888 + * The popup showing listener for the breakpoints conditional expression panel. 1.889 + */ 1.890 + _onConditionalPopupShowing: function() { 1.891 + this._conditionalPopupVisible = true; // Used in tests. 1.892 + window.emit(EVENTS.CONDITIONAL_BREAKPOINT_POPUP_SHOWING); 1.893 + }, 1.894 + 1.895 + /** 1.896 + * The popup shown listener for the breakpoints conditional expression panel. 1.897 + */ 1.898 + _onConditionalPopupShown: function() { 1.899 + this._cbTextbox.focus(); 1.900 + this._cbTextbox.select(); 1.901 + }, 1.902 + 1.903 + /** 1.904 + * The popup hiding listener for the breakpoints conditional expression panel. 1.905 + */ 1.906 + _onConditionalPopupHiding: Task.async(function*() { 1.907 + this._conditionalPopupVisible = false; // Used in tests. 1.908 + let breakpointItem = this._selectedBreakpointItem; 1.909 + let attachment = breakpointItem.attachment; 1.910 + 1.911 + // Check if this is an enabled conditional breakpoint, and if so, 1.912 + // save the current conditional epression. 1.913 + let breakpointPromise = DebuggerController.Breakpoints._getAdded(attachment); 1.914 + if (breakpointPromise) { 1.915 + let breakpointClient = yield breakpointPromise; 1.916 + yield DebuggerController.Breakpoints.updateCondition( 1.917 + breakpointClient.location, 1.918 + this._cbTextbox.value 1.919 + ); 1.920 + } 1.921 + 1.922 + window.emit(EVENTS.CONDITIONAL_BREAKPOINT_POPUP_HIDING); 1.923 + }), 1.924 + 1.925 + /** 1.926 + * The keypress listener for the breakpoints conditional expression textbox. 1.927 + */ 1.928 + _onConditionalTextboxKeyPress: function(e) { 1.929 + if (e.keyCode == e.DOM_VK_RETURN) { 1.930 + this._hideConditionalPopup(); 1.931 + } 1.932 + }, 1.933 + 1.934 + /** 1.935 + * Called when the add breakpoint key sequence was pressed. 1.936 + */ 1.937 + _onCmdAddBreakpoint: function(e) { 1.938 + let url = DebuggerView.Sources.selectedValue; 1.939 + let line = DebuggerView.editor.getCursor().line + 1; 1.940 + let location = { url: url, line: line }; 1.941 + let breakpointItem = this.getBreakpoint(location); 1.942 + 1.943 + // If a breakpoint already existed, remove it now. 1.944 + if (breakpointItem) { 1.945 + DebuggerController.Breakpoints.removeBreakpoint(location); 1.946 + } 1.947 + // No breakpoint existed at the required location, add one now. 1.948 + else { 1.949 + DebuggerController.Breakpoints.addBreakpoint(location); 1.950 + } 1.951 + }, 1.952 + 1.953 + /** 1.954 + * Called when the add conditional breakpoint key sequence was pressed. 1.955 + */ 1.956 + _onCmdAddConditionalBreakpoint: function() { 1.957 + let url = DebuggerView.Sources.selectedValue; 1.958 + let line = DebuggerView.editor.getCursor().line + 1; 1.959 + let location = { url: url, line: line }; 1.960 + let breakpointItem = this.getBreakpoint(location); 1.961 + 1.962 + // If a breakpoint already existed or wasn't a conditional, morph it now. 1.963 + if (breakpointItem) { 1.964 + this.highlightBreakpoint(location, { openPopup: true }); 1.965 + } 1.966 + // No breakpoint existed at the required location, add one now. 1.967 + else { 1.968 + DebuggerController.Breakpoints.addBreakpoint(location, { openPopup: true }); 1.969 + } 1.970 + }, 1.971 + 1.972 + /** 1.973 + * Function invoked on the "setConditional" menuitem command. 1.974 + * 1.975 + * @param object aLocation 1.976 + * @see DebuggerController.Breakpoints.addBreakpoint 1.977 + */ 1.978 + _onSetConditional: function(aLocation) { 1.979 + // Highlight the breakpoint and show a conditional expression popup. 1.980 + this.highlightBreakpoint(aLocation, { openPopup: true }); 1.981 + }, 1.982 + 1.983 + /** 1.984 + * Function invoked on the "enableSelf" menuitem command. 1.985 + * 1.986 + * @param object aLocation 1.987 + * @see DebuggerController.Breakpoints.addBreakpoint 1.988 + */ 1.989 + _onEnableSelf: function(aLocation) { 1.990 + // Enable the breakpoint, in this container and the controller store. 1.991 + this.enableBreakpoint(aLocation); 1.992 + }, 1.993 + 1.994 + /** 1.995 + * Function invoked on the "disableSelf" menuitem command. 1.996 + * 1.997 + * @param object aLocation 1.998 + * @see DebuggerController.Breakpoints.addBreakpoint 1.999 + */ 1.1000 + _onDisableSelf: function(aLocation) { 1.1001 + // Disable the breakpoint, in this container and the controller store. 1.1002 + this.disableBreakpoint(aLocation); 1.1003 + }, 1.1004 + 1.1005 + /** 1.1006 + * Function invoked on the "deleteSelf" menuitem command. 1.1007 + * 1.1008 + * @param object aLocation 1.1009 + * @see DebuggerController.Breakpoints.addBreakpoint 1.1010 + */ 1.1011 + _onDeleteSelf: function(aLocation) { 1.1012 + // Remove the breakpoint, from this container and the controller store. 1.1013 + this.removeBreakpoint(aLocation); 1.1014 + DebuggerController.Breakpoints.removeBreakpoint(aLocation); 1.1015 + }, 1.1016 + 1.1017 + /** 1.1018 + * Function invoked on the "enableOthers" menuitem command. 1.1019 + * 1.1020 + * @param object aLocation 1.1021 + * @see DebuggerController.Breakpoints.addBreakpoint 1.1022 + */ 1.1023 + _onEnableOthers: function(aLocation) { 1.1024 + let enableOthers = aCallback => { 1.1025 + let other = this.getOtherBreakpoints(aLocation); 1.1026 + let outstanding = other.map(e => this.enableBreakpoint(e.attachment)); 1.1027 + promise.all(outstanding).then(aCallback); 1.1028 + } 1.1029 + 1.1030 + // Breakpoints can only be set while the debuggee is paused. To avoid 1.1031 + // an avalanche of pause/resume interrupts of the main thread, simply 1.1032 + // pause it beforehand if it's not already. 1.1033 + if (gThreadClient.state != "paused") { 1.1034 + gThreadClient.interrupt(() => enableOthers(() => gThreadClient.resume())); 1.1035 + } else { 1.1036 + enableOthers(); 1.1037 + } 1.1038 + }, 1.1039 + 1.1040 + /** 1.1041 + * Function invoked on the "disableOthers" menuitem command. 1.1042 + * 1.1043 + * @param object aLocation 1.1044 + * @see DebuggerController.Breakpoints.addBreakpoint 1.1045 + */ 1.1046 + _onDisableOthers: function(aLocation) { 1.1047 + let other = this.getOtherBreakpoints(aLocation); 1.1048 + other.forEach(e => this._onDisableSelf(e.attachment)); 1.1049 + }, 1.1050 + 1.1051 + /** 1.1052 + * Function invoked on the "deleteOthers" menuitem command. 1.1053 + * 1.1054 + * @param object aLocation 1.1055 + * @see DebuggerController.Breakpoints.addBreakpoint 1.1056 + */ 1.1057 + _onDeleteOthers: function(aLocation) { 1.1058 + let other = this.getOtherBreakpoints(aLocation); 1.1059 + other.forEach(e => this._onDeleteSelf(e.attachment)); 1.1060 + }, 1.1061 + 1.1062 + /** 1.1063 + * Function invoked on the "enableAll" menuitem command. 1.1064 + */ 1.1065 + _onEnableAll: function() { 1.1066 + this._onEnableOthers(undefined); 1.1067 + }, 1.1068 + 1.1069 + /** 1.1070 + * Function invoked on the "disableAll" menuitem command. 1.1071 + */ 1.1072 + _onDisableAll: function() { 1.1073 + this._onDisableOthers(undefined); 1.1074 + }, 1.1075 + 1.1076 + /** 1.1077 + * Function invoked on the "deleteAll" menuitem command. 1.1078 + */ 1.1079 + _onDeleteAll: function() { 1.1080 + this._onDeleteOthers(undefined); 1.1081 + }, 1.1082 + 1.1083 + _commandset: null, 1.1084 + _popupset: null, 1.1085 + _cmPopup: null, 1.1086 + _cbPanel: null, 1.1087 + _cbTextbox: null, 1.1088 + _selectedBreakpointItem: null, 1.1089 + _conditionalPopupVisible: false 1.1090 +}); 1.1091 + 1.1092 +/** 1.1093 + * Functions handling the traces UI. 1.1094 + */ 1.1095 +function TracerView() { 1.1096 + this._selectedItem = null; 1.1097 + this._matchingItems = null; 1.1098 + this.widget = null; 1.1099 + 1.1100 + this._highlightItem = this._highlightItem.bind(this); 1.1101 + this._isNotSelectedItem = this._isNotSelectedItem.bind(this); 1.1102 + 1.1103 + this._unhighlightMatchingItems = 1.1104 + DevToolsUtils.makeInfallible(this._unhighlightMatchingItems.bind(this)); 1.1105 + this._onToggleTracing = 1.1106 + DevToolsUtils.makeInfallible(this._onToggleTracing.bind(this)); 1.1107 + this._onStartTracing = 1.1108 + DevToolsUtils.makeInfallible(this._onStartTracing.bind(this)); 1.1109 + this._onClear = 1.1110 + DevToolsUtils.makeInfallible(this._onClear.bind(this)); 1.1111 + this._onSelect = 1.1112 + DevToolsUtils.makeInfallible(this._onSelect.bind(this)); 1.1113 + this._onMouseOver = 1.1114 + DevToolsUtils.makeInfallible(this._onMouseOver.bind(this)); 1.1115 + this._onSearch = DevToolsUtils.makeInfallible(this._onSearch.bind(this)); 1.1116 +} 1.1117 + 1.1118 +TracerView.MAX_TRACES = 200; 1.1119 + 1.1120 +TracerView.prototype = Heritage.extend(WidgetMethods, { 1.1121 + /** 1.1122 + * Initialization function, called when the debugger is started. 1.1123 + */ 1.1124 + initialize: function() { 1.1125 + dumpn("Initializing the TracerView"); 1.1126 + 1.1127 + this._traceButton = document.getElementById("trace"); 1.1128 + this._tracerTab = document.getElementById("tracer-tab"); 1.1129 + 1.1130 + // Remove tracer related elements from the dom and tear everything down if 1.1131 + // the tracer isn't enabled. 1.1132 + if (!Prefs.tracerEnabled) { 1.1133 + this._traceButton.remove(); 1.1134 + this._traceButton = null; 1.1135 + this._tracerTab.remove(); 1.1136 + this._tracerTab = null; 1.1137 + return; 1.1138 + } 1.1139 + 1.1140 + this.widget = new FastListWidget(document.getElementById("tracer-traces")); 1.1141 + this._traceButton.removeAttribute("hidden"); 1.1142 + this._tracerTab.removeAttribute("hidden"); 1.1143 + 1.1144 + this._search = document.getElementById("tracer-search"); 1.1145 + this._template = document.getElementsByClassName("trace-item-template")[0]; 1.1146 + this._templateItem = this._template.getElementsByClassName("trace-item")[0]; 1.1147 + this._templateTypeIcon = this._template.getElementsByClassName("trace-type")[0]; 1.1148 + this._templateNameNode = this._template.getElementsByClassName("trace-name")[0]; 1.1149 + 1.1150 + this.widget.addEventListener("select", this._onSelect, false); 1.1151 + this.widget.addEventListener("mouseover", this._onMouseOver, false); 1.1152 + this.widget.addEventListener("mouseout", this._unhighlightMatchingItems, false); 1.1153 + this._search.addEventListener("input", this._onSearch, false); 1.1154 + 1.1155 + this._startTooltip = L10N.getStr("startTracingTooltip"); 1.1156 + this._stopTooltip = L10N.getStr("stopTracingTooltip"); 1.1157 + this._tracingNotStartedString = L10N.getStr("tracingNotStartedText"); 1.1158 + this._noFunctionCallsString = L10N.getStr("noFunctionCallsText"); 1.1159 + 1.1160 + this._traceButton.setAttribute("tooltiptext", this._startTooltip); 1.1161 + this.emptyText = this._tracingNotStartedString; 1.1162 + }, 1.1163 + 1.1164 + /** 1.1165 + * Destruction function, called when the debugger is closed. 1.1166 + */ 1.1167 + destroy: function() { 1.1168 + dumpn("Destroying the TracerView"); 1.1169 + 1.1170 + if (!this.widget) { 1.1171 + return; 1.1172 + } 1.1173 + 1.1174 + this.widget.removeEventListener("select", this._onSelect, false); 1.1175 + this.widget.removeEventListener("mouseover", this._onMouseOver, false); 1.1176 + this.widget.removeEventListener("mouseout", this._unhighlightMatchingItems, false); 1.1177 + this._search.removeEventListener("input", this._onSearch, false); 1.1178 + }, 1.1179 + 1.1180 + /** 1.1181 + * Function invoked by the "toggleTracing" command to switch the tracer state. 1.1182 + */ 1.1183 + _onToggleTracing: function() { 1.1184 + if (DebuggerController.Tracer.tracing) { 1.1185 + this._onStopTracing(); 1.1186 + } else { 1.1187 + this._onStartTracing(); 1.1188 + } 1.1189 + }, 1.1190 + 1.1191 + /** 1.1192 + * Function invoked either by the "startTracing" command or by 1.1193 + * _onToggleTracing to start execution tracing in the backend. 1.1194 + * 1.1195 + * @return object 1.1196 + * A promise resolved once the tracing has successfully started. 1.1197 + */ 1.1198 + _onStartTracing: function() { 1.1199 + this._traceButton.setAttribute("checked", true); 1.1200 + this._traceButton.setAttribute("tooltiptext", this._stopTooltip); 1.1201 + 1.1202 + this.empty(); 1.1203 + this.emptyText = this._noFunctionCallsString; 1.1204 + 1.1205 + let deferred = promise.defer(); 1.1206 + DebuggerController.Tracer.startTracing(deferred.resolve); 1.1207 + return deferred.promise; 1.1208 + }, 1.1209 + 1.1210 + /** 1.1211 + * Function invoked by _onToggleTracing to stop execution tracing in the 1.1212 + * backend. 1.1213 + * 1.1214 + * @return object 1.1215 + * A promise resolved once the tracing has successfully stopped. 1.1216 + */ 1.1217 + _onStopTracing: function() { 1.1218 + this._traceButton.removeAttribute("checked"); 1.1219 + this._traceButton.setAttribute("tooltiptext", this._startTooltip); 1.1220 + 1.1221 + this.emptyText = this._tracingNotStartedString; 1.1222 + 1.1223 + let deferred = promise.defer(); 1.1224 + DebuggerController.Tracer.stopTracing(deferred.resolve); 1.1225 + return deferred.promise; 1.1226 + }, 1.1227 + 1.1228 + /** 1.1229 + * Function invoked by the "clearTraces" command to empty the traces pane. 1.1230 + */ 1.1231 + _onClear: function() { 1.1232 + this.empty(); 1.1233 + }, 1.1234 + 1.1235 + /** 1.1236 + * Populate the given parent scope with the variable with the provided name 1.1237 + * and value. 1.1238 + * 1.1239 + * @param String aName 1.1240 + * The name of the variable. 1.1241 + * @param Object aParent 1.1242 + * The parent scope. 1.1243 + * @param Object aValue 1.1244 + * The value of the variable. 1.1245 + */ 1.1246 + _populateVariable: function(aName, aParent, aValue) { 1.1247 + let item = aParent.addItem(aName, { value: aValue }); 1.1248 + if (aValue) { 1.1249 + let wrappedValue = new DebuggerController.Tracer.WrappedObject(aValue); 1.1250 + DebuggerView.Variables.controller.populate(item, wrappedValue); 1.1251 + item.expand(); 1.1252 + item.twisty = false; 1.1253 + } 1.1254 + }, 1.1255 + 1.1256 + /** 1.1257 + * Handler for the widget's "select" event. Displays parameters, exception, or 1.1258 + * return value depending on whether the selected trace is a call, throw, or 1.1259 + * return respectively. 1.1260 + * 1.1261 + * @param Object traceItem 1.1262 + * The selected trace item. 1.1263 + */ 1.1264 + _onSelect: function _onSelect({ detail: traceItem }) { 1.1265 + if (!traceItem) { 1.1266 + return; 1.1267 + } 1.1268 + 1.1269 + const data = traceItem.attachment.trace; 1.1270 + const { location: { url, line } } = data; 1.1271 + DebuggerView.setEditorLocation(url, line, { noDebug: true }); 1.1272 + 1.1273 + DebuggerView.Variables.empty(); 1.1274 + const scope = DebuggerView.Variables.addScope(); 1.1275 + 1.1276 + if (data.type == "call") { 1.1277 + const params = DevToolsUtils.zip(data.parameterNames, data.arguments); 1.1278 + for (let [name, val] of params) { 1.1279 + if (val === undefined) { 1.1280 + scope.addItem(name, { value: "<value not available>" }); 1.1281 + } else { 1.1282 + this._populateVariable(name, scope, val); 1.1283 + } 1.1284 + } 1.1285 + } else { 1.1286 + const varName = "<" + (data.type == "throw" ? "exception" : data.type) + ">"; 1.1287 + this._populateVariable(varName, scope, data.returnVal); 1.1288 + } 1.1289 + 1.1290 + scope.expand(); 1.1291 + DebuggerView.showInstrumentsPane(); 1.1292 + }, 1.1293 + 1.1294 + /** 1.1295 + * Add the hover frame enter/exit highlighting to a given item. 1.1296 + */ 1.1297 + _highlightItem: function(aItem) { 1.1298 + if (!aItem || !aItem.target) { 1.1299 + return; 1.1300 + } 1.1301 + const trace = aItem.target.querySelector(".trace-item"); 1.1302 + trace.classList.add("selected-matching"); 1.1303 + }, 1.1304 + 1.1305 + /** 1.1306 + * Remove the hover frame enter/exit highlighting to a given item. 1.1307 + */ 1.1308 + _unhighlightItem: function(aItem) { 1.1309 + if (!aItem || !aItem.target) { 1.1310 + return; 1.1311 + } 1.1312 + const match = aItem.target.querySelector(".selected-matching"); 1.1313 + if (match) { 1.1314 + match.classList.remove("selected-matching"); 1.1315 + } 1.1316 + }, 1.1317 + 1.1318 + /** 1.1319 + * Remove the frame enter/exit pair highlighting we do when hovering. 1.1320 + */ 1.1321 + _unhighlightMatchingItems: function() { 1.1322 + if (this._matchingItems) { 1.1323 + this._matchingItems.forEach(this._unhighlightItem); 1.1324 + this._matchingItems = null; 1.1325 + } 1.1326 + }, 1.1327 + 1.1328 + /** 1.1329 + * Returns true if the given item is not the selected item. 1.1330 + */ 1.1331 + _isNotSelectedItem: function(aItem) { 1.1332 + return aItem !== this.selectedItem; 1.1333 + }, 1.1334 + 1.1335 + /** 1.1336 + * Highlight the frame enter/exit pair of items for the given item. 1.1337 + */ 1.1338 + _highlightMatchingItems: function(aItem) { 1.1339 + const frameId = aItem.attachment.trace.frameId; 1.1340 + const predicate = e => e.attachment.trace.frameId == frameId; 1.1341 + 1.1342 + this._unhighlightMatchingItems(); 1.1343 + this._matchingItems = this.items.filter(predicate); 1.1344 + this._matchingItems 1.1345 + .filter(this._isNotSelectedItem) 1.1346 + .forEach(this._highlightItem); 1.1347 + }, 1.1348 + 1.1349 + /** 1.1350 + * Listener for the mouseover event. 1.1351 + */ 1.1352 + _onMouseOver: function({ target }) { 1.1353 + const traceItem = this.getItemForElement(target); 1.1354 + if (traceItem) { 1.1355 + this._highlightMatchingItems(traceItem); 1.1356 + } 1.1357 + }, 1.1358 + 1.1359 + /** 1.1360 + * Listener for typing in the search box. 1.1361 + */ 1.1362 + _onSearch: function() { 1.1363 + const query = this._search.value.trim().toLowerCase(); 1.1364 + const predicate = name => name.toLowerCase().contains(query); 1.1365 + this.filterContents(item => predicate(item.attachment.trace.name)); 1.1366 + }, 1.1367 + 1.1368 + /** 1.1369 + * Select the traces tab in the sidebar. 1.1370 + */ 1.1371 + selectTab: function() { 1.1372 + const tabs = this._tracerTab.parentElement; 1.1373 + tabs.selectedIndex = Array.indexOf(tabs.children, this._tracerTab); 1.1374 + }, 1.1375 + 1.1376 + /** 1.1377 + * Commit all staged items to the widget. Overridden so that we can call 1.1378 + * |FastListWidget.prototype.flush|. 1.1379 + */ 1.1380 + commit: function() { 1.1381 + WidgetMethods.commit.call(this); 1.1382 + // TODO: Accessing non-standard widget properties. Figure out what's the 1.1383 + // best way to expose such things. Bug 895514. 1.1384 + this.widget.flush(); 1.1385 + }, 1.1386 + 1.1387 + /** 1.1388 + * Adds the trace record provided as an argument to the view. 1.1389 + * 1.1390 + * @param object aTrace 1.1391 + * The trace record coming from the tracer actor. 1.1392 + */ 1.1393 + addTrace: function(aTrace) { 1.1394 + // Create the element node for the trace item. 1.1395 + let view = this._createView(aTrace); 1.1396 + 1.1397 + // Append a source item to this container. 1.1398 + this.push([view], { 1.1399 + staged: true, 1.1400 + attachment: { 1.1401 + trace: aTrace 1.1402 + } 1.1403 + }); 1.1404 + }, 1.1405 + 1.1406 + /** 1.1407 + * Customization function for creating an item's UI. 1.1408 + * 1.1409 + * @return nsIDOMNode 1.1410 + * The network request view. 1.1411 + */ 1.1412 + _createView: function(aTrace) { 1.1413 + let { type, name, location, depth, frameId } = aTrace; 1.1414 + let { parameterNames, returnVal, arguments: args } = aTrace; 1.1415 + let fragment = document.createDocumentFragment(); 1.1416 + 1.1417 + this._templateItem.setAttribute("tooltiptext", SourceUtils.trimUrl(location.url)); 1.1418 + this._templateItem.style.MozPaddingStart = depth + "em"; 1.1419 + 1.1420 + const TYPES = ["call", "yield", "return", "throw"]; 1.1421 + for (let t of TYPES) { 1.1422 + this._templateTypeIcon.classList.toggle("trace-" + t, t == type); 1.1423 + } 1.1424 + this._templateTypeIcon.setAttribute("value", { 1.1425 + call: "\u2192", 1.1426 + yield: "Y", 1.1427 + return: "\u2190", 1.1428 + throw: "E", 1.1429 + terminated: "TERMINATED" 1.1430 + }[type]); 1.1431 + 1.1432 + this._templateNameNode.setAttribute("value", name); 1.1433 + 1.1434 + // All extra syntax and parameter nodes added. 1.1435 + const addedNodes = []; 1.1436 + 1.1437 + if (parameterNames) { 1.1438 + const syntax = (p) => { 1.1439 + const el = document.createElement("label"); 1.1440 + el.setAttribute("value", p); 1.1441 + el.classList.add("trace-syntax"); 1.1442 + el.classList.add("plain"); 1.1443 + addedNodes.push(el); 1.1444 + return el; 1.1445 + }; 1.1446 + 1.1447 + this._templateItem.appendChild(syntax("(")); 1.1448 + 1.1449 + for (let i = 0, n = parameterNames.length; i < n; i++) { 1.1450 + let param = document.createElement("label"); 1.1451 + param.setAttribute("value", parameterNames[i]); 1.1452 + param.classList.add("trace-param"); 1.1453 + param.classList.add("plain"); 1.1454 + addedNodes.push(param); 1.1455 + this._templateItem.appendChild(param); 1.1456 + 1.1457 + if (i + 1 !== n) { 1.1458 + this._templateItem.appendChild(syntax(", ")); 1.1459 + } 1.1460 + } 1.1461 + 1.1462 + this._templateItem.appendChild(syntax(")")); 1.1463 + } 1.1464 + 1.1465 + // Flatten the DOM by removing one redundant box (the template container). 1.1466 + for (let node of this._template.childNodes) { 1.1467 + fragment.appendChild(node.cloneNode(true)); 1.1468 + } 1.1469 + 1.1470 + // Remove any added nodes from the template. 1.1471 + for (let node of addedNodes) { 1.1472 + this._templateItem.removeChild(node); 1.1473 + } 1.1474 + 1.1475 + return fragment; 1.1476 + } 1.1477 +}); 1.1478 + 1.1479 +/** 1.1480 + * Utility functions for handling sources. 1.1481 + */ 1.1482 +let SourceUtils = { 1.1483 + _labelsCache: new Map(), // Can't use WeakMaps because keys are strings. 1.1484 + _groupsCache: new Map(), 1.1485 + _minifiedCache: new WeakMap(), 1.1486 + 1.1487 + /** 1.1488 + * Returns true if the specified url and/or content type are specific to 1.1489 + * javascript files. 1.1490 + * 1.1491 + * @return boolean 1.1492 + * True if the source is likely javascript. 1.1493 + */ 1.1494 + isJavaScript: function(aUrl, aContentType = "") { 1.1495 + return /\.jsm?$/.test(this.trimUrlQuery(aUrl)) || 1.1496 + aContentType.contains("javascript"); 1.1497 + }, 1.1498 + 1.1499 + /** 1.1500 + * Determines if the source text is minified by using 1.1501 + * the percentage indented of a subset of lines 1.1502 + * 1.1503 + * @param string aText 1.1504 + * The source text. 1.1505 + * @return boolean 1.1506 + * True if source text is minified. 1.1507 + */ 1.1508 + isMinified: function(sourceClient, aText){ 1.1509 + if (this._minifiedCache.has(sourceClient)) { 1.1510 + return this._minifiedCache.get(sourceClient); 1.1511 + } 1.1512 + 1.1513 + let isMinified; 1.1514 + let lineEndIndex = 0; 1.1515 + let lineStartIndex = 0; 1.1516 + let lines = 0; 1.1517 + let indentCount = 0; 1.1518 + let overCharLimit = false; 1.1519 + 1.1520 + // Strip comments. 1.1521 + aText = aText.replace(/\/\*[\S\s]*?\*\/|\/\/(.+|\n)/g, ""); 1.1522 + 1.1523 + while (lines++ < SAMPLE_SIZE) { 1.1524 + lineEndIndex = aText.indexOf("\n", lineStartIndex); 1.1525 + if (lineEndIndex == -1) { 1.1526 + break; 1.1527 + } 1.1528 + if (/^\s+/.test(aText.slice(lineStartIndex, lineEndIndex))) { 1.1529 + indentCount++; 1.1530 + } 1.1531 + // For files with no indents but are not minified. 1.1532 + if ((lineEndIndex - lineStartIndex) > CHARACTER_LIMIT) { 1.1533 + overCharLimit = true; 1.1534 + break; 1.1535 + } 1.1536 + lineStartIndex = lineEndIndex + 1; 1.1537 + } 1.1538 + isMinified = ((indentCount / lines ) * 100) < INDENT_COUNT_THRESHOLD || 1.1539 + overCharLimit; 1.1540 + 1.1541 + this._minifiedCache.set(sourceClient, isMinified); 1.1542 + return isMinified; 1.1543 + }, 1.1544 + 1.1545 + /** 1.1546 + * Clears the labels, groups and minify cache, populated by methods like 1.1547 + * SourceUtils.getSourceLabel or Source Utils.getSourceGroup. 1.1548 + * This should be done every time the content location changes. 1.1549 + */ 1.1550 + clearCache: function() { 1.1551 + this._labelsCache.clear(); 1.1552 + this._groupsCache.clear(); 1.1553 + this._minifiedCache.clear(); 1.1554 + }, 1.1555 + 1.1556 + /** 1.1557 + * Gets a unique, simplified label from a source url. 1.1558 + * 1.1559 + * @param string aUrl 1.1560 + * The source url. 1.1561 + * @return string 1.1562 + * The simplified label. 1.1563 + */ 1.1564 + getSourceLabel: function(aUrl) { 1.1565 + let cachedLabel = this._labelsCache.get(aUrl); 1.1566 + if (cachedLabel) { 1.1567 + return cachedLabel; 1.1568 + } 1.1569 + 1.1570 + let sourceLabel = null; 1.1571 + 1.1572 + for (let name of Object.keys(KNOWN_SOURCE_GROUPS)) { 1.1573 + if (aUrl.startsWith(KNOWN_SOURCE_GROUPS[name])) { 1.1574 + sourceLabel = aUrl.substring(KNOWN_SOURCE_GROUPS[name].length); 1.1575 + } 1.1576 + } 1.1577 + 1.1578 + if (!sourceLabel) { 1.1579 + sourceLabel = this.trimUrl(aUrl); 1.1580 + } 1.1581 + let unicodeLabel = NetworkHelper.convertToUnicode(unescape(sourceLabel)); 1.1582 + this._labelsCache.set(aUrl, unicodeLabel); 1.1583 + return unicodeLabel; 1.1584 + }, 1.1585 + 1.1586 + /** 1.1587 + * Gets as much information as possible about the hostname and directory paths 1.1588 + * of an url to create a short url group identifier. 1.1589 + * 1.1590 + * @param string aUrl 1.1591 + * The source url. 1.1592 + * @return string 1.1593 + * The simplified group. 1.1594 + */ 1.1595 + getSourceGroup: function(aUrl) { 1.1596 + let cachedGroup = this._groupsCache.get(aUrl); 1.1597 + if (cachedGroup) { 1.1598 + return cachedGroup; 1.1599 + } 1.1600 + 1.1601 + try { 1.1602 + // Use an nsIURL to parse all the url path parts. 1.1603 + var uri = Services.io.newURI(aUrl, null, null).QueryInterface(Ci.nsIURL); 1.1604 + } catch (e) { 1.1605 + // This doesn't look like a url, or nsIURL can't handle it. 1.1606 + return ""; 1.1607 + } 1.1608 + 1.1609 + let groupLabel = uri.prePath; 1.1610 + 1.1611 + for (let name of Object.keys(KNOWN_SOURCE_GROUPS)) { 1.1612 + if (aUrl.startsWith(KNOWN_SOURCE_GROUPS[name])) { 1.1613 + groupLabel = name; 1.1614 + } 1.1615 + } 1.1616 + 1.1617 + let unicodeLabel = NetworkHelper.convertToUnicode(unescape(groupLabel)); 1.1618 + this._groupsCache.set(aUrl, unicodeLabel) 1.1619 + return unicodeLabel; 1.1620 + }, 1.1621 + 1.1622 + /** 1.1623 + * Trims the url by shortening it if it exceeds a certain length, adding an 1.1624 + * ellipsis at the end. 1.1625 + * 1.1626 + * @param string aUrl 1.1627 + * The source url. 1.1628 + * @param number aLength [optional] 1.1629 + * The expected source url length. 1.1630 + * @param number aSection [optional] 1.1631 + * The section to trim. Supported values: "start", "center", "end" 1.1632 + * @return string 1.1633 + * The shortened url. 1.1634 + */ 1.1635 + trimUrlLength: function(aUrl, aLength, aSection) { 1.1636 + aLength = aLength || SOURCE_URL_DEFAULT_MAX_LENGTH; 1.1637 + aSection = aSection || "end"; 1.1638 + 1.1639 + if (aUrl.length > aLength) { 1.1640 + switch (aSection) { 1.1641 + case "start": 1.1642 + return L10N.ellipsis + aUrl.slice(-aLength); 1.1643 + break; 1.1644 + case "center": 1.1645 + return aUrl.substr(0, aLength / 2 - 1) + L10N.ellipsis + aUrl.slice(-aLength / 2 + 1); 1.1646 + break; 1.1647 + case "end": 1.1648 + return aUrl.substr(0, aLength) + L10N.ellipsis; 1.1649 + break; 1.1650 + } 1.1651 + } 1.1652 + return aUrl; 1.1653 + }, 1.1654 + 1.1655 + /** 1.1656 + * Trims the query part or reference identifier of a url string, if necessary. 1.1657 + * 1.1658 + * @param string aUrl 1.1659 + * The source url. 1.1660 + * @return string 1.1661 + * The shortened url. 1.1662 + */ 1.1663 + trimUrlQuery: function(aUrl) { 1.1664 + let length = aUrl.length; 1.1665 + let q1 = aUrl.indexOf('?'); 1.1666 + let q2 = aUrl.indexOf('&'); 1.1667 + let q3 = aUrl.indexOf('#'); 1.1668 + let q = Math.min(q1 != -1 ? q1 : length, 1.1669 + q2 != -1 ? q2 : length, 1.1670 + q3 != -1 ? q3 : length); 1.1671 + 1.1672 + return aUrl.slice(0, q); 1.1673 + }, 1.1674 + 1.1675 + /** 1.1676 + * Trims as much as possible from a url, while keeping the label unique 1.1677 + * in the sources container. 1.1678 + * 1.1679 + * @param string | nsIURL aUrl 1.1680 + * The source url. 1.1681 + * @param string aLabel [optional] 1.1682 + * The resulting label at each step. 1.1683 + * @param number aSeq [optional] 1.1684 + * The current iteration step. 1.1685 + * @return string 1.1686 + * The resulting label at the final step. 1.1687 + */ 1.1688 + trimUrl: function(aUrl, aLabel, aSeq) { 1.1689 + if (!(aUrl instanceof Ci.nsIURL)) { 1.1690 + try { 1.1691 + // Use an nsIURL to parse all the url path parts. 1.1692 + aUrl = Services.io.newURI(aUrl, null, null).QueryInterface(Ci.nsIURL); 1.1693 + } catch (e) { 1.1694 + // This doesn't look like a url, or nsIURL can't handle it. 1.1695 + return aUrl; 1.1696 + } 1.1697 + } 1.1698 + if (!aSeq) { 1.1699 + let name = aUrl.fileName; 1.1700 + if (name) { 1.1701 + // This is a regular file url, get only the file name (contains the 1.1702 + // base name and extension if available). 1.1703 + 1.1704 + // If this url contains an invalid query, unfortunately nsIURL thinks 1.1705 + // it's part of the file extension. It must be removed. 1.1706 + aLabel = aUrl.fileName.replace(/\&.*/, ""); 1.1707 + } else { 1.1708 + // This is not a file url, hence there is no base name, nor extension. 1.1709 + // Proceed using other available information. 1.1710 + aLabel = ""; 1.1711 + } 1.1712 + aSeq = 1; 1.1713 + } 1.1714 + 1.1715 + // If we have a label and it doesn't only contain a query... 1.1716 + if (aLabel && aLabel.indexOf("?") != 0) { 1.1717 + // A page may contain multiple requests to the same url but with different 1.1718 + // queries. It is *not* redundant to show each one. 1.1719 + if (!DebuggerView.Sources.getItemForAttachment(e => e.label == aLabel)) { 1.1720 + return aLabel; 1.1721 + } 1.1722 + } 1.1723 + 1.1724 + // Append the url query. 1.1725 + if (aSeq == 1) { 1.1726 + let query = aUrl.query; 1.1727 + if (query) { 1.1728 + return this.trimUrl(aUrl, aLabel + "?" + query, aSeq + 1); 1.1729 + } 1.1730 + aSeq++; 1.1731 + } 1.1732 + // Append the url reference. 1.1733 + if (aSeq == 2) { 1.1734 + let ref = aUrl.ref; 1.1735 + if (ref) { 1.1736 + return this.trimUrl(aUrl, aLabel + "#" + aUrl.ref, aSeq + 1); 1.1737 + } 1.1738 + aSeq++; 1.1739 + } 1.1740 + // Prepend the url directory. 1.1741 + if (aSeq == 3) { 1.1742 + let dir = aUrl.directory; 1.1743 + if (dir) { 1.1744 + return this.trimUrl(aUrl, dir.replace(/^\//, "") + aLabel, aSeq + 1); 1.1745 + } 1.1746 + aSeq++; 1.1747 + } 1.1748 + // Prepend the hostname and port number. 1.1749 + if (aSeq == 4) { 1.1750 + let host = aUrl.hostPort; 1.1751 + if (host) { 1.1752 + return this.trimUrl(aUrl, host + "/" + aLabel, aSeq + 1); 1.1753 + } 1.1754 + aSeq++; 1.1755 + } 1.1756 + // Use the whole url spec but ignoring the reference. 1.1757 + if (aSeq == 5) { 1.1758 + return this.trimUrl(aUrl, aUrl.specIgnoringRef, aSeq + 1); 1.1759 + } 1.1760 + // Give up. 1.1761 + return aUrl.spec; 1.1762 + } 1.1763 +}; 1.1764 + 1.1765 +/** 1.1766 + * Functions handling the variables bubble UI. 1.1767 + */ 1.1768 +function VariableBubbleView() { 1.1769 + dumpn("VariableBubbleView was instantiated"); 1.1770 + 1.1771 + this._onMouseMove = this._onMouseMove.bind(this); 1.1772 + this._onMouseLeave = this._onMouseLeave.bind(this); 1.1773 + this._onPopupHiding = this._onPopupHiding.bind(this); 1.1774 +} 1.1775 + 1.1776 +VariableBubbleView.prototype = { 1.1777 + /** 1.1778 + * Initialization function, called when the debugger is started. 1.1779 + */ 1.1780 + initialize: function() { 1.1781 + dumpn("Initializing the VariableBubbleView"); 1.1782 + 1.1783 + this._editorContainer = document.getElementById("editor"); 1.1784 + this._editorContainer.addEventListener("mousemove", this._onMouseMove, false); 1.1785 + this._editorContainer.addEventListener("mouseleave", this._onMouseLeave, false); 1.1786 + 1.1787 + this._tooltip = new Tooltip(document, { 1.1788 + closeOnEvents: [{ 1.1789 + emitter: DebuggerController._toolbox, 1.1790 + event: "select" 1.1791 + }, { 1.1792 + emitter: this._editorContainer, 1.1793 + event: "scroll", 1.1794 + useCapture: true 1.1795 + }] 1.1796 + }); 1.1797 + this._tooltip.defaultPosition = EDITOR_VARIABLE_POPUP_POSITION; 1.1798 + this._tooltip.defaultShowDelay = EDITOR_VARIABLE_HOVER_DELAY; 1.1799 + this._tooltip.panel.addEventListener("popuphiding", this._onPopupHiding); 1.1800 + }, 1.1801 + 1.1802 + /** 1.1803 + * Destruction function, called when the debugger is closed. 1.1804 + */ 1.1805 + destroy: function() { 1.1806 + dumpn("Destroying the VariableBubbleView"); 1.1807 + 1.1808 + this._tooltip.panel.removeEventListener("popuphiding", this._onPopupHiding); 1.1809 + this._editorContainer.removeEventListener("mousemove", this._onMouseMove, false); 1.1810 + this._editorContainer.removeEventListener("mouseleave", this._onMouseLeave, false); 1.1811 + }, 1.1812 + 1.1813 + /** 1.1814 + * Specifies whether literals can be (redundantly) inspected in a popup. 1.1815 + * This behavior is deprecated, but still tested in a few places. 1.1816 + */ 1.1817 + _ignoreLiterals: true, 1.1818 + 1.1819 + /** 1.1820 + * Searches for an identifier underneath the specified position in the 1.1821 + * source editor, and if found, opens a VariablesView inspection popup. 1.1822 + * 1.1823 + * @param number x, y 1.1824 + * The left/top coordinates where to look for an identifier. 1.1825 + */ 1.1826 + _findIdentifier: function(x, y) { 1.1827 + let editor = DebuggerView.editor; 1.1828 + 1.1829 + // Calculate the editor's line and column at the current x and y coords. 1.1830 + let hoveredPos = editor.getPositionFromCoords({ left: x, top: y }); 1.1831 + let hoveredOffset = editor.getOffset(hoveredPos); 1.1832 + let hoveredLine = hoveredPos.line; 1.1833 + let hoveredColumn = hoveredPos.ch; 1.1834 + 1.1835 + // A source contains multiple scripts. Find the start index of the script 1.1836 + // containing the specified offset relative to its parent source. 1.1837 + let contents = editor.getText(); 1.1838 + let location = DebuggerView.Sources.selectedValue; 1.1839 + let parsedSource = DebuggerController.Parser.get(contents, location); 1.1840 + let scriptInfo = parsedSource.getScriptInfo(hoveredOffset); 1.1841 + 1.1842 + // If the script length is negative, we're not hovering JS source code. 1.1843 + if (scriptInfo.length == -1) { 1.1844 + return; 1.1845 + } 1.1846 + 1.1847 + // Using the script offset, determine the actual line and column inside the 1.1848 + // script, to use when finding identifiers. 1.1849 + let scriptStart = editor.getPosition(scriptInfo.start); 1.1850 + let scriptLineOffset = scriptStart.line; 1.1851 + let scriptColumnOffset = (hoveredLine == scriptStart.line ? scriptStart.ch : 0); 1.1852 + 1.1853 + let scriptLine = hoveredLine - scriptLineOffset; 1.1854 + let scriptColumn = hoveredColumn - scriptColumnOffset; 1.1855 + let identifierInfo = parsedSource.getIdentifierAt({ 1.1856 + line: scriptLine + 1, 1.1857 + column: scriptColumn, 1.1858 + scriptIndex: scriptInfo.index, 1.1859 + ignoreLiterals: this._ignoreLiterals 1.1860 + }); 1.1861 + 1.1862 + // If the info is null, we're not hovering any identifier. 1.1863 + if (!identifierInfo) { 1.1864 + return; 1.1865 + } 1.1866 + 1.1867 + // Transform the line and column relative to the parsed script back 1.1868 + // to the context of the parent source. 1.1869 + let { start: identifierStart, end: identifierEnd } = identifierInfo.location; 1.1870 + let identifierCoords = { 1.1871 + line: identifierStart.line + scriptLineOffset, 1.1872 + column: identifierStart.column + scriptColumnOffset, 1.1873 + length: identifierEnd.column - identifierStart.column 1.1874 + }; 1.1875 + 1.1876 + // Evaluate the identifier in the current stack frame and show the 1.1877 + // results in a VariablesView inspection popup. 1.1878 + DebuggerController.StackFrames.evaluate(identifierInfo.evalString) 1.1879 + .then(frameFinished => { 1.1880 + if ("return" in frameFinished) { 1.1881 + this.showContents({ 1.1882 + coords: identifierCoords, 1.1883 + evalPrefix: identifierInfo.evalString, 1.1884 + objectActor: frameFinished.return 1.1885 + }); 1.1886 + } else { 1.1887 + let msg = "Evaluation has thrown for: " + identifierInfo.evalString; 1.1888 + console.warn(msg); 1.1889 + dumpn(msg); 1.1890 + } 1.1891 + }) 1.1892 + .then(null, err => { 1.1893 + let msg = "Couldn't evaluate: " + err.message; 1.1894 + console.error(msg); 1.1895 + dumpn(msg); 1.1896 + }); 1.1897 + }, 1.1898 + 1.1899 + /** 1.1900 + * Shows an inspection popup for a specified object actor grip. 1.1901 + * 1.1902 + * @param string object 1.1903 + * An object containing the following properties: 1.1904 + * - coords: the inspected identifier coordinates in the editor, 1.1905 + * containing the { line, column, length } properties. 1.1906 + * - evalPrefix: a prefix for the variables view evaluation macros. 1.1907 + * - objectActor: the value grip for the object actor. 1.1908 + */ 1.1909 + showContents: function({ coords, evalPrefix, objectActor }) { 1.1910 + let editor = DebuggerView.editor; 1.1911 + let { line, column, length } = coords; 1.1912 + 1.1913 + // Highlight the function found at the mouse position. 1.1914 + this._markedText = editor.markText( 1.1915 + { line: line - 1, ch: column }, 1.1916 + { line: line - 1, ch: column + length }); 1.1917 + 1.1918 + // If the grip represents a primitive value, use a more lightweight 1.1919 + // machinery to display it. 1.1920 + if (VariablesView.isPrimitive({ value: objectActor })) { 1.1921 + let className = VariablesView.getClass(objectActor); 1.1922 + let textContent = VariablesView.getString(objectActor); 1.1923 + this._tooltip.setTextContent({ 1.1924 + messages: [textContent], 1.1925 + messagesClass: className, 1.1926 + containerClass: "plain" 1.1927 + }, [{ 1.1928 + label: L10N.getStr('addWatchExpressionButton'), 1.1929 + className: "dbg-expression-button", 1.1930 + command: () => { 1.1931 + DebuggerView.VariableBubble.hideContents(); 1.1932 + DebuggerView.WatchExpressions.addExpression(evalPrefix, true); 1.1933 + } 1.1934 + }]); 1.1935 + } else { 1.1936 + this._tooltip.setVariableContent(objectActor, { 1.1937 + searchPlaceholder: L10N.getStr("emptyPropertiesFilterText"), 1.1938 + searchEnabled: Prefs.variablesSearchboxVisible, 1.1939 + eval: (variable, value) => { 1.1940 + let string = variable.evaluationMacro(variable, value); 1.1941 + DebuggerController.StackFrames.evaluate(string); 1.1942 + DebuggerView.VariableBubble.hideContents(); 1.1943 + } 1.1944 + }, { 1.1945 + getEnvironmentClient: aObject => gThreadClient.environment(aObject), 1.1946 + getObjectClient: aObject => gThreadClient.pauseGrip(aObject), 1.1947 + simpleValueEvalMacro: this._getSimpleValueEvalMacro(evalPrefix), 1.1948 + getterOrSetterEvalMacro: this._getGetterOrSetterEvalMacro(evalPrefix), 1.1949 + overrideValueEvalMacro: this._getOverrideValueEvalMacro(evalPrefix) 1.1950 + }, { 1.1951 + fetched: (aEvent, aType) => { 1.1952 + if (aType == "properties") { 1.1953 + window.emit(EVENTS.FETCHED_BUBBLE_PROPERTIES); 1.1954 + } 1.1955 + } 1.1956 + }, [{ 1.1957 + label: L10N.getStr("addWatchExpressionButton"), 1.1958 + className: "dbg-expression-button", 1.1959 + command: () => { 1.1960 + DebuggerView.VariableBubble.hideContents(); 1.1961 + DebuggerView.WatchExpressions.addExpression(evalPrefix, true); 1.1962 + } 1.1963 + }], DebuggerController._toolbox); 1.1964 + } 1.1965 + 1.1966 + this._tooltip.show(this._markedText.anchor); 1.1967 + }, 1.1968 + 1.1969 + /** 1.1970 + * Hides the inspection popup. 1.1971 + */ 1.1972 + hideContents: function() { 1.1973 + clearNamedTimeout("editor-mouse-move"); 1.1974 + this._tooltip.hide(); 1.1975 + }, 1.1976 + 1.1977 + /** 1.1978 + * Checks whether the inspection popup is shown. 1.1979 + * 1.1980 + * @return boolean 1.1981 + * True if the panel is shown or showing, false otherwise. 1.1982 + */ 1.1983 + contentsShown: function() { 1.1984 + return this._tooltip.isShown(); 1.1985 + }, 1.1986 + 1.1987 + /** 1.1988 + * Functions for getting customized variables view evaluation macros. 1.1989 + * 1.1990 + * @param string aPrefix 1.1991 + * See the corresponding VariablesView.* functions. 1.1992 + */ 1.1993 + _getSimpleValueEvalMacro: function(aPrefix) { 1.1994 + return (item, string) => 1.1995 + VariablesView.simpleValueEvalMacro(item, string, aPrefix); 1.1996 + }, 1.1997 + _getGetterOrSetterEvalMacro: function(aPrefix) { 1.1998 + return (item, string) => 1.1999 + VariablesView.getterOrSetterEvalMacro(item, string, aPrefix); 1.2000 + }, 1.2001 + _getOverrideValueEvalMacro: function(aPrefix) { 1.2002 + return (item, string) => 1.2003 + VariablesView.overrideValueEvalMacro(item, string, aPrefix); 1.2004 + }, 1.2005 + 1.2006 + /** 1.2007 + * The mousemove listener for the source editor. 1.2008 + */ 1.2009 + _onMouseMove: function({ clientX: x, clientY: y, buttons: btns }) { 1.2010 + // Prevent the variable inspection popup from showing when the thread client 1.2011 + // is not paused, or while a popup is already visible, or when the user tries 1.2012 + // to select text in the editor. 1.2013 + if (gThreadClient && gThreadClient.state != "paused" 1.2014 + || !this._tooltip.isHidden() 1.2015 + || (DebuggerView.editor.somethingSelected() 1.2016 + && btns > 0)) { 1.2017 + clearNamedTimeout("editor-mouse-move"); 1.2018 + return; 1.2019 + } 1.2020 + // Allow events to settle down first. If the mouse hovers over 1.2021 + // a certain point in the editor long enough, try showing a variable bubble. 1.2022 + setNamedTimeout("editor-mouse-move", 1.2023 + EDITOR_VARIABLE_HOVER_DELAY, () => this._findIdentifier(x, y)); 1.2024 + }, 1.2025 + 1.2026 + /** 1.2027 + * The mouseleave listener for the source editor container node. 1.2028 + */ 1.2029 + _onMouseLeave: function() { 1.2030 + clearNamedTimeout("editor-mouse-move"); 1.2031 + }, 1.2032 + 1.2033 + /** 1.2034 + * Listener handling the popup hiding event. 1.2035 + */ 1.2036 + _onPopupHiding: function({ target }) { 1.2037 + if (this._tooltip.panel != target) { 1.2038 + return; 1.2039 + } 1.2040 + if (this._markedText) { 1.2041 + this._markedText.clear(); 1.2042 + this._markedText = null; 1.2043 + } 1.2044 + if (!this._tooltip.isEmpty()) { 1.2045 + this._tooltip.empty(); 1.2046 + } 1.2047 + }, 1.2048 + 1.2049 + _editorContainer: null, 1.2050 + _markedText: null, 1.2051 + _tooltip: null 1.2052 +}; 1.2053 + 1.2054 +/** 1.2055 + * Functions handling the watch expressions UI. 1.2056 + */ 1.2057 +function WatchExpressionsView() { 1.2058 + dumpn("WatchExpressionsView was instantiated"); 1.2059 + 1.2060 + this.switchExpression = this.switchExpression.bind(this); 1.2061 + this.deleteExpression = this.deleteExpression.bind(this); 1.2062 + this._createItemView = this._createItemView.bind(this); 1.2063 + this._onClick = this._onClick.bind(this); 1.2064 + this._onClose = this._onClose.bind(this); 1.2065 + this._onBlur = this._onBlur.bind(this); 1.2066 + this._onKeyPress = this._onKeyPress.bind(this); 1.2067 +} 1.2068 + 1.2069 +WatchExpressionsView.prototype = Heritage.extend(WidgetMethods, { 1.2070 + /** 1.2071 + * Initialization function, called when the debugger is started. 1.2072 + */ 1.2073 + initialize: function() { 1.2074 + dumpn("Initializing the WatchExpressionsView"); 1.2075 + 1.2076 + this.widget = new SimpleListWidget(document.getElementById("expressions")); 1.2077 + this.widget.setAttribute("context", "debuggerWatchExpressionsContextMenu"); 1.2078 + this.widget.addEventListener("click", this._onClick, false); 1.2079 + 1.2080 + this.headerText = L10N.getStr("addWatchExpressionText"); 1.2081 + }, 1.2082 + 1.2083 + /** 1.2084 + * Destruction function, called when the debugger is closed. 1.2085 + */ 1.2086 + destroy: function() { 1.2087 + dumpn("Destroying the WatchExpressionsView"); 1.2088 + 1.2089 + this.widget.removeEventListener("click", this._onClick, false); 1.2090 + }, 1.2091 + 1.2092 + /** 1.2093 + * Adds a watch expression in this container. 1.2094 + * 1.2095 + * @param string aExpression [optional] 1.2096 + * An optional initial watch expression text. 1.2097 + * @param boolean aSkipUserInput [optional] 1.2098 + * Pass true to avoid waiting for additional user input 1.2099 + * on the watch expression. 1.2100 + */ 1.2101 + addExpression: function(aExpression = "", aSkipUserInput = false) { 1.2102 + // Watch expressions are UI elements which benefit from visible panes. 1.2103 + DebuggerView.showInstrumentsPane(); 1.2104 + 1.2105 + // Create the element node for the watch expression item. 1.2106 + let itemView = this._createItemView(aExpression); 1.2107 + 1.2108 + // Append a watch expression item to this container. 1.2109 + let expressionItem = this.push([itemView.container], { 1.2110 + index: 0, /* specifies on which position should the item be appended */ 1.2111 + attachment: { 1.2112 + view: itemView, 1.2113 + initialExpression: aExpression, 1.2114 + currentExpression: "", 1.2115 + } 1.2116 + }); 1.2117 + 1.2118 + // Automatically focus the new watch expression input 1.2119 + // if additional user input is desired. 1.2120 + if (!aSkipUserInput) { 1.2121 + expressionItem.attachment.view.inputNode.select(); 1.2122 + expressionItem.attachment.view.inputNode.focus(); 1.2123 + DebuggerView.Variables.parentNode.scrollTop = 0; 1.2124 + } 1.2125 + // Otherwise, add and evaluate the new watch expression immediately. 1.2126 + else { 1.2127 + this.toggleContents(false); 1.2128 + this._onBlur({ target: expressionItem.attachment.view.inputNode }); 1.2129 + } 1.2130 + }, 1.2131 + 1.2132 + /** 1.2133 + * Changes the watch expression corresponding to the specified variable item. 1.2134 + * This function is called whenever a watch expression's code is edited in 1.2135 + * the variables view container. 1.2136 + * 1.2137 + * @param Variable aVar 1.2138 + * The variable representing the watch expression evaluation. 1.2139 + * @param string aExpression 1.2140 + * The new watch expression text. 1.2141 + */ 1.2142 + switchExpression: function(aVar, aExpression) { 1.2143 + let expressionItem = 1.2144 + [i for (i of this) if (i.attachment.currentExpression == aVar.name)][0]; 1.2145 + 1.2146 + // Remove the watch expression if it's going to be empty or a duplicate. 1.2147 + if (!aExpression || this.getAllStrings().indexOf(aExpression) != -1) { 1.2148 + this.deleteExpression(aVar); 1.2149 + return; 1.2150 + } 1.2151 + 1.2152 + // Save the watch expression code string. 1.2153 + expressionItem.attachment.currentExpression = aExpression; 1.2154 + expressionItem.attachment.view.inputNode.value = aExpression; 1.2155 + 1.2156 + // Synchronize with the controller's watch expressions store. 1.2157 + DebuggerController.StackFrames.syncWatchExpressions(); 1.2158 + }, 1.2159 + 1.2160 + /** 1.2161 + * Removes the watch expression corresponding to the specified variable item. 1.2162 + * This function is called whenever a watch expression's value is edited in 1.2163 + * the variables view container. 1.2164 + * 1.2165 + * @param Variable aVar 1.2166 + * The variable representing the watch expression evaluation. 1.2167 + */ 1.2168 + deleteExpression: function(aVar) { 1.2169 + let expressionItem = 1.2170 + [i for (i of this) if (i.attachment.currentExpression == aVar.name)][0]; 1.2171 + 1.2172 + // Remove the watch expression. 1.2173 + this.remove(expressionItem); 1.2174 + 1.2175 + // Synchronize with the controller's watch expressions store. 1.2176 + DebuggerController.StackFrames.syncWatchExpressions(); 1.2177 + }, 1.2178 + 1.2179 + /** 1.2180 + * Gets the watch expression code string for an item in this container. 1.2181 + * 1.2182 + * @param number aIndex 1.2183 + * The index used to identify the watch expression. 1.2184 + * @return string 1.2185 + * The watch expression code string. 1.2186 + */ 1.2187 + getString: function(aIndex) { 1.2188 + return this.getItemAtIndex(aIndex).attachment.currentExpression; 1.2189 + }, 1.2190 + 1.2191 + /** 1.2192 + * Gets the watch expressions code strings for all items in this container. 1.2193 + * 1.2194 + * @return array 1.2195 + * The watch expressions code strings. 1.2196 + */ 1.2197 + getAllStrings: function() { 1.2198 + return this.items.map(e => e.attachment.currentExpression); 1.2199 + }, 1.2200 + 1.2201 + /** 1.2202 + * Customization function for creating an item's UI. 1.2203 + * 1.2204 + * @param string aExpression 1.2205 + * The watch expression string. 1.2206 + */ 1.2207 + _createItemView: function(aExpression) { 1.2208 + let container = document.createElement("hbox"); 1.2209 + container.className = "list-widget-item dbg-expression"; 1.2210 + 1.2211 + let arrowNode = document.createElement("hbox"); 1.2212 + arrowNode.className = "dbg-expression-arrow"; 1.2213 + 1.2214 + let inputNode = document.createElement("textbox"); 1.2215 + inputNode.className = "plain dbg-expression-input devtools-monospace"; 1.2216 + inputNode.setAttribute("value", aExpression); 1.2217 + inputNode.setAttribute("flex", "1"); 1.2218 + 1.2219 + let closeNode = document.createElement("toolbarbutton"); 1.2220 + closeNode.className = "plain variables-view-delete"; 1.2221 + 1.2222 + closeNode.addEventListener("click", this._onClose, false); 1.2223 + inputNode.addEventListener("blur", this._onBlur, false); 1.2224 + inputNode.addEventListener("keypress", this._onKeyPress, false); 1.2225 + 1.2226 + container.appendChild(arrowNode); 1.2227 + container.appendChild(inputNode); 1.2228 + container.appendChild(closeNode); 1.2229 + 1.2230 + return { 1.2231 + container: container, 1.2232 + arrowNode: arrowNode, 1.2233 + inputNode: inputNode, 1.2234 + closeNode: closeNode 1.2235 + }; 1.2236 + }, 1.2237 + 1.2238 + /** 1.2239 + * Called when the add watch expression key sequence was pressed. 1.2240 + */ 1.2241 + _onCmdAddExpression: function(aText) { 1.2242 + // Only add a new expression if there's no pending input. 1.2243 + if (this.getAllStrings().indexOf("") == -1) { 1.2244 + this.addExpression(aText || DebuggerView.editor.getSelection()); 1.2245 + } 1.2246 + }, 1.2247 + 1.2248 + /** 1.2249 + * Called when the remove all watch expressions key sequence was pressed. 1.2250 + */ 1.2251 + _onCmdRemoveAllExpressions: function() { 1.2252 + // Empty the view of all the watch expressions and clear the cache. 1.2253 + this.empty(); 1.2254 + 1.2255 + // Synchronize with the controller's watch expressions store. 1.2256 + DebuggerController.StackFrames.syncWatchExpressions(); 1.2257 + }, 1.2258 + 1.2259 + /** 1.2260 + * The click listener for this container. 1.2261 + */ 1.2262 + _onClick: function(e) { 1.2263 + if (e.button != 0) { 1.2264 + // Only allow left-click to trigger this event. 1.2265 + return; 1.2266 + } 1.2267 + let expressionItem = this.getItemForElement(e.target); 1.2268 + if (!expressionItem) { 1.2269 + // The container is empty or we didn't click on an actual item. 1.2270 + this.addExpression(); 1.2271 + } 1.2272 + }, 1.2273 + 1.2274 + /** 1.2275 + * The click listener for a watch expression's close button. 1.2276 + */ 1.2277 + _onClose: function(e) { 1.2278 + // Remove the watch expression. 1.2279 + this.remove(this.getItemForElement(e.target)); 1.2280 + 1.2281 + // Synchronize with the controller's watch expressions store. 1.2282 + DebuggerController.StackFrames.syncWatchExpressions(); 1.2283 + 1.2284 + // Prevent clicking the expression element itself. 1.2285 + e.preventDefault(); 1.2286 + e.stopPropagation(); 1.2287 + }, 1.2288 + 1.2289 + /** 1.2290 + * The blur listener for a watch expression's textbox. 1.2291 + */ 1.2292 + _onBlur: function({ target: textbox }) { 1.2293 + let expressionItem = this.getItemForElement(textbox); 1.2294 + let oldExpression = expressionItem.attachment.currentExpression; 1.2295 + let newExpression = textbox.value.trim(); 1.2296 + 1.2297 + // Remove the watch expression if it's empty. 1.2298 + if (!newExpression) { 1.2299 + this.remove(expressionItem); 1.2300 + } 1.2301 + // Remove the watch expression if it's a duplicate. 1.2302 + else if (!oldExpression && this.getAllStrings().indexOf(newExpression) != -1) { 1.2303 + this.remove(expressionItem); 1.2304 + } 1.2305 + // Expression is eligible. 1.2306 + else { 1.2307 + expressionItem.attachment.currentExpression = newExpression; 1.2308 + } 1.2309 + 1.2310 + // Synchronize with the controller's watch expressions store. 1.2311 + DebuggerController.StackFrames.syncWatchExpressions(); 1.2312 + }, 1.2313 + 1.2314 + /** 1.2315 + * The keypress listener for a watch expression's textbox. 1.2316 + */ 1.2317 + _onKeyPress: function(e) { 1.2318 + switch(e.keyCode) { 1.2319 + case e.DOM_VK_RETURN: 1.2320 + case e.DOM_VK_ESCAPE: 1.2321 + e.stopPropagation(); 1.2322 + DebuggerView.editor.focus(); 1.2323 + return; 1.2324 + } 1.2325 + } 1.2326 +}); 1.2327 + 1.2328 +/** 1.2329 + * Functions handling the event listeners UI. 1.2330 + */ 1.2331 +function EventListenersView() { 1.2332 + dumpn("EventListenersView was instantiated"); 1.2333 + 1.2334 + this._onCheck = this._onCheck.bind(this); 1.2335 + this._onClick = this._onClick.bind(this); 1.2336 +} 1.2337 + 1.2338 +EventListenersView.prototype = Heritage.extend(WidgetMethods, { 1.2339 + /** 1.2340 + * Initialization function, called when the debugger is started. 1.2341 + */ 1.2342 + initialize: function() { 1.2343 + dumpn("Initializing the EventListenersView"); 1.2344 + 1.2345 + this.widget = new SideMenuWidget(document.getElementById("event-listeners"), { 1.2346 + showItemCheckboxes: true, 1.2347 + showGroupCheckboxes: true 1.2348 + }); 1.2349 + 1.2350 + this.emptyText = L10N.getStr("noEventListenersText"); 1.2351 + this._eventCheckboxTooltip = L10N.getStr("eventCheckboxTooltip"); 1.2352 + this._onSelectorString = " " + L10N.getStr("eventOnSelector") + " "; 1.2353 + this._inSourceString = " " + L10N.getStr("eventInSource") + " "; 1.2354 + this._inNativeCodeString = L10N.getStr("eventNative"); 1.2355 + 1.2356 + this.widget.addEventListener("check", this._onCheck, false); 1.2357 + this.widget.addEventListener("click", this._onClick, false); 1.2358 + }, 1.2359 + 1.2360 + /** 1.2361 + * Destruction function, called when the debugger is closed. 1.2362 + */ 1.2363 + destroy: function() { 1.2364 + dumpn("Destroying the EventListenersView"); 1.2365 + 1.2366 + this.widget.removeEventListener("check", this._onCheck, false); 1.2367 + this.widget.removeEventListener("click", this._onClick, false); 1.2368 + }, 1.2369 + 1.2370 + /** 1.2371 + * Adds an event to this event listeners container. 1.2372 + * 1.2373 + * @param object aListener 1.2374 + * The listener object coming from the active thread. 1.2375 + * @param object aOptions [optional] 1.2376 + * Additional options for adding the source. Supported options: 1.2377 + * - staged: true to stage the item to be appended later 1.2378 + */ 1.2379 + addListener: function(aListener, aOptions = {}) { 1.2380 + let { node: { selector }, function: { url }, type } = aListener; 1.2381 + if (!type) return; 1.2382 + 1.2383 + // Some listener objects may be added from plugins, thus getting 1.2384 + // translated to native code. 1.2385 + if (!url) { 1.2386 + url = this._inNativeCodeString; 1.2387 + } 1.2388 + 1.2389 + // If an event item for this listener's url and type was already added, 1.2390 + // avoid polluting the view and simply increase the "targets" count. 1.2391 + let eventItem = this.getItemForPredicate(aItem => 1.2392 + aItem.attachment.url == url && 1.2393 + aItem.attachment.type == type); 1.2394 + if (eventItem) { 1.2395 + let { selectors, view: { targets } } = eventItem.attachment; 1.2396 + if (selectors.indexOf(selector) == -1) { 1.2397 + selectors.push(selector); 1.2398 + targets.setAttribute("value", L10N.getFormatStr("eventNodes", selectors.length)); 1.2399 + } 1.2400 + return; 1.2401 + } 1.2402 + 1.2403 + // There's no easy way of grouping event types into higher-level groups, 1.2404 + // so we need to do this by hand. 1.2405 + let is = (...args) => args.indexOf(type) != -1; 1.2406 + let has = str => type.contains(str); 1.2407 + let starts = str => type.startsWith(str); 1.2408 + let group; 1.2409 + 1.2410 + if (starts("animation")) { 1.2411 + group = L10N.getStr("animationEvents"); 1.2412 + } else if (starts("audio")) { 1.2413 + group = L10N.getStr("audioEvents"); 1.2414 + } else if (is("levelchange")) { 1.2415 + group = L10N.getStr("batteryEvents"); 1.2416 + } else if (is("cut", "copy", "paste")) { 1.2417 + group = L10N.getStr("clipboardEvents"); 1.2418 + } else if (starts("composition")) { 1.2419 + group = L10N.getStr("compositionEvents"); 1.2420 + } else if (starts("device")) { 1.2421 + group = L10N.getStr("deviceEvents"); 1.2422 + } else if (is("fullscreenchange", "fullscreenerror", "orientationchange", 1.2423 + "overflow", "resize", "scroll", "underflow", "zoom")) { 1.2424 + group = L10N.getStr("displayEvents"); 1.2425 + } else if (starts("drag") || starts("drop")) { 1.2426 + group = L10N.getStr("Drag and dropEvents"); 1.2427 + } else if (starts("gamepad")) { 1.2428 + group = L10N.getStr("gamepadEvents"); 1.2429 + } else if (is("canplay", "canplaythrough", "durationchange", "emptied", 1.2430 + "ended", "loadeddata", "loadedmetadata", "pause", "play", "playing", 1.2431 + "ratechange", "seeked", "seeking", "stalled", "suspend", "timeupdate", 1.2432 + "volumechange", "waiting")) { 1.2433 + group = L10N.getStr("mediaEvents"); 1.2434 + } else if (is("blocked", "complete", "success", "upgradeneeded", "versionchange")) { 1.2435 + group = L10N.getStr("indexedDBEvents"); 1.2436 + } else if (is("blur", "change", "focus", "focusin", "focusout", "invalid", 1.2437 + "reset", "select", "submit")) { 1.2438 + group = L10N.getStr("interactionEvents"); 1.2439 + } else if (starts("key") || is("input")) { 1.2440 + group = L10N.getStr("keyboardEvents"); 1.2441 + } else if (starts("mouse") || has("click") || is("contextmenu", "show", "wheel")) { 1.2442 + group = L10N.getStr("mouseEvents"); 1.2443 + } else if (starts("DOM")) { 1.2444 + group = L10N.getStr("mutationEvents"); 1.2445 + } else if (is("abort", "error", "hashchange", "load", "loadend", "loadstart", 1.2446 + "pagehide", "pageshow", "progress", "timeout", "unload", "uploadprogress", 1.2447 + "visibilitychange")) { 1.2448 + group = L10N.getStr("navigationEvents"); 1.2449 + } else if (is("pointerlockchange", "pointerlockerror")) { 1.2450 + group = L10N.getStr("Pointer lockEvents"); 1.2451 + } else if (is("compassneedscalibration", "userproximity")) { 1.2452 + group = L10N.getStr("sensorEvents"); 1.2453 + } else if (starts("storage")) { 1.2454 + group = L10N.getStr("storageEvents"); 1.2455 + } else if (is("beginEvent", "endEvent", "repeatEvent")) { 1.2456 + group = L10N.getStr("timeEvents"); 1.2457 + } else if (starts("touch")) { 1.2458 + group = L10N.getStr("touchEvents"); 1.2459 + } else { 1.2460 + group = L10N.getStr("otherEvents"); 1.2461 + } 1.2462 + 1.2463 + // Create the element node for the event listener item. 1.2464 + let itemView = this._createItemView(type, selector, url); 1.2465 + 1.2466 + // Event breakpoints survive target navigations. Make sure the newly 1.2467 + // inserted event item is correctly checked. 1.2468 + let checkboxState = 1.2469 + DebuggerController.Breakpoints.DOM.activeEventNames.indexOf(type) != -1; 1.2470 + 1.2471 + // Append an event listener item to this container. 1.2472 + this.push([itemView.container], { 1.2473 + staged: aOptions.staged, /* stage the item to be appended later? */ 1.2474 + attachment: { 1.2475 + url: url, 1.2476 + type: type, 1.2477 + view: itemView, 1.2478 + selectors: [selector], 1.2479 + group: group, 1.2480 + checkboxState: checkboxState, 1.2481 + checkboxTooltip: this._eventCheckboxTooltip 1.2482 + } 1.2483 + }); 1.2484 + }, 1.2485 + 1.2486 + /** 1.2487 + * Gets all the event types known to this container. 1.2488 + * 1.2489 + * @return array 1.2490 + * List of event types, for example ["load", "click"...] 1.2491 + */ 1.2492 + getAllEvents: function() { 1.2493 + return this.attachments.map(e => e.type); 1.2494 + }, 1.2495 + 1.2496 + /** 1.2497 + * Gets the checked event types in this container. 1.2498 + * 1.2499 + * @return array 1.2500 + * List of event types, for example ["load", "click"...] 1.2501 + */ 1.2502 + getCheckedEvents: function() { 1.2503 + return this.attachments.filter(e => e.checkboxState).map(e => e.type); 1.2504 + }, 1.2505 + 1.2506 + /** 1.2507 + * Customization function for creating an item's UI. 1.2508 + * 1.2509 + * @param string aType 1.2510 + * The event type, for example "click". 1.2511 + * @param string aSelector 1.2512 + * The target element's selector. 1.2513 + * @param string url 1.2514 + * The source url in which the event listener is located. 1.2515 + * @return object 1.2516 + * An object containing the event listener view nodes. 1.2517 + */ 1.2518 + _createItemView: function(aType, aSelector, aUrl) { 1.2519 + let container = document.createElement("hbox"); 1.2520 + container.className = "dbg-event-listener"; 1.2521 + 1.2522 + let eventType = document.createElement("label"); 1.2523 + eventType.className = "plain dbg-event-listener-type"; 1.2524 + eventType.setAttribute("value", aType); 1.2525 + container.appendChild(eventType); 1.2526 + 1.2527 + let typeSeparator = document.createElement("label"); 1.2528 + typeSeparator.className = "plain dbg-event-listener-separator"; 1.2529 + typeSeparator.setAttribute("value", this._onSelectorString); 1.2530 + container.appendChild(typeSeparator); 1.2531 + 1.2532 + let eventTargets = document.createElement("label"); 1.2533 + eventTargets.className = "plain dbg-event-listener-targets"; 1.2534 + eventTargets.setAttribute("value", aSelector); 1.2535 + container.appendChild(eventTargets); 1.2536 + 1.2537 + let selectorSeparator = document.createElement("label"); 1.2538 + selectorSeparator.className = "plain dbg-event-listener-separator"; 1.2539 + selectorSeparator.setAttribute("value", this._inSourceString); 1.2540 + container.appendChild(selectorSeparator); 1.2541 + 1.2542 + let eventLocation = document.createElement("label"); 1.2543 + eventLocation.className = "plain dbg-event-listener-location"; 1.2544 + eventLocation.setAttribute("value", SourceUtils.getSourceLabel(aUrl)); 1.2545 + eventLocation.setAttribute("flex", "1"); 1.2546 + eventLocation.setAttribute("crop", "center"); 1.2547 + container.appendChild(eventLocation); 1.2548 + 1.2549 + return { 1.2550 + container: container, 1.2551 + type: eventType, 1.2552 + targets: eventTargets, 1.2553 + location: eventLocation 1.2554 + }; 1.2555 + }, 1.2556 + 1.2557 + /** 1.2558 + * The check listener for the event listeners container. 1.2559 + */ 1.2560 + _onCheck: function({ detail: { description, checked }, target }) { 1.2561 + if (description == "item") { 1.2562 + this.getItemForElement(target).attachment.checkboxState = checked; 1.2563 + DebuggerController.Breakpoints.DOM.scheduleEventBreakpointsUpdate(); 1.2564 + return; 1.2565 + } 1.2566 + 1.2567 + // Check all the event items in this group. 1.2568 + this.items 1.2569 + .filter(e => e.attachment.group == description) 1.2570 + .forEach(e => this.callMethod("checkItem", e.target, checked)); 1.2571 + }, 1.2572 + 1.2573 + /** 1.2574 + * The select listener for the event listeners container. 1.2575 + */ 1.2576 + _onClick: function({ target }) { 1.2577 + // Changing the checkbox state is handled by the _onCheck event. Avoid 1.2578 + // handling that again in this click event, so pass in "noSiblings" 1.2579 + // when retrieving the target's item, to ignore the checkbox. 1.2580 + let eventItem = this.getItemForElement(target, { noSiblings: true }); 1.2581 + if (eventItem) { 1.2582 + let newState = eventItem.attachment.checkboxState ^= 1; 1.2583 + this.callMethod("checkItem", eventItem.target, newState); 1.2584 + } 1.2585 + }, 1.2586 + 1.2587 + _eventCheckboxTooltip: "", 1.2588 + _onSelectorString: "", 1.2589 + _inSourceString: "", 1.2590 + _inNativeCodeString: "" 1.2591 +}); 1.2592 + 1.2593 +/** 1.2594 + * Functions handling the global search UI. 1.2595 + */ 1.2596 +function GlobalSearchView() { 1.2597 + dumpn("GlobalSearchView was instantiated"); 1.2598 + 1.2599 + this._onHeaderClick = this._onHeaderClick.bind(this); 1.2600 + this._onLineClick = this._onLineClick.bind(this); 1.2601 + this._onMatchClick = this._onMatchClick.bind(this); 1.2602 +} 1.2603 + 1.2604 +GlobalSearchView.prototype = Heritage.extend(WidgetMethods, { 1.2605 + /** 1.2606 + * Initialization function, called when the debugger is started. 1.2607 + */ 1.2608 + initialize: function() { 1.2609 + dumpn("Initializing the GlobalSearchView"); 1.2610 + 1.2611 + this.widget = new SimpleListWidget(document.getElementById("globalsearch")); 1.2612 + this._splitter = document.querySelector("#globalsearch + .devtools-horizontal-splitter"); 1.2613 + 1.2614 + this.emptyText = L10N.getStr("noMatchingStringsText"); 1.2615 + }, 1.2616 + 1.2617 + /** 1.2618 + * Destruction function, called when the debugger is closed. 1.2619 + */ 1.2620 + destroy: function() { 1.2621 + dumpn("Destroying the GlobalSearchView"); 1.2622 + }, 1.2623 + 1.2624 + /** 1.2625 + * Sets the results container hidden or visible. It's hidden by default. 1.2626 + * @param boolean aFlag 1.2627 + */ 1.2628 + set hidden(aFlag) { 1.2629 + this.widget.setAttribute("hidden", aFlag); 1.2630 + this._splitter.setAttribute("hidden", aFlag); 1.2631 + }, 1.2632 + 1.2633 + /** 1.2634 + * Gets the visibility state of the global search container. 1.2635 + * @return boolean 1.2636 + */ 1.2637 + get hidden() 1.2638 + this.widget.getAttribute("hidden") == "true" || 1.2639 + this._splitter.getAttribute("hidden") == "true", 1.2640 + 1.2641 + /** 1.2642 + * Hides and removes all items from this search container. 1.2643 + */ 1.2644 + clearView: function() { 1.2645 + this.hidden = true; 1.2646 + this.empty(); 1.2647 + }, 1.2648 + 1.2649 + /** 1.2650 + * Selects the next found item in this container. 1.2651 + * Does not change the currently focused node. 1.2652 + */ 1.2653 + selectNext: function() { 1.2654 + let totalLineResults = LineResults.size(); 1.2655 + if (!totalLineResults) { 1.2656 + return; 1.2657 + } 1.2658 + if (++this._currentlyFocusedMatch >= totalLineResults) { 1.2659 + this._currentlyFocusedMatch = 0; 1.2660 + } 1.2661 + this._onMatchClick({ 1.2662 + target: LineResults.getElementAtIndex(this._currentlyFocusedMatch) 1.2663 + }); 1.2664 + }, 1.2665 + 1.2666 + /** 1.2667 + * Selects the previously found item in this container. 1.2668 + * Does not change the currently focused node. 1.2669 + */ 1.2670 + selectPrev: function() { 1.2671 + let totalLineResults = LineResults.size(); 1.2672 + if (!totalLineResults) { 1.2673 + return; 1.2674 + } 1.2675 + if (--this._currentlyFocusedMatch < 0) { 1.2676 + this._currentlyFocusedMatch = totalLineResults - 1; 1.2677 + } 1.2678 + this._onMatchClick({ 1.2679 + target: LineResults.getElementAtIndex(this._currentlyFocusedMatch) 1.2680 + }); 1.2681 + }, 1.2682 + 1.2683 + /** 1.2684 + * Schedules searching for a string in all of the sources. 1.2685 + * 1.2686 + * @param string aToken 1.2687 + * The string to search for. 1.2688 + * @param number aWait 1.2689 + * The amount of milliseconds to wait until draining. 1.2690 + */ 1.2691 + scheduleSearch: function(aToken, aWait) { 1.2692 + // The amount of time to wait for the requests to settle. 1.2693 + let maxDelay = GLOBAL_SEARCH_ACTION_MAX_DELAY; 1.2694 + let delay = aWait === undefined ? maxDelay / aToken.length : aWait; 1.2695 + 1.2696 + // Allow requests to settle down first. 1.2697 + setNamedTimeout("global-search", delay, () => { 1.2698 + // Start fetching as many sources as possible, then perform the search. 1.2699 + let urls = DebuggerView.Sources.values; 1.2700 + let sourcesFetched = DebuggerController.SourceScripts.getTextForSources(urls); 1.2701 + sourcesFetched.then(aSources => this._doSearch(aToken, aSources)); 1.2702 + }); 1.2703 + }, 1.2704 + 1.2705 + /** 1.2706 + * Finds string matches in all the sources stored in the controller's cache, 1.2707 + * and groups them by url and line number. 1.2708 + * 1.2709 + * @param string aToken 1.2710 + * The string to search for. 1.2711 + * @param array aSources 1.2712 + * An array of [url, text] tuples for each source. 1.2713 + */ 1.2714 + _doSearch: function(aToken, aSources) { 1.2715 + // Don't continue filtering if the searched token is an empty string. 1.2716 + if (!aToken) { 1.2717 + this.clearView(); 1.2718 + return; 1.2719 + } 1.2720 + 1.2721 + // Search is not case sensitive, prepare the actual searched token. 1.2722 + let lowerCaseToken = aToken.toLowerCase(); 1.2723 + let tokenLength = aToken.length; 1.2724 + 1.2725 + // Create a Map containing search details for each source. 1.2726 + let globalResults = new GlobalResults(); 1.2727 + 1.2728 + // Search for the specified token in each source's text. 1.2729 + for (let [url, text] of aSources) { 1.2730 + // Verify that the search token is found anywhere in the source. 1.2731 + if (!text.toLowerCase().contains(lowerCaseToken)) { 1.2732 + continue; 1.2733 + } 1.2734 + // ...and if so, create a Map containing search details for each line. 1.2735 + let sourceResults = new SourceResults(url, globalResults); 1.2736 + 1.2737 + // Search for the specified token in each line's text. 1.2738 + text.split("\n").forEach((aString, aLine) => { 1.2739 + // Search is not case sensitive, prepare the actual searched line. 1.2740 + let lowerCaseLine = aString.toLowerCase(); 1.2741 + 1.2742 + // Verify that the search token is found anywhere in this line. 1.2743 + if (!lowerCaseLine.contains(lowerCaseToken)) { 1.2744 + return; 1.2745 + } 1.2746 + // ...and if so, create a Map containing search details for each word. 1.2747 + let lineResults = new LineResults(aLine, sourceResults); 1.2748 + 1.2749 + // Search for the specified token this line's text. 1.2750 + lowerCaseLine.split(lowerCaseToken).reduce((aPrev, aCurr, aIndex, aArray) => { 1.2751 + let prevLength = aPrev.length; 1.2752 + let currLength = aCurr.length; 1.2753 + 1.2754 + // Everything before the token is unmatched. 1.2755 + let unmatched = aString.substr(prevLength, currLength); 1.2756 + lineResults.add(unmatched); 1.2757 + 1.2758 + // The lowered-case line was split by the lowered-case token. So, 1.2759 + // get the actual matched text from the original line's text. 1.2760 + if (aIndex != aArray.length - 1) { 1.2761 + let matched = aString.substr(prevLength + currLength, tokenLength); 1.2762 + let range = { start: prevLength + currLength, length: matched.length }; 1.2763 + lineResults.add(matched, range, true); 1.2764 + } 1.2765 + 1.2766 + // Continue with the next sub-region in this line's text. 1.2767 + return aPrev + aToken + aCurr; 1.2768 + }, ""); 1.2769 + 1.2770 + if (lineResults.matchCount) { 1.2771 + sourceResults.add(lineResults); 1.2772 + } 1.2773 + }); 1.2774 + 1.2775 + if (sourceResults.matchCount) { 1.2776 + globalResults.add(sourceResults); 1.2777 + } 1.2778 + } 1.2779 + 1.2780 + // Rebuild the results, then signal if there are any matches. 1.2781 + if (globalResults.matchCount) { 1.2782 + this.hidden = false; 1.2783 + this._currentlyFocusedMatch = -1; 1.2784 + this._createGlobalResultsUI(globalResults); 1.2785 + window.emit(EVENTS.GLOBAL_SEARCH_MATCH_FOUND); 1.2786 + } else { 1.2787 + window.emit(EVENTS.GLOBAL_SEARCH_MATCH_NOT_FOUND); 1.2788 + } 1.2789 + }, 1.2790 + 1.2791 + /** 1.2792 + * Creates global search results entries and adds them to this container. 1.2793 + * 1.2794 + * @param GlobalResults aGlobalResults 1.2795 + * An object containing all source results, grouped by source location. 1.2796 + */ 1.2797 + _createGlobalResultsUI: function(aGlobalResults) { 1.2798 + let i = 0; 1.2799 + 1.2800 + for (let sourceResults of aGlobalResults) { 1.2801 + if (i++ == 0) { 1.2802 + this._createSourceResultsUI(sourceResults); 1.2803 + } else { 1.2804 + // Dispatch subsequent document manipulation operations, to avoid 1.2805 + // blocking the main thread when a large number of search results 1.2806 + // is found, thus giving the impression of faster searching. 1.2807 + Services.tm.currentThread.dispatch({ run: 1.2808 + this._createSourceResultsUI.bind(this, sourceResults) 1.2809 + }, 0); 1.2810 + } 1.2811 + } 1.2812 + }, 1.2813 + 1.2814 + /** 1.2815 + * Creates source search results entries and adds them to this container. 1.2816 + * 1.2817 + * @param SourceResults aSourceResults 1.2818 + * An object containing all the matched lines for a specific source. 1.2819 + */ 1.2820 + _createSourceResultsUI: function(aSourceResults) { 1.2821 + // Create the element node for the source results item. 1.2822 + let container = document.createElement("hbox"); 1.2823 + aSourceResults.createView(container, { 1.2824 + onHeaderClick: this._onHeaderClick, 1.2825 + onLineClick: this._onLineClick, 1.2826 + onMatchClick: this._onMatchClick 1.2827 + }); 1.2828 + 1.2829 + // Append a source results item to this container. 1.2830 + let item = this.push([container], { 1.2831 + index: -1, /* specifies on which position should the item be appended */ 1.2832 + attachment: { 1.2833 + sourceResults: aSourceResults 1.2834 + } 1.2835 + }); 1.2836 + }, 1.2837 + 1.2838 + /** 1.2839 + * The click listener for a results header. 1.2840 + */ 1.2841 + _onHeaderClick: function(e) { 1.2842 + let sourceResultsItem = SourceResults.getItemForElement(e.target); 1.2843 + sourceResultsItem.instance.toggle(e); 1.2844 + }, 1.2845 + 1.2846 + /** 1.2847 + * The click listener for a results line. 1.2848 + */ 1.2849 + _onLineClick: function(e) { 1.2850 + let lineResultsItem = LineResults.getItemForElement(e.target); 1.2851 + this._onMatchClick({ target: lineResultsItem.firstMatch }); 1.2852 + }, 1.2853 + 1.2854 + /** 1.2855 + * The click listener for a result match. 1.2856 + */ 1.2857 + _onMatchClick: function(e) { 1.2858 + if (e instanceof Event) { 1.2859 + e.preventDefault(); 1.2860 + e.stopPropagation(); 1.2861 + } 1.2862 + 1.2863 + let target = e.target; 1.2864 + let sourceResultsItem = SourceResults.getItemForElement(target); 1.2865 + let lineResultsItem = LineResults.getItemForElement(target); 1.2866 + 1.2867 + sourceResultsItem.instance.expand(); 1.2868 + this._currentlyFocusedMatch = LineResults.indexOfElement(target); 1.2869 + this._scrollMatchIntoViewIfNeeded(target); 1.2870 + this._bounceMatch(target); 1.2871 + 1.2872 + let url = sourceResultsItem.instance.url; 1.2873 + let line = lineResultsItem.instance.line; 1.2874 + 1.2875 + DebuggerView.setEditorLocation(url, line + 1, { noDebug: true }); 1.2876 + 1.2877 + let range = lineResultsItem.lineData.range; 1.2878 + let cursor = DebuggerView.editor.getOffset({ line: line, ch: 0 }); 1.2879 + let [ anchor, head ] = DebuggerView.editor.getPosition( 1.2880 + cursor + range.start, 1.2881 + cursor + range.start + range.length 1.2882 + ); 1.2883 + 1.2884 + DebuggerView.editor.setSelection(anchor, head); 1.2885 + }, 1.2886 + 1.2887 + /** 1.2888 + * Scrolls a match into view if not already visible. 1.2889 + * 1.2890 + * @param nsIDOMNode aMatch 1.2891 + * The match to scroll into view. 1.2892 + */ 1.2893 + _scrollMatchIntoViewIfNeeded: function(aMatch) { 1.2894 + this.widget.ensureElementIsVisible(aMatch); 1.2895 + }, 1.2896 + 1.2897 + /** 1.2898 + * Starts a bounce animation for a match. 1.2899 + * 1.2900 + * @param nsIDOMNode aMatch 1.2901 + * The match to start a bounce animation for. 1.2902 + */ 1.2903 + _bounceMatch: function(aMatch) { 1.2904 + Services.tm.currentThread.dispatch({ run: () => { 1.2905 + aMatch.addEventListener("transitionend", function onEvent() { 1.2906 + aMatch.removeEventListener("transitionend", onEvent); 1.2907 + aMatch.removeAttribute("focused"); 1.2908 + }); 1.2909 + aMatch.setAttribute("focused", ""); 1.2910 + }}, 0); 1.2911 + aMatch.setAttribute("focusing", ""); 1.2912 + }, 1.2913 + 1.2914 + _splitter: null, 1.2915 + _currentlyFocusedMatch: -1, 1.2916 + _forceExpandResults: false 1.2917 +}); 1.2918 + 1.2919 +/** 1.2920 + * An object containing all source results, grouped by source location. 1.2921 + * Iterable via "for (let [location, sourceResults] of globalResults) { }". 1.2922 + */ 1.2923 +function GlobalResults() { 1.2924 + this._store = []; 1.2925 + SourceResults._itemsByElement = new Map(); 1.2926 + LineResults._itemsByElement = new Map(); 1.2927 +} 1.2928 + 1.2929 +GlobalResults.prototype = { 1.2930 + /** 1.2931 + * Adds source results to this store. 1.2932 + * 1.2933 + * @param SourceResults aSourceResults 1.2934 + * An object containing search results for a specific source. 1.2935 + */ 1.2936 + add: function(aSourceResults) { 1.2937 + this._store.push(aSourceResults); 1.2938 + }, 1.2939 + 1.2940 + /** 1.2941 + * Gets the number of source results in this store. 1.2942 + */ 1.2943 + get matchCount() this._store.length 1.2944 +}; 1.2945 + 1.2946 +/** 1.2947 + * An object containing all the matched lines for a specific source. 1.2948 + * Iterable via "for (let [lineNumber, lineResults] of sourceResults) { }". 1.2949 + * 1.2950 + * @param string aUrl 1.2951 + * The target source url. 1.2952 + * @param GlobalResults aGlobalResults 1.2953 + * An object containing all source results, grouped by source location. 1.2954 + */ 1.2955 +function SourceResults(aUrl, aGlobalResults) { 1.2956 + this.url = aUrl; 1.2957 + this._globalResults = aGlobalResults; 1.2958 + this._store = []; 1.2959 +} 1.2960 + 1.2961 +SourceResults.prototype = { 1.2962 + /** 1.2963 + * Adds line results to this store. 1.2964 + * 1.2965 + * @param LineResults aLineResults 1.2966 + * An object containing search results for a specific line. 1.2967 + */ 1.2968 + add: function(aLineResults) { 1.2969 + this._store.push(aLineResults); 1.2970 + }, 1.2971 + 1.2972 + /** 1.2973 + * Gets the number of line results in this store. 1.2974 + */ 1.2975 + get matchCount() this._store.length, 1.2976 + 1.2977 + /** 1.2978 + * Expands the element, showing all the added details. 1.2979 + */ 1.2980 + expand: function() { 1.2981 + this._resultsContainer.removeAttribute("hidden"); 1.2982 + this._arrow.setAttribute("open", ""); 1.2983 + }, 1.2984 + 1.2985 + /** 1.2986 + * Collapses the element, hiding all the added details. 1.2987 + */ 1.2988 + collapse: function() { 1.2989 + this._resultsContainer.setAttribute("hidden", "true"); 1.2990 + this._arrow.removeAttribute("open"); 1.2991 + }, 1.2992 + 1.2993 + /** 1.2994 + * Toggles between the element collapse/expand state. 1.2995 + */ 1.2996 + toggle: function(e) { 1.2997 + this.expanded ^= 1; 1.2998 + }, 1.2999 + 1.3000 + /** 1.3001 + * Gets this element's expanded state. 1.3002 + * @return boolean 1.3003 + */ 1.3004 + get expanded() 1.3005 + this._resultsContainer.getAttribute("hidden") != "true" && 1.3006 + this._arrow.hasAttribute("open"), 1.3007 + 1.3008 + /** 1.3009 + * Sets this element's expanded state. 1.3010 + * @param boolean aFlag 1.3011 + */ 1.3012 + set expanded(aFlag) this[aFlag ? "expand" : "collapse"](), 1.3013 + 1.3014 + /** 1.3015 + * Gets the element associated with this item. 1.3016 + * @return nsIDOMNode 1.3017 + */ 1.3018 + get target() this._target, 1.3019 + 1.3020 + /** 1.3021 + * Customization function for creating this item's UI. 1.3022 + * 1.3023 + * @param nsIDOMNode aElementNode 1.3024 + * The element associated with the displayed item. 1.3025 + * @param object aCallbacks 1.3026 + * An object containing all the necessary callback functions: 1.3027 + * - onHeaderClick 1.3028 + * - onMatchClick 1.3029 + */ 1.3030 + createView: function(aElementNode, aCallbacks) { 1.3031 + this._target = aElementNode; 1.3032 + 1.3033 + let arrow = this._arrow = document.createElement("box"); 1.3034 + arrow.className = "arrow"; 1.3035 + 1.3036 + let locationNode = document.createElement("label"); 1.3037 + locationNode.className = "plain dbg-results-header-location"; 1.3038 + locationNode.setAttribute("value", this.url); 1.3039 + 1.3040 + let matchCountNode = document.createElement("label"); 1.3041 + matchCountNode.className = "plain dbg-results-header-match-count"; 1.3042 + matchCountNode.setAttribute("value", "(" + this.matchCount + ")"); 1.3043 + 1.3044 + let resultsHeader = this._resultsHeader = document.createElement("hbox"); 1.3045 + resultsHeader.className = "dbg-results-header"; 1.3046 + resultsHeader.setAttribute("align", "center") 1.3047 + resultsHeader.appendChild(arrow); 1.3048 + resultsHeader.appendChild(locationNode); 1.3049 + resultsHeader.appendChild(matchCountNode); 1.3050 + resultsHeader.addEventListener("click", aCallbacks.onHeaderClick, false); 1.3051 + 1.3052 + let resultsContainer = this._resultsContainer = document.createElement("vbox"); 1.3053 + resultsContainer.className = "dbg-results-container"; 1.3054 + resultsContainer.setAttribute("hidden", "true"); 1.3055 + 1.3056 + // Create lines search results entries and add them to this container. 1.3057 + // Afterwards, if the number of matches is reasonable, expand this 1.3058 + // container automatically. 1.3059 + for (let lineResults of this._store) { 1.3060 + lineResults.createView(resultsContainer, aCallbacks); 1.3061 + } 1.3062 + if (this.matchCount < GLOBAL_SEARCH_EXPAND_MAX_RESULTS) { 1.3063 + this.expand(); 1.3064 + } 1.3065 + 1.3066 + let resultsBox = document.createElement("vbox"); 1.3067 + resultsBox.setAttribute("flex", "1"); 1.3068 + resultsBox.appendChild(resultsHeader); 1.3069 + resultsBox.appendChild(resultsContainer); 1.3070 + 1.3071 + aElementNode.id = "source-results-" + this.url; 1.3072 + aElementNode.className = "dbg-source-results"; 1.3073 + aElementNode.appendChild(resultsBox); 1.3074 + 1.3075 + SourceResults._itemsByElement.set(aElementNode, { instance: this }); 1.3076 + }, 1.3077 + 1.3078 + url: "", 1.3079 + _globalResults: null, 1.3080 + _store: null, 1.3081 + _target: null, 1.3082 + _arrow: null, 1.3083 + _resultsHeader: null, 1.3084 + _resultsContainer: null 1.3085 +}; 1.3086 + 1.3087 +/** 1.3088 + * An object containing all the matches for a specific line. 1.3089 + * Iterable via "for (let chunk of lineResults) { }". 1.3090 + * 1.3091 + * @param number aLine 1.3092 + * The target line in the source. 1.3093 + * @param SourceResults aSourceResults 1.3094 + * An object containing all the matched lines for a specific source. 1.3095 + */ 1.3096 +function LineResults(aLine, aSourceResults) { 1.3097 + this.line = aLine; 1.3098 + this._sourceResults = aSourceResults; 1.3099 + this._store = []; 1.3100 + this._matchCount = 0; 1.3101 +} 1.3102 + 1.3103 +LineResults.prototype = { 1.3104 + /** 1.3105 + * Adds string details to this store. 1.3106 + * 1.3107 + * @param string aString 1.3108 + * The text contents chunk in the line. 1.3109 + * @param object aRange 1.3110 + * An object containing the { start, length } of the chunk. 1.3111 + * @param boolean aMatchFlag 1.3112 + * True if the chunk is a matched string, false if just text content. 1.3113 + */ 1.3114 + add: function(aString, aRange, aMatchFlag) { 1.3115 + this._store.push({ string: aString, range: aRange, match: !!aMatchFlag }); 1.3116 + this._matchCount += aMatchFlag ? 1 : 0; 1.3117 + }, 1.3118 + 1.3119 + /** 1.3120 + * Gets the number of word results in this store. 1.3121 + */ 1.3122 + get matchCount() this._matchCount, 1.3123 + 1.3124 + /** 1.3125 + * Gets the element associated with this item. 1.3126 + * @return nsIDOMNode 1.3127 + */ 1.3128 + get target() this._target, 1.3129 + 1.3130 + /** 1.3131 + * Customization function for creating this item's UI. 1.3132 + * 1.3133 + * @param nsIDOMNode aElementNode 1.3134 + * The element associated with the displayed item. 1.3135 + * @param object aCallbacks 1.3136 + * An object containing all the necessary callback functions: 1.3137 + * - onMatchClick 1.3138 + * - onLineClick 1.3139 + */ 1.3140 + createView: function(aElementNode, aCallbacks) { 1.3141 + this._target = aElementNode; 1.3142 + 1.3143 + let lineNumberNode = document.createElement("label"); 1.3144 + lineNumberNode.className = "plain dbg-results-line-number"; 1.3145 + lineNumberNode.classList.add("devtools-monospace"); 1.3146 + lineNumberNode.setAttribute("value", this.line + 1); 1.3147 + 1.3148 + let lineContentsNode = document.createElement("hbox"); 1.3149 + lineContentsNode.className = "dbg-results-line-contents"; 1.3150 + lineContentsNode.classList.add("devtools-monospace"); 1.3151 + lineContentsNode.setAttribute("flex", "1"); 1.3152 + 1.3153 + let lineString = ""; 1.3154 + let lineLength = 0; 1.3155 + let firstMatch = null; 1.3156 + 1.3157 + for (let lineChunk of this._store) { 1.3158 + let { string, range, match } = lineChunk; 1.3159 + lineString = string.substr(0, GLOBAL_SEARCH_LINE_MAX_LENGTH - lineLength); 1.3160 + lineLength += string.length; 1.3161 + 1.3162 + let lineChunkNode = document.createElement("label"); 1.3163 + lineChunkNode.className = "plain dbg-results-line-contents-string"; 1.3164 + lineChunkNode.setAttribute("value", lineString); 1.3165 + lineChunkNode.setAttribute("match", match); 1.3166 + lineContentsNode.appendChild(lineChunkNode); 1.3167 + 1.3168 + if (match) { 1.3169 + this._entangleMatch(lineChunkNode, lineChunk); 1.3170 + lineChunkNode.addEventListener("click", aCallbacks.onMatchClick, false); 1.3171 + firstMatch = firstMatch || lineChunkNode; 1.3172 + } 1.3173 + if (lineLength >= GLOBAL_SEARCH_LINE_MAX_LENGTH) { 1.3174 + lineContentsNode.appendChild(this._ellipsis.cloneNode(true)); 1.3175 + break; 1.3176 + } 1.3177 + } 1.3178 + 1.3179 + this._entangleLine(lineContentsNode, firstMatch); 1.3180 + lineContentsNode.addEventListener("click", aCallbacks.onLineClick, false); 1.3181 + 1.3182 + let searchResult = document.createElement("hbox"); 1.3183 + searchResult.className = "dbg-search-result"; 1.3184 + searchResult.appendChild(lineNumberNode); 1.3185 + searchResult.appendChild(lineContentsNode); 1.3186 + 1.3187 + aElementNode.appendChild(searchResult); 1.3188 + }, 1.3189 + 1.3190 + /** 1.3191 + * Handles a match while creating the view. 1.3192 + * @param nsIDOMNode aNode 1.3193 + * @param object aMatchChunk 1.3194 + */ 1.3195 + _entangleMatch: function(aNode, aMatchChunk) { 1.3196 + LineResults._itemsByElement.set(aNode, { 1.3197 + instance: this, 1.3198 + lineData: aMatchChunk 1.3199 + }); 1.3200 + }, 1.3201 + 1.3202 + /** 1.3203 + * Handles a line while creating the view. 1.3204 + * @param nsIDOMNode aNode 1.3205 + * @param nsIDOMNode aFirstMatch 1.3206 + */ 1.3207 + _entangleLine: function(aNode, aFirstMatch) { 1.3208 + LineResults._itemsByElement.set(aNode, { 1.3209 + instance: this, 1.3210 + firstMatch: aFirstMatch, 1.3211 + ignored: true 1.3212 + }); 1.3213 + }, 1.3214 + 1.3215 + /** 1.3216 + * An nsIDOMNode label with an ellipsis value. 1.3217 + */ 1.3218 + _ellipsis: (function() { 1.3219 + let label = document.createElement("label"); 1.3220 + label.className = "plain dbg-results-line-contents-string"; 1.3221 + label.setAttribute("value", L10N.ellipsis); 1.3222 + return label; 1.3223 + })(), 1.3224 + 1.3225 + line: 0, 1.3226 + _sourceResults: null, 1.3227 + _store: null, 1.3228 + _target: null 1.3229 +}; 1.3230 + 1.3231 +/** 1.3232 + * A generator-iterator over the global, source or line results. 1.3233 + */ 1.3234 +GlobalResults.prototype["@@iterator"] = 1.3235 +SourceResults.prototype["@@iterator"] = 1.3236 +LineResults.prototype["@@iterator"] = function*() { 1.3237 + yield* this._store; 1.3238 +}; 1.3239 + 1.3240 +/** 1.3241 + * Gets the item associated with the specified element. 1.3242 + * 1.3243 + * @param nsIDOMNode aElement 1.3244 + * The element used to identify the item. 1.3245 + * @return object 1.3246 + * The matched item, or null if nothing is found. 1.3247 + */ 1.3248 +SourceResults.getItemForElement = 1.3249 +LineResults.getItemForElement = function(aElement) { 1.3250 + return WidgetMethods.getItemForElement.call(this, aElement, { noSiblings: true }); 1.3251 +}; 1.3252 + 1.3253 +/** 1.3254 + * Gets the element associated with a particular item at a specified index. 1.3255 + * 1.3256 + * @param number aIndex 1.3257 + * The index used to identify the item. 1.3258 + * @return nsIDOMNode 1.3259 + * The matched element, or null if nothing is found. 1.3260 + */ 1.3261 +SourceResults.getElementAtIndex = 1.3262 +LineResults.getElementAtIndex = function(aIndex) { 1.3263 + for (let [element, item] of this._itemsByElement) { 1.3264 + if (!item.ignored && !aIndex--) { 1.3265 + return element; 1.3266 + } 1.3267 + } 1.3268 + return null; 1.3269 +}; 1.3270 + 1.3271 +/** 1.3272 + * Gets the index of an item associated with the specified element. 1.3273 + * 1.3274 + * @param nsIDOMNode aElement 1.3275 + * The element to get the index for. 1.3276 + * @return number 1.3277 + * The index of the matched element, or -1 if nothing is found. 1.3278 + */ 1.3279 +SourceResults.indexOfElement = 1.3280 +LineResults.indexOfElement = function(aElement) { 1.3281 + let count = 0; 1.3282 + for (let [element, item] of this._itemsByElement) { 1.3283 + if (element == aElement) { 1.3284 + return count; 1.3285 + } 1.3286 + if (!item.ignored) { 1.3287 + count++; 1.3288 + } 1.3289 + } 1.3290 + return -1; 1.3291 +}; 1.3292 + 1.3293 +/** 1.3294 + * Gets the number of cached items associated with a specified element. 1.3295 + * 1.3296 + * @return number 1.3297 + * The number of key/value pairs in the corresponding map. 1.3298 + */ 1.3299 +SourceResults.size = 1.3300 +LineResults.size = function() { 1.3301 + let count = 0; 1.3302 + for (let [, item] of this._itemsByElement) { 1.3303 + if (!item.ignored) { 1.3304 + count++; 1.3305 + } 1.3306 + } 1.3307 + return count; 1.3308 +}; 1.3309 + 1.3310 +/** 1.3311 + * Preliminary setup for the DebuggerView object. 1.3312 + */ 1.3313 +DebuggerView.Sources = new SourcesView(); 1.3314 +DebuggerView.VariableBubble = new VariableBubbleView(); 1.3315 +DebuggerView.Tracer = new TracerView(); 1.3316 +DebuggerView.WatchExpressions = new WatchExpressionsView(); 1.3317 +DebuggerView.EventListeners = new EventListenersView(); 1.3318 +DebuggerView.GlobalSearch = new GlobalSearchView();