Wed, 31 Dec 2014 06:09:35 +0100
Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.
michael@0 | 1 | /* -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ |
michael@0 | 2 | /* vim: set ft=javascript ts=2 et sw=2 tw=80: */ |
michael@0 | 3 | /* This Source Code Form is subject to the terms of the Mozilla Public |
michael@0 | 4 | * License, v. 2.0. If a copy of the MPL was not distributed with this |
michael@0 | 5 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ |
michael@0 | 6 | |
michael@0 | 7 | "use strict"; |
michael@0 | 8 | |
michael@0 | 9 | XPCOMUtils.defineLazyModuleGetter(this, "Task", |
michael@0 | 10 | "resource://gre/modules/Task.jsm"); |
michael@0 | 11 | |
michael@0 | 12 | // Used to detect minification for automatic pretty printing |
michael@0 | 13 | const SAMPLE_SIZE = 50; // no of lines |
michael@0 | 14 | const INDENT_COUNT_THRESHOLD = 5; // percentage |
michael@0 | 15 | const CHARACTER_LIMIT = 250; // line character limit |
michael@0 | 16 | |
michael@0 | 17 | // Maps known URLs to friendly source group names |
michael@0 | 18 | const KNOWN_SOURCE_GROUPS = { |
michael@0 | 19 | "Add-on SDK": "resource://gre/modules/commonjs/", |
michael@0 | 20 | }; |
michael@0 | 21 | |
michael@0 | 22 | /** |
michael@0 | 23 | * Functions handling the sources UI. |
michael@0 | 24 | */ |
michael@0 | 25 | function SourcesView() { |
michael@0 | 26 | dumpn("SourcesView was instantiated"); |
michael@0 | 27 | |
michael@0 | 28 | this.togglePrettyPrint = this.togglePrettyPrint.bind(this); |
michael@0 | 29 | this.toggleBlackBoxing = this.toggleBlackBoxing.bind(this); |
michael@0 | 30 | this.toggleBreakpoints = this.toggleBreakpoints.bind(this); |
michael@0 | 31 | |
michael@0 | 32 | this._onEditorLoad = this._onEditorLoad.bind(this); |
michael@0 | 33 | this._onEditorUnload = this._onEditorUnload.bind(this); |
michael@0 | 34 | this._onEditorCursorActivity = this._onEditorCursorActivity.bind(this); |
michael@0 | 35 | this._onSourceSelect = this._onSourceSelect.bind(this); |
michael@0 | 36 | this._onStopBlackBoxing = this._onStopBlackBoxing.bind(this); |
michael@0 | 37 | this._onBreakpointRemoved = this._onBreakpointRemoved.bind(this); |
michael@0 | 38 | this._onBreakpointClick = this._onBreakpointClick.bind(this); |
michael@0 | 39 | this._onBreakpointCheckboxClick = this._onBreakpointCheckboxClick.bind(this); |
michael@0 | 40 | this._onConditionalPopupShowing = this._onConditionalPopupShowing.bind(this); |
michael@0 | 41 | this._onConditionalPopupShown = this._onConditionalPopupShown.bind(this); |
michael@0 | 42 | this._onConditionalPopupHiding = this._onConditionalPopupHiding.bind(this); |
michael@0 | 43 | this._onConditionalTextboxKeyPress = this._onConditionalTextboxKeyPress.bind(this); |
michael@0 | 44 | |
michael@0 | 45 | this.updateToolbarButtonsState = this.updateToolbarButtonsState.bind(this); |
michael@0 | 46 | } |
michael@0 | 47 | |
michael@0 | 48 | SourcesView.prototype = Heritage.extend(WidgetMethods, { |
michael@0 | 49 | /** |
michael@0 | 50 | * Initialization function, called when the debugger is started. |
michael@0 | 51 | */ |
michael@0 | 52 | initialize: function() { |
michael@0 | 53 | dumpn("Initializing the SourcesView"); |
michael@0 | 54 | |
michael@0 | 55 | this.widget = new SideMenuWidget(document.getElementById("sources"), { |
michael@0 | 56 | showArrows: true |
michael@0 | 57 | }); |
michael@0 | 58 | |
michael@0 | 59 | // Sort known source groups towards the end of the list |
michael@0 | 60 | this.widget.groupSortPredicate = function(a, b) { |
michael@0 | 61 | if ((a in KNOWN_SOURCE_GROUPS) == (b in KNOWN_SOURCE_GROUPS)) { |
michael@0 | 62 | return a.localeCompare(b); |
michael@0 | 63 | } |
michael@0 | 64 | |
michael@0 | 65 | return (a in KNOWN_SOURCE_GROUPS) ? 1 : -1; |
michael@0 | 66 | }; |
michael@0 | 67 | |
michael@0 | 68 | this.emptyText = L10N.getStr("noSourcesText"); |
michael@0 | 69 | this._blackBoxCheckboxTooltip = L10N.getStr("blackBoxCheckboxTooltip"); |
michael@0 | 70 | |
michael@0 | 71 | this._commandset = document.getElementById("debuggerCommands"); |
michael@0 | 72 | this._popupset = document.getElementById("debuggerPopupset"); |
michael@0 | 73 | this._cmPopup = document.getElementById("sourceEditorContextMenu"); |
michael@0 | 74 | this._cbPanel = document.getElementById("conditional-breakpoint-panel"); |
michael@0 | 75 | this._cbTextbox = document.getElementById("conditional-breakpoint-panel-textbox"); |
michael@0 | 76 | this._blackBoxButton = document.getElementById("black-box"); |
michael@0 | 77 | this._stopBlackBoxButton = document.getElementById("black-boxed-message-button"); |
michael@0 | 78 | this._prettyPrintButton = document.getElementById("pretty-print"); |
michael@0 | 79 | this._toggleBreakpointsButton = document.getElementById("toggle-breakpoints"); |
michael@0 | 80 | |
michael@0 | 81 | if (Prefs.prettyPrintEnabled) { |
michael@0 | 82 | this._prettyPrintButton.removeAttribute("hidden"); |
michael@0 | 83 | } |
michael@0 | 84 | |
michael@0 | 85 | window.on(EVENTS.EDITOR_LOADED, this._onEditorLoad, false); |
michael@0 | 86 | window.on(EVENTS.EDITOR_UNLOADED, this._onEditorUnload, false); |
michael@0 | 87 | this.widget.addEventListener("select", this._onSourceSelect, false); |
michael@0 | 88 | this._stopBlackBoxButton.addEventListener("click", this._onStopBlackBoxing, false); |
michael@0 | 89 | this._cbPanel.addEventListener("popupshowing", this._onConditionalPopupShowing, false); |
michael@0 | 90 | this._cbPanel.addEventListener("popupshown", this._onConditionalPopupShown, false); |
michael@0 | 91 | this._cbPanel.addEventListener("popuphiding", this._onConditionalPopupHiding, false); |
michael@0 | 92 | this._cbTextbox.addEventListener("keypress", this._onConditionalTextboxKeyPress, false); |
michael@0 | 93 | |
michael@0 | 94 | this.autoFocusOnSelection = false; |
michael@0 | 95 | |
michael@0 | 96 | // Sort the contents by the displayed label. |
michael@0 | 97 | this.sortContents((aFirst, aSecond) => { |
michael@0 | 98 | return +(aFirst.attachment.label.toLowerCase() > |
michael@0 | 99 | aSecond.attachment.label.toLowerCase()); |
michael@0 | 100 | }); |
michael@0 | 101 | }, |
michael@0 | 102 | |
michael@0 | 103 | /** |
michael@0 | 104 | * Destruction function, called when the debugger is closed. |
michael@0 | 105 | */ |
michael@0 | 106 | destroy: function() { |
michael@0 | 107 | dumpn("Destroying the SourcesView"); |
michael@0 | 108 | |
michael@0 | 109 | window.off(EVENTS.EDITOR_LOADED, this._onEditorLoad, false); |
michael@0 | 110 | window.off(EVENTS.EDITOR_UNLOADED, this._onEditorUnload, false); |
michael@0 | 111 | this.widget.removeEventListener("select", this._onSourceSelect, false); |
michael@0 | 112 | this._stopBlackBoxButton.removeEventListener("click", this._onStopBlackBoxing, false); |
michael@0 | 113 | this._cbPanel.removeEventListener("popupshowing", this._onConditionalPopupShowing, false); |
michael@0 | 114 | this._cbPanel.removeEventListener("popupshowing", this._onConditionalPopupShown, false); |
michael@0 | 115 | this._cbPanel.removeEventListener("popuphiding", this._onConditionalPopupHiding, false); |
michael@0 | 116 | this._cbTextbox.removeEventListener("keypress", this._onConditionalTextboxKeyPress, false); |
michael@0 | 117 | }, |
michael@0 | 118 | |
michael@0 | 119 | /** |
michael@0 | 120 | * Sets the preferred location to be selected in this sources container. |
michael@0 | 121 | * @param string aUrl |
michael@0 | 122 | */ |
michael@0 | 123 | set preferredSource(aUrl) { |
michael@0 | 124 | this._preferredValue = aUrl; |
michael@0 | 125 | |
michael@0 | 126 | // Selects the element with the specified value in this sources container, |
michael@0 | 127 | // if already inserted. |
michael@0 | 128 | if (this.containsValue(aUrl)) { |
michael@0 | 129 | this.selectedValue = aUrl; |
michael@0 | 130 | } |
michael@0 | 131 | }, |
michael@0 | 132 | |
michael@0 | 133 | /** |
michael@0 | 134 | * Adds a source to this sources container. |
michael@0 | 135 | * |
michael@0 | 136 | * @param object aSource |
michael@0 | 137 | * The source object coming from the active thread. |
michael@0 | 138 | * @param object aOptions [optional] |
michael@0 | 139 | * Additional options for adding the source. Supported options: |
michael@0 | 140 | * - staged: true to stage the item to be appended later |
michael@0 | 141 | */ |
michael@0 | 142 | addSource: function(aSource, aOptions = {}) { |
michael@0 | 143 | let fullUrl = aSource.url; |
michael@0 | 144 | let url = fullUrl.split(" -> ").pop(); |
michael@0 | 145 | let label = aSource.addonPath ? aSource.addonPath : SourceUtils.getSourceLabel(url); |
michael@0 | 146 | let group = aSource.addonID ? aSource.addonID : SourceUtils.getSourceGroup(url); |
michael@0 | 147 | let unicodeUrl = NetworkHelper.convertToUnicode(unescape(fullUrl)); |
michael@0 | 148 | |
michael@0 | 149 | let contents = document.createElement("label"); |
michael@0 | 150 | contents.className = "plain dbg-source-item"; |
michael@0 | 151 | contents.setAttribute("value", label); |
michael@0 | 152 | contents.setAttribute("crop", "start"); |
michael@0 | 153 | contents.setAttribute("flex", "1"); |
michael@0 | 154 | contents.setAttribute("tooltiptext", unicodeUrl); |
michael@0 | 155 | |
michael@0 | 156 | // Append a source item to this container. |
michael@0 | 157 | this.push([contents, fullUrl], { |
michael@0 | 158 | staged: aOptions.staged, /* stage the item to be appended later? */ |
michael@0 | 159 | attachment: { |
michael@0 | 160 | label: label, |
michael@0 | 161 | group: group, |
michael@0 | 162 | checkboxState: !aSource.isBlackBoxed, |
michael@0 | 163 | checkboxTooltip: this._blackBoxCheckboxTooltip, |
michael@0 | 164 | source: aSource |
michael@0 | 165 | } |
michael@0 | 166 | }); |
michael@0 | 167 | }, |
michael@0 | 168 | |
michael@0 | 169 | /** |
michael@0 | 170 | * Adds a breakpoint to this sources container. |
michael@0 | 171 | * |
michael@0 | 172 | * @param object aBreakpointData |
michael@0 | 173 | * Information about the breakpoint to be shown. |
michael@0 | 174 | * This object must have the following properties: |
michael@0 | 175 | * - location: the breakpoint's source location and line number |
michael@0 | 176 | * - disabled: the breakpoint's disabled state, boolean |
michael@0 | 177 | * - text: the breakpoint's line text to be displayed |
michael@0 | 178 | * @param object aOptions [optional] |
michael@0 | 179 | * @see DebuggerController.Breakpoints.addBreakpoint |
michael@0 | 180 | */ |
michael@0 | 181 | addBreakpoint: function(aBreakpointData, aOptions = {}) { |
michael@0 | 182 | let { location, disabled } = aBreakpointData; |
michael@0 | 183 | |
michael@0 | 184 | // Make sure we're not duplicating anything. If a breakpoint at the |
michael@0 | 185 | // specified source url and line already exists, just toggle it. |
michael@0 | 186 | if (this.getBreakpoint(location)) { |
michael@0 | 187 | this[disabled ? "disableBreakpoint" : "enableBreakpoint"](location); |
michael@0 | 188 | return; |
michael@0 | 189 | } |
michael@0 | 190 | |
michael@0 | 191 | // Get the source item to which the breakpoint should be attached. |
michael@0 | 192 | let sourceItem = this.getItemByValue(location.url); |
michael@0 | 193 | |
michael@0 | 194 | // Create the element node and menu popup for the breakpoint item. |
michael@0 | 195 | let breakpointArgs = Heritage.extend(aBreakpointData, aOptions); |
michael@0 | 196 | let breakpointView = this._createBreakpointView.call(this, breakpointArgs); |
michael@0 | 197 | let contextMenu = this._createContextMenu.call(this, breakpointArgs); |
michael@0 | 198 | |
michael@0 | 199 | // Append a breakpoint child item to the corresponding source item. |
michael@0 | 200 | sourceItem.append(breakpointView.container, { |
michael@0 | 201 | attachment: Heritage.extend(breakpointArgs, { |
michael@0 | 202 | url: location.url, |
michael@0 | 203 | line: location.line, |
michael@0 | 204 | view: breakpointView, |
michael@0 | 205 | popup: contextMenu |
michael@0 | 206 | }), |
michael@0 | 207 | attributes: [ |
michael@0 | 208 | ["contextmenu", contextMenu.menupopupId] |
michael@0 | 209 | ], |
michael@0 | 210 | // Make sure that when the breakpoint item is removed, the corresponding |
michael@0 | 211 | // menupopup and commandset are also destroyed. |
michael@0 | 212 | finalize: this._onBreakpointRemoved |
michael@0 | 213 | }); |
michael@0 | 214 | |
michael@0 | 215 | // Highlight the newly appended breakpoint child item if necessary. |
michael@0 | 216 | if (aOptions.openPopup || !aOptions.noEditorUpdate) { |
michael@0 | 217 | this.highlightBreakpoint(location, aOptions); |
michael@0 | 218 | } |
michael@0 | 219 | }, |
michael@0 | 220 | |
michael@0 | 221 | /** |
michael@0 | 222 | * Removes a breakpoint from this sources container. |
michael@0 | 223 | * It does not also remove the breakpoint from the controller. Be careful. |
michael@0 | 224 | * |
michael@0 | 225 | * @param object aLocation |
michael@0 | 226 | * @see DebuggerController.Breakpoints.addBreakpoint |
michael@0 | 227 | */ |
michael@0 | 228 | removeBreakpoint: function(aLocation) { |
michael@0 | 229 | // When a parent source item is removed, all the child breakpoint items are |
michael@0 | 230 | // also automagically removed. |
michael@0 | 231 | let sourceItem = this.getItemByValue(aLocation.url); |
michael@0 | 232 | if (!sourceItem) { |
michael@0 | 233 | return; |
michael@0 | 234 | } |
michael@0 | 235 | let breakpointItem = this.getBreakpoint(aLocation); |
michael@0 | 236 | if (!breakpointItem) { |
michael@0 | 237 | return; |
michael@0 | 238 | } |
michael@0 | 239 | |
michael@0 | 240 | // Clear the breakpoint view. |
michael@0 | 241 | sourceItem.remove(breakpointItem); |
michael@0 | 242 | }, |
michael@0 | 243 | |
michael@0 | 244 | /** |
michael@0 | 245 | * Returns the breakpoint at the specified source url and line. |
michael@0 | 246 | * |
michael@0 | 247 | * @param object aLocation |
michael@0 | 248 | * @see DebuggerController.Breakpoints.addBreakpoint |
michael@0 | 249 | * @return object |
michael@0 | 250 | * The corresponding breakpoint item if found, null otherwise. |
michael@0 | 251 | */ |
michael@0 | 252 | getBreakpoint: function(aLocation) { |
michael@0 | 253 | return this.getItemForPredicate(aItem => |
michael@0 | 254 | aItem.attachment.url == aLocation.url && |
michael@0 | 255 | aItem.attachment.line == aLocation.line); |
michael@0 | 256 | }, |
michael@0 | 257 | |
michael@0 | 258 | /** |
michael@0 | 259 | * Returns all breakpoints for all sources. |
michael@0 | 260 | * |
michael@0 | 261 | * @return array |
michael@0 | 262 | * The breakpoints for all sources if any, an empty array otherwise. |
michael@0 | 263 | */ |
michael@0 | 264 | getAllBreakpoints: function(aStore = []) { |
michael@0 | 265 | return this.getOtherBreakpoints(undefined, aStore); |
michael@0 | 266 | }, |
michael@0 | 267 | |
michael@0 | 268 | /** |
michael@0 | 269 | * Returns all breakpoints which are not at the specified source url and line. |
michael@0 | 270 | * |
michael@0 | 271 | * @param object aLocation [optional] |
michael@0 | 272 | * @see DebuggerController.Breakpoints.addBreakpoint |
michael@0 | 273 | * @param array aStore [optional] |
michael@0 | 274 | * A list in which to store the corresponding breakpoints. |
michael@0 | 275 | * @return array |
michael@0 | 276 | * The corresponding breakpoints if found, an empty array otherwise. |
michael@0 | 277 | */ |
michael@0 | 278 | getOtherBreakpoints: function(aLocation = {}, aStore = []) { |
michael@0 | 279 | for (let source of this) { |
michael@0 | 280 | for (let breakpointItem of source) { |
michael@0 | 281 | let { url, line } = breakpointItem.attachment; |
michael@0 | 282 | if (url != aLocation.url || line != aLocation.line) { |
michael@0 | 283 | aStore.push(breakpointItem); |
michael@0 | 284 | } |
michael@0 | 285 | } |
michael@0 | 286 | } |
michael@0 | 287 | return aStore; |
michael@0 | 288 | }, |
michael@0 | 289 | |
michael@0 | 290 | /** |
michael@0 | 291 | * Enables a breakpoint. |
michael@0 | 292 | * |
michael@0 | 293 | * @param object aLocation |
michael@0 | 294 | * @see DebuggerController.Breakpoints.addBreakpoint |
michael@0 | 295 | * @param object aOptions [optional] |
michael@0 | 296 | * Additional options or flags supported by this operation: |
michael@0 | 297 | * - silent: pass true to not update the checkbox checked state; |
michael@0 | 298 | * this is usually necessary when the checked state will |
michael@0 | 299 | * be updated automatically (e.g: on a checkbox click). |
michael@0 | 300 | * @return object |
michael@0 | 301 | * A promise that is resolved after the breakpoint is enabled, or |
michael@0 | 302 | * rejected if no breakpoint was found at the specified location. |
michael@0 | 303 | */ |
michael@0 | 304 | enableBreakpoint: function(aLocation, aOptions = {}) { |
michael@0 | 305 | let breakpointItem = this.getBreakpoint(aLocation); |
michael@0 | 306 | if (!breakpointItem) { |
michael@0 | 307 | return promise.reject(new Error("No breakpoint found.")); |
michael@0 | 308 | } |
michael@0 | 309 | |
michael@0 | 310 | // Breakpoint will now be enabled. |
michael@0 | 311 | let attachment = breakpointItem.attachment; |
michael@0 | 312 | attachment.disabled = false; |
michael@0 | 313 | |
michael@0 | 314 | // Update the corresponding menu items to reflect the enabled state. |
michael@0 | 315 | let prefix = "bp-cMenu-"; // "breakpoints context menu" |
michael@0 | 316 | let identifier = DebuggerController.Breakpoints.getIdentifier(attachment); |
michael@0 | 317 | let enableSelfId = prefix + "enableSelf-" + identifier + "-menuitem"; |
michael@0 | 318 | let disableSelfId = prefix + "disableSelf-" + identifier + "-menuitem"; |
michael@0 | 319 | document.getElementById(enableSelfId).setAttribute("hidden", "true"); |
michael@0 | 320 | document.getElementById(disableSelfId).removeAttribute("hidden"); |
michael@0 | 321 | |
michael@0 | 322 | // Update the breakpoint toggle button checked state. |
michael@0 | 323 | this._toggleBreakpointsButton.removeAttribute("checked"); |
michael@0 | 324 | |
michael@0 | 325 | // Update the checkbox state if necessary. |
michael@0 | 326 | if (!aOptions.silent) { |
michael@0 | 327 | attachment.view.checkbox.setAttribute("checked", "true"); |
michael@0 | 328 | } |
michael@0 | 329 | |
michael@0 | 330 | return DebuggerController.Breakpoints.addBreakpoint(aLocation, { |
michael@0 | 331 | // No need to update the pane, since this method is invoked because |
michael@0 | 332 | // a breakpoint's view was interacted with. |
michael@0 | 333 | noPaneUpdate: true |
michael@0 | 334 | }); |
michael@0 | 335 | }, |
michael@0 | 336 | |
michael@0 | 337 | /** |
michael@0 | 338 | * Disables a breakpoint. |
michael@0 | 339 | * |
michael@0 | 340 | * @param object aLocation |
michael@0 | 341 | * @see DebuggerController.Breakpoints.addBreakpoint |
michael@0 | 342 | * @param object aOptions [optional] |
michael@0 | 343 | * Additional options or flags supported by this operation: |
michael@0 | 344 | * - silent: pass true to not update the checkbox checked state; |
michael@0 | 345 | * this is usually necessary when the checked state will |
michael@0 | 346 | * be updated automatically (e.g: on a checkbox click). |
michael@0 | 347 | * @return object |
michael@0 | 348 | * A promise that is resolved after the breakpoint is disabled, or |
michael@0 | 349 | * rejected if no breakpoint was found at the specified location. |
michael@0 | 350 | */ |
michael@0 | 351 | disableBreakpoint: function(aLocation, aOptions = {}) { |
michael@0 | 352 | let breakpointItem = this.getBreakpoint(aLocation); |
michael@0 | 353 | if (!breakpointItem) { |
michael@0 | 354 | return promise.reject(new Error("No breakpoint found.")); |
michael@0 | 355 | } |
michael@0 | 356 | |
michael@0 | 357 | // Breakpoint will now be disabled. |
michael@0 | 358 | let attachment = breakpointItem.attachment; |
michael@0 | 359 | attachment.disabled = true; |
michael@0 | 360 | |
michael@0 | 361 | // Update the corresponding menu items to reflect the disabled state. |
michael@0 | 362 | let prefix = "bp-cMenu-"; // "breakpoints context menu" |
michael@0 | 363 | let identifier = DebuggerController.Breakpoints.getIdentifier(attachment); |
michael@0 | 364 | let enableSelfId = prefix + "enableSelf-" + identifier + "-menuitem"; |
michael@0 | 365 | let disableSelfId = prefix + "disableSelf-" + identifier + "-menuitem"; |
michael@0 | 366 | document.getElementById(enableSelfId).removeAttribute("hidden"); |
michael@0 | 367 | document.getElementById(disableSelfId).setAttribute("hidden", "true"); |
michael@0 | 368 | |
michael@0 | 369 | // Update the checkbox state if necessary. |
michael@0 | 370 | if (!aOptions.silent) { |
michael@0 | 371 | attachment.view.checkbox.removeAttribute("checked"); |
michael@0 | 372 | } |
michael@0 | 373 | |
michael@0 | 374 | return DebuggerController.Breakpoints.removeBreakpoint(aLocation, { |
michael@0 | 375 | // No need to update this pane, since this method is invoked because |
michael@0 | 376 | // a breakpoint's view was interacted with. |
michael@0 | 377 | noPaneUpdate: true, |
michael@0 | 378 | // Mark this breakpoint as being "disabled", not completely removed. |
michael@0 | 379 | // This makes sure it will not be forgotten across target navigations. |
michael@0 | 380 | rememberDisabled: true |
michael@0 | 381 | }); |
michael@0 | 382 | }, |
michael@0 | 383 | |
michael@0 | 384 | /** |
michael@0 | 385 | * Highlights a breakpoint in this sources container. |
michael@0 | 386 | * |
michael@0 | 387 | * @param object aLocation |
michael@0 | 388 | * @see DebuggerController.Breakpoints.addBreakpoint |
michael@0 | 389 | * @param object aOptions [optional] |
michael@0 | 390 | * An object containing some of the following boolean properties: |
michael@0 | 391 | * - openPopup: tells if the expression popup should be shown. |
michael@0 | 392 | * - noEditorUpdate: tells if you want to skip editor updates. |
michael@0 | 393 | */ |
michael@0 | 394 | highlightBreakpoint: function(aLocation, aOptions = {}) { |
michael@0 | 395 | let breakpointItem = this.getBreakpoint(aLocation); |
michael@0 | 396 | if (!breakpointItem) { |
michael@0 | 397 | return; |
michael@0 | 398 | } |
michael@0 | 399 | |
michael@0 | 400 | // Breakpoint will now be selected. |
michael@0 | 401 | this._selectBreakpoint(breakpointItem); |
michael@0 | 402 | |
michael@0 | 403 | // Update the editor location if necessary. |
michael@0 | 404 | if (!aOptions.noEditorUpdate) { |
michael@0 | 405 | DebuggerView.setEditorLocation(aLocation.url, aLocation.line, { noDebug: true }); |
michael@0 | 406 | } |
michael@0 | 407 | |
michael@0 | 408 | // If the breakpoint requires a new conditional expression, display |
michael@0 | 409 | // the panel to input the corresponding expression. |
michael@0 | 410 | if (aOptions.openPopup) { |
michael@0 | 411 | this._openConditionalPopup(); |
michael@0 | 412 | } else { |
michael@0 | 413 | this._hideConditionalPopup(); |
michael@0 | 414 | } |
michael@0 | 415 | }, |
michael@0 | 416 | |
michael@0 | 417 | /** |
michael@0 | 418 | * Unhighlights the current breakpoint in this sources container. |
michael@0 | 419 | */ |
michael@0 | 420 | unhighlightBreakpoint: function() { |
michael@0 | 421 | this._unselectBreakpoint(); |
michael@0 | 422 | this._hideConditionalPopup(); |
michael@0 | 423 | }, |
michael@0 | 424 | |
michael@0 | 425 | /** |
michael@0 | 426 | * Update the checked/unchecked and enabled/disabled states of the buttons in |
michael@0 | 427 | * the sources toolbar based on the currently selected source's state. |
michael@0 | 428 | */ |
michael@0 | 429 | updateToolbarButtonsState: function() { |
michael@0 | 430 | const { source } = this.selectedItem.attachment; |
michael@0 | 431 | const sourceClient = gThreadClient.source(source); |
michael@0 | 432 | |
michael@0 | 433 | if (sourceClient.isBlackBoxed) { |
michael@0 | 434 | this._prettyPrintButton.setAttribute("disabled", true); |
michael@0 | 435 | this._blackBoxButton.setAttribute("checked", true); |
michael@0 | 436 | } else { |
michael@0 | 437 | this._prettyPrintButton.removeAttribute("disabled"); |
michael@0 | 438 | this._blackBoxButton.removeAttribute("checked"); |
michael@0 | 439 | } |
michael@0 | 440 | |
michael@0 | 441 | if (sourceClient.isPrettyPrinted) { |
michael@0 | 442 | this._prettyPrintButton.setAttribute("checked", true); |
michael@0 | 443 | } else { |
michael@0 | 444 | this._prettyPrintButton.removeAttribute("checked"); |
michael@0 | 445 | } |
michael@0 | 446 | }, |
michael@0 | 447 | |
michael@0 | 448 | /** |
michael@0 | 449 | * Toggle the pretty printing of the selected source. |
michael@0 | 450 | */ |
michael@0 | 451 | togglePrettyPrint: function() { |
michael@0 | 452 | if (this._prettyPrintButton.hasAttribute("disabled")) { |
michael@0 | 453 | return; |
michael@0 | 454 | } |
michael@0 | 455 | |
michael@0 | 456 | const resetEditor = ([{ url }]) => { |
michael@0 | 457 | // Only set the text when the source is still selected. |
michael@0 | 458 | if (url == this.selectedValue) { |
michael@0 | 459 | DebuggerView.setEditorLocation(url, 0, { force: true }); |
michael@0 | 460 | } |
michael@0 | 461 | }; |
michael@0 | 462 | |
michael@0 | 463 | const printError = ([{ url }, error]) => { |
michael@0 | 464 | DevToolsUtils.reportException("togglePrettyPrint", error); |
michael@0 | 465 | }; |
michael@0 | 466 | |
michael@0 | 467 | DebuggerView.showProgressBar(); |
michael@0 | 468 | const { source } = this.selectedItem.attachment; |
michael@0 | 469 | const sourceClient = gThreadClient.source(source); |
michael@0 | 470 | const shouldPrettyPrint = !sourceClient.isPrettyPrinted; |
michael@0 | 471 | |
michael@0 | 472 | if (shouldPrettyPrint) { |
michael@0 | 473 | this._prettyPrintButton.setAttribute("checked", true); |
michael@0 | 474 | } else { |
michael@0 | 475 | this._prettyPrintButton.removeAttribute("checked"); |
michael@0 | 476 | } |
michael@0 | 477 | |
michael@0 | 478 | DebuggerController.SourceScripts.togglePrettyPrint(source) |
michael@0 | 479 | .then(resetEditor, printError) |
michael@0 | 480 | .then(DebuggerView.showEditor) |
michael@0 | 481 | .then(this.updateToolbarButtonsState); |
michael@0 | 482 | }, |
michael@0 | 483 | |
michael@0 | 484 | /** |
michael@0 | 485 | * Toggle the black boxed state of the selected source. |
michael@0 | 486 | */ |
michael@0 | 487 | toggleBlackBoxing: function() { |
michael@0 | 488 | const { source } = this.selectedItem.attachment; |
michael@0 | 489 | const sourceClient = gThreadClient.source(source); |
michael@0 | 490 | const shouldBlackBox = !sourceClient.isBlackBoxed; |
michael@0 | 491 | |
michael@0 | 492 | // Be optimistic that the (un-)black boxing will succeed, so enable/disable |
michael@0 | 493 | // the pretty print button and check/uncheck the black box button |
michael@0 | 494 | // immediately. Then, once we actually get the results from the server, make |
michael@0 | 495 | // sure that it is in the correct state again by calling |
michael@0 | 496 | // `updateToolbarButtonsState`. |
michael@0 | 497 | |
michael@0 | 498 | if (shouldBlackBox) { |
michael@0 | 499 | this._prettyPrintButton.setAttribute("disabled", true); |
michael@0 | 500 | this._blackBoxButton.setAttribute("checked", true); |
michael@0 | 501 | } else { |
michael@0 | 502 | this._prettyPrintButton.removeAttribute("disabled"); |
michael@0 | 503 | this._blackBoxButton.removeAttribute("checked"); |
michael@0 | 504 | } |
michael@0 | 505 | |
michael@0 | 506 | DebuggerController.SourceScripts.setBlackBoxing(source, shouldBlackBox) |
michael@0 | 507 | .then(this.updateToolbarButtonsState, |
michael@0 | 508 | this.updateToolbarButtonsState); |
michael@0 | 509 | }, |
michael@0 | 510 | |
michael@0 | 511 | /** |
michael@0 | 512 | * Toggles all breakpoints enabled/disabled. |
michael@0 | 513 | */ |
michael@0 | 514 | toggleBreakpoints: function() { |
michael@0 | 515 | let breakpoints = this.getAllBreakpoints(); |
michael@0 | 516 | let hasBreakpoints = breakpoints.length > 0; |
michael@0 | 517 | let hasEnabledBreakpoints = breakpoints.some(e => !e.attachment.disabled); |
michael@0 | 518 | |
michael@0 | 519 | if (hasBreakpoints && hasEnabledBreakpoints) { |
michael@0 | 520 | this._toggleBreakpointsButton.setAttribute("checked", true); |
michael@0 | 521 | this._onDisableAll(); |
michael@0 | 522 | } else { |
michael@0 | 523 | this._toggleBreakpointsButton.removeAttribute("checked"); |
michael@0 | 524 | this._onEnableAll(); |
michael@0 | 525 | } |
michael@0 | 526 | }, |
michael@0 | 527 | |
michael@0 | 528 | /** |
michael@0 | 529 | * Marks a breakpoint as selected in this sources container. |
michael@0 | 530 | * |
michael@0 | 531 | * @param object aItem |
michael@0 | 532 | * The breakpoint item to select. |
michael@0 | 533 | */ |
michael@0 | 534 | _selectBreakpoint: function(aItem) { |
michael@0 | 535 | if (this._selectedBreakpointItem == aItem) { |
michael@0 | 536 | return; |
michael@0 | 537 | } |
michael@0 | 538 | this._unselectBreakpoint(); |
michael@0 | 539 | this._selectedBreakpointItem = aItem; |
michael@0 | 540 | this._selectedBreakpointItem.target.classList.add("selected"); |
michael@0 | 541 | |
michael@0 | 542 | // Ensure the currently selected breakpoint is visible. |
michael@0 | 543 | this.widget.ensureElementIsVisible(aItem.target); |
michael@0 | 544 | }, |
michael@0 | 545 | |
michael@0 | 546 | /** |
michael@0 | 547 | * Marks the current breakpoint as unselected in this sources container. |
michael@0 | 548 | */ |
michael@0 | 549 | _unselectBreakpoint: function() { |
michael@0 | 550 | if (!this._selectedBreakpointItem) { |
michael@0 | 551 | return; |
michael@0 | 552 | } |
michael@0 | 553 | this._selectedBreakpointItem.target.classList.remove("selected"); |
michael@0 | 554 | this._selectedBreakpointItem = null; |
michael@0 | 555 | }, |
michael@0 | 556 | |
michael@0 | 557 | /** |
michael@0 | 558 | * Opens a conditional breakpoint's expression input popup. |
michael@0 | 559 | */ |
michael@0 | 560 | _openConditionalPopup: function() { |
michael@0 | 561 | let breakpointItem = this._selectedBreakpointItem; |
michael@0 | 562 | let attachment = breakpointItem.attachment; |
michael@0 | 563 | // Check if this is an enabled conditional breakpoint, and if so, |
michael@0 | 564 | // retrieve the current conditional epression. |
michael@0 | 565 | let breakpointPromise = DebuggerController.Breakpoints._getAdded(attachment); |
michael@0 | 566 | if (breakpointPromise) { |
michael@0 | 567 | breakpointPromise.then(aBreakpointClient => { |
michael@0 | 568 | let isConditionalBreakpoint = aBreakpointClient.hasCondition(); |
michael@0 | 569 | let condition = aBreakpointClient.getCondition(); |
michael@0 | 570 | doOpen.call(this, isConditionalBreakpoint ? condition : "") |
michael@0 | 571 | }); |
michael@0 | 572 | } else { |
michael@0 | 573 | doOpen.call(this, "") |
michael@0 | 574 | } |
michael@0 | 575 | |
michael@0 | 576 | function doOpen(aConditionalExpression) { |
michael@0 | 577 | // Update the conditional expression textbox. If no expression was |
michael@0 | 578 | // previously set, revert to using an empty string by default. |
michael@0 | 579 | this._cbTextbox.value = aConditionalExpression; |
michael@0 | 580 | |
michael@0 | 581 | // Show the conditional expression panel. The popup arrow should be pointing |
michael@0 | 582 | // at the line number node in the breakpoint item view. |
michael@0 | 583 | this._cbPanel.hidden = false; |
michael@0 | 584 | this._cbPanel.openPopup(breakpointItem.attachment.view.lineNumber, |
michael@0 | 585 | BREAKPOINT_CONDITIONAL_POPUP_POSITION, |
michael@0 | 586 | BREAKPOINT_CONDITIONAL_POPUP_OFFSET_X, |
michael@0 | 587 | BREAKPOINT_CONDITIONAL_POPUP_OFFSET_Y); |
michael@0 | 588 | } |
michael@0 | 589 | }, |
michael@0 | 590 | |
michael@0 | 591 | /** |
michael@0 | 592 | * Hides a conditional breakpoint's expression input popup. |
michael@0 | 593 | */ |
michael@0 | 594 | _hideConditionalPopup: function() { |
michael@0 | 595 | this._cbPanel.hidden = true; |
michael@0 | 596 | |
michael@0 | 597 | // Sometimes this._cbPanel doesn't have hidePopup method which doesn't |
michael@0 | 598 | // break anything but simply outputs an exception to the console. |
michael@0 | 599 | if (this._cbPanel.hidePopup) { |
michael@0 | 600 | this._cbPanel.hidePopup(); |
michael@0 | 601 | } |
michael@0 | 602 | }, |
michael@0 | 603 | |
michael@0 | 604 | /** |
michael@0 | 605 | * Customization function for creating a breakpoint item's UI. |
michael@0 | 606 | * |
michael@0 | 607 | * @param object aOptions |
michael@0 | 608 | * A couple of options or flags supported by this operation: |
michael@0 | 609 | * - location: the breakpoint's source location and line number |
michael@0 | 610 | * - disabled: the breakpoint's disabled state, boolean |
michael@0 | 611 | * - text: the breakpoint's line text to be displayed |
michael@0 | 612 | * @return object |
michael@0 | 613 | * An object containing the breakpoint container, checkbox, |
michael@0 | 614 | * line number and line text nodes. |
michael@0 | 615 | */ |
michael@0 | 616 | _createBreakpointView: function(aOptions) { |
michael@0 | 617 | let { location, disabled, text } = aOptions; |
michael@0 | 618 | let identifier = DebuggerController.Breakpoints.getIdentifier(location); |
michael@0 | 619 | |
michael@0 | 620 | let checkbox = document.createElement("checkbox"); |
michael@0 | 621 | checkbox.setAttribute("checked", !disabled); |
michael@0 | 622 | checkbox.className = "dbg-breakpoint-checkbox"; |
michael@0 | 623 | |
michael@0 | 624 | let lineNumberNode = document.createElement("label"); |
michael@0 | 625 | lineNumberNode.className = "plain dbg-breakpoint-line"; |
michael@0 | 626 | lineNumberNode.setAttribute("value", location.line); |
michael@0 | 627 | |
michael@0 | 628 | let lineTextNode = document.createElement("label"); |
michael@0 | 629 | lineTextNode.className = "plain dbg-breakpoint-text"; |
michael@0 | 630 | lineTextNode.setAttribute("value", text); |
michael@0 | 631 | lineTextNode.setAttribute("crop", "end"); |
michael@0 | 632 | lineTextNode.setAttribute("flex", "1"); |
michael@0 | 633 | |
michael@0 | 634 | let tooltip = text.substr(0, BREAKPOINT_LINE_TOOLTIP_MAX_LENGTH); |
michael@0 | 635 | lineTextNode.setAttribute("tooltiptext", tooltip); |
michael@0 | 636 | |
michael@0 | 637 | let container = document.createElement("hbox"); |
michael@0 | 638 | container.id = "breakpoint-" + identifier; |
michael@0 | 639 | container.className = "dbg-breakpoint side-menu-widget-item-other"; |
michael@0 | 640 | container.classList.add("devtools-monospace"); |
michael@0 | 641 | container.setAttribute("align", "center"); |
michael@0 | 642 | container.setAttribute("flex", "1"); |
michael@0 | 643 | |
michael@0 | 644 | container.addEventListener("click", this._onBreakpointClick, false); |
michael@0 | 645 | checkbox.addEventListener("click", this._onBreakpointCheckboxClick, false); |
michael@0 | 646 | |
michael@0 | 647 | container.appendChild(checkbox); |
michael@0 | 648 | container.appendChild(lineNumberNode); |
michael@0 | 649 | container.appendChild(lineTextNode); |
michael@0 | 650 | |
michael@0 | 651 | return { |
michael@0 | 652 | container: container, |
michael@0 | 653 | checkbox: checkbox, |
michael@0 | 654 | lineNumber: lineNumberNode, |
michael@0 | 655 | lineText: lineTextNode |
michael@0 | 656 | }; |
michael@0 | 657 | }, |
michael@0 | 658 | |
michael@0 | 659 | /** |
michael@0 | 660 | * Creates a context menu for a breakpoint element. |
michael@0 | 661 | * |
michael@0 | 662 | * @param object aOptions |
michael@0 | 663 | * A couple of options or flags supported by this operation: |
michael@0 | 664 | * - location: the breakpoint's source location and line number |
michael@0 | 665 | * - disabled: the breakpoint's disabled state, boolean |
michael@0 | 666 | * @return object |
michael@0 | 667 | * An object containing the breakpoint commandset and menu popup ids. |
michael@0 | 668 | */ |
michael@0 | 669 | _createContextMenu: function(aOptions) { |
michael@0 | 670 | let { location, disabled } = aOptions; |
michael@0 | 671 | let identifier = DebuggerController.Breakpoints.getIdentifier(location); |
michael@0 | 672 | |
michael@0 | 673 | let commandset = document.createElement("commandset"); |
michael@0 | 674 | let menupopup = document.createElement("menupopup"); |
michael@0 | 675 | commandset.id = "bp-cSet-" + identifier; |
michael@0 | 676 | menupopup.id = "bp-mPop-" + identifier; |
michael@0 | 677 | |
michael@0 | 678 | createMenuItem.call(this, "enableSelf", !disabled); |
michael@0 | 679 | createMenuItem.call(this, "disableSelf", disabled); |
michael@0 | 680 | createMenuItem.call(this, "deleteSelf"); |
michael@0 | 681 | createMenuSeparator(); |
michael@0 | 682 | createMenuItem.call(this, "setConditional"); |
michael@0 | 683 | createMenuSeparator(); |
michael@0 | 684 | createMenuItem.call(this, "enableOthers"); |
michael@0 | 685 | createMenuItem.call(this, "disableOthers"); |
michael@0 | 686 | createMenuItem.call(this, "deleteOthers"); |
michael@0 | 687 | createMenuSeparator(); |
michael@0 | 688 | createMenuItem.call(this, "enableAll"); |
michael@0 | 689 | createMenuItem.call(this, "disableAll"); |
michael@0 | 690 | createMenuSeparator(); |
michael@0 | 691 | createMenuItem.call(this, "deleteAll"); |
michael@0 | 692 | |
michael@0 | 693 | this._popupset.appendChild(menupopup); |
michael@0 | 694 | this._commandset.appendChild(commandset); |
michael@0 | 695 | |
michael@0 | 696 | return { |
michael@0 | 697 | commandsetId: commandset.id, |
michael@0 | 698 | menupopupId: menupopup.id |
michael@0 | 699 | }; |
michael@0 | 700 | |
michael@0 | 701 | /** |
michael@0 | 702 | * Creates a menu item specified by a name with the appropriate attributes |
michael@0 | 703 | * (label and handler). |
michael@0 | 704 | * |
michael@0 | 705 | * @param string aName |
michael@0 | 706 | * A global identifier for the menu item. |
michael@0 | 707 | * @param boolean aHiddenFlag |
michael@0 | 708 | * True if this menuitem should be hidden. |
michael@0 | 709 | */ |
michael@0 | 710 | function createMenuItem(aName, aHiddenFlag) { |
michael@0 | 711 | let menuitem = document.createElement("menuitem"); |
michael@0 | 712 | let command = document.createElement("command"); |
michael@0 | 713 | |
michael@0 | 714 | let prefix = "bp-cMenu-"; // "breakpoints context menu" |
michael@0 | 715 | let commandId = prefix + aName + "-" + identifier + "-command"; |
michael@0 | 716 | let menuitemId = prefix + aName + "-" + identifier + "-menuitem"; |
michael@0 | 717 | |
michael@0 | 718 | let label = L10N.getStr("breakpointMenuItem." + aName); |
michael@0 | 719 | let func = "_on" + aName.charAt(0).toUpperCase() + aName.slice(1); |
michael@0 | 720 | |
michael@0 | 721 | command.id = commandId; |
michael@0 | 722 | command.setAttribute("label", label); |
michael@0 | 723 | command.addEventListener("command", () => this[func](location), false); |
michael@0 | 724 | |
michael@0 | 725 | menuitem.id = menuitemId; |
michael@0 | 726 | menuitem.setAttribute("command", commandId); |
michael@0 | 727 | aHiddenFlag && menuitem.setAttribute("hidden", "true"); |
michael@0 | 728 | |
michael@0 | 729 | commandset.appendChild(command); |
michael@0 | 730 | menupopup.appendChild(menuitem); |
michael@0 | 731 | } |
michael@0 | 732 | |
michael@0 | 733 | /** |
michael@0 | 734 | * Creates a simple menu separator element and appends it to the current |
michael@0 | 735 | * menupopup hierarchy. |
michael@0 | 736 | */ |
michael@0 | 737 | function createMenuSeparator() { |
michael@0 | 738 | let menuseparator = document.createElement("menuseparator"); |
michael@0 | 739 | menupopup.appendChild(menuseparator); |
michael@0 | 740 | } |
michael@0 | 741 | }, |
michael@0 | 742 | |
michael@0 | 743 | /** |
michael@0 | 744 | * Function called each time a breakpoint item is removed. |
michael@0 | 745 | * |
michael@0 | 746 | * @param object aItem |
michael@0 | 747 | * The corresponding item. |
michael@0 | 748 | */ |
michael@0 | 749 | _onBreakpointRemoved: function(aItem) { |
michael@0 | 750 | dumpn("Finalizing breakpoint item: " + aItem); |
michael@0 | 751 | |
michael@0 | 752 | // Destroy the context menu for the breakpoint. |
michael@0 | 753 | let contextMenu = aItem.attachment.popup; |
michael@0 | 754 | document.getElementById(contextMenu.commandsetId).remove(); |
michael@0 | 755 | document.getElementById(contextMenu.menupopupId).remove(); |
michael@0 | 756 | |
michael@0 | 757 | // Clear the breakpoint selection. |
michael@0 | 758 | if (this._selectedBreakpointItem == aItem) { |
michael@0 | 759 | this._selectedBreakpointItem = null; |
michael@0 | 760 | } |
michael@0 | 761 | }, |
michael@0 | 762 | |
michael@0 | 763 | /** |
michael@0 | 764 | * The load listener for the source editor. |
michael@0 | 765 | */ |
michael@0 | 766 | _onEditorLoad: function(aName, aEditor) { |
michael@0 | 767 | aEditor.on("cursorActivity", this._onEditorCursorActivity); |
michael@0 | 768 | }, |
michael@0 | 769 | |
michael@0 | 770 | /** |
michael@0 | 771 | * The unload listener for the source editor. |
michael@0 | 772 | */ |
michael@0 | 773 | _onEditorUnload: function(aName, aEditor) { |
michael@0 | 774 | aEditor.off("cursorActivity", this._onEditorCursorActivity); |
michael@0 | 775 | }, |
michael@0 | 776 | |
michael@0 | 777 | /** |
michael@0 | 778 | * The selection listener for the source editor. |
michael@0 | 779 | */ |
michael@0 | 780 | _onEditorCursorActivity: function(e) { |
michael@0 | 781 | let editor = DebuggerView.editor; |
michael@0 | 782 | let start = editor.getCursor("start").line + 1; |
michael@0 | 783 | let end = editor.getCursor().line + 1; |
michael@0 | 784 | let url = this.selectedValue; |
michael@0 | 785 | |
michael@0 | 786 | let location = { url: url, line: start }; |
michael@0 | 787 | |
michael@0 | 788 | if (this.getBreakpoint(location) && start == end) { |
michael@0 | 789 | this.highlightBreakpoint(location, { noEditorUpdate: true }); |
michael@0 | 790 | } else { |
michael@0 | 791 | this.unhighlightBreakpoint(); |
michael@0 | 792 | } |
michael@0 | 793 | }, |
michael@0 | 794 | |
michael@0 | 795 | /** |
michael@0 | 796 | * The select listener for the sources container. |
michael@0 | 797 | */ |
michael@0 | 798 | _onSourceSelect: function({ detail: sourceItem }) { |
michael@0 | 799 | if (!sourceItem) { |
michael@0 | 800 | return; |
michael@0 | 801 | } |
michael@0 | 802 | const { source } = sourceItem.attachment; |
michael@0 | 803 | const sourceClient = gThreadClient.source(source); |
michael@0 | 804 | |
michael@0 | 805 | // The container is not empty and an actual item was selected. |
michael@0 | 806 | DebuggerView.setEditorLocation(sourceItem.value); |
michael@0 | 807 | |
michael@0 | 808 | if (Prefs.autoPrettyPrint && !sourceClient.isPrettyPrinted) { |
michael@0 | 809 | DebuggerController.SourceScripts.getText(source).then(([, aText]) => { |
michael@0 | 810 | if (SourceUtils.isMinified(sourceClient, aText)) { |
michael@0 | 811 | this.togglePrettyPrint(); |
michael@0 | 812 | } |
michael@0 | 813 | }).then(null, e => DevToolsUtils.reportException("_onSourceSelect", e)); |
michael@0 | 814 | } |
michael@0 | 815 | |
michael@0 | 816 | // Set window title. No need to split the url by " -> " here, because it was |
michael@0 | 817 | // already sanitized when the source was added. |
michael@0 | 818 | document.title = L10N.getFormatStr("DebuggerWindowScriptTitle", sourceItem.value); |
michael@0 | 819 | |
michael@0 | 820 | DebuggerView.maybeShowBlackBoxMessage(); |
michael@0 | 821 | this.updateToolbarButtonsState(); |
michael@0 | 822 | }, |
michael@0 | 823 | |
michael@0 | 824 | /** |
michael@0 | 825 | * The click listener for the "stop black boxing" button. |
michael@0 | 826 | */ |
michael@0 | 827 | _onStopBlackBoxing: function() { |
michael@0 | 828 | const { source } = this.selectedItem.attachment; |
michael@0 | 829 | |
michael@0 | 830 | DebuggerController.SourceScripts.setBlackBoxing(source, false) |
michael@0 | 831 | .then(this.updateToolbarButtonsState, |
michael@0 | 832 | this.updateToolbarButtonsState); |
michael@0 | 833 | }, |
michael@0 | 834 | |
michael@0 | 835 | /** |
michael@0 | 836 | * The click listener for a breakpoint container. |
michael@0 | 837 | */ |
michael@0 | 838 | _onBreakpointClick: function(e) { |
michael@0 | 839 | let sourceItem = this.getItemForElement(e.target); |
michael@0 | 840 | let breakpointItem = this.getItemForElement.call(sourceItem, e.target); |
michael@0 | 841 | let attachment = breakpointItem.attachment; |
michael@0 | 842 | |
michael@0 | 843 | // Check if this is an enabled conditional breakpoint. |
michael@0 | 844 | let breakpointPromise = DebuggerController.Breakpoints._getAdded(attachment); |
michael@0 | 845 | if (breakpointPromise) { |
michael@0 | 846 | breakpointPromise.then(aBreakpointClient => { |
michael@0 | 847 | doHighlight.call(this, aBreakpointClient.hasCondition()); |
michael@0 | 848 | }); |
michael@0 | 849 | } else { |
michael@0 | 850 | doHighlight.call(this, false); |
michael@0 | 851 | } |
michael@0 | 852 | |
michael@0 | 853 | function doHighlight(aConditionalBreakpointFlag) { |
michael@0 | 854 | // Highlight the breakpoint in this pane and in the editor. |
michael@0 | 855 | this.highlightBreakpoint(attachment, { |
michael@0 | 856 | // Don't show the conditional expression popup if this is not a |
michael@0 | 857 | // conditional breakpoint, or the right mouse button was pressed (to |
michael@0 | 858 | // avoid clashing the popup with the context menu). |
michael@0 | 859 | openPopup: aConditionalBreakpointFlag && e.button == 0 |
michael@0 | 860 | }); |
michael@0 | 861 | } |
michael@0 | 862 | }, |
michael@0 | 863 | |
michael@0 | 864 | /** |
michael@0 | 865 | * The click listener for a breakpoint checkbox. |
michael@0 | 866 | */ |
michael@0 | 867 | _onBreakpointCheckboxClick: function(e) { |
michael@0 | 868 | let sourceItem = this.getItemForElement(e.target); |
michael@0 | 869 | let breakpointItem = this.getItemForElement.call(sourceItem, e.target); |
michael@0 | 870 | let attachment = breakpointItem.attachment; |
michael@0 | 871 | |
michael@0 | 872 | // Toggle the breakpoint enabled or disabled. |
michael@0 | 873 | this[attachment.disabled ? "enableBreakpoint" : "disableBreakpoint"](attachment, { |
michael@0 | 874 | // Do this silently (don't update the checkbox checked state), since |
michael@0 | 875 | // this listener is triggered because a checkbox was already clicked. |
michael@0 | 876 | silent: true |
michael@0 | 877 | }); |
michael@0 | 878 | |
michael@0 | 879 | // Don't update the editor location (avoid propagating into _onBreakpointClick). |
michael@0 | 880 | e.preventDefault(); |
michael@0 | 881 | e.stopPropagation(); |
michael@0 | 882 | }, |
michael@0 | 883 | |
michael@0 | 884 | /** |
michael@0 | 885 | * The popup showing listener for the breakpoints conditional expression panel. |
michael@0 | 886 | */ |
michael@0 | 887 | _onConditionalPopupShowing: function() { |
michael@0 | 888 | this._conditionalPopupVisible = true; // Used in tests. |
michael@0 | 889 | window.emit(EVENTS.CONDITIONAL_BREAKPOINT_POPUP_SHOWING); |
michael@0 | 890 | }, |
michael@0 | 891 | |
michael@0 | 892 | /** |
michael@0 | 893 | * The popup shown listener for the breakpoints conditional expression panel. |
michael@0 | 894 | */ |
michael@0 | 895 | _onConditionalPopupShown: function() { |
michael@0 | 896 | this._cbTextbox.focus(); |
michael@0 | 897 | this._cbTextbox.select(); |
michael@0 | 898 | }, |
michael@0 | 899 | |
michael@0 | 900 | /** |
michael@0 | 901 | * The popup hiding listener for the breakpoints conditional expression panel. |
michael@0 | 902 | */ |
michael@0 | 903 | _onConditionalPopupHiding: Task.async(function*() { |
michael@0 | 904 | this._conditionalPopupVisible = false; // Used in tests. |
michael@0 | 905 | let breakpointItem = this._selectedBreakpointItem; |
michael@0 | 906 | let attachment = breakpointItem.attachment; |
michael@0 | 907 | |
michael@0 | 908 | // Check if this is an enabled conditional breakpoint, and if so, |
michael@0 | 909 | // save the current conditional epression. |
michael@0 | 910 | let breakpointPromise = DebuggerController.Breakpoints._getAdded(attachment); |
michael@0 | 911 | if (breakpointPromise) { |
michael@0 | 912 | let breakpointClient = yield breakpointPromise; |
michael@0 | 913 | yield DebuggerController.Breakpoints.updateCondition( |
michael@0 | 914 | breakpointClient.location, |
michael@0 | 915 | this._cbTextbox.value |
michael@0 | 916 | ); |
michael@0 | 917 | } |
michael@0 | 918 | |
michael@0 | 919 | window.emit(EVENTS.CONDITIONAL_BREAKPOINT_POPUP_HIDING); |
michael@0 | 920 | }), |
michael@0 | 921 | |
michael@0 | 922 | /** |
michael@0 | 923 | * The keypress listener for the breakpoints conditional expression textbox. |
michael@0 | 924 | */ |
michael@0 | 925 | _onConditionalTextboxKeyPress: function(e) { |
michael@0 | 926 | if (e.keyCode == e.DOM_VK_RETURN) { |
michael@0 | 927 | this._hideConditionalPopup(); |
michael@0 | 928 | } |
michael@0 | 929 | }, |
michael@0 | 930 | |
michael@0 | 931 | /** |
michael@0 | 932 | * Called when the add breakpoint key sequence was pressed. |
michael@0 | 933 | */ |
michael@0 | 934 | _onCmdAddBreakpoint: function(e) { |
michael@0 | 935 | let url = DebuggerView.Sources.selectedValue; |
michael@0 | 936 | let line = DebuggerView.editor.getCursor().line + 1; |
michael@0 | 937 | let location = { url: url, line: line }; |
michael@0 | 938 | let breakpointItem = this.getBreakpoint(location); |
michael@0 | 939 | |
michael@0 | 940 | // If a breakpoint already existed, remove it now. |
michael@0 | 941 | if (breakpointItem) { |
michael@0 | 942 | DebuggerController.Breakpoints.removeBreakpoint(location); |
michael@0 | 943 | } |
michael@0 | 944 | // No breakpoint existed at the required location, add one now. |
michael@0 | 945 | else { |
michael@0 | 946 | DebuggerController.Breakpoints.addBreakpoint(location); |
michael@0 | 947 | } |
michael@0 | 948 | }, |
michael@0 | 949 | |
michael@0 | 950 | /** |
michael@0 | 951 | * Called when the add conditional breakpoint key sequence was pressed. |
michael@0 | 952 | */ |
michael@0 | 953 | _onCmdAddConditionalBreakpoint: function() { |
michael@0 | 954 | let url = DebuggerView.Sources.selectedValue; |
michael@0 | 955 | let line = DebuggerView.editor.getCursor().line + 1; |
michael@0 | 956 | let location = { url: url, line: line }; |
michael@0 | 957 | let breakpointItem = this.getBreakpoint(location); |
michael@0 | 958 | |
michael@0 | 959 | // If a breakpoint already existed or wasn't a conditional, morph it now. |
michael@0 | 960 | if (breakpointItem) { |
michael@0 | 961 | this.highlightBreakpoint(location, { openPopup: true }); |
michael@0 | 962 | } |
michael@0 | 963 | // No breakpoint existed at the required location, add one now. |
michael@0 | 964 | else { |
michael@0 | 965 | DebuggerController.Breakpoints.addBreakpoint(location, { openPopup: true }); |
michael@0 | 966 | } |
michael@0 | 967 | }, |
michael@0 | 968 | |
michael@0 | 969 | /** |
michael@0 | 970 | * Function invoked on the "setConditional" menuitem command. |
michael@0 | 971 | * |
michael@0 | 972 | * @param object aLocation |
michael@0 | 973 | * @see DebuggerController.Breakpoints.addBreakpoint |
michael@0 | 974 | */ |
michael@0 | 975 | _onSetConditional: function(aLocation) { |
michael@0 | 976 | // Highlight the breakpoint and show a conditional expression popup. |
michael@0 | 977 | this.highlightBreakpoint(aLocation, { openPopup: true }); |
michael@0 | 978 | }, |
michael@0 | 979 | |
michael@0 | 980 | /** |
michael@0 | 981 | * Function invoked on the "enableSelf" menuitem command. |
michael@0 | 982 | * |
michael@0 | 983 | * @param object aLocation |
michael@0 | 984 | * @see DebuggerController.Breakpoints.addBreakpoint |
michael@0 | 985 | */ |
michael@0 | 986 | _onEnableSelf: function(aLocation) { |
michael@0 | 987 | // Enable the breakpoint, in this container and the controller store. |
michael@0 | 988 | this.enableBreakpoint(aLocation); |
michael@0 | 989 | }, |
michael@0 | 990 | |
michael@0 | 991 | /** |
michael@0 | 992 | * Function invoked on the "disableSelf" menuitem command. |
michael@0 | 993 | * |
michael@0 | 994 | * @param object aLocation |
michael@0 | 995 | * @see DebuggerController.Breakpoints.addBreakpoint |
michael@0 | 996 | */ |
michael@0 | 997 | _onDisableSelf: function(aLocation) { |
michael@0 | 998 | // Disable the breakpoint, in this container and the controller store. |
michael@0 | 999 | this.disableBreakpoint(aLocation); |
michael@0 | 1000 | }, |
michael@0 | 1001 | |
michael@0 | 1002 | /** |
michael@0 | 1003 | * Function invoked on the "deleteSelf" menuitem command. |
michael@0 | 1004 | * |
michael@0 | 1005 | * @param object aLocation |
michael@0 | 1006 | * @see DebuggerController.Breakpoints.addBreakpoint |
michael@0 | 1007 | */ |
michael@0 | 1008 | _onDeleteSelf: function(aLocation) { |
michael@0 | 1009 | // Remove the breakpoint, from this container and the controller store. |
michael@0 | 1010 | this.removeBreakpoint(aLocation); |
michael@0 | 1011 | DebuggerController.Breakpoints.removeBreakpoint(aLocation); |
michael@0 | 1012 | }, |
michael@0 | 1013 | |
michael@0 | 1014 | /** |
michael@0 | 1015 | * Function invoked on the "enableOthers" menuitem command. |
michael@0 | 1016 | * |
michael@0 | 1017 | * @param object aLocation |
michael@0 | 1018 | * @see DebuggerController.Breakpoints.addBreakpoint |
michael@0 | 1019 | */ |
michael@0 | 1020 | _onEnableOthers: function(aLocation) { |
michael@0 | 1021 | let enableOthers = aCallback => { |
michael@0 | 1022 | let other = this.getOtherBreakpoints(aLocation); |
michael@0 | 1023 | let outstanding = other.map(e => this.enableBreakpoint(e.attachment)); |
michael@0 | 1024 | promise.all(outstanding).then(aCallback); |
michael@0 | 1025 | } |
michael@0 | 1026 | |
michael@0 | 1027 | // Breakpoints can only be set while the debuggee is paused. To avoid |
michael@0 | 1028 | // an avalanche of pause/resume interrupts of the main thread, simply |
michael@0 | 1029 | // pause it beforehand if it's not already. |
michael@0 | 1030 | if (gThreadClient.state != "paused") { |
michael@0 | 1031 | gThreadClient.interrupt(() => enableOthers(() => gThreadClient.resume())); |
michael@0 | 1032 | } else { |
michael@0 | 1033 | enableOthers(); |
michael@0 | 1034 | } |
michael@0 | 1035 | }, |
michael@0 | 1036 | |
michael@0 | 1037 | /** |
michael@0 | 1038 | * Function invoked on the "disableOthers" menuitem command. |
michael@0 | 1039 | * |
michael@0 | 1040 | * @param object aLocation |
michael@0 | 1041 | * @see DebuggerController.Breakpoints.addBreakpoint |
michael@0 | 1042 | */ |
michael@0 | 1043 | _onDisableOthers: function(aLocation) { |
michael@0 | 1044 | let other = this.getOtherBreakpoints(aLocation); |
michael@0 | 1045 | other.forEach(e => this._onDisableSelf(e.attachment)); |
michael@0 | 1046 | }, |
michael@0 | 1047 | |
michael@0 | 1048 | /** |
michael@0 | 1049 | * Function invoked on the "deleteOthers" menuitem command. |
michael@0 | 1050 | * |
michael@0 | 1051 | * @param object aLocation |
michael@0 | 1052 | * @see DebuggerController.Breakpoints.addBreakpoint |
michael@0 | 1053 | */ |
michael@0 | 1054 | _onDeleteOthers: function(aLocation) { |
michael@0 | 1055 | let other = this.getOtherBreakpoints(aLocation); |
michael@0 | 1056 | other.forEach(e => this._onDeleteSelf(e.attachment)); |
michael@0 | 1057 | }, |
michael@0 | 1058 | |
michael@0 | 1059 | /** |
michael@0 | 1060 | * Function invoked on the "enableAll" menuitem command. |
michael@0 | 1061 | */ |
michael@0 | 1062 | _onEnableAll: function() { |
michael@0 | 1063 | this._onEnableOthers(undefined); |
michael@0 | 1064 | }, |
michael@0 | 1065 | |
michael@0 | 1066 | /** |
michael@0 | 1067 | * Function invoked on the "disableAll" menuitem command. |
michael@0 | 1068 | */ |
michael@0 | 1069 | _onDisableAll: function() { |
michael@0 | 1070 | this._onDisableOthers(undefined); |
michael@0 | 1071 | }, |
michael@0 | 1072 | |
michael@0 | 1073 | /** |
michael@0 | 1074 | * Function invoked on the "deleteAll" menuitem command. |
michael@0 | 1075 | */ |
michael@0 | 1076 | _onDeleteAll: function() { |
michael@0 | 1077 | this._onDeleteOthers(undefined); |
michael@0 | 1078 | }, |
michael@0 | 1079 | |
michael@0 | 1080 | _commandset: null, |
michael@0 | 1081 | _popupset: null, |
michael@0 | 1082 | _cmPopup: null, |
michael@0 | 1083 | _cbPanel: null, |
michael@0 | 1084 | _cbTextbox: null, |
michael@0 | 1085 | _selectedBreakpointItem: null, |
michael@0 | 1086 | _conditionalPopupVisible: false |
michael@0 | 1087 | }); |
michael@0 | 1088 | |
michael@0 | 1089 | /** |
michael@0 | 1090 | * Functions handling the traces UI. |
michael@0 | 1091 | */ |
michael@0 | 1092 | function TracerView() { |
michael@0 | 1093 | this._selectedItem = null; |
michael@0 | 1094 | this._matchingItems = null; |
michael@0 | 1095 | this.widget = null; |
michael@0 | 1096 | |
michael@0 | 1097 | this._highlightItem = this._highlightItem.bind(this); |
michael@0 | 1098 | this._isNotSelectedItem = this._isNotSelectedItem.bind(this); |
michael@0 | 1099 | |
michael@0 | 1100 | this._unhighlightMatchingItems = |
michael@0 | 1101 | DevToolsUtils.makeInfallible(this._unhighlightMatchingItems.bind(this)); |
michael@0 | 1102 | this._onToggleTracing = |
michael@0 | 1103 | DevToolsUtils.makeInfallible(this._onToggleTracing.bind(this)); |
michael@0 | 1104 | this._onStartTracing = |
michael@0 | 1105 | DevToolsUtils.makeInfallible(this._onStartTracing.bind(this)); |
michael@0 | 1106 | this._onClear = |
michael@0 | 1107 | DevToolsUtils.makeInfallible(this._onClear.bind(this)); |
michael@0 | 1108 | this._onSelect = |
michael@0 | 1109 | DevToolsUtils.makeInfallible(this._onSelect.bind(this)); |
michael@0 | 1110 | this._onMouseOver = |
michael@0 | 1111 | DevToolsUtils.makeInfallible(this._onMouseOver.bind(this)); |
michael@0 | 1112 | this._onSearch = DevToolsUtils.makeInfallible(this._onSearch.bind(this)); |
michael@0 | 1113 | } |
michael@0 | 1114 | |
michael@0 | 1115 | TracerView.MAX_TRACES = 200; |
michael@0 | 1116 | |
michael@0 | 1117 | TracerView.prototype = Heritage.extend(WidgetMethods, { |
michael@0 | 1118 | /** |
michael@0 | 1119 | * Initialization function, called when the debugger is started. |
michael@0 | 1120 | */ |
michael@0 | 1121 | initialize: function() { |
michael@0 | 1122 | dumpn("Initializing the TracerView"); |
michael@0 | 1123 | |
michael@0 | 1124 | this._traceButton = document.getElementById("trace"); |
michael@0 | 1125 | this._tracerTab = document.getElementById("tracer-tab"); |
michael@0 | 1126 | |
michael@0 | 1127 | // Remove tracer related elements from the dom and tear everything down if |
michael@0 | 1128 | // the tracer isn't enabled. |
michael@0 | 1129 | if (!Prefs.tracerEnabled) { |
michael@0 | 1130 | this._traceButton.remove(); |
michael@0 | 1131 | this._traceButton = null; |
michael@0 | 1132 | this._tracerTab.remove(); |
michael@0 | 1133 | this._tracerTab = null; |
michael@0 | 1134 | return; |
michael@0 | 1135 | } |
michael@0 | 1136 | |
michael@0 | 1137 | this.widget = new FastListWidget(document.getElementById("tracer-traces")); |
michael@0 | 1138 | this._traceButton.removeAttribute("hidden"); |
michael@0 | 1139 | this._tracerTab.removeAttribute("hidden"); |
michael@0 | 1140 | |
michael@0 | 1141 | this._search = document.getElementById("tracer-search"); |
michael@0 | 1142 | this._template = document.getElementsByClassName("trace-item-template")[0]; |
michael@0 | 1143 | this._templateItem = this._template.getElementsByClassName("trace-item")[0]; |
michael@0 | 1144 | this._templateTypeIcon = this._template.getElementsByClassName("trace-type")[0]; |
michael@0 | 1145 | this._templateNameNode = this._template.getElementsByClassName("trace-name")[0]; |
michael@0 | 1146 | |
michael@0 | 1147 | this.widget.addEventListener("select", this._onSelect, false); |
michael@0 | 1148 | this.widget.addEventListener("mouseover", this._onMouseOver, false); |
michael@0 | 1149 | this.widget.addEventListener("mouseout", this._unhighlightMatchingItems, false); |
michael@0 | 1150 | this._search.addEventListener("input", this._onSearch, false); |
michael@0 | 1151 | |
michael@0 | 1152 | this._startTooltip = L10N.getStr("startTracingTooltip"); |
michael@0 | 1153 | this._stopTooltip = L10N.getStr("stopTracingTooltip"); |
michael@0 | 1154 | this._tracingNotStartedString = L10N.getStr("tracingNotStartedText"); |
michael@0 | 1155 | this._noFunctionCallsString = L10N.getStr("noFunctionCallsText"); |
michael@0 | 1156 | |
michael@0 | 1157 | this._traceButton.setAttribute("tooltiptext", this._startTooltip); |
michael@0 | 1158 | this.emptyText = this._tracingNotStartedString; |
michael@0 | 1159 | }, |
michael@0 | 1160 | |
michael@0 | 1161 | /** |
michael@0 | 1162 | * Destruction function, called when the debugger is closed. |
michael@0 | 1163 | */ |
michael@0 | 1164 | destroy: function() { |
michael@0 | 1165 | dumpn("Destroying the TracerView"); |
michael@0 | 1166 | |
michael@0 | 1167 | if (!this.widget) { |
michael@0 | 1168 | return; |
michael@0 | 1169 | } |
michael@0 | 1170 | |
michael@0 | 1171 | this.widget.removeEventListener("select", this._onSelect, false); |
michael@0 | 1172 | this.widget.removeEventListener("mouseover", this._onMouseOver, false); |
michael@0 | 1173 | this.widget.removeEventListener("mouseout", this._unhighlightMatchingItems, false); |
michael@0 | 1174 | this._search.removeEventListener("input", this._onSearch, false); |
michael@0 | 1175 | }, |
michael@0 | 1176 | |
michael@0 | 1177 | /** |
michael@0 | 1178 | * Function invoked by the "toggleTracing" command to switch the tracer state. |
michael@0 | 1179 | */ |
michael@0 | 1180 | _onToggleTracing: function() { |
michael@0 | 1181 | if (DebuggerController.Tracer.tracing) { |
michael@0 | 1182 | this._onStopTracing(); |
michael@0 | 1183 | } else { |
michael@0 | 1184 | this._onStartTracing(); |
michael@0 | 1185 | } |
michael@0 | 1186 | }, |
michael@0 | 1187 | |
michael@0 | 1188 | /** |
michael@0 | 1189 | * Function invoked either by the "startTracing" command or by |
michael@0 | 1190 | * _onToggleTracing to start execution tracing in the backend. |
michael@0 | 1191 | * |
michael@0 | 1192 | * @return object |
michael@0 | 1193 | * A promise resolved once the tracing has successfully started. |
michael@0 | 1194 | */ |
michael@0 | 1195 | _onStartTracing: function() { |
michael@0 | 1196 | this._traceButton.setAttribute("checked", true); |
michael@0 | 1197 | this._traceButton.setAttribute("tooltiptext", this._stopTooltip); |
michael@0 | 1198 | |
michael@0 | 1199 | this.empty(); |
michael@0 | 1200 | this.emptyText = this._noFunctionCallsString; |
michael@0 | 1201 | |
michael@0 | 1202 | let deferred = promise.defer(); |
michael@0 | 1203 | DebuggerController.Tracer.startTracing(deferred.resolve); |
michael@0 | 1204 | return deferred.promise; |
michael@0 | 1205 | }, |
michael@0 | 1206 | |
michael@0 | 1207 | /** |
michael@0 | 1208 | * Function invoked by _onToggleTracing to stop execution tracing in the |
michael@0 | 1209 | * backend. |
michael@0 | 1210 | * |
michael@0 | 1211 | * @return object |
michael@0 | 1212 | * A promise resolved once the tracing has successfully stopped. |
michael@0 | 1213 | */ |
michael@0 | 1214 | _onStopTracing: function() { |
michael@0 | 1215 | this._traceButton.removeAttribute("checked"); |
michael@0 | 1216 | this._traceButton.setAttribute("tooltiptext", this._startTooltip); |
michael@0 | 1217 | |
michael@0 | 1218 | this.emptyText = this._tracingNotStartedString; |
michael@0 | 1219 | |
michael@0 | 1220 | let deferred = promise.defer(); |
michael@0 | 1221 | DebuggerController.Tracer.stopTracing(deferred.resolve); |
michael@0 | 1222 | return deferred.promise; |
michael@0 | 1223 | }, |
michael@0 | 1224 | |
michael@0 | 1225 | /** |
michael@0 | 1226 | * Function invoked by the "clearTraces" command to empty the traces pane. |
michael@0 | 1227 | */ |
michael@0 | 1228 | _onClear: function() { |
michael@0 | 1229 | this.empty(); |
michael@0 | 1230 | }, |
michael@0 | 1231 | |
michael@0 | 1232 | /** |
michael@0 | 1233 | * Populate the given parent scope with the variable with the provided name |
michael@0 | 1234 | * and value. |
michael@0 | 1235 | * |
michael@0 | 1236 | * @param String aName |
michael@0 | 1237 | * The name of the variable. |
michael@0 | 1238 | * @param Object aParent |
michael@0 | 1239 | * The parent scope. |
michael@0 | 1240 | * @param Object aValue |
michael@0 | 1241 | * The value of the variable. |
michael@0 | 1242 | */ |
michael@0 | 1243 | _populateVariable: function(aName, aParent, aValue) { |
michael@0 | 1244 | let item = aParent.addItem(aName, { value: aValue }); |
michael@0 | 1245 | if (aValue) { |
michael@0 | 1246 | let wrappedValue = new DebuggerController.Tracer.WrappedObject(aValue); |
michael@0 | 1247 | DebuggerView.Variables.controller.populate(item, wrappedValue); |
michael@0 | 1248 | item.expand(); |
michael@0 | 1249 | item.twisty = false; |
michael@0 | 1250 | } |
michael@0 | 1251 | }, |
michael@0 | 1252 | |
michael@0 | 1253 | /** |
michael@0 | 1254 | * Handler for the widget's "select" event. Displays parameters, exception, or |
michael@0 | 1255 | * return value depending on whether the selected trace is a call, throw, or |
michael@0 | 1256 | * return respectively. |
michael@0 | 1257 | * |
michael@0 | 1258 | * @param Object traceItem |
michael@0 | 1259 | * The selected trace item. |
michael@0 | 1260 | */ |
michael@0 | 1261 | _onSelect: function _onSelect({ detail: traceItem }) { |
michael@0 | 1262 | if (!traceItem) { |
michael@0 | 1263 | return; |
michael@0 | 1264 | } |
michael@0 | 1265 | |
michael@0 | 1266 | const data = traceItem.attachment.trace; |
michael@0 | 1267 | const { location: { url, line } } = data; |
michael@0 | 1268 | DebuggerView.setEditorLocation(url, line, { noDebug: true }); |
michael@0 | 1269 | |
michael@0 | 1270 | DebuggerView.Variables.empty(); |
michael@0 | 1271 | const scope = DebuggerView.Variables.addScope(); |
michael@0 | 1272 | |
michael@0 | 1273 | if (data.type == "call") { |
michael@0 | 1274 | const params = DevToolsUtils.zip(data.parameterNames, data.arguments); |
michael@0 | 1275 | for (let [name, val] of params) { |
michael@0 | 1276 | if (val === undefined) { |
michael@0 | 1277 | scope.addItem(name, { value: "<value not available>" }); |
michael@0 | 1278 | } else { |
michael@0 | 1279 | this._populateVariable(name, scope, val); |
michael@0 | 1280 | } |
michael@0 | 1281 | } |
michael@0 | 1282 | } else { |
michael@0 | 1283 | const varName = "<" + (data.type == "throw" ? "exception" : data.type) + ">"; |
michael@0 | 1284 | this._populateVariable(varName, scope, data.returnVal); |
michael@0 | 1285 | } |
michael@0 | 1286 | |
michael@0 | 1287 | scope.expand(); |
michael@0 | 1288 | DebuggerView.showInstrumentsPane(); |
michael@0 | 1289 | }, |
michael@0 | 1290 | |
michael@0 | 1291 | /** |
michael@0 | 1292 | * Add the hover frame enter/exit highlighting to a given item. |
michael@0 | 1293 | */ |
michael@0 | 1294 | _highlightItem: function(aItem) { |
michael@0 | 1295 | if (!aItem || !aItem.target) { |
michael@0 | 1296 | return; |
michael@0 | 1297 | } |
michael@0 | 1298 | const trace = aItem.target.querySelector(".trace-item"); |
michael@0 | 1299 | trace.classList.add("selected-matching"); |
michael@0 | 1300 | }, |
michael@0 | 1301 | |
michael@0 | 1302 | /** |
michael@0 | 1303 | * Remove the hover frame enter/exit highlighting to a given item. |
michael@0 | 1304 | */ |
michael@0 | 1305 | _unhighlightItem: function(aItem) { |
michael@0 | 1306 | if (!aItem || !aItem.target) { |
michael@0 | 1307 | return; |
michael@0 | 1308 | } |
michael@0 | 1309 | const match = aItem.target.querySelector(".selected-matching"); |
michael@0 | 1310 | if (match) { |
michael@0 | 1311 | match.classList.remove("selected-matching"); |
michael@0 | 1312 | } |
michael@0 | 1313 | }, |
michael@0 | 1314 | |
michael@0 | 1315 | /** |
michael@0 | 1316 | * Remove the frame enter/exit pair highlighting we do when hovering. |
michael@0 | 1317 | */ |
michael@0 | 1318 | _unhighlightMatchingItems: function() { |
michael@0 | 1319 | if (this._matchingItems) { |
michael@0 | 1320 | this._matchingItems.forEach(this._unhighlightItem); |
michael@0 | 1321 | this._matchingItems = null; |
michael@0 | 1322 | } |
michael@0 | 1323 | }, |
michael@0 | 1324 | |
michael@0 | 1325 | /** |
michael@0 | 1326 | * Returns true if the given item is not the selected item. |
michael@0 | 1327 | */ |
michael@0 | 1328 | _isNotSelectedItem: function(aItem) { |
michael@0 | 1329 | return aItem !== this.selectedItem; |
michael@0 | 1330 | }, |
michael@0 | 1331 | |
michael@0 | 1332 | /** |
michael@0 | 1333 | * Highlight the frame enter/exit pair of items for the given item. |
michael@0 | 1334 | */ |
michael@0 | 1335 | _highlightMatchingItems: function(aItem) { |
michael@0 | 1336 | const frameId = aItem.attachment.trace.frameId; |
michael@0 | 1337 | const predicate = e => e.attachment.trace.frameId == frameId; |
michael@0 | 1338 | |
michael@0 | 1339 | this._unhighlightMatchingItems(); |
michael@0 | 1340 | this._matchingItems = this.items.filter(predicate); |
michael@0 | 1341 | this._matchingItems |
michael@0 | 1342 | .filter(this._isNotSelectedItem) |
michael@0 | 1343 | .forEach(this._highlightItem); |
michael@0 | 1344 | }, |
michael@0 | 1345 | |
michael@0 | 1346 | /** |
michael@0 | 1347 | * Listener for the mouseover event. |
michael@0 | 1348 | */ |
michael@0 | 1349 | _onMouseOver: function({ target }) { |
michael@0 | 1350 | const traceItem = this.getItemForElement(target); |
michael@0 | 1351 | if (traceItem) { |
michael@0 | 1352 | this._highlightMatchingItems(traceItem); |
michael@0 | 1353 | } |
michael@0 | 1354 | }, |
michael@0 | 1355 | |
michael@0 | 1356 | /** |
michael@0 | 1357 | * Listener for typing in the search box. |
michael@0 | 1358 | */ |
michael@0 | 1359 | _onSearch: function() { |
michael@0 | 1360 | const query = this._search.value.trim().toLowerCase(); |
michael@0 | 1361 | const predicate = name => name.toLowerCase().contains(query); |
michael@0 | 1362 | this.filterContents(item => predicate(item.attachment.trace.name)); |
michael@0 | 1363 | }, |
michael@0 | 1364 | |
michael@0 | 1365 | /** |
michael@0 | 1366 | * Select the traces tab in the sidebar. |
michael@0 | 1367 | */ |
michael@0 | 1368 | selectTab: function() { |
michael@0 | 1369 | const tabs = this._tracerTab.parentElement; |
michael@0 | 1370 | tabs.selectedIndex = Array.indexOf(tabs.children, this._tracerTab); |
michael@0 | 1371 | }, |
michael@0 | 1372 | |
michael@0 | 1373 | /** |
michael@0 | 1374 | * Commit all staged items to the widget. Overridden so that we can call |
michael@0 | 1375 | * |FastListWidget.prototype.flush|. |
michael@0 | 1376 | */ |
michael@0 | 1377 | commit: function() { |
michael@0 | 1378 | WidgetMethods.commit.call(this); |
michael@0 | 1379 | // TODO: Accessing non-standard widget properties. Figure out what's the |
michael@0 | 1380 | // best way to expose such things. Bug 895514. |
michael@0 | 1381 | this.widget.flush(); |
michael@0 | 1382 | }, |
michael@0 | 1383 | |
michael@0 | 1384 | /** |
michael@0 | 1385 | * Adds the trace record provided as an argument to the view. |
michael@0 | 1386 | * |
michael@0 | 1387 | * @param object aTrace |
michael@0 | 1388 | * The trace record coming from the tracer actor. |
michael@0 | 1389 | */ |
michael@0 | 1390 | addTrace: function(aTrace) { |
michael@0 | 1391 | // Create the element node for the trace item. |
michael@0 | 1392 | let view = this._createView(aTrace); |
michael@0 | 1393 | |
michael@0 | 1394 | // Append a source item to this container. |
michael@0 | 1395 | this.push([view], { |
michael@0 | 1396 | staged: true, |
michael@0 | 1397 | attachment: { |
michael@0 | 1398 | trace: aTrace |
michael@0 | 1399 | } |
michael@0 | 1400 | }); |
michael@0 | 1401 | }, |
michael@0 | 1402 | |
michael@0 | 1403 | /** |
michael@0 | 1404 | * Customization function for creating an item's UI. |
michael@0 | 1405 | * |
michael@0 | 1406 | * @return nsIDOMNode |
michael@0 | 1407 | * The network request view. |
michael@0 | 1408 | */ |
michael@0 | 1409 | _createView: function(aTrace) { |
michael@0 | 1410 | let { type, name, location, depth, frameId } = aTrace; |
michael@0 | 1411 | let { parameterNames, returnVal, arguments: args } = aTrace; |
michael@0 | 1412 | let fragment = document.createDocumentFragment(); |
michael@0 | 1413 | |
michael@0 | 1414 | this._templateItem.setAttribute("tooltiptext", SourceUtils.trimUrl(location.url)); |
michael@0 | 1415 | this._templateItem.style.MozPaddingStart = depth + "em"; |
michael@0 | 1416 | |
michael@0 | 1417 | const TYPES = ["call", "yield", "return", "throw"]; |
michael@0 | 1418 | for (let t of TYPES) { |
michael@0 | 1419 | this._templateTypeIcon.classList.toggle("trace-" + t, t == type); |
michael@0 | 1420 | } |
michael@0 | 1421 | this._templateTypeIcon.setAttribute("value", { |
michael@0 | 1422 | call: "\u2192", |
michael@0 | 1423 | yield: "Y", |
michael@0 | 1424 | return: "\u2190", |
michael@0 | 1425 | throw: "E", |
michael@0 | 1426 | terminated: "TERMINATED" |
michael@0 | 1427 | }[type]); |
michael@0 | 1428 | |
michael@0 | 1429 | this._templateNameNode.setAttribute("value", name); |
michael@0 | 1430 | |
michael@0 | 1431 | // All extra syntax and parameter nodes added. |
michael@0 | 1432 | const addedNodes = []; |
michael@0 | 1433 | |
michael@0 | 1434 | if (parameterNames) { |
michael@0 | 1435 | const syntax = (p) => { |
michael@0 | 1436 | const el = document.createElement("label"); |
michael@0 | 1437 | el.setAttribute("value", p); |
michael@0 | 1438 | el.classList.add("trace-syntax"); |
michael@0 | 1439 | el.classList.add("plain"); |
michael@0 | 1440 | addedNodes.push(el); |
michael@0 | 1441 | return el; |
michael@0 | 1442 | }; |
michael@0 | 1443 | |
michael@0 | 1444 | this._templateItem.appendChild(syntax("(")); |
michael@0 | 1445 | |
michael@0 | 1446 | for (let i = 0, n = parameterNames.length; i < n; i++) { |
michael@0 | 1447 | let param = document.createElement("label"); |
michael@0 | 1448 | param.setAttribute("value", parameterNames[i]); |
michael@0 | 1449 | param.classList.add("trace-param"); |
michael@0 | 1450 | param.classList.add("plain"); |
michael@0 | 1451 | addedNodes.push(param); |
michael@0 | 1452 | this._templateItem.appendChild(param); |
michael@0 | 1453 | |
michael@0 | 1454 | if (i + 1 !== n) { |
michael@0 | 1455 | this._templateItem.appendChild(syntax(", ")); |
michael@0 | 1456 | } |
michael@0 | 1457 | } |
michael@0 | 1458 | |
michael@0 | 1459 | this._templateItem.appendChild(syntax(")")); |
michael@0 | 1460 | } |
michael@0 | 1461 | |
michael@0 | 1462 | // Flatten the DOM by removing one redundant box (the template container). |
michael@0 | 1463 | for (let node of this._template.childNodes) { |
michael@0 | 1464 | fragment.appendChild(node.cloneNode(true)); |
michael@0 | 1465 | } |
michael@0 | 1466 | |
michael@0 | 1467 | // Remove any added nodes from the template. |
michael@0 | 1468 | for (let node of addedNodes) { |
michael@0 | 1469 | this._templateItem.removeChild(node); |
michael@0 | 1470 | } |
michael@0 | 1471 | |
michael@0 | 1472 | return fragment; |
michael@0 | 1473 | } |
michael@0 | 1474 | }); |
michael@0 | 1475 | |
michael@0 | 1476 | /** |
michael@0 | 1477 | * Utility functions for handling sources. |
michael@0 | 1478 | */ |
michael@0 | 1479 | let SourceUtils = { |
michael@0 | 1480 | _labelsCache: new Map(), // Can't use WeakMaps because keys are strings. |
michael@0 | 1481 | _groupsCache: new Map(), |
michael@0 | 1482 | _minifiedCache: new WeakMap(), |
michael@0 | 1483 | |
michael@0 | 1484 | /** |
michael@0 | 1485 | * Returns true if the specified url and/or content type are specific to |
michael@0 | 1486 | * javascript files. |
michael@0 | 1487 | * |
michael@0 | 1488 | * @return boolean |
michael@0 | 1489 | * True if the source is likely javascript. |
michael@0 | 1490 | */ |
michael@0 | 1491 | isJavaScript: function(aUrl, aContentType = "") { |
michael@0 | 1492 | return /\.jsm?$/.test(this.trimUrlQuery(aUrl)) || |
michael@0 | 1493 | aContentType.contains("javascript"); |
michael@0 | 1494 | }, |
michael@0 | 1495 | |
michael@0 | 1496 | /** |
michael@0 | 1497 | * Determines if the source text is minified by using |
michael@0 | 1498 | * the percentage indented of a subset of lines |
michael@0 | 1499 | * |
michael@0 | 1500 | * @param string aText |
michael@0 | 1501 | * The source text. |
michael@0 | 1502 | * @return boolean |
michael@0 | 1503 | * True if source text is minified. |
michael@0 | 1504 | */ |
michael@0 | 1505 | isMinified: function(sourceClient, aText){ |
michael@0 | 1506 | if (this._minifiedCache.has(sourceClient)) { |
michael@0 | 1507 | return this._minifiedCache.get(sourceClient); |
michael@0 | 1508 | } |
michael@0 | 1509 | |
michael@0 | 1510 | let isMinified; |
michael@0 | 1511 | let lineEndIndex = 0; |
michael@0 | 1512 | let lineStartIndex = 0; |
michael@0 | 1513 | let lines = 0; |
michael@0 | 1514 | let indentCount = 0; |
michael@0 | 1515 | let overCharLimit = false; |
michael@0 | 1516 | |
michael@0 | 1517 | // Strip comments. |
michael@0 | 1518 | aText = aText.replace(/\/\*[\S\s]*?\*\/|\/\/(.+|\n)/g, ""); |
michael@0 | 1519 | |
michael@0 | 1520 | while (lines++ < SAMPLE_SIZE) { |
michael@0 | 1521 | lineEndIndex = aText.indexOf("\n", lineStartIndex); |
michael@0 | 1522 | if (lineEndIndex == -1) { |
michael@0 | 1523 | break; |
michael@0 | 1524 | } |
michael@0 | 1525 | if (/^\s+/.test(aText.slice(lineStartIndex, lineEndIndex))) { |
michael@0 | 1526 | indentCount++; |
michael@0 | 1527 | } |
michael@0 | 1528 | // For files with no indents but are not minified. |
michael@0 | 1529 | if ((lineEndIndex - lineStartIndex) > CHARACTER_LIMIT) { |
michael@0 | 1530 | overCharLimit = true; |
michael@0 | 1531 | break; |
michael@0 | 1532 | } |
michael@0 | 1533 | lineStartIndex = lineEndIndex + 1; |
michael@0 | 1534 | } |
michael@0 | 1535 | isMinified = ((indentCount / lines ) * 100) < INDENT_COUNT_THRESHOLD || |
michael@0 | 1536 | overCharLimit; |
michael@0 | 1537 | |
michael@0 | 1538 | this._minifiedCache.set(sourceClient, isMinified); |
michael@0 | 1539 | return isMinified; |
michael@0 | 1540 | }, |
michael@0 | 1541 | |
michael@0 | 1542 | /** |
michael@0 | 1543 | * Clears the labels, groups and minify cache, populated by methods like |
michael@0 | 1544 | * SourceUtils.getSourceLabel or Source Utils.getSourceGroup. |
michael@0 | 1545 | * This should be done every time the content location changes. |
michael@0 | 1546 | */ |
michael@0 | 1547 | clearCache: function() { |
michael@0 | 1548 | this._labelsCache.clear(); |
michael@0 | 1549 | this._groupsCache.clear(); |
michael@0 | 1550 | this._minifiedCache.clear(); |
michael@0 | 1551 | }, |
michael@0 | 1552 | |
michael@0 | 1553 | /** |
michael@0 | 1554 | * Gets a unique, simplified label from a source url. |
michael@0 | 1555 | * |
michael@0 | 1556 | * @param string aUrl |
michael@0 | 1557 | * The source url. |
michael@0 | 1558 | * @return string |
michael@0 | 1559 | * The simplified label. |
michael@0 | 1560 | */ |
michael@0 | 1561 | getSourceLabel: function(aUrl) { |
michael@0 | 1562 | let cachedLabel = this._labelsCache.get(aUrl); |
michael@0 | 1563 | if (cachedLabel) { |
michael@0 | 1564 | return cachedLabel; |
michael@0 | 1565 | } |
michael@0 | 1566 | |
michael@0 | 1567 | let sourceLabel = null; |
michael@0 | 1568 | |
michael@0 | 1569 | for (let name of Object.keys(KNOWN_SOURCE_GROUPS)) { |
michael@0 | 1570 | if (aUrl.startsWith(KNOWN_SOURCE_GROUPS[name])) { |
michael@0 | 1571 | sourceLabel = aUrl.substring(KNOWN_SOURCE_GROUPS[name].length); |
michael@0 | 1572 | } |
michael@0 | 1573 | } |
michael@0 | 1574 | |
michael@0 | 1575 | if (!sourceLabel) { |
michael@0 | 1576 | sourceLabel = this.trimUrl(aUrl); |
michael@0 | 1577 | } |
michael@0 | 1578 | let unicodeLabel = NetworkHelper.convertToUnicode(unescape(sourceLabel)); |
michael@0 | 1579 | this._labelsCache.set(aUrl, unicodeLabel); |
michael@0 | 1580 | return unicodeLabel; |
michael@0 | 1581 | }, |
michael@0 | 1582 | |
michael@0 | 1583 | /** |
michael@0 | 1584 | * Gets as much information as possible about the hostname and directory paths |
michael@0 | 1585 | * of an url to create a short url group identifier. |
michael@0 | 1586 | * |
michael@0 | 1587 | * @param string aUrl |
michael@0 | 1588 | * The source url. |
michael@0 | 1589 | * @return string |
michael@0 | 1590 | * The simplified group. |
michael@0 | 1591 | */ |
michael@0 | 1592 | getSourceGroup: function(aUrl) { |
michael@0 | 1593 | let cachedGroup = this._groupsCache.get(aUrl); |
michael@0 | 1594 | if (cachedGroup) { |
michael@0 | 1595 | return cachedGroup; |
michael@0 | 1596 | } |
michael@0 | 1597 | |
michael@0 | 1598 | try { |
michael@0 | 1599 | // Use an nsIURL to parse all the url path parts. |
michael@0 | 1600 | var uri = Services.io.newURI(aUrl, null, null).QueryInterface(Ci.nsIURL); |
michael@0 | 1601 | } catch (e) { |
michael@0 | 1602 | // This doesn't look like a url, or nsIURL can't handle it. |
michael@0 | 1603 | return ""; |
michael@0 | 1604 | } |
michael@0 | 1605 | |
michael@0 | 1606 | let groupLabel = uri.prePath; |
michael@0 | 1607 | |
michael@0 | 1608 | for (let name of Object.keys(KNOWN_SOURCE_GROUPS)) { |
michael@0 | 1609 | if (aUrl.startsWith(KNOWN_SOURCE_GROUPS[name])) { |
michael@0 | 1610 | groupLabel = name; |
michael@0 | 1611 | } |
michael@0 | 1612 | } |
michael@0 | 1613 | |
michael@0 | 1614 | let unicodeLabel = NetworkHelper.convertToUnicode(unescape(groupLabel)); |
michael@0 | 1615 | this._groupsCache.set(aUrl, unicodeLabel) |
michael@0 | 1616 | return unicodeLabel; |
michael@0 | 1617 | }, |
michael@0 | 1618 | |
michael@0 | 1619 | /** |
michael@0 | 1620 | * Trims the url by shortening it if it exceeds a certain length, adding an |
michael@0 | 1621 | * ellipsis at the end. |
michael@0 | 1622 | * |
michael@0 | 1623 | * @param string aUrl |
michael@0 | 1624 | * The source url. |
michael@0 | 1625 | * @param number aLength [optional] |
michael@0 | 1626 | * The expected source url length. |
michael@0 | 1627 | * @param number aSection [optional] |
michael@0 | 1628 | * The section to trim. Supported values: "start", "center", "end" |
michael@0 | 1629 | * @return string |
michael@0 | 1630 | * The shortened url. |
michael@0 | 1631 | */ |
michael@0 | 1632 | trimUrlLength: function(aUrl, aLength, aSection) { |
michael@0 | 1633 | aLength = aLength || SOURCE_URL_DEFAULT_MAX_LENGTH; |
michael@0 | 1634 | aSection = aSection || "end"; |
michael@0 | 1635 | |
michael@0 | 1636 | if (aUrl.length > aLength) { |
michael@0 | 1637 | switch (aSection) { |
michael@0 | 1638 | case "start": |
michael@0 | 1639 | return L10N.ellipsis + aUrl.slice(-aLength); |
michael@0 | 1640 | break; |
michael@0 | 1641 | case "center": |
michael@0 | 1642 | return aUrl.substr(0, aLength / 2 - 1) + L10N.ellipsis + aUrl.slice(-aLength / 2 + 1); |
michael@0 | 1643 | break; |
michael@0 | 1644 | case "end": |
michael@0 | 1645 | return aUrl.substr(0, aLength) + L10N.ellipsis; |
michael@0 | 1646 | break; |
michael@0 | 1647 | } |
michael@0 | 1648 | } |
michael@0 | 1649 | return aUrl; |
michael@0 | 1650 | }, |
michael@0 | 1651 | |
michael@0 | 1652 | /** |
michael@0 | 1653 | * Trims the query part or reference identifier of a url string, if necessary. |
michael@0 | 1654 | * |
michael@0 | 1655 | * @param string aUrl |
michael@0 | 1656 | * The source url. |
michael@0 | 1657 | * @return string |
michael@0 | 1658 | * The shortened url. |
michael@0 | 1659 | */ |
michael@0 | 1660 | trimUrlQuery: function(aUrl) { |
michael@0 | 1661 | let length = aUrl.length; |
michael@0 | 1662 | let q1 = aUrl.indexOf('?'); |
michael@0 | 1663 | let q2 = aUrl.indexOf('&'); |
michael@0 | 1664 | let q3 = aUrl.indexOf('#'); |
michael@0 | 1665 | let q = Math.min(q1 != -1 ? q1 : length, |
michael@0 | 1666 | q2 != -1 ? q2 : length, |
michael@0 | 1667 | q3 != -1 ? q3 : length); |
michael@0 | 1668 | |
michael@0 | 1669 | return aUrl.slice(0, q); |
michael@0 | 1670 | }, |
michael@0 | 1671 | |
michael@0 | 1672 | /** |
michael@0 | 1673 | * Trims as much as possible from a url, while keeping the label unique |
michael@0 | 1674 | * in the sources container. |
michael@0 | 1675 | * |
michael@0 | 1676 | * @param string | nsIURL aUrl |
michael@0 | 1677 | * The source url. |
michael@0 | 1678 | * @param string aLabel [optional] |
michael@0 | 1679 | * The resulting label at each step. |
michael@0 | 1680 | * @param number aSeq [optional] |
michael@0 | 1681 | * The current iteration step. |
michael@0 | 1682 | * @return string |
michael@0 | 1683 | * The resulting label at the final step. |
michael@0 | 1684 | */ |
michael@0 | 1685 | trimUrl: function(aUrl, aLabel, aSeq) { |
michael@0 | 1686 | if (!(aUrl instanceof Ci.nsIURL)) { |
michael@0 | 1687 | try { |
michael@0 | 1688 | // Use an nsIURL to parse all the url path parts. |
michael@0 | 1689 | aUrl = Services.io.newURI(aUrl, null, null).QueryInterface(Ci.nsIURL); |
michael@0 | 1690 | } catch (e) { |
michael@0 | 1691 | // This doesn't look like a url, or nsIURL can't handle it. |
michael@0 | 1692 | return aUrl; |
michael@0 | 1693 | } |
michael@0 | 1694 | } |
michael@0 | 1695 | if (!aSeq) { |
michael@0 | 1696 | let name = aUrl.fileName; |
michael@0 | 1697 | if (name) { |
michael@0 | 1698 | // This is a regular file url, get only the file name (contains the |
michael@0 | 1699 | // base name and extension if available). |
michael@0 | 1700 | |
michael@0 | 1701 | // If this url contains an invalid query, unfortunately nsIURL thinks |
michael@0 | 1702 | // it's part of the file extension. It must be removed. |
michael@0 | 1703 | aLabel = aUrl.fileName.replace(/\&.*/, ""); |
michael@0 | 1704 | } else { |
michael@0 | 1705 | // This is not a file url, hence there is no base name, nor extension. |
michael@0 | 1706 | // Proceed using other available information. |
michael@0 | 1707 | aLabel = ""; |
michael@0 | 1708 | } |
michael@0 | 1709 | aSeq = 1; |
michael@0 | 1710 | } |
michael@0 | 1711 | |
michael@0 | 1712 | // If we have a label and it doesn't only contain a query... |
michael@0 | 1713 | if (aLabel && aLabel.indexOf("?") != 0) { |
michael@0 | 1714 | // A page may contain multiple requests to the same url but with different |
michael@0 | 1715 | // queries. It is *not* redundant to show each one. |
michael@0 | 1716 | if (!DebuggerView.Sources.getItemForAttachment(e => e.label == aLabel)) { |
michael@0 | 1717 | return aLabel; |
michael@0 | 1718 | } |
michael@0 | 1719 | } |
michael@0 | 1720 | |
michael@0 | 1721 | // Append the url query. |
michael@0 | 1722 | if (aSeq == 1) { |
michael@0 | 1723 | let query = aUrl.query; |
michael@0 | 1724 | if (query) { |
michael@0 | 1725 | return this.trimUrl(aUrl, aLabel + "?" + query, aSeq + 1); |
michael@0 | 1726 | } |
michael@0 | 1727 | aSeq++; |
michael@0 | 1728 | } |
michael@0 | 1729 | // Append the url reference. |
michael@0 | 1730 | if (aSeq == 2) { |
michael@0 | 1731 | let ref = aUrl.ref; |
michael@0 | 1732 | if (ref) { |
michael@0 | 1733 | return this.trimUrl(aUrl, aLabel + "#" + aUrl.ref, aSeq + 1); |
michael@0 | 1734 | } |
michael@0 | 1735 | aSeq++; |
michael@0 | 1736 | } |
michael@0 | 1737 | // Prepend the url directory. |
michael@0 | 1738 | if (aSeq == 3) { |
michael@0 | 1739 | let dir = aUrl.directory; |
michael@0 | 1740 | if (dir) { |
michael@0 | 1741 | return this.trimUrl(aUrl, dir.replace(/^\//, "") + aLabel, aSeq + 1); |
michael@0 | 1742 | } |
michael@0 | 1743 | aSeq++; |
michael@0 | 1744 | } |
michael@0 | 1745 | // Prepend the hostname and port number. |
michael@0 | 1746 | if (aSeq == 4) { |
michael@0 | 1747 | let host = aUrl.hostPort; |
michael@0 | 1748 | if (host) { |
michael@0 | 1749 | return this.trimUrl(aUrl, host + "/" + aLabel, aSeq + 1); |
michael@0 | 1750 | } |
michael@0 | 1751 | aSeq++; |
michael@0 | 1752 | } |
michael@0 | 1753 | // Use the whole url spec but ignoring the reference. |
michael@0 | 1754 | if (aSeq == 5) { |
michael@0 | 1755 | return this.trimUrl(aUrl, aUrl.specIgnoringRef, aSeq + 1); |
michael@0 | 1756 | } |
michael@0 | 1757 | // Give up. |
michael@0 | 1758 | return aUrl.spec; |
michael@0 | 1759 | } |
michael@0 | 1760 | }; |
michael@0 | 1761 | |
michael@0 | 1762 | /** |
michael@0 | 1763 | * Functions handling the variables bubble UI. |
michael@0 | 1764 | */ |
michael@0 | 1765 | function VariableBubbleView() { |
michael@0 | 1766 | dumpn("VariableBubbleView was instantiated"); |
michael@0 | 1767 | |
michael@0 | 1768 | this._onMouseMove = this._onMouseMove.bind(this); |
michael@0 | 1769 | this._onMouseLeave = this._onMouseLeave.bind(this); |
michael@0 | 1770 | this._onPopupHiding = this._onPopupHiding.bind(this); |
michael@0 | 1771 | } |
michael@0 | 1772 | |
michael@0 | 1773 | VariableBubbleView.prototype = { |
michael@0 | 1774 | /** |
michael@0 | 1775 | * Initialization function, called when the debugger is started. |
michael@0 | 1776 | */ |
michael@0 | 1777 | initialize: function() { |
michael@0 | 1778 | dumpn("Initializing the VariableBubbleView"); |
michael@0 | 1779 | |
michael@0 | 1780 | this._editorContainer = document.getElementById("editor"); |
michael@0 | 1781 | this._editorContainer.addEventListener("mousemove", this._onMouseMove, false); |
michael@0 | 1782 | this._editorContainer.addEventListener("mouseleave", this._onMouseLeave, false); |
michael@0 | 1783 | |
michael@0 | 1784 | this._tooltip = new Tooltip(document, { |
michael@0 | 1785 | closeOnEvents: [{ |
michael@0 | 1786 | emitter: DebuggerController._toolbox, |
michael@0 | 1787 | event: "select" |
michael@0 | 1788 | }, { |
michael@0 | 1789 | emitter: this._editorContainer, |
michael@0 | 1790 | event: "scroll", |
michael@0 | 1791 | useCapture: true |
michael@0 | 1792 | }] |
michael@0 | 1793 | }); |
michael@0 | 1794 | this._tooltip.defaultPosition = EDITOR_VARIABLE_POPUP_POSITION; |
michael@0 | 1795 | this._tooltip.defaultShowDelay = EDITOR_VARIABLE_HOVER_DELAY; |
michael@0 | 1796 | this._tooltip.panel.addEventListener("popuphiding", this._onPopupHiding); |
michael@0 | 1797 | }, |
michael@0 | 1798 | |
michael@0 | 1799 | /** |
michael@0 | 1800 | * Destruction function, called when the debugger is closed. |
michael@0 | 1801 | */ |
michael@0 | 1802 | destroy: function() { |
michael@0 | 1803 | dumpn("Destroying the VariableBubbleView"); |
michael@0 | 1804 | |
michael@0 | 1805 | this._tooltip.panel.removeEventListener("popuphiding", this._onPopupHiding); |
michael@0 | 1806 | this._editorContainer.removeEventListener("mousemove", this._onMouseMove, false); |
michael@0 | 1807 | this._editorContainer.removeEventListener("mouseleave", this._onMouseLeave, false); |
michael@0 | 1808 | }, |
michael@0 | 1809 | |
michael@0 | 1810 | /** |
michael@0 | 1811 | * Specifies whether literals can be (redundantly) inspected in a popup. |
michael@0 | 1812 | * This behavior is deprecated, but still tested in a few places. |
michael@0 | 1813 | */ |
michael@0 | 1814 | _ignoreLiterals: true, |
michael@0 | 1815 | |
michael@0 | 1816 | /** |
michael@0 | 1817 | * Searches for an identifier underneath the specified position in the |
michael@0 | 1818 | * source editor, and if found, opens a VariablesView inspection popup. |
michael@0 | 1819 | * |
michael@0 | 1820 | * @param number x, y |
michael@0 | 1821 | * The left/top coordinates where to look for an identifier. |
michael@0 | 1822 | */ |
michael@0 | 1823 | _findIdentifier: function(x, y) { |
michael@0 | 1824 | let editor = DebuggerView.editor; |
michael@0 | 1825 | |
michael@0 | 1826 | // Calculate the editor's line and column at the current x and y coords. |
michael@0 | 1827 | let hoveredPos = editor.getPositionFromCoords({ left: x, top: y }); |
michael@0 | 1828 | let hoveredOffset = editor.getOffset(hoveredPos); |
michael@0 | 1829 | let hoveredLine = hoveredPos.line; |
michael@0 | 1830 | let hoveredColumn = hoveredPos.ch; |
michael@0 | 1831 | |
michael@0 | 1832 | // A source contains multiple scripts. Find the start index of the script |
michael@0 | 1833 | // containing the specified offset relative to its parent source. |
michael@0 | 1834 | let contents = editor.getText(); |
michael@0 | 1835 | let location = DebuggerView.Sources.selectedValue; |
michael@0 | 1836 | let parsedSource = DebuggerController.Parser.get(contents, location); |
michael@0 | 1837 | let scriptInfo = parsedSource.getScriptInfo(hoveredOffset); |
michael@0 | 1838 | |
michael@0 | 1839 | // If the script length is negative, we're not hovering JS source code. |
michael@0 | 1840 | if (scriptInfo.length == -1) { |
michael@0 | 1841 | return; |
michael@0 | 1842 | } |
michael@0 | 1843 | |
michael@0 | 1844 | // Using the script offset, determine the actual line and column inside the |
michael@0 | 1845 | // script, to use when finding identifiers. |
michael@0 | 1846 | let scriptStart = editor.getPosition(scriptInfo.start); |
michael@0 | 1847 | let scriptLineOffset = scriptStart.line; |
michael@0 | 1848 | let scriptColumnOffset = (hoveredLine == scriptStart.line ? scriptStart.ch : 0); |
michael@0 | 1849 | |
michael@0 | 1850 | let scriptLine = hoveredLine - scriptLineOffset; |
michael@0 | 1851 | let scriptColumn = hoveredColumn - scriptColumnOffset; |
michael@0 | 1852 | let identifierInfo = parsedSource.getIdentifierAt({ |
michael@0 | 1853 | line: scriptLine + 1, |
michael@0 | 1854 | column: scriptColumn, |
michael@0 | 1855 | scriptIndex: scriptInfo.index, |
michael@0 | 1856 | ignoreLiterals: this._ignoreLiterals |
michael@0 | 1857 | }); |
michael@0 | 1858 | |
michael@0 | 1859 | // If the info is null, we're not hovering any identifier. |
michael@0 | 1860 | if (!identifierInfo) { |
michael@0 | 1861 | return; |
michael@0 | 1862 | } |
michael@0 | 1863 | |
michael@0 | 1864 | // Transform the line and column relative to the parsed script back |
michael@0 | 1865 | // to the context of the parent source. |
michael@0 | 1866 | let { start: identifierStart, end: identifierEnd } = identifierInfo.location; |
michael@0 | 1867 | let identifierCoords = { |
michael@0 | 1868 | line: identifierStart.line + scriptLineOffset, |
michael@0 | 1869 | column: identifierStart.column + scriptColumnOffset, |
michael@0 | 1870 | length: identifierEnd.column - identifierStart.column |
michael@0 | 1871 | }; |
michael@0 | 1872 | |
michael@0 | 1873 | // Evaluate the identifier in the current stack frame and show the |
michael@0 | 1874 | // results in a VariablesView inspection popup. |
michael@0 | 1875 | DebuggerController.StackFrames.evaluate(identifierInfo.evalString) |
michael@0 | 1876 | .then(frameFinished => { |
michael@0 | 1877 | if ("return" in frameFinished) { |
michael@0 | 1878 | this.showContents({ |
michael@0 | 1879 | coords: identifierCoords, |
michael@0 | 1880 | evalPrefix: identifierInfo.evalString, |
michael@0 | 1881 | objectActor: frameFinished.return |
michael@0 | 1882 | }); |
michael@0 | 1883 | } else { |
michael@0 | 1884 | let msg = "Evaluation has thrown for: " + identifierInfo.evalString; |
michael@0 | 1885 | console.warn(msg); |
michael@0 | 1886 | dumpn(msg); |
michael@0 | 1887 | } |
michael@0 | 1888 | }) |
michael@0 | 1889 | .then(null, err => { |
michael@0 | 1890 | let msg = "Couldn't evaluate: " + err.message; |
michael@0 | 1891 | console.error(msg); |
michael@0 | 1892 | dumpn(msg); |
michael@0 | 1893 | }); |
michael@0 | 1894 | }, |
michael@0 | 1895 | |
michael@0 | 1896 | /** |
michael@0 | 1897 | * Shows an inspection popup for a specified object actor grip. |
michael@0 | 1898 | * |
michael@0 | 1899 | * @param string object |
michael@0 | 1900 | * An object containing the following properties: |
michael@0 | 1901 | * - coords: the inspected identifier coordinates in the editor, |
michael@0 | 1902 | * containing the { line, column, length } properties. |
michael@0 | 1903 | * - evalPrefix: a prefix for the variables view evaluation macros. |
michael@0 | 1904 | * - objectActor: the value grip for the object actor. |
michael@0 | 1905 | */ |
michael@0 | 1906 | showContents: function({ coords, evalPrefix, objectActor }) { |
michael@0 | 1907 | let editor = DebuggerView.editor; |
michael@0 | 1908 | let { line, column, length } = coords; |
michael@0 | 1909 | |
michael@0 | 1910 | // Highlight the function found at the mouse position. |
michael@0 | 1911 | this._markedText = editor.markText( |
michael@0 | 1912 | { line: line - 1, ch: column }, |
michael@0 | 1913 | { line: line - 1, ch: column + length }); |
michael@0 | 1914 | |
michael@0 | 1915 | // If the grip represents a primitive value, use a more lightweight |
michael@0 | 1916 | // machinery to display it. |
michael@0 | 1917 | if (VariablesView.isPrimitive({ value: objectActor })) { |
michael@0 | 1918 | let className = VariablesView.getClass(objectActor); |
michael@0 | 1919 | let textContent = VariablesView.getString(objectActor); |
michael@0 | 1920 | this._tooltip.setTextContent({ |
michael@0 | 1921 | messages: [textContent], |
michael@0 | 1922 | messagesClass: className, |
michael@0 | 1923 | containerClass: "plain" |
michael@0 | 1924 | }, [{ |
michael@0 | 1925 | label: L10N.getStr('addWatchExpressionButton'), |
michael@0 | 1926 | className: "dbg-expression-button", |
michael@0 | 1927 | command: () => { |
michael@0 | 1928 | DebuggerView.VariableBubble.hideContents(); |
michael@0 | 1929 | DebuggerView.WatchExpressions.addExpression(evalPrefix, true); |
michael@0 | 1930 | } |
michael@0 | 1931 | }]); |
michael@0 | 1932 | } else { |
michael@0 | 1933 | this._tooltip.setVariableContent(objectActor, { |
michael@0 | 1934 | searchPlaceholder: L10N.getStr("emptyPropertiesFilterText"), |
michael@0 | 1935 | searchEnabled: Prefs.variablesSearchboxVisible, |
michael@0 | 1936 | eval: (variable, value) => { |
michael@0 | 1937 | let string = variable.evaluationMacro(variable, value); |
michael@0 | 1938 | DebuggerController.StackFrames.evaluate(string); |
michael@0 | 1939 | DebuggerView.VariableBubble.hideContents(); |
michael@0 | 1940 | } |
michael@0 | 1941 | }, { |
michael@0 | 1942 | getEnvironmentClient: aObject => gThreadClient.environment(aObject), |
michael@0 | 1943 | getObjectClient: aObject => gThreadClient.pauseGrip(aObject), |
michael@0 | 1944 | simpleValueEvalMacro: this._getSimpleValueEvalMacro(evalPrefix), |
michael@0 | 1945 | getterOrSetterEvalMacro: this._getGetterOrSetterEvalMacro(evalPrefix), |
michael@0 | 1946 | overrideValueEvalMacro: this._getOverrideValueEvalMacro(evalPrefix) |
michael@0 | 1947 | }, { |
michael@0 | 1948 | fetched: (aEvent, aType) => { |
michael@0 | 1949 | if (aType == "properties") { |
michael@0 | 1950 | window.emit(EVENTS.FETCHED_BUBBLE_PROPERTIES); |
michael@0 | 1951 | } |
michael@0 | 1952 | } |
michael@0 | 1953 | }, [{ |
michael@0 | 1954 | label: L10N.getStr("addWatchExpressionButton"), |
michael@0 | 1955 | className: "dbg-expression-button", |
michael@0 | 1956 | command: () => { |
michael@0 | 1957 | DebuggerView.VariableBubble.hideContents(); |
michael@0 | 1958 | DebuggerView.WatchExpressions.addExpression(evalPrefix, true); |
michael@0 | 1959 | } |
michael@0 | 1960 | }], DebuggerController._toolbox); |
michael@0 | 1961 | } |
michael@0 | 1962 | |
michael@0 | 1963 | this._tooltip.show(this._markedText.anchor); |
michael@0 | 1964 | }, |
michael@0 | 1965 | |
michael@0 | 1966 | /** |
michael@0 | 1967 | * Hides the inspection popup. |
michael@0 | 1968 | */ |
michael@0 | 1969 | hideContents: function() { |
michael@0 | 1970 | clearNamedTimeout("editor-mouse-move"); |
michael@0 | 1971 | this._tooltip.hide(); |
michael@0 | 1972 | }, |
michael@0 | 1973 | |
michael@0 | 1974 | /** |
michael@0 | 1975 | * Checks whether the inspection popup is shown. |
michael@0 | 1976 | * |
michael@0 | 1977 | * @return boolean |
michael@0 | 1978 | * True if the panel is shown or showing, false otherwise. |
michael@0 | 1979 | */ |
michael@0 | 1980 | contentsShown: function() { |
michael@0 | 1981 | return this._tooltip.isShown(); |
michael@0 | 1982 | }, |
michael@0 | 1983 | |
michael@0 | 1984 | /** |
michael@0 | 1985 | * Functions for getting customized variables view evaluation macros. |
michael@0 | 1986 | * |
michael@0 | 1987 | * @param string aPrefix |
michael@0 | 1988 | * See the corresponding VariablesView.* functions. |
michael@0 | 1989 | */ |
michael@0 | 1990 | _getSimpleValueEvalMacro: function(aPrefix) { |
michael@0 | 1991 | return (item, string) => |
michael@0 | 1992 | VariablesView.simpleValueEvalMacro(item, string, aPrefix); |
michael@0 | 1993 | }, |
michael@0 | 1994 | _getGetterOrSetterEvalMacro: function(aPrefix) { |
michael@0 | 1995 | return (item, string) => |
michael@0 | 1996 | VariablesView.getterOrSetterEvalMacro(item, string, aPrefix); |
michael@0 | 1997 | }, |
michael@0 | 1998 | _getOverrideValueEvalMacro: function(aPrefix) { |
michael@0 | 1999 | return (item, string) => |
michael@0 | 2000 | VariablesView.overrideValueEvalMacro(item, string, aPrefix); |
michael@0 | 2001 | }, |
michael@0 | 2002 | |
michael@0 | 2003 | /** |
michael@0 | 2004 | * The mousemove listener for the source editor. |
michael@0 | 2005 | */ |
michael@0 | 2006 | _onMouseMove: function({ clientX: x, clientY: y, buttons: btns }) { |
michael@0 | 2007 | // Prevent the variable inspection popup from showing when the thread client |
michael@0 | 2008 | // is not paused, or while a popup is already visible, or when the user tries |
michael@0 | 2009 | // to select text in the editor. |
michael@0 | 2010 | if (gThreadClient && gThreadClient.state != "paused" |
michael@0 | 2011 | || !this._tooltip.isHidden() |
michael@0 | 2012 | || (DebuggerView.editor.somethingSelected() |
michael@0 | 2013 | && btns > 0)) { |
michael@0 | 2014 | clearNamedTimeout("editor-mouse-move"); |
michael@0 | 2015 | return; |
michael@0 | 2016 | } |
michael@0 | 2017 | // Allow events to settle down first. If the mouse hovers over |
michael@0 | 2018 | // a certain point in the editor long enough, try showing a variable bubble. |
michael@0 | 2019 | setNamedTimeout("editor-mouse-move", |
michael@0 | 2020 | EDITOR_VARIABLE_HOVER_DELAY, () => this._findIdentifier(x, y)); |
michael@0 | 2021 | }, |
michael@0 | 2022 | |
michael@0 | 2023 | /** |
michael@0 | 2024 | * The mouseleave listener for the source editor container node. |
michael@0 | 2025 | */ |
michael@0 | 2026 | _onMouseLeave: function() { |
michael@0 | 2027 | clearNamedTimeout("editor-mouse-move"); |
michael@0 | 2028 | }, |
michael@0 | 2029 | |
michael@0 | 2030 | /** |
michael@0 | 2031 | * Listener handling the popup hiding event. |
michael@0 | 2032 | */ |
michael@0 | 2033 | _onPopupHiding: function({ target }) { |
michael@0 | 2034 | if (this._tooltip.panel != target) { |
michael@0 | 2035 | return; |
michael@0 | 2036 | } |
michael@0 | 2037 | if (this._markedText) { |
michael@0 | 2038 | this._markedText.clear(); |
michael@0 | 2039 | this._markedText = null; |
michael@0 | 2040 | } |
michael@0 | 2041 | if (!this._tooltip.isEmpty()) { |
michael@0 | 2042 | this._tooltip.empty(); |
michael@0 | 2043 | } |
michael@0 | 2044 | }, |
michael@0 | 2045 | |
michael@0 | 2046 | _editorContainer: null, |
michael@0 | 2047 | _markedText: null, |
michael@0 | 2048 | _tooltip: null |
michael@0 | 2049 | }; |
michael@0 | 2050 | |
michael@0 | 2051 | /** |
michael@0 | 2052 | * Functions handling the watch expressions UI. |
michael@0 | 2053 | */ |
michael@0 | 2054 | function WatchExpressionsView() { |
michael@0 | 2055 | dumpn("WatchExpressionsView was instantiated"); |
michael@0 | 2056 | |
michael@0 | 2057 | this.switchExpression = this.switchExpression.bind(this); |
michael@0 | 2058 | this.deleteExpression = this.deleteExpression.bind(this); |
michael@0 | 2059 | this._createItemView = this._createItemView.bind(this); |
michael@0 | 2060 | this._onClick = this._onClick.bind(this); |
michael@0 | 2061 | this._onClose = this._onClose.bind(this); |
michael@0 | 2062 | this._onBlur = this._onBlur.bind(this); |
michael@0 | 2063 | this._onKeyPress = this._onKeyPress.bind(this); |
michael@0 | 2064 | } |
michael@0 | 2065 | |
michael@0 | 2066 | WatchExpressionsView.prototype = Heritage.extend(WidgetMethods, { |
michael@0 | 2067 | /** |
michael@0 | 2068 | * Initialization function, called when the debugger is started. |
michael@0 | 2069 | */ |
michael@0 | 2070 | initialize: function() { |
michael@0 | 2071 | dumpn("Initializing the WatchExpressionsView"); |
michael@0 | 2072 | |
michael@0 | 2073 | this.widget = new SimpleListWidget(document.getElementById("expressions")); |
michael@0 | 2074 | this.widget.setAttribute("context", "debuggerWatchExpressionsContextMenu"); |
michael@0 | 2075 | this.widget.addEventListener("click", this._onClick, false); |
michael@0 | 2076 | |
michael@0 | 2077 | this.headerText = L10N.getStr("addWatchExpressionText"); |
michael@0 | 2078 | }, |
michael@0 | 2079 | |
michael@0 | 2080 | /** |
michael@0 | 2081 | * Destruction function, called when the debugger is closed. |
michael@0 | 2082 | */ |
michael@0 | 2083 | destroy: function() { |
michael@0 | 2084 | dumpn("Destroying the WatchExpressionsView"); |
michael@0 | 2085 | |
michael@0 | 2086 | this.widget.removeEventListener("click", this._onClick, false); |
michael@0 | 2087 | }, |
michael@0 | 2088 | |
michael@0 | 2089 | /** |
michael@0 | 2090 | * Adds a watch expression in this container. |
michael@0 | 2091 | * |
michael@0 | 2092 | * @param string aExpression [optional] |
michael@0 | 2093 | * An optional initial watch expression text. |
michael@0 | 2094 | * @param boolean aSkipUserInput [optional] |
michael@0 | 2095 | * Pass true to avoid waiting for additional user input |
michael@0 | 2096 | * on the watch expression. |
michael@0 | 2097 | */ |
michael@0 | 2098 | addExpression: function(aExpression = "", aSkipUserInput = false) { |
michael@0 | 2099 | // Watch expressions are UI elements which benefit from visible panes. |
michael@0 | 2100 | DebuggerView.showInstrumentsPane(); |
michael@0 | 2101 | |
michael@0 | 2102 | // Create the element node for the watch expression item. |
michael@0 | 2103 | let itemView = this._createItemView(aExpression); |
michael@0 | 2104 | |
michael@0 | 2105 | // Append a watch expression item to this container. |
michael@0 | 2106 | let expressionItem = this.push([itemView.container], { |
michael@0 | 2107 | index: 0, /* specifies on which position should the item be appended */ |
michael@0 | 2108 | attachment: { |
michael@0 | 2109 | view: itemView, |
michael@0 | 2110 | initialExpression: aExpression, |
michael@0 | 2111 | currentExpression: "", |
michael@0 | 2112 | } |
michael@0 | 2113 | }); |
michael@0 | 2114 | |
michael@0 | 2115 | // Automatically focus the new watch expression input |
michael@0 | 2116 | // if additional user input is desired. |
michael@0 | 2117 | if (!aSkipUserInput) { |
michael@0 | 2118 | expressionItem.attachment.view.inputNode.select(); |
michael@0 | 2119 | expressionItem.attachment.view.inputNode.focus(); |
michael@0 | 2120 | DebuggerView.Variables.parentNode.scrollTop = 0; |
michael@0 | 2121 | } |
michael@0 | 2122 | // Otherwise, add and evaluate the new watch expression immediately. |
michael@0 | 2123 | else { |
michael@0 | 2124 | this.toggleContents(false); |
michael@0 | 2125 | this._onBlur({ target: expressionItem.attachment.view.inputNode }); |
michael@0 | 2126 | } |
michael@0 | 2127 | }, |
michael@0 | 2128 | |
michael@0 | 2129 | /** |
michael@0 | 2130 | * Changes the watch expression corresponding to the specified variable item. |
michael@0 | 2131 | * This function is called whenever a watch expression's code is edited in |
michael@0 | 2132 | * the variables view container. |
michael@0 | 2133 | * |
michael@0 | 2134 | * @param Variable aVar |
michael@0 | 2135 | * The variable representing the watch expression evaluation. |
michael@0 | 2136 | * @param string aExpression |
michael@0 | 2137 | * The new watch expression text. |
michael@0 | 2138 | */ |
michael@0 | 2139 | switchExpression: function(aVar, aExpression) { |
michael@0 | 2140 | let expressionItem = |
michael@0 | 2141 | [i for (i of this) if (i.attachment.currentExpression == aVar.name)][0]; |
michael@0 | 2142 | |
michael@0 | 2143 | // Remove the watch expression if it's going to be empty or a duplicate. |
michael@0 | 2144 | if (!aExpression || this.getAllStrings().indexOf(aExpression) != -1) { |
michael@0 | 2145 | this.deleteExpression(aVar); |
michael@0 | 2146 | return; |
michael@0 | 2147 | } |
michael@0 | 2148 | |
michael@0 | 2149 | // Save the watch expression code string. |
michael@0 | 2150 | expressionItem.attachment.currentExpression = aExpression; |
michael@0 | 2151 | expressionItem.attachment.view.inputNode.value = aExpression; |
michael@0 | 2152 | |
michael@0 | 2153 | // Synchronize with the controller's watch expressions store. |
michael@0 | 2154 | DebuggerController.StackFrames.syncWatchExpressions(); |
michael@0 | 2155 | }, |
michael@0 | 2156 | |
michael@0 | 2157 | /** |
michael@0 | 2158 | * Removes the watch expression corresponding to the specified variable item. |
michael@0 | 2159 | * This function is called whenever a watch expression's value is edited in |
michael@0 | 2160 | * the variables view container. |
michael@0 | 2161 | * |
michael@0 | 2162 | * @param Variable aVar |
michael@0 | 2163 | * The variable representing the watch expression evaluation. |
michael@0 | 2164 | */ |
michael@0 | 2165 | deleteExpression: function(aVar) { |
michael@0 | 2166 | let expressionItem = |
michael@0 | 2167 | [i for (i of this) if (i.attachment.currentExpression == aVar.name)][0]; |
michael@0 | 2168 | |
michael@0 | 2169 | // Remove the watch expression. |
michael@0 | 2170 | this.remove(expressionItem); |
michael@0 | 2171 | |
michael@0 | 2172 | // Synchronize with the controller's watch expressions store. |
michael@0 | 2173 | DebuggerController.StackFrames.syncWatchExpressions(); |
michael@0 | 2174 | }, |
michael@0 | 2175 | |
michael@0 | 2176 | /** |
michael@0 | 2177 | * Gets the watch expression code string for an item in this container. |
michael@0 | 2178 | * |
michael@0 | 2179 | * @param number aIndex |
michael@0 | 2180 | * The index used to identify the watch expression. |
michael@0 | 2181 | * @return string |
michael@0 | 2182 | * The watch expression code string. |
michael@0 | 2183 | */ |
michael@0 | 2184 | getString: function(aIndex) { |
michael@0 | 2185 | return this.getItemAtIndex(aIndex).attachment.currentExpression; |
michael@0 | 2186 | }, |
michael@0 | 2187 | |
michael@0 | 2188 | /** |
michael@0 | 2189 | * Gets the watch expressions code strings for all items in this container. |
michael@0 | 2190 | * |
michael@0 | 2191 | * @return array |
michael@0 | 2192 | * The watch expressions code strings. |
michael@0 | 2193 | */ |
michael@0 | 2194 | getAllStrings: function() { |
michael@0 | 2195 | return this.items.map(e => e.attachment.currentExpression); |
michael@0 | 2196 | }, |
michael@0 | 2197 | |
michael@0 | 2198 | /** |
michael@0 | 2199 | * Customization function for creating an item's UI. |
michael@0 | 2200 | * |
michael@0 | 2201 | * @param string aExpression |
michael@0 | 2202 | * The watch expression string. |
michael@0 | 2203 | */ |
michael@0 | 2204 | _createItemView: function(aExpression) { |
michael@0 | 2205 | let container = document.createElement("hbox"); |
michael@0 | 2206 | container.className = "list-widget-item dbg-expression"; |
michael@0 | 2207 | |
michael@0 | 2208 | let arrowNode = document.createElement("hbox"); |
michael@0 | 2209 | arrowNode.className = "dbg-expression-arrow"; |
michael@0 | 2210 | |
michael@0 | 2211 | let inputNode = document.createElement("textbox"); |
michael@0 | 2212 | inputNode.className = "plain dbg-expression-input devtools-monospace"; |
michael@0 | 2213 | inputNode.setAttribute("value", aExpression); |
michael@0 | 2214 | inputNode.setAttribute("flex", "1"); |
michael@0 | 2215 | |
michael@0 | 2216 | let closeNode = document.createElement("toolbarbutton"); |
michael@0 | 2217 | closeNode.className = "plain variables-view-delete"; |
michael@0 | 2218 | |
michael@0 | 2219 | closeNode.addEventListener("click", this._onClose, false); |
michael@0 | 2220 | inputNode.addEventListener("blur", this._onBlur, false); |
michael@0 | 2221 | inputNode.addEventListener("keypress", this._onKeyPress, false); |
michael@0 | 2222 | |
michael@0 | 2223 | container.appendChild(arrowNode); |
michael@0 | 2224 | container.appendChild(inputNode); |
michael@0 | 2225 | container.appendChild(closeNode); |
michael@0 | 2226 | |
michael@0 | 2227 | return { |
michael@0 | 2228 | container: container, |
michael@0 | 2229 | arrowNode: arrowNode, |
michael@0 | 2230 | inputNode: inputNode, |
michael@0 | 2231 | closeNode: closeNode |
michael@0 | 2232 | }; |
michael@0 | 2233 | }, |
michael@0 | 2234 | |
michael@0 | 2235 | /** |
michael@0 | 2236 | * Called when the add watch expression key sequence was pressed. |
michael@0 | 2237 | */ |
michael@0 | 2238 | _onCmdAddExpression: function(aText) { |
michael@0 | 2239 | // Only add a new expression if there's no pending input. |
michael@0 | 2240 | if (this.getAllStrings().indexOf("") == -1) { |
michael@0 | 2241 | this.addExpression(aText || DebuggerView.editor.getSelection()); |
michael@0 | 2242 | } |
michael@0 | 2243 | }, |
michael@0 | 2244 | |
michael@0 | 2245 | /** |
michael@0 | 2246 | * Called when the remove all watch expressions key sequence was pressed. |
michael@0 | 2247 | */ |
michael@0 | 2248 | _onCmdRemoveAllExpressions: function() { |
michael@0 | 2249 | // Empty the view of all the watch expressions and clear the cache. |
michael@0 | 2250 | this.empty(); |
michael@0 | 2251 | |
michael@0 | 2252 | // Synchronize with the controller's watch expressions store. |
michael@0 | 2253 | DebuggerController.StackFrames.syncWatchExpressions(); |
michael@0 | 2254 | }, |
michael@0 | 2255 | |
michael@0 | 2256 | /** |
michael@0 | 2257 | * The click listener for this container. |
michael@0 | 2258 | */ |
michael@0 | 2259 | _onClick: function(e) { |
michael@0 | 2260 | if (e.button != 0) { |
michael@0 | 2261 | // Only allow left-click to trigger this event. |
michael@0 | 2262 | return; |
michael@0 | 2263 | } |
michael@0 | 2264 | let expressionItem = this.getItemForElement(e.target); |
michael@0 | 2265 | if (!expressionItem) { |
michael@0 | 2266 | // The container is empty or we didn't click on an actual item. |
michael@0 | 2267 | this.addExpression(); |
michael@0 | 2268 | } |
michael@0 | 2269 | }, |
michael@0 | 2270 | |
michael@0 | 2271 | /** |
michael@0 | 2272 | * The click listener for a watch expression's close button. |
michael@0 | 2273 | */ |
michael@0 | 2274 | _onClose: function(e) { |
michael@0 | 2275 | // Remove the watch expression. |
michael@0 | 2276 | this.remove(this.getItemForElement(e.target)); |
michael@0 | 2277 | |
michael@0 | 2278 | // Synchronize with the controller's watch expressions store. |
michael@0 | 2279 | DebuggerController.StackFrames.syncWatchExpressions(); |
michael@0 | 2280 | |
michael@0 | 2281 | // Prevent clicking the expression element itself. |
michael@0 | 2282 | e.preventDefault(); |
michael@0 | 2283 | e.stopPropagation(); |
michael@0 | 2284 | }, |
michael@0 | 2285 | |
michael@0 | 2286 | /** |
michael@0 | 2287 | * The blur listener for a watch expression's textbox. |
michael@0 | 2288 | */ |
michael@0 | 2289 | _onBlur: function({ target: textbox }) { |
michael@0 | 2290 | let expressionItem = this.getItemForElement(textbox); |
michael@0 | 2291 | let oldExpression = expressionItem.attachment.currentExpression; |
michael@0 | 2292 | let newExpression = textbox.value.trim(); |
michael@0 | 2293 | |
michael@0 | 2294 | // Remove the watch expression if it's empty. |
michael@0 | 2295 | if (!newExpression) { |
michael@0 | 2296 | this.remove(expressionItem); |
michael@0 | 2297 | } |
michael@0 | 2298 | // Remove the watch expression if it's a duplicate. |
michael@0 | 2299 | else if (!oldExpression && this.getAllStrings().indexOf(newExpression) != -1) { |
michael@0 | 2300 | this.remove(expressionItem); |
michael@0 | 2301 | } |
michael@0 | 2302 | // Expression is eligible. |
michael@0 | 2303 | else { |
michael@0 | 2304 | expressionItem.attachment.currentExpression = newExpression; |
michael@0 | 2305 | } |
michael@0 | 2306 | |
michael@0 | 2307 | // Synchronize with the controller's watch expressions store. |
michael@0 | 2308 | DebuggerController.StackFrames.syncWatchExpressions(); |
michael@0 | 2309 | }, |
michael@0 | 2310 | |
michael@0 | 2311 | /** |
michael@0 | 2312 | * The keypress listener for a watch expression's textbox. |
michael@0 | 2313 | */ |
michael@0 | 2314 | _onKeyPress: function(e) { |
michael@0 | 2315 | switch(e.keyCode) { |
michael@0 | 2316 | case e.DOM_VK_RETURN: |
michael@0 | 2317 | case e.DOM_VK_ESCAPE: |
michael@0 | 2318 | e.stopPropagation(); |
michael@0 | 2319 | DebuggerView.editor.focus(); |
michael@0 | 2320 | return; |
michael@0 | 2321 | } |
michael@0 | 2322 | } |
michael@0 | 2323 | }); |
michael@0 | 2324 | |
michael@0 | 2325 | /** |
michael@0 | 2326 | * Functions handling the event listeners UI. |
michael@0 | 2327 | */ |
michael@0 | 2328 | function EventListenersView() { |
michael@0 | 2329 | dumpn("EventListenersView was instantiated"); |
michael@0 | 2330 | |
michael@0 | 2331 | this._onCheck = this._onCheck.bind(this); |
michael@0 | 2332 | this._onClick = this._onClick.bind(this); |
michael@0 | 2333 | } |
michael@0 | 2334 | |
michael@0 | 2335 | EventListenersView.prototype = Heritage.extend(WidgetMethods, { |
michael@0 | 2336 | /** |
michael@0 | 2337 | * Initialization function, called when the debugger is started. |
michael@0 | 2338 | */ |
michael@0 | 2339 | initialize: function() { |
michael@0 | 2340 | dumpn("Initializing the EventListenersView"); |
michael@0 | 2341 | |
michael@0 | 2342 | this.widget = new SideMenuWidget(document.getElementById("event-listeners"), { |
michael@0 | 2343 | showItemCheckboxes: true, |
michael@0 | 2344 | showGroupCheckboxes: true |
michael@0 | 2345 | }); |
michael@0 | 2346 | |
michael@0 | 2347 | this.emptyText = L10N.getStr("noEventListenersText"); |
michael@0 | 2348 | this._eventCheckboxTooltip = L10N.getStr("eventCheckboxTooltip"); |
michael@0 | 2349 | this._onSelectorString = " " + L10N.getStr("eventOnSelector") + " "; |
michael@0 | 2350 | this._inSourceString = " " + L10N.getStr("eventInSource") + " "; |
michael@0 | 2351 | this._inNativeCodeString = L10N.getStr("eventNative"); |
michael@0 | 2352 | |
michael@0 | 2353 | this.widget.addEventListener("check", this._onCheck, false); |
michael@0 | 2354 | this.widget.addEventListener("click", this._onClick, false); |
michael@0 | 2355 | }, |
michael@0 | 2356 | |
michael@0 | 2357 | /** |
michael@0 | 2358 | * Destruction function, called when the debugger is closed. |
michael@0 | 2359 | */ |
michael@0 | 2360 | destroy: function() { |
michael@0 | 2361 | dumpn("Destroying the EventListenersView"); |
michael@0 | 2362 | |
michael@0 | 2363 | this.widget.removeEventListener("check", this._onCheck, false); |
michael@0 | 2364 | this.widget.removeEventListener("click", this._onClick, false); |
michael@0 | 2365 | }, |
michael@0 | 2366 | |
michael@0 | 2367 | /** |
michael@0 | 2368 | * Adds an event to this event listeners container. |
michael@0 | 2369 | * |
michael@0 | 2370 | * @param object aListener |
michael@0 | 2371 | * The listener object coming from the active thread. |
michael@0 | 2372 | * @param object aOptions [optional] |
michael@0 | 2373 | * Additional options for adding the source. Supported options: |
michael@0 | 2374 | * - staged: true to stage the item to be appended later |
michael@0 | 2375 | */ |
michael@0 | 2376 | addListener: function(aListener, aOptions = {}) { |
michael@0 | 2377 | let { node: { selector }, function: { url }, type } = aListener; |
michael@0 | 2378 | if (!type) return; |
michael@0 | 2379 | |
michael@0 | 2380 | // Some listener objects may be added from plugins, thus getting |
michael@0 | 2381 | // translated to native code. |
michael@0 | 2382 | if (!url) { |
michael@0 | 2383 | url = this._inNativeCodeString; |
michael@0 | 2384 | } |
michael@0 | 2385 | |
michael@0 | 2386 | // If an event item for this listener's url and type was already added, |
michael@0 | 2387 | // avoid polluting the view and simply increase the "targets" count. |
michael@0 | 2388 | let eventItem = this.getItemForPredicate(aItem => |
michael@0 | 2389 | aItem.attachment.url == url && |
michael@0 | 2390 | aItem.attachment.type == type); |
michael@0 | 2391 | if (eventItem) { |
michael@0 | 2392 | let { selectors, view: { targets } } = eventItem.attachment; |
michael@0 | 2393 | if (selectors.indexOf(selector) == -1) { |
michael@0 | 2394 | selectors.push(selector); |
michael@0 | 2395 | targets.setAttribute("value", L10N.getFormatStr("eventNodes", selectors.length)); |
michael@0 | 2396 | } |
michael@0 | 2397 | return; |
michael@0 | 2398 | } |
michael@0 | 2399 | |
michael@0 | 2400 | // There's no easy way of grouping event types into higher-level groups, |
michael@0 | 2401 | // so we need to do this by hand. |
michael@0 | 2402 | let is = (...args) => args.indexOf(type) != -1; |
michael@0 | 2403 | let has = str => type.contains(str); |
michael@0 | 2404 | let starts = str => type.startsWith(str); |
michael@0 | 2405 | let group; |
michael@0 | 2406 | |
michael@0 | 2407 | if (starts("animation")) { |
michael@0 | 2408 | group = L10N.getStr("animationEvents"); |
michael@0 | 2409 | } else if (starts("audio")) { |
michael@0 | 2410 | group = L10N.getStr("audioEvents"); |
michael@0 | 2411 | } else if (is("levelchange")) { |
michael@0 | 2412 | group = L10N.getStr("batteryEvents"); |
michael@0 | 2413 | } else if (is("cut", "copy", "paste")) { |
michael@0 | 2414 | group = L10N.getStr("clipboardEvents"); |
michael@0 | 2415 | } else if (starts("composition")) { |
michael@0 | 2416 | group = L10N.getStr("compositionEvents"); |
michael@0 | 2417 | } else if (starts("device")) { |
michael@0 | 2418 | group = L10N.getStr("deviceEvents"); |
michael@0 | 2419 | } else if (is("fullscreenchange", "fullscreenerror", "orientationchange", |
michael@0 | 2420 | "overflow", "resize", "scroll", "underflow", "zoom")) { |
michael@0 | 2421 | group = L10N.getStr("displayEvents"); |
michael@0 | 2422 | } else if (starts("drag") || starts("drop")) { |
michael@0 | 2423 | group = L10N.getStr("Drag and dropEvents"); |
michael@0 | 2424 | } else if (starts("gamepad")) { |
michael@0 | 2425 | group = L10N.getStr("gamepadEvents"); |
michael@0 | 2426 | } else if (is("canplay", "canplaythrough", "durationchange", "emptied", |
michael@0 | 2427 | "ended", "loadeddata", "loadedmetadata", "pause", "play", "playing", |
michael@0 | 2428 | "ratechange", "seeked", "seeking", "stalled", "suspend", "timeupdate", |
michael@0 | 2429 | "volumechange", "waiting")) { |
michael@0 | 2430 | group = L10N.getStr("mediaEvents"); |
michael@0 | 2431 | } else if (is("blocked", "complete", "success", "upgradeneeded", "versionchange")) { |
michael@0 | 2432 | group = L10N.getStr("indexedDBEvents"); |
michael@0 | 2433 | } else if (is("blur", "change", "focus", "focusin", "focusout", "invalid", |
michael@0 | 2434 | "reset", "select", "submit")) { |
michael@0 | 2435 | group = L10N.getStr("interactionEvents"); |
michael@0 | 2436 | } else if (starts("key") || is("input")) { |
michael@0 | 2437 | group = L10N.getStr("keyboardEvents"); |
michael@0 | 2438 | } else if (starts("mouse") || has("click") || is("contextmenu", "show", "wheel")) { |
michael@0 | 2439 | group = L10N.getStr("mouseEvents"); |
michael@0 | 2440 | } else if (starts("DOM")) { |
michael@0 | 2441 | group = L10N.getStr("mutationEvents"); |
michael@0 | 2442 | } else if (is("abort", "error", "hashchange", "load", "loadend", "loadstart", |
michael@0 | 2443 | "pagehide", "pageshow", "progress", "timeout", "unload", "uploadprogress", |
michael@0 | 2444 | "visibilitychange")) { |
michael@0 | 2445 | group = L10N.getStr("navigationEvents"); |
michael@0 | 2446 | } else if (is("pointerlockchange", "pointerlockerror")) { |
michael@0 | 2447 | group = L10N.getStr("Pointer lockEvents"); |
michael@0 | 2448 | } else if (is("compassneedscalibration", "userproximity")) { |
michael@0 | 2449 | group = L10N.getStr("sensorEvents"); |
michael@0 | 2450 | } else if (starts("storage")) { |
michael@0 | 2451 | group = L10N.getStr("storageEvents"); |
michael@0 | 2452 | } else if (is("beginEvent", "endEvent", "repeatEvent")) { |
michael@0 | 2453 | group = L10N.getStr("timeEvents"); |
michael@0 | 2454 | } else if (starts("touch")) { |
michael@0 | 2455 | group = L10N.getStr("touchEvents"); |
michael@0 | 2456 | } else { |
michael@0 | 2457 | group = L10N.getStr("otherEvents"); |
michael@0 | 2458 | } |
michael@0 | 2459 | |
michael@0 | 2460 | // Create the element node for the event listener item. |
michael@0 | 2461 | let itemView = this._createItemView(type, selector, url); |
michael@0 | 2462 | |
michael@0 | 2463 | // Event breakpoints survive target navigations. Make sure the newly |
michael@0 | 2464 | // inserted event item is correctly checked. |
michael@0 | 2465 | let checkboxState = |
michael@0 | 2466 | DebuggerController.Breakpoints.DOM.activeEventNames.indexOf(type) != -1; |
michael@0 | 2467 | |
michael@0 | 2468 | // Append an event listener item to this container. |
michael@0 | 2469 | this.push([itemView.container], { |
michael@0 | 2470 | staged: aOptions.staged, /* stage the item to be appended later? */ |
michael@0 | 2471 | attachment: { |
michael@0 | 2472 | url: url, |
michael@0 | 2473 | type: type, |
michael@0 | 2474 | view: itemView, |
michael@0 | 2475 | selectors: [selector], |
michael@0 | 2476 | group: group, |
michael@0 | 2477 | checkboxState: checkboxState, |
michael@0 | 2478 | checkboxTooltip: this._eventCheckboxTooltip |
michael@0 | 2479 | } |
michael@0 | 2480 | }); |
michael@0 | 2481 | }, |
michael@0 | 2482 | |
michael@0 | 2483 | /** |
michael@0 | 2484 | * Gets all the event types known to this container. |
michael@0 | 2485 | * |
michael@0 | 2486 | * @return array |
michael@0 | 2487 | * List of event types, for example ["load", "click"...] |
michael@0 | 2488 | */ |
michael@0 | 2489 | getAllEvents: function() { |
michael@0 | 2490 | return this.attachments.map(e => e.type); |
michael@0 | 2491 | }, |
michael@0 | 2492 | |
michael@0 | 2493 | /** |
michael@0 | 2494 | * Gets the checked event types in this container. |
michael@0 | 2495 | * |
michael@0 | 2496 | * @return array |
michael@0 | 2497 | * List of event types, for example ["load", "click"...] |
michael@0 | 2498 | */ |
michael@0 | 2499 | getCheckedEvents: function() { |
michael@0 | 2500 | return this.attachments.filter(e => e.checkboxState).map(e => e.type); |
michael@0 | 2501 | }, |
michael@0 | 2502 | |
michael@0 | 2503 | /** |
michael@0 | 2504 | * Customization function for creating an item's UI. |
michael@0 | 2505 | * |
michael@0 | 2506 | * @param string aType |
michael@0 | 2507 | * The event type, for example "click". |
michael@0 | 2508 | * @param string aSelector |
michael@0 | 2509 | * The target element's selector. |
michael@0 | 2510 | * @param string url |
michael@0 | 2511 | * The source url in which the event listener is located. |
michael@0 | 2512 | * @return object |
michael@0 | 2513 | * An object containing the event listener view nodes. |
michael@0 | 2514 | */ |
michael@0 | 2515 | _createItemView: function(aType, aSelector, aUrl) { |
michael@0 | 2516 | let container = document.createElement("hbox"); |
michael@0 | 2517 | container.className = "dbg-event-listener"; |
michael@0 | 2518 | |
michael@0 | 2519 | let eventType = document.createElement("label"); |
michael@0 | 2520 | eventType.className = "plain dbg-event-listener-type"; |
michael@0 | 2521 | eventType.setAttribute("value", aType); |
michael@0 | 2522 | container.appendChild(eventType); |
michael@0 | 2523 | |
michael@0 | 2524 | let typeSeparator = document.createElement("label"); |
michael@0 | 2525 | typeSeparator.className = "plain dbg-event-listener-separator"; |
michael@0 | 2526 | typeSeparator.setAttribute("value", this._onSelectorString); |
michael@0 | 2527 | container.appendChild(typeSeparator); |
michael@0 | 2528 | |
michael@0 | 2529 | let eventTargets = document.createElement("label"); |
michael@0 | 2530 | eventTargets.className = "plain dbg-event-listener-targets"; |
michael@0 | 2531 | eventTargets.setAttribute("value", aSelector); |
michael@0 | 2532 | container.appendChild(eventTargets); |
michael@0 | 2533 | |
michael@0 | 2534 | let selectorSeparator = document.createElement("label"); |
michael@0 | 2535 | selectorSeparator.className = "plain dbg-event-listener-separator"; |
michael@0 | 2536 | selectorSeparator.setAttribute("value", this._inSourceString); |
michael@0 | 2537 | container.appendChild(selectorSeparator); |
michael@0 | 2538 | |
michael@0 | 2539 | let eventLocation = document.createElement("label"); |
michael@0 | 2540 | eventLocation.className = "plain dbg-event-listener-location"; |
michael@0 | 2541 | eventLocation.setAttribute("value", SourceUtils.getSourceLabel(aUrl)); |
michael@0 | 2542 | eventLocation.setAttribute("flex", "1"); |
michael@0 | 2543 | eventLocation.setAttribute("crop", "center"); |
michael@0 | 2544 | container.appendChild(eventLocation); |
michael@0 | 2545 | |
michael@0 | 2546 | return { |
michael@0 | 2547 | container: container, |
michael@0 | 2548 | type: eventType, |
michael@0 | 2549 | targets: eventTargets, |
michael@0 | 2550 | location: eventLocation |
michael@0 | 2551 | }; |
michael@0 | 2552 | }, |
michael@0 | 2553 | |
michael@0 | 2554 | /** |
michael@0 | 2555 | * The check listener for the event listeners container. |
michael@0 | 2556 | */ |
michael@0 | 2557 | _onCheck: function({ detail: { description, checked }, target }) { |
michael@0 | 2558 | if (description == "item") { |
michael@0 | 2559 | this.getItemForElement(target).attachment.checkboxState = checked; |
michael@0 | 2560 | DebuggerController.Breakpoints.DOM.scheduleEventBreakpointsUpdate(); |
michael@0 | 2561 | return; |
michael@0 | 2562 | } |
michael@0 | 2563 | |
michael@0 | 2564 | // Check all the event items in this group. |
michael@0 | 2565 | this.items |
michael@0 | 2566 | .filter(e => e.attachment.group == description) |
michael@0 | 2567 | .forEach(e => this.callMethod("checkItem", e.target, checked)); |
michael@0 | 2568 | }, |
michael@0 | 2569 | |
michael@0 | 2570 | /** |
michael@0 | 2571 | * The select listener for the event listeners container. |
michael@0 | 2572 | */ |
michael@0 | 2573 | _onClick: function({ target }) { |
michael@0 | 2574 | // Changing the checkbox state is handled by the _onCheck event. Avoid |
michael@0 | 2575 | // handling that again in this click event, so pass in "noSiblings" |
michael@0 | 2576 | // when retrieving the target's item, to ignore the checkbox. |
michael@0 | 2577 | let eventItem = this.getItemForElement(target, { noSiblings: true }); |
michael@0 | 2578 | if (eventItem) { |
michael@0 | 2579 | let newState = eventItem.attachment.checkboxState ^= 1; |
michael@0 | 2580 | this.callMethod("checkItem", eventItem.target, newState); |
michael@0 | 2581 | } |
michael@0 | 2582 | }, |
michael@0 | 2583 | |
michael@0 | 2584 | _eventCheckboxTooltip: "", |
michael@0 | 2585 | _onSelectorString: "", |
michael@0 | 2586 | _inSourceString: "", |
michael@0 | 2587 | _inNativeCodeString: "" |
michael@0 | 2588 | }); |
michael@0 | 2589 | |
michael@0 | 2590 | /** |
michael@0 | 2591 | * Functions handling the global search UI. |
michael@0 | 2592 | */ |
michael@0 | 2593 | function GlobalSearchView() { |
michael@0 | 2594 | dumpn("GlobalSearchView was instantiated"); |
michael@0 | 2595 | |
michael@0 | 2596 | this._onHeaderClick = this._onHeaderClick.bind(this); |
michael@0 | 2597 | this._onLineClick = this._onLineClick.bind(this); |
michael@0 | 2598 | this._onMatchClick = this._onMatchClick.bind(this); |
michael@0 | 2599 | } |
michael@0 | 2600 | |
michael@0 | 2601 | GlobalSearchView.prototype = Heritage.extend(WidgetMethods, { |
michael@0 | 2602 | /** |
michael@0 | 2603 | * Initialization function, called when the debugger is started. |
michael@0 | 2604 | */ |
michael@0 | 2605 | initialize: function() { |
michael@0 | 2606 | dumpn("Initializing the GlobalSearchView"); |
michael@0 | 2607 | |
michael@0 | 2608 | this.widget = new SimpleListWidget(document.getElementById("globalsearch")); |
michael@0 | 2609 | this._splitter = document.querySelector("#globalsearch + .devtools-horizontal-splitter"); |
michael@0 | 2610 | |
michael@0 | 2611 | this.emptyText = L10N.getStr("noMatchingStringsText"); |
michael@0 | 2612 | }, |
michael@0 | 2613 | |
michael@0 | 2614 | /** |
michael@0 | 2615 | * Destruction function, called when the debugger is closed. |
michael@0 | 2616 | */ |
michael@0 | 2617 | destroy: function() { |
michael@0 | 2618 | dumpn("Destroying the GlobalSearchView"); |
michael@0 | 2619 | }, |
michael@0 | 2620 | |
michael@0 | 2621 | /** |
michael@0 | 2622 | * Sets the results container hidden or visible. It's hidden by default. |
michael@0 | 2623 | * @param boolean aFlag |
michael@0 | 2624 | */ |
michael@0 | 2625 | set hidden(aFlag) { |
michael@0 | 2626 | this.widget.setAttribute("hidden", aFlag); |
michael@0 | 2627 | this._splitter.setAttribute("hidden", aFlag); |
michael@0 | 2628 | }, |
michael@0 | 2629 | |
michael@0 | 2630 | /** |
michael@0 | 2631 | * Gets the visibility state of the global search container. |
michael@0 | 2632 | * @return boolean |
michael@0 | 2633 | */ |
michael@0 | 2634 | get hidden() |
michael@0 | 2635 | this.widget.getAttribute("hidden") == "true" || |
michael@0 | 2636 | this._splitter.getAttribute("hidden") == "true", |
michael@0 | 2637 | |
michael@0 | 2638 | /** |
michael@0 | 2639 | * Hides and removes all items from this search container. |
michael@0 | 2640 | */ |
michael@0 | 2641 | clearView: function() { |
michael@0 | 2642 | this.hidden = true; |
michael@0 | 2643 | this.empty(); |
michael@0 | 2644 | }, |
michael@0 | 2645 | |
michael@0 | 2646 | /** |
michael@0 | 2647 | * Selects the next found item in this container. |
michael@0 | 2648 | * Does not change the currently focused node. |
michael@0 | 2649 | */ |
michael@0 | 2650 | selectNext: function() { |
michael@0 | 2651 | let totalLineResults = LineResults.size(); |
michael@0 | 2652 | if (!totalLineResults) { |
michael@0 | 2653 | return; |
michael@0 | 2654 | } |
michael@0 | 2655 | if (++this._currentlyFocusedMatch >= totalLineResults) { |
michael@0 | 2656 | this._currentlyFocusedMatch = 0; |
michael@0 | 2657 | } |
michael@0 | 2658 | this._onMatchClick({ |
michael@0 | 2659 | target: LineResults.getElementAtIndex(this._currentlyFocusedMatch) |
michael@0 | 2660 | }); |
michael@0 | 2661 | }, |
michael@0 | 2662 | |
michael@0 | 2663 | /** |
michael@0 | 2664 | * Selects the previously found item in this container. |
michael@0 | 2665 | * Does not change the currently focused node. |
michael@0 | 2666 | */ |
michael@0 | 2667 | selectPrev: function() { |
michael@0 | 2668 | let totalLineResults = LineResults.size(); |
michael@0 | 2669 | if (!totalLineResults) { |
michael@0 | 2670 | return; |
michael@0 | 2671 | } |
michael@0 | 2672 | if (--this._currentlyFocusedMatch < 0) { |
michael@0 | 2673 | this._currentlyFocusedMatch = totalLineResults - 1; |
michael@0 | 2674 | } |
michael@0 | 2675 | this._onMatchClick({ |
michael@0 | 2676 | target: LineResults.getElementAtIndex(this._currentlyFocusedMatch) |
michael@0 | 2677 | }); |
michael@0 | 2678 | }, |
michael@0 | 2679 | |
michael@0 | 2680 | /** |
michael@0 | 2681 | * Schedules searching for a string in all of the sources. |
michael@0 | 2682 | * |
michael@0 | 2683 | * @param string aToken |
michael@0 | 2684 | * The string to search for. |
michael@0 | 2685 | * @param number aWait |
michael@0 | 2686 | * The amount of milliseconds to wait until draining. |
michael@0 | 2687 | */ |
michael@0 | 2688 | scheduleSearch: function(aToken, aWait) { |
michael@0 | 2689 | // The amount of time to wait for the requests to settle. |
michael@0 | 2690 | let maxDelay = GLOBAL_SEARCH_ACTION_MAX_DELAY; |
michael@0 | 2691 | let delay = aWait === undefined ? maxDelay / aToken.length : aWait; |
michael@0 | 2692 | |
michael@0 | 2693 | // Allow requests to settle down first. |
michael@0 | 2694 | setNamedTimeout("global-search", delay, () => { |
michael@0 | 2695 | // Start fetching as many sources as possible, then perform the search. |
michael@0 | 2696 | let urls = DebuggerView.Sources.values; |
michael@0 | 2697 | let sourcesFetched = DebuggerController.SourceScripts.getTextForSources(urls); |
michael@0 | 2698 | sourcesFetched.then(aSources => this._doSearch(aToken, aSources)); |
michael@0 | 2699 | }); |
michael@0 | 2700 | }, |
michael@0 | 2701 | |
michael@0 | 2702 | /** |
michael@0 | 2703 | * Finds string matches in all the sources stored in the controller's cache, |
michael@0 | 2704 | * and groups them by url and line number. |
michael@0 | 2705 | * |
michael@0 | 2706 | * @param string aToken |
michael@0 | 2707 | * The string to search for. |
michael@0 | 2708 | * @param array aSources |
michael@0 | 2709 | * An array of [url, text] tuples for each source. |
michael@0 | 2710 | */ |
michael@0 | 2711 | _doSearch: function(aToken, aSources) { |
michael@0 | 2712 | // Don't continue filtering if the searched token is an empty string. |
michael@0 | 2713 | if (!aToken) { |
michael@0 | 2714 | this.clearView(); |
michael@0 | 2715 | return; |
michael@0 | 2716 | } |
michael@0 | 2717 | |
michael@0 | 2718 | // Search is not case sensitive, prepare the actual searched token. |
michael@0 | 2719 | let lowerCaseToken = aToken.toLowerCase(); |
michael@0 | 2720 | let tokenLength = aToken.length; |
michael@0 | 2721 | |
michael@0 | 2722 | // Create a Map containing search details for each source. |
michael@0 | 2723 | let globalResults = new GlobalResults(); |
michael@0 | 2724 | |
michael@0 | 2725 | // Search for the specified token in each source's text. |
michael@0 | 2726 | for (let [url, text] of aSources) { |
michael@0 | 2727 | // Verify that the search token is found anywhere in the source. |
michael@0 | 2728 | if (!text.toLowerCase().contains(lowerCaseToken)) { |
michael@0 | 2729 | continue; |
michael@0 | 2730 | } |
michael@0 | 2731 | // ...and if so, create a Map containing search details for each line. |
michael@0 | 2732 | let sourceResults = new SourceResults(url, globalResults); |
michael@0 | 2733 | |
michael@0 | 2734 | // Search for the specified token in each line's text. |
michael@0 | 2735 | text.split("\n").forEach((aString, aLine) => { |
michael@0 | 2736 | // Search is not case sensitive, prepare the actual searched line. |
michael@0 | 2737 | let lowerCaseLine = aString.toLowerCase(); |
michael@0 | 2738 | |
michael@0 | 2739 | // Verify that the search token is found anywhere in this line. |
michael@0 | 2740 | if (!lowerCaseLine.contains(lowerCaseToken)) { |
michael@0 | 2741 | return; |
michael@0 | 2742 | } |
michael@0 | 2743 | // ...and if so, create a Map containing search details for each word. |
michael@0 | 2744 | let lineResults = new LineResults(aLine, sourceResults); |
michael@0 | 2745 | |
michael@0 | 2746 | // Search for the specified token this line's text. |
michael@0 | 2747 | lowerCaseLine.split(lowerCaseToken).reduce((aPrev, aCurr, aIndex, aArray) => { |
michael@0 | 2748 | let prevLength = aPrev.length; |
michael@0 | 2749 | let currLength = aCurr.length; |
michael@0 | 2750 | |
michael@0 | 2751 | // Everything before the token is unmatched. |
michael@0 | 2752 | let unmatched = aString.substr(prevLength, currLength); |
michael@0 | 2753 | lineResults.add(unmatched); |
michael@0 | 2754 | |
michael@0 | 2755 | // The lowered-case line was split by the lowered-case token. So, |
michael@0 | 2756 | // get the actual matched text from the original line's text. |
michael@0 | 2757 | if (aIndex != aArray.length - 1) { |
michael@0 | 2758 | let matched = aString.substr(prevLength + currLength, tokenLength); |
michael@0 | 2759 | let range = { start: prevLength + currLength, length: matched.length }; |
michael@0 | 2760 | lineResults.add(matched, range, true); |
michael@0 | 2761 | } |
michael@0 | 2762 | |
michael@0 | 2763 | // Continue with the next sub-region in this line's text. |
michael@0 | 2764 | return aPrev + aToken + aCurr; |
michael@0 | 2765 | }, ""); |
michael@0 | 2766 | |
michael@0 | 2767 | if (lineResults.matchCount) { |
michael@0 | 2768 | sourceResults.add(lineResults); |
michael@0 | 2769 | } |
michael@0 | 2770 | }); |
michael@0 | 2771 | |
michael@0 | 2772 | if (sourceResults.matchCount) { |
michael@0 | 2773 | globalResults.add(sourceResults); |
michael@0 | 2774 | } |
michael@0 | 2775 | } |
michael@0 | 2776 | |
michael@0 | 2777 | // Rebuild the results, then signal if there are any matches. |
michael@0 | 2778 | if (globalResults.matchCount) { |
michael@0 | 2779 | this.hidden = false; |
michael@0 | 2780 | this._currentlyFocusedMatch = -1; |
michael@0 | 2781 | this._createGlobalResultsUI(globalResults); |
michael@0 | 2782 | window.emit(EVENTS.GLOBAL_SEARCH_MATCH_FOUND); |
michael@0 | 2783 | } else { |
michael@0 | 2784 | window.emit(EVENTS.GLOBAL_SEARCH_MATCH_NOT_FOUND); |
michael@0 | 2785 | } |
michael@0 | 2786 | }, |
michael@0 | 2787 | |
michael@0 | 2788 | /** |
michael@0 | 2789 | * Creates global search results entries and adds them to this container. |
michael@0 | 2790 | * |
michael@0 | 2791 | * @param GlobalResults aGlobalResults |
michael@0 | 2792 | * An object containing all source results, grouped by source location. |
michael@0 | 2793 | */ |
michael@0 | 2794 | _createGlobalResultsUI: function(aGlobalResults) { |
michael@0 | 2795 | let i = 0; |
michael@0 | 2796 | |
michael@0 | 2797 | for (let sourceResults of aGlobalResults) { |
michael@0 | 2798 | if (i++ == 0) { |
michael@0 | 2799 | this._createSourceResultsUI(sourceResults); |
michael@0 | 2800 | } else { |
michael@0 | 2801 | // Dispatch subsequent document manipulation operations, to avoid |
michael@0 | 2802 | // blocking the main thread when a large number of search results |
michael@0 | 2803 | // is found, thus giving the impression of faster searching. |
michael@0 | 2804 | Services.tm.currentThread.dispatch({ run: |
michael@0 | 2805 | this._createSourceResultsUI.bind(this, sourceResults) |
michael@0 | 2806 | }, 0); |
michael@0 | 2807 | } |
michael@0 | 2808 | } |
michael@0 | 2809 | }, |
michael@0 | 2810 | |
michael@0 | 2811 | /** |
michael@0 | 2812 | * Creates source search results entries and adds them to this container. |
michael@0 | 2813 | * |
michael@0 | 2814 | * @param SourceResults aSourceResults |
michael@0 | 2815 | * An object containing all the matched lines for a specific source. |
michael@0 | 2816 | */ |
michael@0 | 2817 | _createSourceResultsUI: function(aSourceResults) { |
michael@0 | 2818 | // Create the element node for the source results item. |
michael@0 | 2819 | let container = document.createElement("hbox"); |
michael@0 | 2820 | aSourceResults.createView(container, { |
michael@0 | 2821 | onHeaderClick: this._onHeaderClick, |
michael@0 | 2822 | onLineClick: this._onLineClick, |
michael@0 | 2823 | onMatchClick: this._onMatchClick |
michael@0 | 2824 | }); |
michael@0 | 2825 | |
michael@0 | 2826 | // Append a source results item to this container. |
michael@0 | 2827 | let item = this.push([container], { |
michael@0 | 2828 | index: -1, /* specifies on which position should the item be appended */ |
michael@0 | 2829 | attachment: { |
michael@0 | 2830 | sourceResults: aSourceResults |
michael@0 | 2831 | } |
michael@0 | 2832 | }); |
michael@0 | 2833 | }, |
michael@0 | 2834 | |
michael@0 | 2835 | /** |
michael@0 | 2836 | * The click listener for a results header. |
michael@0 | 2837 | */ |
michael@0 | 2838 | _onHeaderClick: function(e) { |
michael@0 | 2839 | let sourceResultsItem = SourceResults.getItemForElement(e.target); |
michael@0 | 2840 | sourceResultsItem.instance.toggle(e); |
michael@0 | 2841 | }, |
michael@0 | 2842 | |
michael@0 | 2843 | /** |
michael@0 | 2844 | * The click listener for a results line. |
michael@0 | 2845 | */ |
michael@0 | 2846 | _onLineClick: function(e) { |
michael@0 | 2847 | let lineResultsItem = LineResults.getItemForElement(e.target); |
michael@0 | 2848 | this._onMatchClick({ target: lineResultsItem.firstMatch }); |
michael@0 | 2849 | }, |
michael@0 | 2850 | |
michael@0 | 2851 | /** |
michael@0 | 2852 | * The click listener for a result match. |
michael@0 | 2853 | */ |
michael@0 | 2854 | _onMatchClick: function(e) { |
michael@0 | 2855 | if (e instanceof Event) { |
michael@0 | 2856 | e.preventDefault(); |
michael@0 | 2857 | e.stopPropagation(); |
michael@0 | 2858 | } |
michael@0 | 2859 | |
michael@0 | 2860 | let target = e.target; |
michael@0 | 2861 | let sourceResultsItem = SourceResults.getItemForElement(target); |
michael@0 | 2862 | let lineResultsItem = LineResults.getItemForElement(target); |
michael@0 | 2863 | |
michael@0 | 2864 | sourceResultsItem.instance.expand(); |
michael@0 | 2865 | this._currentlyFocusedMatch = LineResults.indexOfElement(target); |
michael@0 | 2866 | this._scrollMatchIntoViewIfNeeded(target); |
michael@0 | 2867 | this._bounceMatch(target); |
michael@0 | 2868 | |
michael@0 | 2869 | let url = sourceResultsItem.instance.url; |
michael@0 | 2870 | let line = lineResultsItem.instance.line; |
michael@0 | 2871 | |
michael@0 | 2872 | DebuggerView.setEditorLocation(url, line + 1, { noDebug: true }); |
michael@0 | 2873 | |
michael@0 | 2874 | let range = lineResultsItem.lineData.range; |
michael@0 | 2875 | let cursor = DebuggerView.editor.getOffset({ line: line, ch: 0 }); |
michael@0 | 2876 | let [ anchor, head ] = DebuggerView.editor.getPosition( |
michael@0 | 2877 | cursor + range.start, |
michael@0 | 2878 | cursor + range.start + range.length |
michael@0 | 2879 | ); |
michael@0 | 2880 | |
michael@0 | 2881 | DebuggerView.editor.setSelection(anchor, head); |
michael@0 | 2882 | }, |
michael@0 | 2883 | |
michael@0 | 2884 | /** |
michael@0 | 2885 | * Scrolls a match into view if not already visible. |
michael@0 | 2886 | * |
michael@0 | 2887 | * @param nsIDOMNode aMatch |
michael@0 | 2888 | * The match to scroll into view. |
michael@0 | 2889 | */ |
michael@0 | 2890 | _scrollMatchIntoViewIfNeeded: function(aMatch) { |
michael@0 | 2891 | this.widget.ensureElementIsVisible(aMatch); |
michael@0 | 2892 | }, |
michael@0 | 2893 | |
michael@0 | 2894 | /** |
michael@0 | 2895 | * Starts a bounce animation for a match. |
michael@0 | 2896 | * |
michael@0 | 2897 | * @param nsIDOMNode aMatch |
michael@0 | 2898 | * The match to start a bounce animation for. |
michael@0 | 2899 | */ |
michael@0 | 2900 | _bounceMatch: function(aMatch) { |
michael@0 | 2901 | Services.tm.currentThread.dispatch({ run: () => { |
michael@0 | 2902 | aMatch.addEventListener("transitionend", function onEvent() { |
michael@0 | 2903 | aMatch.removeEventListener("transitionend", onEvent); |
michael@0 | 2904 | aMatch.removeAttribute("focused"); |
michael@0 | 2905 | }); |
michael@0 | 2906 | aMatch.setAttribute("focused", ""); |
michael@0 | 2907 | }}, 0); |
michael@0 | 2908 | aMatch.setAttribute("focusing", ""); |
michael@0 | 2909 | }, |
michael@0 | 2910 | |
michael@0 | 2911 | _splitter: null, |
michael@0 | 2912 | _currentlyFocusedMatch: -1, |
michael@0 | 2913 | _forceExpandResults: false |
michael@0 | 2914 | }); |
michael@0 | 2915 | |
michael@0 | 2916 | /** |
michael@0 | 2917 | * An object containing all source results, grouped by source location. |
michael@0 | 2918 | * Iterable via "for (let [location, sourceResults] of globalResults) { }". |
michael@0 | 2919 | */ |
michael@0 | 2920 | function GlobalResults() { |
michael@0 | 2921 | this._store = []; |
michael@0 | 2922 | SourceResults._itemsByElement = new Map(); |
michael@0 | 2923 | LineResults._itemsByElement = new Map(); |
michael@0 | 2924 | } |
michael@0 | 2925 | |
michael@0 | 2926 | GlobalResults.prototype = { |
michael@0 | 2927 | /** |
michael@0 | 2928 | * Adds source results to this store. |
michael@0 | 2929 | * |
michael@0 | 2930 | * @param SourceResults aSourceResults |
michael@0 | 2931 | * An object containing search results for a specific source. |
michael@0 | 2932 | */ |
michael@0 | 2933 | add: function(aSourceResults) { |
michael@0 | 2934 | this._store.push(aSourceResults); |
michael@0 | 2935 | }, |
michael@0 | 2936 | |
michael@0 | 2937 | /** |
michael@0 | 2938 | * Gets the number of source results in this store. |
michael@0 | 2939 | */ |
michael@0 | 2940 | get matchCount() this._store.length |
michael@0 | 2941 | }; |
michael@0 | 2942 | |
michael@0 | 2943 | /** |
michael@0 | 2944 | * An object containing all the matched lines for a specific source. |
michael@0 | 2945 | * Iterable via "for (let [lineNumber, lineResults] of sourceResults) { }". |
michael@0 | 2946 | * |
michael@0 | 2947 | * @param string aUrl |
michael@0 | 2948 | * The target source url. |
michael@0 | 2949 | * @param GlobalResults aGlobalResults |
michael@0 | 2950 | * An object containing all source results, grouped by source location. |
michael@0 | 2951 | */ |
michael@0 | 2952 | function SourceResults(aUrl, aGlobalResults) { |
michael@0 | 2953 | this.url = aUrl; |
michael@0 | 2954 | this._globalResults = aGlobalResults; |
michael@0 | 2955 | this._store = []; |
michael@0 | 2956 | } |
michael@0 | 2957 | |
michael@0 | 2958 | SourceResults.prototype = { |
michael@0 | 2959 | /** |
michael@0 | 2960 | * Adds line results to this store. |
michael@0 | 2961 | * |
michael@0 | 2962 | * @param LineResults aLineResults |
michael@0 | 2963 | * An object containing search results for a specific line. |
michael@0 | 2964 | */ |
michael@0 | 2965 | add: function(aLineResults) { |
michael@0 | 2966 | this._store.push(aLineResults); |
michael@0 | 2967 | }, |
michael@0 | 2968 | |
michael@0 | 2969 | /** |
michael@0 | 2970 | * Gets the number of line results in this store. |
michael@0 | 2971 | */ |
michael@0 | 2972 | get matchCount() this._store.length, |
michael@0 | 2973 | |
michael@0 | 2974 | /** |
michael@0 | 2975 | * Expands the element, showing all the added details. |
michael@0 | 2976 | */ |
michael@0 | 2977 | expand: function() { |
michael@0 | 2978 | this._resultsContainer.removeAttribute("hidden"); |
michael@0 | 2979 | this._arrow.setAttribute("open", ""); |
michael@0 | 2980 | }, |
michael@0 | 2981 | |
michael@0 | 2982 | /** |
michael@0 | 2983 | * Collapses the element, hiding all the added details. |
michael@0 | 2984 | */ |
michael@0 | 2985 | collapse: function() { |
michael@0 | 2986 | this._resultsContainer.setAttribute("hidden", "true"); |
michael@0 | 2987 | this._arrow.removeAttribute("open"); |
michael@0 | 2988 | }, |
michael@0 | 2989 | |
michael@0 | 2990 | /** |
michael@0 | 2991 | * Toggles between the element collapse/expand state. |
michael@0 | 2992 | */ |
michael@0 | 2993 | toggle: function(e) { |
michael@0 | 2994 | this.expanded ^= 1; |
michael@0 | 2995 | }, |
michael@0 | 2996 | |
michael@0 | 2997 | /** |
michael@0 | 2998 | * Gets this element's expanded state. |
michael@0 | 2999 | * @return boolean |
michael@0 | 3000 | */ |
michael@0 | 3001 | get expanded() |
michael@0 | 3002 | this._resultsContainer.getAttribute("hidden") != "true" && |
michael@0 | 3003 | this._arrow.hasAttribute("open"), |
michael@0 | 3004 | |
michael@0 | 3005 | /** |
michael@0 | 3006 | * Sets this element's expanded state. |
michael@0 | 3007 | * @param boolean aFlag |
michael@0 | 3008 | */ |
michael@0 | 3009 | set expanded(aFlag) this[aFlag ? "expand" : "collapse"](), |
michael@0 | 3010 | |
michael@0 | 3011 | /** |
michael@0 | 3012 | * Gets the element associated with this item. |
michael@0 | 3013 | * @return nsIDOMNode |
michael@0 | 3014 | */ |
michael@0 | 3015 | get target() this._target, |
michael@0 | 3016 | |
michael@0 | 3017 | /** |
michael@0 | 3018 | * Customization function for creating this item's UI. |
michael@0 | 3019 | * |
michael@0 | 3020 | * @param nsIDOMNode aElementNode |
michael@0 | 3021 | * The element associated with the displayed item. |
michael@0 | 3022 | * @param object aCallbacks |
michael@0 | 3023 | * An object containing all the necessary callback functions: |
michael@0 | 3024 | * - onHeaderClick |
michael@0 | 3025 | * - onMatchClick |
michael@0 | 3026 | */ |
michael@0 | 3027 | createView: function(aElementNode, aCallbacks) { |
michael@0 | 3028 | this._target = aElementNode; |
michael@0 | 3029 | |
michael@0 | 3030 | let arrow = this._arrow = document.createElement("box"); |
michael@0 | 3031 | arrow.className = "arrow"; |
michael@0 | 3032 | |
michael@0 | 3033 | let locationNode = document.createElement("label"); |
michael@0 | 3034 | locationNode.className = "plain dbg-results-header-location"; |
michael@0 | 3035 | locationNode.setAttribute("value", this.url); |
michael@0 | 3036 | |
michael@0 | 3037 | let matchCountNode = document.createElement("label"); |
michael@0 | 3038 | matchCountNode.className = "plain dbg-results-header-match-count"; |
michael@0 | 3039 | matchCountNode.setAttribute("value", "(" + this.matchCount + ")"); |
michael@0 | 3040 | |
michael@0 | 3041 | let resultsHeader = this._resultsHeader = document.createElement("hbox"); |
michael@0 | 3042 | resultsHeader.className = "dbg-results-header"; |
michael@0 | 3043 | resultsHeader.setAttribute("align", "center") |
michael@0 | 3044 | resultsHeader.appendChild(arrow); |
michael@0 | 3045 | resultsHeader.appendChild(locationNode); |
michael@0 | 3046 | resultsHeader.appendChild(matchCountNode); |
michael@0 | 3047 | resultsHeader.addEventListener("click", aCallbacks.onHeaderClick, false); |
michael@0 | 3048 | |
michael@0 | 3049 | let resultsContainer = this._resultsContainer = document.createElement("vbox"); |
michael@0 | 3050 | resultsContainer.className = "dbg-results-container"; |
michael@0 | 3051 | resultsContainer.setAttribute("hidden", "true"); |
michael@0 | 3052 | |
michael@0 | 3053 | // Create lines search results entries and add them to this container. |
michael@0 | 3054 | // Afterwards, if the number of matches is reasonable, expand this |
michael@0 | 3055 | // container automatically. |
michael@0 | 3056 | for (let lineResults of this._store) { |
michael@0 | 3057 | lineResults.createView(resultsContainer, aCallbacks); |
michael@0 | 3058 | } |
michael@0 | 3059 | if (this.matchCount < GLOBAL_SEARCH_EXPAND_MAX_RESULTS) { |
michael@0 | 3060 | this.expand(); |
michael@0 | 3061 | } |
michael@0 | 3062 | |
michael@0 | 3063 | let resultsBox = document.createElement("vbox"); |
michael@0 | 3064 | resultsBox.setAttribute("flex", "1"); |
michael@0 | 3065 | resultsBox.appendChild(resultsHeader); |
michael@0 | 3066 | resultsBox.appendChild(resultsContainer); |
michael@0 | 3067 | |
michael@0 | 3068 | aElementNode.id = "source-results-" + this.url; |
michael@0 | 3069 | aElementNode.className = "dbg-source-results"; |
michael@0 | 3070 | aElementNode.appendChild(resultsBox); |
michael@0 | 3071 | |
michael@0 | 3072 | SourceResults._itemsByElement.set(aElementNode, { instance: this }); |
michael@0 | 3073 | }, |
michael@0 | 3074 | |
michael@0 | 3075 | url: "", |
michael@0 | 3076 | _globalResults: null, |
michael@0 | 3077 | _store: null, |
michael@0 | 3078 | _target: null, |
michael@0 | 3079 | _arrow: null, |
michael@0 | 3080 | _resultsHeader: null, |
michael@0 | 3081 | _resultsContainer: null |
michael@0 | 3082 | }; |
michael@0 | 3083 | |
michael@0 | 3084 | /** |
michael@0 | 3085 | * An object containing all the matches for a specific line. |
michael@0 | 3086 | * Iterable via "for (let chunk of lineResults) { }". |
michael@0 | 3087 | * |
michael@0 | 3088 | * @param number aLine |
michael@0 | 3089 | * The target line in the source. |
michael@0 | 3090 | * @param SourceResults aSourceResults |
michael@0 | 3091 | * An object containing all the matched lines for a specific source. |
michael@0 | 3092 | */ |
michael@0 | 3093 | function LineResults(aLine, aSourceResults) { |
michael@0 | 3094 | this.line = aLine; |
michael@0 | 3095 | this._sourceResults = aSourceResults; |
michael@0 | 3096 | this._store = []; |
michael@0 | 3097 | this._matchCount = 0; |
michael@0 | 3098 | } |
michael@0 | 3099 | |
michael@0 | 3100 | LineResults.prototype = { |
michael@0 | 3101 | /** |
michael@0 | 3102 | * Adds string details to this store. |
michael@0 | 3103 | * |
michael@0 | 3104 | * @param string aString |
michael@0 | 3105 | * The text contents chunk in the line. |
michael@0 | 3106 | * @param object aRange |
michael@0 | 3107 | * An object containing the { start, length } of the chunk. |
michael@0 | 3108 | * @param boolean aMatchFlag |
michael@0 | 3109 | * True if the chunk is a matched string, false if just text content. |
michael@0 | 3110 | */ |
michael@0 | 3111 | add: function(aString, aRange, aMatchFlag) { |
michael@0 | 3112 | this._store.push({ string: aString, range: aRange, match: !!aMatchFlag }); |
michael@0 | 3113 | this._matchCount += aMatchFlag ? 1 : 0; |
michael@0 | 3114 | }, |
michael@0 | 3115 | |
michael@0 | 3116 | /** |
michael@0 | 3117 | * Gets the number of word results in this store. |
michael@0 | 3118 | */ |
michael@0 | 3119 | get matchCount() this._matchCount, |
michael@0 | 3120 | |
michael@0 | 3121 | /** |
michael@0 | 3122 | * Gets the element associated with this item. |
michael@0 | 3123 | * @return nsIDOMNode |
michael@0 | 3124 | */ |
michael@0 | 3125 | get target() this._target, |
michael@0 | 3126 | |
michael@0 | 3127 | /** |
michael@0 | 3128 | * Customization function for creating this item's UI. |
michael@0 | 3129 | * |
michael@0 | 3130 | * @param nsIDOMNode aElementNode |
michael@0 | 3131 | * The element associated with the displayed item. |
michael@0 | 3132 | * @param object aCallbacks |
michael@0 | 3133 | * An object containing all the necessary callback functions: |
michael@0 | 3134 | * - onMatchClick |
michael@0 | 3135 | * - onLineClick |
michael@0 | 3136 | */ |
michael@0 | 3137 | createView: function(aElementNode, aCallbacks) { |
michael@0 | 3138 | this._target = aElementNode; |
michael@0 | 3139 | |
michael@0 | 3140 | let lineNumberNode = document.createElement("label"); |
michael@0 | 3141 | lineNumberNode.className = "plain dbg-results-line-number"; |
michael@0 | 3142 | lineNumberNode.classList.add("devtools-monospace"); |
michael@0 | 3143 | lineNumberNode.setAttribute("value", this.line + 1); |
michael@0 | 3144 | |
michael@0 | 3145 | let lineContentsNode = document.createElement("hbox"); |
michael@0 | 3146 | lineContentsNode.className = "dbg-results-line-contents"; |
michael@0 | 3147 | lineContentsNode.classList.add("devtools-monospace"); |
michael@0 | 3148 | lineContentsNode.setAttribute("flex", "1"); |
michael@0 | 3149 | |
michael@0 | 3150 | let lineString = ""; |
michael@0 | 3151 | let lineLength = 0; |
michael@0 | 3152 | let firstMatch = null; |
michael@0 | 3153 | |
michael@0 | 3154 | for (let lineChunk of this._store) { |
michael@0 | 3155 | let { string, range, match } = lineChunk; |
michael@0 | 3156 | lineString = string.substr(0, GLOBAL_SEARCH_LINE_MAX_LENGTH - lineLength); |
michael@0 | 3157 | lineLength += string.length; |
michael@0 | 3158 | |
michael@0 | 3159 | let lineChunkNode = document.createElement("label"); |
michael@0 | 3160 | lineChunkNode.className = "plain dbg-results-line-contents-string"; |
michael@0 | 3161 | lineChunkNode.setAttribute("value", lineString); |
michael@0 | 3162 | lineChunkNode.setAttribute("match", match); |
michael@0 | 3163 | lineContentsNode.appendChild(lineChunkNode); |
michael@0 | 3164 | |
michael@0 | 3165 | if (match) { |
michael@0 | 3166 | this._entangleMatch(lineChunkNode, lineChunk); |
michael@0 | 3167 | lineChunkNode.addEventListener("click", aCallbacks.onMatchClick, false); |
michael@0 | 3168 | firstMatch = firstMatch || lineChunkNode; |
michael@0 | 3169 | } |
michael@0 | 3170 | if (lineLength >= GLOBAL_SEARCH_LINE_MAX_LENGTH) { |
michael@0 | 3171 | lineContentsNode.appendChild(this._ellipsis.cloneNode(true)); |
michael@0 | 3172 | break; |
michael@0 | 3173 | } |
michael@0 | 3174 | } |
michael@0 | 3175 | |
michael@0 | 3176 | this._entangleLine(lineContentsNode, firstMatch); |
michael@0 | 3177 | lineContentsNode.addEventListener("click", aCallbacks.onLineClick, false); |
michael@0 | 3178 | |
michael@0 | 3179 | let searchResult = document.createElement("hbox"); |
michael@0 | 3180 | searchResult.className = "dbg-search-result"; |
michael@0 | 3181 | searchResult.appendChild(lineNumberNode); |
michael@0 | 3182 | searchResult.appendChild(lineContentsNode); |
michael@0 | 3183 | |
michael@0 | 3184 | aElementNode.appendChild(searchResult); |
michael@0 | 3185 | }, |
michael@0 | 3186 | |
michael@0 | 3187 | /** |
michael@0 | 3188 | * Handles a match while creating the view. |
michael@0 | 3189 | * @param nsIDOMNode aNode |
michael@0 | 3190 | * @param object aMatchChunk |
michael@0 | 3191 | */ |
michael@0 | 3192 | _entangleMatch: function(aNode, aMatchChunk) { |
michael@0 | 3193 | LineResults._itemsByElement.set(aNode, { |
michael@0 | 3194 | instance: this, |
michael@0 | 3195 | lineData: aMatchChunk |
michael@0 | 3196 | }); |
michael@0 | 3197 | }, |
michael@0 | 3198 | |
michael@0 | 3199 | /** |
michael@0 | 3200 | * Handles a line while creating the view. |
michael@0 | 3201 | * @param nsIDOMNode aNode |
michael@0 | 3202 | * @param nsIDOMNode aFirstMatch |
michael@0 | 3203 | */ |
michael@0 | 3204 | _entangleLine: function(aNode, aFirstMatch) { |
michael@0 | 3205 | LineResults._itemsByElement.set(aNode, { |
michael@0 | 3206 | instance: this, |
michael@0 | 3207 | firstMatch: aFirstMatch, |
michael@0 | 3208 | ignored: true |
michael@0 | 3209 | }); |
michael@0 | 3210 | }, |
michael@0 | 3211 | |
michael@0 | 3212 | /** |
michael@0 | 3213 | * An nsIDOMNode label with an ellipsis value. |
michael@0 | 3214 | */ |
michael@0 | 3215 | _ellipsis: (function() { |
michael@0 | 3216 | let label = document.createElement("label"); |
michael@0 | 3217 | label.className = "plain dbg-results-line-contents-string"; |
michael@0 | 3218 | label.setAttribute("value", L10N.ellipsis); |
michael@0 | 3219 | return label; |
michael@0 | 3220 | })(), |
michael@0 | 3221 | |
michael@0 | 3222 | line: 0, |
michael@0 | 3223 | _sourceResults: null, |
michael@0 | 3224 | _store: null, |
michael@0 | 3225 | _target: null |
michael@0 | 3226 | }; |
michael@0 | 3227 | |
michael@0 | 3228 | /** |
michael@0 | 3229 | * A generator-iterator over the global, source or line results. |
michael@0 | 3230 | */ |
michael@0 | 3231 | GlobalResults.prototype["@@iterator"] = |
michael@0 | 3232 | SourceResults.prototype["@@iterator"] = |
michael@0 | 3233 | LineResults.prototype["@@iterator"] = function*() { |
michael@0 | 3234 | yield* this._store; |
michael@0 | 3235 | }; |
michael@0 | 3236 | |
michael@0 | 3237 | /** |
michael@0 | 3238 | * Gets the item associated with the specified element. |
michael@0 | 3239 | * |
michael@0 | 3240 | * @param nsIDOMNode aElement |
michael@0 | 3241 | * The element used to identify the item. |
michael@0 | 3242 | * @return object |
michael@0 | 3243 | * The matched item, or null if nothing is found. |
michael@0 | 3244 | */ |
michael@0 | 3245 | SourceResults.getItemForElement = |
michael@0 | 3246 | LineResults.getItemForElement = function(aElement) { |
michael@0 | 3247 | return WidgetMethods.getItemForElement.call(this, aElement, { noSiblings: true }); |
michael@0 | 3248 | }; |
michael@0 | 3249 | |
michael@0 | 3250 | /** |
michael@0 | 3251 | * Gets the element associated with a particular item at a specified index. |
michael@0 | 3252 | * |
michael@0 | 3253 | * @param number aIndex |
michael@0 | 3254 | * The index used to identify the item. |
michael@0 | 3255 | * @return nsIDOMNode |
michael@0 | 3256 | * The matched element, or null if nothing is found. |
michael@0 | 3257 | */ |
michael@0 | 3258 | SourceResults.getElementAtIndex = |
michael@0 | 3259 | LineResults.getElementAtIndex = function(aIndex) { |
michael@0 | 3260 | for (let [element, item] of this._itemsByElement) { |
michael@0 | 3261 | if (!item.ignored && !aIndex--) { |
michael@0 | 3262 | return element; |
michael@0 | 3263 | } |
michael@0 | 3264 | } |
michael@0 | 3265 | return null; |
michael@0 | 3266 | }; |
michael@0 | 3267 | |
michael@0 | 3268 | /** |
michael@0 | 3269 | * Gets the index of an item associated with the specified element. |
michael@0 | 3270 | * |
michael@0 | 3271 | * @param nsIDOMNode aElement |
michael@0 | 3272 | * The element to get the index for. |
michael@0 | 3273 | * @return number |
michael@0 | 3274 | * The index of the matched element, or -1 if nothing is found. |
michael@0 | 3275 | */ |
michael@0 | 3276 | SourceResults.indexOfElement = |
michael@0 | 3277 | LineResults.indexOfElement = function(aElement) { |
michael@0 | 3278 | let count = 0; |
michael@0 | 3279 | for (let [element, item] of this._itemsByElement) { |
michael@0 | 3280 | if (element == aElement) { |
michael@0 | 3281 | return count; |
michael@0 | 3282 | } |
michael@0 | 3283 | if (!item.ignored) { |
michael@0 | 3284 | count++; |
michael@0 | 3285 | } |
michael@0 | 3286 | } |
michael@0 | 3287 | return -1; |
michael@0 | 3288 | }; |
michael@0 | 3289 | |
michael@0 | 3290 | /** |
michael@0 | 3291 | * Gets the number of cached items associated with a specified element. |
michael@0 | 3292 | * |
michael@0 | 3293 | * @return number |
michael@0 | 3294 | * The number of key/value pairs in the corresponding map. |
michael@0 | 3295 | */ |
michael@0 | 3296 | SourceResults.size = |
michael@0 | 3297 | LineResults.size = function() { |
michael@0 | 3298 | let count = 0; |
michael@0 | 3299 | for (let [, item] of this._itemsByElement) { |
michael@0 | 3300 | if (!item.ignored) { |
michael@0 | 3301 | count++; |
michael@0 | 3302 | } |
michael@0 | 3303 | } |
michael@0 | 3304 | return count; |
michael@0 | 3305 | }; |
michael@0 | 3306 | |
michael@0 | 3307 | /** |
michael@0 | 3308 | * Preliminary setup for the DebuggerView object. |
michael@0 | 3309 | */ |
michael@0 | 3310 | DebuggerView.Sources = new SourcesView(); |
michael@0 | 3311 | DebuggerView.VariableBubble = new VariableBubbleView(); |
michael@0 | 3312 | DebuggerView.Tracer = new TracerView(); |
michael@0 | 3313 | DebuggerView.WatchExpressions = new WatchExpressionsView(); |
michael@0 | 3314 | DebuggerView.EventListeners = new EventListenersView(); |
michael@0 | 3315 | DebuggerView.GlobalSearch = new GlobalSearchView(); |