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.

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

mercurial