browser/devtools/debugger/debugger-panes.js

Wed, 31 Dec 2014 06:09:35 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:09:35 +0100
changeset 0
6474c204b198
permissions
-rw-r--r--

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();

mercurial